Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.00% covered (warning)
88.00%
44 / 50
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConditionalDefaultsLookup
88.00% covered (warning)
88.00%
44 / 50
71.43% covered (warning)
71.43%
5 / 7
22.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasConditionalDefault
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getConditionallyDefaultOptions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getOptionDefaultForUser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 checkConditionsForUser
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getExtraConditions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkConditionForUser
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
11.31
1<?php
2
3namespace MediaWiki\User\Options;
4
5use InvalidArgumentException;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\HookContainer\HookRunner;
8use MediaWiki\MainConfigNames;
9use MediaWiki\User\Registration\UserRegistrationLookup;
10use MediaWiki\User\UserGroupManager;
11use MediaWiki\User\UserIdentity;
12use MediaWiki\User\UserIdentityUtils;
13use Wikimedia\Timestamp\ConvertibleTimestamp;
14use Wikimedia\Timestamp\TimestampFormat as TS;
15
16class ConditionalDefaultsLookup {
17
18    /**
19     * @internal Exposed for ServiceWiring only
20     */
21    public const CONSTRUCTOR_OPTIONS = [
22        MainConfigNames::ConditionalUserOptions,
23    ];
24
25    private HookRunner $hookRunner;
26    private ServiceOptions $options;
27    private UserRegistrationLookup $userRegistrationLookup;
28    private UserIdentityUtils $userIdentityUtils;
29    /**
30     * UserGroupManager must be provided as a callback function to avoid circular dependency
31     * @var callable
32     */
33    private $userGroupManagerCallback;
34    private ?array $extraConditions = null;
35
36    public function __construct(
37        HookRunner $hookRunner,
38        ServiceOptions $options,
39        UserRegistrationLookup $userRegistrationLookup,
40        UserIdentityUtils $userIdentityUtils,
41        callable $userGroupManagerCallback
42    ) {
43        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
44
45        $this->hookRunner = $hookRunner;
46        $this->options = $options;
47        $this->userRegistrationLookup = $userRegistrationLookup;
48        $this->userIdentityUtils = $userIdentityUtils;
49        $this->userGroupManagerCallback = $userGroupManagerCallback;
50    }
51
52    /**
53     * Does the option support conditional defaults?
54     *
55     * @param string $option
56     * @return bool
57     */
58    public function hasConditionalDefault( string $option ): bool {
59        return array_key_exists(
60            $option,
61            $this->options->get( MainConfigNames::ConditionalUserOptions )
62        );
63    }
64
65    /**
66     * Get all conditionally default user options
67     *
68     * @return string[]
69     */
70    public function getConditionallyDefaultOptions(): array {
71        return array_keys(
72            $this->options->get( MainConfigNames::ConditionalUserOptions )
73        );
74    }
75
76    /**
77     * Get the conditional default for user and option
78     *
79     * @param string $optionName
80     * @param UserIdentity $userIdentity
81     * @return mixed|null The default value if set, or null if it cannot be determined
82     * conditionally (default from DefaultOptionsLookup should be used in that case).
83     */
84    public function getOptionDefaultForUser(
85        string $optionName,
86        UserIdentity $userIdentity
87    ) {
88        $conditionalDefaults = $this->options
89            ->get( MainConfigNames::ConditionalUserOptions )[$optionName] ?? [];
90        foreach ( $conditionalDefaults as $conditionalDefault ) {
91            // At the zeroth index of the conditional case, the intended value is found; the rest
92            // of the array are conditions, which are evaluated in checkConditionsForUser().
93            $value = array_shift( $conditionalDefault );
94            if ( $this->checkConditionsForUser( $userIdentity, $conditionalDefault ) ) {
95                return $value;
96            }
97        }
98
99        return null;
100    }
101
102    /**
103     * Are ALL conditions satisfied for the given user?
104     *
105     * @param UserIdentity $userIdentity
106     * @param array $conditions
107     * @return bool
108     */
109    private function checkConditionsForUser( UserIdentity $userIdentity, array $conditions ): bool {
110        foreach ( $conditions as $condition ) {
111            if ( !$this->checkConditionForUser( $userIdentity, $condition ) ) {
112                return false;
113            }
114        }
115        return true;
116    }
117
118    private function getExtraConditions(): array {
119        if ( !$this->extraConditions ) {
120            $this->extraConditions = [];
121            $this->hookRunner->onConditionalDefaultOptionsAddCondition( $this->extraConditions );
122        }
123        return $this->extraConditions;
124    }
125
126    /**
127     * Is ONE condition satisfied for the given user?
128     *
129     * @param UserIdentity $userIdentity
130     * @param array|int $cond Either [ CUDCOND_*, args ] or CUDCOND_*, depending on whether the
131     * condition has any arguments.
132     * @return bool
133     */
134    private function checkConditionForUser(
135        UserIdentity $userIdentity,
136        $cond
137    ): bool {
138        if ( !is_array( $cond ) ) {
139            $cond = [ $cond ];
140        }
141        if ( $cond === [] ) {
142            throw new InvalidArgumentException( 'Empty condition' );
143        }
144        $condName = array_shift( $cond );
145        switch ( $condName ) {
146            case CUDCOND_AFTER:
147                $registration = $this->userRegistrationLookup->getRegistration( $userIdentity );
148                if ( $registration === null || $registration === false ) {
149                    return false;
150                }
151
152                return $registration > ConvertibleTimestamp::convert( TS::MW, $cond[0] );
153            case CUDCOND_ANON:
154                return !$userIdentity->isRegistered();
155            case CUDCOND_NAMED:
156                return $this->userIdentityUtils->isNamed( $userIdentity );
157            case CUDCOND_USERGROUP:
158                $userGroupManagerCallback = $this->userGroupManagerCallback;
159                /** @var UserGroupManager */
160                $userGroupManager = $userGroupManagerCallback();
161                return in_array( $cond[0], $userGroupManager->getUserEffectiveGroups( $userIdentity ) );
162            default:
163                $extraConditions = $this->getExtraConditions();
164                if ( array_key_exists( $condName, $extraConditions ) ) {
165                    return $extraConditions[$condName]( $userIdentity, $cond );
166                }
167                throw new InvalidArgumentException( 'Unsupported condition ' . $condName );
168        }
169    }
170}