8 require_once( 'Database.php' );
15 const DELETED_TEXT
= 1;
16 const DELETED_COMMENT
= 2;
17 const DELETED_USER
= 4;
18 const DELETED_RESTRICTED
= 8;
21 * Load a page revision from a given revision ID number.
22 * Returns null if no such revision can be found.
28 public static function newFromId( $id ) {
29 return Revision
::newFromConds(
30 array( 'page_id=rev_page',
31 'rev_id' => intval( $id ) ) );
35 * Load either the current, or a specified, revision
36 * that's attached to a given title. If not attached
37 * to that title, will return null.
45 public static function newFromTitle( &$title, $id = 0 ) {
47 $matchId = intval( $id );
49 $matchId = 'page_latest';
51 return Revision
::newFromConds(
52 array( "rev_id=$matchId",
54 'page_namespace' => $title->getNamespace(),
55 'page_title' => $title->getDbkey() ) );
59 * Load either the current, or a specified, revision
60 * that's attached to a given page. If not attached
61 * to that page, will return null.
69 public static function loadFromPageId( &$db, $pageid, $id = 0 ) {
70 $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
72 $conds['rev_id']=intval($id);
74 $conds[]='rev_id=page_latest';
76 return Revision
::loadFromConds( $db, $conds );
80 * Load either the current, or a specified, revision
81 * that's attached to a given page. If not attached
82 * to that page, will return null.
90 function loadFromTitle( &$db, $title, $id = 0 ) {
92 $matchId = intval( $id );
94 $matchId = 'page_latest';
96 return Revision
::loadFromConds(
98 array( "rev_id=$matchId",
100 'page_namespace' => $title->getNamespace(),
101 'page_title' => $title->getDbkey() ) );
105 * Load the revision for the given title with the given timestamp.
106 * WARNING: Timestamps may in some circumstances not be unique,
107 * so this isn't the best key to use.
109 * @param Database $db
110 * @param Title $title
111 * @param string $timestamp
116 function loadFromTimestamp( &$db, &$title, $timestamp ) {
117 return Revision
::loadFromConds(
119 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
121 'page_namespace' => $title->getNamespace(),
122 'page_title' => $title->getDbkey() ) );
126 * Given a set of conditions, fetch a revision.
128 * @param array $conditions
133 private static function newFromConds( $conditions ) {
134 $db =& wfGetDB( DB_SLAVE
);
135 $row = Revision
::loadFromConds( $db, $conditions );
136 if( is_null( $row ) ) {
137 $dbw =& wfGetDB( DB_MASTER
);
138 $row = Revision
::loadFromConds( $dbw, $conditions );
144 * Given a set of conditions, fetch a revision from
145 * the given database connection.
147 * @param Database $db
148 * @param array $conditions
153 private static function loadFromConds( &$db, $conditions ) {
154 $res = Revision
::fetchFromConds( $db, $conditions );
156 $row = $res->fetchObject();
159 $ret = new Revision( $row );
168 * Return a wrapper for a series of database rows to
169 * fetch all of a given page's revisions in turn.
170 * Each row can be fed to the constructor to get objects.
172 * @param Title $title
173 * @return ResultWrapper
177 function fetchAllRevisions( &$title ) {
178 return Revision
::fetchFromConds(
180 array( 'page_namespace' => $title->getNamespace(),
181 'page_title' => $title->getDbkey(),
182 'page_id=rev_page' ) );
186 * Return a wrapper for a series of database rows to
187 * fetch all of a given page's revisions in turn.
188 * Each row can be fed to the constructor to get objects.
190 * @param Title $title
191 * @return ResultWrapper
195 public static function fetchRevision( &$title ) {
196 return Revision
::fetchFromConds(
198 array( 'rev_id=page_latest',
199 'page_namespace' => $title->getNamespace(),
200 'page_title' => $title->getDbkey(),
201 'page_id=rev_page' ) );
205 * Given a set of conditions, return a ResultWrapper
206 * which will return matching database rows with the
207 * fields necessary to build Revision objects.
209 * @param Database $db
210 * @param array $conditions
211 * @return ResultWrapper
215 private static function fetchFromConds( &$db, $conditions ) {
217 array( 'page', 'revision' ),
218 array( 'page_namespace',
231 'Revision::fetchRow',
232 array( 'LIMIT' => 1 ) );
233 $ret = $db->resultObject( $res );
241 function Revision( $row ) {
242 if( is_object( $row ) ) {
243 $this->mId
= intval( $row->rev_id
);
244 $this->mPage
= intval( $row->rev_page
);
245 $this->mTextId
= intval( $row->rev_text_id
);
246 $this->mComment
= $row->rev_comment
;
247 $this->mUserText
= $row->rev_user_text
;
248 $this->mUser
= intval( $row->rev_user
);
249 $this->mMinorEdit
= intval( $row->rev_minor_edit
);
250 $this->mTimestamp
= $row->rev_timestamp
;
251 $this->mDeleted
= intval( $row->rev_deleted
);
253 if( isset( $row->page_latest
) ) {
254 $this->mCurrent
= ( $row->rev_id
== $row->page_latest
);
255 $this->mTitle
= Title
::makeTitle( $row->page_namespace
,
258 $this->mCurrent
= false;
259 $this->mTitle
= null;
262 // Lazy extraction...
264 if( isset( $row->old_text
) ) {
265 $this->mTextRow
= $row;
267 // 'text' table row entry will be lazy-loaded
268 $this->mTextRow
= null;
270 } elseif( is_array( $row ) ) {
271 // Build a new revision to be saved...
274 $this->mId
= isset( $row['id'] ) ?
intval( $row['id'] ) : null;
275 $this->mPage
= isset( $row['page'] ) ?
intval( $row['page'] ) : null;
276 $this->mTextId
= isset( $row['text_id'] ) ?
intval( $row['text_id'] ) : null;
277 $this->mUserText
= isset( $row['user_text'] ) ?
strval( $row['user_text'] ) : $wgUser->getName();
278 $this->mUser
= isset( $row['user'] ) ?
intval( $row['user'] ) : $wgUser->getId();
279 $this->mMinorEdit
= isset( $row['minor_edit'] ) ?
intval( $row['minor_edit'] ) : 0;
280 $this->mTimestamp
= isset( $row['timestamp'] ) ?
strval( $row['timestamp'] ) : wfTimestamp( TS_MW
);
281 $this->mDeleted
= isset( $row['deleted'] ) ?
intval( $row['deleted'] ) : 0;
283 // Enforce spacing trimming on supplied text
284 $this->mComment
= isset( $row['comment'] ) ?
trim( strval( $row['comment'] ) ) : null;
285 $this->mText
= isset( $row['text'] ) ?
rtrim( strval( $row['text'] ) ) : null;
287 $this->mTitle
= null; # Load on demand if needed
288 $this->mCurrent
= false;
290 throw new MWException( 'Revision constructor passed invalid row format.' );
308 function getTextId() {
309 return $this->mTextId
;
313 * Returns the title of the page associated with this entry.
316 function getTitle() {
317 if( isset( $this->mTitle
) ) {
318 return $this->mTitle
;
320 $dbr =& wfGetDB( DB_SLAVE
);
321 $row = $dbr->selectRow(
322 array( 'page', 'revision' ),
323 array( 'page_namespace', 'page_title' ),
324 array( 'page_id=rev_page',
325 'rev_id' => $this->mId
),
326 'Revision::getTitle' );
328 $this->mTitle
= Title
::makeTitle( $row->page_namespace
,
331 return $this->mTitle
;
335 * Set the title of the revision
336 * @param Title $title
338 function setTitle( $title ) {
339 $this->mTitle
= $title;
350 * Fetch revision's user id if it's available to all users
354 if( $this->isDeleted( self
::DELETED_USER
) ) {
362 * Fetch revision's user id without regard for the current user's permissions
365 function getRawUser() {
370 * Fetch revision's username if it's available to all users
373 function getUserText() {
374 if( $this->isDeleted( self
::DELETED_USER
) ) {
377 return $this->mUserText
;
382 * Fetch revision's username without regard for view restrictions
385 function getRawUserText() {
386 return $this->mUserText
;
390 * Fetch revision comment if it's available to all users
393 function getComment() {
394 if( $this->isDeleted( self
::DELETED_COMMENT
) ) {
397 return $this->mComment
;
402 * Fetch revision comment without regard for the current user's permissions
405 function getRawComment() {
406 return $this->mComment
;
413 return (bool)$this->mMinorEdit
;
417 * int $field one of DELETED_* bitfield constants
420 function isDeleted( $field ) {
421 return ($this->mDeleted
& $field) == $field;
425 * Fetch revision text if it's available to all users
429 if( $this->isDeleted( self
::DELETED_TEXT
) ) {
432 return $this->getRawText();
437 * Fetch revision text without regard for view restrictions
440 function getRawText() {
441 if( is_null( $this->mText
) ) {
442 // Revision text is immutable. Load on demand:
443 $this->mText
= $this->loadText();
451 function getTimestamp() {
452 return wfTimestamp(TS_MW
, $this->mTimestamp
);
458 function isCurrent() {
459 return $this->mCurrent
;
465 function getPrevious() {
466 $prev = $this->mTitle
->getPreviousRevisionID( $this->mId
);
468 return Revision
::newFromTitle( $this->mTitle
, $prev );
478 $next = $this->mTitle
->getNextRevisionID( $this->mId
);
480 return Revision
::newFromTitle( $this->mTitle
, $next );
488 * Get revision text associated with an old or archive row
489 * $row is usually an object from wfFetchRow(), both the flags and the text
490 * field must be included
492 * @param integer $row Id of a row
493 * @param string $prefix table prefix (default 'old_')
494 * @return string $text|false the text requested
496 function getRevisionText( $row, $prefix = 'old_' ) {
497 $fname = 'Revision::getRevisionText';
498 wfProfileIn( $fname );
501 $textField = $prefix . 'text';
502 $flagsField = $prefix . 'flags';
504 if( isset( $row->$flagsField ) ) {
505 $flags = explode( ',', $row->$flagsField );
510 if( isset( $row->$textField ) ) {
511 $text = $row->$textField;
513 wfProfileOut( $fname );
517 # Use external methods for external objects, text in table is URL-only then
518 if ( in_array( 'external', $flags ) ) {
520 @list
($proto,$path)=explode('://',$url,2);
522 wfProfileOut( $fname );
525 require_once('ExternalStore.php');
526 $text=ExternalStore
::fetchFromURL($url);
529 // If the text was fetched without an error, convert it
530 if ( $text !== false ) {
531 if( in_array( 'gzip', $flags ) ) {
532 # Deal with optional compression of archived pages.
533 # This can be done periodically via maintenance/compressOld.php, and
534 # as pages are saved if $wgCompressRevisions is set.
535 $text = gzinflate( $text );
538 if( in_array( 'object', $flags ) ) {
539 # Generic compressed storage
540 $obj = unserialize( $text );
541 if ( !is_object( $obj ) ) {
543 wfProfileOut( $fname );
546 $text = $obj->getText();
549 global $wgLegacyEncoding;
550 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
551 # Old revisions kept around in a legacy encoding?
552 # Upconvert on demand.
553 global $wgInputEncoding, $wgContLang;
554 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
557 wfProfileOut( $fname );
562 * If $wgCompressRevisions is enabled, we will compress data.
563 * The input string is modified in place.
564 * Return value is the flags field: contains 'gzip' if the
565 * data is compressed, and 'utf-8' if we're saving in UTF-8
569 * @param mixed $text reference to a text
572 function compressRevisionText( &$text ) {
573 global $wgCompressRevisions;
576 # Revisions not marked this way will be converted
577 # on load if $wgLegacyCharset is set in the future.
580 if( $wgCompressRevisions ) {
581 if( function_exists( 'gzdeflate' ) ) {
582 $text = gzdeflate( $text );
585 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
588 return implode( ',', $flags );
592 * Insert a new revision into the database, returning the new revision ID
593 * number on success and dies horribly on failure.
595 * @param Database $dbw
598 function insertOn( &$dbw ) {
599 global $wgDefaultExternalStore;
601 $fname = 'Revision::insertOn';
602 wfProfileIn( $fname );
604 $data = $this->mText
;
605 $flags = Revision
::compressRevisionText( $data );
607 # Write to external storage if required
608 if ( $wgDefaultExternalStore ) {
609 if ( is_array( $wgDefaultExternalStore ) ) {
610 // Distribute storage across multiple clusters
611 $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
613 $store = $wgDefaultExternalStore;
615 require_once('ExternalStore.php');
616 // Store and get the URL
617 $data = ExternalStore
::insert( $store, $data );
619 # This should only happen in the case of a configuration error, where the external store is not valid
620 throw new MWException( "Unable to store text to external storage $store" );
625 $flags .= 'external';
628 # Record the text (or external storage URL) to the text table
629 if( !isset( $this->mTextId
) ) {
630 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
631 $dbw->insert( 'text',
635 'old_flags' => $flags,
638 $this->mTextId
= $dbw->insertId();
641 # Record the edit in revisions
642 $rev_id = isset( $this->mId
)
644 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
645 $dbw->insert( 'revision',
648 'rev_page' => $this->mPage
,
649 'rev_text_id' => $this->mTextId
,
650 'rev_comment' => $this->mComment
,
651 'rev_minor_edit' => $this->mMinorEdit ?
1 : 0,
652 'rev_user' => $this->mUser
,
653 'rev_user_text' => $this->mUserText
,
654 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp
),
655 'rev_deleted' => $this->mDeleted
,
659 $this->mId
= !is_null($rev_id) ?
$rev_id : $dbw->insertId();
660 wfProfileOut( $fname );
665 * Lazy-load the revision's text.
666 * Currently hardcoded to the 'text' table storage engine.
671 function loadText() {
672 $fname = 'Revision::loadText';
673 wfProfileIn( $fname );
675 // Caching may be beneficial for massive use of external storage
676 global $wgRevisionCacheExpiry, $wgMemc, $wgDBname;
677 $key = "$wgDBname:revisiontext:textid:" . $this->getTextId();
678 if( $wgRevisionCacheExpiry ) {
679 $text = $wgMemc->get( $key );
680 if( is_string( $text ) ) {
681 wfProfileOut( $fname );
686 // If we kept data for lazy extraction, use it now...
687 $row = $this->mTextRow
;
688 $this->mTextRow
= null;
691 // Text data is immutable; check slaves first.
692 $dbr =& wfGetDB( DB_SLAVE
);
693 $row = $dbr->selectRow( 'text',
694 array( 'old_text', 'old_flags' ),
695 array( 'old_id' => $this->getTextId() ),
700 // Possible slave lag!
701 $dbw =& wfGetDB( DB_MASTER
);
702 $row = $dbw->selectRow( 'text',
703 array( 'old_text', 'old_flags' ),
704 array( 'old_id' => $this->getTextId() ),
708 $text = Revision
::getRevisionText( $row );
710 if( $wgRevisionCacheExpiry ) {
711 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
714 wfProfileOut( $fname );
720 * Create a new null-revision for insertion into a page's
721 * history. This will not re-save the text, but simply refer
722 * to the text from the previous version.
724 * Such revisions can for instance identify page rename
725 * operations and other such meta-modifications.
727 * @param Database $dbw
728 * @param int $pageId ID number of the page to read from
729 * @param string $summary
733 function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
734 $fname = 'Revision::newNullRevision';
735 wfProfileIn( $fname );
737 $current = $dbw->selectRow(
738 array( 'page', 'revision' ),
739 array( 'page_latest', 'rev_text_id' ),
741 'page_id' => $pageId,
742 'page_latest=rev_id',
747 $revision = new Revision( array(
749 'comment' => $summary,
750 'minor_edit' => $minor,
751 'text_id' => $current->rev_text_id
,
757 wfProfileOut( $fname );
762 * Determine if the current user is allowed to view a particular
763 * field of this revision, if it's marked as deleted.
764 * @param int $field one of self::DELETED_TEXT,
765 * self::DELETED_COMMENT,
769 function userCan( $field ) {
770 if( ( $this->mDeleted
& $field ) == $field ) {
772 $permission = ( $this->mDeleted
& self
::DELETED_RESTRICTED
) == self
::DELETED_RESTRICTED
775 wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
776 return $wgUser->isAllowed( $permission );
784 * Get rev_timestamp from rev_id, without loading the rest of the row
787 static function getTimestampFromID( $id ) {
788 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
789 array( 'rev_id' => $id ), __METHOD__
);
790 if ( $timestamp === false ) {
791 # Not in slave, try master
792 $dbw =& wfGetDB( DB_MASTER
);
793 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
794 array( 'rev_id' => $id ), __METHOD__
);
799 static function countByPageId( $db, $id ) {
800 $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
801 array( 'rev_page' => $id ), __METHOD__
);
803 return $row->revCount
;
808 static function countByTitle( $db, $title ) {
809 $id = $title->getArticleId();
811 return Revision
::countByPageId( $db, $id );
818 * Aliases for backwards compatibility with 1.6
820 define( 'MW_REV_DELETED_TEXT', Revision
::DELETED_TEXT
);
821 define( 'MW_REV_DELETED_COMMENT', Revision
::DELETED_COMMENT
);
822 define( 'MW_REV_DELETED_USER', Revision
::DELETED_USER
);
823 define( 'MW_REV_DELETED_RESTRICTED', Revision
::DELETED_RESTRICTED
);