Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.04% covered (success)
98.04%
100 / 102
93.55% covered (success)
93.55%
29 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionRecord
98.04% covered (success)
98.04%
100 / 102
93.55% covered (success)
93.55%
29 / 31
61
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 hasSameContent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 getMainContentRaw
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getMainContentModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContentOrThrow
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSlot
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasSlot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSlotRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSlots
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOriginalSlots
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInheritedSlots
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimarySlots
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParentId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSize
n/a
0 / 0
n/a
0 / 0
0
 getSha1
n/a
0 / 0
n/a
0 / 0
0
 getPageId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageAsLinkTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getComment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isMinor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDeleted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVisibility
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 audienceCan
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 userCan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 userCanBitfield
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 dieIfCompositeBits
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 isReadyForInsertion
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isCurrent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Page revision base class.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Revision;
10
11use InvalidArgumentException;
12use MediaWiki\CommentStore\CommentStoreComment;
13use MediaWiki\Content\Content;
14use MediaWiki\DAO\WikiAwareEntity;
15use MediaWiki\DAO\WikiAwareEntityTrait;
16use MediaWiki\Linker\LinkTarget;
17use MediaWiki\Page\LegacyArticleIdAccess;
18use MediaWiki\Page\PageIdentity;
19use MediaWiki\Page\WikiPage;
20use MediaWiki\Permissions\Authority;
21use MediaWiki\Title\Title;
22use MediaWiki\Title\TitleValue;
23use MediaWiki\User\UserIdentity;
24use Wikimedia\NonSerializable\NonSerializableTrait;
25
26/**
27 * Page revision base class.
28 *
29 * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
30 * Note that while the base class has no setters, subclasses may offer a mutable interface.
31 *
32 * @since 1.31
33 * @since 1.32 Renamed from MediaWiki\Storage\RevisionRecord
34 */
35abstract class RevisionRecord implements WikiAwareEntity {
36    use LegacyArticleIdAccess;
37    use NonSerializableTrait;
38    use WikiAwareEntityTrait;
39
40    // RevisionRecord deletion constants
41    public const DELETED_TEXT = 1;
42    public const DELETED_COMMENT = 2;
43    public const DELETED_USER = 4;
44    public const DELETED_RESTRICTED = 8;
45    public const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience
46    public const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER |
47        self::DELETED_RESTRICTED; // convenience
48
49    // Audience options for accessors
50    public const FOR_PUBLIC = 1;
51    public const FOR_THIS_USER = 2;
52    public const RAW = 3;
53
54    /** @var string|false Wiki ID; false means the current wiki */
55    protected $wikiId = false;
56    /** @var int|null */
57    protected $mId;
58    /** @var int */
59    protected $mPageId;
60    /** @var UserIdentity|null */
61    protected $mUser;
62    /** @var bool */
63    protected $mMinorEdit = false;
64    /** @var string|null */
65    protected $mTimestamp;
66    /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
67    protected $mDeleted = 0;
68    /** @var int|null */
69    protected $mSize;
70    /** @var int|null */
71    protected $mParentId;
72    /** @var CommentStoreComment|null */
73    protected $mComment;
74
75    /** @var PageIdentity */
76    protected $mPage;
77
78    /** @var RevisionSlots */
79    protected $mSlots;
80
81    /**
82     * @note Avoid calling this constructor directly. Use the appropriate methods
83     * in RevisionStore instead.
84     *
85     * @param PageIdentity $page The page this RevisionRecord is associated with.
86     * @param RevisionSlots $slots The slots of this revision.
87     * @param false|string $wikiId Relevant wiki id or self::LOCAL for the current one.
88     */
89    public function __construct( PageIdentity $page, RevisionSlots $slots, $wikiId = self::LOCAL ) {
90        $this->assertWikiIdParam( $wikiId );
91
92        // Make sure $page is immutable, see T380536. This is a nasty hack.
93        if ( !$page->canExist() ) {
94            // NOTE: We continue to support non-proper Titles for fake
95            // revisions used during parsing (T381982).
96            // TODO: Emit a deprecation warning for non-proper pages once
97            // we have a good alternative (T382341).
98        } elseif ( $page instanceof Title ) {
99            // Hack. Eventually, all PageIdentities should be immutable and "proper".
100            $page = $page->toPageIdentity();
101        } elseif ( $page instanceof WikiPage ) {
102            $page = $page->getTitle()->toPageIdentity();
103        }
104
105        $this->mPage = $page;
106        $this->mSlots = $slots;
107        $this->wikiId = $wikiId;
108        $this->mPageId = $this->getArticleId( $page );
109    }
110
111    /**
112     * @param RevisionRecord $rec
113     *
114     * @return bool True if this RevisionRecord is known to have same content as $rec.
115     *         False if the content is different (or not known to be the same).
116     */
117    public function hasSameContent( RevisionRecord $rec ): bool {
118        if ( $rec === $this ) {
119            return true;
120        }
121
122        if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
123            return true;
124        }
125
126        // check size before hash, since size is quicker to compute
127        if ( $this->getSize() !== $rec->getSize() ) {
128            return false;
129        }
130
131        // instead of checking the hash, we could also check the content addresses of all slots.
132
133        if ( $this->getSha1() === $rec->getSha1() ) {
134            return true;
135        }
136
137        return false;
138    }
139
140    /**
141     * Returns the Content of the main slot of this revision.
142     *
143     * @see getContent()
144     *
145     * @return Content|null The content of the main slot, or null on error
146     * @throws RevisionAccessException
147     */
148    public function getMainContentRaw(): ?Content {
149        return $this->getContent( SlotRecord::MAIN, self::RAW );
150    }
151
152    /**
153     * Returns the Content of the given slot of this revision.
154     * Call getSlotNames() to get a list of available slots.
155     *
156     * Note that for mutable Content objects, each call to this method will return a
157     * fresh clone.
158     *
159     * Use getContentOrThrow() for more specific error information.
160     *
161     * @param string $role The role name of the desired slot
162     * @param int $audience
163     * @param Authority|null $performer user on whose behalf to check
164     *
165     * @return Content|null The content of the given slot, or null on error
166     * @throws RevisionAccessException
167     */
168    public function getContent( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?Content {
169        try {
170            $content = $this->getSlot( $role, $audience, $performer )->getContent();
171        } catch ( BadRevisionException | SuppressedDataException ) {
172            return null;
173        }
174        return $content->copy();
175    }
176
177    /**
178     * Returns the content model of the main slot of this revision.
179     *
180     * @return string The content model
181     * @throws RevisionAccessException
182     */
183    public function getMainContentModel(): string {
184        return $this->getSlot( SlotRecord::MAIN, self::RAW )->getModel();
185    }
186
187    /**
188     * Get the Content of the given slot of this revision.
189     *
190     * @param string $role The role name of the desired slot
191     * @param int $audience
192     * @param Authority|null $performer user on whose behalf to check
193     *
194     * @return Content
195     * @throws SuppressedDataException if the content is not viewable by the given audience
196     * @throws BadRevisionException if the content is missing or corrupted
197     * @throws RevisionAccessException
198     */
199    public function getContentOrThrow( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): Content {
200        if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $performer ) ) {
201            throw new SuppressedDataException(
202                'Access to the content has been suppressed for this audience' );
203        }
204
205        $content = $this->getSlot( $role, $audience, $performer )->getContent();
206        return $content->copy();
207    }
208
209    /**
210     * Returns meta-data for the given slot.
211     *
212     * @param string $role The role name of the desired slot
213     * @param int $audience
214     * @param Authority|null $performer user on whose behalf to check
215     *
216     * @throws RevisionAccessException if the slot does not exist or slot data
217     *        could not be lazy-loaded.
218     * @return SlotRecord The slot meta-data. If access to the slot's content is forbidden,
219     *         calling getContent() on the SlotRecord will throw an exception.
220     */
221    public function getSlot( $role, $audience = self::FOR_PUBLIC, ?Authority $performer = null ): SlotRecord {
222        $slot = $this->mSlots->getSlot( $role );
223
224        if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $performer ) ) {
225            return SlotRecord::newWithSuppressedContent( $slot );
226        }
227
228        return $slot;
229    }
230
231    /**
232     * Returns whether the given slot is defined in this revision.
233     *
234     * @param string $role The role name of the desired slot
235     *
236     * @return bool
237     */
238    public function hasSlot( $role ): bool {
239        return $this->mSlots->hasSlot( $role );
240    }
241
242    /**
243     * Returns the slot names (roles) of all slots present in this revision.
244     * getContent() will succeed only for the names returned by this method.
245     *
246     * @return string[]
247     */
248    public function getSlotRoles(): array {
249        return $this->mSlots->getSlotRoles();
250    }
251
252    /**
253     * Returns the slots defined for this revision.
254     *
255     * @note This provides access to slot content with no audience checks applied.
256     * Calling getContent() on the RevisionSlots object returned here, or on any
257     * SlotRecord it returns from getSlot(), will not fail due to access restrictions.
258     * If audience checks are desired, use getSlot( $role, $audience, $performer )
259     * or getContent( $role, $audience, $performer ) instead.
260     *
261     * @return RevisionSlots
262     */
263    public function getSlots(): RevisionSlots {
264        return $this->mSlots;
265    }
266
267    /**
268     * Returns the slots that originate in this revision.
269     *
270     * Note that this does not include any slots inherited from some earlier revision,
271     * even if they are different from the slots in the immediate parent revision.
272     * This is the case for rollbacks: slots of a rollback revision are inherited from
273     * the rollback target, and are different from the slots in the parent revision,
274     * which was rolled back.
275     *
276     * To find all slots modified by this revision against its immediate parent
277     * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
278     */
279    public function getOriginalSlots(): RevisionSlots {
280        return new RevisionSlots( $this->mSlots->getOriginalSlots() );
281    }
282
283    /**
284     * Returns slots inherited from some previous revision.
285     *
286     * "Inherited" slots are all slots that do not originate in this revision.
287     * Note that these slots may still differ from the one in the parent revision.
288     * This is the case for rollbacks: slots of a rollback revision are inherited from
289     * the rollback target, and are different from the slots in the parent revision,
290     * which was rolled back.
291     */
292    public function getInheritedSlots(): RevisionSlots {
293        return new RevisionSlots( $this->mSlots->getInheritedSlots() );
294    }
295
296    /**
297     * Returns primary slots (those that are not derived).
298     *
299     * @return RevisionSlots
300     * @since 1.36
301     */
302    public function getPrimarySlots(): RevisionSlots {
303        return new RevisionSlots( $this->mSlots->getPrimarySlots() );
304    }
305
306    /**
307     * Get revision ID. Depending on the concrete subclass, this may return null if
308     * the revision ID is not known (e.g. because the revision does not yet exist
309     * in the database).
310     *
311     * MCR migration note: this replaced Revision::getId
312     *
313     * @param string|false $wikiId The wiki ID expected by the caller.
314     * @return int|null
315     */
316    public function getId( $wikiId = self::LOCAL ) {
317        $this->deprecateInvalidCrossWiki( $wikiId, '1.36' );
318        return $this->mId;
319    }
320
321    /**
322     * Get parent revision ID (the original previous page revision).
323     * If there is no parent revision, this returns 0.
324     * If the parent revision is undefined or unknown, this returns null.
325     *
326     * @note As of MW 1.31, the database schema allows the parent ID to be
327     * NULL to indicate that it is unknown.
328     *
329     * MCR migration note: this replaced Revision::getParentId
330     *
331     * @param string|false $wikiId The wiki ID expected by the caller.
332     * @return int|null
333     */
334    public function getParentId( $wikiId = self::LOCAL ) {
335        $this->deprecateInvalidCrossWiki( $wikiId, '1.36' );
336        return $this->mParentId;
337    }
338
339    /**
340     * Returns the nominal size of this revision, in bogo-bytes.
341     * May be calculated on the fly if not known, which may in the worst
342     * case may involve loading all content.
343     *
344     * MCR migration note: this replaced Revision::getSize
345     *
346     * @throws RevisionAccessException if the size was unknown and could not be calculated.
347     * @return int
348     */
349    abstract public function getSize();
350
351    /**
352     * Returns the base36 sha1 of this revision. This hash is derived from the
353     * hashes of all slots associated with the revision.
354     * May be calculated on the fly if not known, which may in the worst
355     * case may involve loading all content.
356     *
357     * MCR migration note: this replaced Revision::getSha1
358     *
359     * @throws RevisionAccessException if the hash was unknown and could not be calculated.
360     * @return string
361     */
362    abstract public function getSha1();
363
364    /**
365     * Get the page ID. If the page does not yet exist, the page ID is 0.
366     *
367     * MCR migration note: this replaced Revision::getPage
368     *
369     * @param string|false $wikiId The wiki ID expected by the caller.
370     * @return int
371     */
372    public function getPageId( $wikiId = self::LOCAL ) {
373        $this->deprecateInvalidCrossWiki( $wikiId, '1.36' );
374        return $this->mPageId;
375    }
376
377    /**
378     * Get the ID of the wiki this revision belongs to.
379     *
380     * @return string|false The wiki's logical name, of false to indicate the local wiki.
381     */
382    public function getWikiId() {
383        return $this->wikiId;
384    }
385
386    /**
387     * Returns the title of the page this revision is associated with as a LinkTarget object.
388     *
389     * @throws InvalidArgumentException if this revision does not belong to a local wiki
390     * @return LinkTarget
391     */
392    public function getPageAsLinkTarget() {
393        return TitleValue::newFromPage( $this->mPage );
394    }
395
396    /**
397     * Returns the page this revision belongs to.
398     *
399     * MCR migration note: this replaced Revision::getTitle
400     *
401     * @since 1.36
402     *
403     * @return PageIdentity
404     */
405    public function getPage(): PageIdentity {
406        return $this->mPage;
407    }
408
409    /**
410     * Fetch revision's author's user identity, if it's available to the specified audience.
411     * If the specified audience does not have access to it, null will be
412     * returned. Depending on the concrete subclass, null may also be returned if the user is
413     * not yet specified.
414     *
415     * MCR migration note: this replaced Revision::getUser
416     *
417     * @param int $audience One of:
418     * - `RevisionRecord::FOR_PUBLIC`: to be displayed to all users
419     * - `RevisionRecord::FOR_THIS_USER`: to be displayed to the given user
420     * - `RevisionRecord::RAW`: get the user regardless of permissions
421     * @param Authority|null $performer user on whose behalf to check
422     * @return UserIdentity|null The identity of the revision author, null if access is forbidden.
423     */
424    public function getUser( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
425        if ( !$this->audienceCan( self::DELETED_USER, $audience, $performer ) ) {
426            return null;
427        } else {
428            return $this->mUser;
429        }
430    }
431
432    /**
433     * Fetch revision comment, if it's available to the specified audience.
434     * If the specified audience does not have access to the comment,
435     * this will return null. Depending on the concrete subclass, null may also be returned
436     * if the comment is not yet specified.
437     *
438     * MCR migration note: this replaced Revision::getComment
439     *
440     * @param int $audience One of:
441     * - `RevisionRecord::FOR_PUBLIC`: to be displayed to all users
442     * - `RevisionRecord::FOR_THIS_USER`: to be displayed to the given user
443     * - `RevisionRecord::RAW`: get the text regardless of permissions
444     * @param Authority|null $performer user on whose behalf to check
445     *
446     * @return CommentStoreComment|null The revision comment, null if access is forbidden.
447     */
448    public function getComment( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
449        if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $performer ) ) {
450            return null;
451        } else {
452            return $this->mComment;
453        }
454    }
455
456    /**
457     * MCR migration note: this replaced Revision::isMinor
458     *
459     * @return bool
460     */
461    public function isMinor() {
462        return (bool)$this->mMinorEdit;
463    }
464
465    /**
466     * MCR migration note: this replaced Revision::isDeleted
467     *
468     * @param int $field One of DELETED_* bitfield constants
469     *
470     * @return bool
471     */
472    public function isDeleted( int $field ) {
473        return ( $this->getVisibility() & $field ) === $field;
474    }
475
476    /**
477     * Get the deletion bitfield of the revision
478     *
479     * MCR migration note: this replaced Revision::getVisibility
480     *
481     * @return int
482     */
483    public function getVisibility() {
484        return (int)$this->mDeleted;
485    }
486
487    /**
488     * MCR migration note: this replaced Revision::getTimestamp.
489     *
490     * May return null if the timestamp was not specified.
491     *
492     * @return string|null
493     */
494    public function getTimestamp() {
495        return $this->mTimestamp;
496    }
497
498    /**
499     * Check that the given audience has access to the given field.
500     *
501     * MCR migration note: this corresponded to Revision::userCan
502     *
503     * @param int $field Exactly one of the `DELETED_*` constants (single-bit flag):
504     * - `RevisionRecord::DELETED_TEXT` = `File::DELETED_FILE`
505     * - `RevisionRecord::DELETED_COMMENT` = `File::DELETED_COMMENT`
506     * - `RevisionRecord::DELETED_USER` = `File::DELETED_USER`
507     * - `RevisionRecord::DELETED_RESTRICTED` = `File::DELETED_RESTRICTED`
508     * @param int $audience One of:
509     * - `RevisionRecord::FOR_PUBLIC`: to be displayed to all users
510     * - `RevisionRecord::FOR_THIS_USER`: to be displayed to the given user
511     * - `RevisionRecord::RAW`: get the field regardless of permissions
512     * @param Authority|null $performer user on whose behalf to check
513     *
514     * @return bool
515     */
516    public function audienceCan( int $field, int $audience, ?Authority $performer = null ) {
517        if ( $audience === self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
518            return false;
519        } elseif ( $audience === self::FOR_THIS_USER ) {
520            if ( !$performer ) {
521                throw new InvalidArgumentException(
522                    'An Authority object must be given when checking FOR_THIS_USER audience.'
523                );
524            }
525
526            if ( !$this->userCan( $field, $performer ) ) {
527                return false;
528            }
529        }
530
531        return true;
532    }
533
534    /**
535     * Determine if the given authority is allowed to view a particular
536     * field of this revision, if it's marked as deleted.
537     *
538     * MCR migration note: this corresponded to Revision::userCan
539     *
540     * @param int $field Exactly one of the `DELETED_*` constants (single-bit flag):
541     * - `RevisionRecord::DELETED_TEXT` = `File::DELETED_FILE`
542     * - `RevisionRecord::DELETED_COMMENT` = `File::DELETED_COMMENT`
543     * - `RevisionRecord::DELETED_USER` = `File::DELETED_USER`
544     * - `RevisionRecord::DELETED_RESTRICTED` = `File::DELETED_RESTRICTED`
545     * @param Authority $performer user on whose behalf to check
546     * @return bool
547     */
548    public function userCan( int $field, Authority $performer ) {
549        return self::userCanBitfield( $this->getVisibility(), $field, $performer, $this->mPage );
550    }
551
552    /**
553     * Determine if the current user is allowed to view a particular
554     * field of this revision, if it's marked as deleted. This is used
555     * by various classes to avoid duplication.
556     *
557     * MCR migration note: this replaced Revision::userCanBitfield
558     *
559     * @param int $bitfield Current field
560     * @param int $field Exactly one of the `DELETED_*` constants (single-bit flag):
561     * - `self::DELETED_TEXT` = `File::DELETED_FILE`
562     * - `self::DELETED_COMMENT` = `File::DELETED_COMMENT`
563     * - `self::DELETED_USER` = `File::DELETED_USER`
564     * - `self::DELETED_RESTRICTED` = `File::DELETED_RESTRICTED`
565     * Note: When `self::DELETED_RESTRICTED` is passed, this method returns true if the
566     * restricted bit isn't set in `$bitfield` (i.e., if the revision isn't suppressed).
567     * @param Authority $performer user on whose behalf to check
568     * @param PageIdentity|null $page A PageIdentity object to check for per-page restrictions on,
569     *                          instead of just plain user rights
570     * @return bool
571     */
572    public static function userCanBitfield( $bitfield, int $field, Authority $performer, ?PageIdentity $page = null ) {
573        self::dieIfCompositeBits( $field ); // This method is unsafe with composite bit fields (T415584)
574
575        if ( $bitfield & $field ) { // Requested flag is set (deleted/suppressed)
576            if ( $bitfield & self::DELETED_RESTRICTED ) {
577                $permissions = [ 'suppressrevision', 'viewsuppressed' ];
578            } elseif ( $field === self::DELETED_TEXT ) {
579                $permissions = [ 'deletedtext' ];
580            } else {
581                $permissions = [ 'deletedhistory' ];
582            }
583
584            $permissionlist = implode( ', ', $permissions );
585            if ( $page === null ) {
586                wfDebug( "Checking for $permissionlist due to $field match on $bitfield" );
587                return $performer->isAllowedAny( ...$permissions );
588            } else {
589                wfDebug( "Checking for $permissionlist on $page due to $field match on $bitfield" );
590                foreach ( $permissions as $perm ) {
591                    if ( $performer->authorizeRead( $perm, $page ) ) {
592                        return true;
593                    }
594                }
595                return false;
596            }
597        } else {
598            return true;
599        }
600    }
601
602    private static function dieIfCompositeBits( int $field ): void {
603        $validMask = self::DELETED_TEXT
604            | self::DELETED_COMMENT
605            | self::DELETED_USER
606            | self::DELETED_RESTRICTED;
607
608        if (
609            $field === 0 ||
610            ( $field & ( $field - 1 ) ) !== 0 || // Not a power of two
611            ( $field & ~$validMask ) !== 0 // Contains invalid bits
612        ) {
613            throw new \UnexpectedValueException(
614                'Expected $field to be a single DELETED_* constant, got ' . $field
615            );
616        }
617    }
618
619    /**
620     * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
621     * information needed to save it to the database. This should trivially be true for
622     * RevisionRecords loaded from the database.
623     *
624     * Note that this may return true even if getId() or getPage() return null or 0, since these
625     * are generally assigned while the revision is saved to the database, and may not be available
626     * before.
627     *
628     * @return bool
629     */
630    public function isReadyForInsertion() {
631        // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
632        // be loaded in order to calculate the values. Just assume these methods will not return
633        // null if mSlots is not empty.
634
635        // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
636        // check them.
637
638        return $this->getTimestamp() !== null
639            && $this->getComment( self::RAW ) !== null
640            && $this->getUser( self::RAW ) !== null
641            && $this->mSlots->getSlotRoles() !== [];
642    }
643
644    /**
645     * Checks whether the revision record is a stored latest revision.
646     * @since 1.35
647     * @return bool
648     */
649    public function isCurrent() {
650        return false;
651    }
652}