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 PageRevisionUpdatedEvent::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 ],
68 'events' => [ // see registerListeners()
69 PageRevisionUpdatedEvent::TYPE
70 ],
71 ];
72
73 private ChangeTagsStore $changeTagsStore;
74 private UserEditTracker $userEditTracker;
75 private PermissionManager $permissionManager;
76 private WikiPageFactory $wikiPageFactory;
77 private HookRunner $hookRunner;
78 private UserNameUtils $userNameUtils;
79 private TalkPageNotificationManager $talkPageNotificationManager;
80 private JobQueueGroup $jobQueueGroup;
81 private IContentHandlerFactory $contentHandlerFactory;
82 private bool $useRcPatrol;
83 private bool $rcWatchCategoryMembership;
84
85 public function __construct(
86 ChangeTagsStore $changeTagsStore,
87 UserEditTracker $userEditTracker,
88 PermissionManager $permissionManager,
89 WikiPageFactory $wikiPageFactory,
90 HookContainer $hookContainer,
91 UserNameUtils $userNameUtils,
92 TalkPageNotificationManager $talkPageNotificationManager,
93 Config $mainConfig,
94 JobQueueGroup $jobQueueGroup,
95 IContentHandlerFactory $contentHandlerFactory
96 ) {
97 // NOTE: keep in sync with self::OBJECT_SPEC
98 $this->changeTagsStore = $changeTagsStore;
99 $this->userEditTracker = $userEditTracker;
100 $this->permissionManager = $permissionManager;
101 $this->wikiPageFactory = $wikiPageFactory;
102 $this->hookRunner = new HookRunner( $hookContainer );
103 $this->userNameUtils = $userNameUtils;
104 $this->talkPageNotificationManager = $talkPageNotificationManager;
105 $this->jobQueueGroup = $jobQueueGroup;
106 $this->contentHandlerFactory = $contentHandlerFactory;
107
108 $this->useRcPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
109 $this->rcWatchCategoryMembership = $mainConfig->get(
111 );
112 }
113
114 public static function newForTesting(
115 ChangeTagsStore $changeTagsStore,
116 UserEditTracker $userEditTracker,
117 PermissionManager $permissionManager,
118 WikiPageFactory $wikiPageFactory,
119 HookContainer $hookContainer,
120 UserNameUtils $userNameUtils,
121 TalkPageNotificationManager $talkPageNotificationManager,
122 Config $mainConfig,
123 JobQueueGroup $jobQueueGroup,
124 IContentHandlerFactory $contentHandlerFactory
125 ) {
126 $ingress = new self(
127 $changeTagsStore,
128 $userEditTracker,
129 $permissionManager,
130 $wikiPageFactory,
131 $hookContainer,
132 $userNameUtils,
133 $talkPageNotificationManager,
134 $mainConfig,
135 $jobQueueGroup,
136 $contentHandlerFactory
137 );
138 $ingress->initSubscriber( self::OBJECT_SPEC );
139 return $ingress;
140 }
141
142 private static function getEditFlags( PageRevisionUpdatedEvent $event ): int {
143 $flags = $event->isCreation() ? EDIT_NEW : EDIT_UPDATE;
144
145 $flags |= (int)$event->isBotUpdate() * EDIT_FORCE_BOT;
146 $flags |= (int)$event->isSilent() * EDIT_SILENT;
147 $flags |= (int)$event->isImplicit() * EDIT_IMPLICIT;
148 $flags |= (int)$event->getLatestRevisionAfter()->isMinor() * EDIT_MINOR;
149
150 return $flags;
151 }
152
160 if ( $event->changedLatestRevisionId()
161 && !$event->isSilent()
162 ) {
163 $this->updateRecentChangesAfterPageUpdated(
164 $event->getLatestRevisionAfter(),
165 $event->getLatestRevisionBefore(),
166 $event->isBotUpdate(),
167 $event->getPatrolStatus(),
168 $event->getTags(),
169 $event->getEditResult()
170 );
171 } elseif ( $event->getTags() ) {
172 $this->updateChangeTagsAfterPageUpdated(
173 $event->getTags(),
174 $event->getLatestRevisionAfter()->getId(),
175 );
176 }
177
178 if ( $event->isEffectiveContentChange() ) {
179 $this->generateCategoryMembershipChanges( $event );
180
181 if ( !$event->isImplicit() ) {
182 $this->updateUserEditTrackerAfterPageUpdated(
183 $event->getPerformer()
184 );
185
186 $this->updateNewTalkAfterPageUpdated( $event );
187 }
188 }
189
190 if ( $event->isRevert() && $event->isEffectiveContentChange() ) {
191 $this->updateRevertTagAfterPageUpdated( $event );
192 }
193 }
194
202 private function generateCategoryMembershipChanges( PageRevisionUpdatedEvent $event ): void {
203 if ( $this->rcWatchCategoryMembership
204 && !$event->hasCause( PageRevisionUpdatedEvent::CAUSE_UNDELETE )
205 && $this->anyChangedSlotSupportsCategories( $event )
206 ) {
207 // Note: jobs are pushed after deferred updates, so the job should be able to see
208 // the recent change entry (also done via deferred updates) and carry over any
209 // bot/deletion/IP flags, ect.
210 $this->jobQueueGroup->lazyPush(
212 $event->getPage(),
213 $event->getLatestRevisionAfter()->getTimestamp(),
214 $event->hasCause( PageRevisionUpdatedEvent::CAUSE_IMPORT )
215 )
216 );
217 }
218 }
219
225 private function anyChangedSlotSupportsCategories( PageRevisionUpdatedEvent $event ): bool {
226 $slotsUpdate = $event->getSlotsUpdate();
227 foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
228 $model = $slotsUpdate->getModifiedSlot( $role )->getModel();
229
230 if ( $this->contentHandlerFactory->getContentHandler( $model )->supportsCategories() ) {
231 return true;
232 }
233 }
234
235 return false;
236 }
237
238 private function updateChangeTagsAfterPageUpdated( array $tags, int $revId ) {
239 $this->changeTagsStore->addTags( $tags, null, $revId );
240 }
241
242 private function updateRecentChangesAfterPageUpdated(
243 RevisionRecord $newRevisionRecord,
244 ?RevisionRecord $oldRevisionRecord,
245 bool $forceBot,
246 int $patrolStatus,
247 array $tags,
248 ?EditResult $editResult
249 ) {
250 // Update recentchanges
251 if ( !$oldRevisionRecord ) {
252 RecentChange::notifyNew(
253 $newRevisionRecord->getTimestamp(),
254 $newRevisionRecord->getPage(),
255 $newRevisionRecord->isMinor(),
256 $newRevisionRecord->getUser( RevisionRecord::RAW ),
257 $newRevisionRecord->getComment( RevisionRecord::RAW )->text,
258 $forceBot, // $event->hasFlag( EDIT_FORCE_BOT ),
259 '',
260 $newRevisionRecord->getSize(),
261 $newRevisionRecord->getId(),
262 $patrolStatus,
263 $tags
264 );
265 } else {
266 // Add RC row to the DB
267 RecentChange::notifyEdit(
268 $newRevisionRecord->getTimestamp(),
269 $newRevisionRecord->getPage(),
270 $newRevisionRecord->isMinor(),
271 $newRevisionRecord->getUser( RevisionRecord::RAW ),
272 $newRevisionRecord->getComment( RevisionRecord::RAW )->text,
273 $oldRevisionRecord->getId(),
274 $newRevisionRecord->getTimestamp(),
275 $forceBot,
276 '',
277 $oldRevisionRecord->getSize(),
278 $newRevisionRecord->getSize(),
279 $newRevisionRecord->getId(),
280 $patrolStatus,
281 $tags,
282 $editResult
283 );
284 }
285 }
286
287 private function updateUserEditTrackerAfterPageUpdated( UserIdentity $author ) {
288 $this->userEditTracker->incrementUserEditCount( $author );
289 }
290
296 private function updateNewTalkAfterPageUpdated( PageRevisionUpdatedEvent $event ) {
297 // If this is another user's talk page, update newtalk.
298 // Don't do this if $options['changed'] = false (null-edits) nor if
299 // it's a minor edit and the user making the edit doesn't generate notifications for those.
300 $page = $event->getPage();
301 $revRecord = $event->getLatestRevisionAfter();
302 $recipientName = $page->getDBkey();
303 $recipientName = $this->userNameUtils->isIP( $recipientName )
304 ? $recipientName
305 : $this->userNameUtils->getCanonical( $page->getDBkey() );
306
307 if ( $page->getNamespace() === NS_USER_TALK
308 && !( $revRecord->isMinor()
309 && $this->permissionManager->userHasRight(
310 $event->getAuthor(), 'nominornewtalk' ) )
311 && $recipientName != $event->getAuthor()->getName()
312 ) {
313 $recipient = User::newFromName( $recipientName, false );
314 if ( !$recipient ) {
315 wfDebug( __METHOD__ . ": invalid username" );
316 } else {
317 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
318
319 // Allow extensions to prevent user notification
320 // when a new message is added to their talk page
321 if ( $this->hookRunner->onArticleEditUpdateNewTalk( $wikiPage, $recipient ) ) {
322 if ( $this->userNameUtils->isIP( $recipientName ) ) {
323 // An anonymous user
324 $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
325 } elseif ( $recipient->isRegistered() ) {
326 $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
327 } else {
328 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user" );
329 }
330 }
331 }
332 }
333 }
334
335 private function updateRevertTagAfterPageUpdated( PageRevisionUpdatedEvent $event ) {
336 $patrolStatus = $event->getPatrolStatus();
337 $wikiPage = $this->wikiPageFactory->newFromTitle( $event->getPage() );
338
339 // Should the reverted tag update be scheduled right away?
340 // The revert is approved if either patrolling is disabled or the
341 // edit is patrolled or autopatrolled.
342 $approved = !$this->useRcPatrol ||
343 $patrolStatus === RecentChange::PRC_PATROLLED ||
344 $patrolStatus === RecentChange::PRC_AUTOPATROLLED;
345
346 $editResult = $event->getEditResult();
347
348 if ( !$editResult ) {
349 // Reverts should always have an EditResult.
350 throw new LogicException( 'Missing EditResult in revert' );
351 }
352
353 $revisionRecord = $event->getLatestRevisionAfter();
354
355 // Allow extensions to override the patrolling subsystem.
356 $this->hookRunner->onBeforeRevertedTagUpdate(
357 $wikiPage,
358 $event->getAuthor(),
359 $revisionRecord->getComment( RevisionRecord::RAW ),
360 self::getEditFlags( $event ),
361 $revisionRecord,
362 $editResult,
363 $approved
364 );
365
366 // Schedule a deferred update for marking reverted edits if applicable.
367 if ( $approved ) {
368 // Enqueue the job
369 $this->jobQueueGroup->lazyPush(
370 RevertedTagUpdateJob::newSpec(
371 $revisionRecord->getId(),
372 $editResult
373 )
374 );
375 }
376 }
377
378}
const EDIT_FORCE_BOT
Mark the edit a "bot" edit regardless of user rights.
Definition Defines.php:143
const EDIT_UPDATE
Article is assumed to be pre-existing, fail if it doesn't exist.
Definition Defines.php:131
const EDIT_IMPLICIT
The edit is a side effect and does not represent an active user contribution.
Definition Defines.php:155
const NS_USER_TALK
Definition Defines.php:68
const EDIT_SILENT
Do not notify other users (e.g.
Definition Defines.php:137
const EDIT_MINOR
Mark this edit minor, if the user is allowed to do so.
Definition Defines.php:134
const EDIT_NEW
Article is assumed to be non-existent, fail if it exists.
Definition Defines.php:128
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:82
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.
static newSpec(PageIdentity $page, $revisionTimestamp, bool $forImport)
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()
Domain event representing a page update.
getLatestRevisionAfter()
The revision that became the latest as a result of the update.
changedLatestRevisionId()
Whether this event represents a change to the latest revision ID associated with the page.
isBotUpdate()
Whether the update was performed by a bot.
isEffectiveContentChange()
Whether the update effectively changed the content of the page.
getEditResult()
An EditResult representing the effects of the update.
getLatestRevisionBefore()
Returned the revision that used to be latest before 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.
getPatrolStatus()
Returns the page update's initial patrol status.
isRevert()
Whether the update reverts an earlier update to the same page.
getTags()
Returns any tags applied to the edit.
hasCause(string $cause)
Checks whether the update had the given cause.
getPerformer()
Returns the user that performed the update.
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.
static newForTesting(ChangeTagsStore $changeTagsStore, UserEditTracker $userEditTracker, PermissionManager $permissionManager, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, UserNameUtils $userNameUtils, TalkPageNotificationManager $talkPageNotificationManager, Config $mainConfig, JobQueueGroup $jobQueueGroup, IContentHandlerFactory $contentHandlerFactory)
const EVENTS
The events handled by this ingress subscriber.
handlePageRevisionUpdatedEvent(PageRevisionUpdatedEvent $event)
Listener method for PageRevisionUpdatedEvent, 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)
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:123
Interface for configuration instances.
Definition Config.php:32
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Listener interface for PageRevisionUpdatedEvents.
Interface for objects representing user identity.