Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.58% covered (warning)
57.58%
38 / 66
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreferencesHandler
57.58% covered (warning)
57.58%
38 / 66
50.00% covered (danger)
50.00%
3 / 6
116.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onGetPreferences
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 isTruthy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFalsey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 logEvent
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 onSaveUserOptions
57.89% covered (warning)
57.89%
22 / 38
0.00% covered (danger)
0.00%
0 / 1
62.49
1<?php
2
3namespace MediaWiki\IPInfo\HookHandler;
4
5use ExtensionRegistry;
6use MediaWiki\Extension\EventLogging\EventLogging;
7use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketProvider;
8use MediaWiki\IPInfo\Logging\LoggerFactory;
9use MediaWiki\Permissions\PermissionManager;
10use MediaWiki\Preferences\Hook\GetPreferencesHook;
11use MediaWiki\User\Options\UserOptionsLookup;
12use MediaWiki\User\UserGroupManager;
13use MediaWiki\User\UserIdentity;
14
15class PreferencesHandler implements GetPreferencesHook {
16    private PermissionManager $permissionManager;
17
18    private UserOptionsLookup $userOptionsLookup;
19
20    private UserGroupManager $userGroupManager;
21
22    private LoggerFactory $loggerFactory;
23
24    public function __construct(
25        PermissionManager $permissionManager,
26        UserOptionsLookup $userOptionsLookup,
27        UserGroupManager $userGroupManager,
28        LoggerFactory $loggerFactory
29    ) {
30        $this->permissionManager = $permissionManager;
31        $this->userOptionsLookup = $userOptionsLookup;
32        $this->userGroupManager = $userGroupManager;
33        $this->loggerFactory = $loggerFactory;
34    }
35
36    /** @inheritDoc */
37    public function onGetPreferences( $user, &$preferences ): void {
38        if ( !$this->permissionManager->userHasRight( $user, 'ipinfo' ) ) {
39            return;
40        }
41
42        $isBetaFeaturesLoaded = ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' );
43        // If the beta feature isn't enabled, do not show the preference checkbox
44        if ( $isBetaFeaturesLoaded &&
45            !$this->userOptionsLookup->getOption( $user, 'ipinfo-beta-feature-enable' )
46        ) {
47            return;
48        }
49
50        $preferences['ipinfo-use-agreement'] = [
51            'type' => 'toggle',
52            'label-message' => 'ipinfo-preference-use-agreement',
53            'section' => 'personal/ipinfo',
54            'noglobal' => true,
55        ];
56    }
57
58    /**
59     * Utility function to make option checking less verbose.
60     *
61     * @param array $options
62     * @param string $option
63     * @return bool The option is set and truthy
64     */
65    private function isTruthy( $options, $option ): bool {
66        return !empty( $options[$option] );
67    }
68
69    /**
70     * Utility function to make option checking less verbose.
71     * We avoid empty() here because we need the option to be set.
72     *
73     * @param array $options
74     * @param string $option
75     * @return bool The option is set and falsey
76     */
77    private function isFalsey( $options, $option ): bool {
78        return isset( $options[$option] ) && !$options[$option];
79    }
80
81    /**
82     * Send events to the external event server
83     *
84     * @param string $action
85     * @param string $context
86     * @param string $source
87     * @param UserIdentity $user
88     */
89    private function logEvent( $action, $context, $source, $user ) {
90        if ( !ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) {
91            return;
92        }
93        EventLogging::submit( 'mediawiki.ipinfo_interaction', [
94            '$schema' => '/analytics/mediawiki/ipinfo_interaction/1.1.0',
95            'event_action' => $action,
96            'event_context' => $context,
97            'event_source' => $source,
98            'user_edit_bucket' => UserBucketProvider::getUserEditCountBucket( $user ),
99            'user_groups' => implode( '|', $this->userGroupManager->getUserGroups( $user ) )
100        ] );
101    }
102
103    /**
104     * @param UserIdentity $user
105     * @param array &$modifiedOptions
106     * @param array $originalOptions
107     */
108    public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) {
109        $betaFeatureIsEnabled = $this->isTruthy( $originalOptions, 'ipinfo-beta-feature-enable' );
110        $betaFeatureIsDisabled = !$betaFeatureIsEnabled;
111
112        $betaFeatureWillEnable = $this->isTruthy( $modifiedOptions, 'ipinfo-beta-feature-enable' );
113        $betaFeatureWillDisable = $this->isFalsey( $modifiedOptions, 'ipinfo-beta-feature-enable' );
114
115        // If enabling auto-enroll, treat as enabling IPInfo because:
116        // * IPInfo will become enabled
117        // * 'ipinfo-beta-feature-enable' won't be updated before this hook runs
118        // When disabling auto-enroll, do not treat as disabling IPnfo because:
119        // * IPInfo will not necessarily become disabled
120        // * 'ipinfo-beta-feature-enable' will be updated if IPInfo becomes disabled
121        $autoEnrollIsEnabled = $this->isTruthy( $originalOptions, 'betafeatures-auto-enroll' );
122        $autoEnrollIsDisabled = !$autoEnrollIsEnabled;
123        $autoEnrollWillEnable = $this->isTruthy( $modifiedOptions, 'betafeatures-auto-enroll' );
124
125        if (
126            ( $betaFeatureIsEnabled && $betaFeatureWillDisable ) ||
127            ( $betaFeatureIsDisabled && $betaFeatureWillEnable ) ||
128            ( $betaFeatureIsDisabled && $autoEnrollIsDisabled && $autoEnrollWillEnable )
129        ) {
130            // Restore default IPInfo preferences
131            $modifiedOptions[ 'ipinfo-use-agreement' ] = false;
132        }
133
134        // Is IPInfo already enabled?
135        $ipInfoAgreementIsEnabled = $this->isTruthy( $originalOptions, 'ipinfo-use-agreement' );
136        $ipInfoIsEnabled = $betaFeatureIsEnabled && $ipInfoAgreementIsEnabled;
137        $ipInfoIsDisabled = !$ipInfoIsEnabled;
138
139        $ipInfoAgreementWillEnable = $this->isTruthy( $modifiedOptions, 'ipinfo-use-agreement' );
140        $ipInfoAgreementWillDisable = $this->isFalsey( $modifiedOptions, 'ipinfo-use-agreement' );
141        $ipInfoWillEnable = $betaFeatureIsEnabled && !$betaFeatureWillDisable && $ipInfoAgreementWillEnable;
142        $ipInfoWillDisable = $betaFeatureWillDisable || $ipInfoAgreementWillDisable;
143
144        if ( ( !$ipInfoAgreementIsEnabled && $ipInfoAgreementWillEnable ) ||
145            ( $ipInfoAgreementIsEnabled && $ipInfoAgreementWillDisable ) ) {
146            $this->logEvent(
147                (bool)$modifiedOptions['ipinfo-use-agreement'] ? 'accept_disclaimer' : 'uncheck_iagree',
148                'page',
149                'special_preferences',
150                $user
151            );
152        }
153
154        if ( ( $ipInfoIsEnabled && $ipInfoWillDisable ) ||
155            ( $ipInfoIsDisabled && $ipInfoWillEnable )
156        ) {
157            $logger = $this->loggerFactory->getLogger();
158            if ( $ipInfoWillEnable ) {
159                $logger->logAccessEnabled( $user );
160            } else {
161                $logger->logAccessDisabled( $user );
162            }
163
164            $this->logEvent(
165                $ipInfoWillEnable ? 'enable_ipinfo' : 'disable_ipinfo',
166                'page',
167                'special_preferences',
168                $user
169            );
170        }
171    }
172}