Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslationNotifyUser
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 8
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 leaveUserMessage
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
2
 sendTranslationNotificationEmail
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
2
 getRelevantLanguages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getUserLanguageOption
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getUserFirstLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserLanguages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getUrlProtocol
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/*
3* @file
4* @license GPL-2.0-or-later
5*/
6
7namespace MediaWiki\Extension\TranslationNotifications\Utilities;
8
9use MediaWiki\Extension\TranslationNotifications\Jobs\TranslationNotificationsEmailJob;
10use MediaWiki\MassMessage\Job\MassMessageJob;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\SpecialPage\SpecialPage;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15use MediaWiki\User\UserIdentity;
16use MediaWiki\WikiMap\WikiMap;
17use Message;
18
19/**
20 * Encapsulates the logic needed to create a notification to be sent to Users based on the
21 * type of notification they want. Creates the necessary job classes that are then used to
22 * actually deliver the notification.
23 * @since 2019.10
24 */
25class TranslationNotifyUser {
26    private Title $translatablePageTitle;
27    private User $notifier;
28    private string $noReplyAddress;
29    /** @var string[] */
30    private array $localInterwikis;
31    private bool $httpsInEmail;
32
33    // Request information
34    /** Request priority: `unset`, `high`, `medium` or `low` */
35    private string $priority;
36    private string $deadline;
37    private string $notificationText;
38    /**
39     * A list of languages for which the translators are to be notified. Empty for all languages.
40     * @var string[]
41     */
42    private array $languagesToNotify;
43
44    /**
45     * @param Title $translatablePageTitle
46     * @param User $notifier
47     * @param string[] $localInterwikis
48     * @param string $noReplyAddress
49     * @param bool $httpsInEmail
50     * @param array $requestData
51     */
52    public function __construct(
53        Title $translatablePageTitle, User $notifier, $localInterwikis, $noReplyAddress,
54        $httpsInEmail, $requestData
55    ) {
56        $this->notifier = $notifier;
57        $this->translatablePageTitle = $translatablePageTitle;
58
59        $this->noReplyAddress = $noReplyAddress;
60        $this->localInterwikis = $localInterwikis;
61        $this->httpsInEmail = $httpsInEmail;
62
63        $this->notificationText = $requestData['text'];
64        $this->languagesToNotify = $requestData['languagesToNotify'];
65        $this->priority = $requestData['priority'] ?? '';
66        $this->deadline = $requestData['deadline'] ?? '';
67    }
68
69    /**
70     * Leave a message on the user's talk page.
71     * @param User $translator To whom the message to be sent
72     * @param string $destination Whether to send it to a talk page on this wiki
73     * ('talkpageHere', default) or another one ('talkpageInOtherWiki').
74     * @return MassMessageJob
75     */
76    public function leaveUserMessage(
77        User $translator,
78        $destination = 'talkpageHere'
79    ) {
80        $relevantLanguages = $this->getRelevantLanguages( $translator, $this->languagesToNotify );
81        $userFirstLanguageCode = $this->getUserFirstLanguage( $translator );
82        $userFirstLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
83            ->getLanguage( $userFirstLanguageCode );
84
85        $text = wfMessage(
86            'translationnotifications-talkpage-body',
87            $translator->getName(),
88            NotificationMessageBuilder::getUserName( $translator ),
89            $userFirstLanguage->listToText( array_values( $relevantLanguages ) ),
90            NotificationMessageBuilder::getMessageTitle(
91                $this->translatablePageTitle, $destination, $this->localInterwikis
92            ),
93            NotificationMessageBuilder::getTranslationURLs(
94                $this->translatablePageTitle, $relevantLanguages, 'talkpage',
95                $userFirstLanguage, $this->getUrlProtocol()
96            ),
97            NotificationMessageBuilder::getPriorityClause( $userFirstLanguage, $this->priority ),
98            NotificationMessageBuilder::getDeadlineClause( $userFirstLanguage, $this->deadline ),
99            NotificationMessageBuilder::getNotificationMessage(
100                MediaWikiServices::getInstance()->getContentLanguage(),
101                $this->notificationText
102            )
103        )->numParams( count( $relevantLanguages ) ) // $9
104            ->params( NotificationMessageBuilder::getSignupURL( $this->getUrlProtocol() ) ) // $10
105            ->inLanguage( $userFirstLanguage )
106            ->text();
107
108        // Bidi-isolation of site name from date
109        $text .= $userFirstLanguage->getDirMarkEntity() .
110            ', ~~~~~'; // Date and time
111
112        // Note: Maybe this was originally meant for edit summary, but it's actually used as subject
113        $subject = wfMessage(
114            'translationnotifications-edit-summary',
115            $this->translatablePageTitle
116        )->inLanguage( $userFirstLanguage )->text();
117
118        $listUrl = SpecialPage::getTitleFor( 'NotifyTranslators' )->getCanonicalURL();
119
120        $params = [
121            // This is not the edit summary, but rather hidden comment left after the message
122            'comment' => [
123                $this->notifier->getName(),
124                WikiMap::getCurrentWikiId(),
125                $listUrl
126            ],
127            'message' => $text,
128            'subject' => $subject,
129            // Use canonical version of the namespace that works in all wikis and assume that
130            // user names are global across wikis
131            'title' => 'User_talk:' . $translator->getName(),
132        ];
133
134        // Ignored, the page to deliver to is read from $params['title']
135        $jobTitle = $translator->getTalkPage();
136
137        return new MassMessageJob( $jobTitle, $params );
138    }
139
140    /**
141     * Notify a user by email.
142     * @param User $translator User to whom the email is being sent
143     * @return TranslationNotificationsEmailJob
144     */
145    public function sendTranslationNotificationEmail(
146        User $translator
147    ): TranslationNotificationsEmailJob {
148        $relevantLanguages = $this->getRelevantLanguages( $translator, $this->languagesToNotify );
149        $userFirstLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
150            ->getLanguage( $this->getUserFirstLanguage( $translator ) );
151        $emailSubject = NotificationMessageBuilder::getNotificationSubject(
152            $this->translatablePageTitle, $userFirstLanguage
153        );
154
155        $translationUrls = NotificationMessageBuilder::getTranslationURLs(
156            $this->translatablePageTitle,
157            $relevantLanguages,
158            'email',
159            $userFirstLanguage,
160            $this->getUrlProtocol()
161        );
162
163        $emailBody = wfMessage( 'translationnotifications-email-body' )
164            ->params(
165                NotificationMessageBuilder::getUserName( $translator ), // $1
166                $userFirstLanguage->listToText( array_values( $relevantLanguages ) ),
167                $this->translatablePageTitle,
168                $translationUrls, // $4
169                NotificationMessageBuilder::getPriorityClause( $userFirstLanguage, $this->priority ),
170                NotificationMessageBuilder::getDeadlineClause( $userFirstLanguage, $this->deadline ),
171                $this->notificationText, // $7
172                NotificationMessageBuilder::getSignupURL( $this->getUrlProtocol() ),
173                Message::numParam( count( $relevantLanguages ) ),
174                $translator->getName() // $10
175            )->inLanguage( $userFirstLanguage )->text();
176
177        $sender = $this->notifier;
178
179        // Do not publish the sender's email, but include his/her name
180        $emailFrom = TranslationNotificationsEmailJob::buildAddress(
181            $this->noReplyAddress,
182            $sender->getName(),
183            $sender->getRealName()
184        );
185
186        $params = [
187            'to' => TranslationNotificationsEmailJob::addressFromUser( $translator ),
188            'from' => $emailFrom,
189            'body' => $emailBody,
190            'subject' => $emailSubject,
191            'replyTo' => $emailFrom,
192        ];
193
194        return new TranslationNotificationsEmailJob( $this->translatablePageTitle, $params );
195    }
196
197    /**
198     * Returns a list of language codes and names for the current
199     * notification to the user.
200     * @param User $user User to whom the email is being sent
201     * @param string[] $languagesToNotify A list of languages that are notified.
202     * Empty for all languages.
203     * @return string[] Array of language codes
204     */
205    protected function getRelevantLanguages( User $user, $languagesToNotify ) {
206        $userLanguages = $this->getUserLanguages( $user );
207        $userFirstLanguageCode = $userLanguages[0];
208        $limitLanguages = count( $languagesToNotify );
209        $userLanguageNames = [];
210
211        $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
212        foreach ( $userLanguages as $langCode ) {
213            // Don't add this language if particular languages were
214            // specified and this language was not one of them.
215            if ( ( $limitLanguages && !in_array( $langCode, $languagesToNotify ) ) ) {
216                continue;
217            }
218
219            $userLanguageNames[$langCode] = $languageNameUtils->getLanguageName(
220                $langCode,
221                $userFirstLanguageCode
222            );
223        }
224
225        return $userLanguageNames;
226    }
227
228    /**
229     * Returns a language that a user signed up for in
230     * Special:TranslatorSignup.
231     * @param UserIdentity $user
232     * @param int $langNum Number of language.
233     * @return string Language code, or null if it wasn't defined.
234     */
235    protected function getUserLanguageOption( UserIdentity $user, $langNum ) {
236        return MediaWikiServices::getInstance()
237            ->getUserOptionsLookup()
238            ->getOption( $user, "translationnotifications-lang-$langNum" );
239    }
240
241    /**
242     * Returns the code of the first language to which a user signed up in
243     * Special:TranslatorSignup.
244     * @param User $user
245     * @return string Language code.
246     */
247    protected function getUserFirstLanguage( User $user ) {
248        return $this->getUserLanguageOption( $user, 1 );
249    }
250
251    /**
252     * Returns an array of all language codes to which a user signed up in
253     * Special:TranslatorSignup.
254     * @param User $user
255     * @return array of language codes.
256     */
257    protected function getUserLanguages( User $user ) {
258        $userLanguages = [];
259
260        foreach ( range( 1, 3 ) as $langNum ) {
261            $nextLanguage = $this->getUserLanguageOption( $user, $langNum );
262            if ( $nextLanguage !== '' ) {
263                $userLanguages[] = $nextLanguage;
264            }
265        }
266
267        return $userLanguages;
268    }
269
270    /**
271     * Returns the URL protocol to be used based on configuration
272     * @return string|int a PROTO_* constant
273     */
274    protected function getUrlProtocol() {
275        return !$this->httpsInEmail
276            ? PROTO_CANONICAL
277            : PROTO_HTTPS;
278    }
279}