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