Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
32.58% |
43 / 132 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
| RecentChangeNotifier | |
32.82% |
43 / 131 |
|
0.00% |
0 / 4 |
746.42 | |
0.00% |
0 / 1 |
| notifyOnPageChange | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
7.01 | |||
| shouldSendNotification | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
7.39 | |||
| actuallyNotifyOnPageChange | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
506 | |||
| canSendUserTalkEmail | |
19.05% |
4 / 21 |
|
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 | |
| 11 | namespace MediaWiki\RecentChanges; |
| 12 | |
| 13 | use MediaWiki\Config\Config; |
| 14 | use MediaWiki\HookContainer\HookRunner; |
| 15 | use MediaWiki\MainConfigNames; |
| 16 | use MediaWiki\MediaWikiServices; |
| 17 | use MediaWiki\Notification\RecipientSet; |
| 18 | use MediaWiki\Permissions\Authority; |
| 19 | use MediaWiki\Title\Title; |
| 20 | use MediaWiki\User\User; |
| 21 | use MediaWiki\User\UserArray; |
| 22 | use MediaWiki\User\UserIdentity; |
| 23 | use 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 | */ |
| 41 | class 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 */ |
| 292 | class_alias( RecentChangeNotifier::class, 'EmailNotification' ); |