MediaWiki master
UserRequirementsConditionChecker.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\User;
8
9use InvalidArgumentException;
10use LogicException;
17
22
28 public const VALID_OPS = [ '&', '|', '^', '!' ];
29
31 public const CONSTRUCTOR_OPTIONS = [
33 ];
34
35 private HookRunner $hookRunner;
36
37 public function __construct(
38 private readonly ServiceOptions $options,
39 HookContainer $hookContainer,
40 private readonly UserFactory $userFactory,
41 private readonly IContextSource $context,
42 private readonly UserRequirementsConditionValidator $userRequirementsConditionValidator,
44 private readonly array $evaluators = [],
45 ) {
46 $this->hookRunner = new HookRunner( $hookContainer );
47 $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
48 }
49
63 protected function checkCondition( array $cond, UserIdentity $user ): ?bool {
64 $isPerformingRequest = !defined( 'MW_NO_SESSION' ) && $user->equals( $this->context->getUser() );
65
66 $conditionType = $cond[0];
67 $args = array_slice( $cond, 1 );
68
69 foreach ( $this->evaluators as $evaluator ) {
70 $result = $evaluator->checkCondition( $conditionType, $args, $user, $isPerformingRequest );
71 if ( $result !== null ) {
72 return $result;
73 }
74 }
75
76 $result = null;
77 $type = $cond[0];
78 $args = array_slice( $cond, 1 );
79 $this->hookRunner->onUserRequirementsCondition( $conditionType, $args, $user, $isPerformingRequest, $result );
80
81 $isCurrentWiki = ( $user->getWikiId() === false ) || WikiMap::isCurrentWikiId( $user->getWikiId() );
82 if ( $isPerformingRequest && $isCurrentWiki ) {
83 // The legacy hook is run only if the tested user is the one performing
84 // the request (like for autopromote), and the user is from the local wiki.
85 // If any of these conditions is not met, we cannot invoke the hook,
86 // as it may produce incorrect results.
87 $userObject = $this->userFactory->newFromUserIdentity( $user );
88 $this->hookRunner->onAutopromoteCondition( $conditionType, $args, $userObject, $result );
89 }
90
91 if ( $result === null ) {
92 throw new InvalidArgumentException(
93 "Unrecognized condition $type in UserRequirementsCondition!"
94 );
95 }
96
97 return (bool)$result;
98 }
99
125 public function recursivelyCheckCondition( $cond, UserIdentity $user, bool $usePrivateConditions = true ): ?bool {
126 if ( !$this->userRequirementsConditionValidator->isValid( $cond ) ) {
127 return false;
128 }
129
130 $skippedConditions = [];
131 if ( !$usePrivateConditions ) {
132 $skippedConditions = $this->options->get( MainConfigNames::UserRequirementsPrivateConditions );
133 $skippedConditions = array_fill_keys( $skippedConditions, true );
134 }
135
136 return $this->recursivelyCheckConditionInternal( $cond, $user, $skippedConditions );
137 }
138
150 private function recursivelyCheckConditionInternal( $cond, UserIdentity $user, array $skippedConditions ): ?bool {
151 if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], self::VALID_OPS ) ) {
152 // Recursive condition
153
154 // AND (all conditions pass)
155 if ( $cond[0] === '&' ) {
156 $hasNulls = false;
157 foreach ( array_slice( $cond, 1 ) as $subcond ) {
158 $result = $this->recursivelyCheckConditionInternal( $subcond, $user, $skippedConditions );
159 if ( $result === false ) {
160 return false;
161 }
162 $hasNulls = $hasNulls || $result === null;
163 }
164
165 return $hasNulls ? null : true;
166 }
167
168 // OR (at least one condition passes)
169 if ( $cond[0] === '|' ) {
170 $hasNulls = false;
171 foreach ( array_slice( $cond, 1 ) as $subcond ) {
172 $result = $this->recursivelyCheckConditionInternal( $subcond, $user, $skippedConditions );
173 if ( $result === true ) {
174 return true;
175 }
176 $hasNulls = $hasNulls || $result === null;
177 }
178
179 return $hasNulls ? null : false;
180 }
181
182 // XOR (exactly one condition passes)
183 if ( $cond[0] === '^' ) {
184 $result1 = $this->recursivelyCheckConditionInternal( $cond[1], $user, $skippedConditions );
185 $result2 = $this->recursivelyCheckConditionInternal( $cond[2], $user, $skippedConditions );
186 if ( $result1 === null || $result2 === null ) {
187 return null;
188 }
189 return $result1 xor $result2;
190 }
191
192 // NOT (no conditions pass)
193 if ( $cond[0] === '!' ) {
194 $hasNulls = false;
195 foreach ( array_slice( $cond, 1 ) as $subcond ) {
196 $result = $this->recursivelyCheckConditionInternal( $subcond, $user, $skippedConditions );
197 if ( $result === true ) {
198 return false;
199 }
200 $hasNulls = $hasNulls || $result === null;
201 }
202
203 return $hasNulls ? null : true;
204 }
205 }
206 // If we got here, the array presumably does not contain other conditions;
207 // it's not recursive. Pass it off to checkCondition.
208 if ( !is_array( $cond ) ) {
209 $cond = [ $cond ];
210 }
211
212 // Ensure the condition makes sense at all
213 if ( count( $cond ) < 1 ) {
214 return false;
215 }
216
217 if ( isset( $skippedConditions[$cond[0]] ) ) {
218 return null;
219 }
220
221 return $this->checkCondition( $cond, $user );
222 }
223
230 public function extractPrivateConditions( $cond ): array {
231 $allPrivateConditions = $this->options->get( MainConfigNames::UserRequirementsPrivateConditions );
232 $allConditionsUsed = $this->extractConditions( $cond );
233 $privateConditionsUsed = array_intersect( $allPrivateConditions, $allConditionsUsed );
234 return array_values( $privateConditionsUsed );
235 }
236
246 public function extractConditions( $cond ): array {
247 $result = $this->extractConditionsInternal( $cond );
248 return array_values( array_unique( $result ) );
249 }
250
257 private function extractConditionsInternal( $cond ): array {
258 if ( $cond === [] ) {
259 return [];
260 }
261
262 $result = [];
263 if ( is_array( $cond ) ) {
264 $op = $cond[0];
265 if ( in_array( $op, self::VALID_OPS ) ) {
266 foreach ( array_slice( $cond, 1 ) as $subcond ) {
267 $result = array_merge(
268 $result, $this->extractConditionsInternal( $subcond ) );
269 }
270 } else {
271 $result[] = $op;
272 }
273 } else {
274 $result[] = $cond;
275 }
276 return $result;
277 }
278}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
A class for passing options to services.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
const UserRequirementsPrivateConditions
Name constant for the UserRequirementsPrivateConditions setting, for use with Config::get()
Create User objects.
__construct(private readonly ServiceOptions $options, HookContainer $hookContainer, private readonly UserFactory $userFactory, private readonly IContextSource $context, private readonly UserRequirementsConditionValidator $userRequirementsConditionValidator, private readonly array UserRequirementsConditionEvaluatorBase[] $evaluators=[],)
const VALID_OPS
Logical operators recognized in $wgAutopromote.
extractPrivateConditions( $cond)
Goes through a condition passed as the input and extracts all private conditions that are used within...
extractConditions( $cond)
Goes through a condition passed as the input and extracts all simple conditions that are used within ...
checkCondition(array $cond, UserIdentity $user)
As recursivelyCheckCondition, but not recursive.
recursivelyCheckCondition( $cond, UserIdentity $user, bool $usePrivateConditions=true)
Recursively check a condition.
A base class for classes that evaluate atomic user requirements conditions (i.e., conditions checked ...
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Interface for objects which can provide a MediaWiki context on request.
getWikiId()
Get the ID of the wiki this page belongs to.
Interface for objects representing user identity.