Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 236
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventBusHooks
0.00% covered (danger)
0.00%
0 / 236
0.00% covered (danger)
0.00%
0 / 14
1190
0.00% covered (danger)
0.00%
0 / 1
 getRevisionLookup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendResourceChangedEvent
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 onPageDeleteComplete
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 onPageUndeleteComplete
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 onPageMoveComplete
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 onArticleRevisionVisibilitySet
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 onArticlePurge
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onRevisionRecordInserted
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 sendRevisionCreateEvent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 onBlockIpComplete
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 onLinksUpdateComplete
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
110
 onArticleProtectComplete
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsAfterUpdateTags
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Hooks for production of events to an HTTP service.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @author Eric Evans, Andrew Otto
23 */
24
25namespace MediaWiki\Extension\EventBus;
26
27use IDBAccessObject;
28use ManualLogEntry;
29use MediaWiki\Block\DatabaseBlock;
30use MediaWiki\ChangeTags\Hook\ChangeTagsAfterUpdateTagsHook;
31use MediaWiki\Deferred\DeferredUpdates;
32use MediaWiki\Deferred\LinksUpdate\LinksTable;
33use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
34use MediaWiki\Extension\EventBus\HookHandlers\MediaWiki\PageChangeHooks;
35use MediaWiki\Hook\ArticleRevisionVisibilitySetHook;
36use MediaWiki\Hook\BlockIpCompleteHook;
37use MediaWiki\Hook\LinksUpdateCompleteHook;
38use MediaWiki\Hook\PageMoveCompleteHook;
39use MediaWiki\Linker\LinkTarget;
40use MediaWiki\MediaWikiServices;
41use MediaWiki\Page\Hook\ArticleProtectCompleteHook;
42use MediaWiki\Page\Hook\ArticlePurgeHook;
43use MediaWiki\Page\Hook\PageDeleteCompleteHook;
44use MediaWiki\Page\Hook\PageUndeleteCompleteHook;
45use MediaWiki\Page\ProperPageIdentity;
46use MediaWiki\Permissions\Authority;
47use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
48use MediaWiki\Revision\RevisionLookup;
49use MediaWiki\Revision\RevisionRecord;
50use MediaWiki\Storage\EditResult;
51use MediaWiki\Storage\Hook\PageSaveCompleteHook;
52use MediaWiki\Title\Title;
53use MediaWiki\User\User;
54use MediaWiki\User\UserIdentity;
55use RecentChange;
56use RequestContext;
57use Wikimedia\Assert\Assert;
58use WikiPage;
59
60/**
61 * @deprecated since EventBus 0.5.0 Use specific feature based hooks in HookHandlers/,
62 *     or even better, put them in your own extension instead of in EventBus.
63 */
64class EventBusHooks implements
65    PageSaveCompleteHook,
66    PageMoveCompleteHook,
67    PageDeleteCompleteHook,
68    PageUndeleteCompleteHook,
69    ArticleRevisionVisibilitySetHook,
70    ArticlePurgeHook,
71    BlockIpCompleteHook,
72    LinksUpdateCompleteHook,
73    ArticleProtectCompleteHook,
74    ChangeTagsAfterUpdateTagsHook,
75    RevisionRecordInsertedHook
76{
77
78    /**
79     * @return RevisionLookup
80     */
81    private static function getRevisionLookup(): RevisionLookup {
82        return MediaWikiServices::getInstance()->getRevisionLookup();
83    }
84
85    /**
86     * Creates and sends a single resource_change event to EventBus
87     *
88     * @param LinkTarget $title article title object
89     * @param array $tags the array of tags to use in the event
90     */
91    private static function sendResourceChangedEvent(
92        LinkTarget $title,
93        array $tags
94    ) {
95        $stream = 'resource_change';
96        $eventbus = EventBus::getInstanceForStream( $stream );
97        $event = $eventbus->getFactory()->createResourceChangeEvent( $stream, $title, $tags );
98
99        DeferredUpdates::addCallableUpdate( static function () use ( $eventbus, $event ) {
100            $eventbus->send( [ $event ] );
101        } );
102    }
103
104    /**
105     * Occurs after the delete article request has been processed.
106     *
107     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleDeleteComplete
108     *
109     * @param ProperPageIdentity $page Page that was deleted.
110     * @param Authority $deleter Who deleted the page
111     * @param string $reason Reason the page was deleted
112     * @param int $pageID ID of the page that was deleted
113     * @param RevisionRecord $deletedRev Last revision of the deleted page
114     * @param ManualLogEntry $logEntry ManualLogEntry used to record the deletion
115     * @param int $archivedRevisionCount Number of revisions archived during the deletion
116     * @return true|void
117     */
118    public function onPageDeleteComplete(
119        ProperPageIdentity $page,
120        Authority $deleter,
121        string $reason,
122        int $pageID,
123        RevisionRecord $deletedRev,
124        ManualLogEntry $logEntry,
125        int $archivedRevisionCount
126    ) {
127        $stream = $logEntry->getType() === 'suppress' ?
128            'mediawiki.page-suppress' : 'mediawiki.page-delete';
129        $eventbus = EventBus::getInstanceForStream( $stream );
130
131        // Don't set performer in the event if this delete suppresses the page from other admins.
132        // https://phabricator.wikimedia.org/T342487
133        $performerForEvent = $logEntry->getType() == 'suppress' ? null : $deleter->getUser();
134
135        $eventBusFactory = $eventbus->getFactory();
136        $eventBusFactory->setCommentFormatter( MediaWikiServices::getInstance()->getCommentFormatter() );
137        $title = Title::castFromPageIdentity( $page );
138        Assert::postcondition( $title !== null, '$page can be cast to a LinkTarget' );
139
140        $event = $eventBusFactory->createPageDeleteEvent(
141            $stream,
142            $performerForEvent,
143            $pageID,
144            $title,
145            $title->isRedirect(),
146            $archivedRevisionCount,
147            $deletedRev,
148            $reason
149        );
150
151        DeferredUpdates::addCallableUpdate(
152            static function () use ( $eventbus, $event ) {
153                $eventbus->send( [ $event ] );
154            }
155        );
156    }
157
158    /**
159     * When one or more revisions of an article are restored.
160     *
161     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageUndeleteComplete
162     *
163     * @param ProperPageIdentity $page Page that was undeleted.
164     * @param Authority $restorer Who undeleted the page
165     * @param string $reason Reason the page was undeleted
166     * @param RevisionRecord $restoredRev Last revision of the undeleted page
167     * @param ManualLogEntry $logEntry Log entry generated by the restoration
168     * @param int $restoredRevisionCount Number of revisions restored during the deletion
169     * @param bool $created Whether the undeletion result in a page being created
170     * @param array $restoredPageIds Array of all undeleted page IDs.
171     *        This will have multiple page IDs if there was more than one deleted page with the same page title.
172     * @return void This hook must not abort, it must return no value
173     */
174    public function onPageUndeleteComplete(
175        ProperPageIdentity $page,
176        Authority $restorer,
177        string $reason,
178        RevisionRecord $restoredRev,
179        ManualLogEntry $logEntry,
180        int $restoredRevisionCount,
181        bool $created,
182        array $restoredPageIds
183    ): void {
184        $stream = 'mediawiki.page-undelete';
185        $eventBus = EventBus::getInstanceForStream( $stream );
186        $eventBusFactory = $eventBus->getFactory();
187        $eventBusFactory->setCommentFormatter( MediaWikiServices::getInstance()->getCommentFormatter() );
188
189        $event = $eventBusFactory->createPageUndeleteEvent(
190            $stream,
191            $restorer->getUser(),
192            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable PageIdentity is not null
193            Title::castFromPageIdentity( $page ),
194            $reason,
195            $page->getId(),
196            $restoredRev,
197        );
198
199        DeferredUpdates::addCallableUpdate( static function () use ( $eventBus, $event ) {
200            $eventBus->send( [ $event ] );
201        } );
202    }
203
204    /**
205     * Occurs whenever a request to move an article is completed.
206     *
207     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageMoveComplete
208     *
209     * @param LinkTarget $oldTitle the old title
210     * @param LinkTarget $newTitle the new title
211     * @param UserIdentity $userIdentity User who did the move
212     * @param int $pageid database page_id of the page that's been moved
213     * @param int $redirid database page_id of the created redirect, or 0 if suppressed
214     * @param string $reason reason for the move
215     * @param RevisionRecord $newRevisionRecord revision created by the move
216     */
217    public function onPageMoveComplete(
218        $oldTitle,
219        $newTitle,
220        $userIdentity,
221        $pageid,
222        $redirid,
223        $reason,
224        $newRevisionRecord
225    ) {
226        $stream = 'mediawiki.page-move';
227        $eventBus = EventBus::getInstanceForStream( $stream );
228        $eventBusFactory = $eventBus->getFactory();
229        $eventBusFactory->setCommentFormatter( MediaWikiServices::getInstance()->getCommentFormatter() );
230        $event = $eventBusFactory->createPageMoveEvent(
231            $stream,
232            $oldTitle,
233            $newTitle,
234            $newRevisionRecord,
235            $userIdentity,
236            $reason
237        );
238
239        DeferredUpdates::addCallableUpdate(
240            static function () use ( $eventBus, $event ) {
241                $eventBus->send( [ $event ] );
242            }
243        );
244    }
245
246    /**
247     * Called when changing visibility of one or more revisions of an article.
248     * Produces mediawiki.revision-visibility-change events.
249     *
250     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleRevisionVisibilitySet
251     *
252     * @param Title $title title object of the article
253     * @param array $revIds array of integer revision IDs
254     * @param array $visibilityChangeMap map of revision id to oldBits and newBits.
255     *              This array can be examined to determine exactly what visibility
256     *              bits have changed for each revision.  This array is of the form
257     *              [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
258     */
259    public function onArticleRevisionVisibilitySet(
260        $title,
261        $revIds,
262        $visibilityChangeMap
263    ) {
264        $stream = 'mediawiki.revision-visibility-change';
265        $events = [];
266        $eventBus = EventBus::getInstanceForStream( $stream );
267        // https://phabricator.wikimedia.org/T321411
268        $performer = RequestContext::getMain()->getUser();
269        $performer->loadFromId();
270
271        // Create an event for each revId that was changed.
272        foreach ( $revIds as $revId ) {
273            // Read from primary since due to replication lag the updated field visibility
274            // might still not be available on a replica and we are at risk of leaking
275            // just suppressed data.
276            $revision = self::getRevisionLookup()
277                ->getRevisionById( $revId, IDBAccessObject::READ_LATEST );
278
279            // If the page is deleted simultaneously (null $revision) or if
280            // this revId is not in the $visibilityChangeMap, then we can't
281            // send a meaningful event.
282            if ( $revision === null ) {
283                wfDebug(
284                    __METHOD__ . ' revision ' . $revId .
285                    ' could not be found and may have been deleted. Cannot ' .
286                    "create mediawiki/revision/visibility-change event.\n"
287                );
288                continue;
289            } elseif ( !array_key_exists( $revId, $visibilityChangeMap ) ) {
290                // This should not happen, log it.
291                wfDebug(
292                    __METHOD__ . ' revision id ' . $revId .
293                    ' not found in visibilityChangeMap. Cannot create ' .
294                    "mediawiki/revision/visibility-change event.\n"
295                );
296                continue;
297            } else {
298                $eventBusFactory = $eventBus->getFactory();
299                $eventBusFactory->setCommentFormatter( MediaWikiServices::getInstance()->getCommentFormatter() );
300
301                // If this revision is 'suppressed' AKA restricted, then the person performing
302                // 'RevisionDelete' should not be visible in public data.
303                // https://phabricator.wikimedia.org/T342487
304                //
305                // NOTE: This event stream tries to match the visibility of MediaWiki core logs,
306                // where regular delete/revision events are public, and suppress/revision events
307                // are private. In MediaWiki core logs, private events are fully hidden from
308                // the public.  Here, we need to produce a 'private' event to the
309                // mediawiki.page_change stream, to indicate to consumers that
310                // they should also 'suppress' the revision.  When this is done, we need to
311                // make sure that we do not reproduce the data that has been suppressed
312                // in the event itself.  E.g. if the username of the editor of the revision has been
313                // suppressed, we should not include any information about that editor in the event.
314                $performerForEvent = PageChangeHooks::isSecretRevisionVisibilityChange(
315                    $visibilityChangeMap[$revId]['oldBits'],
316                    $visibilityChangeMap[$revId]['newBits']
317                ) ? null : $performer;
318
319                $events[] = $eventBusFactory->createRevisionVisibilityChangeEvent(
320                    $stream,
321                    $revision,
322                    $performerForEvent,
323                    $visibilityChangeMap[$revId]
324                );
325            }
326        }
327
328        if ( $events === [] ) {
329            // For revision-visibility-set it's possible that
330            // the page was deleted simultaneously and we can not
331            // send a meaningful event.
332            return;
333        }
334
335        DeferredUpdates::addCallableUpdate(
336            static function () use ( $eventBus, $events ) {
337                $eventBus->send( $events );
338            }
339        );
340    }
341
342    /**
343     * Callback for article purge.
344     *
345     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticlePurge
346     *
347     * @param WikiPage $wikiPage
348     */
349    public function onArticlePurge( $wikiPage ) {
350        self::sendResourceChangedEvent( $wikiPage->getTitle(), [ 'purge' ] );
351    }
352
353    /**
354     * Occurs after the save page request has been processed.
355     *
356     * Sends an event if the new revision was also a page creation
357     *
358     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
359     *
360     * @param WikiPage $wikiPage
361     * @param UserIdentity $userIdentity
362     * @param string $summary
363     * @param int $flags
364     * @param RevisionRecord $revisionRecord
365     * @param EditResult $editResult
366     */
367    public function onPageSaveComplete(
368        $wikiPage,
369        $userIdentity,
370        $summary,
371        $flags,
372        $revisionRecord,
373        $editResult
374    ) {
375        if ( $editResult->isNullEdit() ) {
376            self::sendResourceChangedEvent( $wikiPage->getTitle(), [ 'null_edit' ] );
377            return;
378        }
379
380        if ( $flags & EDIT_NEW ) {
381            // Not just a new revision, but a new page
382            self::sendRevisionCreateEvent(
383                'mediawiki.page-create',
384                $revisionRecord
385            );
386        }
387    }
388
389    /**
390     * Occurs after a revision is inserted into the database.
391     *
392     * @see https://www.mediawiki.org/wiki/Manual:Hooks/RevisionRecordInserted
393     *
394     * @param RevisionRecord $revisionRecord RevisionRecord that has just been inserted
395     */
396    public function onRevisionRecordInserted( $revisionRecord ) {
397        self::sendRevisionCreateEvent(
398            'mediawiki.revision-create',
399            $revisionRecord
400        );
401    }
402
403    /**
404     * @param string $stream
405     * @param RevisionRecord $revisionRecord
406     */
407    private static function sendRevisionCreateEvent(
408        string $stream,
409        RevisionRecord $revisionRecord
410    ) {
411        $eventBus = EventBus::getInstanceForStream( $stream );
412        $eventBusFactory = $eventBus->getFactory();
413        $eventBusFactory->setCommentFormatter(
414            MediaWikiServices::getInstance()->getCommentFormatter()
415        );
416        $event = $eventBusFactory->createRevisionCreateEvent(
417            $stream,
418            $revisionRecord
419        );
420
421        DeferredUpdates::addCallableUpdate(
422            static function () use ( $eventBus, $event ) {
423                $eventBus->send( [ $event ] );
424            }
425        );
426    }
427
428    /**
429     * Occurs after the request to block an IP or user has been processed
430     *
431     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BlockIpComplete
432     *
433     * @param DatabaseBlock $block the block object that was saved
434     * @param User $user the user who did the block (not the one being blocked)
435     * @param DatabaseBlock|null $previousBlock the previous block state for the block target.
436     *        null if this is a new block target.
437     */
438    public function onBlockIpComplete(
439        $block,
440        $user,
441        $previousBlock
442    ) {
443        $stream = 'mediawiki.user-blocks-change';
444        $eventBus = EventBus::getInstanceForStream( 'mediawiki.user-blocks-change' );
445        $eventFactory = $eventBus->getFactory();
446        $event = $eventFactory->createUserBlockChangeEvent(
447            $stream, $user, $block, $previousBlock );
448
449        DeferredUpdates::addCallableUpdate(
450            static function () use ( $eventBus, $event ) {
451                $eventBus->send( [ $event ] );
452            }
453        );
454    }
455
456    /**
457     * Sends page-properties-change and page-links-change events
458     *
459     * Emits two events separately: one when the page properties change, and
460     * the other when links are added to or removed from the page.
461     *
462     * @see https://www.mediawiki.org/wiki/Manual:Hooks/LinksUpdateComplete
463     *
464     * @param LinksUpdate $linksUpdate the update object
465     * @param mixed $ticket
466     */
467    public function onLinksUpdateComplete(
468        $linksUpdate, $ticket
469    ) {
470        $addedProps = $linksUpdate->getAddedProperties();
471        $removedProps = $linksUpdate->getRemovedProperties();
472        $arePropsEmpty = !$removedProps && !$addedProps;
473
474        $addedLinks = $linksUpdate->getPageReferenceArray( 'pagelinks', LinksTable::INSERTED );
475        $addedExternalLinks = $linksUpdate->getAddedExternalLinks();
476        $removedLinks = $linksUpdate->getPageReferenceArray( 'pagelinks', LinksTable::DELETED );
477        $removedExternalLinks = $linksUpdate->getRemovedExternalLinks();
478        $areLinksEmpty = !$removedLinks && !$addedLinks
479            && !$removedExternalLinks && !$addedExternalLinks;
480
481        if ( $arePropsEmpty && $areLinksEmpty ) {
482            return;
483        }
484
485        $title = $linksUpdate->getTitle();
486        $user = $linksUpdate->getTriggeringUser();
487
488        // Use triggering revision's rev_id if it is set.
489        // If the LinksUpdate didn't have a triggering revision
490        // (probably because it was triggered by sysadmin maintenance).
491        // Use the page's latest revision.
492        $revRecord = $linksUpdate->getRevisionRecord();
493        if ( $revRecord ) {
494            $revId = $revRecord->getId();
495        } else {
496            $revId = $title->getLatestRevID();
497        }
498        $pageId = $linksUpdate->getPageId();
499
500        if ( !$arePropsEmpty ) {
501            $stream = 'mediawiki.page-properties-change';
502            $eventBus = EventBus::getInstanceForStream( $stream );
503            $eventFactory = $eventBus->getFactory();
504            $propEvent = $eventFactory->createPagePropertiesChangeEvent(
505                $stream,
506                $title,
507                $addedProps,
508                $removedProps,
509                $user,
510                $revId,
511                $pageId
512            );
513
514            DeferredUpdates::addCallableUpdate(
515                static function () use ( $eventBus, $propEvent ) {
516                    $eventBus->send( [ $propEvent ] );
517                }
518            );
519        }
520
521        if ( !$areLinksEmpty ) {
522            $stream = 'mediawiki.page-properties-change';
523            $eventBus = EventBus::getInstanceForStream( $stream );
524            $eventFactory = $eventBus->getFactory();
525            $linkEvent = $eventFactory->createPageLinksChangeEvent(
526                'mediawiki.page-links-change',
527                $title,
528                $addedLinks,
529                $addedExternalLinks,
530                $removedLinks,
531                $removedExternalLinks,
532                $user,
533                $revId,
534                $pageId
535            );
536
537            DeferredUpdates::addCallableUpdate(
538                static function () use ( $eventBus, $linkEvent ) {
539                    $eventBus->send( [ $linkEvent ] );
540                }
541            );
542        }
543    }
544
545    /**
546     * Sends a page-restrictions-change event
547     *
548     * @param WikiPage $wikiPage the article which restrictions were changed
549     * @param User $user the user who have changed the article
550     * @param string[] $protect set of new restrictions details
551     * @param string $reason the reason for page protection
552     */
553    public function onArticleProtectComplete(
554        $wikiPage,
555        $user,
556        $protect,
557        $reason
558    ) {
559        $stream = 'mediawiki.page-restrictions-change';
560        $eventBus = EventBus::getInstanceForStream( $stream );
561        $eventFactory = $eventBus->getFactory();
562
563        $event = $eventFactory->createPageRestrictionsChangeEvent(
564            $stream,
565            $user,
566            $wikiPage->getTitle(),
567            $wikiPage->getId(),
568            $wikiPage->getRevisionRecord(),
569            $wikiPage->isRedirect(),
570            $reason,
571            $protect
572        );
573
574        DeferredUpdates::addCallableUpdate(
575            static function () use ( $eventBus, $event ) {
576                $eventBus->send( [ $event ] );
577            }
578        );
579    }
580
581    /** Called after tags have been updated with the ChangeTags::updateTags function.
582     *
583     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsAfterUpdateTags
584     *
585     * @param array $addedTags tags effectively added in the update
586     * @param array $removedTags tags effectively removed in the update
587     * @param array $prevTags tags that were present prior to the update
588     * @param int|null $rc_id recentchanges table id
589     * @param int|null $rev_id revision table id
590     * @param int|null $log_id logging table id
591     * @param string|null $params tag params
592     * @param RecentChange|null $rc RecentChange being tagged when the tagging accompanies
593     * the action, or null
594     * @param User|null $user User who performed the tagging when the tagging is subsequent
595     * to the action, or null
596     */
597    public function onChangeTagsAfterUpdateTags(
598        $addedTags,
599        $removedTags,
600        $prevTags,
601        $rc_id,
602        $rev_id,
603        $log_id,
604        $params,
605        $rc,
606        $user
607    ) {
608        if ( $rev_id === null ) {
609            // We're only interested for revision (edits) tags for now.
610            return;
611        }
612
613        $revisionRecord = self::getRevisionLookup()->getRevisionById( $rev_id );
614        if ( $revisionRecord === null ) {
615            // Revision might already have been deleted, so we're not interested in tagging those.
616            return;
617        }
618
619        $stream = 'mediawiki.revision-tags-change';
620        $eventBus = EventBus::getInstanceForStream( $stream );
621        $eventBusFactory = $eventBus->getFactory();
622        $eventBusFactory->setCommentFormatter( MediaWikiServices::getInstance()->getCommentFormatter() );
623        $event = $eventBusFactory->createRevisionTagsChangeEvent(
624            $stream,
625            $revisionRecord,
626            $prevTags,
627            $addedTags,
628            $removedTags,
629            $user
630        );
631
632        DeferredUpdates::addCallableUpdate(
633            static function () use ( $eventBus, $event ) {
634                $eventBus->send( [ $event ] );
635            }
636        );
637    }
638
639}