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
26 class Revision
implements IDBAccessObject
{
30 protected $mOrigUserText;
32 protected $mMinorEdit;
33 protected $mTimestamp;
44 // Revision deletion constants
45 const DELETED_TEXT
= 1;
46 const DELETED_COMMENT
= 2;
47 const DELETED_USER
= 4;
48 const DELETED_RESTRICTED
= 8;
49 const SUPPRESSED_USER
= 12; // convenience
51 // Audience options for accessors
53 const FOR_THIS_USER
= 2;
57 * Load a page revision from a given revision ID number.
58 * Returns null if no such revision can be found.
61 * Revision::READ_LATEST : Select the data from the master
62 * Revision::READ_LOCKING : Select & lock the data from the master
65 * @param $flags Integer (optional)
66 * @return Revision or null
68 public static function newFromId( $id, $flags = 0 ) {
69 return self
::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
73 * Load either the current, or a specified, revision
74 * that's attached to a given title. If not attached
75 * to that title, will return null.
78 * Revision::READ_LATEST : Select the data from the master
79 * Revision::READ_LOCKING : Select & lock the data from the master
82 * @param $id Integer (optional)
83 * @param $flags Integer Bitfield (optional)
84 * @return Revision or null
86 public static function newFromTitle( $title, $id = 0, $flags = null ) {
88 'page_namespace' => $title->getNamespace(),
89 'page_title' => $title->getDBkey()
92 // Use the specified ID
93 $conds['rev_id'] = $id;
95 // Use a join to get the latest revision
96 $conds[] = 'rev_id=page_latest';
97 // Callers assume this will be up-to-date
98 $flags = is_int( $flags ) ?
$flags : self
::READ_LATEST
; // b/c
100 return self
::newFromConds( $conds, (int)$flags );
104 * Load either the current, or a specified, revision
105 * that's attached to a given page ID.
106 * Returns null if no such revision can be found.
109 * Revision::READ_LATEST : Select the data from the master
110 * Revision::READ_LOCKING : Select & lock the data from the master
112 * @param $revId Integer
113 * @param $pageId Integer (optional)
114 * @param $flags Integer Bitfield (optional)
115 * @return Revision or null
117 public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
118 $conds = array( 'page_id' => $pageId );
120 $conds['rev_id'] = $revId;
122 // Use a join to get the latest revision
123 $conds[] = 'rev_id = page_latest';
125 return self
::newFromConds( $conds, (int)$flags );
129 * Make a fake revision object from an archive table row. This is queried
130 * for permissions or even inserted (as in Special:Undelete)
131 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
134 * @param $overrides array
138 public static function newFromArchiveRow( $row, $overrides = array() ) {
139 $attribs = $overrides +
array(
140 'page' => isset( $row->ar_page_id
) ?
$row->ar_page_id
: null,
141 'id' => isset( $row->ar_rev_id
) ?
$row->ar_rev_id
: null,
142 'comment' => $row->ar_comment
,
143 'user' => $row->ar_user
,
144 'user_text' => $row->ar_user_text
,
145 'timestamp' => $row->ar_timestamp
,
146 'minor_edit' => $row->ar_minor_edit
,
147 'text_id' => isset( $row->ar_text_id
) ?
$row->ar_text_id
: null,
148 'deleted' => $row->ar_deleted
,
149 'len' => $row->ar_len
,
150 'sha1' => isset( $row->ar_sha1
) ?
$row->ar_sha1
: null,
152 if ( isset( $row->ar_text
) && !$row->ar_text_id
) {
153 // Pre-1.5 ar_text row
154 $attribs['text'] = self
::getRevisionText( $row, 'ar_' );
155 if ( $attribs['text'] === false ) {
156 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
159 return new self( $attribs );
168 public static function newFromRow( $row ) {
169 return new self( $row );
173 * Load a page revision from a given revision ID number.
174 * Returns null if no such revision can be found.
176 * @param $db DatabaseBase
178 * @return Revision or null
180 public static function loadFromId( $db, $id ) {
181 return self
::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
185 * Load either the current, or a specified, revision
186 * that's attached to a given page. If not attached
187 * to that page, will return null.
189 * @param $db DatabaseBase
190 * @param $pageid Integer
192 * @return Revision or null
194 public static function loadFromPageId( $db, $pageid, $id = 0 ) {
195 $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
197 $conds['rev_id'] = intval( $id );
199 $conds[] = 'rev_id=page_latest';
201 return self
::loadFromConds( $db, $conds );
205 * Load either the current, or a specified, revision
206 * that's attached to a given page. If not attached
207 * to that page, will return null.
209 * @param $db DatabaseBase
210 * @param $title Title
212 * @return Revision or null
214 public static function loadFromTitle( $db, $title, $id = 0 ) {
216 $matchId = intval( $id );
218 $matchId = 'page_latest';
220 return self
::loadFromConds( $db,
221 array( "rev_id=$matchId",
222 'page_namespace' => $title->getNamespace(),
223 'page_title' => $title->getDBkey() )
228 * Load the revision for the given title with the given timestamp.
229 * WARNING: Timestamps may in some circumstances not be unique,
230 * so this isn't the best key to use.
232 * @param $db DatabaseBase
233 * @param $title Title
234 * @param $timestamp String
235 * @return Revision or null
237 public static function loadFromTimestamp( $db, $title, $timestamp ) {
238 return self
::loadFromConds( $db,
239 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
240 'page_namespace' => $title->getNamespace(),
241 'page_title' => $title->getDBkey() )
246 * Given a set of conditions, fetch a revision.
248 * @param $conditions Array
249 * @param $flags integer (optional)
250 * @return Revision or null
252 private static function newFromConds( $conditions, $flags = 0 ) {
253 $db = wfGetDB( ( $flags & self
::READ_LATEST
) ? DB_MASTER
: DB_SLAVE
);
254 $rev = self
::loadFromConds( $db, $conditions, $flags );
255 if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
256 if ( !( $flags & self
::READ_LATEST
) ) {
257 $dbw = wfGetDB( DB_MASTER
);
258 $rev = self
::loadFromConds( $dbw, $conditions, $flags );
265 * Given a set of conditions, fetch a revision from
266 * the given database connection.
268 * @param $db DatabaseBase
269 * @param $conditions Array
270 * @param $flags integer (optional)
271 * @return Revision or null
273 private static function loadFromConds( $db, $conditions, $flags = 0 ) {
274 $res = self
::fetchFromConds( $db, $conditions, $flags );
276 $row = $res->fetchObject();
278 $ret = new Revision( $row );
287 * Return a wrapper for a series of database rows to
288 * fetch all of a given page's revisions in turn.
289 * Each row can be fed to the constructor to get objects.
291 * @param $title Title
292 * @return ResultWrapper
294 public static function fetchRevision( $title ) {
295 return self
::fetchFromConds(
297 array( 'rev_id=page_latest',
298 'page_namespace' => $title->getNamespace(),
299 'page_title' => $title->getDBkey() )
304 * Given a set of conditions, return a ResultWrapper
305 * which will return matching database rows with the
306 * fields necessary to build Revision objects.
308 * @param $db DatabaseBase
309 * @param $conditions Array
310 * @param $flags integer (optional)
311 * @return ResultWrapper
313 private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
314 $fields = array_merge(
315 self
::selectFields(),
316 self
::selectPageFields(),
317 self
::selectUserFields()
319 $options = array( 'LIMIT' => 1 );
320 if ( ( $flags & self
::READ_LOCKING
) == self
::READ_LOCKING
) {
321 $options[] = 'FOR UPDATE';
324 array( 'revision', 'page', 'user' ),
329 array( 'page' => self
::pageJoinCond(), 'user' => self
::userJoinCond() )
334 * Return the value of a select() JOIN conds array for the user table.
335 * This will get user table rows for logged-in users.
339 public static function userJoinCond() {
340 return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
344 * Return the value of a select() page conds array for the paeg table.
345 * This will assure that the revision(s) are not orphaned from live pages.
349 public static function pageJoinCond() {
350 return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
354 * Return the list of revision fields that should be selected to create
358 public static function selectFields() {
376 * Return the list of text fields that should be selected to read the
380 public static function selectTextFields() {
388 * Return the list of page fields that should be selected from page table
391 public static function selectPageFields() {
403 * Return the list of user fields that should be selected from user table
406 public static function selectUserFields() {
407 return array( 'user_name' );
411 * Do a batched query to get the parent revision lengths
412 * @param $db DatabaseBase
413 * @param $revIds Array
416 public static function getParentLengths( $db, array $revIds ) {
419 return $revLens; // empty
421 wfProfileIn( __METHOD__
);
422 $res = $db->select( 'revision',
423 array( 'rev_id', 'rev_len' ),
424 array( 'rev_id' => $revIds ),
426 foreach ( $res as $row ) {
427 $revLens[$row->rev_id
] = $row->rev_len
;
429 wfProfileOut( __METHOD__
);
436 * @param $row Mixed: either a database row or an array
439 function __construct( $row ) {
440 if( is_object( $row ) ) {
441 $this->mId
= intval( $row->rev_id
);
442 $this->mPage
= intval( $row->rev_page
);
443 $this->mTextId
= intval( $row->rev_text_id
);
444 $this->mComment
= $row->rev_comment
;
445 $this->mUser
= intval( $row->rev_user
);
446 $this->mMinorEdit
= intval( $row->rev_minor_edit
);
447 $this->mTimestamp
= $row->rev_timestamp
;
448 $this->mDeleted
= intval( $row->rev_deleted
);
450 if( !isset( $row->rev_parent_id
) ) {
451 $this->mParentId
= is_null( $row->rev_parent_id
) ?
null : 0;
453 $this->mParentId
= intval( $row->rev_parent_id
);
456 if( !isset( $row->rev_len
) ||
is_null( $row->rev_len
) ) {
459 $this->mSize
= intval( $row->rev_len
);
462 if ( !isset( $row->rev_sha1
) ) {
465 $this->mSha1
= $row->rev_sha1
;
468 if( isset( $row->page_latest
) ) {
469 $this->mCurrent
= ( $row->rev_id
== $row->page_latest
);
470 $this->mTitle
= Title
::newFromRow( $row );
472 $this->mCurrent
= false;
473 $this->mTitle
= null;
476 // Lazy extraction...
478 if( isset( $row->old_text
) ) {
479 $this->mTextRow
= $row;
481 // 'text' table row entry will be lazy-loaded
482 $this->mTextRow
= null;
485 // Use user_name for users and rev_user_text for IPs...
486 $this->mUserText
= null; // lazy load if left null
487 if ( $this->mUser
== 0 ) {
488 $this->mUserText
= $row->rev_user_text
; // IP user
489 } elseif ( isset( $row->user_name
) ) {
490 $this->mUserText
= $row->user_name
; // logged-in user
492 $this->mOrigUserText
= $row->rev_user_text
;
493 } elseif( is_array( $row ) ) {
494 // Build a new revision to be saved...
495 global $wgUser; // ugh
497 $this->mId
= isset( $row['id'] ) ?
intval( $row['id'] ) : null;
498 $this->mPage
= isset( $row['page'] ) ?
intval( $row['page'] ) : null;
499 $this->mTextId
= isset( $row['text_id'] ) ?
intval( $row['text_id'] ) : null;
500 $this->mUserText
= isset( $row['user_text'] ) ?
strval( $row['user_text'] ) : $wgUser->getName();
501 $this->mUser
= isset( $row['user'] ) ?
intval( $row['user'] ) : $wgUser->getId();
502 $this->mMinorEdit
= isset( $row['minor_edit'] ) ?
intval( $row['minor_edit'] ) : 0;
503 $this->mTimestamp
= isset( $row['timestamp'] ) ?
strval( $row['timestamp'] ) : wfTimestampNow();
504 $this->mDeleted
= isset( $row['deleted'] ) ?
intval( $row['deleted'] ) : 0;
505 $this->mSize
= isset( $row['len'] ) ?
intval( $row['len'] ) : null;
506 $this->mParentId
= isset( $row['parent_id'] ) ?
intval( $row['parent_id'] ) : null;
507 $this->mSha1
= isset( $row['sha1'] ) ?
strval( $row['sha1'] ) : null;
509 // Enforce spacing trimming on supplied text
510 $this->mComment
= isset( $row['comment'] ) ?
trim( strval( $row['comment'] ) ) : null;
511 $this->mText
= isset( $row['text'] ) ?
rtrim( strval( $row['text'] ) ) : null;
512 $this->mTextRow
= null;
514 $this->mTitle
= null; # Load on demand if needed
515 $this->mCurrent
= false;
516 # If we still have no length, see it we have the text to figure it out
517 if ( !$this->mSize
) {
518 $this->mSize
= is_null( $this->mText
) ?
null : strlen( $this->mText
);
521 if ( $this->mSha1
=== null ) {
522 $this->mSha1
= is_null( $this->mText
) ?
null : self
::base36Sha1( $this->mText
);
525 throw new MWException( 'Revision constructor passed invalid row format.' );
527 $this->mUnpatrolled
= null;
533 * @return Integer|null
535 public function getId() {
540 * Set the revision ID
545 public function setId( $id ) {
552 * @return Integer|null
554 public function getTextId() {
555 return $this->mTextId
;
559 * Get parent revision ID (the original previous page revision)
561 * @return Integer|null
563 public function getParentId() {
564 return $this->mParentId
;
568 * Returns the length of the text in this revision, or null if unknown.
570 * @return Integer|null
572 public function getSize() {
577 * Returns the base36 sha1 of the text in this revision, or null if unknown.
579 * @return String|null
581 public function getSha1() {
586 * Returns the title of the page associated with this entry or null.
588 * Will do a query, when title is not set and id is given.
592 public function getTitle() {
593 if( isset( $this->mTitle
) ) {
594 return $this->mTitle
;
596 if( !is_null( $this->mId
) ) { //rev_id is defined as NOT NULL
597 $dbr = wfGetDB( DB_SLAVE
);
598 $row = $dbr->selectRow(
599 array( 'page', 'revision' ),
600 self
::selectPageFields(),
601 array( 'page_id=rev_page',
602 'rev_id' => $this->mId
),
605 $this->mTitle
= Title
::newFromRow( $row );
608 return $this->mTitle
;
612 * Set the title of the revision
614 * @param $title Title
616 public function setTitle( $title ) {
617 $this->mTitle
= $title;
623 * @return Integer|null
625 public function getPage() {
630 * Fetch revision's user id if it's available to the specified audience.
631 * If the specified audience does not have access to it, zero will be
634 * @param $audience Integer: one of:
635 * Revision::FOR_PUBLIC to be displayed to all users
636 * Revision::FOR_THIS_USER to be displayed to the given user
637 * Revision::RAW get the ID regardless of permissions
638 * @param $user User object to check for, only if FOR_THIS_USER is passed
639 * to the $audience parameter
642 public function getUser( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
643 if( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_USER
) ) {
645 } elseif( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_USER
, $user ) ) {
653 * Fetch revision's user id without regard for the current user's permissions
657 public function getRawUser() {
662 * Fetch revision's username if it's available to the specified audience.
663 * If the specified audience does not have access to the username, an
664 * empty string will be returned.
666 * @param $audience Integer: one of:
667 * Revision::FOR_PUBLIC to be displayed to all users
668 * Revision::FOR_THIS_USER to be displayed to the given user
669 * Revision::RAW get the text regardless of permissions
670 * @param $user User object to check for, only if FOR_THIS_USER is passed
671 * to the $audience parameter
674 public function getUserText( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
675 if( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_USER
) ) {
677 } elseif( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_USER
, $user ) ) {
680 return $this->getRawUserText();
685 * Fetch revision's username without regard for view restrictions
689 public function getRawUserText() {
690 if ( $this->mUserText
=== null ) {
691 $this->mUserText
= User
::whoIs( $this->mUser
); // load on demand
692 if ( $this->mUserText
=== false ) {
693 # This shouldn't happen, but it can if the wiki was recovered
694 # via importing revs and there is no user table entry yet.
695 $this->mUserText
= $this->mOrigUserText
;
698 return $this->mUserText
;
702 * Fetch revision comment if it's available to the specified audience.
703 * If the specified audience does not have access to the comment, an
704 * empty string will be returned.
706 * @param $audience Integer: one of:
707 * Revision::FOR_PUBLIC to be displayed to all users
708 * Revision::FOR_THIS_USER to be displayed to the given user
709 * Revision::RAW get the text regardless of permissions
710 * @param $user User object to check for, only if FOR_THIS_USER is passed
711 * to the $audience parameter
714 function getComment( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
715 if( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_COMMENT
) ) {
717 } elseif( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_COMMENT
, $user ) ) {
720 return $this->mComment
;
725 * Fetch revision comment without regard for the current user's permissions
729 public function getRawComment() {
730 return $this->mComment
;
736 public function isMinor() {
737 return (bool)$this->mMinorEdit
;
741 * @return Integer rcid of the unpatrolled row, zero if there isn't one
743 public function isUnpatrolled() {
744 if( $this->mUnpatrolled
!== null ) {
745 return $this->mUnpatrolled
;
747 $dbr = wfGetDB( DB_SLAVE
);
748 $this->mUnpatrolled
= $dbr->selectField( 'recentchanges',
750 array( // Add redundant user,timestamp condition so we can use the existing index
751 'rc_user_text' => $this->getRawUserText(),
752 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
753 'rc_this_oldid' => $this->getId(),
758 return (int)$this->mUnpatrolled
;
762 * @param $field int one of DELETED_* bitfield constants
766 public function isDeleted( $field ) {
767 return ( $this->mDeleted
& $field ) == $field;
771 * Get the deletion bitfield of the revision
775 public function getVisibility() {
776 return (int)$this->mDeleted
;
780 * Fetch revision text if it's available to the specified audience.
781 * If the specified audience does not have the ability to view this
782 * revision, an empty string will be returned.
784 * @param $audience Integer: one of:
785 * Revision::FOR_PUBLIC to be displayed to all users
786 * Revision::FOR_THIS_USER to be displayed to the given user
787 * Revision::RAW get the text regardless of permissions
788 * @param $user User object to check for, only if FOR_THIS_USER is passed
789 * to the $audience parameter
792 public function getText( $audience = self
::FOR_PUBLIC
, User
$user = null ) {
793 if( $audience == self
::FOR_PUBLIC
&& $this->isDeleted( self
::DELETED_TEXT
) ) {
795 } elseif( $audience == self
::FOR_THIS_USER
&& !$this->userCan( self
::DELETED_TEXT
, $user ) ) {
798 return $this->getRawText();
803 * Alias for getText(Revision::FOR_THIS_USER)
805 * @deprecated since 1.17
808 public function revText() {
809 wfDeprecated( __METHOD__
, '1.17' );
810 return $this->getText( self
::FOR_THIS_USER
);
814 * Fetch revision text without regard for view restrictions
818 public function getRawText() {
819 if( is_null( $this->mText
) ) {
820 // Revision text is immutable. Load on demand:
821 $this->mText
= $this->loadText();
829 public function getTimestamp() {
830 return wfTimestamp( TS_MW
, $this->mTimestamp
);
836 public function isCurrent() {
837 return $this->mCurrent
;
841 * Get previous revision for this title
843 * @return Revision or null
845 public function getPrevious() {
846 if( $this->getTitle() ) {
847 $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
849 return self
::newFromTitle( $this->getTitle(), $prev );
856 * Get next revision for this title
858 * @return Revision or null
860 public function getNext() {
861 if( $this->getTitle() ) {
862 $next = $this->getTitle()->getNextRevisionID( $this->getId() );
864 return self
::newFromTitle( $this->getTitle(), $next );
871 * Get previous revision Id for this page_id
872 * This is used to populate rev_parent_id on save
874 * @param $db DatabaseBase
877 private function getPreviousRevisionId( $db ) {
878 if( is_null( $this->mPage
) ) {
881 # Use page_latest if ID is not given
883 $prevId = $db->selectField( 'page', 'page_latest',
884 array( 'page_id' => $this->mPage
),
887 $prevId = $db->selectField( 'revision', 'rev_id',
888 array( 'rev_page' => $this->mPage
, 'rev_id < ' . $this->mId
),
890 array( 'ORDER BY' => 'rev_id DESC' ) );
892 return intval( $prevId );
896 * Get revision text associated with an old or archive row
897 * $row is usually an object from wfFetchRow(), both the flags and the text
898 * field must be included
900 * @param $row Object: the text data
901 * @param $prefix String: table prefix (default 'old_')
902 * @return String: text the text requested or false on failure
904 public static function getRevisionText( $row, $prefix = 'old_' ) {
905 wfProfileIn( __METHOD__
);
908 $textField = $prefix . 'text';
909 $flagsField = $prefix . 'flags';
911 if( isset( $row->$flagsField ) ) {
912 $flags = explode( ',', $row->$flagsField );
917 if( isset( $row->$textField ) ) {
918 $text = $row->$textField;
920 wfProfileOut( __METHOD__
);
924 # Use external methods for external objects, text in table is URL-only then
925 if ( in_array( 'external', $flags ) ) {
927 $parts = explode( '://', $url, 2 );
928 if( count( $parts ) == 1 ||
$parts[1] == '' ) {
929 wfProfileOut( __METHOD__
);
932 $text = ExternalStore
::fetchFromURL( $url );
935 // If the text was fetched without an error, convert it
936 if ( $text !== false ) {
937 if( in_array( 'gzip', $flags ) ) {
938 # Deal with optional compression of archived pages.
939 # This can be done periodically via maintenance/compressOld.php, and
940 # as pages are saved if $wgCompressRevisions is set.
941 $text = gzinflate( $text );
944 if( in_array( 'object', $flags ) ) {
945 # Generic compressed storage
946 $obj = unserialize( $text );
947 if ( !is_object( $obj ) ) {
949 wfProfileOut( __METHOD__
);
952 $text = $obj->getText();
955 global $wgLegacyEncoding;
956 if( $text !== false && $wgLegacyEncoding
957 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
959 # Old revisions kept around in a legacy encoding?
960 # Upconvert on demand.
961 # ("utf8" checked for compatibility with some broken
962 # conversion scripts 2008-12-30)
964 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
967 wfProfileOut( __METHOD__
);
972 * If $wgCompressRevisions is enabled, we will compress data.
973 * The input string is modified in place.
974 * Return value is the flags field: contains 'gzip' if the
975 * data is compressed, and 'utf-8' if we're saving in UTF-8
978 * @param $text Mixed: reference to a text
981 public static function compressRevisionText( &$text ) {
982 global $wgCompressRevisions;
985 # Revisions not marked this way will be converted
986 # on load if $wgLegacyCharset is set in the future.
989 if( $wgCompressRevisions ) {
990 if( function_exists( 'gzdeflate' ) ) {
991 $text = gzdeflate( $text );
994 wfDebug( __METHOD__
. " -- no zlib support, not compressing\n" );
997 return implode( ',', $flags );
1001 * Insert a new revision into the database, returning the new revision ID
1002 * number on success and dies horribly on failure.
1004 * @param $dbw DatabaseBase: (master connection)
1007 public function insertOn( $dbw ) {
1008 global $wgDefaultExternalStore;
1010 wfProfileIn( __METHOD__
);
1012 $data = $this->mText
;
1013 $flags = self
::compressRevisionText( $data );
1015 # Write to external storage if required
1016 if( $wgDefaultExternalStore ) {
1017 // Store and get the URL
1018 $data = ExternalStore
::insertToDefault( $data );
1020 throw new MWException( "Unable to store text to external storage" );
1025 $flags .= 'external';
1028 # Record the text (or external storage URL) to the text table
1029 if( !isset( $this->mTextId
) ) {
1030 $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1031 $dbw->insert( 'text',
1033 'old_id' => $old_id,
1034 'old_text' => $data,
1035 'old_flags' => $flags,
1038 $this->mTextId
= $dbw->insertId();
1041 if ( $this->mComment
=== null ) $this->mComment
= "";
1043 # Record the edit in revisions
1044 $rev_id = isset( $this->mId
)
1046 : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1047 $dbw->insert( 'revision',
1049 'rev_id' => $rev_id,
1050 'rev_page' => $this->mPage
,
1051 'rev_text_id' => $this->mTextId
,
1052 'rev_comment' => $this->mComment
,
1053 'rev_minor_edit' => $this->mMinorEdit ?
1 : 0,
1054 'rev_user' => $this->mUser
,
1055 'rev_user_text' => $this->mUserText
,
1056 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp
),
1057 'rev_deleted' => $this->mDeleted
,
1058 'rev_len' => $this->mSize
,
1059 'rev_parent_id' => is_null( $this->mParentId
)
1060 ?
$this->getPreviousRevisionId( $dbw )
1062 'rev_sha1' => is_null( $this->mSha1
)
1063 ? self
::base36Sha1( $this->mText
)
1068 $this->mId
= !is_null( $rev_id ) ?
$rev_id : $dbw->insertId();
1070 wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
1072 wfProfileOut( __METHOD__
);
1077 * Get the base 36 SHA-1 value for a string of text
1078 * @param $text String
1081 public static function base36Sha1( $text ) {
1082 return wfBaseConvert( sha1( $text ), 16, 36, 31 );
1086 * Lazy-load the revision's text.
1087 * Currently hardcoded to the 'text' table storage engine.
1091 protected function loadText() {
1092 wfProfileIn( __METHOD__
);
1094 // Caching may be beneficial for massive use of external storage
1095 global $wgRevisionCacheExpiry, $wgMemc;
1096 $textId = $this->getTextId();
1097 $key = wfMemcKey( 'revisiontext', 'textid', $textId );
1098 if( $wgRevisionCacheExpiry ) {
1099 $text = $wgMemc->get( $key );
1100 if( is_string( $text ) ) {
1101 wfDebug( __METHOD__
. ": got id $textId from cache\n" );
1102 wfProfileOut( __METHOD__
);
1107 // If we kept data for lazy extraction, use it now...
1108 if ( isset( $this->mTextRow
) ) {
1109 $row = $this->mTextRow
;
1110 $this->mTextRow
= null;
1116 // Text data is immutable; check slaves first.
1117 $dbr = wfGetDB( DB_SLAVE
);
1118 $row = $dbr->selectRow( 'text',
1119 array( 'old_text', 'old_flags' ),
1120 array( 'old_id' => $this->getTextId() ),
1124 if( !$row && wfGetLB()->getServerCount() > 1 ) {
1125 // Possible slave lag!
1126 $dbw = wfGetDB( DB_MASTER
);
1127 $row = $dbw->selectRow( 'text',
1128 array( 'old_text', 'old_flags' ),
1129 array( 'old_id' => $this->getTextId() ),
1133 $text = self
::getRevisionText( $row );
1135 # No negative caching -- negative hits on text rows may be due to corrupted slave servers
1136 if( $wgRevisionCacheExpiry && $text !== false ) {
1137 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
1140 wfProfileOut( __METHOD__
);
1146 * Create a new null-revision for insertion into a page's
1147 * history. This will not re-save the text, but simply refer
1148 * to the text from the previous version.
1150 * Such revisions can for instance identify page rename
1151 * operations and other such meta-modifications.
1153 * @param $dbw DatabaseBase
1154 * @param $pageId Integer: ID number of the page to read from
1155 * @param $summary String: revision's summary
1156 * @param $minor Boolean: whether the revision should be considered as minor
1157 * @return Revision|null on error
1159 public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
1160 wfProfileIn( __METHOD__
);
1162 $current = $dbw->selectRow(
1163 array( 'page', 'revision' ),
1164 array( 'page_latest', 'page_namespace', 'page_title',
1165 'rev_text_id', 'rev_len', 'rev_sha1' ),
1167 'page_id' => $pageId,
1168 'page_latest=rev_id',
1173 $revision = new Revision( array(
1175 'comment' => $summary,
1176 'minor_edit' => $minor,
1177 'text_id' => $current->rev_text_id
,
1178 'parent_id' => $current->page_latest
,
1179 'len' => $current->rev_len
,
1180 'sha1' => $current->rev_sha1
1182 $revision->setTitle( Title
::makeTitle( $current->page_namespace
, $current->page_title
) );
1187 wfProfileOut( __METHOD__
);
1192 * Determine if the current user is allowed to view a particular
1193 * field of this revision, if it's marked as deleted.
1195 * @param $field Integer:one of self::DELETED_TEXT,
1196 * self::DELETED_COMMENT,
1197 * self::DELETED_USER
1198 * @param $user User object to check, or null to use $wgUser
1201 public function userCan( $field, User
$user = null ) {
1202 return self
::userCanBitfield( $this->mDeleted
, $field, $user );
1206 * Determine if the current user is allowed to view a particular
1207 * field of this revision, if it's marked as deleted. This is used
1208 * by various classes to avoid duplication.
1210 * @param $bitfield Integer: current field
1211 * @param $field Integer: one of self::DELETED_TEXT = File::DELETED_FILE,
1212 * self::DELETED_COMMENT = File::DELETED_COMMENT,
1213 * self::DELETED_USER = File::DELETED_USER
1214 * @param $user User object to check, or null to use $wgUser
1217 public static function userCanBitfield( $bitfield, $field, User
$user = null ) {
1218 if( $bitfield & $field ) { // aspect is deleted
1219 if ( $bitfield & self
::DELETED_RESTRICTED
) {
1220 $permission = 'suppressrevision';
1221 } elseif ( $field & self
::DELETED_TEXT
) {
1222 $permission = 'deletedtext';
1224 $permission = 'deletedhistory';
1226 wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
1227 if ( $user === null ) {
1231 return $user->isAllowed( $permission );
1238 * Get rev_timestamp from rev_id, without loading the rest of the row
1240 * @param $title Title
1241 * @param $id Integer
1244 static function getTimestampFromId( $title, $id ) {
1245 $dbr = wfGetDB( DB_SLAVE
);
1246 // Casting fix for DB2
1250 $conds = array( 'rev_id' => $id );
1251 $conds['rev_page'] = $title->getArticleID();
1252 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__
);
1253 if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
1254 # Not in slave, try master
1255 $dbw = wfGetDB( DB_MASTER
);
1256 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__
);
1258 return wfTimestamp( TS_MW
, $timestamp );
1262 * Get count of revisions per page...not very efficient
1264 * @param $db DatabaseBase
1265 * @param $id Integer: page id
1268 static function countByPageId( $db, $id ) {
1269 $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
1270 array( 'rev_page' => $id ), __METHOD__
);
1272 return $row->revCount
;
1278 * Get count of revisions per page...not very efficient
1280 * @param $db DatabaseBase
1281 * @param $title Title
1284 static function countByTitle( $db, $title ) {
1285 $id = $title->getArticleID();
1287 return self
::countByPageId( $db, $id );
1293 * Check if no edits were made by other users since
1294 * the time a user started editing the page. Limit to
1295 * 50 revisions for the sake of performance.
1299 * @param DatabaseBase|int $db the Database to perform the check on. May be given as a Database object or
1300 * a database identifier usable with wfGetDB.
1301 * @param int $pageId the ID of the page in question
1302 * @param int $userId the ID of the user in question
1303 * @param string $since look at edits since this time
1305 * @return bool True if the given user was the only one to edit since the given timestamp
1307 public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
1308 if ( !$userId ) return false;
1310 if ( is_int( $db ) ) {
1311 $db = wfGetDB( $db );
1314 $res = $db->select( 'revision',
1317 'rev_page' => $pageId,
1318 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
1321 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
1322 foreach ( $res as $row ) {
1323 if ( $row->rev_user
!= $userId ) {