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;
35 use Psr\Log\LoggerInterface;
36 use ReadOnlyMode;
37 use Sanitizer;
38 use User;
41 use WikiMap;
42 use Wikimedia\Assert\Assert;
43 use Wikimedia\IPUtils;
47 
53 
57  public const CONSTRUCTOR_OPTIONS = [
58  'AddGroups',
59  'Autopromote',
60  'AutopromoteOnce',
61  'AutopromoteOnceLogInRC',
62  'EmailAuthentication',
63  'ImplicitGroups',
64  'GroupInheritsPermissions',
65  'GroupPermissions',
66  'GroupsAddToSelf',
67  'GroupsRemoveFromSelf',
68  'RevokePermissions',
69  'RemoveGroups',
70  ];
71 
73  private $options;
74 
77 
79  private $loadBalancer;
80 
82  private $hookContainer;
83 
85  private $hookRunner;
86 
88  private $readOnlyMode;
89 
92 
95 
97  private $jobQueueGroup;
98 
100  private $logger;
101 
104 
106  private $dbDomain;
107 
109  private const CACHE_IMPLICIT = 'implicit';
110 
112  private const CACHE_EFFECTIVE = 'effective';
113 
115  private const CACHE_MEMBERSHIP = 'membership';
116 
118  private const CACHE_FORMER = 'former';
119 
131  private $userGroupCache = [];
132 
145 
158  public function __construct(
160  ConfiguredReadOnlyMode $configuredReadOnlyMode,
166  LoggerInterface $logger,
167  array $clearCacheCallbacks = [],
168  $dbDomain = false
169  ) {
170  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
171  $this->options = $options;
172  $this->loadBalancerFactory = $loadBalancerFactory;
173  $this->loadBalancer = $loadBalancerFactory->getMainLB( $dbDomain );
174  $this->hookContainer = $hookContainer;
175  $this->hookRunner = new HookRunner( $hookContainer );
176  $this->userEditTracker = $userEditTracker;
177  $this->groupPermissionsLookup = $groupPermissionsLookup;
178  $this->jobQueueGroup = $jobQueueGroup;
179  $this->logger = $logger;
180  // Can't just inject ROM since we LB can be for foreign wiki
181  $this->readOnlyMode = new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
182  $this->clearCacheCallbacks = $clearCacheCallbacks;
183  $this->dbDomain = $dbDomain;
184  }
185 
192  public function listAllGroups(): array {
193  return array_values( array_diff(
194  array_merge(
195  array_keys( $this->options->get( 'GroupPermissions' ) ),
196  array_keys( $this->options->get( 'RevokePermissions' ) ),
197  array_keys( $this->options->get( 'GroupInheritsPermissions' ) )
198  ),
199  $this->listAllImplicitGroups()
200  ) );
201  }
202 
207  public function listAllImplicitGroups(): array {
208  return $this->options->get( 'ImplicitGroups' );
209  }
210 
220  public function newGroupMembershipFromRow( \stdClass $row ): UserGroupMembership {
221  return new UserGroupMembership(
222  (int)$row->ug_user,
223  $row->ug_group,
224  $row->ug_expiry === null ? null : wfTimestamp(
225  TS_MW,
226  $row->ug_expiry
227  )
228  );
229  }
230 
239  UserIdentity $user,
240  array $userGroups,
241  int $queryFlags = self::READ_NORMAL
242  ) {
243  $membershipGroups = [];
244  reset( $userGroups );
245  foreach ( $userGroups as $row ) {
246  $ugm = $this->newGroupMembershipFromRow( $row );
247  $membershipGroups[ $ugm->getGroup() ] = $ugm;
248  }
249  $this->setCache(
250  $this->getCacheKey( $user ),
251  self::CACHE_MEMBERSHIP,
252  $membershipGroups,
253  $queryFlags
254  );
255  }
256 
268  public function getUserImplicitGroups(
269  UserIdentity $user,
270  int $queryFlags = self::READ_NORMAL,
271  bool $recache = false
272  ): array {
273  $userKey = $this->getCacheKey( $user );
274  if ( $recache ||
275  !isset( $this->userGroupCache[$userKey][self::CACHE_IMPLICIT] ) ||
276  !$this->canUseCachedValues( $user, self::CACHE_IMPLICIT, $queryFlags )
277  ) {
278  $groups = [ '*' ];
279  if ( $user->isRegistered() ) {
280  $groups[] = 'user';
281 
282  $groups = array_unique( array_merge(
283  $groups,
284  $this->getUserAutopromoteGroups( $user )
285  ) );
286  }
287  $this->setCache( $userKey, self::CACHE_IMPLICIT, $groups, $queryFlags );
288  if ( $recache ) {
289  // Assure data consistency with rights/groups,
290  // as getUserEffectiveGroups() depends on this function
291  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
292  }
293  }
294  return $this->userGroupCache[$userKey][self::CACHE_IMPLICIT];
295  }
296 
308  public function getUserEffectiveGroups(
309  UserIdentity $user,
310  int $queryFlags = self::READ_NORMAL,
311  bool $recache = false
312  ): array {
313  $userKey = $this->getCacheKey( $user );
314  // Ignore cache if the $recache flag is set, cached values can not be used
315  // or the cache value is missing
316  if ( $recache ||
317  !$this->canUseCachedValues( $user, self::CACHE_EFFECTIVE, $queryFlags ) ||
318  !isset( $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE] )
319  ) {
320  $groups = array_unique( array_merge(
321  $this->getUserGroups( $user, $queryFlags ), // explicit groups
322  $this->getUserImplicitGroups( $user, $queryFlags, $recache ) // implicit groups
323  ) );
324  // TODO: Deprecate passing out user object in the hook by introducing
325  // an alternative hook
326  if ( $this->hookContainer->isRegistered( 'UserEffectiveGroups' ) ) {
327  $userObj = User::newFromIdentity( $user );
328  $userObj->load();
329  // Hook for additional groups
330  $this->hookRunner->onUserEffectiveGroups( $userObj, $groups );
331  }
332  // Force reindexation of groups when a hook has unset one of them
333  $effectiveGroups = array_values( array_unique( $groups ) );
334  $this->setCache( $userKey, self::CACHE_EFFECTIVE, $effectiveGroups, $queryFlags );
335  }
336  return $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE];
337  }
338 
351  public function getUserFormerGroups(
352  UserIdentity $user,
353  int $queryFlags = self::READ_NORMAL
354  ): array {
355  $userKey = $this->getCacheKey( $user );
356 
357  if ( $this->canUseCachedValues( $user, self::CACHE_FORMER, $queryFlags ) &&
358  isset( $this->userGroupCache[$userKey][self::CACHE_FORMER] )
359  ) {
360  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
361  }
362 
363  if ( !$user->isRegistered() ) {
364  // Anon users don't have groups stored in the database
365  return [];
366  }
367 
368  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
369  $res = $db->select(
370  'user_former_groups',
371  [ 'ufg_group' ],
372  [ 'ufg_user' => $user->getId() ],
373  __METHOD__
374  );
375  $formerGroups = [];
376  foreach ( $res as $row ) {
377  $formerGroups[] = $row->ufg_group;
378  }
379  $this->setCache( $userKey, self::CACHE_FORMER, $formerGroups, $queryFlags );
380 
381  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
382  }
383 
392  public function getUserAutopromoteGroups( UserIdentity $user ): array {
393  $promote = [];
394  // TODO: remove the need for the full user object
395  $userObj = User::newFromIdentity( $user );
396  foreach ( $this->options->get( 'Autopromote' ) as $group => $cond ) {
397  if ( $this->recCheckCondition( $cond, $userObj ) ) {
398  $promote[] = $group;
399  }
400  }
401 
402  $this->hookRunner->onGetAutoPromoteGroups( $userObj, $promote );
403  return $promote;
404  }
405 
419  UserIdentity $user,
420  string $event
421  ): array {
422  $autopromoteOnce = $this->options->get( 'AutopromoteOnce' );
423  $promote = [];
424 
425  if ( isset( $autopromoteOnce[$event] ) && count( $autopromoteOnce[$event] ) ) {
426  $currentGroups = $this->getUserGroups( $user );
427  $formerGroups = $this->getUserFormerGroups( $user );
428  // TODO: remove the need for the full user object
429  $userObj = User::newFromIdentity( $user );
430  foreach ( $autopromoteOnce[$event] as $group => $cond ) {
431  // Do not check if the user's already a member
432  if ( in_array( $group, $currentGroups ) ) {
433  continue;
434  }
435  // Do not autopromote if the user has belonged to the group
436  if ( in_array( $group, $formerGroups ) ) {
437  continue;
438  }
439  // Finally - check the conditions
440  if ( $this->recCheckCondition( $cond, $userObj ) ) {
441  $promote[] = $group;
442  }
443  }
444  }
445 
446  return $promote;
447  }
448 
465  private function recCheckCondition( $cond, User $user ): bool {
466  $validOps = [ '&', '|', '^', '!' ];
467 
468  if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
469  // Recursive condition
470  if ( $cond[0] == '&' ) { // AND (all conds pass)
471  foreach ( array_slice( $cond, 1 ) as $subcond ) {
472  if ( !$this->recCheckCondition( $subcond, $user ) ) {
473  return false;
474  }
475  }
476 
477  return true;
478  } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes)
479  foreach ( array_slice( $cond, 1 ) as $subcond ) {
480  if ( $this->recCheckCondition( $subcond, $user ) ) {
481  return true;
482  }
483  }
484 
485  return false;
486  } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes)
487  if ( count( $cond ) > 3 ) {
488  $this->logger->warning(
489  'recCheckCondition() given XOR ("^") condition on three or more conditions.' .
490  ' Check your $wgAutopromote and $wgAutopromoteOnce settings.'
491  );
492  }
493  return $this->recCheckCondition( $cond[1], $user )
494  xor $this->recCheckCondition( $cond[2], $user );
495  } elseif ( $cond[0] == '!' ) { // NOT (no conds pass)
496  foreach ( array_slice( $cond, 1 ) as $subcond ) {
497  if ( $this->recCheckCondition( $subcond, $user ) ) {
498  return false;
499  }
500  }
501 
502  return true;
503  }
504  }
505  // If we got here, the array presumably does not contain other conditions;
506  // it's not recursive. Pass it off to checkCondition.
507  if ( !is_array( $cond ) ) {
508  $cond = [ $cond ];
509  }
510 
511  return $this->checkCondition( $cond, $user );
512  }
513 
524  private function checkCondition( array $cond, User $user ): bool {
525  if ( count( $cond ) < 1 ) {
526  return false;
527  }
528 
529  switch ( $cond[0] ) {
531  if ( Sanitizer::validateEmail( $user->getEmail() ) ) {
532  if ( $this->options->get( 'EmailAuthentication' ) ) {
533  return (bool)$user->getEmailAuthenticationTimestamp();
534  } else {
535  return true;
536  }
537  }
538  return false;
539  case APCOND_EDITCOUNT:
540  $reqEditCount = $cond[1];
541 
542  // T157718: Avoid edit count lookup if specified edit count is 0 or invalid
543  if ( $reqEditCount <= 0 ) {
544  return true;
545  }
546  return $user->isRegistered() && $this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount;
547  case APCOND_AGE:
548  $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
549  return $age >= $cond[1];
551  $age = time() - (int)wfTimestampOrNull(
552  TS_UNIX, $this->userEditTracker->getFirstEditTimestamp( $user ) );
553  return $age >= $cond[1];
554  case APCOND_INGROUPS:
555  $groups = array_slice( $cond, 1 );
556  return count( array_intersect( $groups, $this->getUserGroups( $user ) ) ) == count( $groups );
557  case APCOND_ISIP:
558  return $cond[1] == $user->getRequest()->getIP();
559  case APCOND_IPINRANGE:
560  return IPUtils::isInRange( $user->getRequest()->getIP(), $cond[1] );
561  case APCOND_BLOCKED:
562  // Because checking for ipblock-exempt leads back to here (thus infinite recursion),
563  // we stop checking for ipblock-exempt via here. We do this by setting the second
564  // param to true.
565  // See T270145.
566  $block = $user->getBlock( Authority::READ_LATEST, true );
567  return $block && $block->isSitewide();
568  case APCOND_ISBOT:
569  return in_array( 'bot', $this->groupPermissionsLookup
570  ->getGroupPermissions( $this->getUserGroups( $user ) ) );
571  default:
572  $result = null;
573  $this->hookRunner->onAutopromoteCondition( $cond[0],
574  array_slice( $cond, 1 ), $user, $result );
575  if ( $result === null ) {
576  throw new InvalidArgumentException(
577  "Unrecognized condition {$cond[0]} for autopromotion!"
578  );
579  }
580 
581  return (bool)$result;
582  }
583  }
584 
601  UserIdentity $user,
602  string $event
603  ): array {
604  Assert::precondition(
605  !$this->dbDomain || WikiMap::isCurrentWikiDbDomain( $this->dbDomain ),
606  __METHOD__ . " is not supported for foreign domains: {$this->dbDomain} used"
607  );
608 
609  if ( $this->readOnlyMode->isReadOnly() || !$user->getId() ) {
610  return [];
611  }
612 
613  $toPromote = $this->getUserAutopromoteOnceGroups( $user, $event );
614  if ( $toPromote === [] ) {
615  return [];
616  }
617 
618  $userObj = User::newFromIdentity( $user );
619  if ( !$userObj->checkAndSetTouched() ) {
620  return []; // raced out (bug T48834)
621  }
622 
623  $oldGroups = $this->getUserGroups( $user ); // previous groups
624  $oldUGMs = $this->getUserGroupMemberships( $user );
625  $this->addUserToMultipleGroups( $user, $toPromote );
626  $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
627  $newUGMs = $this->getUserGroupMemberships( $user );
628 
629  // update groups in external authentication database
630  // TODO: deprecate passing full User object to hook
631  $this->hookRunner->onUserGroupsChanged(
632  $userObj,
633  $toPromote, [],
634  false,
635  false,
636  $oldUGMs,
637  $newUGMs
638  );
639 
640  $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
641  $logEntry->setPerformer( $user );
642  $logEntry->setTarget( $userObj->getUserPage() );
643  $logEntry->setParameters( [
644  '4::oldgroups' => $oldGroups,
645  '5::newgroups' => $newGroups,
646  ] );
647  $logid = $logEntry->insert();
648  if ( $this->options->get( 'AutopromoteOnceLogInRC' ) ) {
649  $logEntry->publish( $logid );
650  }
651 
652  return $toPromote;
653  }
654 
663  public function getUserGroups(
664  UserIdentity $user,
665  int $queryFlags = self::READ_NORMAL
666  ): array {
667  return array_keys( $this->getUserGroupMemberships( $user, $queryFlags ) );
668  }
669 
678  public function getUserGroupMemberships(
679  UserIdentity $user,
680  int $queryFlags = self::READ_NORMAL
681  ): array {
682  $userKey = $this->getCacheKey( $user );
683 
684  if ( $this->canUseCachedValues( $user, self::CACHE_MEMBERSHIP, $queryFlags ) &&
685  isset( $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP] )
686  ) {
688  return $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP];
689  }
690 
691  if ( !$user->isRegistered() ) {
692  // Anon users don't have groups stored in the database
693  return [];
694  }
695 
696  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
697  $queryInfo = $this->getQueryInfo();
698  $res = $db->select(
699  $queryInfo['tables'],
700  $queryInfo['fields'],
701  [ 'ug_user' => $user->getId() ],
702  __METHOD__,
703  [],
704  $queryInfo['joins']
705  );
706 
707  $ugms = [];
708  foreach ( $res as $row ) {
709  $ugm = $this->newGroupMembershipFromRow( $row );
710  if ( !$ugm->isExpired() ) {
711  $ugms[$ugm->getGroup()] = $ugm;
712  }
713  }
714  ksort( $ugms );
715 
716  $this->setCache( $userKey, self::CACHE_MEMBERSHIP, $ugms, $queryFlags );
717 
718  return $ugms;
719  }
720 
736  public function addUserToGroup(
737  UserIdentity $user,
738  string $group,
739  string $expiry = null,
740  bool $allowUpdate = false
741  ): bool {
742  if ( $this->readOnlyMode->isReadOnly() ) {
743  return false;
744  }
745 
746  if ( !$user->isRegistered() ) {
747  throw new InvalidArgumentException(
748  'UserGroupManager::addUserToGroup() needs a positive user ID. ' .
749  'Perhaps addUserToGroup() was called before the user was added to the database.'
750  );
751  }
752 
753  if ( $expiry ) {
754  $expiry = wfTimestamp( TS_MW, $expiry );
755  }
756 
757  // TODO: Deprecate passing out user object in the hook by introducing
758  // an alternative hook
759  if ( $this->hookContainer->isRegistered( 'UserAddGroup' ) ) {
760  $userObj = User::newFromIdentity( $user );
761  $userObj->load();
762  if ( !$this->hookRunner->onUserAddGroup( $userObj, $group, $expiry ) ) {
763  return false;
764  }
765  }
766 
767  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
768  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->dbDomain );
769 
770  $dbw->startAtomic( __METHOD__ );
771  $dbw->insert(
772  'user_groups',
773  [
774  'ug_user' => $user->getId(),
775  'ug_group' => $group,
776  'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null,
777  ],
778  __METHOD__,
779  [ 'IGNORE' ]
780  );
781 
782  $affected = $dbw->affectedRows();
783  if ( !$affected ) {
784  // Conflicting row already exists; it should be overridden if it is either expired
785  // or if $allowUpdate is true and the current row is different than the loaded row.
786  $conds = [
787  'ug_user' => $user->getId(),
788  'ug_group' => $group
789  ];
790  if ( $allowUpdate ) {
791  // Update the current row if its expiry does not match that of the loaded row
792  $conds[] = $expiry
793  ? "ug_expiry IS NULL OR ug_expiry != {$dbw->addQuotes( $dbw->timestamp( $expiry ) )}"
794  : 'ug_expiry IS NOT NULL';
795  } else {
796  // Update the current row if it is expired
797  $conds[] = "ug_expiry < {$dbw->addQuotes( $dbw->timestamp() )}";
798  }
799  $dbw->update(
800  'user_groups',
801  [ 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null ],
802  $conds,
803  __METHOD__
804  );
805  $affected = $dbw->affectedRows();
806  }
807  $dbw->endAtomic( __METHOD__ );
808 
809  // Purge old, expired memberships from the DB
810  $fname = __METHOD__;
811  DeferredUpdates::addCallableUpdate( function () use ( $fname ) {
812  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
813  $hasExpiredRow = (bool)$dbr->selectField( 'user_groups', '1',
814  [ 'ug_expiry < ' . $dbr->addQuotes( $dbr->timestamp() ) ],
815  $fname
816  );
817  if ( $hasExpiredRow ) {
818  $this->jobQueueGroup->push( new UserGroupExpiryJob( [] ) );
819  }
820  } );
821 
822  if ( $affected > 0 ) {
823  $oldUgms[$group] = new UserGroupMembership( $user->getId(), $group, $expiry );
824  if ( !$oldUgms[$group]->isExpired() ) {
825  $this->setCache(
826  $this->getCacheKey( $user ),
827  self::CACHE_MEMBERSHIP,
828  $oldUgms,
829  self::READ_LATEST
830  );
831  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
832  }
833  foreach ( $this->clearCacheCallbacks as $callback ) {
834  $callback( $user );
835  }
836  return true;
837  }
838  return false;
839  }
840 
854  public function addUserToMultipleGroups(
855  UserIdentity $user,
856  array $groups,
857  string $expiry = null,
858  bool $allowUpdate = false
859  ) {
860  foreach ( $groups as $group ) {
861  $this->addUserToGroup( $user, $group, $expiry, $allowUpdate );
862  }
863  }
864 
873  public function removeUserFromGroup( UserIdentity $user, string $group ): bool {
874  // TODO: Deprecate passing out user object in the hook by introducing
875  // an alternative hook
876  if ( $this->hookContainer->isRegistered( 'UserRemoveGroup' ) ) {
877  $userObj = User::newFromIdentity( $user );
878  $userObj->load();
879  if ( !$this->hookRunner->onUserRemoveGroup( $userObj, $group ) ) {
880  return false;
881  }
882  }
883 
884  if ( $this->readOnlyMode->isReadOnly() ) {
885  return false;
886  }
887 
888  if ( !$user->isRegistered() ) {
889  throw new InvalidArgumentException(
890  'UserGroupManager::removeUserFromGroup() needs a positive user ID. ' .
891  'Perhaps removeUserFromGroup() was called before the user was added to the database.'
892  );
893  }
894 
895  $oldUgms = $this->getUserGroupMemberships( $user, self::READ_LATEST );
896  $oldFormerGroups = $this->getUserFormerGroups( $user, self::READ_LATEST );
897  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->dbDomain );
898  $dbw->delete(
899  'user_groups',
900  [ 'ug_user' => $user->getId(), 'ug_group' => $group ],
901  __METHOD__
902  );
903 
904  if ( !$dbw->affectedRows() ) {
905  return false;
906  }
907  // Remember that the user was in this group
908  $dbw->insert(
909  'user_former_groups',
910  [ 'ufg_user' => $user->getId(), 'ufg_group' => $group ],
911  __METHOD__,
912  [ 'IGNORE' ]
913  );
914 
915  unset( $oldUgms[$group] );
916  $userKey = $this->getCacheKey( $user );
917  $this->setCache( $userKey, self::CACHE_MEMBERSHIP, $oldUgms, self::READ_LATEST );
918  $oldFormerGroups[] = $group;
919  $this->setCache( $userKey, self::CACHE_FORMER, $oldFormerGroups, self::READ_LATEST );
920  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
921  foreach ( $this->clearCacheCallbacks as $callback ) {
922  $callback( $user );
923  }
924  return true;
925  }
926 
938  public function getQueryInfo(): array {
939  return [
940  'tables' => [ 'user_groups' ],
941  'fields' => [
942  'ug_user',
943  'ug_group',
944  'ug_expiry',
945  ],
946  'joins' => []
947  ];
948  }
949 
957  public function purgeExpired() {
958  if ( $this->readOnlyMode->isReadOnly() ) {
959  return false;
960  }
961 
962  $ticket = $this->loadBalancerFactory->getEmptyTransactionTicket( __METHOD__ );
963  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
964 
965  $lockKey = "{$dbw->getDomainID()}:UserGroupManager:purge"; // per-wiki
966  $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
967  if ( !$scopedLock ) {
968  return false; // already running
969  }
970 
971  $now = time();
972  $purgedRows = 0;
973  $queryInfo = $this->getQueryInfo();
974  do {
975  $dbw->startAtomic( __METHOD__ );
976 
977  $res = $dbw->select(
978  $queryInfo['tables'],
979  $queryInfo['fields'],
980  [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
981  __METHOD__,
982  [ 'FOR UPDATE', 'LIMIT' => 100 ],
983  $queryInfo['joins']
984  );
985 
986  if ( $res->numRows() > 0 ) {
987  $insertData = []; // array of users/groups to insert to user_former_groups
988  $deleteCond = []; // array for deleting the rows that are to be moved around
989  foreach ( $res as $row ) {
990  $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
991  $deleteCond[] = $dbw->makeList(
992  [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
994  );
995  }
996  // Delete the rows we're about to move
997  $dbw->delete(
998  'user_groups',
999  $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
1000  __METHOD__
1001  );
1002  // Push the groups to user_former_groups
1003  $dbw->insert(
1004  'user_former_groups',
1005  $insertData,
1006  __METHOD__,
1007  [ 'IGNORE' ]
1008  );
1009  // Count how many rows were purged
1010  $purgedRows += $res->numRows();
1011  }
1012 
1013  $dbw->endAtomic( __METHOD__ );
1014 
1015  $this->loadBalancerFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1016  } while ( $res->numRows() > 0 );
1017  return $purgedRows;
1018  }
1019 
1025  private function expandChangeableGroupConfig( array $config, string $group ): array {
1026  if ( empty( $config[$group] ) ) {
1027  return [];
1028  } elseif ( $config[$group] === true ) {
1029  // You get everything
1030  return $this->listAllGroups();
1031  } elseif ( is_array( $config[$group] ) ) {
1032  return $config[$group];
1033  }
1034  return [];
1035  }
1036 
1048  public function getGroupsChangeableByGroup( string $group ): array {
1049  return [
1050  'add' => $this->expandChangeableGroupConfig(
1051  $this->options->get( 'AddGroups' ), $group
1052  ),
1053  'remove' => $this->expandChangeableGroupConfig(
1054  $this->options->get( 'RemoveGroups' ), $group
1055  ),
1056  'add-self' => $this->expandChangeableGroupConfig(
1057  $this->options->get( 'GroupsAddToSelf' ), $group
1058  ),
1059  'remove-self' => $this->expandChangeableGroupConfig(
1060  $this->options->get( 'GroupsRemoveFromSelf' ), $group
1061  ),
1062  ];
1063  }
1064 
1077  public function getGroupsChangeableBy( Authority $authority ): array {
1078  if ( $authority->isAllowed( 'userrights' ) ) {
1079  // This group gives the right to modify everything (reverse-
1080  // compatibility with old "userrights lets you change
1081  // everything")
1082  // Using array_merge to make the groups reindexed
1083  $all = array_merge( $this->listAllGroups() );
1084  return [
1085  'add' => $all,
1086  'remove' => $all,
1087  'add-self' => [],
1088  'remove-self' => []
1089  ];
1090  }
1091 
1092  // Okay, it's not so simple, we will have to go through the arrays
1093  $groups = [
1094  'add' => [],
1095  'remove' => [],
1096  'add-self' => [],
1097  'remove-self' => []
1098  ];
1099  $actorGroups = $this->getUserEffectiveGroups( $authority->getUser() );
1100 
1101  foreach ( $actorGroups as $actorGroup ) {
1102  $groups = array_merge_recursive(
1103  $groups, $this->getGroupsChangeableByGroup( $actorGroup )
1104  );
1105  $groups['add'] = array_unique( $groups['add'] );
1106  $groups['remove'] = array_unique( $groups['remove'] );
1107  $groups['add-self'] = array_unique( $groups['add-self'] );
1108  $groups['remove-self'] = array_unique( $groups['remove-self'] );
1109  }
1110  return $groups;
1111  }
1112 
1118  public function clearCache( UserIdentity $user ) {
1119  $userKey = $this->getCacheKey( $user );
1120  unset( $this->userGroupCache[$userKey] );
1121  unset( $this->queryFlagsUsedForCaching[$userKey] );
1122  }
1123 
1132  private function setCache(
1133  string $userKey,
1134  string $cacheKind,
1135  array $groupValue,
1136  int $queryFlags
1137  ) {
1138  $this->userGroupCache[$userKey][$cacheKind] = $groupValue;
1139  $this->queryFlagsUsedForCaching[$userKey][$cacheKind] = $queryFlags;
1140  }
1141 
1148  private function clearUserCacheForKind( UserIdentity $user, string $cacheKind ) {
1149  $userKey = $this->getCacheKey( $user );
1150  unset( $this->userGroupCache[$userKey][$cacheKind] );
1151  unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] );
1152  }
1153 
1158  private function getDBConnectionRefForQueryFlags( int $queryFlags ): DBConnRef {
1159  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1160  return $this->loadBalancer->getConnectionRef( $mode, [], $this->dbDomain );
1161  }
1162 
1168  private function getCacheKey( UserIdentity $user ): string {
1169  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1170  }
1171 
1179  private function canUseCachedValues(
1180  UserIdentity $user,
1181  string $cacheKind,
1182  int $queryFlags
1183  ): bool {
1184  if ( !$user->isRegistered() ) {
1185  // Anon users don't have groups stored in the database,
1186  // so $queryFlags are ignored.
1187  return true;
1188  }
1189  if ( $queryFlags >= self::READ_LOCKING ) {
1190  return false;
1191  }
1192  $userKey = $this->getCacheKey( $user );
1193  $queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ?? self::READ_NONE;
1194  return $queryFlagsUsed >= $queryFlags;
1195  }
1196 }
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:179
APCOND_ISBOT
const APCOND_ISBOT
Definition: Defines.php:186
MediaWiki\User\UserGroupManager\checkCondition
checkCondition(array $cond, User $user)
As recCheckCondition, but not recursive.
Definition: UserGroupManager.php:524
MediaWiki\User\UserGroupManager\clearCache
clearCache(UserIdentity $user)
Cleans cached group memberships for a given user.
Definition: UserGroupManager.php:1118
MediaWiki\User\UserGroupManager\removeUserFromGroup
removeUserFromGroup(UserIdentity $user, string $group)
Remove the user from the given group.
Definition: UserGroupManager.php:873
MediaWiki\User\UserGroupManager\CACHE_FORMER
const CACHE_FORMER
string key for former groups cache
Definition: UserGroupManager.php:118
MediaWiki\User\UserGroupManager\$clearCacheCallbacks
callable[] $clearCacheCallbacks
Definition: UserGroupManager.php:103
MediaWiki\User\UserGroupManager\getCacheKey
getCacheKey(UserIdentity $user)
Gets a unique key for various caches.
Definition: UserGroupManager.php:1168
User\isRegistered
isRegistered()
Get whether the user is registered.
Definition: User.php:2530
MediaWiki\User\UserGroupManager\$userGroupCache
array $userGroupCache
Service caches, an assoc.
Definition: UserGroupManager.php:131
APCOND_INGROUPS
const APCOND_INGROUPS
Definition: Defines.php:181
MediaWiki\Permissions\GroupPermissionsLookup
Definition: GroupPermissionsLookup.php:30
MediaWiki\User\UserGroupManager\$options
ServiceOptions $options
Definition: UserGroupManager.php:73
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:308
MediaWiki\User\UserGroupManager\$jobQueueGroup
JobQueueGroup $jobQueueGroup
for this $dbDomain
Definition: UserGroupManager.php:97
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1649
MediaWiki\User\UserGroupManager\__construct
__construct(ServiceOptions $options, ConfiguredReadOnlyMode $configuredReadOnlyMode, ILBFactory $loadBalancerFactory, HookContainer $hookContainer, UserEditTracker $userEditTracker, GroupPermissionsLookup $groupPermissionsLookup, JobQueueGroup $jobQueueGroup, LoggerInterface $logger, array $clearCacheCallbacks=[], $dbDomain=false)
Definition: UserGroupManager.php:158
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:351
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:1744
MediaWiki\User\UserGroupManager\$logger
LoggerInterface $logger
Definition: UserGroupManager.php:100
MediaWiki\User\UserGroupManager\purgeExpired
purgeExpired()
Purge expired memberships from the user_groups table.
Definition: UserGroupManager.php:957
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:672
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 the tracked load balancer instance for a main cluster.
User\getEmailAuthenticationTimestamp
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition: User.php:2243
$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:57
User\getBlock
getBlock( $freshness=self::READ_NORMAL, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:1731
MediaWiki\Permissions\Authority\getUser
getUser()
Returns the performer of the actions associated with this authority.
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:2629
MediaWiki\User\UserGroupManager\CACHE_IMPLICIT
const CACHE_IMPLICIT
string key for implicit groups cache
Definition: UserGroupManager.php:109
$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:418
MediaWiki\User\UserGroupManager\$dbDomain
string false $dbDomain
Definition: UserGroupManager.php:106
User\getEmail
getEmail()
Get the user's e-mail address.
Definition: User.php:2230
MediaWiki\User\UserGroupManager
Managers user groups.
Definition: UserGroupManager.php:52
MediaWiki\User\UserGroupManager\$userEditTracker
UserEditTracker $userEditTracker
Definition: UserGroupManager.php:91
MediaWiki\User\UserGroupManager\$hookRunner
HookRunner $hookRunner
Definition: UserGroupManager.php:85
MediaWiki\User\UserGroupManager\addUserToAutopromoteOnceGroups
addUserToAutopromoteOnceGroups(UserIdentity $user, string $event)
Add the user to the group if he/she meets given criteria.
Definition: UserGroupManager.php:600
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:736
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
MediaWiki\User\UserGroupManager\$hookContainer
HookContainer $hookContainer
Definition: UserGroupManager.php:82
MediaWiki\User\UserGroupManager\listAllImplicitGroups
listAllImplicitGroups()
Get a list of all configured implicit groups.
Definition: UserGroupManager.php:207
MediaWiki\User\UserGroupManager\addUserToMultipleGroups
addUserToMultipleGroups(UserIdentity $user, array $groups, string $expiry=null, bool $allowUpdate=false)
Add the user to the given list of groups.
Definition: UserGroupManager.php:854
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:678
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:1665
MediaWiki\User\UserGroupManager\setCache
setCache(string $userKey, string $cacheKind, array $groupValue, int $queryFlags)
Sets cached group memberships and query flags for a given user.
Definition: UserGroupManager.php:1132
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:1179
APCOND_EDITCOUNT
const APCOND_EDITCOUNT
Definition: Defines.php:178
MediaWiki\User\UserGroupManager\getGroupsChangeableByGroup
getGroupsChangeableByGroup(string $group)
Returns an array of the groups that a particular group can add/remove.
Definition: UserGroupManager.php:1048
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
UserGroupExpiryJob
Definition: UserGroupExpiryJob.php:27
APCOND_EMAILCONFIRMED
const APCOND_EMAILCONFIRMED
Definition: Defines.php:180
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:182
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
MediaWiki\User\UserGroupManager\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: UserGroupManager.php:88
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:268
MediaWiki\User\UserGroupManager\newGroupMembershipFromRow
newGroupMembershipFromRow(\stdClass $row)
Creates a new UserGroupMembership instance from $row.
Definition: UserGroupManager.php:220
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
MediaWiki\User\UserGroupManager\expandChangeableGroupConfig
expandChangeableGroupConfig(array $config, string $group)
Definition: UserGroupManager.php:1025
MediaWiki\User
Definition: ActorCache.php:21
MediaWiki\User\UserGroupManager\getGroupsChangeableBy
getGroupsChangeableBy(Authority $authority)
Returns an array of groups that this $actor can add and remove.
Definition: UserGroupManager.php:1077
MediaWiki\User\UserGroupManager\$groupPermissionsLookup
GroupPermissionsLookup $groupPermissionsLookup
Definition: UserGroupManager.php:94
MediaWiki\User\UserGroupManager\getUserAutopromoteGroups
getUserAutopromoteGroups(UserIdentity $user)
Get the groups for the given user based on $wgAutopromote.
Definition: UserGroupManager.php:392
APCOND_IPINRANGE
const APCOND_IPINRANGE
Definition: Defines.php:183
APCOND_BLOCKED
const APCOND_BLOCKED
Definition: Defines.php:185
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:663
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:30
User\getRegistration
getRegistration()
Get the timestamp of account creation.
Definition: User.php:3468
MediaWiki\User\UserGroupManager\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags(int $queryFlags)
Definition: UserGroupManager.php:1158
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
MediaWiki\User\UserEditTracker
Track info about user edit counts and timings.
Definition: UserEditTracker.php:21
MediaWiki\User\UserGroupManager\recCheckCondition
recCheckCondition( $cond, User $user)
Recursively check a condition.
Definition: UserGroupManager.php:465
MediaWiki\User\UserGroupManager\$queryFlagsUsedForCaching
array $queryFlagsUsedForCaching
An assoc.
Definition: UserGroupManager.php:144
MediaWiki\$config
Config $config
Definition: MediaWiki.php:42
APCOND_AGE_FROM_EDIT
const APCOND_AGE_FROM_EDIT
Definition: Defines.php:184
MediaWiki\User\UserGroupManager\CACHE_EFFECTIVE
const CACHE_EFFECTIVE
string key for effective groups cache
Definition: UserGroupManager.php:112
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:45
MediaWiki\User\UserGroupManager\clearUserCacheForKind
clearUserCacheForKind(UserIdentity $user, string $cacheKind)
Clears a cached group membership and query key for a given user.
Definition: UserGroupManager.php:1148
MediaWiki\User\UserGroupManager\CACHE_MEMBERSHIP
const CACHE_MEMBERSHIP
string key for group memberships cache
Definition: UserGroupManager.php:115
MediaWiki\User\UserGroupManager\$loadBalancerFactory
ILBFactory $loadBalancerFactory
Definition: UserGroupManager.php:76
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:557
MediaWiki\User\UserGroupManager\listAllGroups
listAllGroups()
Return the set of defined explicit groups.
Definition: UserGroupManager.php:192
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:238
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:67
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:938
Sanitizer
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:35
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:35
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:71
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:32
MediaWiki\User\UserGroupManager\$loadBalancer
ILoadBalancer $loadBalancer
Definition: UserGroupManager.php:79