3 * Representation of a page version.
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
22 use MediaWiki\Linker\LinkTarget
;
23 use MediaWiki\MediaWikiServices
;
24 use Wikimedia\Rdbms\ResultWrapper
;
25 use Wikimedia\Rdbms\FakeResultWrapper
;
30 class Revision
implements IDBAccessObject
{
38 protected $mOrigUserText;
42 protected $mMinorEdit;
44 protected $mTimestamp;
60 protected $mUnpatrolled;
62 /** @var stdClass|null */
65 /** @var null|Title */
70 protected $mContentModel;
72 protected $mContentFormat;
74 /** @var Content|null|bool */
76 /** @var null|ContentHandler */
77 protected $mContentHandler;
80 protected $mQueryFlags = 0;
81 /** @var bool Used for cached values to reload user text and rev_deleted */
82 protected $mRefreshMutableFields = false;
83 /** @var string Wiki ID; false means the current wiki */
84 protected $mWiki = false;
86 // Revision deletion constants
87 const DELETED_TEXT
= 1;
88 const DELETED_COMMENT
= 2;
89 const DELETED_USER
= 4;
90 const DELETED_RESTRICTED
= 8;
91 const SUPPRESSED_USER
= 12; // convenience
92 const SUPPRESSED_ALL
= 15; // convenience
94 // Audience options for accessors
96 const FOR_THIS_USER
= 2;
99 const TEXT_CACHE_GROUP
= 'revisiontext:10'; // process cache name and max key count
102 * Load a page revision from a given revision ID number.
103 * Returns null if no such revision can be found.
106 * Revision::READ_LATEST : Select the data from the master
107 * Revision::READ_LOCKING : Select & lock the data from the master
110 * @param int $flags (optional)
111 * @return Revision|null
113 public static function newFromId( $id, $flags = 0 ) {
114 return self
::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
118 * Load either the current, or a specified, revision
119 * that's attached to a given link target. If not attached
120 * to that link target, will return null.
123 * Revision::READ_LATEST : Select the data from the master
124 * Revision::READ_LOCKING : Select & lock the data from the master
126 * @param LinkTarget $linkTarget
127 * @param int $id (optional)
128 * @param int $flags Bitfield (optional)
129 * @return Revision|null
131 public static function newFromTitle( LinkTarget
$linkTarget, $id = 0, $flags = 0 ) {
133 'page_namespace' => $linkTarget->getNamespace(),
134 'page_title' => $linkTarget->getDBkey()
137 // Use the specified ID
138 $conds['rev_id'] = $id;
139 return self
::newFromConds( $conds, $flags );
141 // Use a join to get the latest revision
142 $conds[] = 'rev_id=page_latest';
143 $db = wfGetDB( ( $flags & self
::READ_LATEST
) ? DB_MASTER
: DB_REPLICA
);
144 return self
::loadFromConds( $db, $conds, $flags );
149 * Load either the current, or a specified, revision
150 * that's attached to a given page ID.
151 * Returns null if no such revision can be found.
154 * Revision::READ_LATEST : Select the data from the master (since 1.20)
155 * Revision::READ_LOCKING : Select & lock the data from the master
158 * @param int $revId (optional)
159 * @param int $flags Bitfield (optional)
160 * @return Revision|null
162 public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
163 $conds = [ 'page_id' => $pageId ];
165 $conds['rev_id'] = $revId;
166 return self
::newFromConds( $conds, $flags );
168 // Use a join to get the latest revision
169 $conds[] = 'rev_id = page_latest';
170 $db = wfGetDB( ( $flags & self
::READ_LATEST
) ? DB_MASTER
: DB_REPLICA
);
171 return self
::loadFromConds( $db, $conds, $flags );
176 * Make a fake revision object from an archive table row. This is queried
177 * for permissions or even inserted (as in Special:Undelete)
178 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
181 * @param array $overrides
183 * @throws MWException
186 public static function newFromArchiveRow( $row, $overrides = [] ) {
187 global $wgContentHandlerUseDB;
189 $attribs = $overrides +
[
190 'page' => isset( $row->ar_page_id
) ?
$row->ar_page_id
: null,
191 'id' => isset( $row->ar_rev_id
) ?
$row->ar_rev_id
: null,
192 'comment' => $row->ar_comment
,
193 'user' => $row->ar_user
,
194 'user_text' => $row->ar_user_text
,
195 'timestamp' => $row->ar_timestamp
,
196 'minor_edit' => $row->ar_minor_edit
,
197 'text_id' => isset( $row->ar_text_id
) ?
$row->ar_text_id
: null,
198 'deleted' => $row->ar_deleted
,
199 'len' => $row->ar_len
,
200 'sha1' => isset( $row->ar_sha1
) ?
$row->ar_sha1
: null,
201 'content_model' => isset( $row->ar_content_model
) ?
$row->ar_content_model
: null,
202 'content_format' => isset( $row->ar_content_format
) ?
$row->ar_content_format
: null,
205 if ( !$wgContentHandlerUseDB ) {
206 unset( $attribs['content_model'] );
207 unset( $attribs['content_format'] );
210 if ( !isset( $attribs['title'] )
211 && isset( $row->ar_namespace
)
212 && isset( $row->ar_title
)
214 $attribs['title'] = Title
::makeTitle( $row->ar_namespace
, $row->ar_title
);
217 if ( isset( $row->ar_text
) && !$row->ar_text_id
) {
218 // Pre-1.5 ar_text row
219 $attribs['text'] = self
::getRevisionText( $row, 'ar_' );
220 if ( $attribs['text'] === false ) {
221 throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
224 return new self( $attribs );
233 public static function newFromRow( $row ) {
234 return new self( $row );
238 * Load a page revision from a given revision ID number.
239 * Returns null if no such revision can be found.
241 * @param IDatabase $db
243 * @return Revision|null
245 public static function loadFromId( $db, $id ) {
246 return self
::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
250 * Load either the current, or a specified, revision
251 * that's attached to a given page. If not attached
252 * to that page, will return null.
254 * @param IDatabase $db
257 * @return Revision|null
259 public static function loadFromPageId( $db, $pageid, $id = 0 ) {
260 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
262 $conds['rev_id'] = intval( $id );
264 $conds[] = 'rev_id=page_latest';
266 return self
::loadFromConds( $db, $conds );
270 * Load either the current, or a specified, revision
271 * that's attached to a given page. If not attached
272 * to that page, will return null.
274 * @param IDatabase $db
275 * @param Title $title
277 * @return Revision|null
279 public static function loadFromTitle( $db, $title, $id = 0 ) {
281 $matchId = intval( $id );
283 $matchId = 'page_latest';
285 return self
::loadFromConds( $db,
288 'page_namespace' => $title->getNamespace(),
289 'page_title' => $title->getDBkey()
295 * Load the revision for the given title with the given timestamp.
296 * WARNING: Timestamps may in some circumstances not be unique,
297 * so this isn't the best key to use.
299 * @param IDatabase $db
300 * @param Title $title
301 * @param string $timestamp
302 * @return Revision|null
304 public static function loadFromTimestamp( $db, $title, $timestamp ) {
305 return self
::loadFromConds( $db,
307 'rev_timestamp' => $db->timestamp( $timestamp ),
308 'page_namespace' => $title->getNamespace(),
309 'page_title' => $title->getDBkey()
315 * Given a set of conditions, fetch a revision
317 * This method is used then a revision ID is qualified and
318 * will incorporate some basic replica DB/master fallback logic
320 * @param array $conditions
321 * @param int $flags (optional)
322 * @return Revision|null
324 private static function newFromConds( $conditions, $flags = 0 ) {
325 $db = wfGetDB( ( $flags & self
::READ_LATEST
) ? DB_MASTER
: DB_REPLICA
);
327 $rev = self
::loadFromConds( $db, $conditions, $flags );
328 // Make sure new pending/committed revision are visibile later on
329 // within web requests to certain avoid bugs like T93866 and T94407.
331 && !( $flags & self
::READ_LATEST
)
332 && wfGetLB()->getServerCount() > 1
333 && wfGetLB()->hasOrMadeRecentMasterChanges()
335 $flags = self
::READ_LATEST
;
336 $db = wfGetDB( DB_MASTER
);
337 $rev = self
::loadFromConds( $db, $conditions, $flags );
341 $rev->mQueryFlags
= $flags;
348 * Given a set of conditions, fetch a revision from
349 * the given database connection.
351 * @param IDatabase $db
352 * @param array $conditions
353 * @param int $flags (optional)
354 * @return Revision|null
356 private static function loadFromConds( $db, $conditions, $flags = 0 ) {
357 $row = self
::fetchFromConds( $db, $conditions, $flags );
359 $rev = new Revision( $row );
360 $rev->mWiki
= $db->getWikiID();
369 * Return a wrapper for a series of database rows to
370 * fetch all of a given page's revisions in turn.
371 * Each row can be fed to the constructor to get objects.
373 * @param LinkTarget $title
374 * @return ResultWrapper
375 * @deprecated Since 1.28
377 public static function fetchRevision( LinkTarget
$title ) {
378 $row = self
::fetchFromConds(
379 wfGetDB( DB_REPLICA
),
381 'rev_id=page_latest',
382 'page_namespace' => $title->getNamespace(),
383 'page_title' => $title->getDBkey()
387 return new FakeResultWrapper( $row ?
[ $row ] : [] );
391 * Given a set of conditions, return a ResultWrapper
392 * which will return matching database rows with the
393 * fields necessary to build Revision objects.
395 * @param IDatabase $db
396 * @param array $conditions
397 * @param int $flags (optional)
400 private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
401 $fields = array_merge(
402 self
::selectFields(),
403 self
::selectPageFields(),
404 self
::selectUserFields()
407 if ( ( $flags & self
::READ_LOCKING
) == self
::READ_LOCKING
) {
408 $options[] = 'FOR UPDATE';
410 return $db->selectRow(
411 [ 'revision', 'page', 'user' ],
416 [ 'page' => self
::pageJoinCond(), 'user' => self
::userJoinCond() ]
421 * Return the value of a select() JOIN conds array for the user table.
422 * This will get user table rows for logged-in users.
426 public static function userJoinCond() {
427 return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
431 * Return the value of a select() page conds array for the page table.
432 * This will assure that the revision(s) are not orphaned from live pages.
436 public static function pageJoinCond() {
437 return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
441 * Return the list of revision fields that should be selected to create
445 public static function selectFields() {
446 global $wgContentHandlerUseDB;
463 if ( $wgContentHandlerUseDB ) {
464 $fields[] = 'rev_content_format';
465 $fields[] = 'rev_content_model';
472 * Return the list of revision fields that should be selected to create
473 * a new revision from an archive row.
476 public static function selectArchiveFields() {
477 global $wgContentHandlerUseDB;
495 if ( $wgContentHandlerUseDB ) {
496 $fields[] = 'ar_content_format';
497 $fields[] = 'ar_content_model';
503 * Return the list of text fields that should be selected to read the
507 public static function selectTextFields() {
515 * Return the list of page fields that should be selected from page table
518 public static function selectPageFields() {
530 * Return the list of user fields that should be selected from user table
533 public static function selectUserFields() {
534 return [ 'user_name' ];
538 * Do a batched query to get the parent revision lengths
539 * @param IDatabase $db
540 * @param array $revIds
543 public static function getParentLengths( $db, array $revIds ) {
546 return $revLens; // empty
548 $res = $db->select( 'revision',
549 [ 'rev_id', 'rev_len' ],
550 [ 'rev_id' => $revIds ],
552 foreach ( $res as $row ) {
553 $revLens[$row->rev_id
] = $row->rev_len
;
561 * @param object|array $row Either a database row or an array
562 * @throws MWException
565 function __construct( $row ) {
566 if ( is_object( $row ) ) {
567 $this->mId
= intval( $row->rev_id
);
568 $this->mPage
= intval( $row->rev_page
);
569 $this->mTextId
= intval( $row->rev_text_id
);
570 $this->mComment
= $row->rev_comment
;
571 $this->mUser
= intval( $row->rev_user
);
572 $this->mMinorEdit
= intval( $row->rev_minor_edit
);
573 $this->mTimestamp
= $row->rev_timestamp
;
574 $this->mDeleted
= intval( $row->rev_deleted
);
576 if ( !isset( $row->rev_parent_id
) ) {
577 $this->mParentId
= null;
579 $this->mParentId
= intval( $row->rev_parent_id
);
582 if ( !isset( $row->rev_len
) ) {
585 $this->mSize
= intval( $row->rev_len
);
588 if ( !isset( $row->rev_sha1
) ) {
591 $this->mSha1
= $row->rev_sha1
;
594 if ( isset( $row->page_latest
) ) {
595 $this->mCurrent
= ( $row->rev_id
== $row->page_latest
);
596 $this->mTitle
= Title
::newFromRow( $row );
598 $this->mCurrent
= false;
599 $this->mTitle
= null;
602 if ( !isset( $row->rev_content_model
) ) {
603 $this->mContentModel
= null; # determine on demand if needed
605 $this->mContentModel
= strval( $row->rev_content_model
);
608 if ( !isset( $row->rev_content_format
) ) {
609 $this->mContentFormat
= null; # determine on demand if needed
611 $this->mContentFormat
= strval( $row->rev_content_format
);
614 // Lazy extraction...
616 if ( isset( $row->old_text
) ) {
617 $this->mTextRow
= $row;
619 // 'text' table row entry will be lazy-loaded
620 $this->mTextRow
= null;
623 // Use user_name for users and rev_user_text for IPs...
624 $this->mUserText
= null; // lazy load if left null
625 if ( $this->mUser
== 0 ) {
626 $this->mUserText
= $row->rev_user_text
; // IP user
627 } elseif ( isset( $row->user_name
) ) {
628 $this->mUserText
= $row->user_name
; // logged-in user
630 $this->mOrigUserText
= $row->rev_user_text
;
631 } elseif ( is_array( $row ) ) {
632 // Build a new revision to be saved...
633 global $wgUser; // ugh
635 # if we have a content object, use it to set the model and type
636 if ( !empty( $row['content'] ) ) {
637 // @todo when is that set? test with external store setup! check out insertOn() [dk]
638 if ( !empty( $row['text_id'] ) ) {
639 throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
640 "can't serialize content object" );
643 $row['content_model'] = $row['content']->getModel();
644 # note: mContentFormat is initializes later accordingly
645 # note: content is serialized later in this method!
646 # also set text to null?
649 $this->mId
= isset( $row['id'] ) ?
intval( $row['id'] ) : null;
650 $this->mPage
= isset( $row['page'] ) ?
intval( $row['page'] ) : null;
651 $this->mTextId
= isset( $row['text_id'] ) ?
intval( $row['text_id'] ) : null;
652 $this->mUserText
= isset( $row['user_text'] )
653 ?
strval( $row['user_text'] ) : $wgUser->getName();
654 $this->mUser
= isset( $row['user'] ) ?
intval( $row['user'] ) : $wgUser->getId();
655 $this->mMinorEdit
= isset( $row['minor_edit'] ) ?
intval( $row['minor_edit'] ) : 0;
656 $this->mTimestamp
= isset( $row['timestamp'] )
657 ?
strval( $row['timestamp'] ) : wfTimestampNow();
658 $this->mDeleted
= isset( $row['deleted'] ) ?
intval( $row['deleted'] ) : 0;
659 $this->mSize
= isset( $row['len'] ) ?
intval( $row['len'] ) : null;
660 $this->mParentId
= isset( $row['parent_id'] ) ?
intval( $row['parent_id'] ) : null;
661 $this->mSha1
= isset( $row['sha1'] ) ?
strval( $row['sha1'] ) : null;
663 $this->mContentModel
= isset( $row['content_model'] )
664 ?
strval( $row['content_model'] ) : null;
665 $this->mContentFormat
= isset( $row['content_format'] )
666 ?
strval( $row['content_format'] ) : null;
668 // Enforce spacing trimming on supplied text
669 $this->mComment
= isset( $row['comment'] ) ?
trim( strval( $row['comment'] ) ) : null;
670 $this->mText
= isset( $row['text'] ) ?
rtrim( strval( $row['text'] ) ) : null;
671 $this->mTextRow
= null;
673 $this->mTitle
= isset( $row['title'] ) ?
$row['title'] : null;
675 // if we have a Content object, override mText and mContentModel
676 if ( !empty( $row['content'] ) ) {
677 if ( !( $row['content'] instanceof Content
) ) {
678 throw new MWException( '`content` field must contain a Content object.' );
681 $handler = $this->getContentHandler();
682 $this->mContent
= $row['content'];
684 $this->mContentModel
= $this->mContent
->getModel();
685 $this->mContentHandler
= null;
687 $this->mText
= $handler->serializeContent( $row['content'], $this->getContentFormat() );
688 } elseif ( $this->mText
!== null ) {
689 $handler = $this->getContentHandler();
690 $this->mContent
= $handler->unserializeContent( $this->mText
);
693 // If we have a Title object, make sure it is consistent with mPage.
694 if ( $this->mTitle
&& $this->mTitle
->exists() ) {
695 if ( $this->mPage
=== null ) {
696 // if the page ID wasn't known, set it now
697 $this->mPage
= $this->mTitle
->getArticleID();
698 } elseif ( $this->mTitle
->getArticleID() !== $this->mPage
) {
699 // Got different page IDs. This may be legit (e.g. during undeletion),
700 // but it seems worth mentioning it in the log.
701 wfDebug( "Page ID " . $this->mPage
. " mismatches the ID " .
702 $this->mTitle
->getArticleID() . " provided by the Title object." );
706 $this->mCurrent
= false;
708 // If we still have no length, see it we have the text to figure it out
709 if ( !$this->mSize
&& $this->mContent
!== null ) {
710 $this->mSize
= $this->mContent
->getSize();
714 if ( $this->mSha1
=== null ) {
715 $this->mSha1
= $this->mText
=== null ?
null : self
::base36Sha1( $this->mText
);
719 $this->getContentModel();
720 $this->getContentFormat();
722 throw new MWException( 'Revision constructor passed invalid row format.' );
724 $this->mUnpatrolled
= null;
732 public function getId() {
737 * Set the revision ID
739 * This should only be used for proposed revisions that turn out to be null edits
744 public function setId( $id ) {
745 $this->mId
= (int)$id;
749 * Set the user ID/name
751 * This should only be used for proposed revisions that turn out to be null edits
754 * @param integer $id User ID
755 * @param string $name User name
757 public function setUserIdAndName( $id, $name ) {
758 $this->mUser
= (int)$id;
759 $this->mUserText
= $name;
760 $this->mOrigUserText
= $name;
768 public function getTextId() {
769 return $this->mTextId
;
773 * Get parent revision ID (the original previous page revision)
777 public function getParentId() {
778 return $this->mParentId
;
782 * Returns the length of the text in this revision, or null if unknown.
786 public function getSize() {
791 * Returns the base36 sha1 of the text in this revision, or null if unknown.
793 * @return string|null
795 public function getSha1() {
800 * Returns the title of the page associated with this entry or null.
802 * Will do a query, when title is not set and id is given.
806 public function getTitle() {
807 if ( $this->mTitle
!== null ) {
808 return $this->mTitle
;
810 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
811 if ( $this->mId
!== null ) {
812 $dbr = wfGetLB( $this->mWiki
)->getConnectionRef( DB_REPLICA
, [], $this->mWiki
);
813 $row = $dbr->selectRow(
814 [ 'page', 'revision' ],
815 self
::selectPageFields(),
816 [ 'page_id=rev_page', 'rev_id' => $this->mId
],
820 // @TODO: better foreign title handling
821 $this->mTitle
= Title
::newFromRow( $row );
825 if ( $this->mWiki
=== false ||
$this->mWiki
=== wfWikiID() ) {
826 // Loading by ID is best, though not possible for foreign titles
827 if ( !$this->mTitle
&& $this->mPage
!== null && $this->mPage
> 0 ) {
828 $this->mTitle
= Title
::newFromID( $this->mPage
);
832 return $this->mTitle
;
836 * Set the title of the revision
838 * @param Title $title
840 public function setTitle( $title ) {
841 $this->mTitle
= $title;
849 public function getPage() {
854 * Fetch revision's user id if it's available to the specified audience.
855 * If the specified audience does not have access to it, zero will be
858 * @param int $audience One of:
859 * Revision::FOR_PUBLIC to be displayed to all users
860 * Revision::FOR_THIS_USER to be displayed to the given user
861 * Revision::RAW get the ID regardless of permissions
862 * @param User $user User object to check for, only if FOR_THIS_USER is passed
863 * to the $audience parameter
866 public function getUser( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
867 if ( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_USER
) ) {
869 } elseif ( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_USER
, $user ) ) {
877 * Fetch revision's user id without regard for the current user's permissions
880 * @deprecated since 1.25, use getUser( Revision::RAW )
882 public function getRawUser() {
883 wfDeprecated( __METHOD__
, '1.25' );
884 return $this->getUser( self
::RAW
);
888 * Fetch revision's username if it's available to the specified audience.
889 * If the specified audience does not have access to the username, an
890 * empty string will be returned.
892 * @param int $audience One of:
893 * Revision::FOR_PUBLIC to be displayed to all users
894 * Revision::FOR_THIS_USER to be displayed to the given user
895 * Revision::RAW get the text regardless of permissions
896 * @param User $user User object to check for, only if FOR_THIS_USER is passed
897 * to the $audience parameter
900 public function getUserText( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
901 $this->loadMutableFields();
903 if ( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_USER
) ) {
905 } elseif ( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_USER
, $user ) ) {
908 if ( $this->mUserText
=== null ) {
909 $this->mUserText
= User
::whoIs( $this->mUser
); // load on demand
910 if ( $this->mUserText
=== false ) {
911 # This shouldn't happen, but it can if the wiki was recovered
912 # via importing revs and there is no user table entry yet.
913 $this->mUserText
= $this->mOrigUserText
;
916 return $this->mUserText
;
921 * Fetch revision's username without regard for view restrictions
924 * @deprecated since 1.25, use getUserText( Revision::RAW )
926 public function getRawUserText() {
927 wfDeprecated( __METHOD__
, '1.25' );
928 return $this->getUserText( self
::RAW
);
932 * Fetch revision comment if it's available to the specified audience.
933 * If the specified audience does not have access to the comment, an
934 * empty string will be returned.
936 * @param int $audience One of:
937 * Revision::FOR_PUBLIC to be displayed to all users
938 * Revision::FOR_THIS_USER to be displayed to the given user
939 * Revision::RAW get the text regardless of permissions
940 * @param User $user User object to check for, only if FOR_THIS_USER is passed
941 * to the $audience parameter
944 function getComment( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
945 if ( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_COMMENT
) ) {
947 } elseif ( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_COMMENT
, $user ) ) {
950 return $this->mComment
;
955 * Fetch revision comment without regard for the current user's permissions
958 * @deprecated since 1.25, use getComment( Revision::RAW )
960 public function getRawComment() {
961 wfDeprecated( __METHOD__
, '1.25' );
962 return $this->getComment( self
::RAW
);
968 public function isMinor() {
969 return (bool)$this->mMinorEdit
;
973 * @return int Rcid of the unpatrolled row, zero if there isn't one
975 public function isUnpatrolled() {
976 if ( $this->mUnpatrolled
!== null ) {
977 return $this->mUnpatrolled
;
979 $rc = $this->getRecentChange();
980 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
981 $this->mUnpatrolled
= $rc->getAttribute( 'rc_id' );
983 $this->mUnpatrolled
= 0;
985 return $this->mUnpatrolled
;
989 * Get the RC object belonging to the current revision, if there's one
991 * @param int $flags (optional) $flags include:
992 * Revision::READ_LATEST : Select the data from the master
995 * @return RecentChange|null
997 public function getRecentChange( $flags = 0 ) {
998 $dbr = wfGetDB( DB_REPLICA
);
1000 list( $dbType, ) = DBAccessObjectUtils
::getDBOptions( $flags );
1002 return RecentChange
::newFromConds(
1004 'rc_user_text' => $this->getUserText( Revision
::RAW
),
1005 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
1006 'rc_this_oldid' => $this->getId()
1014 * @param int $field One of DELETED_* bitfield constants
1018 public function isDeleted( $field ) {
1019 if ( $this->isCurrent() && $field === self
::DELETED_TEXT
) {
1020 // Current revisions of pages cannot have the content hidden. Skipping this
1021 // check is very useful for Parser as it fetches templates using newKnownCurrent().
1022 // Calling getVisibility() in that case triggers a verification database query.
1023 return false; // no need to check
1026 return ( $this->getVisibility() & $field ) == $field;
1030 * Get the deletion bitfield of the revision
1034 public function getVisibility() {
1035 $this->loadMutableFields();
1037 return (int)$this->mDeleted
;
1041 * Fetch revision content if it's available to the specified audience.
1042 * If the specified audience does not have the ability to view this
1043 * revision, null will be returned.
1045 * @param int $audience One of:
1046 * Revision::FOR_PUBLIC to be displayed to all users
1047 * Revision::FOR_THIS_USER to be displayed to $wgUser
1048 * Revision::RAW get the text regardless of permissions
1049 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1050 * to the $audience parameter
1052 * @return Content|null
1054 public function getContent( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
1055 if ( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_TEXT
) ) {
1057 } elseif ( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_TEXT
, $user ) ) {
1060 return $this->getContentInternal();
1065 * Get original serialized data (without checking view restrictions)
1070 public function getSerializedData() {
1071 if ( $this->mText
=== null ) {
1072 // Revision is immutable. Load on demand.
1073 $this->mText
= $this->loadText();
1076 return $this->mText
;
1080 * Gets the content object for the revision (or null on failure).
1082 * Note that for mutable Content objects, each call to this method will return a
1086 * @return Content|null The Revision's content, or null on failure.
1088 protected function getContentInternal() {
1089 if ( $this->mContent
=== null ) {
1090 $text = $this->getSerializedData();
1092 if ( $text !== null && $text !== false ) {
1093 // Unserialize content
1094 $handler = $this->getContentHandler();
1095 $format = $this->getContentFormat();
1097 $this->mContent
= $handler->unserializeContent( $text, $format );
1101 // NOTE: copy() will return $this for immutable content objects
1102 return $this->mContent ?
$this->mContent
->copy() : null;
1106 * Returns the content model for this revision.
1108 * If no content model was stored in the database, the default content model for the title is
1109 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
1110 * is used as a last resort.
1112 * @return string The content model id associated with this revision,
1113 * see the CONTENT_MODEL_XXX constants.
1115 public function getContentModel() {
1116 if ( !$this->mContentModel
) {
1117 $title = $this->getTitle();
1119 $this->mContentModel
= ContentHandler
::getDefaultModelFor( $title );
1121 $this->mContentModel
= CONTENT_MODEL_WIKITEXT
;
1124 assert( !empty( $this->mContentModel
) );
1127 return $this->mContentModel
;
1131 * Returns the content format for this revision.
1133 * If no content format was stored in the database, the default format for this
1134 * revision's content model is returned.
1136 * @return string The content format id associated with this revision,
1137 * see the CONTENT_FORMAT_XXX constants.
1139 public function getContentFormat() {
1140 if ( !$this->mContentFormat
) {
1141 $handler = $this->getContentHandler();
1142 $this->mContentFormat
= $handler->getDefaultFormat();
1144 assert( !empty( $this->mContentFormat
) );
1147 return $this->mContentFormat
;
1151 * Returns the content handler appropriate for this revision's content model.
1153 * @throws MWException
1154 * @return ContentHandler
1156 public function getContentHandler() {
1157 if ( !$this->mContentHandler
) {
1158 $model = $this->getContentModel();
1159 $this->mContentHandler
= ContentHandler
::getForModelID( $model );
1161 $format = $this->getContentFormat();
1163 if ( !$this->mContentHandler
->isSupportedFormat( $format ) ) {
1164 throw new MWException( "Oops, the content format $format is not supported for "
1165 . "this content model, $model" );
1169 return $this->mContentHandler
;
1175 public function getTimestamp() {
1176 return wfTimestamp( TS_MW
, $this->mTimestamp
);
1182 public function isCurrent() {
1183 return $this->mCurrent
;
1187 * Get previous revision for this title
1189 * @return Revision|null
1191 public function getPrevious() {
1192 if ( $this->getTitle() ) {
1193 $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
1195 return self
::newFromTitle( $this->getTitle(), $prev );
1202 * Get next revision for this title
1204 * @return Revision|null
1206 public function getNext() {
1207 if ( $this->getTitle() ) {
1208 $next = $this->getTitle()->getNextRevisionID( $this->getId() );
1210 return self
::newFromTitle( $this->getTitle(), $next );
1217 * Get previous revision Id for this page_id
1218 * This is used to populate rev_parent_id on save
1220 * @param IDatabase $db
1223 private function getPreviousRevisionId( $db ) {
1224 if ( $this->mPage
=== null ) {
1227 # Use page_latest if ID is not given
1228 if ( !$this->mId
) {
1229 $prevId = $db->selectField( 'page', 'page_latest',
1230 [ 'page_id' => $this->mPage
],
1233 $prevId = $db->selectField( 'revision', 'rev_id',
1234 [ 'rev_page' => $this->mPage
, 'rev_id < ' . $this->mId
],
1236 [ 'ORDER BY' => 'rev_id DESC' ] );
1238 return intval( $prevId );
1242 * Get revision text associated with an old or archive row
1244 * Both the flags and the text field must be included. Including the old_id
1245 * field will activate cache usage as long as the $wiki parameter is not set.
1247 * @param stdClass $row The text data
1248 * @param string $prefix Table prefix (default 'old_')
1249 * @param string|bool $wiki The name of the wiki to load the revision text from
1250 * (same as the the wiki $row was loaded from) or false to indicate the local
1251 * wiki (this is the default). Otherwise, it must be a symbolic wiki database
1252 * identifier as understood by the LoadBalancer class.
1253 * @return string|false Text the text requested or false on failure
1255 public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
1256 $textField = $prefix . 'text';
1257 $flagsField = $prefix . 'flags';
1259 if ( isset( $row->$flagsField ) ) {
1260 $flags = explode( ',', $row->$flagsField );
1265 if ( isset( $row->$textField ) ) {
1266 $text = $row->$textField;
1271 // Use external methods for external objects, text in table is URL-only then
1272 if ( in_array( 'external', $flags ) ) {
1274 $parts = explode( '://', $url, 2 );
1275 if ( count( $parts ) == 1 ||
$parts[1] == '' ) {
1279 if ( isset( $row->old_id
) && $wiki === false ) {
1280 // Make use of the wiki-local revision text cache
1281 $cache = MediaWikiServices
::getInstance()->getMainWANObjectCache();
1282 $text = $cache->getWithSetCallback(
1283 $cache->makeKey( 'revisiontext', 'textid', $row->old_id
),
1284 self
::getCacheTTL( $cache ),
1285 function () use ( $url, $wiki ) {
1286 // No negative caching per Revision::loadText()
1287 return ExternalStore
::fetchFromURL( $url, [ 'wiki' => $wiki ] );
1289 [ 'pcGroup' => self
::TEXT_CACHE_GROUP
, 'pcTTL' => $cache::TTL_PROC_LONG
]
1292 $text = ExternalStore
::fetchFromURL( $url, [ 'wiki' => $wiki ] );
1296 // If the text was fetched without an error, convert it
1297 if ( $text !== false ) {
1298 $text = self
::decompressRevisionText( $text, $flags );
1305 * If $wgCompressRevisions is enabled, we will compress data.
1306 * The input string is modified in place.
1307 * Return value is the flags field: contains 'gzip' if the
1308 * data is compressed, and 'utf-8' if we're saving in UTF-8
1311 * @param mixed $text Reference to a text
1314 public static function compressRevisionText( &$text ) {
1315 global $wgCompressRevisions;
1318 # Revisions not marked this way will be converted
1319 # on load if $wgLegacyCharset is set in the future.
1322 if ( $wgCompressRevisions ) {
1323 if ( function_exists( 'gzdeflate' ) ) {
1324 $deflated = gzdeflate( $text );
1326 if ( $deflated === false ) {
1327 wfLogWarning( __METHOD__
. ': gzdeflate() failed' );
1333 wfDebug( __METHOD__
. " -- no zlib support, not compressing\n" );
1336 return implode( ',', $flags );
1340 * Re-converts revision text according to it's flags.
1342 * @param mixed $text Reference to a text
1343 * @param array $flags Compression flags
1344 * @return string|bool Decompressed text, or false on failure
1346 public static function decompressRevisionText( $text, $flags ) {
1347 if ( in_array( 'gzip', $flags ) ) {
1348 # Deal with optional compression of archived pages.
1349 # This can be done periodically via maintenance/compressOld.php, and
1350 # as pages are saved if $wgCompressRevisions is set.
1351 $text = gzinflate( $text );
1353 if ( $text === false ) {
1354 wfLogWarning( __METHOD__
. ': gzinflate() failed' );
1359 if ( in_array( 'object', $flags ) ) {
1360 # Generic compressed storage
1361 $obj = unserialize( $text );
1362 if ( !is_object( $obj ) ) {
1366 $text = $obj->getText();
1369 global $wgLegacyEncoding;
1370 if ( $text !== false && $wgLegacyEncoding
1371 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
1373 # Old revisions kept around in a legacy encoding?
1374 # Upconvert on demand.
1375 # ("utf8" checked for compatibility with some broken
1376 # conversion scripts 2008-12-30)
1378 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
1385 * Insert a new revision into the database, returning the new revision ID
1386 * number on success and dies horribly on failure.
1388 * @param IDatabase $dbw (master connection)
1389 * @throws MWException
1392 public function insertOn( $dbw ) {
1393 global $wgDefaultExternalStore, $wgContentHandlerUseDB;
1395 // We're inserting a new revision, so we have to use master anyway.
1396 // If it's a null revision, it may have references to rows that
1397 // are not in the replica yet (the text row).
1398 $this->mQueryFlags |
= self
::READ_LATEST
;
1400 // Not allowed to have rev_page equal to 0, false, etc.
1401 if ( !$this->mPage
) {
1402 $title = $this->getTitle();
1403 if ( $title instanceof Title
) {
1404 $titleText = ' for page ' . $title->getPrefixedText();
1408 throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
1411 $this->checkContentModel();
1413 $data = $this->mText
;
1414 $flags = self
::compressRevisionText( $data );
1416 # Write to external storage if required
1417 if ( $wgDefaultExternalStore ) {
1418 // Store and get the URL
1419 $data = ExternalStore
::insertToDefault( $data );
1421 throw new MWException( "Unable to store text to external storage" );
1426 $flags .= 'external';
1429 # Record the text (or external storage URL) to the text table
1430 if ( $this->mTextId
=== null ) {
1431 $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1432 $dbw->insert( 'text',
1434 'old_id' => $old_id,
1435 'old_text' => $data,
1436 'old_flags' => $flags,
1439 $this->mTextId
= $dbw->insertId();
1442 if ( $this->mComment
=== null ) {
1443 $this->mComment
= "";
1446 # Record the edit in revisions
1447 $rev_id = $this->mId
!== null
1449 : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1451 'rev_id' => $rev_id,
1452 'rev_page' => $this->mPage
,
1453 'rev_text_id' => $this->mTextId
,
1454 'rev_comment' => $this->mComment
,
1455 'rev_minor_edit' => $this->mMinorEdit ?
1 : 0,
1456 'rev_user' => $this->mUser
,
1457 'rev_user_text' => $this->mUserText
,
1458 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp
),
1459 'rev_deleted' => $this->mDeleted
,
1460 'rev_len' => $this->mSize
,
1461 'rev_parent_id' => $this->mParentId
=== null
1462 ?
$this->getPreviousRevisionId( $dbw )
1464 'rev_sha1' => $this->mSha1
=== null
1465 ? Revision
::base36Sha1( $this->mText
)
1469 if ( $wgContentHandlerUseDB ) {
1470 // NOTE: Store null for the default model and format, to save space.
1471 // XXX: Makes the DB sensitive to changed defaults.
1472 // Make this behavior optional? Only in miser mode?
1474 $model = $this->getContentModel();
1475 $format = $this->getContentFormat();
1477 $title = $this->getTitle();
1479 if ( $title === null ) {
1480 throw new MWException( "Insufficient information to determine the title of the "
1481 . "revision's page!" );
1484 $defaultModel = ContentHandler
::getDefaultModelFor( $title );
1485 $defaultFormat = ContentHandler
::getForModelID( $defaultModel )->getDefaultFormat();
1487 $row['rev_content_model'] = ( $model === $defaultModel ) ?
null : $model;
1488 $row['rev_content_format'] = ( $format === $defaultFormat ) ?
null : $format;
1491 $dbw->insert( 'revision', $row, __METHOD__
);
1493 $this->mId
= $rev_id !== null ?
$rev_id : $dbw->insertId();
1495 // Assertion to try to catch T92046
1496 if ( (int)$this->mId
=== 0 ) {
1497 throw new UnexpectedValueException(
1498 'After insert, Revision mId is ' . var_export( $this->mId
, 1 ) . ': ' .
1499 var_export( $row, 1 )
1503 // Avoid PHP 7.1 warning of passing $this by reference
1505 Hooks
::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
1510 protected function checkContentModel() {
1511 global $wgContentHandlerUseDB;
1513 // Note: may return null for revisions that have not yet been inserted
1514 $title = $this->getTitle();
1516 $model = $this->getContentModel();
1517 $format = $this->getContentFormat();
1518 $handler = $this->getContentHandler();
1520 if ( !$handler->isSupportedFormat( $format ) ) {
1521 $t = $title->getPrefixedDBkey();
1523 throw new MWException( "Can't use format $format with content model $model on $t" );
1526 if ( !$wgContentHandlerUseDB && $title ) {
1527 // if $wgContentHandlerUseDB is not set,
1528 // all revisions must use the default content model and format.
1530 $defaultModel = ContentHandler
::getDefaultModelFor( $title );
1531 $defaultHandler = ContentHandler
::getForModelID( $defaultModel );
1532 $defaultFormat = $defaultHandler->getDefaultFormat();
1534 if ( $this->getContentModel() != $defaultModel ) {
1535 $t = $title->getPrefixedDBkey();
1537 throw new MWException( "Can't save non-default content model with "
1538 . "\$wgContentHandlerUseDB disabled: model is $model, "
1539 . "default for $t is $defaultModel" );
1542 if ( $this->getContentFormat() != $defaultFormat ) {
1543 $t = $title->getPrefixedDBkey();
1545 throw new MWException( "Can't use non-default content format with "
1546 . "\$wgContentHandlerUseDB disabled: format is $format, "
1547 . "default for $t is $defaultFormat" );
1551 $content = $this->getContent( Revision
::RAW
);
1552 $prefixedDBkey = $title->getPrefixedDBkey();
1553 $revId = $this->mId
;
1556 throw new MWException(
1557 "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
1560 if ( !$content->isValid() ) {
1561 throw new MWException(
1562 "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
1568 * Get the base 36 SHA-1 value for a string of text
1569 * @param string $text
1572 public static function base36Sha1( $text ) {
1573 return Wikimedia\base_convert
( sha1( $text ), 16, 36, 31 );
1577 * Get the text cache TTL
1579 * @param WANObjectCache $cache
1582 private static function getCacheTTL( WANObjectCache
$cache ) {
1583 global $wgRevisionCacheExpiry;
1585 if ( $cache->getQoS( $cache::ATTR_EMULATION
) <= $cache::QOS_EMULATION_SQL
) {
1586 // Do not cache RDBMs blobs in...the RDBMs store
1587 $ttl = $cache::TTL_UNCACHEABLE
;
1589 $ttl = $wgRevisionCacheExpiry ?
: $cache::TTL_UNCACHEABLE
;
1596 * Lazy-load the revision's text.
1597 * Currently hardcoded to the 'text' table storage engine.
1599 * @return string|bool The revision's text, or false on failure
1601 private function loadText() {
1602 $cache = ObjectCache
::getMainWANInstance();
1604 // No negative caching; negative hits on text rows may be due to corrupted replica DBs
1605 return $cache->getWithSetCallback(
1606 $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
1607 self
::getCacheTTL( $cache ),
1609 return $this->fetchText();
1611 [ 'pcGroup' => self
::TEXT_CACHE_GROUP
, 'pcTTL' => $cache::TTL_PROC_LONG
]
1615 private function fetchText() {
1616 $textId = $this->getTextId();
1618 // If we kept data for lazy extraction, use it now...
1619 if ( $this->mTextRow
!== null ) {
1620 $row = $this->mTextRow
;
1621 $this->mTextRow
= null;
1626 // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
1627 // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
1628 $flags = $this->mQueryFlags
;
1629 $flags |
= DBAccessObjectUtils
::hasFlags( $flags, self
::READ_LATEST
)
1630 ? self
::READ_LATEST_IMMUTABLE
1633 list( $index, $options, $fallbackIndex, $fallbackOptions ) =
1634 DBAccessObjectUtils
::getDBOptions( $flags );
1637 // Text data is immutable; check replica DBs first.
1638 $row = wfGetDB( $index )->selectRow(
1640 [ 'old_text', 'old_flags' ],
1641 [ 'old_id' => $textId ],
1647 // Fallback to DB_MASTER in some cases if the row was not found
1648 if ( !$row && $fallbackIndex !== null ) {
1649 // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
1650 // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
1651 $row = wfGetDB( $fallbackIndex )->selectRow(
1653 [ 'old_text', 'old_flags' ],
1654 [ 'old_id' => $textId ],
1661 wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
1664 $text = self
::getRevisionText( $row );
1665 if ( $row && $text === false ) {
1666 wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
1669 return is_string( $text ) ?
$text : false;
1673 * Create a new null-revision for insertion into a page's
1674 * history. This will not re-save the text, but simply refer
1675 * to the text from the previous version.
1677 * Such revisions can for instance identify page rename
1678 * operations and other such meta-modifications.
1680 * @param IDatabase $dbw
1681 * @param int $pageId ID number of the page to read from
1682 * @param string $summary Revision's summary
1683 * @param bool $minor Whether the revision should be considered as minor
1684 * @param User|null $user User object to use or null for $wgUser
1685 * @return Revision|null Revision or null on error
1687 public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
1688 global $wgContentHandlerUseDB, $wgContLang;
1690 $fields = [ 'page_latest', 'page_namespace', 'page_title',
1691 'rev_text_id', 'rev_len', 'rev_sha1' ];
1693 if ( $wgContentHandlerUseDB ) {
1694 $fields[] = 'rev_content_model';
1695 $fields[] = 'rev_content_format';
1698 $current = $dbw->selectRow(
1699 [ 'page', 'revision' ],
1702 'page_id' => $pageId,
1703 'page_latest=rev_id',
1706 [ 'FOR UPDATE' ] // T51581
1715 // Truncate for whole multibyte characters
1716 $summary = $wgContLang->truncate( $summary, 255 );
1720 'user_text' => $user->getName(),
1721 'user' => $user->getId(),
1722 'comment' => $summary,
1723 'minor_edit' => $minor,
1724 'text_id' => $current->rev_text_id
,
1725 'parent_id' => $current->page_latest
,
1726 'len' => $current->rev_len
,
1727 'sha1' => $current->rev_sha1
1730 if ( $wgContentHandlerUseDB ) {
1731 $row['content_model'] = $current->rev_content_model
;
1732 $row['content_format'] = $current->rev_content_format
;
1735 $row['title'] = Title
::makeTitle( $current->page_namespace
, $current->page_title
);
1737 $revision = new Revision( $row );
1746 * Determine if the current user is allowed to view a particular
1747 * field of this revision, if it's marked as deleted.
1749 * @param int $field One of self::DELETED_TEXT,
1750 * self::DELETED_COMMENT,
1751 * self::DELETED_USER
1752 * @param User|null $user User object to check, or null to use $wgUser
1755 public function userCan( $field, User
$user = null ) {
1756 return self
::userCanBitfield( $this->getVisibility(), $field, $user );
1760 * Determine if the current user is allowed to view a particular
1761 * field of this revision, if it's marked as deleted. This is used
1762 * by various classes to avoid duplication.
1764 * @param int $bitfield Current field
1765 * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
1766 * self::DELETED_COMMENT = File::DELETED_COMMENT,
1767 * self::DELETED_USER = File::DELETED_USER
1768 * @param User|null $user User object to check, or null to use $wgUser
1769 * @param Title|null $title A Title object to check for per-page restrictions on,
1770 * instead of just plain userrights
1773 public static function userCanBitfield( $bitfield, $field, User
$user = null,
1776 if ( $bitfield & $field ) { // aspect is deleted
1777 if ( $user === null ) {
1781 if ( $bitfield & self
::DELETED_RESTRICTED
) {
1782 $permissions = [ 'suppressrevision', 'viewsuppressed' ];
1783 } elseif ( $field & self
::DELETED_TEXT
) {
1784 $permissions = [ 'deletedtext' ];
1786 $permissions = [ 'deletedhistory' ];
1788 $permissionlist = implode( ', ', $permissions );
1789 if ( $title === null ) {
1790 wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
1791 return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
1793 $text = $title->getPrefixedText();
1794 wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
1795 foreach ( $permissions as $perm ) {
1796 if ( $title->userCan( $perm, $user ) ) {
1808 * Get rev_timestamp from rev_id, without loading the rest of the row
1810 * @param Title $title
1813 * @return string|bool False if not found
1815 static function getTimestampFromId( $title, $id, $flags = 0 ) {
1816 $db = ( $flags & self
::READ_LATEST
)
1817 ?
wfGetDB( DB_MASTER
)
1818 : wfGetDB( DB_REPLICA
);
1819 // Casting fix for databases that can't take '' for rev_id
1823 $conds = [ 'rev_id' => $id ];
1824 $conds['rev_page'] = $title->getArticleID();
1825 $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__
);
1827 return ( $timestamp !== false ) ?
wfTimestamp( TS_MW
, $timestamp ) : false;
1831 * Get count of revisions per page...not very efficient
1833 * @param IDatabase $db
1834 * @param int $id Page id
1837 static function countByPageId( $db, $id ) {
1838 $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
1839 [ 'rev_page' => $id ], __METHOD__
);
1841 return $row->revCount
;
1847 * Get count of revisions per page...not very efficient
1849 * @param IDatabase $db
1850 * @param Title $title
1853 static function countByTitle( $db, $title ) {
1854 $id = $title->getArticleID();
1856 return self
::countByPageId( $db, $id );
1862 * Check if no edits were made by other users since
1863 * the time a user started editing the page. Limit to
1864 * 50 revisions for the sake of performance.
1867 * @deprecated since 1.24
1869 * @param IDatabase|int $db The Database to perform the check on. May be given as a
1870 * Database object or a database identifier usable with wfGetDB.
1871 * @param int $pageId The ID of the page in question
1872 * @param int $userId The ID of the user in question
1873 * @param string $since Look at edits since this time
1875 * @return bool True if the given user was the only one to edit since the given timestamp
1877 public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
1882 if ( is_int( $db ) ) {
1883 $db = wfGetDB( $db );
1886 $res = $db->select( 'revision',
1889 'rev_page' => $pageId,
1890 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
1893 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
1894 foreach ( $res as $row ) {
1895 if ( $row->rev_user
!= $userId ) {
1903 * Load a revision based on a known page ID and current revision ID from the DB
1905 * This method allows for the use of caching, though accessing anything that normally
1906 * requires permission checks (aside from the text) will trigger a small DB lookup.
1907 * The title will also be lazy loaded, though setTitle() can be used to preload it.
1909 * @param IDatabase $db
1910 * @param int $pageId Page ID
1911 * @param int $revId Known current revision of this page
1912 * @return Revision|bool Returns false if missing
1915 public static function newKnownCurrent( IDatabase
$db, $pageId, $revId ) {
1916 $cache = MediaWikiServices
::getInstance()->getMainWANObjectCache();
1917 return $cache->getWithSetCallback(
1918 // Page/rev IDs passed in from DB to reflect history merges
1919 $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ),
1921 function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
1922 $setOpts +
= Database
::getCacheSetOptions( $db );
1924 $rev = Revision
::loadFromPageId( $db, $pageId, $revId );
1925 // Reflect revision deletion and user renames
1927 $rev->mTitle
= null; // mutable; lazy-load
1928 $rev->mRefreshMutableFields
= true;
1931 return $rev ?
: false; // don't cache negatives
1937 * For cached revisions, make sure the user name and rev_deleted is up-to-date
1939 private function loadMutableFields() {
1940 if ( !$this->mRefreshMutableFields
) {
1941 return; // not needed
1944 $this->mRefreshMutableFields
= false;
1945 $dbr = wfGetLB( $this->mWiki
)->getConnectionRef( DB_REPLICA
, [], $this->mWiki
);
1946 $row = $dbr->selectRow(
1947 [ 'revision', 'user' ],
1948 [ 'rev_deleted', 'user_name' ],
1949 [ 'rev_id' => $this->mId
, 'user_id = rev_user' ],
1952 if ( $row ) { // update values
1953 $this->mDeleted
= (int)$row->rev_deleted
;
1954 $this->mUserText
= $row->user_name
;