-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathMentorPageMentorManager.php
401 lines (352 loc) · 12 KB
/
MentorPageMentorManager.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
<?php
namespace GrowthExperiments\Mentorship;
use GrowthExperiments\Mentorship\Store\MentorStore;
use GrowthExperiments\Mentorship\Store\PreferenceMentorStore;
use GrowthExperiments\WikiConfigException;
use Language;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserOptionsLookup;
use MessageLocalizer;
use ParserOptions;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Title;
use TitleFactory;
use Wikimedia\Rdbms\DBReadOnlyError;
use WikiPage;
use WikitextContent;
class MentorPageMentorManager extends MentorManager implements LoggerAwareInterface {
use LoggerAwareTrait;
/**
* @var string User preference for storing the mentor.
* @deprecated since 1.36, use PreferenceMentorStore::MENTOR_PREF instead
*/
public const MENTOR_PREF = PreferenceMentorStore::MENTOR_PREF;
public const MENTORSHIP_ENABLED_PREF = 'growthexperiments-homepage-mentorship-enabled';
/** @var int Maximum mentor intro length. */
private const INTRO_TEXT_LENGTH = 240;
private $mentorStore;
/** @var TitleFactory */
private $titleFactory;
/** @var WikiPageFactory */
private $wikiPageFactory;
/** @var UserNameUtils */
private $userNameUtils;
/** @var UserIdentityLookup */
private $userIdentityLookup;
private $userOptionsLookup;
/** @var MessageLocalizer */
private $messageLocalizer;
/** @var bool */
private $wasPosted;
/** @var Language */
private $language;
/** @var string|null */
private $mentorsPageName;
/** @var string|null */
private $manuallyAssignedMentorsPageName;
/**
* @param MentorStore $mentorStore
* @param TitleFactory $titleFactory
* @param WikiPageFactory $wikiPageFactory
* @param UserNameUtils $userNameUtils
* @param UserIdentityLookup $userIdentityLookup
* @param UserOptionsLookup $userOptionsLookup
* @param MessageLocalizer $messageLocalizer
* @param Language $language
* @param string|null $mentorsPageName Title of the page which contains the list of available mentors.
* See the documentation of the GEHomepageMentorsList config variable for format. May be null if no
* such page exists.
* @param string|null $manuallyAssignedMentorsPageName Title of the page which contains the list of automatically
* assigned mentors. May be null if no such page exists.
* See the documentation for GEHomepageManualAssignmentMentorsList for format.
* @param bool $wasPosted Is this a POST request?
*/
public function __construct(
MentorStore $mentorStore,
TitleFactory $titleFactory,
WikiPageFactory $wikiPageFactory,
UserNameUtils $userNameUtils,
UserIdentityLookup $userIdentityLookup,
UserOptionsLookup $userOptionsLookup,
MessageLocalizer $messageLocalizer,
Language $language,
?string $mentorsPageName,
?string $manuallyAssignedMentorsPageName,
$wasPosted
) {
$this->mentorStore = $mentorStore;
$this->titleFactory = $titleFactory;
$this->wikiPageFactory = $wikiPageFactory;
$this->userNameUtils = $userNameUtils;
$this->userIdentityLookup = $userIdentityLookup;
$this->userOptionsLookup = $userOptionsLookup;
$this->messageLocalizer = $messageLocalizer;
$this->language = $language;
$this->mentorsPageName = $mentorsPageName;
$this->manuallyAssignedMentorsPageName = $manuallyAssignedMentorsPageName;
$this->wasPosted = $wasPosted;
$this->setLogger( new NullLogger() );
}
/** @inheritDoc */
public function getMentorForUserIfExists( UserIdentity $user ): ?Mentor {
$mentorUser = $this->mentorStore->loadMentorUser( $user );
if ( !$mentorUser ) {
return null;
}
return $this->newMentorFromUserIdentity( $mentorUser, $user );
}
/** @inheritDoc */
public function getMentorForUser( UserIdentity $user ): Mentor {
$mentorUser = $this->mentorStore->loadMentorUser( $user );
if ( !$mentorUser ) {
$mentorUser = $this->getRandomAutoAssignedMentor( $user );
if ( !$mentorUser ) {
// TODO: Remove this call (T290371)
throw new WikiConfigException( 'Mentorship: No mentor available' );
}
$this->mentorStore->setMentorForUser( $user, $mentorUser );
}
return $this->newMentorFromUserIdentity( $mentorUser, $user );
}
/** @inheritDoc */
public function getMentorForUserSafe( UserIdentity $user ): ?Mentor {
try {
return $this->getMentorForUser( $user );
} catch ( WikiConfigException $e ) {
// WikiConfigException is thrown when no mentor is available
// Log as info level, as not-yet-developed wikis may have
// zero mentors for long period of time (T274035)
$this->logger->info( 'No mentor available for {user}', [
'user' => $user->getName(),
'exception' => $e
] );
} catch ( DBReadOnlyError $e ) {
// Just pretend the user doesn't have a mentor. It will be set later, and often
// this call is made in the context of something not specifically mentorship-
// related, such as the homepage, so it's better than erroring out.
}
return null;
}
/**
* @inheritDoc
*/
public function newMentorFromUserIdentity(
UserIdentity $mentorUser,
?UserIdentity $menteeUser = null
): Mentor {
return new Mentor(
$mentorUser,
$this->getMentorIntroText( $mentorUser, $menteeUser ?? $mentorUser )
);
}
/**
* Helper method returning a list of mentors listed at a specified page
*
* @param WikiPage|null $page Page to work with or null if no page is provided
* @return string[]
*/
private function getMentorsForPage( ?WikiPage $page ): array {
if ( $page === null ) {
return [];
}
$links = $page->getParserOutput( ParserOptions::newCanonical( 'canonical' ) )->getLinks();
if ( !isset( $links[ NS_USER ] ) ) {
$this->logger->info( __METHOD__ . ' found zero mentors, no links at {mentorsList}', [
'mentorsList' => $page->getTitle()->getPrefixedText()
] );
return [];
}
$mentorsRaw = array_keys( $links[ NS_USER ] );
foreach ( $mentorsRaw as &$username ) {
$canonical = $this->userNameUtils->getCanonical( $username );
if ( $canonical === false ) {
continue;
}
$username = $canonical;
}
unset( $username );
return $this->userIdentityLookup
->newSelectQueryBuilder()
->whereUserNames( $mentorsRaw )
->registered()
->fetchUserNames();
}
/** @inheritDoc */
public function getAutoAssignedMentors(): array {
return $this->getMentorsForPage( $this->getMentorsPage() );
}
/** @inheritDoc */
public function getManuallyAssignedMentors(): array {
return $this->getMentorsForPage( $this->getManuallyAssignedMentorsPage() );
}
/**
* @inheritDoc
*/
public function getRandomAutoAssignedMentor(
UserIdentity $mentee, array $excluded = []
): ?UserIdentity {
$autoAssignedMentors = $this->getAutoAssignedMentors();
if ( count( $autoAssignedMentors ) === 0 ) {
$this->logger->debug(
'Mentorship: No mentor available for user {user}',
[
'user' => $mentee->getName()
]
);
return null;
}
$autoAssignedMentors = array_values( array_diff( $autoAssignedMentors,
array_map( static function ( UserIdentity $excludedUser ) {
return $excludedUser->getName();
}, $excluded )
) );
if ( count( $autoAssignedMentors ) === 0 ) {
$this->logger->debug(
'Mentorship: No mentor available for {user} but excluded users',
[
'user' => $mentee->getName()
]
);
return null;
}
$autoAssignedMentors = array_values( array_diff( $autoAssignedMentors, [ $mentee->getName() ] ) );
if ( count( $autoAssignedMentors ) === 0 ) {
$this->logger->debug(
'Mentorship: No mentor available for {user} but themselves',
[
'user' => $mentee->getName()
]
);
return null;
}
$selectedMentorName = $autoAssignedMentors[ rand( 0, count( $autoAssignedMentors ) - 1 ) ];
$result = $this->userIdentityLookup->getUserIdentityByName( $selectedMentorName );
if ( $result === null ) {
throw new WikiConfigException(
'Mentorship: Mentor ' . $selectedMentorName . ' does not have a valid username'
);
}
return $result;
}
/**
* @inheritDoc
*/
public function getAutoMentorsListTitle(): ?Title {
if ( $this->mentorsPageName === null ) {
return null;
}
$title = $this->titleFactory->newFromText( $this->mentorsPageName );
if ( !$title ) {
// TitleFactory failed to construct a title object -- the configured list must be invalid
throw new WikiConfigException( 'wgGEHomepageMentorsList is invalid: ' . $this->mentorsPageName );
}
return $title;
}
/**
* Get the WikiPage object for the mentor page.
* @return WikiPage|null A page that's guaranteed to exist or null when no mentors page available
* @throws WikiConfigException If the mentor page cannot be fetched due to misconfiguration.
*/
private function getMentorsPage(): ?WikiPage {
$title = $this->getAutoMentorsListTitle();
if ( !$title ) {
// page was not configured -- configuration is valid, do not throw
return null;
}
if ( !$title->exists() ) {
// page does not exist, throw WikiConfigException
throw new WikiConfigException(
'Page defined by wgGEHomepageMentorsList does not exist: ' .
$this->mentorsPageName
);
}
return $this->wikiPageFactory->newFromTitle( $title );
}
/**
* Get the WikiPage object for the manually assigned mentor page.
* @throws WikiConfigException If the mentor page cannot be fetched due to misconfiguration.
* @return WikiPage|null A page that's guaranteed to exist, or null if impossible to get.
*/
public function getManuallyAssignedMentorsPage(): ?WikiPage {
if ( $this->manuallyAssignedMentorsPageName === null ) {
return null;
}
$title = $this->titleFactory->newFromText( $this->manuallyAssignedMentorsPageName );
if ( !$title || !$title->exists() ) {
throw new WikiConfigException(
'wgGEHomepageManualAssignmentMentorsList is invalid: ' . $this->manuallyAssignedMentorsPageName
);
}
return $this->wikiPageFactory->newFromTitle( $title );
}
/**
* Get the description used for presenting the mentor to the mentee.
* @param UserIdentity $mentor
* @param UserIdentity $mentee
* @return string
* @throws WikiConfigException If the mentor intro text cannot be fetched due to misconfiguration.
*/
private function getMentorIntroText( UserIdentity $mentor, UserIdentity $mentee ) {
return $this->getCustomMentorIntroText( $mentor )
?? $this->getDefaultMentorIntroText( $mentor, $mentee );
}
/**
* @param UserIdentity $mentor
* @param UserIdentity $mentee
* @return string
*/
private function getDefaultMentorIntroText( UserIdentity $mentor, UserIdentity $mentee ) {
return $this->messageLocalizer
->msg( 'growthexperiments-homepage-mentorship-intro' )
->params( $mentor->getName() )
->params( $mentee->getName() )
->text();
}
/**
* Custom mentor intro text which mentors can set on the mentor page.
* @param UserIdentity $mentor
* @return string|null Null when no custom text has been set for this mentor.
* @throws WikiConfigException If the mentor page cannot be fetched due to misconfiguration.
*/
private function getCustomMentorIntroText( UserIdentity $mentor ) {
// Use \h (horizontal whitespace) instead of \s (whitespace) to avoid matching newlines (T227535)
preg_match(
sprintf( '/:%s]]\h*\|\h*(.*)/', preg_quote( $mentor->getName(), '/' ) ),
$this->getMentorsPageContent(),
$matches
);
$introText = $matches[1] ?? '';
if ( $introText === '' ) {
return null;
}
return $this->messageLocalizer->msg( 'quotation-marks' )
->rawParams( $this->language->truncateForVisual( $introText, self::INTRO_TEXT_LENGTH ) )
->text();
}
/**
* Get the text of the mentor page.
* @return string
* @throws WikiConfigException If the mentor page cannot be fetched due to misconfiguration.
*/
private function getMentorsPageContent() {
$page = $this->getMentorsPage();
if ( $page === null ) {
return "";
}
/** @var $content WikitextContent */
$content = $page->getContent();
// @phan-suppress-next-line PhanUndeclaredMethod
return $content->getText();
}
/**
* @inheritDoc
*/
public function isMentorshipEnabledForUser( UserIdentity $user ): bool {
return $this->userOptionsLookup->getBoolOption( $user, self::MENTORSHIP_ENABLED_PREF );
}
}