Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageLatestRevisionChangedEvent
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 18
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 getPageRecordAfter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCreation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 changedLatestRevisionId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isNominalContentChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isEffectiveContentChange
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getAuthor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSlotsUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isModifiedSlot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEditResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLatestRevisionBefore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLatestRevisionAfter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPatrolStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSilent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isImplicit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isRevert
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isBotUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Page\Event;
8
9use MediaWiki\Page\ExistingPageRecord;
10use MediaWiki\Page\ProperPageIdentity;
11use MediaWiki\Revision\RevisionRecord;
12use MediaWiki\Storage\EditResult;
13use MediaWiki\Storage\PageUpdateCauses;
14use MediaWiki\Storage\RevisionSlotsUpdate;
15use MediaWiki\User\UserIdentity;
16use Wikimedia\Assert\Assert;
17
18/**
19 * Domain event representing a change to the page's latest revision.
20 *
21 * PageLatestRevisionChanged events emitted for the same page ID represent a
22 * continuous chain of changes to pages' latest revision, even if the content
23 * did not change (for a dummy revision). This change is observable as the
24 * difference between getPageRecordBefore()->getLatest() and
25 * getPageRecordAfter()->getLatest(), resp. between getLatestRevisionBefore()
26 * and getLatestRevisionAfter().
27 *
28 * For two consecutive PageLatestRevisionChangedEvents for the same page ID, the
29 * return value of getLatestRevisionAfter() on the first event will match
30 * the return value of getLatestRevisionBefore() on the second event.
31 * Other aspects of the page, such as the title, may change independently.
32 *
33 * A reconciliation version of this event may be triggered even when the page's
34 * latest version did not change (on null edits), to provide an opportunity to
35 * listeners to recover from data loss and corruption by re-generating any derived
36 * data. In that case, getPageRecordBefore() and getPageRecordAfter() return the
37 * same value.
38 *
39 * PageLatestRevisionChangedEvents are emitted by DerivedPageDataUpdater, typically
40 * triggered by PageUpdater.
41 *
42 * User activities that trigger PageLatestRevisionChangeds event include:
43 * - editing, including page creation and null-edits
44 * - moving pages
45 * - undeleting pages
46 * - importing revisions
47 * - uploading media files
48 * - Any activity that creates a dummy revision, such as changing the page's
49 *   protection level.
50 *
51 * @note Events may be delivered out of order! The continuity semantics apply
52 * to the sequence in which the events were emitted. The event dispatcher tries to
53 * deliver events in the order they were emitted, but this cannot be guaranteed
54 * under all circumstances.
55 *
56 * @since 1.45
57 */
58class PageLatestRevisionChangedEvent extends PageRecordChangedEvent implements PageUpdateCauses {
59
60    public const TYPE = 'PageLatestRevisionChanged';
61
62    /**
63     * @var string Do not notify other users (e.g. via RecentChanges or
64     * watchlist).
65     * See EDIT_SILENT.
66     */
67    public const FLAG_SILENT = 'silent';
68
69    /**
70     * @var string The update was performed by a bot.
71     * See EDIT_FORCE_BOT.
72     */
73    public const FLAG_BOT = 'bot';
74
75    /**
76     * @var string The page update is a side effect and does not represent an
77     * active user contribution.
78     * See EDIT_IMPLICIT.
79     */
80    public const FLAG_IMPLICIT = 'implicit';
81
82    /**
83     * All available flags and their default values.
84     */
85    public const DEFAULT_FLAGS = [
86        self::FLAG_SILENT => false,
87        self::FLAG_BOT => false,
88        self::FLAG_IMPLICIT => false,
89    ];
90
91    private RevisionSlotsUpdate $slotsUpdate;
92    private RevisionRecord $latestRevisionAfter;
93    private ?RevisionRecord $latestRevisionBefore;
94    private ?EditResult $editResult;
95
96    private int $patrolStatus;
97
98    /**
99     * @param string $cause See the self::CAUSE_XXX constants.
100     * @param ?ExistingPageRecord $pageRecordBefore The page record before the change.
101     * @param ExistingPageRecord $pageRecordAfter The page record after the change.
102     * @param ?RevisionRecord $latestRevisionBefore The revision that used
103     *        to be the latest before the updated.
104     * @param RevisionRecord $latestRevisionAfter The revision object that became
105     *        the latest as a result of the update.
106     * @param RevisionSlotsUpdate $slotsUpdate Page content changed by the update.
107     * @param ?EditResult $editResult An EditResult representing the effects
108     *        of an edit.
109     * @param UserIdentity $performer The user performing the update.
110     * @param array<string> $tags Applicable tags, see ChangeTags.
111     * @param array<string,bool> $flags See the self::FLAG_XXX constants.
112     * @param int $patrolStatus See PageUpdater::setRcPatrolStatus()
113     */
114    public function __construct(
115        string $cause,
116        ?ExistingPageRecord $pageRecordBefore,
117        ExistingPageRecord $pageRecordAfter,
118        ?RevisionRecord $latestRevisionBefore,
119        RevisionRecord $latestRevisionAfter,
120        RevisionSlotsUpdate $slotsUpdate,
121        ?EditResult $editResult,
122        UserIdentity $performer,
123        array $tags = [],
124        array $flags = [],
125        int $patrolStatus = 0
126    ) {
127        parent::__construct(
128            $cause,
129            $pageRecordBefore,
130            $pageRecordAfter,
131            $performer,
132            $tags,
133            $flags,
134            $latestRevisionAfter->getTimestamp()
135        );
136        $this->declareEventType( self::TYPE );
137
138        // Legacy event type name, deprecated.
139        $this->declareEventType( 'PageRevisionUpdated' );
140
141        Assert::parameter(
142            $pageRecordAfter->isSamePageAs( $latestRevisionAfter->getPage() ),
143            '$latestRevisionAfter',
144            'must match to $pageRecordAfter'
145        );
146
147        if ( $latestRevisionBefore && $pageRecordBefore ) {
148            Assert::parameter(
149                $pageRecordBefore->isSamePageAs( $latestRevisionBefore->getPage() ),
150                '$latestRevisionBefore',
151                'must match to $pageRecordBefore'
152            );
153        } else {
154            Assert::parameter( $pageRecordBefore === null,
155                '$pageRecordBefore',
156                'must be null if $latestRevisionBefore is null'
157            );
158            Assert::parameter( $latestRevisionBefore === null,
159                '$latestRevisionBefore',
160                'must be null if $pageRecordBefore is null'
161            );
162        }
163
164        $this->slotsUpdate = $slotsUpdate;
165        $this->latestRevisionAfter = $latestRevisionAfter;
166        $this->latestRevisionBefore = $latestRevisionBefore;
167        $this->editResult = $editResult;
168        $this->patrolStatus = $patrolStatus;
169    }
170
171    /**
172     * @inheritDoc
173     */
174    public function getPageRecordAfter(): ExistingPageRecord {
175        // Overwritten to guarantee that the return value is not null.
176        // @phan-suppress-next-line PhanTypeMismatchReturnNullable
177        return parent::getPageRecordAfter();
178    }
179
180    /**
181     * Returns the page that was updated.
182     */
183    public function getPage(): ProperPageIdentity {
184        // Deprecated on the base class, not deprecated here.
185        return $this->getPageRecordAfter();
186    }
187
188    /**
189     * Whether the updated created the page.
190     * A deleted/archived page is not considered to "exist".
191     * When undeleting a page, the page will be restored using its old page ID,
192     * so the "created" page may have an ID that was seen previously.
193     */
194    public function isCreation(): bool {
195        return $this->latestRevisionBefore === null;
196    }
197
198    /**
199     * Whether this event represents a change to the latest revision ID
200     * associated with the page. In other words, the page's latest revision
201     * after the change is different from the page's latest revision before
202     * the change.
203     *
204     * This method will return true under most circumstances.
205     * It will however return false for reconciliation requests like null edits.
206     * In that case, isReconciliationRequest() should return true.
207     *
208     * @note Listeners should generally not use this method to check if
209     * event processing can be skipped, since that would mean ignoring
210     * reconciliation requests used to recover from data loss or corruption.
211     * The preferred way to check if processing would be redundant is
212     * isNominalContentChange().
213     *
214     * @see DomainEvent::isReconciliationRequest()
215     * @see DomainEvent::isNominalContentChange()
216     */
217    public function changedLatestRevisionId(): bool {
218        return $this->latestRevisionBefore === null
219            || $this->latestRevisionBefore->getId() !== $this->latestRevisionAfter->getId();
220    }
221
222    /**
223     * Whether the update nominally changed the content of the page.
224     * This is the case if:
225     * - the update actually changed the page's content, see isEffectiveContentChange().
226     * - the event is a reconciliation request, see isReconciliationRequest().
227     *
228     * On other words, this will return true for actual changes and null edits,
229     * but will return false for "dummy revisions".
230     *
231     * @note This is preferred over isEffectiveContentChange() for listeners
232     * aiming to avoid redundant processing when the content didn't change.
233     * The purpose of reconciliation requests is to re-trigger such processing
234     * to recover from data loss and corruption, even when there was no actual
235     * change in content.
236     *
237     * @see isEffectiveContentChange()
238     * @see DomainEvent::isReconciliationRequest()
239     */
240    public function isNominalContentChange(): bool {
241        return $this->isEffectiveContentChange() || $this->isReconciliationRequest();
242    }
243
244    /**
245     * Whether the update effectively changed the content of the page.
246     *
247     * This will return false for "dummy revisions" that represent an entry
248     * in the page history but do not modify the content. It will also be false
249     * for reconciliation events (null edits).
250     *
251     * @note Listeners aiming to skip processing of events that didn't change
252     * the content for optimization should use isNominalContentChange() instead.
253     * That way, they would not skip processing for reconciliation requests,
254     * providing a way to recover from data loss and corruption.
255     *
256     * @see isNominalContentChange()
257     */
258    public function isEffectiveContentChange(): bool {
259        return $this->latestRevisionBefore === null
260            || $this->latestRevisionBefore->getSha1() !== $this->latestRevisionAfter->getSha1();
261    }
262
263    /**
264     * Returns the author of the new revision.
265     * Note that this may be different from the user returned by
266     * getPerformer() for update events caused e.g. by
267     * undeletion or imports.
268     */
269    public function getAuthor(): UserIdentity {
270        return $this->latestRevisionAfter->getUser( RevisionRecord::RAW );
271    }
272
273    /**
274     * Returns which slots were changed, added, or removed by the update.
275     */
276    public function getSlotsUpdate(): RevisionSlotsUpdate {
277        return $this->slotsUpdate;
278    }
279
280    /**
281     * Whether the given slot was modified by the page update.
282     * Slots that were removed do not count as modified.
283     * This is a convenience method for
284     * $this->getSlotsUpdate()->isModifiedSlot( $slotRole ).
285     */
286    public function isModifiedSlot( string $slotRole ): bool {
287        return $this->getSlotsUpdate()->isModifiedSlot( $slotRole );
288    }
289
290    /**
291     * An EditResult representing the effects of the update.
292     * Can be used to determine whether the edit was a revert
293     * and which edits were reverted.
294     *
295     * This may return null for updates that do not result from edits,
296     * such as imports or undeletions.
297     */
298    public function getEditResult(): ?EditResult {
299        return $this->editResult;
300    }
301
302    /**
303     * Returned the revision that used to be latest before the update.
304     * Will be null if the edit created the page.
305     * Will be the same as getLatestRevisionAfter() if the edit was a
306     * "null-edit".
307     *
308     * Note that this is not necessarily the new revision's parent revision.
309     * For instance, when undeleting a page, getLatestRevisionBefore() will
310     * return null because the page didn't exist before, even if the undeleted
311     * page has many revisions and the new latest revision indeed has a parent
312     * revision.
313     *
314     * The parent revision can be determined by calling
315     * getLatestRevisionAfter()->getParentId().
316     */
317    public function getLatestRevisionBefore(): ?RevisionRecord {
318        return $this->latestRevisionBefore;
319    }
320
321    /**
322     * The revision that became the latest as a result of the update.
323     */
324    public function getLatestRevisionAfter(): RevisionRecord {
325        return $this->latestRevisionAfter;
326    }
327
328    /**
329     * Returns the page update's initial patrol status.
330     * @see PageUpdater::setRcPatrolStatus()
331     * @see RecentChange::PRC_XXX
332     */
333    public function getPatrolStatus(): int {
334        return $this->patrolStatus;
335    }
336
337    /**
338     * Whether the update should be omitted from update feeds presented to the
339     * user.
340     */
341    public function isSilent(): bool {
342        return $this->hasFlag( self::FLAG_SILENT );
343    }
344
345    /**
346     * Whether the update was performed automatically without the user's
347     * initiative.
348     */
349    public function isImplicit(): bool {
350        return $this->hasFlag( self::FLAG_IMPLICIT );
351    }
352
353    /**
354     * Whether the update reverts an earlier update to the same page.
355     * Note that an "undo" style revert may create a new revision that is
356     * different from any previous revision by applying the inverse of a
357     * past update to the latest revision.
358     *
359     * @see EditResult::isRevert
360     */
361    public function isRevert(): bool {
362        return $this->editResult && $this->editResult->isRevert();
363    }
364
365    /**
366     * Whether the update was performed by a bot.
367     */
368    public function isBotUpdate(): bool {
369        return $this->hasFlag( self::FLAG_BOT );
370    }
371
372}
373
374// @deprecated temporary alias, remove before 1.45 release
375class_alias( PageLatestRevisionChangedEvent::class, 'MediaWiki\Page\Event\PageRevisionUpdatedEvent' );