Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.58% covered (danger)
32.58%
43 / 132
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecentChangeNotifier
32.82% covered (danger)
32.82%
43 / 131
0.00% covered (danger)
0.00%
0 / 4
746.42
0.00% covered (danger)
0.00%
0 / 1
 notifyOnPageChange
93.94% covered (success)
93.94%
31 / 33
0.00% covered (danger)
0.00%
0 / 1
7.01
 shouldSendNotification
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
7.39
 actuallyNotifyOnPageChange
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
506
 canSendUserTalkEmail
19.05% covered (danger)
19.05%
4 / 21
0.00% covered (danger)
0.00%
0 / 1
88.39
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @author Brooke Vibber
6 * @author <mail@tgries.de>
7 * @author Tim Starling
8 * @author Luke Welling lwelling@wikimedia.org
9 */
10
11namespace MediaWiki\RecentChanges;
12
13use MediaWiki\Config\Config;
14use MediaWiki\HookContainer\HookRunner;
15use MediaWiki\MainConfigNames;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Notification\RecipientSet;
18use MediaWiki\Permissions\Authority;
19use MediaWiki\Title\Title;
20use MediaWiki\User\User;
21use MediaWiki\User\UserArray;
22use MediaWiki\User\UserIdentity;
23use UnexpectedValueException;
24
25/**
26 * Find watchers and create notifications after a page is changed.
27 *
28 * After an edit is published to RCFeed, RecentChange::save calls RecentChangeNotifier.
29 * Here we query the `watchlist` table (via WatchedItemStore) to find who is watching
30 * a given page, format the emails in question, and dispatch notifications to each of them
31 * via the JobQueue.
32 *
33 * Visit the documentation pages under
34 * https://www.mediawiki.org/wiki/Help:Watching_pages
35 *
36 * @todo Use UserOptionsLookup and other services, consider converting this to a service
37 *
38 * @since 1.11.0
39 * @ingroup RecentChanges
40 * @ingroup Mail
41 */
42class RecentChangeNotifier {
43
44    /**
45     * Send emails corresponding to the user $editor editing the page $title.
46     *
47     * May be deferred via the job queue.
48     *
49     * @param RecentChange $recentChange
50     * @return bool Whether an email & notification job was created or not.
51     * @internal
52     */
53    public function notifyOnPageChange(
54        RecentChange $recentChange
55    ): bool {
56        // Never send an RC notification email about categorization changes
57        if ( $recentChange->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE ) {
58            return false;
59        }
60        $mwServices = MediaWikiServices::getInstance();
61        $config = $mwServices->getMainConfig();
62
63        $minorEdit = $recentChange->getAttribute( 'rc_minor' );
64        $editor = $mwServices->getUserFactory()
65            ->newFromUserIdentity( $recentChange->getPerformerIdentity() );
66
67        $title = Title::castFromPageReference( $recentChange->getPage() );
68        if ( $title === null || $title->getNamespace() < 0 ) {
69            return false;
70        }
71
72        // update wl_notificationtimestamp for watchers
73        $watchers = [];
74        if ( $config->get( MainConfigNames::EnotifWatchlist ) || $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
75            $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
76                $editor,
77                $title,
78                $recentChange->getAttribute( 'rc_timestamp' )
79            );
80        }
81
82        $sendNotification = $this->shouldSendNotification(
83            $editor, $title, $watchers, $minorEdit, $config
84        );
85
86        if ( $sendNotification ) {
87            $mwServices->getJobQueueGroup()->lazyPush( new RecentChangeNotifyJob(
88                $title,
89                [
90                    'editor' => $editor->getName(),
91                    'editorID' => $editor->getId(),
92                    'watchers' => $watchers,
93                    'pageStatus' => $recentChange->mExtra['pageStatus'] ?? 'changed',
94                    'rc_id' => $recentChange->getAttribute( 'rc_id' ),
95                ],
96                $mwServices->getRecentChangeLookup()
97            ) );
98        }
99
100        return $sendNotification;
101    }
102
103    private function shouldSendNotification(
104        User $editor,
105        Title $title,
106        array $watchers,
107        bool $minorEdit,
108        Config $config
109    ): bool {
110        // Don't send notifications for bots
111        if ( $editor->isBot() ) {
112            return false;
113        }
114
115        // If someone is watching the page or there are users notified on all changes
116        if ( count( $watchers ) ||
117            count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) ) ) {
118            return true;
119        }
120
121        // if it's a talk page with an applicable notification.
122        if ( !$minorEdit ||
123            ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
124                !$editor->isAllowed( 'nominornewtalk' ) )
125        ) {
126            return $this->canSendUserTalkEmail( $editor, $title, $minorEdit );
127        }
128
129        return false;
130    }
131
132    /**
133     * Immediate version of notifyOnPageChange().
134     *
135     * Send emails corresponding to the user $editor editing the page $title.
136     *
137     * @note Use notifyOnPageChange so that wl_notificationtimestamp is updated.
138     *
139     * @param Authority $editor
140     * @param Title $title
141     * @param RecentChange $recentChange
142     * @param array $watchers Array of user IDs
143     * @param string $pageStatus
144     * @internal
145     */
146    public function actuallyNotifyOnPageChange(
147        Authority $editor,
148        $title,
149        RecentChange $recentChange,
150        array $watchers,
151        $pageStatus = 'changed'
152    ) {
153        # we use $wgPasswordSender as sender's address
154        $mwServices = MediaWikiServices::getInstance();
155        $config = $mwServices->getMainConfig();
156        $notifService = $mwServices->getNotificationService();
157        $userFactory = $mwServices->getUserFactory();
158        $hookRunner = new HookRunner( $mwServices->getHookContainer() );
159
160        $minorEdit = $recentChange->getAttribute( 'rc_minor' );
161        # The following code is only run, if several conditions are met:
162        # 1. RecentChangeNotifier for pages (other than user_talk pages) must be enabled
163        # 2. minor edits (changes) are only regarded if the global flag indicates so
164        $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
165        if ( !in_array( $pageStatus, $formattedPageStatus ) ) {
166            throw new UnexpectedValueException( 'Not a valid page status!' );
167        }
168        $agent = $mwServices->getUserFactory()->newFromAuthority( $editor );
169
170        $userTalkId = false;
171        if ( !$minorEdit ||
172            ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
173                !$editor->isAllowed( 'nominornewtalk' ) )
174        ) {
175            if ( $config->get( MainConfigNames::EnotifUserTalk )
176                && $title->getNamespace() === NS_USER_TALK
177                && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
178            ) {
179                $targetUser = $userFactory->newFromName( $title->getText() );
180                if ( $targetUser ) {
181                    $talkNotification = new RecentChangeNotification(
182                        $agent,
183                        $title,
184                        $recentChange,
185                        $pageStatus,
186                        RecentChangeNotification::TALK_NOTIFICATION
187                    );
188                    $notifService->notify( $talkNotification, new RecipientSet( [ $targetUser ] ) );
189                    $userTalkId = $targetUser->getId();
190                }
191            }
192
193            if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
194                $userOptionsLookup = $mwServices->getUserOptionsLookup();
195                // Send updates to watchers other than the current editor
196                // and don't send to watchers who are blocked and cannot login
197                $userArray = UserArray::newFromIDs( $watchers );
198                $recipients = new RecipientSet( [] );
199                foreach ( $userArray as $watchingUser ) {
200                    if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
201                        && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
202                        && $watchingUser->getId() != $userTalkId
203                        && !in_array( $watchingUser->getName(),
204                            $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
205                        // @TODO Partial blocks should not prevent the user from logging in.
206                        //       see: https://phabricator.wikimedia.org/T208895
207                        && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
208                            $watchingUser->getBlock() )
209                    ) {
210                        $recipients->addRecipient( $watchingUser );
211                    }
212                }
213                if ( count( $recipients ) !== 0 ) {
214                    $talkNotification = new RecentChangeNotification(
215                        $agent,
216                        $title,
217                        $recentChange,
218                        $pageStatus,
219                        RecentChangeNotification::WATCHLIST_NOTIFICATION
220                    );
221                    $notifService->notify( $talkNotification, $recipients );
222                }
223            }
224        }
225
226        foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
227            $admins = [];
228            if ( $editor->getUser()->getName() == $name ) {
229                // No point notifying the user that actually made the change!
230                continue;
231            }
232            $user = $userFactory->newFromName( $name );
233            if ( $user instanceof User ) {
234                $admins[] = $user;
235            }
236            $notifService->notify(
237                new RecentChangeNotification(
238                    $agent,
239                    $title,
240                    $recentChange,
241                    $pageStatus,
242                    RecentChangeNotification::ADMIN_NOTIFICATION
243                ),
244                new RecipientSet( $admins )
245            );
246
247        }
248    }
249
250    /**
251     * @param UserIdentity $editor
252     * @param Title $title
253     * @param bool $minorEdit
254     * @return bool
255     */
256    private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
257        $services = MediaWikiServices::getInstance();
258        $config = $services->getMainConfig();
259
260        if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) {
261            return false;
262        }
263
264        $userOptionsLookup = $services->getUserOptionsLookup();
265        $targetUser = User::newFromName( $title->getText() );
266
267        if ( !$targetUser || $targetUser->isAnon() ) {
268            wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
269        } elseif ( $targetUser->getId() == $editor->getId() ) {
270            wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
271        } elseif ( $targetUser->isTemp() ) {
272            wfDebug( __METHOD__ . ": talk page owner is a temporary user so doesn't have email" );
273        } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
274            $targetUser->getBlock()
275        ) {
276            // @TODO Partial blocks should not prevent the user from logging in.
277            //       see: https://phabricator.wikimedia.org/T208895
278            wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
279        } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
280            && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
281        ) {
282            wfDebug( __METHOD__ . ": sending talk page update notification" );
283            return true;
284        } else {
285            wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
286        }
287        return false;
288    }
289
290}
291
292/** @deprecated class alias since 1.45 */
293class_alias( RecentChangeNotifier::class, 'EmailNotification' );