Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
90.77% |
59 / 65 |
|
66.67% |
2 / 3 |
CRAP | |
0.00% |
0 / 1 |
| UserRequirementsConditionEvaluator | |
90.77% |
59 / 65 |
|
66.67% |
2 / 3 |
27.57 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| checkCondition | |
90.48% |
57 / 63 |
|
0.00% |
0 / 1 |
25.54 | |||
| getCacheKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\User; |
| 8 | |
| 9 | use LogicException; |
| 10 | use MediaWiki\Config\ServiceOptions; |
| 11 | use MediaWiki\Context\IContextSource; |
| 12 | use MediaWiki\MainConfigNames; |
| 13 | use MediaWiki\Parser\Sanitizer; |
| 14 | use MediaWiki\Permissions\GroupPermissionsLookup; |
| 15 | use MediaWiki\User\Registration\UserRegistrationLookup; |
| 16 | use MediaWiki\WikiMap\WikiMap; |
| 17 | use Wikimedia\IPUtils; |
| 18 | use Wikimedia\Rdbms\IDBAccessObject; |
| 19 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 20 | |
| 21 | class UserRequirementsConditionEvaluator extends UserRequirementsConditionEvaluatorBase { |
| 22 | |
| 23 | /** @internal For use by UserRequirementsConditionCheckerFactory */ |
| 24 | public const CONSTRUCTOR_OPTIONS = [ |
| 25 | MainConfigNames::AutoConfirmAge, |
| 26 | MainConfigNames::AutoConfirmCount, |
| 27 | MainConfigNames::EmailAuthentication, |
| 28 | ]; |
| 29 | |
| 30 | /** |
| 31 | * @internal For use preventing an infinite loop when checking APCOND_BLOCKED |
| 32 | * @var array An assoc. array mapping the getCacheKey userKey to a bool indicating |
| 33 | * an ongoing condition check. |
| 34 | */ |
| 35 | private array $recursionMap = []; |
| 36 | |
| 37 | public function __construct( |
| 38 | private readonly ServiceOptions $options, |
| 39 | private readonly GroupPermissionsLookup $groupPermissionsLookup, |
| 40 | private readonly UserEditTracker $userEditTracker, |
| 41 | private readonly UserRegistrationLookup $userRegistrationLookup, |
| 42 | private readonly UserFactory $userFactory, |
| 43 | private readonly IContextSource $context, |
| 44 | private readonly UserGroupManager $userGroupManager, |
| 45 | ) { |
| 46 | $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
| 47 | } |
| 48 | |
| 49 | /** @inheritDoc */ |
| 50 | public function checkCondition( |
| 51 | string|int $conditionType, |
| 52 | array $args, |
| 53 | UserIdentity $user, |
| 54 | bool $isPerformingRequest |
| 55 | ): ?bool { |
| 56 | // Some checks depend on hooks or other dynamically-determined state, so we can fetch them only |
| 57 | // for the local wiki and not for remote users. The latter may require API requests to the remote |
| 58 | // wiki, which has not been implemented for now due to performance concerns. |
| 59 | $isCurrentWiki = ( $user->getWikiId() === false ) || WikiMap::isCurrentWikiId( $user->getWikiId() ); |
| 60 | |
| 61 | switch ( $conditionType ) { |
| 62 | case APCOND_EMAILCONFIRMED: |
| 63 | if ( !$isCurrentWiki ) { |
| 64 | return false; |
| 65 | } |
| 66 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 67 | return Sanitizer::validateEmail( $userObject->getEmail() ) && |
| 68 | ( !$this->options->get( MainConfigNames::EmailAuthentication ) || |
| 69 | $userObject->getEmailAuthenticationTimestamp() ); |
| 70 | case APCOND_EDITCOUNT: |
| 71 | $reqEditCount = $args[0] ?? $this->options->get( MainConfigNames::AutoConfirmCount ); |
| 72 | |
| 73 | // T157718: Avoid edit count lookup if the specified edit count is 0 or invalid |
| 74 | if ( $reqEditCount <= 0 ) { |
| 75 | return true; |
| 76 | } |
| 77 | return (int)$this->userEditTracker->getUserEditCount( $user ) >= $reqEditCount; |
| 78 | case APCOND_AGE: |
| 79 | $reqAge = $args[0] ?? $this->options->get( MainConfigNames::AutoConfirmAge ); |
| 80 | if ( $reqAge <= 0 ) { |
| 81 | return true; |
| 82 | } |
| 83 | $registration = $this->userRegistrationLookup->getRegistration( $user ); |
| 84 | $age = time() - (int)wfTimestampOrNull( TS::UNIX, $registration ); |
| 85 | return $age >= $reqAge; |
| 86 | case APCOND_AGE_FROM_EDIT: |
| 87 | $reqAge = $args[0] ?? $this->options->get( MainConfigNames::AutoConfirmAge ); |
| 88 | if ( $reqAge <= 0 ) { |
| 89 | return true; |
| 90 | } |
| 91 | $firstEdit = $this->userEditTracker->getFirstEditTimestamp( $user ); |
| 92 | if ( $firstEdit === false ) { |
| 93 | // If the user has no edits, then they don't meet the requirement. |
| 94 | return false; |
| 95 | } |
| 96 | $age = time() - (int)wfTimestampOrNull( TS::UNIX, $firstEdit ); |
| 97 | return $age >= $reqAge; |
| 98 | case APCOND_INGROUPS: |
| 99 | if ( !$isCurrentWiki ) { |
| 100 | return false; |
| 101 | } |
| 102 | return count( array_intersect( |
| 103 | $args, |
| 104 | $this->userGroupManager->getUserGroups( $user ) |
| 105 | ) ) === count( $args ); |
| 106 | case APCOND_ISIP: |
| 107 | // Since the IPs are not permanently bound to users, the IP conditions can only be checked |
| 108 | // for the requesting user. Otherwise, assume the condition is false. |
| 109 | return $isPerformingRequest && $args[0] === $this->context->getRequest()->getIP(); |
| 110 | case APCOND_IPINRANGE: |
| 111 | return $isPerformingRequest && IPUtils::isInRange( $this->context->getRequest()->getIP(), $args[0] ); |
| 112 | case APCOND_BLOCKED: |
| 113 | if ( !$isCurrentWiki ) { |
| 114 | // This condition is more likely to be used as "! APCOND_BLOCKED", so ensure it can't be bypassed |
| 115 | // when tested from a remote wiki. |
| 116 | return true; |
| 117 | } |
| 118 | // Because checking for ipblock-exempt leads back to here (thus infinite recursion), |
| 119 | // we if we've been here before for this user without having returned a value. |
| 120 | // See T270145 and T349608 |
| 121 | $userKey = $this->getCacheKey( $user ); |
| 122 | if ( $this->recursionMap[$userKey] ?? false ) { |
| 123 | throw new LogicException( |
| 124 | "Unexpected recursion! APCOND_BLOCKED is being checked during" . |
| 125 | " an existing APCOND_BLOCKED check for \"{$user->getName()}\"!" |
| 126 | ); |
| 127 | } |
| 128 | $this->recursionMap[$userKey] = true; |
| 129 | // Setting the second parameter here to true prevents us from getting back here |
| 130 | // during standard MediaWiki core behavior |
| 131 | $userObject = $this->userFactory->newFromUserIdentity( $user ); |
| 132 | $block = $userObject->getBlock( IDBAccessObject::READ_LATEST, true ); |
| 133 | $this->recursionMap[$userKey] = false; |
| 134 | |
| 135 | return (bool)$block?->isSitewide(); |
| 136 | case APCOND_ISBOT: |
| 137 | if ( !$isCurrentWiki ) { |
| 138 | return false; |
| 139 | } |
| 140 | return in_array( 'bot', $this->groupPermissionsLookup |
| 141 | ->getGroupPermissions( |
| 142 | $this->userGroupManager->getUserGroups( $user ) |
| 143 | ) |
| 144 | ); |
| 145 | default: |
| 146 | return null; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /** Gets a unique key for various caches. */ |
| 151 | private function getCacheKey( UserIdentity $user ): string { |
| 152 | // As long as it's for in-memory caches, identifying users by name is fine |
| 153 | return $user->getWikiId() . ':' . $user->getName(); |
| 154 | } |
| 155 | } |