Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.16% covered (warning)
62.16%
46 / 74
46.67% covered (danger)
46.67%
7 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
CampaignConfig
62.16% covered (warning)
62.16%
46 / 74
46.67% covered (danger)
46.67%
7 / 15
87.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getCampaignTopics
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getTopicsForCampaign
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTopicsToExcludeForUser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getTopicsToExcludeForCampaign
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getCampaignPattern
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getCampaignIndexFromCampaignTerm
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 isUserInCampaign
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 shouldSkipImageRecommendationDailyTaskLimitForUser
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 shouldSkipWelcomeSurvey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 shouldSkipImageRecommendationDailyTaskLimit
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getMessageKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getConfigValue
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getSignupPageTemplate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSignupPageTemplateParameters
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\NewcomerTasks;
4
5use GrowthExperiments\Specials\CampaignBenefitsBlock;
6use GrowthExperiments\VariantHooks;
7use MediaWiki\User\Options\UserOptionsLookup;
8use MediaWiki\User\UserIdentity;
9
10/**
11 * Wrapper for the GECampaigns PHP / community configuration variable, used to retrieve
12 * campaign-specific information.
13 * The variable should be an array with entries in the format
14 *
15 *   <campaign index> => [
16 *       'pattern' => <regex>,
17 *       // ...other campaign settings
18 *   ]
19 *
20 * The pattern is used to select the relevant entry based on the campaign name (which can be used
21 * in the 'campaign' URL query parameter during registration, and will be recorded in user
22 * preferences). The other settings can be used to select a landing page and modify the behavior
23 * of various signup-related features.
24 */
25class CampaignConfig {
26
27    /** @var array */
28    private $config;
29
30    /** @var array */
31    private $topics;
32
33    /** @var array */
34    private $topicConfig;
35
36    /** @var UserOptionsLookup|null */
37    private $userOptionsLookup;
38
39    /**
40     * @param array $config Campaign config
41     * @param array $topicConfig Mapping between topic ID and its search expression, used in
42     *     PageConfigurationLoader to construct CampaignTopic
43     * @param UserOptionsLookup|null $userOptionsLookup
44     */
45    public function __construct(
46        array $config = [],
47        array $topicConfig = [],
48        ?UserOptionsLookup $userOptionsLookup = null
49    ) {
50        $this->config = $config;
51        $this->topicConfig = $topicConfig;
52        $this->topics = array_unique( array_reduce( $config, static function ( $topics, $campaign ) {
53            $campaignTopics = $campaign[ 'topics' ] ?? [];
54            if ( $campaignTopics ) {
55                array_push( $topics, ...$campaignTopics );
56            }
57            return $topics;
58        }, [] ) );
59        $this->userOptionsLookup = $userOptionsLookup;
60    }
61
62    /**
63     * Get an array of mappings between topic ID and its search expression
64     *
65     * @return array
66     */
67    public function getCampaignTopics(): array {
68        $topicConfig = $this->topicConfig;
69        if ( !count( $topicConfig ) ) {
70            return [];
71        }
72        return array_reduce( $this->topics,
73            static function ( $topics, $topicId ) use ( $topicConfig ) {
74                if ( array_key_exists( $topicId, $topicConfig ) ) {
75                    $topics[] = [
76                        'id' => $topicId,
77                        'searchExpression' => $topicConfig[ $topicId ]
78                    ];
79                }
80                return $topics;
81            }, [] );
82    }
83
84    /**
85     * Get the topic IDs for users in the specified campaign
86     *
87     * @param string $campaign Name of the campaign
88     * @return array
89     */
90    public function getTopicsForCampaign( string $campaign ): array {
91        return $this->config[ $campaign ][ 'topics' ] ?? [];
92    }
93
94    /**
95     * Get the topic IDs to exclude for the user
96     *
97     * @param UserIdentity $user
98     * @return array
99     */
100    public function getTopicsToExcludeForUser( UserIdentity $user ): array {
101        $userCampaign = $this->userOptionsLookup->getOption(
102            $user, VariantHooks::GROWTH_CAMPAIGN
103        );
104        if ( !$userCampaign ) {
105            return $this->topics;
106        }
107        $campaign = $this->getCampaignIndexFromCampaignTerm( $userCampaign );
108        return $this->getTopicsToExcludeForCampaign( $campaign );
109    }
110
111    /**
112     * Get the topic IDs to exclude for the specified campaign
113     *
114     * @param string|null $campaign
115     * @return array
116     */
117    public function getTopicsToExcludeForCampaign( ?string $campaign = null ): array {
118        if ( $campaign && array_key_exists( $campaign, $this->config ) ) {
119            // Make sure topics shared between multiple campaigns aren't excluded
120            return array_diff( $this->topics, $this->getTopicsForCampaign( $campaign ) );
121        }
122        return $this->topics;
123    }
124
125    /**
126     * Get the topic IDs to exclude for the specified campaign
127     *
128     * @param string $campaign
129     * @return ?string
130     */
131    public function getCampaignPattern( string $campaign ): ?string {
132        if ( array_key_exists( $campaign, $this->config ) ) {
133            return $this->config[ $campaign ][ 'pattern' ];
134        }
135        return null;
136    }
137
138    /**
139     * Get the name/index of the campaign matching the "campaign" query parameter value.
140     * In case of multiple matches the first one in the config array takes precedence.
141     * @see VariantHooks::onLocalUserCreated()
142     *
143     * @param string|null $campaignTerm The 'campaign' request parameter used at registration.
144     * @return string|null The campaign name, which is the array key used in $wgGECampaigns.
145     */
146    public function getCampaignIndexFromCampaignTerm( ?string $campaignTerm ): ?string {
147        if ( $campaignTerm === null ) {
148            return null;
149        }
150        $campaigns = array_filter( $this->config, static function ( $campaignConfig ) use ( $campaignTerm ) {
151            return $campaignConfig[ 'pattern' ] && preg_match( $campaignConfig[ 'pattern' ], $campaignTerm );
152        } );
153        return array_key_first( $campaigns );
154    }
155
156    /**
157     * Check whether the user is in the specified campaign
158     *
159     * @param UserIdentity $user
160     * @param string $campaign
161     * @return bool
162     */
163    public function isUserInCampaign( UserIdentity $user, string $campaign ): bool {
164        $campaignPattern = $this->getCampaignPattern( $campaign );
165        if ( $this->userOptionsLookup && $campaignPattern ) {
166            $userCampaign = $this->userOptionsLookup->getOption(
167                $user, VariantHooks::GROWTH_CAMPAIGN
168            );
169            return $userCampaign && preg_match( $campaignPattern, $userCampaign );
170        }
171        return false;
172    }
173
174    /**
175     * Check whether the daily task limit should be skipped for the
176     * specified user
177     *
178     * @param UserIdentity $user
179     * @return bool
180     */
181    public function shouldSkipImageRecommendationDailyTaskLimitForUser( UserIdentity $user ): bool {
182        $userCampaignPattern = $this->userOptionsLookup->getOption(
183            $user, VariantHooks::GROWTH_CAMPAIGN
184        );
185        $userCampaign = $this->getCampaignIndexFromCampaignTerm( $userCampaignPattern );
186        if ( !$userCampaign ) {
187            return false;
188        }
189        return $this->shouldSkipImageRecommendationDailyTaskLimit( $userCampaign );
190    }
191
192    /**
193     * Check whether the welcome survey should be skipped for the
194     * specified campaign
195     *
196     * @param string $campaignTerm
197     * @return bool
198     */
199    public function shouldSkipWelcomeSurvey( string $campaignTerm ): bool {
200        $campaign = $this->getCampaignIndexFromCampaignTerm( $campaignTerm );
201        return (bool)$this->getConfigValue( $campaign, 'skipWelcomeSurvey' );
202    }
203
204    /**
205     * Check whether the daily image recommendation task limit should be skipped for the
206     * specified campaign
207     *
208     * @param string $campaign
209     * @return bool
210     */
211    public function shouldSkipImageRecommendationDailyTaskLimit( string $campaign ): bool {
212        $qualityGateIdsToSkip = $this->getConfigValue( $campaign, 'qualityGateIdsToSkip' );
213        if ( !$qualityGateIdsToSkip ) {
214            return false;
215        }
216        if ( array_key_exists( 'image-recommendation', $qualityGateIdsToSkip ) ) {
217            return in_array( 'dailyLimit', $qualityGateIdsToSkip[ 'image-recommendation' ] );
218        }
219        return false;
220    }
221
222    /**
223     * Get the message key value for the given campaign term
224     *
225     * @param string $campaignTerm
226     * @return string
227     */
228    public function getMessageKey( string $campaignTerm ): string {
229        $defaultMessageKey = 'signupcampaign';
230
231        $campaign = $this->getCampaignIndexFromCampaignTerm( $campaignTerm );
232        if ( !$campaign ) {
233            return $defaultMessageKey;
234        }
235
236        return $this->getConfigValue( $campaign, 'messageKey' ) ?? $defaultMessageKey;
237    }
238
239    /**
240     * Get the value of the specified configuration index
241     *
242     * @param string|null $campaign
243     * @param string $campaignConfigurationIndex
244     * @return mixed|null
245     */
246    private function getConfigValue( ?string $campaign, string $campaignConfigurationIndex ) {
247        if ( $campaign === null ) {
248            return null;
249        }
250        return $this->config[ $campaign ][ $campaignConfigurationIndex ] ?? null;
251    }
252
253    /**
254     * Get the ID of a custom template to use for the signup page.
255     * @param string $campaign
256     * @return string|null
257     * @see CampaignBenefitsBlock::getCampaignTemplateHtml()
258     */
259    public function getSignupPageTemplate( string $campaign ): ?string {
260        return $this->getConfigValue( $campaign, 'signupPageTemplate' );
261    }
262
263    /**
264     * Get the parameters to pass to the template from getSignupPageTemplate()
265     * @param string $campaign
266     * @return array
267     */
268    public function getSignupPageTemplateParameters( string $campaign ): array {
269        return $this->getConfigValue( $campaign,
270            'signupPageTemplateParameters' ) ?? [];
271    }
272
273}