* Friendlier check for PHP 5 in command-line scripts; it's common for parallel
[mediawiki.git] / includes / Revision.php
blobbd68e05a8015d5570180ebbab3e53fee5d8ed263
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;
301 $this->mTitle = null; # Load on demand if needed
302 $this->mCurrent = false;
303 } else {
304 throw new MWException( 'Revision constructor passed invalid row format.' );
308 /**#@+
309 * @access public
313 * @return int
315 function getId() {
316 return $this->mId;
320 * @return int
322 function getTextId() {
323 return $this->mTextId;
327 * Returns the title of the page associated with this entry.
328 * @return Title
330 function getTitle() {
331 if( isset( $this->mTitle ) ) {
332 return $this->mTitle;
334 $dbr =& wfGetDB( DB_SLAVE );
335 $row = $dbr->selectRow(
336 array( 'page', 'revision' ),
337 array( 'page_namespace', 'page_title' ),
338 array( 'page_id=rev_page',
339 'rev_id' => $this->mId ),
340 'Revision::getTitle' );
341 if( $row ) {
342 $this->mTitle = Title::makeTitle( $row->page_namespace,
343 $row->page_title );
345 return $this->mTitle;
349 * Set the title of the revision
350 * @param Title $title
352 function setTitle( $title ) {
353 $this->mTitle = $title;
357 * @return int
359 function getPage() {
360 return $this->mPage;
364 * Fetch revision's user id if it's available to all users
365 * @return int
367 function getUser() {
368 if( $this->isDeleted( self::DELETED_USER ) ) {
369 return 0;
370 } else {
371 return $this->mUser;
376 * Fetch revision's user id without regard for the current user's permissions
377 * @return string
379 function getRawUser() {
380 return $this->mUser;
384 * Fetch revision's username if it's available to all users
385 * @return string
387 function getUserText() {
388 if( $this->isDeleted( self::DELETED_USER ) ) {
389 return "";
390 } else {
391 return $this->mUserText;
396 * Fetch revision's username without regard for view restrictions
397 * @return string
399 function getRawUserText() {
400 return $this->mUserText;
404 * Fetch revision comment if it's available to all users
405 * @return string
407 function getComment() {
408 if( $this->isDeleted( self::DELETED_COMMENT ) ) {
409 return "";
410 } else {
411 return $this->mComment;
416 * Fetch revision comment without regard for the current user's permissions
417 * @return string
419 function getRawComment() {
420 return $this->mComment;
424 * @return bool
426 function isMinor() {
427 return (bool)$this->mMinorEdit;
431 * int $field one of DELETED_* bitfield constants
432 * @return bool
434 function isDeleted( $field ) {
435 return ($this->mDeleted & $field) == $field;
439 * Fetch revision text if it's available to all users
440 * @return string
442 function getText() {
443 if( $this->isDeleted( self::DELETED_TEXT ) ) {
444 return "";
445 } else {
446 return $this->getRawText();
451 * Fetch revision text without regard for view restrictions
452 * @return string
454 function getRawText() {
455 if( is_null( $this->mText ) ) {
456 // Revision text is immutable. Load on demand:
457 $this->mText = $this->loadText();
459 return $this->mText;
463 * @return string
465 function getTimestamp() {
466 return wfTimestamp(TS_MW, $this->mTimestamp);
470 * @return bool
472 function isCurrent() {
473 return $this->mCurrent;
477 * @return Revision
479 function getPrevious() {
480 $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
481 if ( $prev ) {
482 return Revision::newFromTitle( $this->mTitle, $prev );
483 } else {
484 return null;
489 * @return Revision
491 function getNext() {
492 $next = $this->mTitle->getNextRevisionID( $this->mId );
493 if ( $next ) {
494 return Revision::newFromTitle( $this->mTitle, $next );
495 } else {
496 return null;
499 /**#@-*/
502 * Get revision text associated with an old or archive row
503 * $row is usually an object from wfFetchRow(), both the flags and the text
504 * field must be included
505 * @static
506 * @param integer $row Id of a row
507 * @param string $prefix table prefix (default 'old_')
508 * @return string $text|false the text requested
510 function getRevisionText( $row, $prefix = 'old_' ) {
511 $fname = 'Revision::getRevisionText';
512 wfProfileIn( $fname );
514 # Get data
515 $textField = $prefix . 'text';
516 $flagsField = $prefix . 'flags';
518 if( isset( $row->$flagsField ) ) {
519 $flags = explode( ',', $row->$flagsField );
520 } else {
521 $flags = array();
524 if( isset( $row->$textField ) ) {
525 $text = $row->$textField;
526 } else {
527 wfProfileOut( $fname );
528 return false;
531 # Use external methods for external objects, text in table is URL-only then
532 if ( in_array( 'external', $flags ) ) {
533 $url=$text;
534 @list($proto,$path)=explode('://',$url,2);
535 if ($path=="") {
536 wfProfileOut( $fname );
537 return false;
539 $text=ExternalStore::fetchFromURL($url);
542 // If the text was fetched without an error, convert it
543 if ( $text !== false ) {
544 if( in_array( 'gzip', $flags ) ) {
545 # Deal with optional compression of archived pages.
546 # This can be done periodically via maintenance/compressOld.php, and
547 # as pages are saved if $wgCompressRevisions is set.
548 $text = gzinflate( $text );
551 if( in_array( 'object', $flags ) ) {
552 # Generic compressed storage
553 $obj = unserialize( $text );
554 if ( !is_object( $obj ) ) {
555 // Invalid object
556 wfProfileOut( $fname );
557 return false;
559 $text = $obj->getText();
562 global $wgLegacyEncoding;
563 if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
564 # Old revisions kept around in a legacy encoding?
565 # Upconvert on demand.
566 global $wgInputEncoding, $wgContLang;
567 $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text );
570 wfProfileOut( $fname );
571 return $text;
575 * If $wgCompressRevisions is enabled, we will compress data.
576 * The input string is modified in place.
577 * Return value is the flags field: contains 'gzip' if the
578 * data is compressed, and 'utf-8' if we're saving in UTF-8
579 * mode.
581 * @static
582 * @param mixed $text reference to a text
583 * @return string
585 function compressRevisionText( &$text ) {
586 global $wgCompressRevisions;
587 $flags = array();
589 # Revisions not marked this way will be converted
590 # on load if $wgLegacyCharset is set in the future.
591 $flags[] = 'utf-8';
593 if( $wgCompressRevisions ) {
594 if( function_exists( 'gzdeflate' ) ) {
595 $text = gzdeflate( $text );
596 $flags[] = 'gzip';
597 } else {
598 wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
601 return implode( ',', $flags );
605 * Insert a new revision into the database, returning the new revision ID
606 * number on success and dies horribly on failure.
608 * @param Database $dbw
609 * @return int
611 function insertOn( &$dbw ) {
612 global $wgDefaultExternalStore;
614 $fname = 'Revision::insertOn';
615 wfProfileIn( $fname );
617 $data = $this->mText;
618 $flags = Revision::compressRevisionText( $data );
620 # Write to external storage if required
621 if ( $wgDefaultExternalStore ) {
622 if ( is_array( $wgDefaultExternalStore ) ) {
623 // Distribute storage across multiple clusters
624 $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
625 } else {
626 $store = $wgDefaultExternalStore;
628 // Store and get the URL
629 $data = ExternalStore::insert( $store, $data );
630 if ( !$data ) {
631 # This should only happen in the case of a configuration error, where the external store is not valid
632 throw new MWException( "Unable to store text to external storage $store" );
634 if ( $flags ) {
635 $flags .= ',';
637 $flags .= 'external';
640 # Record the text (or external storage URL) to the text table
641 if( !isset( $this->mTextId ) ) {
642 $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
643 $dbw->insert( 'text',
644 array(
645 'old_id' => $old_id,
646 'old_text' => $data,
647 'old_flags' => $flags,
648 ), $fname
650 $this->mTextId = $dbw->insertId();
653 # Record the edit in revisions
654 $rev_id = isset( $this->mId )
655 ? $this->mId
656 : $dbw->nextSequenceValue( 'rev_rev_id_val' );
657 $dbw->insert( 'revision',
658 array(
659 'rev_id' => $rev_id,
660 'rev_page' => $this->mPage,
661 'rev_text_id' => $this->mTextId,
662 'rev_comment' => $this->mComment,
663 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
664 'rev_user' => $this->mUser,
665 'rev_user_text' => $this->mUserText,
666 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
667 'rev_deleted' => $this->mDeleted,
668 ), $fname
671 $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
672 wfProfileOut( $fname );
673 return $this->mId;
677 * Lazy-load the revision's text.
678 * Currently hardcoded to the 'text' table storage engine.
680 * @return string
681 * @access private
683 function loadText() {
684 $fname = 'Revision::loadText';
685 wfProfileIn( $fname );
687 // Caching may be beneficial for massive use of external storage
688 global $wgRevisionCacheExpiry, $wgMemc;
689 $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
690 if( $wgRevisionCacheExpiry ) {
691 $text = $wgMemc->get( $key );
692 if( is_string( $text ) ) {
693 wfProfileOut( $fname );
694 return $text;
698 // If we kept data for lazy extraction, use it now...
699 if ( isset( $this->mTextRow ) ) {
700 $row = $this->mTextRow;
701 $this->mTextRow = null;
702 } else {
703 $row = null;
706 if( !$row ) {
707 // Text data is immutable; check slaves first.
708 $dbr =& wfGetDB( DB_SLAVE );
709 $row = $dbr->selectRow( 'text',
710 array( 'old_text', 'old_flags' ),
711 array( 'old_id' => $this->getTextId() ),
712 $fname);
715 if( !$row ) {
716 // Possible slave lag!
717 $dbw =& wfGetDB( DB_MASTER );
718 $row = $dbw->selectRow( 'text',
719 array( 'old_text', 'old_flags' ),
720 array( 'old_id' => $this->getTextId() ),
721 $fname);
724 $text = Revision::getRevisionText( $row );
726 if( $wgRevisionCacheExpiry ) {
727 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
730 wfProfileOut( $fname );
732 return $text;
736 * Create a new null-revision for insertion into a page's
737 * history. This will not re-save the text, but simply refer
738 * to the text from the previous version.
740 * Such revisions can for instance identify page rename
741 * operations and other such meta-modifications.
743 * @param Database $dbw
744 * @param int $pageId ID number of the page to read from
745 * @param string $summary
746 * @param bool $minor
747 * @return Revision
749 function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
750 $fname = 'Revision::newNullRevision';
751 wfProfileIn( $fname );
753 $current = $dbw->selectRow(
754 array( 'page', 'revision' ),
755 array( 'page_latest', 'rev_text_id' ),
756 array(
757 'page_id' => $pageId,
758 'page_latest=rev_id',
760 $fname );
762 if( $current ) {
763 $revision = new Revision( array(
764 'page' => $pageId,
765 'comment' => $summary,
766 'minor_edit' => $minor,
767 'text_id' => $current->rev_text_id,
768 ) );
769 } else {
770 $revision = null;
773 wfProfileOut( $fname );
774 return $revision;
778 * Determine if the current user is allowed to view a particular
779 * field of this revision, if it's marked as deleted.
780 * @param int $field one of self::DELETED_TEXT,
781 * self::DELETED_COMMENT,
782 * self::DELETED_USER
783 * @return bool
785 function userCan( $field ) {
786 if( ( $this->mDeleted & $field ) == $field ) {
787 global $wgUser;
788 $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
789 ? 'hiderevision'
790 : 'deleterevision';
791 wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
792 return $wgUser->isAllowed( $permission );
793 } else {
794 return true;
800 * Get rev_timestamp from rev_id, without loading the rest of the row
801 * @param integer $id
803 static function getTimestampFromID( $id ) {
804 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
805 array( 'rev_id' => $id ), __METHOD__ );
806 if ( $timestamp === false ) {
807 # Not in slave, try master
808 $dbw =& wfGetDB( DB_MASTER );
809 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
810 array( 'rev_id' => $id ), __METHOD__ );
812 return $timestamp;
815 static function countByPageId( $db, $id ) {
816 $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
817 array( 'rev_page' => $id ), __METHOD__ );
818 if( $row ) {
819 return $row->revCount;
821 return 0;
824 static function countByTitle( $db, $title ) {
825 $id = $title->getArticleId();
826 if( $id ) {
827 return Revision::countByPageId( $db, $id );
829 return 0;
834 * Aliases for backwards compatibility with 1.6
836 define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
837 define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
838 define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
839 define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );