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 
56  public const CONSTRUCTOR_OPTIONS = [
57  'Autopromote',
58  'AutopromoteOnce',
59  'AutopromoteOnceLogInRC',
60  'EmailAuthentication',
61  'ImplicitGroups',
62  'GroupPermissions',
63  'RevokePermissions',
64  ];
65 
67  private $options;
68 
71 
73  private $loadBalancer;
74 
76  private $hookContainer;
77 
79  private $hookRunner;
80 
82  private $readOnlyMode;
83 
86 
89 
91  private $logger;
92 
95 
97  private $dbDomain;
98 
100  private const CACHE_IMPLICIT = 'implicit';
101 
103  private const CACHE_EFFECTIVE = 'effective';
104 
106  private const CACHE_MEMBERSHIP = 'membership';
107 
109  private const CACHE_FORMER = 'former';
110 
122  private $userGroupCache = [];
123 
136 
148  public function __construct(
150  ConfiguredReadOnlyMode $configuredReadOnlyMode,
155  LoggerInterface $logger,
156  array $clearCacheCallbacks = [],
157  $dbDomain = false
158  ) {
159  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
160  $this->options = $options;
161  $this->loadBalancerFactory = $loadBalancerFactory;
162  $this->loadBalancer = $loadBalancerFactory->getMainLB( $dbDomain );
163  $this->hookContainer = $hookContainer;
164  $this->hookRunner = new HookRunner( $hookContainer );
165  $this->userEditTracker = $userEditTracker;
166  $this->groupPermissionsLookup = $groupPermissionsLookup;
167  $this->logger = $logger;
168  // Can't just inject ROM since we LB can be for foreign wiki
169  $this->readOnlyMode = new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
170  $this->clearCacheCallbacks = $clearCacheCallbacks;
171  $this->dbDomain = $dbDomain;
172  }
173 
180  public function listAllGroups() : array {
181  return array_values( array_diff(
182  array_merge(
183  array_keys( $this->options->get( 'GroupPermissions' ) ),
184  array_keys( $this->options->get( 'RevokePermissions' ) )
185  ),
186  $this->listAllImplicitGroups()
187  ) );
188  }
189 
194  public function listAllImplicitGroups() : array {
195  return $this->options->get( 'ImplicitGroups' );
196  }
197 
207  public function newGroupMembershipFromRow( \stdClass $row ) : UserGroupMembership {
208  return new UserGroupMembership(
209  (int)$row->ug_user,
210  $row->ug_group,
211  $row->ug_expiry === null ? null : wfTimestamp(
212  TS_MW,
213  $row->ug_expiry
214  )
215  );
216  }
217 
226  UserIdentity $user,
227  array $userGroups,
228  int $queryFlags = self::READ_NORMAL
229  ) {
230  $membershipGroups = [];
231  reset( $userGroups );
232  foreach ( $userGroups as $row ) {
233  $ugm = $this->newGroupMembershipFromRow( $row );
234  $membershipGroups[ $ugm->getGroup() ] = $ugm;
235  }
236  $this->setCache( $user, self::CACHE_MEMBERSHIP, $membershipGroups, $queryFlags );
237  }
238 
250  public function getUserImplicitGroups(
251  UserIdentity $user,
252  int $queryFlags = self::READ_NORMAL,
253  bool $recache = false
254  ) : array {
255  $userKey = $this->getCacheKey( $user );
256  if ( $recache ||
257  !isset( $this->userGroupCache[$userKey][self::CACHE_IMPLICIT] ) ||
258  !$this->canUseCachedValues( $user, self::CACHE_IMPLICIT, $queryFlags )
259  ) {
260  $groups = [ '*' ];
261  if ( $user->isRegistered() ) {
262  $groups[] = 'user';
263 
264  $groups = array_unique( array_merge(
265  $groups,
266  $this->getUserAutopromoteGroups( $user )
267  ) );
268  }
269  $this->setCache( $user, self::CACHE_IMPLICIT, $groups, $queryFlags );
270  if ( $recache ) {
271  // Assure data consistency with rights/groups,
272  // as getEffectiveGroups() depends on this function
273  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
274  }
275  }
276  return $this->userGroupCache[$userKey][self::CACHE_IMPLICIT];
277  }
278 
290  public function getUserEffectiveGroups(
291  UserIdentity $user,
292  int $queryFlags = self::READ_NORMAL,
293  bool $recache = false
294  ) : array {
295  $userKey = $this->getCacheKey( $user );
296  // Ignore cache if the $recache flag is set, cached values can not be used
297  // or the cache value is missing
298  if ( $recache ||
299  !$this->canUseCachedValues( $user, self::CACHE_EFFECTIVE, $queryFlags ) ||
300  !isset( $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE] )
301  ) {
302  $groups = array_unique( array_merge(
303  $this->getUserGroups( $user, $queryFlags ), // explicit groups
304  $this->getUserImplicitGroups( $user, $queryFlags, $recache ) // implicit groups
305  ) );
306  // TODO: Deprecate passing out user object in the hook by introducing
307  // an alternative hook
308  if ( $this->hookContainer->isRegistered( 'UserEffectiveGroups' ) ) {
309  $userObj = User::newFromIdentity( $user );
310  $userObj->load();
311  // Hook for additional groups
312  $this->hookRunner->onUserEffectiveGroups( $userObj, $groups );
313  }
314  // Force reindexation of groups when a hook has unset one of them
315  $effectiveGroups = array_values( array_unique( $groups ) );
316  $this->setCache( $user, self::CACHE_EFFECTIVE, $effectiveGroups, $queryFlags );
317  }
318  return $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE];
319  }
320 
332  public function getUserFormerGroups(
333  UserIdentity $user,
334  int $queryFlags = self::READ_NORMAL
335  ) : array {
336  $userKey = $this->getCacheKey( $user );
337 
338  if ( $this->canUseCachedValues( $user, self::CACHE_FORMER, $queryFlags ) &&
339  isset( $this->userGroupCache[$userKey][self::CACHE_FORMER] )
340  ) {
341  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
342  }
343 
344  if ( !$user->isRegistered() ) {
345  // Anon users don't have groups stored in the database
346  return [];
347  }
348 
349  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
350  $res = $db->select(
351  'user_former_groups',
352  [ 'ufg_group' ],
353  [ 'ufg_user' => $user->getId() ],
354  __METHOD__
355  );
356  $formerGroups = [];
357  foreach ( $res as $row ) {
358  $formerGroups[] = $row->ufg_group;
359  }
360  $this->setCache( $user, self::CACHE_FORMER, $formerGroups, $queryFlags );
361 
362  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
363  }
364 
373  public function getUserAutopromoteGroups( UserIdentity $user ) : array {
374  $promote = [];
375  // TODO: remove the need for the full user object
376  $userObj = User::newFromIdentity( $user );
377  foreach ( $this->options->get( 'Autopromote' ) as $group => $cond ) {
378  if ( $this->recCheckCondition( $cond, $userObj ) ) {
379  $promote[] = $group;
380  }
381  }
382 
383  $this->hookRunner->onGetAutoPromoteGroups( $userObj, $promote );
384  return $promote;
385  }
386 
400  UserIdentity $user,
401  string $event
402  ) : array {
403  $autopromoteOnce = $this->options->get( 'AutopromoteOnce' );
404  $promote = [];
405 
406  if ( isset( $autopromoteOnce[$event] ) && count( $autopromoteOnce[$event] ) ) {
407  $currentGroups = $this->getUserGroups( $user );
408  $formerGroups = $this->getUserFormerGroups( $user );
409  // TODO: remove the need for the full user object
410  $userObj = User::newFromIdentity( $user );
411  foreach ( $autopromoteOnce[$event] as $group => $cond ) {
412  // Do not check if the user's already a member
413  if ( in_array( $group, $currentGroups ) ) {
414  continue;
415  }
416  // Do not autopromote if the user has belonged to the group
417  if ( in_array( $group, $formerGroups ) ) {
418  continue;
419  }
420  // Finally - check the conditions
421  if ( $this->recCheckCondition( $cond, $userObj ) ) {
422  $promote[] = $group;
423  }
424  }
425  }
426 
427  return $promote;
428  }
429 
446  private function recCheckCondition( $cond, User $user ) : bool {
447  $validOps = [ '&', '|', '^', '!' ];
448 
449  if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
450  // Recursive condition
451  if ( $cond[0] == '&' ) { // AND (all conds pass)
452  foreach ( array_slice( $cond, 1 ) as $subcond ) {
453  if ( !$this->recCheckCondition( $subcond, $user ) ) {
454  return false;
455  }
456  }
457 
458  return true;
459  } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes)
460  foreach ( array_slice( $cond, 1 ) as $subcond ) {
461  if ( $this->recCheckCondition( $subcond, $user ) ) {
462  return true;
463  }
464  }
465 
466  return false;
467  } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes)
468  if ( count( $cond ) > 3 ) {
469  $this->logger->warning(
470  'recCheckCondition() given XOR ("^") condition on three or more conditions.' .
471  ' Check your $wgAutopromote and $wgAutopromoteOnce settings.'
472  );
473  }
474  return $this->recCheckCondition( $cond[1], $user )
475  xor $this->recCheckCondition( $cond[2], $user );
476  } elseif ( $cond[0] == '!' ) { // NOT (no conds pass)
477  foreach ( array_slice( $cond, 1 ) as $subcond ) {
478  if ( $this->recCheckCondition( $subcond, $user ) ) {
479  return false;
480  }
481  }
482 
483  return true;
484  }
485  }
486  // If we got here, the array presumably does not contain other conditions;
487  // it's not recursive. Pass it off to checkCondition.
488  if ( !is_array( $cond ) ) {
489  $cond = [ $cond ];
490  }
491 
492  return $this->checkCondition( $cond, $user );
493  }
494 
505  private function checkCondition( array $cond, User $user ) : bool {
506  if ( count( $cond ) < 1 ) {
507  return false;
508  }
509 
510  switch ( $cond[0] ) {
512  if ( Sanitizer::validateEmail( $user->getEmail() ) ) {
513  if ( $this->options->get( 'EmailAuthentication' ) ) {
514  return (bool)$user->getEmailAuthenticationTimestamp();
515  } else {
516  return true;
517  }
518  }
519  return false;
520  case APCOND_EDITCOUNT:
521  $reqEditCount = $cond[1];
522 
523  // T157718: Avoid edit count lookup if specified edit count is 0 or invalid
524  if ( $reqEditCount <= 0 ) {
525  return true;
526  }
527  return $user->isRegistered() && $this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount;
528  case APCOND_AGE:
529  $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
530  return $age >= $cond[1];
532  $age = time() - (int)wfTimestampOrNull(
533  TS_UNIX, $this->userEditTracker->getFirstEditTimestamp( $user ) );
534  return $age >= $cond[1];
535  case APCOND_INGROUPS:
536  $groups = array_slice( $cond, 1 );
537  return count( array_intersect( $groups, $this->getUserGroups( $user ) ) ) == count( $groups );
538  case APCOND_ISIP:
539  return $cond[1] == $user->getRequest()->getIP();
540  case APCOND_IPINRANGE:
541  return IPUtils::isInRange( $user->getRequest()->getIP(), $cond[1] );
542  case APCOND_BLOCKED:
543  // Because checking for ipblock-exempt leads back to here (thus infinite recursion),
544  // we stop checking for ipblock-exempt via here. We do this by setting the second
545  // param to true.
546  // See T270145.
547  $block = $user->getBlock( false, true );
548  return $block && $block->isSitewide();
549  case APCOND_ISBOT:
550  return in_array( 'bot', $this->groupPermissionsLookup
551  ->getGroupPermissions( $this->getUserGroups( $user ) ) );
552  default:
553  $result = null;
554  $this->hookRunner->onAutopromoteCondition( $cond[0],
555  array_slice( $cond, 1 ), $user, $result );
556  if ( $result === null ) {
557  throw new InvalidArgumentException(
558  "Unrecognized condition {$cond[0]} for autopromotion!"
559  );
560  }
561 
562  return (bool)$result;
563  }
564  }
565 
582  UserIdentity $user,
583  string $event
584  ) : array {
585  Assert::precondition(
586  !$this->dbDomain || WikiMap::isCurrentWikiDbDomain( $this->dbDomain ),
587  __METHOD__ . " is not supported for foreign domains: {$this->dbDomain} used"
588  );
589 
590  if ( $this->readOnlyMode->isReadOnly() || !$user->getId() ) {
591  return [];
592  }
593 
594  $toPromote = $this->getUserAutopromoteOnceGroups( $user, $event );
595  if ( $toPromote === [] ) {
596  return [];
597  }
598 
599  $userObj = User::newFromIdentity( $user );
600  if ( !$userObj->checkAndSetTouched() ) {
601  return []; // raced out (bug T48834)
602  }
603 
604  $oldGroups = $this->getUserGroups( $user ); // previous groups
605  $oldUGMs = $this->getUserGroupMemberships( $user );
606  foreach ( $toPromote as $group ) {
607  $this->addUserToGroup( $user, $group );
608  }
609  $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
610  $newUGMs = $this->getUserGroupMemberships( $user );
611 
612  // update groups in external authentication database
613  // TODO: deprecate passing full User object to hook
614  $this->hookRunner->onUserGroupsChanged(
615  $userObj,
616  $toPromote, [],
617  false,
618  false,
619  $oldUGMs,
620  $newUGMs
621  );
622 
623  $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
624  $logEntry->setPerformer( $user );
625  $logEntry->setTarget( $userObj->getUserPage() );
626  $logEntry->setParameters( [
627  '4::oldgroups' => $oldGroups,
628  '5::newgroups' => $newGroups,
629  ] );
630  $logid = $logEntry->insert();
631  if ( $this->options->get( 'AutopromoteOnceLogInRC' ) ) {
632  $logEntry->publish( $logid );
633  }
634 
635  return $toPromote;
636  }
637 
646  public function getUserGroups(
647  UserIdentity $user,
648  int $queryFlags = self::READ_NORMAL
649  ) : array {
650  return array_keys( $this->getUserGroupMemberships( $user, $queryFlags ) );
651  }
652 
661  public function getUserGroupMemberships(
662  UserIdentity $user,
663  int $queryFlags = self::READ_NORMAL
664  ) : array {
665  $userKey = $this->getCacheKey( $user );
666 
667  if ( $this->canUseCachedValues( $user, self::CACHE_MEMBERSHIP, $queryFlags ) &&
668  isset( $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP] )
669  ) {
671  return $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP];
672  }
673 
674  if ( !$user->isRegistered() ) {
675  // Anon users don't have groups stored in the database
676  return [];
677  }
678 
679  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
680  $queryInfo = $this->getQueryInfo();
681  $res = $db->select(
682  $queryInfo['tables'],
683  $queryInfo['fields'],
684  [ 'ug_user' => $user->getId() ],
685  __METHOD__,
686  [],
687  $queryInfo['joins']
688  );
689 
690  $ugms = [];
691  foreach ( $res as $row ) {
692  $ugm = $this->newGroupMembershipFromRow( $row );
693  if ( !$ugm->isExpired() ) {
694  $ugms[$ugm->getGroup()] = $ugm;
695  }
696  }
697  ksort( $ugms );
698 
699  $this->setCache( $user, self::CACHE_MEMBERSHIP, $ugms, $queryFlags );
700 
701  return $ugms;
702  }
703 
719  public function addUserToGroup(
720  UserIdentity $user,
721  string $group,
722  string $expiry = null,
723  bool $allowUpdate = false
724  ) : bool {
725  if ( $this->readOnlyMode->isReadOnly() ) {
726  return false;
727  }
728 
729  if ( !$user->isRegistered() ) {
730  throw new InvalidArgumentException(
731  'UserGroupManager::addUserToGroup() needs a positive user ID. ' .
732  'Perhaps addGroup() was called before the user was added to the database.'
733  );
734  }
735 
736  if ( $expiry ) {
737  $expiry = wfTimestamp( TS_MW, $expiry );
738  }
739 
740  // TODO: Deprecate passing out user object in the hook by introducing
741  // an alternative hook
742  if ( $this->hookContainer->isRegistered( 'UserAddGroup' ) ) {
743  $userObj = User::newFromIdentity( $user );
744  $userObj->load();
745  if ( !$this->hookRunner->onUserAddGroup( $userObj, $group, $expiry ) ) {
746  return false;
747  }
748  }
749 
750  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
751  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER, [], $this->dbDomain );
752 
753  $dbw->startAtomic( __METHOD__ );
754  $dbw->insert(
755  'user_groups',
756  [
757  'ug_user' => $user->getId(),
758  'ug_group' => $group,
759  'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null,
760  ],
761  __METHOD__,
762  [ 'IGNORE' ]
763  );
764 
765  $affected = $dbw->affectedRows();
766  if ( !$affected ) {
767  // Conflicting row already exists; it should be overridden if it is either expired
768  // or if $allowUpdate is true and the current row is different than the loaded row.
769  $conds = [
770  'ug_user' => $user->getId(),
771  'ug_group' => $group
772  ];
773  if ( $allowUpdate ) {
774  // Update the current row if its expiry does not match that of the loaded row
775  $conds[] = $expiry
776  ? "ug_expiry IS NULL OR ug_expiry != {$dbw->addQuotes( $dbw->timestamp( $expiry ) )}"
777  : 'ug_expiry IS NOT NULL';
778  } else {
779  // Update the current row if it is expired
780  $conds[] = "ug_expiry < {$dbw->addQuotes( $dbw->timestamp() )}";
781  }
782  $dbw->update(
783  'user_groups',
784  [ 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null ],
785  $conds,
786  __METHOD__
787  );
788  $affected = $dbw->affectedRows();
789  }
790  $dbw->endAtomic( __METHOD__ );
791 
792  // Purge old, expired memberships from the DB
793  $fname = __METHOD__;
794  DeferredUpdates::addCallableUpdate( function () use ( $fname ) {
795  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
796  $hasExpiredRow = $dbr->selectField(
797  'user_groups',
798  '1',
799  [ "ug_expiry < {$dbr->addQuotes( $dbr->timestamp() )}" ],
800  $fname
801  );
802  if ( $hasExpiredRow ) {
803  JobQueueGroup::singleton( $this->dbDomain )->push( new UserGroupExpiryJob( [] ) );
804  }
805  } );
806 
807  if ( $affected > 0 ) {
808  $oldUgms[$group] = new UserGroupMembership( $user->getId(), $group, $expiry );
809  if ( !$oldUgms[$group]->isExpired() ) {
810  $this->setCache( $user, self::CACHE_MEMBERSHIP,
811  $oldUgms, self::READ_LATEST );
812  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
813  }
814  foreach ( $this->clearCacheCallbacks as $callback ) {
815  $callback( $user );
816  }
817  return true;
818  }
819  return false;
820  }
821 
830  public function removeUserFromGroup( UserIdentity $user, string $group ) : bool {
831  // TODO: Deprecate passing out user object in the hook by introducing
832  // an alternative hook
833  if ( $this->hookContainer->isRegistered( 'UserRemoveGroup' ) ) {
834  $userObj = User::newFromIdentity( $user );
835  $userObj->load();
836  if ( !$this->hookRunner->onUserRemoveGroup( $userObj, $group ) ) {
837  return false;
838  }
839  }
840 
841  if ( $this->readOnlyMode->isReadOnly() ) {
842  return false;
843  }
844 
845  if ( !$user->isRegistered() ) {
846  throw new InvalidArgumentException(
847  'UserGroupManager::removeUserFromGroup() needs a positive user ID. ' .
848  'Perhaps removeUserFromGroup() was called before the user was added to the database.'
849  );
850  }
851 
852  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
853  $oldFormerGroups = $this->getUserFormerGroups( $user, self::READ_LATEST );
854  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER, [], $this->dbDomain );
855  $dbw->delete(
856  'user_groups',
857  [ 'ug_user' => $user->getId(), 'ug_group' => $group ],
858  __METHOD__
859  );
860 
861  if ( !$dbw->affectedRows() ) {
862  return false;
863  }
864  // Remember that the user was in this group
865  $dbw->insert(
866  'user_former_groups',
867  [ 'ufg_user' => $user->getId(), 'ufg_group' => $group ],
868  __METHOD__,
869  [ 'IGNORE' ]
870  );
871 
872  unset( $oldUgms[$group] );
873  $this->setCache( $user, self::CACHE_MEMBERSHIP, $oldUgms, self::READ_LATEST );
874  $oldFormerGroups[] = $group;
875  $this->setCache( $user, self::CACHE_FORMER, $oldFormerGroups, self::READ_LATEST );
876  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
877  foreach ( $this->clearCacheCallbacks as $callback ) {
878  $callback( $user );
879  }
880  return true;
881  }
882 
894  public function getQueryInfo() : array {
895  return [
896  'tables' => [ 'user_groups' ],
897  'fields' => [
898  'ug_user',
899  'ug_group',
900  'ug_expiry',
901  ],
902  'joins' => []
903  ];
904  }
905 
913  public function purgeExpired() {
914  if ( $this->readOnlyMode->isReadOnly() ) {
915  return false;
916  }
917 
918  $ticket = $this->loadBalancerFactory->getEmptyTransactionTicket( __METHOD__ );
919  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
920 
921  $lockKey = "{$dbw->getDomainID()}:UserGroupManager:purge"; // per-wiki
922  $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
923  if ( !$scopedLock ) {
924  return false; // already running
925  }
926 
927  $now = time();
928  $purgedRows = 0;
929  $queryInfo = $this->getQueryInfo();
930  do {
931  $dbw->startAtomic( __METHOD__ );
932 
933  $res = $dbw->select(
934  $queryInfo['tables'],
935  $queryInfo['fields'],
936  [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
937  __METHOD__,
938  [ 'FOR UPDATE', 'LIMIT' => 100 ],
939  $queryInfo['joins']
940  );
941 
942  if ( $res->numRows() > 0 ) {
943  $insertData = []; // array of users/groups to insert to user_former_groups
944  $deleteCond = []; // array for deleting the rows that are to be moved around
945  foreach ( $res as $row ) {
946  $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
947  $deleteCond[] = $dbw->makeList(
948  [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
950  );
951  }
952  // Delete the rows we're about to move
953  $dbw->delete(
954  'user_groups',
955  $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
956  __METHOD__
957  );
958  // Push the groups to user_former_groups
959  $dbw->insert(
960  'user_former_groups',
961  $insertData,
962  __METHOD__,
963  [ 'IGNORE' ]
964  );
965  // Count how many rows were purged
966  $purgedRows += $res->numRows();
967  }
968 
969  $dbw->endAtomic( __METHOD__ );
970 
971  $this->loadBalancerFactory->commitAndWaitForReplication( __METHOD__, $ticket );
972  } while ( $res->numRows() > 0 );
973  return $purgedRows;
974  }
975 
981  public function clearCache( UserIdentity $user ) {
982  $userKey = $this->getCacheKey( $user );
983  unset( $this->userGroupCache[$userKey] );
984  unset( $this->queryFlagsUsedForCaching[$userKey] );
985  }
986 
995  private function setCache(
996  UserIdentity $user,
997  string $cacheKind,
998  array $groupValue,
999  int $queryFlags
1000  ) {
1001  $userKey = $this->getCacheKey( $user );
1002  $this->userGroupCache[$userKey][$cacheKind] = $groupValue;
1003  $this->queryFlagsUsedForCaching[$userKey][$cacheKind] = $queryFlags;
1004  }
1005 
1012  private function clearUserCacheForKind( UserIdentity $user, string $cacheKind ) {
1013  $userKey = $this->getCacheKey( $user );
1014  unset( $this->userGroupCache[$userKey][$cacheKind] );
1015  unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] );
1016  }
1017 
1022  private function getDBConnectionRefForQueryFlags( int $queryFlags ) : DBConnRef {
1023  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1024  return $this->loadBalancer->getConnectionRef( $mode, [], $this->dbDomain );
1025  }
1026 
1032  private function getCacheKey( UserIdentity $user ) : string {
1033  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1034  }
1035 
1043  private function canUseCachedValues(
1044  UserIdentity $user,
1045  string $cacheKind,
1046  int $queryFlags
1047  ) : bool {
1048  if ( !$user->isRegistered() ) {
1049  // Anon users don't have groups stored in the database,
1050  // so $queryFlags are ignored.
1051  return true;
1052  }
1053  if ( $queryFlags >= self::READ_LOCKING ) {
1054  return false;
1055  }
1056  $userKey = $this->getCacheKey( $user );
1057  $queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ?? self::READ_NONE;
1058  return $queryFlagsUsed >= $queryFlags;
1059  }
1060 }
WikiMap\isCurrentWikiDbDomain
static isCurrentWikiDbDomain( $domain)
Definition: WikiMap.php:312
LIST_OR
const LIST_OR
Definition: Defines.php:46
APCOND_AGE
const APCOND_AGE
Definition: Defines.php:190
APCOND_ISBOT
const APCOND_ISBOT
Definition: Defines.php:197
MediaWiki\User\UserGroupManager\checkCondition
checkCondition(array $cond, User $user)
As recCheckCondition, but not recursive.
Definition: UserGroupManager.php:505
MediaWiki\User\UserGroupManager\clearCache
clearCache(UserIdentity $user)
Cleans cached group memberships for a given user.
Definition: UserGroupManager.php:981
MediaWiki\User\UserGroupManager\removeUserFromGroup
removeUserFromGroup(UserIdentity $user, string $group)
Remove the user from the given group.
Definition: UserGroupManager.php:830
MediaWiki\User\UserGroupManager\CACHE_FORMER
const CACHE_FORMER
string key for former groups cache
Definition: UserGroupManager.php:109
MediaWiki\User\UserGroupManager\$clearCacheCallbacks
callable[] $clearCacheCallbacks
Definition: UserGroupManager.php:94
MediaWiki\User\UserGroupManager\getCacheKey
getCacheKey(UserIdentity $user)
Gets a unique key for various caches.
Definition: UserGroupManager.php:1032
User\isRegistered
isRegistered()
Get whether the user is registered.
Definition: User.php:3064
MediaWiki\User\UserGroupManager\$userGroupCache
array $userGroupCache
Service caches, an assoc.
Definition: UserGroupManager.php:122
APCOND_INGROUPS
const APCOND_INGROUPS
Definition: Defines.php:192
MediaWiki\Permissions\GroupPermissionsLookup
Definition: GroupPermissionsLookup.php:30
MediaWiki\User\UserGroupManager\$options
ServiceOptions $options
Definition: UserGroupManager.php:67
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:290
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1832
LIST_AND
const LIST_AND
Definition: Defines.php:43
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:332
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:1707
MediaWiki\User\UserGroupManager\$logger
LoggerInterface $logger
Definition: UserGroupManager.php:91
MediaWiki\User\UserGroupManager\purgeExpired
purgeExpired()
Purge expired memberships from the user_groups table.
Definition: UserGroupManager.php:913
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:663
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
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:2551
$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:56
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
User\getRequest
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3170
MediaWiki\User\UserGroupManager\CACHE_IMPLICIT
const CACHE_IMPLICIT
string key for implicit groups cache
Definition: UserGroupManager.php:100
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\User\UserGroupManager\getUserAutopromoteOnceGroups
getUserAutopromoteOnceGroups(UserIdentity $user, string $event)
Get the groups for the given user based on the given criteria.
Definition: UserGroupManager.php:399
MediaWiki\User\UserGroupManager\$dbDomain
string false $dbDomain
Definition: UserGroupManager.php:97
User\getEmail
getEmail()
Get the user's e-mail address.
Definition: User.php:2541
MediaWiki\User\UserGroupManager
Managers user groups.
Definition: UserGroupManager.php:51
MediaWiki\User\UserGroupManager\$userEditTracker
UserEditTracker $userEditTracker
Definition: UserGroupManager.php:85
MediaWiki\User\UserGroupManager\$hookRunner
HookRunner $hookRunner
Definition: UserGroupManager.php:79
MediaWiki\User\UserGroupManager\addUserToAutopromoteOnceGroups
addUserToAutopromoteOnceGroups(UserIdentity $user, string $event)
Add the user to the group if he/she meets given criteria.
Definition: UserGroupManager.php:581
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
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:719
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
MediaWiki\User\UserGroupManager\$hookContainer
HookContainer $hookContainer
Definition: UserGroupManager.php:76
MediaWiki\User\UserGroupManager\listAllImplicitGroups
listAllImplicitGroups()
Get a list of all configured implicit groups.
Definition: UserGroupManager.php:194
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:661
DeferredUpdates
Class for managing the deferral of updates within the scope of a PHP script invocation.
Definition: DeferredUpdates.php:82
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1848
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:1043
APCOND_EDITCOUNT
const APCOND_EDITCOUNT
Definition: Defines.php:189
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
MediaWiki\User\UserGroupManager\__construct
__construct(ServiceOptions $options, ConfiguredReadOnlyMode $configuredReadOnlyMode, ILBFactory $loadBalancerFactory, HookContainer $hookContainer, UserEditTracker $userEditTracker, GroupPermissionsLookup $groupPermissionsLookup, LoggerInterface $logger, array $clearCacheCallbacks=[], $dbDomain=false)
Definition: UserGroupManager.php:148
DB_MASTER
const DB_MASTER
Definition: defines.php:26
UserGroupExpiryJob
Definition: UserGroupExpiryJob.php:27
APCOND_EMAILCONFIRMED
const APCOND_EMAILCONFIRMED
Definition: Defines.php:191
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
APCOND_ISIP
const APCOND_ISIP
Definition: Defines.php:193
User\getBlock
getBlock( $fromReplica=true, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:1950
MediaWiki\User\UserGroupManager\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: UserGroupManager.php:82
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:250
MediaWiki\User\UserGroupManager\newGroupMembershipFromRow
newGroupMembershipFromRow(\stdClass $row)
Creates a new UserGroupMembership instance from $row.
Definition: UserGroupManager.php:207
MediaWiki\User
Definition: ActorNormalization.php:21
MediaWiki\User\UserGroupManager\$groupPermissionsLookup
GroupPermissionsLookup $groupPermissionsLookup
Definition: UserGroupManager.php:88
MediaWiki\User\UserGroupManager\getUserAutopromoteGroups
getUserAutopromoteGroups(UserIdentity $user)
Get the groups for the given user based on $wgAutopromote.
Definition: UserGroupManager.php:373
APCOND_IPINRANGE
const APCOND_IPINRANGE
Definition: Defines.php:194
APCOND_BLOCKED
const APCOND_BLOCKED
Definition: Defines.php:196
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:646
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:4115
MediaWiki\User\UserGroupManager\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags(int $queryFlags)
Definition: UserGroupManager.php:1022
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:446
MediaWiki\User\UserGroupManager\$queryFlagsUsedForCaching
array $queryFlagsUsedForCaching
An assoc.
Definition: UserGroupManager.php:135
APCOND_AGE_FROM_EDIT
const APCOND_AGE_FROM_EDIT
Definition: Defines.php:195
MediaWiki\User\UserGroupManager\CACHE_EFFECTIVE
const CACHE_EFFECTIVE
string key for effective groups cache
Definition: UserGroupManager.php:103
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:43
MediaWiki\User\UserGroupManager\clearUserCacheForKind
clearUserCacheForKind(UserIdentity $user, string $cacheKind)
Clears a cached group membership and query key for a given user.
Definition: UserGroupManager.php:1012
MediaWiki\User\UserGroupManager\CACHE_MEMBERSHIP
const CACHE_MEMBERSHIP
string key for group memberships cache
Definition: UserGroupManager.php:106
MediaWiki\User\UserGroupManager\$loadBalancerFactory
ILBFactory $loadBalancerFactory
Definition: UserGroupManager.php:70
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
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:995
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:576
MediaWiki\User\UserGroupManager\listAllGroups
listAllGroups()
Return the set of defined explicit groups.
Definition: UserGroupManager.php:180
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:225
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
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:894
Sanitizer
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:34
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:36
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:66
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:73