Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecentChangeMailComposer
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 3
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 composeCommonMailtext
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
156
 compose
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\RecentChanges;
8
9use MediaWiki\Config\Config;
10use MediaWiki\Language\Language;
11use MediaWiki\Language\MessageParser;
12use MediaWiki\Mail\IEmailer;
13use MediaWiki\Mail\MailAddress;
14use MediaWiki\Mail\UserEmailContact;
15use MediaWiki\MainConfigNames;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Permissions\Authority;
18use MediaWiki\Skin\Skin;
19use MediaWiki\SpecialPage\SpecialPage;
20use MediaWiki\Title\Title;
21use MediaWiki\User\Options\UserOptionsLookup;
22use MediaWiki\User\User;
23use MediaWiki\Utils\UrlUtils;
24
25/**
26 * Component responsible for composing and sending emails triggered after a RecentChange.
27 * This includes watchlist notifications, user_talk notifications.
28 *
29 * @internal
30 */
31class RecentChangeMailComposer {
32
33    /**
34     * Notification is due to user's user talk being edited
35     */
36    public const USER_TALK = 'user_talk';
37    /**
38     * Notification is due to a watchlisted page being edited
39     */
40    public const WATCHLIST = 'watchlist';
41    /**
42     * Notification because user is notified for all changes
43     */
44    public const ALL_CHANGES = 'all_changes';
45
46    protected string $subject = '';
47
48    protected string $body = '';
49
50    protected string $summary = '';
51
52    protected ?MailAddress $replyto;
53
54    protected ?MailAddress $from;
55
56    protected bool $composed_common = false;
57
58    /** @var MailAddress[] */
59    protected array $mailTargets = [];
60
61    protected ?bool $minorEdit;
62
63    /** @var int|null|bool */
64    protected $oldid;
65
66    protected string $timestamp;
67
68    protected string $pageStatus = '';
69
70    protected Title $title;
71
72    protected User $editor;
73
74    private Config $mainConfig;
75    private UserOptionsLookup $userOptionsLookup;
76    private UrlUtils $urlUtils;
77    private MessageParser $messageParser;
78    private Language $contentLanguage;
79    private IEmailer $emailer;
80
81    public function __construct(
82        Authority $editor,
83        Title $title,
84        RecentChange $recentChange,
85        string $pageStatus
86    ) {
87        $services = MediaWikiServices::getInstance();
88        $this->editor = $services->getUserFactory()->newFromAuthority( $editor );
89        $this->title = $title;
90        $this->oldid = $recentChange->getAttribute( 'rc_last_oldid' );
91        $this->minorEdit = $recentChange->getAttribute( 'rc_minor' );
92        $this->timestamp = $recentChange->getAttribute( 'rc_timestamp' );
93        $this->summary = $recentChange->getAttribute( 'rc_comment' );
94        $this->pageStatus = $pageStatus;
95
96        // Prepare for dependency injection
97        $this->mainConfig = $services->getMainConfig();
98        $this->userOptionsLookup = $services->getUserOptionsLookup();
99        $this->urlUtils = $services->getUrlUtils();
100        $this->messageParser = $services->getMessageParser();
101        $this->contentLanguage = $services->getContentLanguage();
102        $this->emailer = $services->getEmailer();
103    }
104
105    /**
106     * Generate the generic "this page has been changed" e-mail text.
107     */
108    private function composeCommonMailtext() {
109        $this->composed_common = true;
110
111        # You as the WikiAdmin and Sysops can make use of plenty of
112        # named variables when composing your notification emails while
113        # simply editing the Meta pages
114
115        $keys = [];
116        $postTransformKeys = [];
117        $pageTitleUrl = $this->title->getCanonicalURL();
118        $pageTitle = $this->title->getPrefixedText();
119
120        if ( $this->oldid ) {
121            // Always show a link to the diff which triggered the mail. See T34210.
122            $keys['$NEWPAGE'] = "\n\n" . wfMessage(
123                    'enotif_lastdiff',
124                    $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
125                )->inContentLanguage()->text();
126
127            // For personal mail, also show a link to the diff of all changes
128            // since last visited.
129            $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
130                    'enotif_lastvisited',
131                    $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
132                )->inContentLanguage()->text();
133            $keys['$OLDID'] = $this->oldid;
134            $keys['$PAGELOG'] = '';
135        } else {
136            // If there is no revision to link to, link to the page log, which should have details. See T115183.
137            $keys['$OLDID'] = '';
138            $keys['$NEWPAGE'] = '';
139            $keys['$PAGELOG'] = "\n\n" . wfMessage(
140                    'enotif_pagelog',
141                    SpecialPage::getTitleFor( 'Log' )->getCanonicalURL( [ 'page' =>
142                        $this->title->getPrefixedDBkey() ] )
143                )->inContentLanguage()->text();
144        }
145
146        $keys['$PAGETITLE'] = $this->title->getPrefixedText();
147        $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
148        $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
149            "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
150            '';
151        $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
152
153        if ( $this->editor->isAnon() ) {
154            # real anon (user:xxx.xxx.xxx.xxx)
155            $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
156                ->inContentLanguage()->text();
157            $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
158        } elseif ( $this->editor->isTemp() ) {
159            $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() )
160                ->inContentLanguage()->text();
161            $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
162        } else {
163            $keys['$PAGEEDITOR'] = $this->mainConfig->get( MainConfigNames::EnotifUseRealName ) &&
164            $this->editor->getRealName() !== ''
165                ? $this->editor->getRealName() : $this->editor->getName();
166            $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
167            $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
168        }
169
170        $keys['$PAGEEDITOR_WIKI'] = $this->editor->getTalkPage()->getCanonicalURL();
171        $keys['$HELPPAGE'] = $this->urlUtils->expand(
172            Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ),
173            PROTO_CURRENT
174        ) ?? false;
175
176        # Replace this after transforming the message, T37019
177        $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
178
179        // Now build message's subject and body
180
181        // Messages:
182        // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
183        // enotif_subject_restored, enotif_subject_changed
184        $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
185            ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
186
187        // Messages:
188        // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
189        // enotif_body_intro_restored, enotif_body_intro_changed
190        $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
191            ->inContentLanguage()
192            ->params( $pageTitle, $keys['$PAGEEDITOR'], "<{$pageTitleUrl}>" )
193            ->text();
194
195        $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
196        $body = strtr( $body, $keys );
197        $body = $this->messageParser->transform( $body, false, null, $this->title );
198        $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
199
200        # Reveal the page editor's address as REPLY-TO address only if
201        # the user has not opted-out and the option is enabled at the
202        # global configuration level.
203        $adminAddress = new MailAddress(
204            $this->mainConfig->get( MainConfigNames::PasswordSender ),
205            wfMessage( 'emailsender' )->inContentLanguage()->text()
206        );
207        if ( $this->mainConfig->get( MainConfigNames::EnotifRevealEditorAddress )
208            && ( $this->editor->getEmail() != '' )
209            && $this->userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
210        ) {
211            $editorAddress = MailAddress::newFromUser( $this->editor );
212            if ( $this->mainConfig->get( MainConfigNames::EnotifFromEditor ) ) {
213                $this->from = $editorAddress;
214            } else {
215                $this->from = $adminAddress;
216                $this->replyto = $editorAddress;
217            }
218        } else {
219            $this->from = $adminAddress;
220            $this->replyto = new MailAddress(
221                $this->mainConfig->get( MainConfigNames::NoReplyAddress )
222            );
223        }
224    }
225
226    /**
227     * Compose a mail to a given user and send it now.
228     *
229     * @param UserEmailContact $watchingUser
230     * @param string $source
231     */
232    public function compose( UserEmailContact $watchingUser, $source ) {
233        if ( !$this->composed_common ) {
234            $this->composeCommonMailtext();
235        }
236
237        // From the PHP manual:
238        //   Note: The to parameter cannot be an address in the form of
239        //   "Something <someone@example.com>". The mail command will not parse
240        //   this properly while talking with the MTA.
241        $to = MailAddress::newFromUser( $watchingUser );
242
243        # $PAGEEDITDATE is the time and date of the page change
244        # expressed in terms of individual local time of the notification
245        # recipient, i.e. watching user
246        $watchingUserName = (
247            $this->mainConfig->get( MainConfigNames::EnotifUseRealName ) &&
248            $watchingUser->getRealName() !== ''
249        ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
250        $body = str_replace(
251            [
252                '$WATCHINGUSERNAME',
253                '$PAGEEDITDATE',
254                '$PAGEEDITTIME'
255            ],
256            [
257                $watchingUserName,
258                $this->contentLanguage->userDate( $this->timestamp, $watchingUser->getUser() ),
259                $this->contentLanguage->userTime( $this->timestamp, $watchingUser->getUser() )
260            ],
261            $this->body
262        );
263
264        $headers = [];
265        if ( $source === self::WATCHLIST ) {
266            $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
267        }
268
269        $this->emailer->send(
270                [ $to ],
271                $this->from,
272                $this->subject,
273                $body,
274                null,
275                [
276                    'replyTo' => $this->replyto,
277                    'headers' => $headers,
278                ]
279            );
280    }
281}