MediaWiki  master
UserGroupManager.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\User;
22 
25 use DeferredUpdates;
26 use IDBAccessObject;
27 use InvalidArgumentException;
28 use JobQueueGroup;
29 use ManualLogEntry;
34 use Psr\Log\LoggerInterface;
35 use ReadOnlyMode;
36 use Sanitizer;
37 use User;
40 use WikiMap;
41 use Wikimedia\Assert\Assert;
42 use Wikimedia\IPUtils;
46 
52 
53  public const CONSTRUCTOR_OPTIONS = [
54  'Autopromote',
55  'AutopromoteOnce',
56  'AutopromoteOnceLogInRC',
57  'EmailAuthentication',
58  'ImplicitGroups',
59  'GroupPermissions',
60  'RevokePermissions',
61  ];
62 
64  private $options;
65 
68 
70  private $loadBalancer;
71 
73  private $hookContainer;
74 
76  private $hookRunner;
77 
79  private $readOnlyMode;
80 
83 
85  private $logger;
86 
89 
91  private $dbDomain;
92 
94  private const CACHE_IMPLICIT = 'implicit';
95 
97  private const CACHE_EFFECTIVE = 'effective';
98 
100  private const CACHE_MEMBERSHIP = 'membership';
101 
103  private const CACHE_FORMER = 'former';
104 
116  private $userGroupCache = [];
117 
130 
141  public function __construct(
143  ConfiguredReadOnlyMode $configuredReadOnlyMode,
147  LoggerInterface $logger,
148  array $clearCacheCallbacks = [],
149  $dbDomain = false
150  ) {
151  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
152  $this->options = $options;
153  $this->loadBalancerFactory = $loadBalancerFactory;
154  $this->loadBalancer = $loadBalancerFactory->getMainLB( $dbDomain );
155  $this->hookContainer = $hookContainer;
156  $this->hookRunner = new HookRunner( $hookContainer );
157  $this->userEditTracker = $userEditTracker;
158  $this->logger = $logger;
159  // Can't just inject ROM since we LB can be for foreign wiki
160  $this->readOnlyMode = new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
161  $this->clearCacheCallbacks = $clearCacheCallbacks;
162  $this->dbDomain = $dbDomain;
163  }
164 
171  public function listAllGroups() : array {
172  return array_values( array_diff(
173  array_merge(
174  array_keys( $this->options->get( 'GroupPermissions' ) ),
175  array_keys( $this->options->get( 'RevokePermissions' ) )
176  ),
177  $this->listAllImplicitGroups()
178  ) );
179  }
180 
185  public function listAllImplicitGroups() : array {
186  return $this->options->get( 'ImplicitGroups' );
187  }
188 
198  public function newGroupMembershipFromRow( \stdClass $row ) : UserGroupMembership {
199  return new UserGroupMembership(
200  (int)$row->ug_user,
201  $row->ug_group,
202  $row->ug_expiry === null ? null : wfTimestamp(
203  TS_MW,
204  $row->ug_expiry
205  )
206  );
207  }
208 
217  UserIdentity $user,
218  array $userGroups,
219  int $queryFlags = self::READ_NORMAL
220  ) {
221  $membershipGroups = [];
222  reset( $userGroups );
223  foreach ( $userGroups as $row ) {
224  $ugm = $this->newGroupMembershipFromRow( $row );
225  $membershipGroups[ $ugm->getGroup() ] = $ugm;
226  }
227  $this->setCache( $user, self::CACHE_MEMBERSHIP, $membershipGroups, $queryFlags );
228  }
229 
241  public function getUserImplicitGroups(
242  UserIdentity $user,
243  int $queryFlags = self::READ_NORMAL,
244  bool $recache = false
245  ) : array {
246  $userKey = $this->getCacheKey( $user );
247  if ( $recache ||
248  !isset( $this->userGroupCache[$userKey][self::CACHE_IMPLICIT] ) ||
249  !$this->canUseCachedValues( $user, self::CACHE_IMPLICIT, $queryFlags )
250  ) {
251  $groups = [ '*' ];
252  if ( $user->isRegistered() ) {
253  $groups[] = 'user';
254 
255  $groups = array_unique( array_merge(
256  $groups,
257  $this->getUserAutopromoteGroups( $user )
258  ) );
259  }
260  $this->setCache( $user, self::CACHE_IMPLICIT, $groups, $queryFlags );
261  if ( $recache ) {
262  // Assure data consistency with rights/groups,
263  // as getEffectiveGroups() depends on this function
264  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
265  }
266  }
267  return $this->userGroupCache[$userKey][self::CACHE_IMPLICIT];
268  }
269 
281  public function getUserEffectiveGroups(
282  UserIdentity $user,
283  int $queryFlags = self::READ_NORMAL,
284  bool $recache = false
285  ) : array {
286  $userKey = $this->getCacheKey( $user );
287  // Ignore cache if the $recache flag is set, cached values can not be used
288  // or the cache value is missing
289  if ( $recache ||
290  !$this->canUseCachedValues( $user, self::CACHE_EFFECTIVE, $queryFlags ) ||
291  !isset( $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE] )
292  ) {
293  $groups = array_unique( array_merge(
294  $this->getUserGroups( $user, $queryFlags ), // explicit groups
295  $this->getUserImplicitGroups( $user, $queryFlags, $recache ) // implicit groups
296  ) );
297  // TODO: Deprecate passing out user object in the hook by introducing
298  // an alternative hook
299  if ( $this->hookContainer->isRegistered( 'UserEffectiveGroups' ) ) {
300  $userObj = User::newFromIdentity( $user );
301  $userObj->load();
302  // Hook for additional groups
303  $this->hookRunner->onUserEffectiveGroups( $userObj, $groups );
304  }
305  // Force reindexation of groups when a hook has unset one of them
306  $effectiveGroups = array_values( array_unique( $groups ) );
307  $this->setCache( $user, self::CACHE_EFFECTIVE, $effectiveGroups, $queryFlags );
308  }
309  return $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE];
310  }
311 
323  public function getUserFormerGroups(
324  UserIdentity $user,
325  int $queryFlags = self::READ_NORMAL
326  ) : array {
327  $userKey = $this->getCacheKey( $user );
328 
329  if ( $this->canUseCachedValues( $user, self::CACHE_FORMER, $queryFlags ) &&
330  isset( $this->userGroupCache[$userKey][self::CACHE_FORMER] )
331  ) {
332  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
333  }
334 
335  if ( !$user->isRegistered() ) {
336  // Anon users don't have groups stored in the database
337  return [];
338  }
339 
340  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
341  $res = $db->select(
342  'user_former_groups',
343  [ 'ufg_group' ],
344  [ 'ufg_user' => $user->getId() ],
345  __METHOD__
346  );
347  $formerGroups = [];
348  foreach ( $res as $row ) {
349  $formerGroups[] = $row->ufg_group;
350  }
351  $this->setCache( $user, self::CACHE_FORMER, $formerGroups, $queryFlags );
352 
353  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
354  }
355 
364  public function getUserAutopromoteGroups( UserIdentity $user ) : array {
365  $promote = [];
366  // TODO: remove the need for the full user object
367  $userObj = User::newFromIdentity( $user );
368  foreach ( $this->options->get( 'Autopromote' ) as $group => $cond ) {
369  if ( $this->recCheckCondition( $cond, $userObj ) ) {
370  $promote[] = $group;
371  }
372  }
373 
374  $this->hookRunner->onGetAutoPromoteGroups( $userObj, $promote );
375  return $promote;
376  }
377 
391  UserIdentity $user,
392  string $event
393  ) : array {
394  $autopromoteOnce = $this->options->get( 'AutopromoteOnce' );
395  $promote = [];
396 
397  if ( isset( $autopromoteOnce[$event] ) && count( $autopromoteOnce[$event] ) ) {
398  $currentGroups = $this->getUserGroups( $user );
399  $formerGroups = $this->getUserFormerGroups( $user );
400  // TODO: remove the need for the full user object
401  $userObj = User::newFromIdentity( $user );
402  foreach ( $autopromoteOnce[$event] as $group => $cond ) {
403  // Do not check if the user's already a member
404  if ( in_array( $group, $currentGroups ) ) {
405  continue;
406  }
407  // Do not autopromote if the user has belonged to the group
408  if ( in_array( $group, $formerGroups ) ) {
409  continue;
410  }
411  // Finally - check the conditions
412  if ( $this->recCheckCondition( $cond, $userObj ) ) {
413  $promote[] = $group;
414  }
415  }
416  }
417 
418  return $promote;
419  }
420 
437  private function recCheckCondition( $cond, User $user ) : bool {
438  $validOps = [ '&', '|', '^', '!' ];
439 
440  if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
441  // Recursive condition
442  if ( $cond[0] == '&' ) { // AND (all conds pass)
443  foreach ( array_slice( $cond, 1 ) as $subcond ) {
444  if ( !$this->recCheckCondition( $subcond, $user ) ) {
445  return false;
446  }
447  }
448 
449  return true;
450  } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes)
451  foreach ( array_slice( $cond, 1 ) as $subcond ) {
452  if ( $this->recCheckCondition( $subcond, $user ) ) {
453  return true;
454  }
455  }
456 
457  return false;
458  } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes)
459  if ( count( $cond ) > 3 ) {
460  $this->logger->warning(
461  'recCheckCondition() given XOR ("^") condition on three or more conditions.' .
462  ' Check your $wgAutopromote and $wgAutopromoteOnce settings.'
463  );
464  }
465  return $this->recCheckCondition( $cond[1], $user )
466  xor $this->recCheckCondition( $cond[2], $user );
467  } elseif ( $cond[0] == '!' ) { // NOT (no conds pass)
468  foreach ( array_slice( $cond, 1 ) as $subcond ) {
469  if ( $this->recCheckCondition( $subcond, $user ) ) {
470  return false;
471  }
472  }
473 
474  return true;
475  }
476  }
477  // If we got here, the array presumably does not contain other conditions;
478  // it's not recursive. Pass it off to checkCondition.
479  if ( !is_array( $cond ) ) {
480  $cond = [ $cond ];
481  }
482 
483  return $this->checkCondition( $cond, $user );
484  }
485 
496  private function checkCondition( array $cond, User $user ) : bool {
497  if ( count( $cond ) < 1 ) {
498  return false;
499  }
500 
501  switch ( $cond[0] ) {
503  if ( Sanitizer::validateEmail( $user->getEmail() ) ) {
504  if ( $this->options->get( 'EmailAuthentication' ) ) {
505  return (bool)$user->getEmailAuthenticationTimestamp();
506  } else {
507  return true;
508  }
509  }
510  return false;
511  case APCOND_EDITCOUNT:
512  $reqEditCount = $cond[1];
513 
514  // T157718: Avoid edit count lookup if specified edit count is 0 or invalid
515  if ( $reqEditCount <= 0 ) {
516  return true;
517  }
518  return $user->isRegistered() && $this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount;
519  case APCOND_AGE:
520  $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
521  return $age >= $cond[1];
523  $age = time() - (int)wfTimestampOrNull(
524  TS_UNIX, $this->userEditTracker->getFirstEditTimestamp( $user ) );
525  return $age >= $cond[1];
526  case APCOND_INGROUPS:
527  $groups = array_slice( $cond, 1 );
528  return count( array_intersect( $groups, $this->getUserGroups( $user ) ) ) == count( $groups );
529  case APCOND_ISIP:
530  return $cond[1] == $user->getRequest()->getIP();
531  case APCOND_IPINRANGE:
532  return IPUtils::isInRange( $user->getRequest()->getIP(), $cond[1] );
533  case APCOND_BLOCKED:
534  return $user->getBlock() && $user->getBlock()->isSitewide();
535  case APCOND_ISBOT:
536  // TODO: Injecting permission manager will cause a cyclic dependency. T254537
537  return in_array( 'bot', MediaWikiServices::getInstance()
539  ->getGroupPermissions( $this->getUserGroups( $user ) ) );
540  default:
541  $result = null;
542  $this->hookRunner->onAutopromoteCondition( $cond[0],
543  array_slice( $cond, 1 ), $user, $result );
544  if ( $result === null ) {
545  throw new InvalidArgumentException(
546  "Unrecognized condition {$cond[0]} for autopromotion!"
547  );
548  }
549 
550  return (bool)$result;
551  }
552  }
553 
570  UserIdentity $user,
571  string $event
572  ) : array {
573  Assert::precondition(
574  !$this->dbDomain || WikiMap::isCurrentWikiDbDomain( $this->dbDomain ),
575  __METHOD__ . " is not supported for foreign domains: {$this->dbDomain} used"
576  );
577 
578  if ( $this->readOnlyMode->isReadOnly() || !$user->getId() ) {
579  return [];
580  }
581 
582  $toPromote = $this->getUserAutopromoteOnceGroups( $user, $event );
583  if ( $toPromote === [] ) {
584  return [];
585  }
586 
587  $userObj = User::newFromIdentity( $user );
588  if ( !$userObj->checkAndSetTouched() ) {
589  return []; // raced out (bug T48834)
590  }
591 
592  $oldGroups = $this->getUserGroups( $user ); // previous groups
593  $oldUGMs = $this->getUserGroupMemberships( $user );
594  foreach ( $toPromote as $group ) {
595  $this->addUserToGroup( $user, $group );
596  }
597  $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
598  $newUGMs = $this->getUserGroupMemberships( $user );
599 
600  // update groups in external authentication database
601  // TODO: deprecate passing full User object to hook
602  $this->hookRunner->onUserGroupsChanged(
603  $userObj,
604  $toPromote, [],
605  false,
606  false,
607  $oldUGMs,
608  $newUGMs
609  );
610 
611  $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
612  $logEntry->setPerformer( $user );
613  $logEntry->setTarget( $userObj->getUserPage() );
614  $logEntry->setParameters( [
615  '4::oldgroups' => $oldGroups,
616  '5::newgroups' => $newGroups,
617  ] );
618  $logid = $logEntry->insert();
619  if ( $this->options->get( 'AutopromoteOnceLogInRC' ) ) {
620  $logEntry->publish( $logid );
621  }
622 
623  return $toPromote;
624  }
625 
634  public function getUserGroups(
635  UserIdentity $user,
636  int $queryFlags = self::READ_NORMAL
637  ) : array {
638  return array_keys( $this->getUserGroupMemberships( $user, $queryFlags ) );
639  }
640 
649  public function getUserGroupMemberships(
650  UserIdentity $user,
651  int $queryFlags = self::READ_NORMAL
652  ) : array {
653  $userKey = $this->getCacheKey( $user );
654 
655  if ( $this->canUseCachedValues( $user, self::CACHE_MEMBERSHIP, $queryFlags ) &&
656  isset( $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP] )
657  ) {
659  return $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP];
660  }
661 
662  if ( !$user->isRegistered() ) {
663  // Anon users don't have groups stored in the database
664  return [];
665  }
666 
667  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
668  $queryInfo = $this->getQueryInfo();
669  $res = $db->select(
670  $queryInfo['tables'],
671  $queryInfo['fields'],
672  [ 'ug_user' => $user->getId() ],
673  __METHOD__,
674  [],
675  $queryInfo['joins']
676  );
677 
678  $ugms = [];
679  foreach ( $res as $row ) {
680  $ugm = $this->newGroupMembershipFromRow( $row );
681  if ( !$ugm->isExpired() ) {
682  $ugms[$ugm->getGroup()] = $ugm;
683  }
684  }
685  ksort( $ugms );
686 
687  $this->setCache( $user, self::CACHE_MEMBERSHIP, $ugms, $queryFlags );
688 
689  return $ugms;
690  }
691 
707  public function addUserToGroup(
708  UserIdentity $user,
709  string $group,
710  string $expiry = null,
711  bool $allowUpdate = false
712  ) : bool {
713  if ( $this->readOnlyMode->isReadOnly() ) {
714  return false;
715  }
716 
717  if ( !$user->isRegistered() ) {
718  throw new InvalidArgumentException(
719  'UserGroupManager::addUserToGroup() needs a positive user ID. ' .
720  'Perhaps addGroup() was called before the user was added to the database.'
721  );
722  }
723 
724  if ( $expiry ) {
725  $expiry = wfTimestamp( TS_MW, $expiry );
726  }
727 
728  // TODO: Deprecate passing out user object in the hook by introducing
729  // an alternative hook
730  if ( $this->hookContainer->isRegistered( 'UserAddGroup' ) ) {
731  $userObj = User::newFromIdentity( $user );
732  $userObj->load();
733  if ( !$this->hookRunner->onUserAddGroup( $userObj, $group, $expiry ) ) {
734  return false;
735  }
736  }
737 
738  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
739  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER, [], $this->dbDomain );
740 
741  $dbw->startAtomic( __METHOD__ );
742  $dbw->insert(
743  'user_groups',
744  [
745  'ug_user' => $user->getId(),
746  'ug_group' => $group,
747  'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null,
748  ],
749  __METHOD__,
750  [ 'IGNORE' ]
751  );
752 
753  $affected = $dbw->affectedRows();
754  if ( !$affected ) {
755  // Conflicting row already exists; it should be overridden if it is either expired
756  // or if $allowUpdate is true and the current row is different than the loaded row.
757  $conds = [
758  'ug_user' => $user->getId(),
759  'ug_group' => $group
760  ];
761  if ( $allowUpdate ) {
762  // Update the current row if its expiry does not match that of the loaded row
763  $conds[] = $expiry
764  ? "ug_expiry IS NULL OR ug_expiry != {$dbw->addQuotes( $dbw->timestamp( $expiry ) )}"
765  : 'ug_expiry IS NOT NULL';
766  } else {
767  // Update the current row if it is expired
768  $conds[] = "ug_expiry < {$dbw->addQuotes( $dbw->timestamp() )}";
769  }
770  $dbw->update(
771  'user_groups',
772  [ 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null ],
773  $conds,
774  __METHOD__
775  );
776  $affected = $dbw->affectedRows();
777  }
778  $dbw->endAtomic( __METHOD__ );
779 
780  // Purge old, expired memberships from the DB
781  $fname = __METHOD__;
782  DeferredUpdates::addCallableUpdate( function () use ( $fname ) {
783  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
784  $hasExpiredRow = $dbr->selectField(
785  'user_groups',
786  '1',
787  [ "ug_expiry < {$dbr->addQuotes( $dbr->timestamp() )}" ],
788  $fname
789  );
790  if ( $hasExpiredRow ) {
791  JobQueueGroup::singleton( $this->dbDomain )->push( new UserGroupExpiryJob( [] ) );
792  }
793  } );
794 
795  if ( $affected > 0 ) {
796  $oldUgms[$group] = new UserGroupMembership( $user->getId(), $group, $expiry );
797  if ( !$oldUgms[$group]->isExpired() ) {
798  $this->setCache( $user, self::CACHE_MEMBERSHIP,
799  $oldUgms, self::READ_LATEST );
800  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
801  }
802  foreach ( $this->clearCacheCallbacks as $callback ) {
803  $callback( $user );
804  }
805  return true;
806  }
807  return false;
808  }
809 
818  public function removeUserFromGroup( UserIdentity $user, string $group ) : bool {
819  // TODO: Deprecate passing out user object in the hook by introducing
820  // an alternative hook
821  if ( $this->hookContainer->isRegistered( 'UserRemoveGroup' ) ) {
822  $userObj = User::newFromIdentity( $user );
823  $userObj->load();
824  if ( !$this->hookRunner->onUserRemoveGroup( $userObj, $group ) ) {
825  return false;
826  }
827  }
828 
829  if ( $this->readOnlyMode->isReadOnly() ) {
830  return false;
831  }
832 
833  if ( !$user->isRegistered() ) {
834  throw new InvalidArgumentException(
835  'UserGroupManager::removeUserFromGroup() needs a positive user ID. ' .
836  'Perhaps removeUserFromGroup() was called before the user was added to the database.'
837  );
838  }
839 
840  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
841  $oldFormerGroups = $this->getUserFormerGroups( $user, self::READ_LATEST );
842  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER, [], $this->dbDomain );
843  $dbw->delete(
844  'user_groups',
845  [ 'ug_user' => $user->getId(), 'ug_group' => $group ],
846  __METHOD__
847  );
848 
849  if ( !$dbw->affectedRows() ) {
850  return false;
851  }
852  // Remember that the user was in this group
853  $dbw->insert(
854  'user_former_groups',
855  [ 'ufg_user' => $user->getId(), 'ufg_group' => $group ],
856  __METHOD__,
857  [ 'IGNORE' ]
858  );
859 
860  unset( $oldUgms[$group] );
861  $this->setCache( $user, self::CACHE_MEMBERSHIP, $oldUgms, self::READ_LATEST );
862  $oldFormerGroups[] = $group;
863  $this->setCache( $user, self::CACHE_FORMER, $oldFormerGroups, self::READ_LATEST );
864  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
865  foreach ( $this->clearCacheCallbacks as $callback ) {
866  $callback( $user );
867  }
868  return true;
869  }
870 
882  public function getQueryInfo() : array {
883  return [
884  'tables' => [ 'user_groups' ],
885  'fields' => [
886  'ug_user',
887  'ug_group',
888  'ug_expiry',
889  ],
890  'joins' => []
891  ];
892  }
893 
901  public function purgeExpired() {
902  if ( $this->readOnlyMode->isReadOnly() ) {
903  return false;
904  }
905 
906  $ticket = $this->loadBalancerFactory->getEmptyTransactionTicket( __METHOD__ );
907  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
908 
909  $lockKey = "{$dbw->getDomainID()}:UserGroupManager:purge"; // per-wiki
910  $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
911  if ( !$scopedLock ) {
912  return false; // already running
913  }
914 
915  $now = time();
916  $purgedRows = 0;
917  $queryInfo = $this->getQueryInfo();
918  do {
919  $dbw->startAtomic( __METHOD__ );
920 
921  $res = $dbw->select(
922  $queryInfo['tables'],
923  $queryInfo['fields'],
924  [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
925  __METHOD__,
926  [ 'FOR UPDATE', 'LIMIT' => 100 ],
927  $queryInfo['joins']
928  );
929 
930  if ( $res->numRows() > 0 ) {
931  $insertData = []; // array of users/groups to insert to user_former_groups
932  $deleteCond = []; // array for deleting the rows that are to be moved around
933  foreach ( $res as $row ) {
934  $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
935  $deleteCond[] = $dbw->makeList(
936  [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
938  );
939  }
940  // Delete the rows we're about to move
941  $dbw->delete(
942  'user_groups',
943  $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
944  __METHOD__
945  );
946  // Push the groups to user_former_groups
947  $dbw->insert(
948  'user_former_groups',
949  $insertData,
950  __METHOD__,
951  [ 'IGNORE' ]
952  );
953  // Count how many rows were purged
954  $purgedRows += $res->numRows();
955  }
956 
957  $dbw->endAtomic( __METHOD__ );
958 
959  $this->loadBalancerFactory->commitAndWaitForReplication( __METHOD__, $ticket );
960  } while ( $res->numRows() > 0 );
961  return $purgedRows;
962  }
963 
969  public function clearCache( UserIdentity $user ) {
970  $userKey = $this->getCacheKey( $user );
971  unset( $this->userGroupCache[$userKey] );
972  unset( $this->queryFlagsUsedForCaching[$userKey] );
973  }
974 
983  private function setCache(
984  UserIdentity $user,
985  string $cacheKind,
986  array $groupValue,
987  int $queryFlags
988  ) {
989  $userKey = $this->getCacheKey( $user );
990  $this->userGroupCache[$userKey][$cacheKind] = $groupValue;
991  $this->queryFlagsUsedForCaching[$userKey][$cacheKind] = $queryFlags;
992  }
993 
1000  private function clearUserCacheForKind( UserIdentity $user, string $cacheKind ) {
1001  $userKey = $this->getCacheKey( $user );
1002  unset( $this->userGroupCache[$userKey][$cacheKind] );
1003  unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] );
1004  }
1005 
1010  private function getDBConnectionRefForQueryFlags( int $queryFlags ) : DBConnRef {
1011  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1012  return $this->loadBalancer->getConnectionRef( $mode, [], $this->dbDomain );
1013  }
1014 
1020  private function getCacheKey( UserIdentity $user ) : string {
1021  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1022  }
1023 
1031  private function canUseCachedValues(
1032  UserIdentity $user,
1033  string $cacheKind,
1034  int $queryFlags
1035  ) : bool {
1036  if ( !$user->isRegistered() ) {
1037  // Anon users don't have groups stored in the database,
1038  // so $queryFlags are ignored.
1039  return true;
1040  }
1041  if ( $queryFlags >= self::READ_LOCKING ) {
1042  return false;
1043  }
1044  $userKey = $this->getCacheKey( $user );
1045  $queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ?? self::READ_NONE;
1046  return $queryFlagsUsed >= $queryFlags;
1047  }
1048 }
WikiMap\isCurrentWikiDbDomain
static isCurrentWikiDbDomain( $domain)
Definition: WikiMap.php:312
MediaWiki\User\UserGroupManager\checkCondition
checkCondition(array $cond, User $user)
As recCheckCondition, but not recursive.
Definition: UserGroupManager.php:496
MediaWiki\User\UserGroupManager\clearCache
clearCache(UserIdentity $user)
Cleans cached group memberships for a given user.
Definition: UserGroupManager.php:969
MediaWiki\User\UserGroupManager\removeUserFromGroup
removeUserFromGroup(UserIdentity $user, string $group)
Remove the user from the given group.
Definition: UserGroupManager.php:818
MediaWiki\User\UserGroupManager\CACHE_FORMER
const CACHE_FORMER
string key for former groups cache
Definition: UserGroupManager.php:103
APCOND_ISBOT
const APCOND_ISBOT
Definition: Defines.php:202
MediaWiki\User\UserGroupManager\$clearCacheCallbacks
callable[] $clearCacheCallbacks
Definition: UserGroupManager.php:88
MediaWiki\User\UserGroupManager\getCacheKey
getCacheKey(UserIdentity $user)
Gets a unique key for various caches.
Definition: UserGroupManager.php:1020
User\isRegistered
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition: User.php:2906
MediaWiki\User\UserGroupManager\$userGroupCache
array $userGroupCache
Service caches, an assoc.
Definition: UserGroupManager.php:116
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
MediaWiki\User\UserGroupManager\$options
ServiceOptions $options
Definition: UserGroupManager.php:64
MediaWiki\User\UserGroupManager\getUserEffectiveGroups
getUserEffectiveGroups(UserIdentity $user, int $queryFlags=self::READ_NORMAL, bool $recache=false)
Get the list of implicit group memberships the user has.
Definition: UserGroupManager.php:281
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1808
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
MediaWiki\User\UserGroupManager\getUserFormerGroups
getUserFormerGroups(UserIdentity $user, int $queryFlags=self::READ_NORMAL)
Returns the groups the user has belonged to.
Definition: UserGroupManager.php:323
APCOND_EDITCOUNT
const APCOND_EDITCOUNT
Definition: Defines.php:194
ConfiguredReadOnlyMode
A read-only mode service which does not depend on LoadBalancer.
Definition: ConfiguredReadOnlyMode.php:9
Sanitizer\validateEmail
static validateEmail( $addr)
Does a string look like an e-mail address?
Definition: Sanitizer.php:1936
MediaWiki\User\UserGroupManager\$logger
LoggerInterface $logger
Definition: UserGroupManager.php:85
APCOND_AGE
const APCOND_AGE
Definition: Defines.php:195
MediaWiki\User\UserGroupManager\purgeExpired
purgeExpired()
Purge expired memberships from the user_groups table.
Definition: UserGroupManager.php:901
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:594
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
Wikimedia\Rdbms\ILBFactory\getMainLB
getMainLB( $domain=false)
Get a cached (tracked) load balancer object.
User\getEmailAuthenticationTimestamp
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition: User.php:2392
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
MediaWiki\User\UserGroupManager\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: UserGroupManager.php:53
MediaWiki\User\UserGroupManager\__construct
__construct(ServiceOptions $options, ConfiguredReadOnlyMode $configuredReadOnlyMode, ILBFactory $loadBalancerFactory, HookContainer $hookContainer, UserEditTracker $userEditTracker, LoggerInterface $logger, array $clearCacheCallbacks=[], $dbDomain=false)
Definition: UserGroupManager.php:141
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
LIST_AND
const LIST_AND
Definition: Defines.php:48
User\getRequest
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3040
MediaWiki\User\UserGroupManager\CACHE_IMPLICIT
const CACHE_IMPLICIT
string key for implicit groups cache
Definition: UserGroupManager.php:94
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:185
MediaWiki\User\UserGroupManager\getUserAutopromoteOnceGroups
getUserAutopromoteOnceGroups(UserIdentity $user, string $event)
Get the groups for the given user based on the given criteria.
Definition: UserGroupManager.php:390
MediaWiki\User\UserGroupManager\$dbDomain
string false $dbDomain
Definition: UserGroupManager.php:91
User\getEmail
getEmail()
Get the user's e-mail address.
Definition: User.php:2382
MediaWiki\User\UserGroupManager
Managers user groups.
Definition: UserGroupManager.php:51
MediaWiki\User\UserGroupManager\$userEditTracker
UserEditTracker $userEditTracker
Definition: UserGroupManager.php:82
MediaWiki\User\UserGroupManager\$hookRunner
HookRunner $hookRunner
Definition: UserGroupManager.php:76
LIST_OR
const LIST_OR
Definition: Defines.php:51
MediaWiki\User\UserGroupManager\addUserToAutopromoteOnceGroups
addUserToAutopromoteOnceGroups(UserIdentity $user, string $event)
Add the user to the group if he/she meets given criteria.
Definition: UserGroupManager.php:569
APCOND_AGE_FROM_EDIT
const APCOND_AGE_FROM_EDIT
Definition: Defines.php:200
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
MediaWiki\User\UserGroupManager\addUserToGroup
addUserToGroup(UserIdentity $user, string $group, string $expiry=null, bool $allowUpdate=false)
Add the user to the given group.
Definition: UserGroupManager.php:707
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
MediaWiki\User\UserGroupManager\$hookContainer
HookContainer $hookContainer
Definition: UserGroupManager.php:73
getPermissionManager
getPermissionManager()
MediaWiki\User\UserGroupManager\listAllImplicitGroups
listAllImplicitGroups()
Get a list of all configured implicit groups.
Definition: UserGroupManager.php:185
APCOND_ISIP
const APCOND_ISIP
Definition: Defines.php:198
MediaWiki\User\UserGroupManager\getUserGroupMemberships
getUserGroupMemberships(UserIdentity $user, int $queryFlags=self::READ_NORMAL)
Loads and returns UserGroupMembership objects for all the groups a user currently belongs to.
Definition: UserGroupManager.php:649
DeferredUpdates
Class for managing the deferred updates.
Definition: DeferredUpdates.php:62
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1824
APCOND_BLOCKED
const APCOND_BLOCKED
Definition: Defines.php:201
MediaWiki\User\UserGroupManager\canUseCachedValues
canUseCachedValues(UserIdentity $user, string $cacheKind, int $queryFlags)
Determines if it's ok to use cached options values for a given user and query flags.
Definition: UserGroupManager.php:1031
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
User\getBlock
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:1795
DB_MASTER
const DB_MASTER
Definition: defines.php:26
UserGroupExpiryJob
Definition: UserGroupExpiryJob.php:27
IDBAccessObject\READ_NONE
const READ_NONE
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:75
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
MediaWiki\User\UserGroupManager\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: UserGroupManager.php:79
MediaWiki\User\UserGroupManager\getUserImplicitGroups
getUserImplicitGroups(UserIdentity $user, int $queryFlags=self::READ_NORMAL, bool $recache=false)
Get the list of implicit group memberships this user has.
Definition: UserGroupManager.php:241
MediaWiki\User\UserGroupManager\newGroupMembershipFromRow
newGroupMembershipFromRow(\stdClass $row)
Creates a new UserGroupMembership instance from $row.
Definition: UserGroupManager.php:198
MediaWiki\User
Definition: DefaultOptionsLookup.php:21
MediaWiki\User\UserGroupManager\getUserAutopromoteGroups
getUserAutopromoteGroups(UserIdentity $user)
Get the groups for the given user based on $wgAutopromote.
Definition: UserGroupManager.php:364
APCOND_INGROUPS
const APCOND_INGROUPS
Definition: Defines.php:197
WikiMap
Helper tools for dealing with other locally-hosted wikis.
Definition: WikiMap.php:29
MediaWiki\User\UserGroupManager\getUserGroups
getUserGroups(UserIdentity $user, int $queryFlags=self::READ_NORMAL)
Get the list of explicit group memberships this user has.
Definition: UserGroupManager.php:634
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:29
User\getRegistration
getRegistration()
Get the timestamp of account creation.
Definition: User.php:3991
MediaWiki\User\UserIdentity\getId
getId()
MediaWiki\User\UserGroupManager\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags(int $queryFlags)
Definition: UserGroupManager.php:1010
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
MediaWiki\User\UserEditTracker
Track info about user edit counts and timings.
Definition: UserEditTracker.php:19
MediaWiki\User\UserGroupManager\recCheckCondition
recCheckCondition( $cond, User $user)
Recursively check a condition.
Definition: UserGroupManager.php:437
MediaWiki\User\UserGroupManager\$queryFlagsUsedForCaching
array $queryFlagsUsedForCaching
An assoc.
Definition: UserGroupManager.php:129
MediaWiki\User\UserGroupManager\CACHE_EFFECTIVE
const CACHE_EFFECTIVE
string key for effective groups cache
Definition: UserGroupManager.php:97
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:42
MediaWiki\User\UserGroupManager\clearUserCacheForKind
clearUserCacheForKind(UserIdentity $user, string $cacheKind)
Clears a cached group membership and query key for a given user.
Definition: UserGroupManager.php:1000
MediaWiki\User\UserGroupManager\CACHE_MEMBERSHIP
const CACHE_MEMBERSHIP
string key for group memberships cache
Definition: UserGroupManager.php:100
MediaWiki\User\UserGroupManager\$loadBalancerFactory
ILBFactory $loadBalancerFactory
Definition: UserGroupManager.php:67
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
MediaWiki\User\UserGroupManager\setCache
setCache(UserIdentity $user, string $cacheKind, array $groupValue, int $queryFlags)
Sets cached group memberships and query flags for a given user.
Definition: UserGroupManager.php:983
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:570
MediaWiki\User\UserGroupManager\listAllGroups
listAllGroups()
Return the set of defined explicit groups.
Definition: UserGroupManager.php:171
MediaWiki\User\UserGroupManager\loadGroupMembershipsFromArray
loadGroupMembershipsFromArray(UserIdentity $user, array $userGroups, int $queryFlags=self::READ_NORMAL)
Load the user groups cache from the provided user groups data.
Definition: UserGroupManager.php:216
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
MediaWiki\User\UserGroupManager\getQueryInfo
getQueryInfo()
Return the tables and fields to be selected to construct new UserGroupMembership object using newGrou...
Definition: UserGroupManager.php:882
Sanitizer
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:33
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
UserGroupMembership
Represents a "user group membership" – a specific instance of a user belonging to a group.
Definition: UserGroupMembership.php:37
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
Wikimedia\Rdbms\ILBFactory
An interface for generating database load balancers.
Definition: ILBFactory.php:33
JobQueueGroup
Class to handle enqueueing of background jobs.
Definition: JobQueueGroup.php:30
MediaWiki\User\UserGroupManager\$loadBalancer
ILoadBalancer $loadBalancer
Definition: UserGroupManager.php:70
APCOND_EMAILCONFIRMED
const APCOND_EMAILCONFIRMED
Definition: Defines.php:196
APCOND_IPINRANGE
const APCOND_IPINRANGE
Definition: Defines.php:199