MediaWiki master
ChangeTrackingEventIngress.php
Go to the documentation of this file.
1<?php
2
4
5use LogicException;
27
35 extends DomainEventIngress
37{
38
43 public const EVENTS = [
44 PageLatestRevisionChangedEvent::TYPE
45 ];
46
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(
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
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
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
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
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}
const EDIT_FORCE_BOT
Mark the edit a "bot" edit regardless of user rights.
Definition Defines.php:129
const EDIT_UPDATE
Article is assumed to be pre-existing, fail if it doesn't exist.
Definition Defines.php:117
const EDIT_IMPLICIT
The edit is a side effect and does not represent an active user contribution.
Definition Defines.php:141
const NS_USER_TALK
Definition Defines.php:54
const EDIT_SILENT
Do not notify other users (e.g.
Definition Defines.php:123
const EDIT_MINOR
Mark this edit minor, if the user is allowed to do so.
Definition Defines.php:120
const EDIT_NEW
Article is assumed to be non-existent, fail if it exists.
Definition Defines.php:114
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
Read-write access to the change_tags table.
Base class for event ingress objects.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Handle enqueueing of background jobs.
Job to add recent change entries mentioning category membership changes.
Job for deferring the execution of RevertedTagUpdate.
A class containing constants representing the names of configuration variables.
const UseRCPatrol
Name constant for the UseRCPatrol setting, for use with Config::get()
const RCWatchCategoryMembership
Name constant for the RCWatchCategoryMembership setting, for use with Config::get()
getTags()
Returns any tags applied to the edit.
getPerformer()
Returns the user that performed the update.
Definition PageEvent.php:97
Domain event representing a change to the page's latest revision.
isRevert()
Whether the update reverts an earlier update to the same page.
getLatestRevisionBefore()
Returned the revision that used to be latest before the update.
getPatrolStatus()
Returns the page update's initial patrol status.
isEffectiveContentChange()
Whether the update effectively changed the content of the page.
getLatestRevisionAfter()
The revision that became the latest as a result of the update.
getEditResult()
An EditResult representing the effects of the update.
isImplicit()
Whether the update was performed automatically without the user's initiative.
isSilent()
Whether the update should be omitted from update feeds presented to the user.
Service for creating WikiPage objects.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
The ingress subscriber for the change tracking component.
const OBJECT_SPEC
Object spec used for lazy instantiation.
const EVENTS
The events handled by this ingress subscriber.
static newForTesting(ChangeTagsStore $changeTagsStore, UserEditTracker $userEditTracker, PermissionManager $permissionManager, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, UserNameUtils $userNameUtils, TalkPageNotificationManager $talkPageNotificationManager, Config $mainConfig, JobQueueGroup $jobQueueGroup, IContentHandlerFactory $contentHandlerFactory, RecentChangeFactory $recentChangeFactory)
handlePageLatestRevisionChangedEvent(PageLatestRevisionChangedEvent $event)
Listener method for PageLatestRevisionChangedEvent, to be registered with an DomainEventSource.
__construct(ChangeTagsStore $changeTagsStore, UserEditTracker $userEditTracker, PermissionManager $permissionManager, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, UserNameUtils $userNameUtils, TalkPageNotificationManager $talkPageNotificationManager, Config $mainConfig, JobQueueGroup $jobQueueGroup, IContentHandlerFactory $contentHandlerFactory, RecentChangeFactory $recentChangeFactory)
Page revision base class.
Object for storing information about the effects of an edit.
Track info about user edit counts and timings.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:110
Interface for configuration instances.
Definition Config.php:18
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Listener interface for PageLatestRevisionChangedEvents.
Interface for objects representing user identity.