Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
42.86% covered (danger)
42.86%
3 / 7
CRAP
63.51% covered (warning)
63.51%
47 / 74
PreferencesHandler
0.00% covered (danger)
0.00%
0 / 1
42.86% covered (danger)
42.86%
3 / 7
132.81
63.51% covered (warning)
63.51%
47 / 74
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 9
 checkAllIPInfoAgreements
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 8
 isTruthy
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 isFalsey
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 logEvent
0.00% covered (danger)
0.00%
0 / 1
3.54
27.27% covered (danger)
27.27%
3 / 11
 onSaveUserOptions
0.00% covered (danger)
0.00%
0 / 1
28.11
94.87% covered (success)
94.87%
37 / 39
<?php
namespace MediaWiki\IPInfo\HookHandler;
use ExtensionRegistry;
use MediaWiki\Extension\EventLogging\EventLogging;
use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketProvider;
use MediaWiki\IPInfo\Logging\LoggerFactory;
use Mediawiki\Permissions\PermissionManager;
use MediaWiki\Preferences\Hook\GetPreferencesHook;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserOptionsLookup;
class PreferencesHandler implements GetPreferencesHook {
    /** @var PermissionManager */
    private $permissionManager;
    /** @var UserOptionsLookup */
    private $userOptionsLookup;
    /** @var UserGroupManager */
    private $userGroupManager;
    /** @var LoggerFactory */
    private $loggerFactory;
    /**
     * @param PermissionManager $permissionManager
     * @param UserOptionsLookup $userOptionsLookup
     * @param UserGroupManager $userGroupManager
     * @param LoggerFactory $loggerFactory
     */
    public function __construct(
        PermissionManager $permissionManager,
        UserOptionsLookup $userOptionsLookup,
        UserGroupManager $userGroupManager,
        LoggerFactory $loggerFactory
    ) {
        $this->permissionManager = $permissionManager;
        $this->userOptionsLookup = $userOptionsLookup;
        $this->userGroupManager = $userGroupManager;
        $this->loggerFactory = $loggerFactory;
    }
    /**
     * @inheritDoc
     */
    public function onGetPreferences( $user, &$preferences ): void {
        if ( !$this->permissionManager->userHasRight( $user, 'ipinfo' ) ) {
            return;
        }
        $isBetaFeaturesLoaded = ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' );
        // If the betafeature isn't enabled, do not show preferences checkboxes
        if ( $isBetaFeaturesLoaded &&
            !$this->userOptionsLookup->getOption( $user, 'ipinfo-beta-feature-enable' ) ) {
            return;
        }
        $preferences['ipinfo-enable'] = [
            'type' => 'toggle',
            'label-message' => 'ipinfo-preference-enable',
            'section' => 'personal/ipinfo',
            'noglobal' => true,
        ];
        $preferences['ipinfo-use-agreement'] = [
            'type' => 'toggle',
            'label-message' => 'ipinfo-preference-use-agreement',
            'section' => 'personal/ipinfo',
            'validation-callback' => [
                __CLASS__,
                'checkAllIPInfoAgreements'
            ],
            'disable-if' => [ '!==', 'ipinfo-enable', '1' ],
            'noglobal' => true,
        ];
    }
    /**
     * Check pre-requisite ipinfo-preference-enable is checked
     * if ipinfo-preference-use-agreement is checked
     *
     * @param string|null $value Value of ipinfo-preference-use-agreement
     * @param array $allData All form data
     * @return bool|string|null true on success, string on error
     */
    public static function checkAllIPInfoAgreements( ?string $value, array $allData ) {
        if ( $value === null ) {
            // Return true because form is still setting default values
            // so there's nothing to check against yet
            return true;
        }
        // If ipinfo-preference-use-agreement isn't checked, no need to validate
        if ( !$value ) {
            return true;
        }
        // If it is, check that ipinfo-enable is also checked
        $ipInfoEnable = $allData['ipinfo-enable'];
        if ( !$ipInfoEnable ) {
            return wfMessage( 'ipinfo-preference-agreement-error' );
        }
        // Both are checked
        return true;
    }
    /**
     * Utility function to make option checking less verbose.
     *
     * @param array $options
     * @param string $option
     * @return bool The option is set and truthy
     */
    private function isTruthy( $options, $option ): bool {
        return !empty( $options[$option] );
    }
    /**
     * Utility function to make option checking less verbose.
     * We avoid empty() here because we need the option to be set.
     *
     * @param array $options
     * @param string $option
     * @return bool The option is set and falsey
     */
    private function isFalsey( $options, $option ): bool {
        return isset( $options[$option] ) && !$options[$option];
    }
    /**
     * Send events to the external event server
     *
     * @param string $action
     * @param string $context
     * @param string $source
     * @param UserIdentity $user
     */
    private function logEvent( $action, $context, $source, $user ) {
        $eventLoggingLoaded = ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' );
        if ( !$eventLoggingLoaded ) {
            return;
        }
        EventLogging::submit( 'mediawiki.ipinfo_interaction', [
            '$schema' => '/analytics/mediawiki/ipinfo_interaction/1.1.0',
            'event_action' => $action,
            'event_context' => $context,
            'event_source' => $source,
            'user_edit_bucket' => UserBucketProvider::getUserEditCountBucket( $user ),
            'user_groups' => implode( '|', $this->userGroupManager->getUserGroups( $user ) )
        ] );
    }
    /**
     * @param UserIdentity $user
     * @param array &$modifiedOptions
     * @param array $originalOptions
     */
    public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) {
        $betaFeatureIsEnabled = $this->isTruthy( $originalOptions, 'ipinfo-beta-feature-enable' );
        $betaFeatureIsDisabled = !$betaFeatureIsEnabled;
        $betaFeatureWillEnable = $this->isTruthy( $modifiedOptions, 'ipinfo-beta-feature-enable' );
        $betaFeatureWillDisable = $this->isFalsey( $modifiedOptions, 'ipinfo-beta-feature-enable' );
        // If enabling auto-enroll, treat as enabling IPInfo because:
        // * IPInfo will become enabled
        // * 'ipinfo-beta-feature-enable' won't be updated before this hook runs
        // When disabling auto-enroll, do not treat as disabling IPnfo because:
        // * IPInfo will not necessarily become disabled
        // * 'ipinfo-beta-feature-enable' will be updated if IPInfo becomes disabled
        $autoEnrollIsEnabled = $this->isTruthy( $originalOptions, 'betafeatures-auto-enroll' );
        $autoEnrollIsDisabled = !$autoEnrollIsEnabled;
        $autoEnrollWillEnable = $this->isTruthy( $modifiedOptions, 'betafeatures-auto-enroll' );
        if (
            $betaFeatureIsEnabled && $betaFeatureWillDisable ||
            $betaFeatureIsDisabled && $betaFeatureWillEnable ||
            $betaFeatureIsDisabled && $autoEnrollIsDisabled && $autoEnrollWillEnable
        ) {
            // Restore default IPInfo preferences
            $modifiedOptions[ 'ipinfo-enable' ] = true;
            $modifiedOptions[ 'ipinfo-use-agreement' ] = false;
        }
        // Is IPInfo already enabled?
        $ipInfoBasicIsEnabled = $this->isTruthy( $originalOptions, 'ipinfo-enable' );
        $ipInfoAgreementIsEnabled = $this->isTruthy( $originalOptions, 'ipinfo-use-agreement' );
        $ipInfoIsEnabled = $ipInfoBasicIsEnabled && $ipInfoAgreementIsEnabled;
        $ipInfoIsDisabled = !$ipInfoIsEnabled;
        // Is IPInfo about to be enabled/disabled?
        $ipInfoBasicWillEnable = $this->isTruthy( $modifiedOptions, 'ipinfo-enable' );
        $ipInfoBasicWillDisable = $this->isFalsey( $modifiedOptions, 'ipinfo-enable' );
        $ipInfoAgreementWillEnable = $this->isTruthy( $modifiedOptions, 'ipinfo-use-agreement' );
        $ipInfoAgreementWillDisable = $this->isFalsey( $modifiedOptions, 'ipinfo-use-agreement' );
        $ipInfoWillEnable = $ipInfoBasicWillEnable && $ipInfoAgreementWillEnable ||
            $ipInfoBasicIsEnabled && !$ipInfoBasicWillDisable && $ipInfoAgreementWillEnable ||
            $ipInfoAgreementIsEnabled && !$ipInfoAgreementWillDisable && $ipInfoBasicWillEnable;
        $ipInfoWillDisable = $ipInfoBasicWillDisable || $ipInfoAgreementWillDisable;
        if ( ( !$ipInfoAgreementIsEnabled && $ipInfoAgreementWillEnable ) ||
            ( $ipInfoAgreementIsEnabled && $ipInfoAgreementWillDisable ) ) {
            $this->logEvent(
                (bool)$modifiedOptions['ipinfo-use-agreement'] ? 'accept_disclaimer' : 'uncheck_iagree',
                'page',
                'special_preferences',
                $user
            );
        }
        if ( $ipInfoIsEnabled && $ipInfoWillDisable ||
            $ipInfoIsDisabled && $ipInfoWillEnable
        ) {
            $logger = $this->loggerFactory->getLogger();
            if ( $ipInfoWillEnable ) {
                $logger->logAccessEnabled( $user );
            } else {
                $logger->logAccessDisabled( $user );
            }
            $this->logEvent(
                $ipInfoWillEnable ? 'enable_ipinfo' : 'disable_ipinfo',
                'page',
                'special_preferences',
                $user
            );
        }
    }
}