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