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