Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
72.73% |
112 / 154 |
|
36.36% |
4 / 11 |
CRAP | |
0.00% |
0 / 1 |
| ChangeTrackingEventIngress | |
72.73% |
112 / 154 |
|
36.36% |
4 / 11 |
67.29 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| newForTesting | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
| getEditFlags | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| handlePageLatestRevisionChangedEvent | |
75.00% |
18 / 24 |
|
0.00% |
0 / 1 |
9.00 | |||
| generateCategoryMembershipChanges | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| anyChangedSlotSupportsCategories | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| updateChangeTagsAfterPageUpdated | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| updateRecentChangesAfterPageUpdated | |
48.39% |
15 / 31 |
|
0.00% |
0 / 1 |
2.55 | |||
| updateUserEditTrackerAfterPageUpdated | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| updateNewTalkAfterPageUpdated | |
47.62% |
10 / 21 |
|
0.00% |
0 / 1 |
24.37 | |||
| updateRevertTagAfterPageUpdated | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
5 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\RecentChanges; |
| 4 | |
| 5 | use LogicException; |
| 6 | use MediaWiki\ChangeTags\ChangeTagsStore; |
| 7 | use MediaWiki\Config\Config; |
| 8 | use MediaWiki\Content\IContentHandlerFactory; |
| 9 | use MediaWiki\DomainEvent\DomainEventIngress; |
| 10 | use MediaWiki\HookContainer\HookContainer; |
| 11 | use MediaWiki\HookContainer\HookRunner; |
| 12 | use MediaWiki\JobQueue\JobQueueGroup; |
| 13 | use MediaWiki\JobQueue\Jobs\CategoryMembershipChangeJob; |
| 14 | use MediaWiki\JobQueue\Jobs\RevertedTagUpdateJob; |
| 15 | use MediaWiki\MainConfigNames; |
| 16 | use MediaWiki\Page\Event\PageLatestRevisionChangedEvent; |
| 17 | use MediaWiki\Page\Event\PageLatestRevisionChangedListener; |
| 18 | use MediaWiki\Page\WikiPageFactory; |
| 19 | use MediaWiki\Permissions\PermissionManager; |
| 20 | use MediaWiki\Revision\RevisionRecord; |
| 21 | use MediaWiki\Storage\EditResult; |
| 22 | use MediaWiki\User\TalkPageNotificationManager; |
| 23 | use MediaWiki\User\User; |
| 24 | use MediaWiki\User\UserEditTracker; |
| 25 | use MediaWiki\User\UserIdentity; |
| 26 | use MediaWiki\User\UserNameUtils; |
| 27 | |
| 28 | /** |
| 29 | * The ingress subscriber for the change tracking component. It updates change |
| 30 | * tracking state according to domain events coming from other components. |
| 31 | * |
| 32 | * @internal |
| 33 | */ |
| 34 | class ChangeTrackingEventIngress |
| 35 | extends DomainEventIngress |
| 36 | implements PageLatestRevisionChangedListener |
| 37 | { |
| 38 | |
| 39 | /** |
| 40 | * The events handled by this ingress subscriber. |
| 41 | * @see registerListeners() |
| 42 | */ |
| 43 | public const EVENTS = [ |
| 44 | PageLatestRevisionChangedEvent::TYPE |
| 45 | ]; |
| 46 | |
| 47 | /** |
| 48 | * Object spec used for lazy instantiation. |
| 49 | * Using this spec with DomainEventSource::registerSubscriber defers |
| 50 | * instantiation until one of the listed events is dispatched. |
| 51 | * Declaring it as a constant avoids the overhead of using reflection |
| 52 | * for auto-wiring. |
| 53 | */ |
| 54 | public const OBJECT_SPEC = [ |
| 55 | 'class' => self::class, |
| 56 | 'services' => [ // see __construct |
| 57 | 'ChangeTagsStore', |
| 58 | 'UserEditTracker', |
| 59 | 'PermissionManager', |
| 60 | 'WikiPageFactory', |
| 61 | 'HookContainer', |
| 62 | 'UserNameUtils', |
| 63 | 'TalkPageNotificationManager', |
| 64 | 'MainConfig', |
| 65 | 'JobQueueGroup', |
| 66 | 'ContentHandlerFactory', |
| 67 | 'RecentChangeFactory', |
| 68 | ], |
| 69 | 'events' => [ // see registerListeners() |
| 70 | PageLatestRevisionChangedEvent::TYPE |
| 71 | ], |
| 72 | ]; |
| 73 | |
| 74 | private ChangeTagsStore $changeTagsStore; |
| 75 | private UserEditTracker $userEditTracker; |
| 76 | private PermissionManager $permissionManager; |
| 77 | private WikiPageFactory $wikiPageFactory; |
| 78 | private HookRunner $hookRunner; |
| 79 | private UserNameUtils $userNameUtils; |
| 80 | private TalkPageNotificationManager $talkPageNotificationManager; |
| 81 | private JobQueueGroup $jobQueueGroup; |
| 82 | private IContentHandlerFactory $contentHandlerFactory; |
| 83 | private RecentChangeFactory $recentChangeFactory; |
| 84 | private bool $useRcPatrol; |
| 85 | private bool $rcWatchCategoryMembership; |
| 86 | |
| 87 | public function __construct( |
| 88 | ChangeTagsStore $changeTagsStore, |
| 89 | UserEditTracker $userEditTracker, |
| 90 | PermissionManager $permissionManager, |
| 91 | WikiPageFactory $wikiPageFactory, |
| 92 | HookContainer $hookContainer, |
| 93 | UserNameUtils $userNameUtils, |
| 94 | TalkPageNotificationManager $talkPageNotificationManager, |
| 95 | Config $mainConfig, |
| 96 | JobQueueGroup $jobQueueGroup, |
| 97 | IContentHandlerFactory $contentHandlerFactory, |
| 98 | RecentChangeFactory $recentChangeFactory |
| 99 | ) { |
| 100 | // NOTE: keep in sync with self::OBJECT_SPEC |
| 101 | $this->changeTagsStore = $changeTagsStore; |
| 102 | $this->userEditTracker = $userEditTracker; |
| 103 | $this->permissionManager = $permissionManager; |
| 104 | $this->wikiPageFactory = $wikiPageFactory; |
| 105 | $this->hookRunner = new HookRunner( $hookContainer ); |
| 106 | $this->userNameUtils = $userNameUtils; |
| 107 | $this->talkPageNotificationManager = $talkPageNotificationManager; |
| 108 | $this->jobQueueGroup = $jobQueueGroup; |
| 109 | $this->contentHandlerFactory = $contentHandlerFactory; |
| 110 | $this->recentChangeFactory = $recentChangeFactory; |
| 111 | |
| 112 | $this->useRcPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol ); |
| 113 | $this->rcWatchCategoryMembership = $mainConfig->get( |
| 114 | MainConfigNames::RCWatchCategoryMembership |
| 115 | ); |
| 116 | } |
| 117 | |
| 118 | public static function newForTesting( |
| 119 | ChangeTagsStore $changeTagsStore, |
| 120 | UserEditTracker $userEditTracker, |
| 121 | PermissionManager $permissionManager, |
| 122 | WikiPageFactory $wikiPageFactory, |
| 123 | HookContainer $hookContainer, |
| 124 | UserNameUtils $userNameUtils, |
| 125 | TalkPageNotificationManager $talkPageNotificationManager, |
| 126 | Config $mainConfig, |
| 127 | JobQueueGroup $jobQueueGroup, |
| 128 | IContentHandlerFactory $contentHandlerFactory, |
| 129 | RecentChangeFactory $recentChangeFactory |
| 130 | ): self { |
| 131 | $ingress = new self( |
| 132 | $changeTagsStore, |
| 133 | $userEditTracker, |
| 134 | $permissionManager, |
| 135 | $wikiPageFactory, |
| 136 | $hookContainer, |
| 137 | $userNameUtils, |
| 138 | $talkPageNotificationManager, |
| 139 | $mainConfig, |
| 140 | $jobQueueGroup, |
| 141 | $contentHandlerFactory, |
| 142 | $recentChangeFactory |
| 143 | ); |
| 144 | $ingress->initSubscriber( self::OBJECT_SPEC ); |
| 145 | return $ingress; |
| 146 | } |
| 147 | |
| 148 | private static function getEditFlags( PageLatestRevisionChangedEvent $event ): int { |
| 149 | $flags = $event->isCreation() ? EDIT_NEW : EDIT_UPDATE; |
| 150 | |
| 151 | $flags |= (int)$event->isBotUpdate() * EDIT_FORCE_BOT; |
| 152 | $flags |= (int)$event->isSilent() * EDIT_SILENT; |
| 153 | $flags |= (int)$event->isImplicit() * EDIT_IMPLICIT; |
| 154 | $flags |= (int)$event->getLatestRevisionAfter()->isMinor() * EDIT_MINOR; |
| 155 | |
| 156 | return $flags; |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Listener method for PageLatestRevisionChangedEvent, to be registered with an |
| 161 | * DomainEventSource. |
| 162 | * |
| 163 | * @noinspection PhpUnused |
| 164 | */ |
| 165 | public function handlePageLatestRevisionChangedEvent( PageLatestRevisionChangedEvent $event ): void { |
| 166 | if ( $event->changedLatestRevisionId() |
| 167 | && !$event->isSilent() |
| 168 | ) { |
| 169 | $this->updateRecentChangesAfterPageUpdated( |
| 170 | $event->getLatestRevisionAfter(), |
| 171 | $event->getLatestRevisionBefore(), |
| 172 | $event->isBotUpdate(), |
| 173 | $event->getPatrolStatus(), |
| 174 | $event->getTags(), |
| 175 | $event->getEditResult() |
| 176 | ); |
| 177 | } elseif ( $event->getTags() ) { |
| 178 | $this->updateChangeTagsAfterPageUpdated( |
| 179 | $event->getTags(), |
| 180 | $event->getLatestRevisionAfter()->getId(), |
| 181 | ); |
| 182 | } |
| 183 | |
| 184 | if ( $event->isEffectiveContentChange() ) { |
| 185 | $this->generateCategoryMembershipChanges( $event ); |
| 186 | |
| 187 | if ( !$event->isImplicit() ) { |
| 188 | $this->updateUserEditTrackerAfterPageUpdated( |
| 189 | $event->getPerformer() |
| 190 | ); |
| 191 | |
| 192 | $this->updateNewTalkAfterPageUpdated( $event ); |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | if ( $event->isRevert() && $event->isEffectiveContentChange() ) { |
| 197 | $this->updateRevertTagAfterPageUpdated( $event ); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Create RC entries for category changes that resulted from this update |
| 203 | * if the relevant config is enabled. |
| 204 | * This should only be triggered for actual edits, not reconciliation events (T390636). |
| 205 | * |
| 206 | * @param PageLatestRevisionChangedEvent $event |
| 207 | */ |
| 208 | private function generateCategoryMembershipChanges( PageLatestRevisionChangedEvent $event ): void { |
| 209 | if ( $this->rcWatchCategoryMembership |
| 210 | && !$event->hasCause( PageLatestRevisionChangedEvent::CAUSE_UNDELETE ) |
| 211 | && $this->anyChangedSlotSupportsCategories( $event ) |
| 212 | ) { |
| 213 | // Note: jobs are pushed after deferred updates, so the job should be able to see |
| 214 | // the recent change entry (also done via deferred updates) and carry over any |
| 215 | // bot/deletion/IP flags, etc. |
| 216 | $this->jobQueueGroup->lazyPush( |
| 217 | CategoryMembershipChangeJob::newSpec( |
| 218 | $event->getPage(), |
| 219 | $event->getLatestRevisionAfter()->getTimestamp(), |
| 220 | $event->hasCause( PageLatestRevisionChangedEvent::CAUSE_IMPORT ) |
| 221 | ) |
| 222 | ); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Determine whether any slots changed in this update supports categories. |
| 228 | * |
| 229 | * @param PageLatestRevisionChangedEvent $event |
| 230 | * |
| 231 | * @return bool |
| 232 | */ |
| 233 | private function anyChangedSlotSupportsCategories( PageLatestRevisionChangedEvent $event ): bool { |
| 234 | $slotsUpdate = $event->getSlotsUpdate(); |
| 235 | foreach ( $slotsUpdate->getModifiedRoles() as $role ) { |
| 236 | $model = $slotsUpdate->getModifiedSlot( $role )->getModel(); |
| 237 | |
| 238 | if ( $this->contentHandlerFactory->getContentHandler( $model )->supportsCategories() ) { |
| 239 | return true; |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | return false; |
| 244 | } |
| 245 | |
| 246 | private function updateChangeTagsAfterPageUpdated( array $tags, int $revId ) { |
| 247 | $this->changeTagsStore->addTags( $tags, null, $revId ); |
| 248 | } |
| 249 | |
| 250 | private function updateRecentChangesAfterPageUpdated( |
| 251 | RevisionRecord $newRevisionRecord, |
| 252 | ?RevisionRecord $oldRevisionRecord, |
| 253 | bool $forceBot, |
| 254 | int $patrolStatus, |
| 255 | array $tags, |
| 256 | ?EditResult $editResult |
| 257 | ) { |
| 258 | if ( !$oldRevisionRecord ) { |
| 259 | $recentChange = $this->recentChangeFactory->createNewPageRecentChange( |
| 260 | $newRevisionRecord->getTimestamp(), |
| 261 | $newRevisionRecord->getPage(), |
| 262 | $newRevisionRecord->isMinor(), |
| 263 | $newRevisionRecord->getUser( RevisionRecord::RAW ), |
| 264 | $newRevisionRecord->getComment( RevisionRecord::RAW )->text, |
| 265 | $forceBot, // $event->hasFlag( EDIT_FORCE_BOT ), |
| 266 | '', |
| 267 | $newRevisionRecord->getSize(), |
| 268 | $newRevisionRecord->getId(), |
| 269 | $patrolStatus, |
| 270 | $tags |
| 271 | ); |
| 272 | } else { |
| 273 | $recentChange = $this->recentChangeFactory->createEditRecentChange( |
| 274 | $newRevisionRecord->getTimestamp(), |
| 275 | $newRevisionRecord->getPage(), |
| 276 | $newRevisionRecord->isMinor(), |
| 277 | $newRevisionRecord->getUser( RevisionRecord::RAW ), |
| 278 | $newRevisionRecord->getComment( RevisionRecord::RAW )->text, |
| 279 | $oldRevisionRecord->getId(), |
| 280 | $forceBot, |
| 281 | '', |
| 282 | $oldRevisionRecord->getSize(), |
| 283 | $newRevisionRecord->getSize(), |
| 284 | $newRevisionRecord->getId(), |
| 285 | $patrolStatus, |
| 286 | $tags, |
| 287 | $editResult |
| 288 | ); |
| 289 | } |
| 290 | |
| 291 | $this->recentChangeFactory->insertRecentChange( $recentChange ); |
| 292 | } |
| 293 | |
| 294 | private function updateUserEditTrackerAfterPageUpdated( UserIdentity $author ) { |
| 295 | $this->userEditTracker->incrementUserEditCount( $author ); |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Listener method for PageLatestRevisionChangedEvent, to be registered with a DomainEventSource. |
| 300 | * |
| 301 | * @noinspection PhpUnused |
| 302 | */ |
| 303 | private function updateNewTalkAfterPageUpdated( PageLatestRevisionChangedEvent $event ) { |
| 304 | // If this is another user's talk page, update newtalk. |
| 305 | // Don't do this if $options['changed'] = false (null-edits) nor if |
| 306 | // it's a minor edit and the user making the edit doesn't generate notifications for those. |
| 307 | $page = $event->getPage(); |
| 308 | $revRecord = $event->getLatestRevisionAfter(); |
| 309 | $recipientName = $page->getDBkey(); |
| 310 | $recipientName = $this->userNameUtils->isIP( $recipientName ) |
| 311 | ? $recipientName |
| 312 | : $this->userNameUtils->getCanonical( $page->getDBkey() ); |
| 313 | |
| 314 | if ( $page->getNamespace() === NS_USER_TALK |
| 315 | && !( $revRecord->isMinor() |
| 316 | && $this->permissionManager->userHasRight( |
| 317 | $event->getAuthor(), 'nominornewtalk' ) ) |
| 318 | && $recipientName != $event->getAuthor()->getName() |
| 319 | ) { |
| 320 | $recipient = User::newFromName( $recipientName, false ); |
| 321 | if ( !$recipient ) { |
| 322 | wfDebug( __METHOD__ . ": invalid username" ); |
| 323 | } else { |
| 324 | $wikiPage = $this->wikiPageFactory->newFromTitle( $page ); |
| 325 | |
| 326 | // Allow extensions to prevent user notification |
| 327 | // when a new message is added to their talk page |
| 328 | if ( $this->hookRunner->onArticleEditUpdateNewTalk( $wikiPage, $recipient ) ) { |
| 329 | if ( $this->userNameUtils->isIP( $recipientName ) ) { |
| 330 | // An anonymous user |
| 331 | $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord ); |
| 332 | } elseif ( $recipient->isRegistered() ) { |
| 333 | $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord ); |
| 334 | } else { |
| 335 | wfDebug( __METHOD__ . ": don't need to notify a nonexistent user" ); |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | private function updateRevertTagAfterPageUpdated( PageLatestRevisionChangedEvent $event ) { |
| 343 | $patrolStatus = $event->getPatrolStatus(); |
| 344 | $wikiPage = $this->wikiPageFactory->newFromTitle( $event->getPage() ); |
| 345 | |
| 346 | // Should the reverted tag update be scheduled right away? |
| 347 | // The revert is approved if either patrolling is disabled or the |
| 348 | // edit is patrolled or autopatrolled. |
| 349 | $approved = !$this->useRcPatrol || |
| 350 | $patrolStatus === RecentChange::PRC_PATROLLED || |
| 351 | $patrolStatus === RecentChange::PRC_AUTOPATROLLED; |
| 352 | |
| 353 | $editResult = $event->getEditResult(); |
| 354 | |
| 355 | if ( !$editResult ) { |
| 356 | // Reverts should always have an EditResult. |
| 357 | throw new LogicException( 'Missing EditResult in revert' ); |
| 358 | } |
| 359 | |
| 360 | $revisionRecord = $event->getLatestRevisionAfter(); |
| 361 | |
| 362 | // Allow extensions to override the patrolling subsystem. |
| 363 | $this->hookRunner->onBeforeRevertedTagUpdate( |
| 364 | $wikiPage, |
| 365 | $event->getAuthor(), |
| 366 | $revisionRecord->getComment( RevisionRecord::RAW ), |
| 367 | self::getEditFlags( $event ), |
| 368 | $revisionRecord, |
| 369 | $editResult, |
| 370 | $approved |
| 371 | ); |
| 372 | |
| 373 | // Schedule a deferred update for marking reverted edits if applicable. |
| 374 | if ( $approved ) { |
| 375 | // Enqueue the job |
| 376 | $this->jobQueueGroup->lazyPush( |
| 377 | RevertedTagUpdateJob::newSpec( |
| 378 | $revisionRecord->getId(), |
| 379 | $editResult |
| 380 | ) |
| 381 | ); |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | } |