Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 238 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
TranslationNotificationsSubmitJob | |
0.00% |
0 / 238 |
|
0.00% |
0 / 8 |
1190 | |
0.00% |
0 / 1 |
newJob | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 153 |
|
0.00% |
0 / 1 |
182 | |||
getSourceLanguage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
fetchTranslators | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
getJobsForUser | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
72 | |||
addUserJobsToList | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getCurrentTotalJobs | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /* |
3 | * @file |
4 | * @license GPL-2.0-or-later |
5 | */ |
6 | |
7 | namespace MediaWiki\Extension\TranslationNotifications\Jobs; |
8 | |
9 | use Exception; |
10 | use ManualLogEntry; |
11 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage; |
12 | use MediaWiki\Extension\TranslationNotifications\Utilities\LanguageSet; |
13 | use MediaWiki\Extension\TranslationNotifications\Utilities\TranslationNotifyUser; |
14 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
15 | use MediaWiki\Languages\LanguageFactory; |
16 | use MediaWiki\Languages\LanguageNameUtils; |
17 | use MediaWiki\MediaWikiServices; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\User\Options\UserOptionsManager; |
20 | use MediaWiki\User\User; |
21 | use MediaWiki\WikiMap\WikiMap; |
22 | use Wikimedia\Rdbms\IResultWrapper; |
23 | |
24 | /** |
25 | * Handles a notification request. Uses the TranslationNotifyUsers to create the necessary jobs |
26 | * to deliver users messages based on their preferences. |
27 | * @since 2019.11 |
28 | * @ingroup JobQueue |
29 | */ |
30 | class TranslationNotificationsSubmitJob extends GenericTranslationNotificationsJob { |
31 | |
32 | /** |
33 | * Id of the current wiki |
34 | * @var string |
35 | */ |
36 | private $currentWikiId; |
37 | |
38 | /** |
39 | * @var UserOptionsManager |
40 | */ |
41 | private $userOptionsManager; |
42 | |
43 | /** |
44 | * @var JobQueueGroupFactory |
45 | */ |
46 | private $jobQueueGroupFactory; |
47 | |
48 | /** @var LanguageNameUtils */ |
49 | private $languageNameUtils; |
50 | |
51 | /** @var LanguageFactory */ |
52 | private $languageFactory; |
53 | |
54 | /** |
55 | * Returns an instance of the TranslationNotificationsSubmitJob |
56 | * @param Title $title |
57 | * @param array $requestData |
58 | * @param int $notifierId |
59 | * @param string $translatorLang |
60 | * @return self |
61 | */ |
62 | public static function newJob( |
63 | Title $title, $requestData, $notifierId, $translatorLang |
64 | ) { |
65 | return new TranslationNotificationsSubmitJob( |
66 | $title, |
67 | [ |
68 | 'requestData' => $requestData, |
69 | 'notifierId' => $notifierId, |
70 | 'translatorLanguage' => $translatorLang |
71 | ] |
72 | ); |
73 | } |
74 | |
75 | public function __construct( Title $title, array $params ) { |
76 | $services = MediaWikiServices::getInstance(); |
77 | $this->userOptionsManager = $services->getUserOptionsManager(); |
78 | $this->jobQueueGroupFactory = $services->getJobQueueGroupFactory(); |
79 | $this->languageNameUtils = $services->getLanguageNameUtils(); |
80 | $this->languageFactory = $services->getLanguageFactory(); |
81 | parent::__construct( 'TranslationNotificationsSubmitJob', $title, $params ); |
82 | $this->currentWikiId = WikiMap::getCurrentWikiDbDomain()->getId(); |
83 | } |
84 | |
85 | /** |
86 | * Execute the job |
87 | * @return bool |
88 | */ |
89 | public function run() { |
90 | $this->logInfo( 'Processing translation notification submit request.' ); |
91 | |
92 | $params = $this->params; |
93 | $translatableTitle = $this->title; |
94 | $notifier = User::newFromId( $params['notifierId'] ); |
95 | $sourceLanguage = $this->getSourceLanguage( $translatableTitle ); |
96 | $translatorLangCode = $params['translatorLanguage']; |
97 | |
98 | // Request information |
99 | $notificationText = $params['requestData']['notificationText']; |
100 | $priority = $params['requestData']['priority']; |
101 | $selectedLanguages = $params['requestData']['selectedLanguages']; |
102 | $deadlineDate = $params['requestData']['deadlineDate']; |
103 | $languageSet = $params['requestData']['languageSet']; |
104 | |
105 | if ( !$languageSet instanceof LanguageSet ) { |
106 | $languageSet = LanguageSet::fromArray( $languageSet ); |
107 | } |
108 | |
109 | $translatorsToNotify = $this->fetchTranslators( $selectedLanguages, $sourceLanguage, $languageSet ); |
110 | |
111 | $this->logInfo( |
112 | 'Found ' . $translatorsToNotify->numRows() . |
113 | ' translators to notify with the given conditions.', |
114 | [ |
115 | 'selectedLanguages' => $selectedLanguages |
116 | ] |
117 | ); |
118 | |
119 | $frequencies = [ |
120 | 'always' => 0, |
121 | 'week' => 604800, // seconds in week |
122 | 'month' => 2678400, // seconds in month |
123 | 'weekly' => 604800, // seconds in week |
124 | 'monthly' => 2678400, // seconds in month |
125 | 'none' => null |
126 | ]; |
127 | $currentUnixTime = wfTimestamp(); |
128 | $mwServices = MediaWikiServices::getInstance(); |
129 | $currentDBTime = $mwServices |
130 | ->getConnectionProvider() |
131 | ->getReplicaDatabase() |
132 | ->timestamp( $currentUnixTime ); |
133 | |
134 | $timestampOptionName = 'translationnotifications-timestamp'; |
135 | |
136 | $config = $mwServices->getMainConfig(); |
137 | |
138 | $allLanguages = array_keys( $this->languageNameUtils->getLanguageNames() ); |
139 | $languagesToNotify = []; |
140 | |
141 | switch ( $languageSet->getOption() ) { |
142 | case LanguageSet::ALL: |
143 | $languagesToNotify = array_diff( $allLanguages, [ $sourceLanguage ] ); |
144 | break; |
145 | case LanguageSet::SOME: |
146 | $languagesToNotify = $selectedLanguages; |
147 | break; |
148 | case LanguageSet::ALL_EXCEPT_SOME: |
149 | $languagesToNotify = array_diff( $allLanguages, array_merge( |
150 | $selectedLanguages, [ $sourceLanguage ] |
151 | ) ); |
152 | break; |
153 | } |
154 | |
155 | $notifyUser = new TranslationNotifyUser( |
156 | $translatableTitle, |
157 | $notifier, |
158 | $config->get( 'LocalInterwikis' ), |
159 | $config->get( 'NoReplyAddress' ), |
160 | $config->get( 'TranslationNotificationsAlwaysHttpsInEmail' ), |
161 | [ |
162 | 'text' => $notificationText, |
163 | 'priority' => $priority, |
164 | 'deadline' => $deadlineDate, |
165 | 'languagesToNotify' => $languagesToNotify |
166 | ] |
167 | ); |
168 | |
169 | $this->logInfo( 'Starting notification job creation for translators...' ); |
170 | |
171 | $jobsByTarget = []; |
172 | $stats = [ |
173 | 'jobNoPref' => 0, |
174 | 'jobEmail' => 0, |
175 | 'jobEmailDisabled' => 0, |
176 | 'jobTalkPage' => 0, |
177 | 'jobTalkPageOther' => 0, |
178 | 'tooEarly' => 0, |
179 | 'sendingFailed' => 0, |
180 | 'processedUsers' => 0, |
181 | 'unsubscribed' => 0 |
182 | ]; |
183 | |
184 | $usersWithEmptyWikiId = []; |
185 | foreach ( $translatorsToNotify as $translator ) { |
186 | $user = User::newFromID( $translator->up_user )->getInstanceForUpdate(); |
187 | |
188 | $userTranslationFrequency = |
189 | $frequencies[$this->userOptionsManager->getOption( $user, 'translationnotifications-freq' )]; |
190 | |
191 | if ( $userTranslationFrequency === null ) { |
192 | $stats['processedUsers']++; |
193 | $stats['unsubscribed']++; |
194 | continue; |
195 | } |
196 | |
197 | $userTimestamp = $this->userOptionsManager->getOption( |
198 | $user, |
199 | $timestampOptionName, |
200 | null |
201 | ); |
202 | $userUnixTimestamp = ( $userTimestamp == null ) ? |
203 | wfTimestamp( TS_UNIX, '20120101000000' ) : // An old timestamp |
204 | wfTimestamp( TS_UNIX, $userTimestamp ); |
205 | |
206 | $timeSinceNotification = (int)$currentUnixTime - (int)$userUnixTimestamp; |
207 | |
208 | if ( $timeSinceNotification > $userTranslationFrequency ) { |
209 | $this->logDebug( "Deciding notification to be sent to user: {$user->getId()}" ); |
210 | |
211 | try { |
212 | $userJobs = $this->getJobsForUser( |
213 | $user, |
214 | $notifyUser, |
215 | $this->currentWikiId, |
216 | $usersWithEmptyWikiId |
217 | ); |
218 | $this->addUserJobsToList( $userJobs, $jobsByTarget, $stats ); |
219 | |
220 | $this->userOptionsManager->setOption( $user, $timestampOptionName, $currentDBTime ); |
221 | $user->saveSettings(); |
222 | |
223 | $stats['processedUsers']++; |
224 | } catch ( Exception $e ) { |
225 | $stats['sendingFailed']++; |
226 | $this->logError( |
227 | "Error while generating notification for user - {$user->getId()}.\n Exception: {$e}." |
228 | ); |
229 | } |
230 | } else { |
231 | $this->logDebug( "Skipping sending of notification to user: {$user->getId()}" ); |
232 | $stats['tooEarly']++; |
233 | } |
234 | |
235 | $this->logDebug( "Finished processing user: {$user->getId()}." ); |
236 | } |
237 | |
238 | foreach ( $jobsByTarget as $wiki => $jobs ) { |
239 | $this->logInfo( "Wiki: $wiki, Jobs: " . count( $jobs ) ); |
240 | $this->jobQueueGroupFactory->makeJobQueueGroup( $wiki )->push( $jobs ); |
241 | } |
242 | |
243 | if ( $usersWithEmptyWikiId ) { |
244 | // Empty wiki ID found for some jobs. Log this information. See: T342903 |
245 | $this->logWarn( |
246 | 'Following notification jobs had an empty target wiki id: {param}', |
247 | [ 'param' => implode( ', ', $usersWithEmptyWikiId ) ] |
248 | ); |
249 | } |
250 | |
251 | // Add a log entry |
252 | $languagesForLog = ''; |
253 | if ( count( $selectedLanguages ) ) { |
254 | $languagesForLog = $this->languageFactory->getLanguage( $translatorLangCode ) |
255 | ->commaList( $selectedLanguages ); |
256 | } |
257 | |
258 | $logEntry = new ManualLogEntry( 'notifytranslators', 'sent' ); |
259 | $logEntry->setPerformer( $notifier ); |
260 | $logEntry->setTarget( $translatableTitle ); |
261 | $logEntry->setParameters( [ |
262 | '4::languagesForLog' => $languagesForLog, |
263 | '5::deadlineDate' => $deadlineDate, |
264 | '6::priority' => $priority, |
265 | '7::sentSuccess' => $stats['processedUsers'], |
266 | '8::sentFail' => $stats['sendingFailed'], |
267 | '9::tooEarly' => $stats['tooEarly'], |
268 | '11:plain' => $languageSet->getOptionName(), |
269 | ] ); |
270 | |
271 | $logId = $logEntry->insert(); |
272 | $logEntry->publish( $logId ); |
273 | |
274 | // Log the stats |
275 | $stats[ 'jobTotal' ] = $this->getCurrentTotalJobs( $stats ); |
276 | $stats[ 'jobTotalWithSkipped' ] = $this->getCurrentTotalJobs( $stats, true ); |
277 | $this->logInfo( |
278 | "Finished processing. Overall info: " . |
279 | "Too early: {tooEarly}, Sending failed: {sendingFailed}, Users processed: {processedUsers}. " . |
280 | "Jobs info: " . |
281 | "Total: {jobTotal}, Total with skipped & unsubscribed: {jobTotalWithSkipped}, " . |
282 | "Email: {jobEmail}, Email disabled: {jobEmailDisabled}, Talk page: {jobTalkPage}, " . |
283 | "Talk page other wiki: {jobTalkPageOther}, No preference: {jobNoPref}.", |
284 | $stats |
285 | ); |
286 | |
287 | return true; |
288 | } |
289 | |
290 | private function getSourceLanguage( Title $translatablePageTitle ): string { |
291 | $translatablePage = TranslatablePage::newFromTitle( $translatablePageTitle ); |
292 | return $translatablePage->getMessageGroup()->getSourceLanguage(); |
293 | } |
294 | |
295 | /** |
296 | * Returns translators who match the given language criteria. |
297 | * @param array $selectedLanguages |
298 | * @param string $sourceLanguage |
299 | * @param LanguageSet $languageSet |
300 | * @return IResultWrapper |
301 | */ |
302 | private function fetchTranslators( $selectedLanguages, $sourceLanguage, $languageSet ) { |
303 | $langPropertyPrefix = 'translationnotifications-lang-'; |
304 | $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA ); |
305 | $propertyLikePattern = $dbr->buildLike( $langPropertyPrefix, $dbr->anyString() ); |
306 | $translatorsConditions = [ |
307 | "up_property $propertyLikePattern", |
308 | ]; |
309 | switch ( $languageSet->getOption() ) { |
310 | case LanguageSet::ALL: |
311 | $translatorsConditions[] = $dbr->expr( 'up_value', '!=', $sourceLanguage ); |
312 | break; |
313 | case LanguageSet::SOME: |
314 | $translatorsConditions[] = $dbr->expr( 'up_value', '=', $selectedLanguages ); |
315 | break; |
316 | case LanguageSet::ALL_EXCEPT_SOME: |
317 | $selectedLanguages[] = $sourceLanguage; |
318 | $translatorsConditions[] = $dbr->expr( 'up_value', '!=', $selectedLanguages ); |
319 | break; |
320 | } |
321 | return $dbr->newSelectQueryBuilder() |
322 | ->select( 'up_user' ) |
323 | ->from( 'user_properties' ) |
324 | ->where( $translatorsConditions ) |
325 | ->caller( __METHOD__ ) |
326 | ->distinct() |
327 | ->fetchResultSet(); |
328 | } |
329 | |
330 | /** |
331 | * Return jobs for a user based on the user's preferences. |
332 | * @param User $user |
333 | * @param TranslationNotifyUser $notifyUser |
334 | * @param string $currentWikiId |
335 | * @param array &$usersWithEmptyWikiId |
336 | * @return array |
337 | */ |
338 | private function getJobsForUser( |
339 | User $user, |
340 | TranslationNotifyUser $notifyUser, |
341 | string $currentWikiId, |
342 | array &$usersWithEmptyWikiId |
343 | ): array { |
344 | $jobs = []; |
345 | |
346 | // Email notification |
347 | if ( $this->userOptionsManager->getOption( $user, 'translationnotifications-cmethod-email' ) ) { |
348 | if ( $this->userOptionsManager->getOption( $user, 'disablemail' ) ) { |
349 | // For some reason the user signed up to receive Translation Notifications emails, |
350 | // but receiving email is disabled in the user's preferences. |
351 | // To be on the safe side, disable the email contact method. |
352 | $this->userOptionsManager->setOption( $user, 'translationnotifications-cmethod-email', false ); |
353 | $jobs[] = [ $currentWikiId, 'jobEmailDisabled', null ]; |
354 | } elseif ( $this->userOptionsManager->getOption( $user, 'translationnotifications-freq' ) === 'always' ) { |
355 | // Check if user has email. Don't bother sending email if they don't have it configured |
356 | if ( $user->canReceiveEmail() ) { |
357 | $jobs[] = [ |
358 | $currentWikiId, 'jobEmail', $notifyUser->sendTranslationNotificationEmail( $user ) |
359 | ]; |
360 | } |
361 | } |
362 | } |
363 | |
364 | // Talk page in current wiki |
365 | if ( $this->userOptionsManager->getOption( $user, 'translationnotifications-cmethod-talkpage' ) ) { |
366 | $jobs[] = [ |
367 | $currentWikiId, |
368 | 'jobTalkPage', |
369 | $notifyUser->leaveUserMessage( $user, 'talkpageHere' ) |
370 | ]; |
371 | } |
372 | |
373 | // Talk page in another wiki |
374 | if ( $this->userOptionsManager->getOption( $user, 'translationnotifications-cmethod-talkpage-elsewhere' ) ) { |
375 | $wiki = $this->userOptionsManager->getOption( |
376 | $user, |
377 | 'translationnotifications-cmethod-talkpage-elsewhere-loc' |
378 | ) ?? ''; |
379 | |
380 | // T342903: WikiId is sometimes empty for some users. Don't create jobs for these users. |
381 | if ( $wiki === '' ) { |
382 | $usersWithEmptyWikiId[] = $user->getId(); |
383 | } else { |
384 | $jobs[] = [ |
385 | $wiki, |
386 | 'jobTalkPageOther', |
387 | $notifyUser->leaveUserMessage( $user, 'talkpageInOtherWiki' ) |
388 | ]; |
389 | } |
390 | } |
391 | |
392 | return $jobs; |
393 | } |
394 | |
395 | /** |
396 | * Add jobs for a user to the list of all jobs, also updates the stats. |
397 | * @param array $userJobs |
398 | * @param array &$jobList |
399 | * @param array &$stats |
400 | * @return void |
401 | */ |
402 | private function addUserJobsToList( array $userJobs, array &$jobList, array &$stats ): void { |
403 | if ( !count( $userJobs ) ) { |
404 | $stats[ 'jobNoPref' ]++; |
405 | } |
406 | |
407 | foreach ( $userJobs as $userJob ) { |
408 | [ $wikiId, $jobType, $job ] = $userJob; |
409 | $stats[ $jobType ]++; |
410 | if ( $job === null ) { |
411 | continue; |
412 | } |
413 | |
414 | $jobList[ $wikiId ] ??= []; |
415 | $jobList[ $wikiId ][] = $job; |
416 | } |
417 | } |
418 | |
419 | private function getCurrentTotalJobs( array $stats, bool $withSkipped = false ): int { |
420 | $total = $stats[ 'jobTalkPageOther' ] + $stats[ 'jobTalkPage' ] |
421 | + $stats[ 'jobEmail' ]; |
422 | |
423 | if ( $withSkipped ) { |
424 | $total += $stats[ 'jobEmailDisabled' ] + $stats[ 'jobNoPref' ]; |
425 | $total += $stats[ 'unsubscribed' ]; |
426 | } |
427 | |
428 | return $total; |
429 | } |
430 | } |