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  'GroupPermissions',
65  'GroupsAddToSelf',
66  'GroupsRemoveFromSelf',
67  'RevokePermissions',
68  'RemoveGroups',
69  ];
70 
72  private $options;
73 
76 
78  private $loadBalancer;
79 
81  private $hookContainer;
82 
84  private $hookRunner;
85 
87  private $readOnlyMode;
88 
91 
94 
96  private $logger;
97 
100 
102  private $dbDomain;
103 
105  private const CACHE_IMPLICIT = 'implicit';
106 
108  private const CACHE_EFFECTIVE = 'effective';
109 
111  private const CACHE_MEMBERSHIP = 'membership';
112 
114  private const CACHE_FORMER = 'former';
115 
127  private $userGroupCache = [];
128 
141 
153  public function __construct(
155  ConfiguredReadOnlyMode $configuredReadOnlyMode,
160  LoggerInterface $logger,
161  array $clearCacheCallbacks = [],
162  $dbDomain = false
163  ) {
164  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
165  $this->options = $options;
166  $this->loadBalancerFactory = $loadBalancerFactory;
167  $this->loadBalancer = $loadBalancerFactory->getMainLB( $dbDomain );
168  $this->hookContainer = $hookContainer;
169  $this->hookRunner = new HookRunner( $hookContainer );
170  $this->userEditTracker = $userEditTracker;
171  $this->groupPermissionsLookup = $groupPermissionsLookup;
172  $this->logger = $logger;
173  // Can't just inject ROM since we LB can be for foreign wiki
174  $this->readOnlyMode = new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
175  $this->clearCacheCallbacks = $clearCacheCallbacks;
176  $this->dbDomain = $dbDomain;
177  }
178 
185  public function listAllGroups() : array {
186  return array_values( array_diff(
187  array_merge(
188  array_keys( $this->options->get( 'GroupPermissions' ) ),
189  array_keys( $this->options->get( 'RevokePermissions' ) )
190  ),
191  $this->listAllImplicitGroups()
192  ) );
193  }
194 
199  public function listAllImplicitGroups() : array {
200  return $this->options->get( 'ImplicitGroups' );
201  }
202 
212  public function newGroupMembershipFromRow( \stdClass $row ) : UserGroupMembership {
213  return new UserGroupMembership(
214  (int)$row->ug_user,
215  $row->ug_group,
216  $row->ug_expiry === null ? null : wfTimestamp(
217  TS_MW,
218  $row->ug_expiry
219  )
220  );
221  }
222 
231  UserIdentity $user,
232  array $userGroups,
233  int $queryFlags = self::READ_NORMAL
234  ) {
235  $membershipGroups = [];
236  reset( $userGroups );
237  foreach ( $userGroups as $row ) {
238  $ugm = $this->newGroupMembershipFromRow( $row );
239  $membershipGroups[ $ugm->getGroup() ] = $ugm;
240  }
241  $this->setCache( $user, self::CACHE_MEMBERSHIP, $membershipGroups, $queryFlags );
242  }
243 
255  public function getUserImplicitGroups(
256  UserIdentity $user,
257  int $queryFlags = self::READ_NORMAL,
258  bool $recache = false
259  ) : array {
260  $userKey = $this->getCacheKey( $user );
261  if ( $recache ||
262  !isset( $this->userGroupCache[$userKey][self::CACHE_IMPLICIT] ) ||
263  !$this->canUseCachedValues( $user, self::CACHE_IMPLICIT, $queryFlags )
264  ) {
265  $groups = [ '*' ];
266  if ( $user->isRegistered() ) {
267  $groups[] = 'user';
268 
269  $groups = array_unique( array_merge(
270  $groups,
271  $this->getUserAutopromoteGroups( $user )
272  ) );
273  }
274  $this->setCache( $user, self::CACHE_IMPLICIT, $groups, $queryFlags );
275  if ( $recache ) {
276  // Assure data consistency with rights/groups,
277  // as getEffectiveGroups() depends on this function
278  $this->clearUserCacheForKind( $user, self::CACHE_EFFECTIVE );
279  }
280  }
281  return $this->userGroupCache[$userKey][self::CACHE_IMPLICIT];
282  }
283 
295  public function getUserEffectiveGroups(
296  UserIdentity $user,
297  int $queryFlags = self::READ_NORMAL,
298  bool $recache = false
299  ) : array {
300  $userKey = $this->getCacheKey( $user );
301  // Ignore cache if the $recache flag is set, cached values can not be used
302  // or the cache value is missing
303  if ( $recache ||
304  !$this->canUseCachedValues( $user, self::CACHE_EFFECTIVE, $queryFlags ) ||
305  !isset( $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE] )
306  ) {
307  $groups = array_unique( array_merge(
308  $this->getUserGroups( $user, $queryFlags ), // explicit groups
309  $this->getUserImplicitGroups( $user, $queryFlags, $recache ) // implicit groups
310  ) );
311  // TODO: Deprecate passing out user object in the hook by introducing
312  // an alternative hook
313  if ( $this->hookContainer->isRegistered( 'UserEffectiveGroups' ) ) {
314  $userObj = User::newFromIdentity( $user );
315  $userObj->load();
316  // Hook for additional groups
317  $this->hookRunner->onUserEffectiveGroups( $userObj, $groups );
318  }
319  // Force reindexation of groups when a hook has unset one of them
320  $effectiveGroups = array_values( array_unique( $groups ) );
321  $this->setCache( $user, self::CACHE_EFFECTIVE, $effectiveGroups, $queryFlags );
322  }
323  return $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE];
324  }
325 
337  public function getUserFormerGroups(
338  UserIdentity $user,
339  int $queryFlags = self::READ_NORMAL
340  ) : array {
341  $userKey = $this->getCacheKey( $user );
342 
343  if ( $this->canUseCachedValues( $user, self::CACHE_FORMER, $queryFlags ) &&
344  isset( $this->userGroupCache[$userKey][self::CACHE_FORMER] )
345  ) {
346  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
347  }
348 
349  if ( !$user->isRegistered() ) {
350  // Anon users don't have groups stored in the database
351  return [];
352  }
353 
354  $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
355  $res = $db->select(
356  'user_former_groups',
357  [ 'ufg_group' ],
358  [ 'ufg_user' => $user->getId() ],
359  __METHOD__
360  );
361  $formerGroups = [];
362  foreach ( $res as $row ) {
363  $formerGroups[] = $row->ufg_group;
364  }
365  $this->setCache( $user, self::CACHE_FORMER, $formerGroups, $queryFlags );
366 
367  return $this->userGroupCache[$userKey][self::CACHE_FORMER];
368  }
369 
378  public function getUserAutopromoteGroups( UserIdentity $user ) : array {
379  $promote = [];
380  // TODO: remove the need for the full user object
381  $userObj = User::newFromIdentity( $user );
382  foreach ( $this->options->get( 'Autopromote' ) as $group => $cond ) {
383  if ( $this->recCheckCondition( $cond, $userObj ) ) {
384  $promote[] = $group;
385  }
386  }
387 
388  $this->hookRunner->onGetAutoPromoteGroups( $userObj, $promote );
389  return $promote;
390  }
391 
405  UserIdentity $user,
406  string $event
407  ) : array {
408  $autopromoteOnce = $this->options->get( 'AutopromoteOnce' );
409  $promote = [];
410 
411  if ( isset( $autopromoteOnce[$event] ) && count( $autopromoteOnce[$event] ) ) {
412  $currentGroups = $this->getUserGroups( $user );
413  $formerGroups = $this->getUserFormerGroups( $user );
414  // TODO: remove the need for the full user object
415  $userObj = User::newFromIdentity( $user );
416  foreach ( $autopromoteOnce[$event] as $group => $cond ) {
417  // Do not check if the user's already a member
418  if ( in_array( $group, $currentGroups ) ) {
419  continue;
420  }
421  // Do not autopromote if the user has belonged to the group
422  if ( in_array( $group, $formerGroups ) ) {
423  continue;
424  }
425  // Finally - check the conditions
426  if ( $this->recCheckCondition( $cond, $userObj ) ) {
427  $promote[] = $group;
428  }
429  }
430  }
431 
432  return $promote;
433  }
434 
451  private function recCheckCondition( $cond, User $user ) : bool {
452  $validOps = [ '&', '|', '^', '!' ];
453 
454  if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
455  // Recursive condition
456  if ( $cond[0] == '&' ) { // AND (all conds pass)
457  foreach ( array_slice( $cond, 1 ) as $subcond ) {
458  if ( !$this->recCheckCondition( $subcond, $user ) ) {
459  return false;
460  }
461  }
462 
463  return true;
464  } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes)
465  foreach ( array_slice( $cond, 1 ) as $subcond ) {
466  if ( $this->recCheckCondition( $subcond, $user ) ) {
467  return true;
468  }
469  }
470 
471  return false;
472  } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes)
473  if ( count( $cond ) > 3 ) {
474  $this->logger->warning(
475  'recCheckCondition() given XOR ("^") condition on three or more conditions.' .
476  ' Check your $wgAutopromote and $wgAutopromoteOnce settings.'
477  );
478  }
479  return $this->recCheckCondition( $cond[1], $user )
480  xor $this->recCheckCondition( $cond[2], $user );
481  } elseif ( $cond[0] == '!' ) { // NOT (no conds pass)
482  foreach ( array_slice( $cond, 1 ) as $subcond ) {
483  if ( $this->recCheckCondition( $subcond, $user ) ) {
484  return false;
485  }
486  }
487 
488  return true;
489  }
490  }
491  // If we got here, the array presumably does not contain other conditions;
492  // it's not recursive. Pass it off to checkCondition.
493  if ( !is_array( $cond ) ) {
494  $cond = [ $cond ];
495  }
496 
497  return $this->checkCondition( $cond, $user );
498  }
499 
510  private function checkCondition( array $cond, User $user ) : bool {
511  if ( count( $cond ) < 1 ) {
512  return false;
513  }
514 
515  switch ( $cond[0] ) {
517  if ( Sanitizer::validateEmail( $user->getEmail() ) ) {
518  if ( $this->options->get( 'EmailAuthentication' ) ) {
519  return (bool)$user->getEmailAuthenticationTimestamp();
520  } else {
521  return true;
522  }
523  }
524  return false;
525  case APCOND_EDITCOUNT:
526  $reqEditCount = $cond[1];
527 
528  // T157718: Avoid edit count lookup if specified edit count is 0 or invalid
529  if ( $reqEditCount <= 0 ) {
530  return true;
531  }
532  return $user->isRegistered() && $this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount;
533  case APCOND_AGE:
534  $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
535  return $age >= $cond[1];
537  $age = time() - (int)wfTimestampOrNull(
538  TS_UNIX, $this->userEditTracker->getFirstEditTimestamp( $user ) );
539  return $age >= $cond[1];
540  case APCOND_INGROUPS:
541  $groups = array_slice( $cond, 1 );
542  return count( array_intersect( $groups, $this->getUserGroups( $user ) ) ) == count( $groups );
543  case APCOND_ISIP:
544  return $cond[1] == $user->getRequest()->getIP();
545  case APCOND_IPINRANGE:
546  return IPUtils::isInRange( $user->getRequest()->getIP(), $cond[1] );
547  case APCOND_BLOCKED:
548  return $user->getBlock() && $user->getBlock()->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  private function expandChangeableGroupConfig( array $config, string $group ): array {
982  if ( empty( $config[$group] ) ) {
983  return [];
984  } elseif ( $config[$group] === true ) {
985  // You get everything
986  return $this->listAllGroups();
987  } elseif ( is_array( $config[$group] ) ) {
988  return $config[$group];
989  }
990  return [];
991  }
992 
1003  public function getGroupsChangeableByGroup( string $group ): array {
1004  return [
1005  'add' => $this->expandChangeableGroupConfig(
1006  $this->options->get( 'AddGroups' ), $group
1007  ),
1008  'remove' => $this->expandChangeableGroupConfig(
1009  $this->options->get( 'RemoveGroups' ), $group
1010  ),
1011  'add-self' => $this->expandChangeableGroupConfig(
1012  $this->options->get( 'GroupsAddToSelf' ), $group
1013  ),
1014  'remove-self' => $this->expandChangeableGroupConfig(
1015  $this->options->get( 'GroupsRemoveFromSelf' ), $group
1016  ),
1017  ];
1018  }
1019 
1031  public function getGroupsChangeableBy( Authority $authority ): array {
1032  if ( $authority->isAllowed( 'userrights' ) ) {
1033  // This group gives the right to modify everything (reverse-
1034  // compatibility with old "userrights lets you change
1035  // everything")
1036  // Using array_merge to make the groups reindexed
1037  $all = array_merge( $this->listAllGroups() );
1038  return [
1039  'add' => $all,
1040  'remove' => $all,
1041  'add-self' => [],
1042  'remove-self' => []
1043  ];
1044  }
1045 
1046  // Okay, it's not so simple, we will have to go through the arrays
1047  $groups = [
1048  'add' => [],
1049  'remove' => [],
1050  'add-self' => [],
1051  'remove-self' => []
1052  ];
1053  $actorGroups = $this->getUserEffectiveGroups( $authority->getActor() );
1054 
1055  foreach ( $actorGroups as $actorGroup ) {
1056  $groups = array_merge_recursive(
1057  $groups, $this->getGroupsChangeableByGroup( $actorGroup )
1058  );
1059  $groups['add'] = array_unique( $groups['add'] );
1060  $groups['remove'] = array_unique( $groups['remove'] );
1061  $groups['add-self'] = array_unique( $groups['add-self'] );
1062  $groups['remove-self'] = array_unique( $groups['remove-self'] );
1063  }
1064  return $groups;
1065  }
1066 
1072  public function clearCache( UserIdentity $user ) {
1073  $userKey = $this->getCacheKey( $user );
1074  unset( $this->userGroupCache[$userKey] );
1075  unset( $this->queryFlagsUsedForCaching[$userKey] );
1076  }
1077 
1086  private function setCache(
1087  UserIdentity $user,
1088  string $cacheKind,
1089  array $groupValue,
1090  int $queryFlags
1091  ) {
1092  $userKey = $this->getCacheKey( $user );
1093  $this->userGroupCache[$userKey][$cacheKind] = $groupValue;
1094  $this->queryFlagsUsedForCaching[$userKey][$cacheKind] = $queryFlags;
1095  }
1096 
1103  private function clearUserCacheForKind( UserIdentity $user, string $cacheKind ) {
1104  $userKey = $this->getCacheKey( $user );
1105  unset( $this->userGroupCache[$userKey][$cacheKind] );
1106  unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] );
1107  }
1108 
1113  private function getDBConnectionRefForQueryFlags( int $queryFlags ) : DBConnRef {
1114  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1115  return $this->loadBalancer->getConnectionRef( $mode, [], $this->dbDomain );
1116  }
1117 
1123  private function getCacheKey( UserIdentity $user ) : string {
1124  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1125  }
1126 
1134  private function canUseCachedValues(
1135  UserIdentity $user,
1136  string $cacheKind,
1137  int $queryFlags
1138  ) : bool {
1139  if ( !$user->isRegistered() ) {
1140  // Anon users don't have groups stored in the database,
1141  // so $queryFlags are ignored.
1142  return true;
1143  }
1144  if ( $queryFlags >= self::READ_LOCKING ) {
1145  return false;
1146  }
1147  $userKey = $this->getCacheKey( $user );
1148  $queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ?? self::READ_NONE;
1149  return $queryFlagsUsed >= $queryFlags;
1150  }
1151 }
WikiMap\isCurrentWikiDbDomain
static isCurrentWikiDbDomain( $domain)
Definition: WikiMap.php:312
LIST_OR
const LIST_OR
Definition: Defines.php:45
APCOND_AGE
const APCOND_AGE
Definition: Defines.php:189
APCOND_ISBOT
const APCOND_ISBOT
Definition: Defines.php:196
MediaWiki\User\UserGroupManager\checkCondition
checkCondition(array $cond, User $user)
As recCheckCondition, but not recursive.
Definition: UserGroupManager.php:510
MediaWiki\User\UserGroupManager\clearCache
clearCache(UserIdentity $user)
Cleans cached group memberships for a given user.
Definition: UserGroupManager.php:1072
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:114
MediaWiki\User\UserGroupManager\$clearCacheCallbacks
callable[] $clearCacheCallbacks
Definition: UserGroupManager.php:99
MediaWiki\User\UserGroupManager\getCacheKey
getCacheKey(UserIdentity $user)
Gets a unique key for various caches.
Definition: UserGroupManager.php:1123
User\isRegistered
isRegistered()
Get whether the user is registered.
Definition: User.php:2975
MediaWiki\User\UserGroupManager\$userGroupCache
array $userGroupCache
Service caches, an assoc.
Definition: UserGroupManager.php:127
APCOND_INGROUPS
const APCOND_INGROUPS
Definition: Defines.php:191
MediaWiki\Permissions\GroupPermissionsLookup
Definition: GroupPermissionsLookup.php:30
MediaWiki\User\UserGroupManager\$options
ServiceOptions $options
Definition: UserGroupManager.php:72
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:295
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1831
LIST_AND
const LIST_AND
Definition: Defines.php:42
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:337
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:1711
MediaWiki\User\UserGroupManager\$logger
LoggerInterface $logger
Definition: UserGroupManager.php:96
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:629
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:2462
$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
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
User\getRequest
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3081
MediaWiki\User\UserGroupManager\CACHE_IMPLICIT
const CACHE_IMPLICIT
string key for implicit groups cache
Definition: UserGroupManager.php:105
$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:404
MediaWiki\User\UserGroupManager\$dbDomain
string false $dbDomain
Definition: UserGroupManager.php:102
User\getEmail
getEmail()
Get the user's e-mail address.
Definition: User.php:2452
MediaWiki\User\UserGroupManager
Managers user groups.
Definition: UserGroupManager.php:52
MediaWiki\User\UserGroupManager\$userEditTracker
UserEditTracker $userEditTracker
Definition: UserGroupManager.php:90
MediaWiki\User\UserGroupManager\$hookRunner
HookRunner $hookRunner
Definition: UserGroupManager.php:84
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:81
MediaWiki\User\UserGroupManager\listAllImplicitGroups
listAllImplicitGroups()
Get a list of all configured implicit groups.
Definition: UserGroupManager.php:199
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:1847
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:1134
APCOND_EDITCOUNT
const APCOND_EDITCOUNT
Definition: Defines.php:188
MediaWiki\User\UserGroupManager\getGroupsChangeableByGroup
getGroupsChangeableByGroup(string $group)
Returns an array of the groups that a particular group can add/remove.
Definition: UserGroupManager.php:1003
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:153
User\getBlock
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:1898
DB_MASTER
const DB_MASTER
Definition: defines.php:26
UserGroupExpiryJob
Definition: UserGroupExpiryJob.php:27
APCOND_EMAILCONFIRMED
const APCOND_EMAILCONFIRMED
Definition: Defines.php:190
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\Permissions\Authority\getActor
getActor()
Returns the actor associated with this authority.
APCOND_ISIP
const APCOND_ISIP
Definition: Defines.php:192
MediaWiki\Permissions\Authority
@unstable
Definition: Authority.php:30
MediaWiki\User\UserGroupManager\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: UserGroupManager.php:87
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:255
MediaWiki\User\UserGroupManager\newGroupMembershipFromRow
newGroupMembershipFromRow(\stdClass $row)
Creates a new UserGroupMembership instance from $row.
Definition: UserGroupManager.php:212
MediaWiki\User\UserGroupManager\expandChangeableGroupConfig
expandChangeableGroupConfig(array $config, string $group)
Definition: UserGroupManager.php:981
MediaWiki\User
Definition: DefaultOptionsLookup.php:21
MediaWiki\User\UserGroupManager\getGroupsChangeableBy
getGroupsChangeableBy(Authority $authority)
Returns an array of groups that this $actor can add and remove.
Definition: UserGroupManager.php:1031
MediaWiki\User\UserGroupManager\$groupPermissionsLookup
GroupPermissionsLookup $groupPermissionsLookup
Definition: UserGroupManager.php:93
MediaWiki\User\UserGroupManager\getUserAutopromoteGroups
getUserAutopromoteGroups(UserIdentity $user)
Get the groups for the given user based on $wgAutopromote.
Definition: UserGroupManager.php:378
APCOND_IPINRANGE
const APCOND_IPINRANGE
Definition: Defines.php:193
APCOND_BLOCKED
const APCOND_BLOCKED
Definition: Defines.php:195
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:4040
MediaWiki\User\UserIdentity\getId
getId()
MediaWiki\User\UserGroupManager\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags(int $queryFlags)
Definition: UserGroupManager.php:1113
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
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:451
MediaWiki\User\UserGroupManager\$queryFlagsUsedForCaching
array $queryFlagsUsedForCaching
An assoc.
Definition: UserGroupManager.php:140
MediaWiki\$config
Config $config
Definition: MediaWiki.php:42
APCOND_AGE_FROM_EDIT
const APCOND_AGE_FROM_EDIT
Definition: Defines.php:194
MediaWiki\User\UserGroupManager\CACHE_EFFECTIVE
const CACHE_EFFECTIVE
string key for effective groups cache
Definition: UserGroupManager.php:108
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:1103
MediaWiki\User\UserGroupManager\CACHE_MEMBERSHIP
const CACHE_MEMBERSHIP
string key for group memberships cache
Definition: UserGroupManager.php:111
MediaWiki\User\UserGroupManager\$loadBalancerFactory
ILBFactory $loadBalancerFactory
Definition: UserGroupManager.php:75
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:1086
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
MediaWiki\User\UserGroupManager\listAllGroups
listAllGroups()
Return the set of defined explicit groups.
Definition: UserGroupManager.php:185
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:230
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:63
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:78