27 use InvalidArgumentException;
35 use Psr\Log\LoggerInterface;
42 use Wikimedia\Assert\Assert;
43 use Wikimedia\IPUtils;
61 'AutopromoteOnceLogInRC',
62 'EmailAuthentication',
66 'GroupsRemoveFromSelf',
174 $this->readOnlyMode =
new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
186 return array_values( array_diff(
188 array_keys( $this->options->get(
'GroupPermissions' ) ),
189 array_keys( $this->options->get(
'RevokePermissions' ) )
191 $this->listAllImplicitGroups()
200 return $this->options->get(
'ImplicitGroups' );
233 int $queryFlags = self::READ_NORMAL
235 $membershipGroups = [];
236 reset( $userGroups );
237 foreach ( $userGroups as $row ) {
239 $membershipGroups[ $ugm->getGroup() ] = $ugm;
241 $this->
setCache( $user, self::CACHE_MEMBERSHIP, $membershipGroups, $queryFlags );
257 int $queryFlags = self::READ_NORMAL,
258 bool $recache =
false
262 !isset( $this->userGroupCache[$userKey][self::CACHE_IMPLICIT] ) ||
269 $groups = array_unique( array_merge(
274 $this->
setCache( $user, self::CACHE_IMPLICIT, $groups, $queryFlags );
297 int $queryFlags = self::READ_NORMAL,
298 bool $recache =
false
305 !isset( $this->userGroupCache[$userKey][self::CACHE_EFFECTIVE] )
307 $groups = array_unique( array_merge(
313 if ( $this->hookContainer->isRegistered(
'UserEffectiveGroups' ) ) {
317 $this->hookRunner->onUserEffectiveGroups( $userObj, $groups );
320 $effectiveGroups = array_values( array_unique( $groups ) );
321 $this->
setCache( $user, self::CACHE_EFFECTIVE, $effectiveGroups, $queryFlags );
339 int $queryFlags = self::READ_NORMAL
344 isset( $this->userGroupCache[$userKey][self::CACHE_FORMER] )
356 'user_former_groups',
358 [
'ufg_user' => $user->
getId() ],
362 foreach (
$res as $row ) {
363 $formerGroups[] = $row->ufg_group;
365 $this->
setCache( $user, self::CACHE_FORMER, $formerGroups, $queryFlags );
382 foreach ( $this->options->get(
'Autopromote' ) as $group => $cond ) {
388 $this->hookRunner->onGetAutoPromoteGroups( $userObj, $promote );
408 $autopromoteOnce = $this->options->get(
'AutopromoteOnce' );
411 if ( isset( $autopromoteOnce[$event] ) && count( $autopromoteOnce[$event] ) ) {
416 foreach ( $autopromoteOnce[$event] as $group => $cond ) {
418 if ( in_array( $group, $currentGroups ) ) {
422 if ( in_array( $group, $formerGroups ) ) {
452 $validOps = [
'&',
'|',
'^',
'!' ];
454 if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
456 if ( $cond[0] ==
'&' ) {
457 foreach ( array_slice( $cond, 1 ) as $subcond ) {
464 } elseif ( $cond[0] ==
'|' ) {
465 foreach ( array_slice( $cond, 1 ) as $subcond ) {
472 } elseif ( $cond[0] ==
'^' ) {
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.'
481 } elseif ( $cond[0] ==
'!' ) {
482 foreach ( array_slice( $cond, 1 ) as $subcond ) {
493 if ( !is_array( $cond ) ) {
511 if ( count( $cond ) < 1 ) {
515 switch ( $cond[0] ) {
518 if ( $this->options->get(
'EmailAuthentication' ) ) {
526 $reqEditCount = $cond[1];
529 if ( $reqEditCount <= 0 ) {
532 return $user->
isRegistered() && $this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount;
535 return $age >= $cond[1];
538 TS_UNIX, $this->userEditTracker->getFirstEditTimestamp( $user ) );
539 return $age >= $cond[1];
541 $groups = array_slice( $cond, 1 );
542 return count( array_intersect( $groups, $this->
getUserGroups( $user ) ) ) == count( $groups );
544 return $cond[1] == $user->
getRequest()->getIP();
546 return IPUtils::isInRange( $user->
getRequest()->getIP(), $cond[1] );
550 return in_array(
'bot', $this->groupPermissionsLookup
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!"
562 return (
bool)$result;
585 Assert::precondition(
587 __METHOD__ .
" is not supported for foreign domains: {$this->dbDomain} used"
590 if ( $this->readOnlyMode->isReadOnly() || !$user->
getId() ) {
595 if ( $toPromote === [] ) {
600 if ( !$userObj->checkAndSetTouched() ) {
606 foreach ( $toPromote as $group ) {
609 $newGroups = array_merge( $oldGroups, $toPromote );
614 $this->hookRunner->onUserGroupsChanged(
624 $logEntry->setPerformer( $user );
625 $logEntry->setTarget( $userObj->getUserPage() );
626 $logEntry->setParameters( [
627 '4::oldgroups' => $oldGroups,
628 '5::newgroups' => $newGroups,
630 $logid = $logEntry->insert();
631 if ( $this->options->get(
'AutopromoteOnceLogInRC' ) ) {
632 $logEntry->publish( $logid );
648 int $queryFlags = self::READ_NORMAL
663 int $queryFlags = self::READ_NORMAL
668 isset( $this->userGroupCache[$userKey][self::CACHE_MEMBERSHIP] )
682 $queryInfo[
'tables'],
683 $queryInfo[
'fields'],
684 [
'ug_user' => $user->
getId() ],
691 foreach (
$res as $row ) {
693 if ( !$ugm->isExpired() ) {
694 $ugms[$ugm->getGroup()] = $ugm;
699 $this->
setCache( $user, self::CACHE_MEMBERSHIP, $ugms, $queryFlags );
722 string $expiry =
null,
723 bool $allowUpdate =
false
725 if ( $this->readOnlyMode->isReadOnly() ) {
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.'
742 if ( $this->hookContainer->isRegistered(
'UserAddGroup' ) ) {
745 if ( !$this->hookRunner->onUserAddGroup( $userObj, $group, $expiry ) ) {
751 $dbw = $this->loadBalancer->getConnectionRef(
DB_MASTER, [], $this->dbDomain );
753 $dbw->startAtomic( __METHOD__ );
757 'ug_user' => $user->
getId(),
758 'ug_group' => $group,
759 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) :
null,
765 $affected = $dbw->affectedRows();
770 'ug_user' => $user->
getId(),
773 if ( $allowUpdate ) {
776 ?
"ug_expiry IS NULL OR ug_expiry != {$dbw->addQuotes( $dbw->timestamp( $expiry ) )}"
777 :
'ug_expiry IS NOT NULL';
780 $conds[] =
"ug_expiry < {$dbw->addQuotes( $dbw->timestamp() )}";
784 [
'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) :
null ],
788 $affected = $dbw->affectedRows();
790 $dbw->endAtomic( __METHOD__ );
796 $hasExpiredRow =
$dbr->selectField(
799 [
"ug_expiry < {$dbr->addQuotes( $dbr->timestamp() )}" ],
802 if ( $hasExpiredRow ) {
807 if ( $affected > 0 ) {
809 if ( !$oldUgms[$group]->isExpired() ) {
810 $this->
setCache( $user, self::CACHE_MEMBERSHIP,
811 $oldUgms, self::READ_LATEST );
814 foreach ( $this->clearCacheCallbacks as $callback ) {
833 if ( $this->hookContainer->isRegistered(
'UserRemoveGroup' ) ) {
836 if ( !$this->hookRunner->onUserRemoveGroup( $userObj, $group ) ) {
841 if ( $this->readOnlyMode->isReadOnly() ) {
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.'
854 $dbw = $this->loadBalancer->getConnectionRef(
DB_MASTER, [], $this->dbDomain );
857 [
'ug_user' => $user->
getId(),
'ug_group' => $group ],
861 if ( !$dbw->affectedRows() ) {
866 'user_former_groups',
867 [
'ufg_user' => $user->
getId(),
'ufg_group' => $group ],
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 );
877 foreach ( $this->clearCacheCallbacks as $callback ) {
896 'tables' => [
'user_groups' ],
914 if ( $this->readOnlyMode->isReadOnly() ) {
918 $ticket = $this->loadBalancerFactory->getEmptyTransactionTicket( __METHOD__ );
919 $dbw = $this->loadBalancer->getConnectionRef(
DB_MASTER );
921 $lockKey =
"{$dbw->getDomainID()}:UserGroupManager:purge";
922 $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
923 if ( !$scopedLock ) {
931 $dbw->startAtomic( __METHOD__ );
934 $queryInfo[
'tables'],
935 $queryInfo[
'fields'],
936 [
'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
938 [
'FOR UPDATE',
'LIMIT' => 100 ],
942 if (
$res->numRows() > 0 ) {
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 ],
960 'user_former_groups',
966 $purgedRows +=
$res->numRows();
969 $dbw->endAtomic( __METHOD__ );
971 $this->loadBalancerFactory->commitAndWaitForReplication( __METHOD__, $ticket );
972 }
while (
$res->numRows() > 0 );
982 if ( empty(
$config[$group] ) ) {
984 } elseif (
$config[$group] ===
true ) {
987 } elseif ( is_array(
$config[$group] ) ) {
1006 $this->options->get(
'AddGroups' ), $group
1009 $this->options->get(
'RemoveGroups' ), $group
1012 $this->options->get(
'GroupsAddToSelf' ), $group
1015 $this->options->get(
'GroupsRemoveFromSelf' ), $group
1032 if ( $authority->
isAllowed(
'userrights' ) ) {
1055 foreach ( $actorGroups as $actorGroup ) {
1056 $groups = array_merge_recursive(
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'] );
1074 unset( $this->userGroupCache[$userKey] );
1075 unset( $this->queryFlagsUsedForCaching[$userKey] );
1093 $this->userGroupCache[$userKey][$cacheKind] = $groupValue;
1094 $this->queryFlagsUsedForCaching[$userKey][$cacheKind] = $queryFlags;
1105 unset( $this->userGroupCache[$userKey][$cacheKind] );
1106 unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] );
1115 return $this->loadBalancer->getConnectionRef( $mode, [], $this->dbDomain );
1124 return $user->
isRegistered() ?
"u:{$user->getId()}" :
"anon:{$user->getName()}";
1144 if ( $queryFlags >= self::READ_LOCKING ) {
1148 $queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ??
self::READ_NONE;
1149 return $queryFlagsUsed >= $queryFlags;