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