Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
62.16% |
46 / 74 |
|
46.67% |
7 / 15 |
CRAP | |
0.00% |
0 / 1 |
CampaignConfig | |
62.16% |
46 / 74 |
|
46.67% |
7 / 15 |
87.47 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getCampaignTopics | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
getTopicsForCampaign | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTopicsToExcludeForUser | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getTopicsToExcludeForCampaign | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getCampaignPattern | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getCampaignIndexFromCampaignTerm | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
isUserInCampaign | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
shouldSkipImageRecommendationDailyTaskLimitForUser | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
shouldSkipWelcomeSurvey | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
shouldSkipImageRecommendationDailyTaskLimit | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getMessageKey | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getConfigValue | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getSignupPageTemplate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSignupPageTemplateParameters | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks; |
4 | |
5 | use GrowthExperiments\Specials\CampaignBenefitsBlock; |
6 | use GrowthExperiments\VariantHooks; |
7 | use MediaWiki\User\Options\UserOptionsLookup; |
8 | use 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 | */ |
25 | class 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 | } |