Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
8.81% |
14 / 159 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
MentorHooks | |
8.81% |
14 / 159 |
|
0.00% |
0 / 11 |
808.63 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
onBeforeCreateEchoEvent | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
2 | |||
handleForceMentor | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
onLocalUserCreated | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
onAuthChangeFormFields | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
onPageSaveComplete | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
4.25 | |||
onListDefinedTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onChangeTagsListActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onFormatAutocomments | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
onUserGetRights | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
onBeforePageDisplay | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Mentorship\Hooks; |
4 | |
5 | use EchoAttributeManager; |
6 | use EchoUserLocator; |
7 | use GenderCache; |
8 | use GrowthExperiments\MentorDashboard\PersonalizedPraise\EchoNewPraiseworthyMenteesPresentationModel; |
9 | use GrowthExperiments\Mentorship\EchoMenteeClaimPresentationModel; |
10 | use GrowthExperiments\Mentorship\EchoMentorChangePresentationModel; |
11 | use GrowthExperiments\Mentorship\MentorManager; |
12 | use GrowthExperiments\Mentorship\Provider\MentorProvider; |
13 | use GrowthExperiments\Mentorship\Provider\StructuredMentorWriter; |
14 | use GrowthExperiments\Mentorship\Store\MentorStore; |
15 | use GrowthExperiments\Util; |
16 | use MediaWiki\Auth\Hook\LocalUserCreatedHook; |
17 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
18 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
19 | use MediaWiki\Config\Config; |
20 | use MediaWiki\Deferred\DeferredUpdates; |
21 | use MediaWiki\Hook\BeforePageDisplayHook; |
22 | use MediaWiki\Hook\FormatAutocommentsHook; |
23 | use MediaWiki\Permissions\Hook\UserGetRightsHook; |
24 | use MediaWiki\SpecialPage\Hook\AuthChangeFormFieldsHook; |
25 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
26 | use MediaWiki\User\UserIdentity; |
27 | use MediaWiki\User\UserIdentityLookup; |
28 | use Psr\Log\LogLevel; |
29 | use RequestContext; |
30 | use Throwable; |
31 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
32 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
33 | |
34 | class MentorHooks implements |
35 | LocalUserCreatedHook, |
36 | AuthChangeFormFieldsHook, |
37 | PageSaveCompleteHook, |
38 | ListDefinedTagsHook, |
39 | ChangeTagsListActiveHook, |
40 | FormatAutocommentsHook, |
41 | UserGetRightsHook, |
42 | BeforePageDisplayHook |
43 | { |
44 | |
45 | private Config $config; |
46 | private Config $wikiConfig; |
47 | private UserIdentityLookup $userIdentityLookup; |
48 | private GenderCache $genderCache; |
49 | private MentorManager $mentorManager; |
50 | private MentorProvider $mentorProvider; |
51 | private MentorStore $mentorStore; |
52 | |
53 | /** |
54 | * @param Config $config |
55 | * @param Config $wikiConfig |
56 | * @param UserIdentityLookup $userIdentityLookup |
57 | * @param GenderCache $genderCache |
58 | * @param MentorManager $mentorManager |
59 | * @param MentorProvider $mentorProvider |
60 | * @param MentorStore $mentorStore |
61 | */ |
62 | public function __construct( |
63 | Config $config, |
64 | Config $wikiConfig, |
65 | UserIdentityLookup $userIdentityLookup, |
66 | GenderCache $genderCache, |
67 | MentorManager $mentorManager, |
68 | MentorProvider $mentorProvider, |
69 | MentorStore $mentorStore |
70 | ) { |
71 | $this->config = $config; |
72 | $this->wikiConfig = $wikiConfig; |
73 | $this->userIdentityLookup = $userIdentityLookup; |
74 | $this->genderCache = $genderCache; |
75 | $this->mentorManager = $mentorManager; |
76 | $this->mentorProvider = $mentorProvider; |
77 | $this->mentorStore = $mentorStore; |
78 | } |
79 | |
80 | /** |
81 | * Add Mentorship events to Echo |
82 | * |
83 | * @param array &$notifications array of Echo notifications |
84 | * @param array &$notificationCategories array of Echo notification categories |
85 | * @param array &$icons array of icon details |
86 | */ |
87 | public static function onBeforeCreateEchoEvent( |
88 | &$notifications, &$notificationCategories, &$icons |
89 | ) { |
90 | $notificationCategories['ge-mentorship'] = [ |
91 | 'tooltip' => 'echo-pref-tooltip-ge-mentorship', |
92 | ]; |
93 | |
94 | $notifications['mentor-changed'] = [ |
95 | 'category' => 'system', |
96 | 'group' => 'positive', |
97 | 'section' => 'alert', |
98 | 'presentation-model' => EchoMentorChangePresentationModel::class, |
99 | EchoAttributeManager::ATTR_LOCATORS => [ |
100 | [ |
101 | [ EchoUserLocator::class, 'locateFromEventExtra' ], |
102 | [ 'mentee' ] |
103 | ], |
104 | ], |
105 | ]; |
106 | $notifications['mentee-claimed'] = [ |
107 | 'category' => 'ge-mentorship', |
108 | 'group' => 'positive', |
109 | 'section' => 'message', |
110 | 'presentation-model' => EchoMenteeClaimPresentationModel::class, |
111 | EchoAttributeManager::ATTR_LOCATORS => [ |
112 | [ |
113 | [ EchoUserLocator::class, 'locateFromEventExtra' ], |
114 | [ 'mentor' ] |
115 | ] |
116 | ] |
117 | ]; |
118 | $notifications['new-praiseworthy-mentees'] = [ |
119 | 'category' => 'ge-mentorship', |
120 | 'group' => 'positive', |
121 | 'section' => 'message', |
122 | 'canNotifyAgent' => true, |
123 | 'presentation-model' => EchoNewPraiseworthyMenteesPresentationModel::class, |
124 | EchoAttributeManager::ATTR_LOCATORS => [ |
125 | EchoUserLocator::class . '::locateEventAgent' |
126 | ] |
127 | ]; |
128 | |
129 | $icons['growthexperiments-mentor'] = [ |
130 | 'path' => [ |
131 | 'ltr' => 'GrowthExperiments/images/mentor-ltr.svg', |
132 | 'rtl' => 'GrowthExperiments/images/mentor-rtl.svg' |
133 | ] |
134 | ]; |
135 | // T332732: In he, the mentor icon should be displayed in LTR |
136 | $icons['growthexperiments-mentor-ltr'] = [ |
137 | 'path' => 'GrowthExperiments/images/mentor-ltr.svg' |
138 | ]; |
139 | } |
140 | |
141 | /** |
142 | * Handles `forceMentor` parameter, if present |
143 | * |
144 | * This method checks forceMentor query parameter. If it is present, it: |
145 | * |
146 | * 1) gets one or more username from it (| is used as the delimiter) |
147 | * 2) remove all non-mentors from the lists (determined via MentorProvider::isMentor) |
148 | * 3) assigns a random mentor from the list to $user |
149 | * 4) generates a random backup mentor (who may or may not be in the list) |
150 | * |
151 | * If no forceMentor parameter is provided (or if it does not contain mentors' usernames), |
152 | * the method short-circuits and returns false. |
153 | * |
154 | * @param UserIdentity $user Newly created user |
155 | * @return bool returns true if a mentor was assigned to the user (if false is returned, |
156 | * the caller is responsible for assigning a mentor to the user) |
157 | */ |
158 | private function handleForceMentor( UserIdentity $user ): bool { |
159 | $forceMentorRaw = RequestContext::getMain()->getRequest() |
160 | ->getVal( 'forceMentor', '' ); |
161 | if ( $forceMentorRaw === '' ) { |
162 | return false; |
163 | } |
164 | |
165 | $forceMentorNames = explode( '|', $forceMentorRaw ); |
166 | $forceMentors = array_filter( array_map( |
167 | function ( $username ) { |
168 | $user = $this->userIdentityLookup->getUserIdentityByName( $username ); |
169 | if ( !$user ) { |
170 | return null; |
171 | } |
172 | if ( !$this->mentorProvider->isMentor( $user ) ) { |
173 | return null; |
174 | } |
175 | return $user; |
176 | }, |
177 | $forceMentorNames |
178 | ) ); |
179 | |
180 | if ( $forceMentors ) { |
181 | $forcedPrimaryMentor = $forceMentors[ array_rand( $forceMentors ) ]; |
182 | |
183 | $this->mentorStore->setMentorForUser( |
184 | $user, |
185 | $forcedPrimaryMentor, |
186 | MentorStore::ROLE_PRIMARY |
187 | ); |
188 | // Select a random backup mentor |
189 | $this->mentorManager->getMentorForUser( $user, MentorStore::ROLE_BACKUP ); |
190 | return true; |
191 | } |
192 | return false; |
193 | } |
194 | |
195 | /** @inheritDoc */ |
196 | public function onLocalUserCreated( $user, $autocreated ) { |
197 | if ( $autocreated || $user->isTemp() ) { |
198 | // Excluding autocreated users is necessary, see T276720 |
199 | return; |
200 | } |
201 | if ( $this->wikiConfig->get( 'GEMentorshipEnabled' ) ) { |
202 | try { |
203 | if ( $this->handleForceMentor( $user ) ) { |
204 | return; |
205 | } |
206 | |
207 | // Select a primary & backup mentor. FIXME Not really necessary, but avoids a |
208 | // change in functionality after introducing MentorManager, making debugging easier. |
209 | $this->mentorManager->getMentorForUser( $user, MentorStore::ROLE_PRIMARY ); |
210 | $this->mentorManager->getMentorForUser( $user, MentorStore::ROLE_BACKUP ); |
211 | } catch ( Throwable $throwable ) { |
212 | Util::logException( $throwable, [ |
213 | 'user' => $user->getId(), |
214 | 'impact' => 'Failed to assign mentor for user', |
215 | 'origin' => __METHOD__, |
216 | ], LogLevel::INFO ); |
217 | } |
218 | } |
219 | } |
220 | |
221 | /** |
222 | * Pass through the query parameter used by LocalUserCreated. |
223 | * @inheritDoc |
224 | */ |
225 | public function onAuthChangeFormFields( $requests, $fieldInfo, &$formDescriptor, $action ) { |
226 | $forceMentor = RequestContext::getMain()->getRequest() |
227 | ->getVal( 'forceMentor', '' ); |
228 | if ( $forceMentor !== null ) { |
229 | $formDescriptor['forceMentor'] = [ |
230 | 'type' => 'hidden', |
231 | 'name' => 'forceMentor', |
232 | 'default' => $forceMentor, |
233 | ]; |
234 | } |
235 | } |
236 | |
237 | /** |
238 | * @inheritDoc |
239 | */ |
240 | public function onPageSaveComplete( |
241 | $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult |
242 | ) { |
243 | DeferredUpdates::addCallableUpdate( function () use ( $wikiPage ) { |
244 | $title = $wikiPage->getTitle(); |
245 | |
246 | $sourceTitles = $this->mentorProvider->getSourceTitles(); |
247 | foreach ( $sourceTitles as $sourceTitle ) { |
248 | if ( $sourceTitle->equals( $title ) ) { |
249 | $this->mentorProvider->invalidateCache(); |
250 | break; |
251 | } |
252 | } |
253 | } ); |
254 | DeferredUpdates::addCallableUpdate( function () use ( $user ) { |
255 | if ( $this->mentorStore->isMentee( $user ) ) { |
256 | $this->mentorStore->markMenteeAsActive( $user ); |
257 | } |
258 | } ); |
259 | } |
260 | |
261 | /** |
262 | * @inheritDoc |
263 | */ |
264 | public function onListDefinedTags( &$tags ) { |
265 | $tags[] = StructuredMentorWriter::CHANGE_TAG; |
266 | } |
267 | |
268 | /** |
269 | * @inheritDoc |
270 | */ |
271 | public function onChangeTagsListActive( &$tags ) { |
272 | $tags[] = StructuredMentorWriter::CHANGE_TAG; |
273 | } |
274 | |
275 | /** |
276 | * @inheritDoc |
277 | */ |
278 | public function onFormatAutocomments( &$comment, $pre, $auto, $post, $title, $local, $wikiId ) { |
279 | // NOTE: this message is no longer used, but parsing support needs to be kept to support |
280 | // older revisions. |
281 | $noParamMessageKeys = [ |
282 | 'growthexperiments-mentorship-enrollasmentor-summary', |
283 | ]; |
284 | if ( in_array( $auto, $noParamMessageKeys ) ) { |
285 | $comment = wfMessage( $auto )->text(); |
286 | } |
287 | |
288 | $mentorChangeMessageKeys = [ |
289 | 'growthexperiments-manage-mentors-summary-add-admin-no-reason', |
290 | 'growthexperiments-manage-mentors-summary-add-admin-with-reason', |
291 | 'growthexperiments-manage-mentors-summary-add-self-no-reason', |
292 | 'growthexperiments-manage-mentors-summary-add-self-with-reason', |
293 | 'growthexperiments-manage-mentors-summary-change-admin-no-reason', |
294 | 'growthexperiments-manage-mentors-summary-change-admin-with-reason', |
295 | 'growthexperiments-manage-mentors-summary-change-self-no-reason', |
296 | 'growthexperiments-manage-mentors-summary-change-self-with-reason', |
297 | 'growthexperiments-manage-mentors-summary-remove-admin-no-reason', |
298 | 'growthexperiments-manage-mentors-summary-remove-admin-with-reason', |
299 | 'growthexperiments-manage-mentors-summary-remove-self-no-reason', |
300 | 'growthexperiments-manage-mentors-summary-remove-self-with-reason', |
301 | ]; |
302 | |
303 | $messageParts = explode( ':', $auto, 2 ); |
304 | $messageKey = $messageParts[0]; |
305 | if ( in_array( $messageKey, $mentorChangeMessageKeys ) ) { |
306 | $comment = wfMessage( $messageKey ) |
307 | ->params( ...explode( '|', $messageParts[1] ) ) |
308 | ->inContentLanguage() |
309 | ->parse(); |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * @inheritDoc |
315 | */ |
316 | public function onUserGetRights( $user, &$rights ) { |
317 | if ( !$this->wikiConfig->get( 'GEMentorshipAutomaticEligibility' ) ) { |
318 | return; |
319 | } |
320 | |
321 | // ConvertibleTimestamp::time() used so we can fake the current time in tests |
322 | $userAge = ConvertibleTimestamp::time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); |
323 | if ( |
324 | $userAge >= $this->wikiConfig->get( 'GEMentorshipMinimumAge' ) * ExpirationAwareness::TTL_DAY && |
325 | $user->getEditCount() >= $this->wikiConfig->get( 'GEMentorshipMinimumEditcount' ) |
326 | ) { |
327 | $rights[] = 'enrollasmentor'; |
328 | } |
329 | } |
330 | |
331 | /** |
332 | * @inheritDoc |
333 | */ |
334 | public function onBeforePageDisplay( $out, $skin ): void { |
335 | if ( $out->getRequest()->getBool( 'gepersonalizedpraise' ) ) { |
336 | $out->addModules( 'ext.growthExperiments.MentorDashboard.PostEdit' ); |
337 | |
338 | $jsConfigVars = [ |
339 | 'wgPostEditConfirmationDisabled' => true, |
340 | 'wgGEMentorDashboardPersonalizedPraisePostEdit' => true, |
341 | ]; |
342 | |
343 | // NOTE: gepersonalizedpraise query parameter should be only passed in NS_USER_TALK, |
344 | // but verify that just in case |
345 | $title = $skin->getTitle(); |
346 | if ( $title->getNamespace() === NS_USER_TALK ) { |
347 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $skin->getTitle()->getText() ); |
348 | if ( $userIdentity ) { |
349 | $jsConfigVars['wgGEMentorDashboardPersonalizedPraiseMenteeGender'] = $this->genderCache |
350 | ->getGenderOf( $userIdentity ); |
351 | } |
352 | } |
353 | $out->addJsConfigVars( $jsConfigVars ); |
354 | } |
355 | } |
356 | } |