Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.55% covered (warning)
79.55%
175 / 220
73.33% covered (warning)
73.33%
11 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventBusHooks
79.55% covered (warning)
79.55%
175 / 220
73.33% covered (warning)
73.33%
11 / 15
50.36
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%
13 / 13
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\LinksUpdate\LinksTable;
33use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
34use MediaWiki\Hook\ArticleRevisionVisibilitySetHook;
35use MediaWiki\Hook\BlockIpCompleteHook;
36use MediaWiki\Hook\LinksUpdateCompleteHook;
37use MediaWiki\Hook\PageMoveCompleteHook;
38use MediaWiki\Linker\LinkTarget;
39use MediaWiki\Logging\ManualLogEntry;
40use MediaWiki\Page\Hook\ArticleProtectCompleteHook;
41use MediaWiki\Page\Hook\ArticlePurgeHook;
42use MediaWiki\Page\Hook\PageDeleteCompleteHook;
43use MediaWiki\Page\Hook\PageUndeleteCompleteHook;
44use MediaWiki\Page\ProperPageIdentity;
45use MediaWiki\Page\WikiPage;
46use MediaWiki\Permissions\Authority;
47use MediaWiki\RecentChanges\RecentChange;
48use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
49use MediaWiki\Revision\RevisionLookup;
50use MediaWiki\Revision\RevisionRecord;
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        );
243
244        DeferredUpdates::addCallableUpdate( static fn () => $eventBus->send( [ $event ] ) );
245    }
246
247    /**
248     * Called when changing visibility of one or more revisions of an article.
249     * Produces mediawiki.revision-visibility-change events.
250     *
251     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleRevisionVisibilitySet
252     *
253     * @param Title $title title object of the article
254     * @param array $revIds array of integer revision IDs
255     * @param array $visibilityChangeMap map of revision id to oldBits and newBits.
256     *              This array can be examined to determine exactly what visibility
257     *              bits have changed for each revision.  This array is of the form
258     *              [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
259     */
260    public function onArticleRevisionVisibilitySet(
261        $title,
262        $revIds,
263        $visibilityChangeMap
264    ) {
265        $stream = 'mediawiki.revision-visibility-change';
266        $events = [];
267        $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
268        // https://phabricator.wikimedia.org/T321411
269        $performer = RequestContext::getMain()->getUser();
270        $performer->loadFromId();
271
272        // Create an event for each revId that was changed.
273        foreach ( $revIds as $revId ) {
274            // Read from primary since due to replication lag the updated field visibility
275            // might still not be available on a replica and we are at risk of leaking
276            // just suppressed data.
277            $revision = $this->revisionLookup
278                ->getRevisionById( $revId, IDBAccessObject::READ_LATEST );
279
280            // If the page is deleted simultaneously (null $revision) or if
281            // this revId is not in the $visibilityChangeMap, then we can't
282            // send a meaningful event.
283            if ( $revision === null ) {
284                wfDebug(
285                    __METHOD__ . ' revision ' . $revId .
286                    ' could not be found and may have been deleted. Cannot ' .
287                    "create mediawiki/revision/visibility-change event.\n"
288                );
289                continue;
290            } elseif ( !array_key_exists( $revId, $visibilityChangeMap ) ) {
291                // This should not happen, log it.
292                wfDebug(
293                    __METHOD__ . ' revision id ' . $revId .
294                    ' not found in visibilityChangeMap. Cannot create ' .
295                    "mediawiki/revision/visibility-change event.\n"
296                );
297                continue;
298            } else {
299                $eventFactory = $eventBus->getFactory();
300                $eventFactory->setCommentFormatter( $this->commentFormatter );
301
302                // If this revision is 'suppressed' AKA restricted, then the person performing
303                // 'RevisionDelete' should not be visible in public data.
304                // https://phabricator.wikimedia.org/T342487
305                //
306                // NOTE: This event stream tries to match the visibility of MediaWiki core logs,
307                // where regular delete/revision events are public, and suppress/revision events
308                // are private. In MediaWiki core logs, private events are fully hidden from
309                // the public.  Here, we need to produce a 'private' event to the
310                // mediawiki.page_change stream, to indicate to consumers that
311                // they should also 'suppress' the revision.  When this is done, we need to
312                // make sure that we do not reproduce the data that has been suppressed
313                // in the event itself.  E.g. if the username of the editor of the revision has been
314                // suppressed, we should not include any information about that editor in the event.
315                $performerForEvent = self::isSecretRevisionVisibilityChange(
316                    $visibilityChangeMap[$revId]['oldBits'],
317                    $visibilityChangeMap[$revId]['newBits']
318                ) ? null : $performer;
319
320                $events[] = $eventFactory->createRevisionVisibilityChangeEvent(
321                    $stream,
322                    $revision,
323                    $performerForEvent,
324                    $visibilityChangeMap[$revId]
325                );
326            }
327        }
328
329        if ( $events === [] ) {
330            // For revision-visibility-set it's possible that
331            // the page was deleted simultaneously and we can not
332            // send a meaningful event.
333            return;
334        }
335
336        DeferredUpdates::addCallableUpdate(
337            static function () use ( $eventBus, $events ) {
338                $eventBus->send( $events );
339            }
340        );
341    }
342
343    /**
344     * Callback for article purge.
345     *
346     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticlePurge
347     *
348     * @param WikiPage $wikiPage
349     */
350    public function onArticlePurge( $wikiPage ) {
351        $this->sendResourceChangedEvent( $wikiPage->getTitle(), [ 'purge' ] );
352    }
353
354    /**
355     * Occurs after the save page request has been processed.
356     *
357     * Sends an event if the new revision was also a page creation
358     *
359     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
360     *
361     * @param WikiPage $wikiPage
362     * @param UserIdentity $userIdentity
363     * @param string $summary
364     * @param int $flags
365     * @param RevisionRecord $revisionRecord
366     * @param EditResult $editResult
367     */
368    public function onPageSaveComplete(
369        $wikiPage,
370        $userIdentity,
371        $summary,
372        $flags,
373        $revisionRecord,
374        $editResult
375    ) {
376        if ( $editResult->isNullEdit() ) {
377            $this->sendResourceChangedEvent( $wikiPage->getTitle(), [ 'null_edit' ] );
378            return;
379        }
380
381        if ( $flags & EDIT_NEW ) {
382            // Not just a new revision, but a new page
383            $this->sendRevisionCreateEvent(
384                'mediawiki.page-create',
385                $revisionRecord
386            );
387        }
388    }
389
390    /**
391     * Occurs after a revision is inserted into the database.
392     *
393     * @see https://www.mediawiki.org/wiki/Manual:Hooks/RevisionRecordInserted
394     *
395     * @param RevisionRecord $revisionRecord RevisionRecord that has just been inserted
396     */
397    public function onRevisionRecordInserted( $revisionRecord ) {
398        $this->sendRevisionCreateEvent(
399            'mediawiki.revision-create',
400            $revisionRecord
401        );
402    }
403
404    /**
405     * @param string $stream
406     * @param RevisionRecord $revisionRecord
407     */
408    private function sendRevisionCreateEvent(
409        string $stream,
410        RevisionRecord $revisionRecord
411    ) {
412        $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
413        $eventFactory = $eventBus->getFactory();
414        $eventFactory->setCommentFormatter(
415            $this->commentFormatter
416        );
417        $event = $eventFactory->createRevisionCreateEvent(
418            $stream,
419            $revisionRecord
420        );
421
422        DeferredUpdates::addCallableUpdate( static fn () => $eventBus->send( [ $event ] ) );
423    }
424
425    /**
426     * Occurs after the request to block an IP or user has been processed
427     *
428     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BlockIpComplete
429     *
430     * @param DatabaseBlock $block the block object that was saved
431     * @param User $user the user who did the block (not the one being blocked)
432     * @param DatabaseBlock|null $previousBlock the previous block state for the block target.
433     *        null if this is a new block target.
434     */
435    public function onBlockIpComplete(
436        $block,
437        $user,
438        $previousBlock
439    ) {
440        $stream = 'mediawiki.user-blocks-change';
441        $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
442        $eventFactory = $eventBus->getFactory();
443        $event = $eventFactory->createUserBlockChangeEvent(
444            $stream, $user, $block, $previousBlock );
445
446        DeferredUpdates::addCallableUpdate( static fn () => $eventBus->send( [ $event ] ) );
447    }
448
449    /**
450     * Sends page-properties-change and page-links-change events
451     *
452     * Emits two events separately: one when the page properties change, and
453     * the other when links are added to or removed from the page.
454     *
455     * @see https://www.mediawiki.org/wiki/Manual:Hooks/LinksUpdateComplete
456     *
457     * @param LinksUpdate $linksUpdate the update object
458     * @param mixed $ticket
459     */
460    public function onLinksUpdateComplete(
461        $linksUpdate, $ticket
462    ) {
463        $addedProps = $linksUpdate->getAddedProperties();
464        $removedProps = $linksUpdate->getRemovedProperties();
465        $arePropsEmpty = !$removedProps && !$addedProps;
466
467        $addedLinks = $linksUpdate->getPageReferenceArray( 'pagelinks', LinksTable::INSERTED );
468        $addedExternalLinks = $linksUpdate->getAddedExternalLinks();
469        $removedLinks = $linksUpdate->getPageReferenceArray( 'pagelinks', LinksTable::DELETED );
470        $removedExternalLinks = $linksUpdate->getRemovedExternalLinks();
471        $areLinksEmpty = !$removedLinks && !$addedLinks
472            && !$removedExternalLinks && !$addedExternalLinks;
473
474        if ( $arePropsEmpty && $areLinksEmpty ) {
475            return;
476        }
477
478        $title = $linksUpdate->getTitle();
479        $user = $linksUpdate->getTriggeringUser();
480
481        // Use triggering revision's rev_id if it is set.
482        // If the LinksUpdate didn't have a triggering revision
483        // (probably because it was triggered by sysadmin maintenance).
484        // Use the page's latest revision.
485        $revRecord = $linksUpdate->getRevisionRecord();
486        if ( $revRecord ) {
487            $revId = $revRecord->getId();
488        } else {
489            $revId = $title->getLatestRevID();
490        }
491        $pageId = $linksUpdate->getPageId();
492
493        if ( !$arePropsEmpty ) {
494            $stream = 'mediawiki.page-properties-change';
495            $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
496            $eventFactory = $eventBus->getFactory();
497            $propEvent = $eventFactory->createPagePropertiesChangeEvent(
498                $stream,
499                $title,
500                $addedProps,
501                $removedProps,
502                $user,
503                $revId,
504                $pageId
505            );
506
507            DeferredUpdates::addCallableUpdate(
508                static function () use ( $eventBus, $propEvent ) {
509                    $eventBus->send( [ $propEvent ] );
510                }
511            );
512        }
513
514        if ( !$areLinksEmpty ) {
515            $stream = 'mediawiki.page-properties-change';
516            $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
517            $eventFactory = $eventBus->getFactory();
518            $linkEvent = $eventFactory->createPageLinksChangeEvent(
519                'mediawiki.page-links-change',
520                $title,
521                $addedLinks,
522                $addedExternalLinks,
523                $removedLinks,
524                $removedExternalLinks,
525                $user,
526                $revId,
527                $pageId
528            );
529
530            DeferredUpdates::addCallableUpdate(
531                static function () use ( $eventBus, $linkEvent ) {
532                    $eventBus->send( [ $linkEvent ] );
533                }
534            );
535        }
536    }
537
538    /**
539     * Sends a page-restrictions-change event
540     *
541     * @param WikiPage $wikiPage the article which restrictions were changed
542     * @param User $user the user who have changed the article
543     * @param string[] $protect set of new restrictions details
544     * @param string $reason the reason for page protection
545     */
546    public function onArticleProtectComplete(
547        $wikiPage,
548        $user,
549        $protect,
550        $reason
551    ) {
552        $stream = 'mediawiki.page-restrictions-change';
553        $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
554        $eventFactory = $eventBus->getFactory();
555
556        $event = $eventFactory->createPageRestrictionsChangeEvent(
557            $stream,
558            $user,
559            $wikiPage->getTitle(),
560            $wikiPage->getId(),
561            $wikiPage->getRevisionRecord(),
562            $wikiPage->isRedirect(),
563            $reason,
564            $protect
565        );
566
567        DeferredUpdates::addCallableUpdate( static fn () => $eventBus->send( [ $event ] ) );
568    }
569
570    /** Called after tags have been updated with the ChangeTags::updateTags function.
571     *
572     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsAfterUpdateTags
573     *
574     * @param array $addedTags tags effectively added in the update
575     * @param array $removedTags tags effectively removed in the update
576     * @param array $prevTags tags that were present prior to the update
577     * @param int|null $rc_id recentchanges table id
578     * @param int|null $rev_id revision table id
579     * @param int|null $log_id logging table id
580     * @param string|null $params tag params
581     * @param RecentChange|null $rc RecentChange being tagged when the tagging accompanies
582     * the action, or null
583     * @param User|null $user User who performed the tagging when the tagging is subsequent
584     * to the action, or null
585     */
586    public function onChangeTagsAfterUpdateTags(
587        $addedTags,
588        $removedTags,
589        $prevTags,
590        $rc_id,
591        $rev_id,
592        $log_id,
593        $params,
594        $rc,
595        $user
596    ) {
597        if ( $rev_id === null ) {
598            // We're only interested for revision (edits) tags for now.
599            return;
600        }
601
602        $revisionRecord = $this->revisionLookup->getRevisionById( $rev_id );
603        if ( $revisionRecord === null ) {
604            // Revision might already have been deleted, so we're not interested in tagging those.
605            return;
606        }
607
608        if ( !$user && $rc ) {
609            // If no user was explicitly given, fall back to the user who performed the change.
610            $user = $rc->getPerformerIdentity();
611        }
612
613        $stream = 'mediawiki.revision-tags-change';
614        $eventBus = $this->eventBusFactory->getInstanceForStream( $stream );
615        $eventBusFactory = $eventBus->getFactory();
616        $eventBusFactory->setCommentFormatter( $this->commentFormatter );
617        $event = $eventBusFactory->createRevisionTagsChangeEvent(
618            $stream,
619            $revisionRecord,
620            $prevTags,
621            $addedTags,
622            $removedTags,
623            $user
624        );
625
626        DeferredUpdates::addCallableUpdate( static fn () => $eventBus->send( [ $event ] ) );
627    }
628
629    /**
630     * This function returns true if the visibility bits between the change require the
631     * info about the change to be redacted.
632     * https://phabricator.wikimedia.org/T342487
633     *
634     *
635     * Info about a visibility change is secret (in the secret MW action log)
636     * if the revision was either previously or currently is being suppressed.
637     * The admin performing the action should be hidden in both cases.
638     * The admin performing the action should only be shown if the change is not
639     * affecting the revision's suppression status.
640     * https://phabricator.wikimedia.org/T342487#9292715
641     *
642     * @param int $oldBits
643     * @param int $newBits
644     * @return bool
645     */
646    public static function isSecretRevisionVisibilityChange( int $oldBits, int $newBits ) {
647        return $oldBits & RevisionRecord::DELETED_RESTRICTED ||
648          $newBits & RevisionRecord::DELETED_RESTRICTED;
649    }
650}