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