Add recentchanges recent changes for pg upddates.
[mediawiki.git] / includes / Revision.php
blob75a81fe93e7a8bdc1ad9ee13f27a794cea9587b7
1 <?php
2 /**
3 * @package MediaWiki
4 * @todo document
5 */
7 /**
8 * @package MediaWiki
9 * @todo document
11 class Revision {
12 const DELETED_TEXT = 1;
13 const DELETED_COMMENT = 2;
14 const DELETED_USER = 4;
15 const DELETED_RESTRICTED = 8;
17 /**
18 * Load a page revision from a given revision ID number.
19 * Returns null if no such revision can be found.
21 * @param int $id
22 * @access public
23 * @static
25 public static function newFromId( $id ) {
26 return Revision::newFromConds(
27 array( 'page_id=rev_page',
28 'rev_id' => intval( $id ) ) );
31 /**
32 * Load either the current, or a specified, revision
33 * that's attached to a given title. If not attached
34 * to that title, will return null.
36 * @param Title $title
37 * @param int $id
38 * @return Revision
39 * @access public
40 * @static
42 public static function newFromTitle( &$title, $id = 0 ) {
43 if( $id ) {
44 $matchId = intval( $id );
45 } else {
46 $matchId = 'page_latest';
48 return Revision::newFromConds(
49 array( "rev_id=$matchId",
50 'page_id=rev_page',
51 'page_namespace' => $title->getNamespace(),
52 'page_title' => $title->getDbkey() ) );
55 /**
56 * Load a page revision from a given revision ID number.
57 * Returns null if no such revision can be found.
59 * @param Database $db
60 * @param int $id
61 * @access public
62 * @static
64 public static function loadFromId( &$db, $id ) {
65 return Revision::loadFromConds( $db,
66 array( 'page_id=rev_page',
67 'rev_id' => intval( $id ) ) );
70 /**
71 * Load either the current, or a specified, revision
72 * that's attached to a given page. If not attached
73 * to that page, will return null.
75 * @param Database $db
76 * @param int $pageid
77 * @param int $id
78 * @return Revision
79 * @access public
80 * @static
82 public static function loadFromPageId( &$db, $pageid, $id = 0 ) {
83 $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
84 if( $id ) {
85 $conds['rev_id']=intval($id);
86 } else {
87 $conds[]='rev_id=page_latest';
89 return Revision::loadFromConds( $db, $conds );
92 /**
93 * Load either the current, or a specified, revision
94 * that's attached to a given page. If not attached
95 * to that page, will return null.
97 * @param Database $db
98 * @param Title $title
99 * @param int $id
100 * @return Revision
101 * @access public
102 * @static
104 public static function loadFromTitle( &$db, $title, $id = 0 ) {
105 if( $id ) {
106 $matchId = intval( $id );
107 } else {
108 $matchId = 'page_latest';
110 return Revision::loadFromConds(
111 $db,
112 array( "rev_id=$matchId",
113 'page_id=rev_page',
114 'page_namespace' => $title->getNamespace(),
115 'page_title' => $title->getDbkey() ) );
119 * Load the revision for the given title with the given timestamp.
120 * WARNING: Timestamps may in some circumstances not be unique,
121 * so this isn't the best key to use.
123 * @param Database $db
124 * @param Title $title
125 * @param string $timestamp
126 * @return Revision
127 * @access public
128 * @static
130 public static function loadFromTimestamp( &$db, &$title, $timestamp ) {
131 return Revision::loadFromConds(
132 $db,
133 array( 'rev_timestamp' => $db->timestamp( $timestamp ),
134 'page_id=rev_page',
135 'page_namespace' => $title->getNamespace(),
136 'page_title' => $title->getDbkey() ) );
140 * Given a set of conditions, fetch a revision.
142 * @param array $conditions
143 * @return Revision
144 * @access private
145 * @static
147 private static function newFromConds( $conditions ) {
148 $db =& wfGetDB( DB_SLAVE );
149 $row = Revision::loadFromConds( $db, $conditions );
150 if( is_null( $row ) ) {
151 $dbw =& wfGetDB( DB_MASTER );
152 $row = Revision::loadFromConds( $dbw, $conditions );
154 return $row;
158 * Given a set of conditions, fetch a revision from
159 * the given database connection.
161 * @param Database $db
162 * @param array $conditions
163 * @return Revision
164 * @access private
165 * @static
167 private static function loadFromConds( &$db, $conditions ) {
168 $res = Revision::fetchFromConds( $db, $conditions );
169 if( $res ) {
170 $row = $res->fetchObject();
171 $res->free();
172 if( $row ) {
173 $ret = new Revision( $row );
174 return $ret;
177 $ret = null;
178 return $ret;
182 * Return a wrapper for a series of database rows to
183 * fetch all of a given page's revisions in turn.
184 * Each row can be fed to the constructor to get objects.
186 * @param Title $title
187 * @return ResultWrapper
188 * @access public
189 * @static
191 public static function fetchAllRevisions( &$title ) {
192 return Revision::fetchFromConds(
193 wfGetDB( DB_SLAVE ),
194 array( 'page_namespace' => $title->getNamespace(),
195 'page_title' => $title->getDbkey(),
196 'page_id=rev_page' ) );
200 * Return a wrapper for a series of database rows to
201 * fetch all of a given page's revisions in turn.
202 * Each row can be fed to the constructor to get objects.
204 * @param Title $title
205 * @return ResultWrapper
206 * @access public
207 * @static
209 public static function fetchRevision( &$title ) {
210 return Revision::fetchFromConds(
211 wfGetDB( DB_SLAVE ),
212 array( 'rev_id=page_latest',
213 'page_namespace' => $title->getNamespace(),
214 'page_title' => $title->getDbkey(),
215 'page_id=rev_page' ) );
219 * Given a set of conditions, return a ResultWrapper
220 * which will return matching database rows with the
221 * fields necessary to build Revision objects.
223 * @param Database $db
224 * @param array $conditions
225 * @return ResultWrapper
226 * @access private
227 * @static
229 private static function fetchFromConds( &$db, $conditions ) {
230 $res = $db->select(
231 array( 'page', 'revision' ),
232 array( 'page_namespace',
233 'page_title',
234 'page_latest',
235 'rev_id',
236 'rev_page',
237 'rev_text_id',
238 'rev_comment',
239 'rev_user_text',
240 'rev_user',
241 'rev_minor_edit',
242 'rev_timestamp',
243 'rev_deleted' ),
244 $conditions,
245 'Revision::fetchRow',
246 array( 'LIMIT' => 1 ) );
247 $ret = $db->resultObject( $res );
248 return $ret;
252 * @param object $row
253 * @access private
255 function Revision( $row ) {
256 if( is_object( $row ) ) {
257 $this->mId = intval( $row->rev_id );
258 $this->mPage = intval( $row->rev_page );
259 $this->mTextId = intval( $row->rev_text_id );
260 $this->mComment = $row->rev_comment;
261 $this->mUserText = $row->rev_user_text;
262 $this->mUser = intval( $row->rev_user );
263 $this->mMinorEdit = intval( $row->rev_minor_edit );
264 $this->mTimestamp = $row->rev_timestamp;
265 $this->mDeleted = intval( $row->rev_deleted );
267 if( isset( $row->page_latest ) ) {
268 $this->mCurrent = ( $row->rev_id == $row->page_latest );
269 $this->mTitle = Title::makeTitle( $row->page_namespace,
270 $row->page_title );
271 } else {
272 $this->mCurrent = false;
273 $this->mTitle = null;
276 // Lazy extraction...
277 $this->mText = null;
278 if( isset( $row->old_text ) ) {
279 $this->mTextRow = $row;
280 } else {
281 // 'text' table row entry will be lazy-loaded
282 $this->mTextRow = null;
284 } elseif( is_array( $row ) ) {
285 // Build a new revision to be saved...
286 global $wgUser;
288 $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
289 $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
290 $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
291 $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
292 $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
293 $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
294 $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
295 $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
297 // Enforce spacing trimming on supplied text
298 $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
299 $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
300 $this->mTextRow = null;
302 $this->mTitle = null; # Load on demand if needed
303 $this->mCurrent = false;
304 } else {
305 throw new MWException( 'Revision constructor passed invalid row format.' );
309 /**#@+
310 * @access public
314 * @return int
316 function getId() {
317 return $this->mId;
321 * @return int
323 function getTextId() {
324 return $this->mTextId;
328 * Returns the title of the page associated with this entry.
329 * @return Title
331 function getTitle() {
332 if( isset( $this->mTitle ) ) {
333 return $this->mTitle;
335 $dbr =& wfGetDB( DB_SLAVE );
336 $row = $dbr->selectRow(
337 array( 'page', 'revision' ),
338 array( 'page_namespace', 'page_title' ),
339 array( 'page_id=rev_page',
340 'rev_id' => $this->mId ),
341 'Revision::getTitle' );
342 if( $row ) {
343 $this->mTitle = Title::makeTitle( $row->page_namespace,
344 $row->page_title );
346 return $this->mTitle;
350 * Set the title of the revision
351 * @param Title $title
353 function setTitle( $title ) {
354 $this->mTitle = $title;
358 * @return int
360 function getPage() {
361 return $this->mPage;
365 * Fetch revision's user id if it's available to all users
366 * @return int
368 function getUser() {
369 if( $this->isDeleted( self::DELETED_USER ) ) {
370 return 0;
371 } else {
372 return $this->mUser;
377 * Fetch revision's user id without regard for the current user's permissions
378 * @return string
380 function getRawUser() {
381 return $this->mUser;
385 * Fetch revision's username if it's available to all users
386 * @return string
388 function getUserText() {
389 if( $this->isDeleted( self::DELETED_USER ) ) {
390 return "";
391 } else {
392 return $this->mUserText;
397 * Fetch revision's username without regard for view restrictions
398 * @return string
400 function getRawUserText() {
401 return $this->mUserText;
405 * Fetch revision comment if it's available to all users
406 * @return string
408 function getComment() {
409 if( $this->isDeleted( self::DELETED_COMMENT ) ) {
410 return "";
411 } else {
412 return $this->mComment;
417 * Fetch revision comment without regard for the current user's permissions
418 * @return string
420 function getRawComment() {
421 return $this->mComment;
425 * @return bool
427 function isMinor() {
428 return (bool)$this->mMinorEdit;
432 * int $field one of DELETED_* bitfield constants
433 * @return bool
435 function isDeleted( $field ) {
436 return ($this->mDeleted & $field) == $field;
440 * Fetch revision text if it's available to all users
441 * @return string
443 function getText() {
444 if( $this->isDeleted( self::DELETED_TEXT ) ) {
445 return "";
446 } else {
447 return $this->getRawText();
452 * Fetch revision text without regard for view restrictions
453 * @return string
455 function getRawText() {
456 if( is_null( $this->mText ) ) {
457 // Revision text is immutable. Load on demand:
458 $this->mText = $this->loadText();
460 return $this->mText;
464 * @return string
466 function getTimestamp() {
467 return wfTimestamp(TS_MW, $this->mTimestamp);
471 * @return bool
473 function isCurrent() {
474 return $this->mCurrent;
478 * @return Revision
480 function getPrevious() {
481 $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
482 if ( $prev ) {
483 return Revision::newFromTitle( $this->mTitle, $prev );
484 } else {
485 return null;
490 * @return Revision
492 function getNext() {
493 $next = $this->mTitle->getNextRevisionID( $this->mId );
494 if ( $next ) {
495 return Revision::newFromTitle( $this->mTitle, $next );
496 } else {
497 return null;
500 /**#@-*/
503 * Get revision text associated with an old or archive row
504 * $row is usually an object from wfFetchRow(), both the flags and the text
505 * field must be included
506 * @static
507 * @param integer $row Id of a row
508 * @param string $prefix table prefix (default 'old_')
509 * @return string $text|false the text requested
511 function getRevisionText( $row, $prefix = 'old_' ) {
512 $fname = 'Revision::getRevisionText';
513 wfProfileIn( $fname );
515 # Get data
516 $textField = $prefix . 'text';
517 $flagsField = $prefix . 'flags';
519 if( isset( $row->$flagsField ) ) {
520 $flags = explode( ',', $row->$flagsField );
521 } else {
522 $flags = array();
525 if( isset( $row->$textField ) ) {
526 $text = $row->$textField;
527 } else {
528 wfProfileOut( $fname );
529 return false;
532 # Use external methods for external objects, text in table is URL-only then
533 if ( in_array( 'external', $flags ) ) {
534 $url=$text;
535 @list(/* $proto */,$path)=explode('://',$url,2);
536 if ($path=="") {
537 wfProfileOut( $fname );
538 return false;
540 $text=ExternalStore::fetchFromURL($url);
543 // If the text was fetched without an error, convert it
544 if ( $text !== false ) {
545 if( in_array( 'gzip', $flags ) ) {
546 # Deal with optional compression of archived pages.
547 # This can be done periodically via maintenance/compressOld.php, and
548 # as pages are saved if $wgCompressRevisions is set.
549 $text = gzinflate( $text );
552 if( in_array( 'object', $flags ) ) {
553 # Generic compressed storage
554 $obj = unserialize( $text );
555 if ( !is_object( $obj ) ) {
556 // Invalid object
557 wfProfileOut( $fname );
558 return false;
560 $text = $obj->getText();
563 global $wgLegacyEncoding;
564 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
565 # Old revisions kept around in a legacy encoding?
566 # Upconvert on demand.
567 global $wgInputEncoding, $wgContLang;
568 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
571 wfProfileOut( $fname );
572 return $text;
576 * If $wgCompressRevisions is enabled, we will compress data.
577 * The input string is modified in place.
578 * Return value is the flags field: contains 'gzip' if the
579 * data is compressed, and 'utf-8' if we're saving in UTF-8
580 * mode.
582 * @static
583 * @param mixed $text reference to a text
584 * @return string
586 function compressRevisionText( &$text ) {
587 global $wgCompressRevisions;
588 $flags = array();
590 # Revisions not marked this way will be converted
591 # on load if $wgLegacyCharset is set in the future.
592 $flags[] = 'utf-8';
594 if( $wgCompressRevisions ) {
595 if( function_exists( 'gzdeflate' ) ) {
596 $text = gzdeflate( $text );
597 $flags[] = 'gzip';
598 } else {
599 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
602 return implode( ',', $flags );
606 * Insert a new revision into the database, returning the new revision ID
607 * number on success and dies horribly on failure.
609 * @param Database $dbw
610 * @return int
612 function insertOn( &$dbw ) {
613 global $wgDefaultExternalStore;
615 $fname = 'Revision::insertOn';
616 wfProfileIn( $fname );
618 $data = $this->mText;
619 $flags = Revision::compressRevisionText( $data );
621 # Write to external storage if required
622 if ( $wgDefaultExternalStore ) {
623 if ( is_array( $wgDefaultExternalStore ) ) {
624 // Distribute storage across multiple clusters
625 $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
626 } else {
627 $store = $wgDefaultExternalStore;
629 // Store and get the URL
630 $data = ExternalStore::insert( $store, $data );
631 if ( !$data ) {
632 # This should only happen in the case of a configuration error, where the external store is not valid
633 throw new MWException( "Unable to store text to external storage $store" );
635 if ( $flags ) {
636 $flags .= ',';
638 $flags .= 'external';
641 # Record the text (or external storage URL) to the text table
642 if( !isset( $this->mTextId ) ) {
643 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
644 $dbw->insert( 'text',
645 array(
646 'old_id' => $old_id,
647 'old_text' => $data,
648 'old_flags' => $flags,
649 ), $fname
651 $this->mTextId = $dbw->insertId();
654 # Record the edit in revisions
655 $rev_id = isset( $this->mId )
656 ? $this->mId
657 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
658 $dbw->insert( 'revision',
659 array(
660 'rev_id' => $rev_id,
661 'rev_page' => $this->mPage,
662 'rev_text_id' => $this->mTextId,
663 'rev_comment' => $this->mComment,
664 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
665 'rev_user' => $this->mUser,
666 'rev_user_text' => $this->mUserText,
667 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
668 'rev_deleted' => $this->mDeleted,
669 ), $fname
672 $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
673 wfProfileOut( $fname );
674 return $this->mId;
678 * Lazy-load the revision's text.
679 * Currently hardcoded to the 'text' table storage engine.
681 * @return string
682 * @access private
684 function loadText() {
685 $fname = 'Revision::loadText';
686 wfProfileIn( $fname );
688 // Caching may be beneficial for massive use of external storage
689 global $wgRevisionCacheExpiry, $wgMemc;
690 $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
691 if( $wgRevisionCacheExpiry ) {
692 $text = $wgMemc->get( $key );
693 if( is_string( $text ) ) {
694 wfProfileOut( $fname );
695 return $text;
699 // If we kept data for lazy extraction, use it now...
700 if ( isset( $this->mTextRow ) ) {
701 $row = $this->mTextRow;
702 $this->mTextRow = null;
703 } else {
704 $row = null;
707 if( !$row ) {
708 // Text data is immutable; check slaves first.
709 $dbr =& wfGetDB( DB_SLAVE );
710 $row = $dbr->selectRow( 'text',
711 array( 'old_text', 'old_flags' ),
712 array( 'old_id' => $this->getTextId() ),
713 $fname);
716 if( !$row ) {
717 // Possible slave lag!
718 $dbw =& wfGetDB( DB_MASTER );
719 $row = $dbw->selectRow( 'text',
720 array( 'old_text', 'old_flags' ),
721 array( 'old_id' => $this->getTextId() ),
722 $fname);
725 $text = Revision::getRevisionText( $row );
727 if( $wgRevisionCacheExpiry ) {
728 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
731 wfProfileOut( $fname );
733 return $text;
737 * Create a new null-revision for insertion into a page's
738 * history. This will not re-save the text, but simply refer
739 * to the text from the previous version.
741 * Such revisions can for instance identify page rename
742 * operations and other such meta-modifications.
744 * @param Database $dbw
745 * @param int $pageId ID number of the page to read from
746 * @param string $summary
747 * @param bool $minor
748 * @return Revision
750 function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
751 $fname = 'Revision::newNullRevision';
752 wfProfileIn( $fname );
754 $current = $dbw->selectRow(
755 array( 'page', 'revision' ),
756 array( 'page_latest', 'rev_text_id' ),
757 array(
758 'page_id' => $pageId,
759 'page_latest=rev_id',
761 $fname );
763 if( $current ) {
764 $revision = new Revision( array(
765 'page' => $pageId,
766 'comment' => $summary,
767 'minor_edit' => $minor,
768 'text_id' => $current->rev_text_id,
769 ) );
770 } else {
771 $revision = null;
774 wfProfileOut( $fname );
775 return $revision;
779 * Determine if the current user is allowed to view a particular
780 * field of this revision, if it's marked as deleted.
781 * @param int $field one of self::DELETED_TEXT,
782 * self::DELETED_COMMENT,
783 * self::DELETED_USER
784 * @return bool
786 function userCan( $field ) {
787 if( ( $this->mDeleted & $field ) == $field ) {
788 global $wgUser;
789 $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
790 ? 'hiderevision'
791 : 'deleterevision';
792 wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
793 return $wgUser->isAllowed( $permission );
794 } else {
795 return true;
801 * Get rev_timestamp from rev_id, without loading the rest of the row
802 * @param integer $id
804 static function getTimestampFromID( $id ) {
805 $dbr =& wfGetDB( DB_SLAVE );
806 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
807 array( 'rev_id' => $id ), __METHOD__ );
808 if ( $timestamp === false ) {
809 # Not in slave, try master
810 $dbw =& wfGetDB( DB_MASTER );
811 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
812 array( 'rev_id' => $id ), __METHOD__ );
814 return $timestamp;
817 static function countByPageId( $db, $id ) {
818 $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
819 array( 'rev_page' => $id ), __METHOD__ );
820 if( $row ) {
821 return $row->revCount;
823 return 0;
826 static function countByTitle( $db, $title ) {
827 $id = $title->getArticleId();
828 if( $id ) {
829 return Revision::countByPageId( $db, $id );
831 return 0;
836 * Aliases for backwards compatibility with 1.6
838 define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
839 define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
840 define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
841 define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );