3 * Page revision base class.
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.
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.
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
23 namespace MediaWiki\Storage
;
25 use CommentStoreComment
;
27 use InvalidArgumentException
;
29 use MediaWiki\Linker\LinkTarget
;
30 use MediaWiki\User\UserIdentity
;
34 use Wikimedia\Assert\Assert
;
37 * Page revision base class.
39 * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
40 * Note that while the base class has no setters, subclasses may offer a mutable interface.
44 abstract class RevisionRecord
{
46 // RevisionRecord deletion constants
47 const DELETED_TEXT
= 1;
48 const DELETED_COMMENT
= 2;
49 const DELETED_USER
= 4;
50 const DELETED_RESTRICTED
= 8;
51 const SUPPRESSED_USER
= 12; // convenience
52 const SUPPRESSED_ALL
= 15; // convenience
54 // Audience options for accessors
56 const FOR_THIS_USER
= 2;
59 /** @var string Wiki ID; false means the current wiki */
60 protected $mWiki = false;
65 /** @var UserIdentity|null */
68 protected $mMinorEdit = false;
69 /** @var string|null */
70 protected $mTimestamp;
71 /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
72 protected $mDeleted = 0;
75 /** @var string|null */
79 /** @var CommentStoreComment|null */
83 protected $mTitle; // TODO: we only need the title for permission checks!
85 /** @var RevisionSlots */
89 * @note Avoid calling this constructor directly. Use the appropriate methods
90 * in RevisionStore instead.
92 * @param Title $title The title of the page this Revision is associated with.
93 * @param RevisionSlots $slots The slots of this revision.
94 * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
95 * or false for the local site.
99 function __construct( Title
$title, RevisionSlots
$slots, $wikiId = false ) {
100 Assert
::parameterType( 'string|boolean', $wikiId, '$wikiId' );
102 $this->mTitle
= $title;
103 $this->mSlots
= $slots;
104 $this->mWiki
= $wikiId;
106 // XXX: this is a sensible default, but we may not have a Title object here in the future.
107 $this->mPageId
= $title->getArticleID();
111 * Implemented to defy serialization.
113 * @throws LogicException always
115 public function __sleep() {
116 throw new LogicException( __CLASS__
. ' is not serializable.' );
120 * @param RevisionRecord $rec
122 * @return bool True if this RevisionRecord is known to have same content as $rec.
123 * False if the content is different (or not known to be the same).
125 public function hasSameContent( RevisionRecord
$rec ) {
126 if ( $rec === $this ) {
130 if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
134 // check size before hash, since size is quicker to compute
135 if ( $this->getSize() !== $rec->getSize() ) {
139 // instead of checking the hash, we could also check the content addresses of all slots.
141 if ( $this->getSha1() === $rec->getSha1() ) {
149 * Returns the Content of the given slot of this revision.
150 * Call getSlotNames() to get a list of available slots.
152 * Note that for mutable Content objects, each call to this method will return a
155 * MCR migration note: this replaces Revision::getContent
157 * @param string $role The role name of the desired slot
158 * @param int $audience
159 * @param User|null $user
161 * @throws RevisionAccessException if the slot does not exist or slot data
162 * could not be lazy-loaded.
163 * @return Content|null The content of the given slot, or null if access is forbidden.
165 public function getContent( $role, $audience = self
::FOR_PUBLIC
, User
$user = null ) {
166 // XXX: throwing an exception would be nicer, but would a further
167 // departure from the signature of Revision::getContent(), and thus
168 // more complex and error prone refactoring.
169 if ( !$this->audienceCan( self
::DELETED_TEXT
, $audience, $user ) ) {
173 $content = $this->getSlot( $role, $audience, $user )->getContent();
174 return $content->copy();
178 * Returns meta-data for the given slot.
180 * @param string $role The role name of the desired slot
181 * @param int $audience
182 * @param User|null $user
184 * @throws RevisionAccessException if the slot does not exist or slot data
185 * could not be lazy-loaded.
186 * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
187 * calling getContent() on the SlotRecord will throw an exception.
189 public function getSlot( $role, $audience = self
::FOR_PUBLIC
, User
$user = null ) {
190 $slot = $this->mSlots
->getSlot( $role );
192 if ( !$this->audienceCan( self
::DELETED_TEXT
, $audience, $user ) ) {
193 return SlotRecord
::newWithSuppressedContent( $slot );
200 * Returns whether the given slot is defined in this revision.
202 * @param string $role The role name of the desired slot
206 public function hasSlot( $role ) {
207 return $this->mSlots
->hasSlot( $role );
211 * Returns the slot names (roles) of all slots present in this revision.
212 * getContent() will succeed only for the names returned by this method.
216 public function getSlotRoles() {
217 return $this->mSlots
->getSlotRoles();
221 * Get revision ID. Depending on the concrete subclass, this may return null if
222 * the revision ID is not known (e.g. because the revision does not yet exist
225 * MCR migration note: this replaces Revision::getId
229 public function getId() {
234 * Get parent revision ID (the original previous page revision).
235 * If there is no parent revision, this returns 0.
236 * If the parent revision is undefined or unknown, this returns null.
238 * @note As of MW 1.31, the database schema allows the parent ID to be
239 * NULL to indicate that it is unknown.
241 * MCR migration note: this replaces Revision::getParentId
245 public function getParentId() {
246 return $this->mParentId
;
250 * Returns the nominal size of this revision, in bogo-bytes.
251 * May be calculated on the fly if not known, which may in the worst
252 * case may involve loading all content.
254 * MCR migration note: this replaces Revision::getSize
256 * @throws RevisionAccessException if the size was unknown and could not be calculated.
259 abstract public function getSize();
262 * Returns the base36 sha1 of this revision. This hash is derived from the
263 * hashes of all slots associated with the revision.
264 * May be calculated on the fly if not known, which may in the worst
265 * case may involve loading all content.
267 * MCR migration note: this replaces Revision::getSha1
269 * @throws RevisionAccessException if the hash was unknown and could not be calculated.
272 abstract public function getSha1();
275 * Get the page ID. If the page does not yet exist, the page ID is 0.
277 * MCR migration note: this replaces Revision::getPage
281 public function getPageId() {
282 return $this->mPageId
;
286 * Get the ID of the wiki this revision belongs to.
288 * @return string|false The wiki's logical name, of false to indicate the local wiki.
290 public function getWikiId() {
295 * Returns the title of the page this revision is associated with as a LinkTarget object.
297 * MCR migration note: this replaces Revision::getTitle
301 public function getPageAsLinkTarget() {
302 return $this->mTitle
;
306 * Fetch revision's author's user identity, if it's available to the specified audience.
307 * If the specified audience does not have access to it, null will be
308 * returned. Depending on the concrete subclass, null may also be returned if the user is
311 * MCR migration note: this replaces Revision::getUser
313 * @param int $audience One of:
314 * RevisionRecord::FOR_PUBLIC to be displayed to all users
315 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
316 * RevisionRecord::RAW get the ID regardless of permissions
317 * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
318 * to the $audience parameter
319 * @return UserIdentity|null
321 public function getUser( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
322 if ( !$this->audienceCan( self
::DELETED_USER
, $audience, $user ) ) {
330 * Fetch revision comment, if it's available to the specified audience.
331 * If the specified audience does not have access to the comment,
332 * this will return null. Depending on the concrete subclass, null may also be returned
333 * if the comment is not yet specified.
335 * MCR migration note: this replaces Revision::getComment
337 * @param int $audience One of:
338 * RevisionRecord::FOR_PUBLIC to be displayed to all users
339 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
340 * RevisionRecord::RAW get the text regardless of permissions
341 * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
342 * to the $audience parameter
344 * @return CommentStoreComment|null
346 public function getComment( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
347 if ( !$this->audienceCan( self
::DELETED_COMMENT
, $audience, $user ) ) {
350 return $this->mComment
;
355 * MCR migration note: this replaces Revision::isMinor
359 public function isMinor() {
360 return (bool)$this->mMinorEdit
;
364 * MCR migration note: this replaces Revision::isDeleted
366 * @param int $field One of DELETED_* bitfield constants
370 public function isDeleted( $field ) {
371 return ( $this->getVisibility() & $field ) == $field;
375 * Get the deletion bitfield of the revision
377 * MCR migration note: this replaces Revision::getVisibility
381 public function getVisibility() {
382 return (int)$this->mDeleted
;
386 * MCR migration note: this replaces Revision::getTimestamp.
388 * May return null if the timestamp was not specified.
390 * @return string|null
392 public function getTimestamp() {
393 return $this->mTimestamp
;
397 * Check that the given audience has access to the given field.
399 * MCR migration note: this corresponds to Revision::userCan
401 * @param int $field One of self::DELETED_TEXT,
402 * self::DELETED_COMMENT,
404 * @param int $audience One of:
405 * RevisionRecord::FOR_PUBLIC to be displayed to all users
406 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
407 * RevisionRecord::RAW get the text regardless of permissions
408 * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
413 protected function audienceCan( $field, $audience, User
$user = null ) {
414 if ( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( $field ) ) {
416 } elseif ( $audience == self
::FOR_THIS_USER
) {
418 throw new InvalidArgumentException(
419 'A User object must be given when checking FOR_THIS_USER audience.'
423 if ( !$this->userCan( $field, $user ) ) {
432 * Determine if the current user is allowed to view a particular
433 * field of this revision, if it's marked as deleted.
435 * MCR migration note: this corresponds to Revision::userCan
437 * @param int $field One of self::DELETED_TEXT,
438 * self::DELETED_COMMENT,
440 * @param User $user User object to check
443 protected function userCan( $field, User
$user ) {
444 // TODO: use callback for permission checks, so we don't need to know a Title object!
445 return self
::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle
);
449 * Determine if the current user is allowed to view a particular
450 * field of this revision, if it's marked as deleted. This is used
451 * by various classes to avoid duplication.
453 * MCR migration note: this replaces Revision::userCanBitfield
455 * @param int $bitfield Current field
456 * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
457 * self::DELETED_COMMENT = File::DELETED_COMMENT,
458 * self::DELETED_USER = File::DELETED_USER
459 * @param User $user User object to check
460 * @param Title|null $title A Title object to check for per-page restrictions on,
461 * instead of just plain userrights
464 public static function userCanBitfield( $bitfield, $field, User
$user, Title
$title = null ) {
465 if ( $bitfield & $field ) { // aspect is deleted
466 if ( $bitfield & self
::DELETED_RESTRICTED
) {
467 $permissions = [ 'suppressrevision', 'viewsuppressed' ];
468 } elseif ( $field & self
::DELETED_TEXT
) {
469 $permissions = [ 'deletedtext' ];
471 $permissions = [ 'deletedhistory' ];
473 $permissionlist = implode( ', ', $permissions );
474 if ( $title === null ) {
475 wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
476 return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
478 $text = $title->getPrefixedText();
479 wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
480 foreach ( $permissions as $perm ) {
481 if ( $title->userCan( $perm, $user ) ) {