Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LevelingUpHooks
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 6
420
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
 onVisualEditorApiVisualEditorEditPostSave
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 onBeforeCreateEchoEvent
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 onUserGetDefaultOptions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isLevelingUpEnabledForUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace GrowthExperiments\LevelingUp;
4
5use EchoAttributeManager;
6use EchoUserLocator;
7use GrowthExperiments\ExperimentUserManager;
8use GrowthExperiments\HomepageHooks;
9use GrowthExperiments\HomepageModules\SuggestedEdits;
10use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
11use GrowthExperiments\VariantHooks;
12use GrowthExperiments\VisualEditorHooks;
13use MediaWiki\Config\Config;
14use MediaWiki\Extension\VisualEditor\VisualEditorApiVisualEditorEditPostSaveHook;
15use MediaWiki\Hook\BeforePageDisplayHook;
16use MediaWiki\Page\ProperPageIdentity;
17use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
18use MediaWiki\User\UserIdentity;
19
20/**
21 * Hooks for the "leveling up" feature.
22 * @see https://www.mediawiki.org/wiki/Growth/Positive_reinforcement#Leveling_up
23 */
24class LevelingUpHooks implements
25    BeforePageDisplayHook,
26    VisualEditorApiVisualEditorEditPostSaveHook,
27    UserGetDefaultOptionsHook
28{
29
30    private Config $config;
31    private ConfigurationLoader $configurationLoader;
32    private ExperimentUserManager $experimentUserManager;
33    private LevelingUpManager $levelingUpManager;
34
35    /**
36     * @param Config $config
37     * @param ConfigurationLoader $configurationLoader
38     * @param ExperimentUserManager $experimentUserManager
39     * @param LevelingUpManager $levelingUpManager
40     */
41    public function __construct(
42        Config $config,
43        ConfigurationLoader $configurationLoader,
44        ExperimentUserManager $experimentUserManager,
45        LevelingUpManager $levelingUpManager
46    ) {
47        $this->config = $config;
48        $this->configurationLoader = $configurationLoader;
49        $this->experimentUserManager = $experimentUserManager;
50        $this->levelingUpManager = $levelingUpManager;
51    }
52
53    /**
54     * Load the InviteToSuggestedEdits module after a VisualEditor save.
55     * @inheritDoc
56     */
57    public function onVisualEditorApiVisualEditorEditPostSave(
58        ProperPageIdentity $page,
59        UserIdentity $user,
60        string $wikitext,
61        array $params,
62        array $pluginData,
63        array $saveResult,
64        array &$apiResponse
65    ): void {
66        $taskTypes = $this->configurationLoader->getTaskTypes();
67        $pluginFields = array_map(
68            fn ( $taskTypeId ) => VisualEditorHooks::PLUGIN_PREFIX . $taskTypeId,
69            array_keys( $taskTypes )
70        );
71        $isSuggestedEdit = count( array_intersect( $pluginFields, array_keys( $pluginData ) ) ) > 0;
72
73        // Check that the feature is enabled, we are editing an article and the user
74        // just passed the threshold for the invite.
75        // Also check if the current edit is a suggested edit, as a micro-optimisation
76        // (shouldInviteUserAfterNormalEdit() would discard that case anyway).
77        if ( $page->getNamespace() !== NS_MAIN
78            || $isSuggestedEdit
79            || !self::isLevelingUpEnabledForUser( $user, $this->config, $this->experimentUserManager )
80            || !$this->levelingUpManager->shouldInviteUserAfterNormalEdit( $user )
81        ) {
82            return;
83        }
84
85        $apiResponse['modules'][] = 'ext.growthExperiments.LevelingUp.InviteToSuggestedEdits';
86        $apiResponse['jsconfigvars']['wgPostEditConfirmationDisabled'] = true;
87    }
88
89    /**
90     * Load the InviteToSuggestedEdits module when VE reloads the page after save (which it
91     * indicates by the use of the 'venotify' parameter).
92     *
93     * @inheritDoc
94     */
95    public function onBeforePageDisplay( $out, $skin ): void {
96        // VE sets a query parameter, but there is no elegant way to detect post-edit reloads
97        // in the wikitext editor. Check the JS variable that it uses to configure the notice.
98        $isPostEditReload = $out->getRequest()->getCheck( 'venotify' )
99            || $out->getRequest()->getCheck( 'mfnotify' )
100            || ( $out->getJsConfigVars()['wgPostEdit'] ?? false );
101
102        if (
103            // Check that the feature is enabled, we are indeed in the post-edit reload of
104            // an article, and the user just passed the threshold for the invite.
105            !$isPostEditReload
106            || ( !$out->getTitle() || !$out->getTitle()->inNamespace( NS_MAIN ) )
107            || !self::isLevelingUpEnabledForUser( $out->getUser(), $this->config, $this->experimentUserManager )
108            || !$this->levelingUpManager->shouldInviteUserAfterNormalEdit( $out->getUser() )
109        ) {
110            return;
111        }
112
113        $out->addModules( 'ext.growthExperiments.LevelingUp.InviteToSuggestedEdits' );
114        // Disable the default core post-edit notice.
115        $out->addJsConfigVars( 'wgPostEditConfirmationDisabled', true );
116        $out->addJsConfigVars( 'wgGELevelingUpInviteToSuggestedEditsImmediate', true );
117        $out->addJsConfigVars( 'wgCXSectionTranslationRecentEditInvitationSuppressed', true );
118    }
119
120    /**
121     * Add GrowthExperiments events to Echo
122     *
123     * @param array &$notifications array of Echo notifications
124     * @param array &$notificationCategories array of Echo notification categories
125     * @param array &$icons array of icon details
126     */
127    public static function onBeforeCreateEchoEvent(
128        &$notifications, &$notificationCategories, &$icons
129    ) {
130        $notificationCategories['ge-newcomer'] = [
131            'tooltip' => 'echo-pref-tooltip-ge-newcomer',
132        ];
133        $notifications['keep-going'] = [
134            'category' => 'ge-newcomer',
135            'group' => 'positive',
136            'section' => 'message',
137            'canNotifyAgent' => true,
138            'presentation-model' => EchoKeepGoingPresentationModel::class,
139            EchoAttributeManager::ATTR_LOCATORS => [
140                [ EchoUserLocator::class . '::locateEventAgent' ]
141            ]
142        ];
143
144        $icons['growthexperiments-keep-going'] = [
145            'path' => 'GrowthExperiments/images/notifications-keep-going.svg'
146        ];
147
148        $notifications['get-started'] = [
149            'category' => 'ge-newcomer',
150            'group' => 'positive',
151            'section' => 'message',
152            'canNotifyAgent' => true,
153            'presentation-model' => EchoGetStartedPresentationModel::class,
154            EchoAttributeManager::ATTR_LOCATORS => [
155                [ EchoUserLocator::class . '::locateEventAgent' ]
156            ]
157        ];
158
159        $icons['growthexperiments-get-started'] = [
160            'path' => 'GrowthExperiments/images/notifications-get-started.svg'
161        ];
162    }
163
164    /** @inheritDoc */
165    public function onUserGetDefaultOptions( &$defaultOptions ) {
166        $defaultOptions['echo-subscriptions-email-ge-newcomer'] = true;
167        $defaultOptions['echo-subscriptions-web-ge-newcomer'] = true;
168    }
169
170    /**
171     * Whether leveling up features are available. This does not include any checks based on
172     * the user's contribution history, just site configuration and A/B test cohorts.
173     * @param UserIdentity $user
174     * @param Config $config Site configuration.
175     * @param ExperimentUserManager $experimentUserManager
176     * @return bool
177     */
178    public static function isLevelingUpEnabledForUser(
179        UserIdentity $user,
180        Config $config,
181        ExperimentUserManager $experimentUserManager
182    ): bool {
183        // Leveling up should only be shown if
184        // 1) suggested edits are available on this wiki, as we'll direct the user there
185        // 2) the user's homepage is enabled, which maybe SuggestedEdits::isEnabled should
186        //    check, but it doesn't (this also excludes autocreated potentially-experienced
187        //    users who probably shouldn't get invites)
188        // 3) (for now) the wiki is a pilot wiki and the user is in the experiment group
189        return $config->get( 'GELevelingUpFeaturesEnabled' )
190            && SuggestedEdits::isEnabled( $config )
191            && HomepageHooks::isHomepageEnabled( $user )
192            && $experimentUserManager->isUserInVariant( $user, VariantHooks::VARIANT_CONTROL );
193    }
194
195}