Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.73% covered (success)
97.73%
43 / 44
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionArchiveRecord
97.73% covered (success)
97.73%
43 / 44
91.67% covered (success)
91.67%
11 / 12
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 getPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArchiveId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSha1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getComment
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
 userCan
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 audienceCan
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isReadyForInsertion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Revision;
10
11use MediaWiki\CommentStore\CommentStoreComment;
12use MediaWiki\Page\PageIdentity;
13use MediaWiki\Page\ProperPageIdentity;
14use MediaWiki\Permissions\Authority;
15use MediaWiki\User\UserIdentity;
16use MediaWiki\Utils\MWTimestamp;
17use stdClass;
18use Wikimedia\Assert\Assert;
19use Wikimedia\Timestamp\TimestampFormat as TS;
20
21/**
22 * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
23 * Most getters on RevisionArchiveRecord will never return null. However, getId() and
24 * getParentId() may indeed return null if this information was not stored when the archive entry
25 * was created.
26 *
27 * @since 1.31
28 * @since 1.32 Renamed from MediaWiki\Storage\RevisionArchiveRecord
29 */
30class RevisionArchiveRecord extends RevisionRecord {
31
32    /**
33     * @var int
34     */
35    protected $mArchiveId;
36
37    /**
38     * @note Avoid calling this constructor directly. Use the appropriate methods
39     * in RevisionStore instead.
40     *
41     * @param PageIdentity $page The page this RevisionRecord is associated with.
42     * @param UserIdentity $user
43     * @param CommentStoreComment $comment
44     * @param stdClass $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
45     *        a query that yields the required fields.
46     * @param RevisionSlots $slots The slots of this revision.
47     * @param false|string $wikiId Relevant wiki or self::LOCAL for the current one.
48     */
49    public function __construct(
50        PageIdentity $page,
51        UserIdentity $user,
52        CommentStoreComment $comment,
53        stdClass $row,
54        RevisionSlots $slots,
55        $wikiId = self::LOCAL
56    ) {
57        parent::__construct( $page, $slots, $wikiId );
58
59        $timestamp = MWTimestamp::convert( TS::MW, $row->ar_timestamp );
60        Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
61
62        $this->mArchiveId = intval( $row->ar_id );
63
64        // NOTE: ar_page_id may be different from $this->mPage->getId() in some cases,
65        // notably when a partially restored page has been moved, and a new page has been created
66        // with the same title. Archive rows for that title will then have the wrong page id.
67        $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $this->getArticleId( $this->mPage );
68
69        // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
70        // indicates that the parent revision is unknown. As per MW 1.31, the database schema
71        // allows ar_parent_id to be NULL.
72        $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
73        $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
74        $this->mComment = $comment;
75        $this->mUser = $user;
76        $this->mTimestamp = $timestamp;
77        $this->mMinorEdit = (bool)$row->ar_minor_edit;
78        $this->mDeleted = intval( $row->ar_deleted );
79        $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
80
81        Assert::parameter(
82            $page->canExist(),
83            '$page',
84            'must represent a proper page'
85        );
86        Assert::postcondition(
87            parent::getPage() instanceof ProperPageIdentity,
88            'The parent constructor should have ensured that we have a ProperPageIdentity now.'
89        );
90    }
91
92    /**
93     * Returns the page this revision belongs to.
94     *
95     * @return ProperPageIdentity (before 1.44, this was returning a PageIdentity)
96     */
97    public function getPage(): ProperPageIdentity {
98        // Override to narrow the return type.
99        // We checked in the constructor that the page is a proper page.
100        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
101        return parent::getPage();
102    }
103
104    /**
105     * Get archive row ID
106     *
107     * @return int
108     */
109    public function getArchiveId() {
110        return $this->mArchiveId;
111    }
112
113    /**
114     * @param string|false $wikiId The wiki ID expected by the caller.
115     * @return int|null The revision id, or null if the original revision ID
116     *         was not recorded in the archive table.
117     */
118    public function getId( $wikiId = self::LOCAL ) {
119        // overwritten just to refine the contract specification.
120        return parent::getId( $wikiId );
121    }
122
123    /**
124     * @throws RevisionAccessException if the size was unknown and could not be calculated.
125     * @return int The nominal revision size, never null. May be computed on the fly.
126     */
127    public function getSize() {
128        // If length is null, calculate and remember it (potentially SLOW!).
129        // This is for compatibility with old database rows that don't have the field set.
130        $this->mSize ??= $this->mSlots->computeSize();
131
132        return $this->mSize;
133    }
134
135    /**
136     * @throws RevisionAccessException if the hash was unknown and could not be calculated.
137     * @return string The revision hash, never null. May be computed on the fly.
138     */
139    public function getSha1() {
140        return $this->mSlots->computeSha1();
141    }
142
143    /** @inheritDoc */
144    public function getUser( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
145        // overwritten just to add a guarantee to the contract
146        return parent::getUser( $audience, $performer );
147    }
148
149    /** @inheritDoc */
150    public function getComment( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
151        // overwritten just to add a guarantee to the contract
152        return parent::getComment( $audience, $performer );
153    }
154
155    /**
156     * @return string never null
157     */
158    public function getTimestamp() {
159        // overwritten just to add a guarantee to the contract
160        return parent::getTimestamp();
161    }
162
163    /** @inheritDoc */
164    public function userCan( int $field, Authority $performer ) {
165        // This revision belongs to a deleted page, so check the relevant permissions as well. (T345777)
166
167        // Viewing the content requires either 'deletedtext' or 'undelete' (for legacy reasons)
168        if (
169            $field === self::DELETED_TEXT &&
170            !$performer->authorizeRead( 'deletedtext', $this->getPage() ) &&
171            !$performer->authorizeRead( 'undelete', $this->getPage() )
172        ) {
173            return false;
174        }
175
176        // Viewing the edit summary requires 'deletedhistory'
177        if (
178            $field === self::DELETED_COMMENT &&
179            !$performer->authorizeRead( 'deletedhistory', $this->getPage() )
180        ) {
181            return false;
182        }
183
184        // Other fields of revisions of deleted pages are public, per T232389 (unless revision-deleted)
185
186        return parent::userCan( $field, $performer );
187    }
188
189    /** @inheritDoc */
190    public function audienceCan( int $field, int $audience, ?Authority $performer = null ) {
191        // This revision belongs to a deleted page, so check the relevant permissions as well. (T345777)
192        // See userCan().
193        if (
194            $audience === self::FOR_PUBLIC &&
195            ( $field === self::DELETED_TEXT || $field === self::DELETED_COMMENT )
196        ) {
197            // TODO: Should this use PermissionManager::isEveryoneAllowed() or something?
198            // But RevisionRecord::audienceCan() doesn't do that either…
199            return false;
200        }
201
202        // This calls userCan(), which checks the user's permissions
203        return parent::audienceCan( $field, $audience, $performer );
204    }
205
206    /**
207     * @see RevisionStore::isComplete
208     *
209     * @return bool always true.
210     */
211    public function isReadyForInsertion() {
212        return true;
213    }
214
215}