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