MediaWiki  REL1_24
Revision.php
Go to the documentation of this file.
00001 <?php
00026 class Revision implements IDBAccessObject {
00027     protected $mId;
00028 
00032     protected $mPage;
00033     protected $mUserText;
00034     protected $mOrigUserText;
00035     protected $mUser;
00036     protected $mMinorEdit;
00037     protected $mTimestamp;
00038     protected $mDeleted;
00039     protected $mSize;
00040     protected $mSha1;
00041     protected $mParentId;
00042     protected $mComment;
00043     protected $mText;
00044     protected $mTextId;
00045 
00049     protected $mTextRow;
00050 
00054     protected $mTitle;
00055     protected $mCurrent;
00056     protected $mContentModel;
00057     protected $mContentFormat;
00058 
00062     protected $mContent;
00063 
00067     protected $mContentHandler;
00068 
00072     protected $mQueryFlags = 0;
00073 
00074     // Revision deletion constants
00075     const DELETED_TEXT = 1;
00076     const DELETED_COMMENT = 2;
00077     const DELETED_USER = 4;
00078     const DELETED_RESTRICTED = 8;
00079     const SUPPRESSED_USER = 12; // convenience
00080 
00081     // Audience options for accessors
00082     const FOR_PUBLIC = 1;
00083     const FOR_THIS_USER = 2;
00084     const RAW = 3;
00085 
00098     public static function newFromId( $id, $flags = 0 ) {
00099         return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
00100     }
00101 
00116     public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
00117         $conds = array(
00118             'page_namespace' => $title->getNamespace(),
00119             'page_title' => $title->getDBkey()
00120         );
00121         if ( $id ) {
00122             // Use the specified ID
00123             $conds['rev_id'] = $id;
00124             return self::newFromConds( $conds, (int)$flags );
00125         } else {
00126             // Use a join to get the latest revision
00127             $conds[] = 'rev_id=page_latest';
00128             $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00129             return self::loadFromConds( $db, $conds, $flags );
00130         }
00131     }
00132 
00147     public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
00148         $conds = array( 'page_id' => $pageId );
00149         if ( $revId ) {
00150             $conds['rev_id'] = $revId;
00151         } else {
00152             // Use a join to get the latest revision
00153             $conds[] = 'rev_id = page_latest';
00154         }
00155         return self::newFromConds( $conds, (int)$flags );
00156     }
00157 
00169     public static function newFromArchiveRow( $row, $overrides = array() ) {
00170         global $wgContentHandlerUseDB;
00171 
00172         $attribs = $overrides + array(
00173             'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
00174             'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
00175             'comment'    => $row->ar_comment,
00176             'user'       => $row->ar_user,
00177             'user_text'  => $row->ar_user_text,
00178             'timestamp'  => $row->ar_timestamp,
00179             'minor_edit' => $row->ar_minor_edit,
00180             'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
00181             'deleted'    => $row->ar_deleted,
00182             'len'        => $row->ar_len,
00183             'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
00184             'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
00185             'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
00186         );
00187 
00188         if ( !$wgContentHandlerUseDB ) {
00189             unset( $attribs['content_model'] );
00190             unset( $attribs['content_format'] );
00191         }
00192 
00193         if ( !isset( $attribs['title'] )
00194             && isset( $row->ar_namespace )
00195             && isset( $row->ar_title ) ) {
00196 
00197             $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
00198         }
00199 
00200         if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
00201             // Pre-1.5 ar_text row
00202             $attribs['text'] = self::getRevisionText( $row, 'ar_' );
00203             if ( $attribs['text'] === false ) {
00204                 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
00205             }
00206         }
00207         return new self( $attribs );
00208     }
00209 
00216     public static function newFromRow( $row ) {
00217         return new self( $row );
00218     }
00219 
00228     public static function loadFromId( $db, $id ) {
00229         return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
00230     }
00231 
00242     public static function loadFromPageId( $db, $pageid, $id = 0 ) {
00243         $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
00244         if ( $id ) {
00245             $conds['rev_id'] = intval( $id );
00246         } else {
00247             $conds[] = 'rev_id=page_latest';
00248         }
00249         return self::loadFromConds( $db, $conds );
00250     }
00251 
00262     public static function loadFromTitle( $db, $title, $id = 0 ) {
00263         if ( $id ) {
00264             $matchId = intval( $id );
00265         } else {
00266             $matchId = 'page_latest';
00267         }
00268         return self::loadFromConds( $db,
00269             array(
00270                 "rev_id=$matchId",
00271                 'page_namespace' => $title->getNamespace(),
00272                 'page_title' => $title->getDBkey()
00273             )
00274         );
00275     }
00276 
00287     public static function loadFromTimestamp( $db, $title, $timestamp ) {
00288         return self::loadFromConds( $db,
00289             array(
00290                 'rev_timestamp' => $db->timestamp( $timestamp ),
00291                 'page_namespace' => $title->getNamespace(),
00292                 'page_title' => $title->getDBkey()
00293             )
00294         );
00295     }
00296 
00304     private static function newFromConds( $conditions, $flags = 0 ) {
00305         $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00306         $rev = self::loadFromConds( $db, $conditions, $flags );
00307         if ( $rev === null && wfGetLB()->getServerCount() > 1 ) {
00308             if ( !( $flags & self::READ_LATEST ) ) {
00309                 $dbw = wfGetDB( DB_MASTER );
00310                 $rev = self::loadFromConds( $dbw, $conditions, $flags );
00311             }
00312         }
00313         if ( $rev ) {
00314             $rev->mQueryFlags = $flags;
00315         }
00316         return $rev;
00317     }
00318 
00328     private static function loadFromConds( $db, $conditions, $flags = 0 ) {
00329         $res = self::fetchFromConds( $db, $conditions, $flags );
00330         if ( $res ) {
00331             $row = $res->fetchObject();
00332             if ( $row ) {
00333                 $ret = new Revision( $row );
00334                 return $ret;
00335             }
00336         }
00337         $ret = null;
00338         return $ret;
00339     }
00340 
00349     public static function fetchRevision( $title ) {
00350         return self::fetchFromConds(
00351             wfGetDB( DB_SLAVE ),
00352             array(
00353                 'rev_id=page_latest',
00354                 'page_namespace' => $title->getNamespace(),
00355                 'page_title' => $title->getDBkey()
00356             )
00357         );
00358     }
00359 
00370     private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
00371         $fields = array_merge(
00372             self::selectFields(),
00373             self::selectPageFields(),
00374             self::selectUserFields()
00375         );
00376         $options = array( 'LIMIT' => 1 );
00377         if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
00378             $options[] = 'FOR UPDATE';
00379         }
00380         return $db->select(
00381             array( 'revision', 'page', 'user' ),
00382             $fields,
00383             $conditions,
00384             __METHOD__,
00385             $options,
00386             array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
00387         );
00388     }
00389 
00396     public static function userJoinCond() {
00397         return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
00398     }
00399 
00406     public static function pageJoinCond() {
00407         return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
00408     }
00409 
00415     public static function selectFields() {
00416         global $wgContentHandlerUseDB;
00417 
00418         $fields = array(
00419             'rev_id',
00420             'rev_page',
00421             'rev_text_id',
00422             'rev_timestamp',
00423             'rev_comment',
00424             'rev_user_text',
00425             'rev_user',
00426             'rev_minor_edit',
00427             'rev_deleted',
00428             'rev_len',
00429             'rev_parent_id',
00430             'rev_sha1',
00431         );
00432 
00433         if ( $wgContentHandlerUseDB ) {
00434             $fields[] = 'rev_content_format';
00435             $fields[] = 'rev_content_model';
00436         }
00437 
00438         return $fields;
00439     }
00440 
00446     public static function selectArchiveFields() {
00447         global $wgContentHandlerUseDB;
00448         $fields = array(
00449             'ar_id',
00450             'ar_page_id',
00451             'ar_rev_id',
00452             'ar_text',
00453             'ar_text_id',
00454             'ar_timestamp',
00455             'ar_comment',
00456             'ar_user_text',
00457             'ar_user',
00458             'ar_minor_edit',
00459             'ar_deleted',
00460             'ar_len',
00461             'ar_parent_id',
00462             'ar_sha1',
00463         );
00464 
00465         if ( $wgContentHandlerUseDB ) {
00466             $fields[] = 'ar_content_format';
00467             $fields[] = 'ar_content_model';
00468         }
00469         return $fields;
00470     }
00471 
00477     public static function selectTextFields() {
00478         return array(
00479             'old_text',
00480             'old_flags'
00481         );
00482     }
00483 
00488     public static function selectPageFields() {
00489         return array(
00490             'page_namespace',
00491             'page_title',
00492             'page_id',
00493             'page_latest',
00494             'page_is_redirect',
00495             'page_len',
00496         );
00497     }
00498 
00503     public static function selectUserFields() {
00504         return array( 'user_name' );
00505     }
00506 
00513     public static function getParentLengths( $db, array $revIds ) {
00514         $revLens = array();
00515         if ( !$revIds ) {
00516             return $revLens; // empty
00517         }
00518         wfProfileIn( __METHOD__ );
00519         $res = $db->select( 'revision',
00520             array( 'rev_id', 'rev_len' ),
00521             array( 'rev_id' => $revIds ),
00522             __METHOD__ );
00523         foreach ( $res as $row ) {
00524             $revLens[$row->rev_id] = $row->rev_len;
00525         }
00526         wfProfileOut( __METHOD__ );
00527         return $revLens;
00528     }
00529 
00537     function __construct( $row ) {
00538         if ( is_object( $row ) ) {
00539             $this->mId = intval( $row->rev_id );
00540             $this->mPage = intval( $row->rev_page );
00541             $this->mTextId = intval( $row->rev_text_id );
00542             $this->mComment = $row->rev_comment;
00543             $this->mUser = intval( $row->rev_user );
00544             $this->mMinorEdit = intval( $row->rev_minor_edit );
00545             $this->mTimestamp = $row->rev_timestamp;
00546             $this->mDeleted = intval( $row->rev_deleted );
00547 
00548             if ( !isset( $row->rev_parent_id ) ) {
00549                 $this->mParentId = null;
00550             } else {
00551                 $this->mParentId = intval( $row->rev_parent_id );
00552             }
00553 
00554             if ( !isset( $row->rev_len ) ) {
00555                 $this->mSize = null;
00556             } else {
00557                 $this->mSize = intval( $row->rev_len );
00558             }
00559 
00560             if ( !isset( $row->rev_sha1 ) ) {
00561                 $this->mSha1 = null;
00562             } else {
00563                 $this->mSha1 = $row->rev_sha1;
00564             }
00565 
00566             if ( isset( $row->page_latest ) ) {
00567                 $this->mCurrent = ( $row->rev_id == $row->page_latest );
00568                 $this->mTitle = Title::newFromRow( $row );
00569             } else {
00570                 $this->mCurrent = false;
00571                 $this->mTitle = null;
00572             }
00573 
00574             if ( !isset( $row->rev_content_model ) ) {
00575                 $this->mContentModel = null; # determine on demand if needed
00576             } else {
00577                 $this->mContentModel = strval( $row->rev_content_model );
00578             }
00579 
00580             if ( !isset( $row->rev_content_format ) ) {
00581                 $this->mContentFormat = null; # determine on demand if needed
00582             } else {
00583                 $this->mContentFormat = strval( $row->rev_content_format );
00584             }
00585 
00586             // Lazy extraction...
00587             $this->mText = null;
00588             if ( isset( $row->old_text ) ) {
00589                 $this->mTextRow = $row;
00590             } else {
00591                 // 'text' table row entry will be lazy-loaded
00592                 $this->mTextRow = null;
00593             }
00594 
00595             // Use user_name for users and rev_user_text for IPs...
00596             $this->mUserText = null; // lazy load if left null
00597             if ( $this->mUser == 0 ) {
00598                 $this->mUserText = $row->rev_user_text; // IP user
00599             } elseif ( isset( $row->user_name ) ) {
00600                 $this->mUserText = $row->user_name; // logged-in user
00601             }
00602             $this->mOrigUserText = $row->rev_user_text;
00603         } elseif ( is_array( $row ) ) {
00604             // Build a new revision to be saved...
00605             global $wgUser; // ugh
00606 
00607             # if we have a content object, use it to set the model and type
00608             if ( !empty( $row['content'] ) ) {
00609                 // @todo when is that set? test with external store setup! check out insertOn() [dk]
00610                 if ( !empty( $row['text_id'] ) ) {
00611                     throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
00612                         "can't serialize content object" );
00613                 }
00614 
00615                 $row['content_model'] = $row['content']->getModel();
00616                 # note: mContentFormat is initializes later accordingly
00617                 # note: content is serialized later in this method!
00618                 # also set text to null?
00619             }
00620 
00621             $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
00622             $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
00623             $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
00624             $this->mUserText = isset( $row['user_text'] )
00625                 ? strval( $row['user_text'] ) : $wgUser->getName();
00626             $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
00627             $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
00628             $this->mTimestamp = isset( $row['timestamp'] )
00629                 ? strval( $row['timestamp'] ) : wfTimestampNow();
00630             $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
00631             $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
00632             $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
00633             $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
00634 
00635             $this->mContentModel = isset( $row['content_model'] )
00636                 ? strval( $row['content_model'] ) : null;
00637             $this->mContentFormat = isset( $row['content_format'] )
00638                 ? strval( $row['content_format'] ) : null;
00639 
00640             // Enforce spacing trimming on supplied text
00641             $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
00642             $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
00643             $this->mTextRow = null;
00644 
00645             $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
00646 
00647             // if we have a Content object, override mText and mContentModel
00648             if ( !empty( $row['content'] ) ) {
00649                 if ( !( $row['content'] instanceof Content ) ) {
00650                     throw new MWException( '`content` field must contain a Content object.' );
00651                 }
00652 
00653                 $handler = $this->getContentHandler();
00654                 $this->mContent = $row['content'];
00655 
00656                 $this->mContentModel = $this->mContent->getModel();
00657                 $this->mContentHandler = null;
00658 
00659                 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
00660             } elseif ( $this->mText !== null ) {
00661                 $handler = $this->getContentHandler();
00662                 $this->mContent = $handler->unserializeContent( $this->mText );
00663             }
00664 
00665             // If we have a Title object, make sure it is consistent with mPage.
00666             if ( $this->mTitle && $this->mTitle->exists() ) {
00667                 if ( $this->mPage === null ) {
00668                     // if the page ID wasn't known, set it now
00669                     $this->mPage = $this->mTitle->getArticleID();
00670                 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
00671                     // Got different page IDs. This may be legit (e.g. during undeletion),
00672                     // but it seems worth mentioning it in the log.
00673                     wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
00674                         $this->mTitle->getArticleID() . " provided by the Title object." );
00675                 }
00676             }
00677 
00678             $this->mCurrent = false;
00679 
00680             // If we still have no length, see it we have the text to figure it out
00681             if ( !$this->mSize ) {
00682                 if ( $this->mContent !== null ) {
00683                     $this->mSize = $this->mContent->getSize();
00684                 } else {
00685                     #NOTE: this should never happen if we have either text or content object!
00686                     $this->mSize = null;
00687                 }
00688             }
00689 
00690             // Same for sha1
00691             if ( $this->mSha1 === null ) {
00692                 $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
00693             }
00694 
00695             // force lazy init
00696             $this->getContentModel();
00697             $this->getContentFormat();
00698         } else {
00699             throw new MWException( 'Revision constructor passed invalid row format.' );
00700         }
00701         $this->mUnpatrolled = null;
00702     }
00703 
00709     public function getId() {
00710         return $this->mId;
00711     }
00712 
00719     public function setId( $id ) {
00720         $this->mId = $id;
00721     }
00722 
00728     public function getTextId() {
00729         return $this->mTextId;
00730     }
00731 
00737     public function getParentId() {
00738         return $this->mParentId;
00739     }
00740 
00746     public function getSize() {
00747         return $this->mSize;
00748     }
00749 
00755     public function getSha1() {
00756         return $this->mSha1;
00757     }
00758 
00766     public function getTitle() {
00767         if ( $this->mTitle !== null ) {
00768             return $this->mTitle;
00769         }
00770         //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
00771         if ( $this->mId !== null ) {
00772             $dbr = wfGetDB( DB_SLAVE );
00773             $row = $dbr->selectRow(
00774                 array( 'page', 'revision' ),
00775                 self::selectPageFields(),
00776                 array( 'page_id=rev_page',
00777                     'rev_id' => $this->mId ),
00778                 __METHOD__ );
00779             if ( $row ) {
00780                 $this->mTitle = Title::newFromRow( $row );
00781             }
00782         }
00783 
00784         if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
00785             $this->mTitle = Title::newFromID( $this->mPage );
00786         }
00787 
00788         return $this->mTitle;
00789     }
00790 
00796     public function setTitle( $title ) {
00797         $this->mTitle = $title;
00798     }
00799 
00805     public function getPage() {
00806         return $this->mPage;
00807     }
00808 
00822     public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
00823         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00824             return 0;
00825         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00826             return 0;
00827         } else {
00828             return $this->mUser;
00829         }
00830     }
00831 
00837     public function getRawUser() {
00838         return $this->mUser;
00839     }
00840 
00854     public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
00855         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00856             return '';
00857         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00858             return '';
00859         } else {
00860             return $this->getRawUserText();
00861         }
00862     }
00863 
00869     public function getRawUserText() {
00870         if ( $this->mUserText === null ) {
00871             $this->mUserText = User::whoIs( $this->mUser ); // load on demand
00872             if ( $this->mUserText === false ) {
00873                 # This shouldn't happen, but it can if the wiki was recovered
00874                 # via importing revs and there is no user table entry yet.
00875                 $this->mUserText = $this->mOrigUserText;
00876             }
00877         }
00878         return $this->mUserText;
00879     }
00880 
00894     function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
00895         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
00896             return '';
00897         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
00898             return '';
00899         } else {
00900             return $this->mComment;
00901         }
00902     }
00903 
00909     public function getRawComment() {
00910         return $this->mComment;
00911     }
00912 
00916     public function isMinor() {
00917         return (bool)$this->mMinorEdit;
00918     }
00919 
00923     public function isUnpatrolled() {
00924         if ( $this->mUnpatrolled !== null ) {
00925             return $this->mUnpatrolled;
00926         }
00927         $rc = $this->getRecentChange();
00928         if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
00929             $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
00930         } else {
00931             $this->mUnpatrolled = 0;
00932         }
00933         return $this->mUnpatrolled;
00934     }
00935 
00942     public function getRecentChange() {
00943         $dbr = wfGetDB( DB_SLAVE );
00944         return RecentChange::newFromConds(
00945             array(
00946                 'rc_user_text' => $this->getRawUserText(),
00947                 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
00948                 'rc_this_oldid' => $this->getId()
00949             ),
00950             __METHOD__
00951         );
00952     }
00953 
00959     public function isDeleted( $field ) {
00960         return ( $this->mDeleted & $field ) == $field;
00961     }
00962 
00968     public function getVisibility() {
00969         return (int)$this->mDeleted;
00970     }
00971 
00988     public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
00989         ContentHandler::deprecated( __METHOD__, '1.21' );
00990 
00991         $content = $this->getContent( $audience, $user );
00992         return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
00993     }
00994 
01009     public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
01010         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
01011             return null;
01012         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
01013             return null;
01014         } else {
01015             return $this->getContentInternal();
01016         }
01017     }
01018 
01027     public function getRawText() {
01028         ContentHandler::deprecated( __METHOD__, "1.21" );
01029         return $this->getText( self::RAW );
01030     }
01031 
01038     public function getSerializedData() {
01039         if ( $this->mText === null ) {
01040             $this->mText = $this->loadText();
01041         }
01042 
01043         return $this->mText;
01044     }
01045 
01055     protected function getContentInternal() {
01056         if ( $this->mContent === null ) {
01057             // Revision is immutable. Load on demand:
01058             if ( $this->mText === null ) {
01059                 $this->mText = $this->loadText();
01060             }
01061 
01062             if ( $this->mText !== null && $this->mText !== false ) {
01063                 // Unserialize content
01064                 $handler = $this->getContentHandler();
01065                 $format = $this->getContentFormat();
01066 
01067                 $this->mContent = $handler->unserializeContent( $this->mText, $format );
01068             } else {
01069                 $this->mContent = false; // negative caching!
01070             }
01071         }
01072 
01073         // NOTE: copy() will return $this for immutable content objects
01074         return $this->mContent ? $this->mContent->copy() : null;
01075     }
01076 
01087     public function getContentModel() {
01088         if ( !$this->mContentModel ) {
01089             $title = $this->getTitle();
01090             $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
01091 
01092             assert( !empty( $this->mContentModel ) );
01093         }
01094 
01095         return $this->mContentModel;
01096     }
01097 
01107     public function getContentFormat() {
01108         if ( !$this->mContentFormat ) {
01109             $handler = $this->getContentHandler();
01110             $this->mContentFormat = $handler->getDefaultFormat();
01111 
01112             assert( !empty( $this->mContentFormat ) );
01113         }
01114 
01115         return $this->mContentFormat;
01116     }
01117 
01124     public function getContentHandler() {
01125         if ( !$this->mContentHandler ) {
01126             $model = $this->getContentModel();
01127             $this->mContentHandler = ContentHandler::getForModelID( $model );
01128 
01129             $format = $this->getContentFormat();
01130 
01131             if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
01132                 throw new MWException( "Oops, the content format $format is not supported for "
01133                     . "this content model, $model" );
01134             }
01135         }
01136 
01137         return $this->mContentHandler;
01138     }
01139 
01143     public function getTimestamp() {
01144         return wfTimestamp( TS_MW, $this->mTimestamp );
01145     }
01146 
01150     public function isCurrent() {
01151         return $this->mCurrent;
01152     }
01153 
01159     public function getPrevious() {
01160         if ( $this->getTitle() ) {
01161             $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
01162             if ( $prev ) {
01163                 return self::newFromTitle( $this->getTitle(), $prev );
01164             }
01165         }
01166         return null;
01167     }
01168 
01174     public function getNext() {
01175         if ( $this->getTitle() ) {
01176             $next = $this->getTitle()->getNextRevisionID( $this->getId() );
01177             if ( $next ) {
01178                 return self::newFromTitle( $this->getTitle(), $next );
01179             }
01180         }
01181         return null;
01182     }
01183 
01191     private function getPreviousRevisionId( $db ) {
01192         if ( $this->mPage === null ) {
01193             return 0;
01194         }
01195         # Use page_latest if ID is not given
01196         if ( !$this->mId ) {
01197             $prevId = $db->selectField( 'page', 'page_latest',
01198                 array( 'page_id' => $this->mPage ),
01199                 __METHOD__ );
01200         } else {
01201             $prevId = $db->selectField( 'revision', 'rev_id',
01202                 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
01203                 __METHOD__,
01204                 array( 'ORDER BY' => 'rev_id DESC' ) );
01205         }
01206         return intval( $prevId );
01207     }
01208 
01222     public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
01223         wfProfileIn( __METHOD__ );
01224 
01225         # Get data
01226         $textField = $prefix . 'text';
01227         $flagsField = $prefix . 'flags';
01228 
01229         if ( isset( $row->$flagsField ) ) {
01230             $flags = explode( ',', $row->$flagsField );
01231         } else {
01232             $flags = array();
01233         }
01234 
01235         if ( isset( $row->$textField ) ) {
01236             $text = $row->$textField;
01237         } else {
01238             wfProfileOut( __METHOD__ );
01239             return false;
01240         }
01241 
01242         # Use external methods for external objects, text in table is URL-only then
01243         if ( in_array( 'external', $flags ) ) {
01244             $url = $text;
01245             $parts = explode( '://', $url, 2 );
01246             if ( count( $parts ) == 1 || $parts[1] == '' ) {
01247                 wfProfileOut( __METHOD__ );
01248                 return false;
01249             }
01250             $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) );
01251         }
01252 
01253         // If the text was fetched without an error, convert it
01254         if ( $text !== false ) {
01255             $text = self::decompressRevisionText( $text, $flags );
01256         }
01257         wfProfileOut( __METHOD__ );
01258         return $text;
01259     }
01260 
01271     public static function compressRevisionText( &$text ) {
01272         global $wgCompressRevisions;
01273         $flags = array();
01274 
01275         # Revisions not marked this way will be converted
01276         # on load if $wgLegacyCharset is set in the future.
01277         $flags[] = 'utf-8';
01278 
01279         if ( $wgCompressRevisions ) {
01280             if ( function_exists( 'gzdeflate' ) ) {
01281                 $text = gzdeflate( $text );
01282                 $flags[] = 'gzip';
01283             } else {
01284                 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
01285             }
01286         }
01287         return implode( ',', $flags );
01288     }
01289 
01297     public static function decompressRevisionText( $text, $flags ) {
01298         if ( in_array( 'gzip', $flags ) ) {
01299             # Deal with optional compression of archived pages.
01300             # This can be done periodically via maintenance/compressOld.php, and
01301             # as pages are saved if $wgCompressRevisions is set.
01302             $text = gzinflate( $text );
01303         }
01304 
01305         if ( in_array( 'object', $flags ) ) {
01306             # Generic compressed storage
01307             $obj = unserialize( $text );
01308             if ( !is_object( $obj ) ) {
01309                 // Invalid object
01310                 return false;
01311             }
01312             $text = $obj->getText();
01313         }
01314 
01315         global $wgLegacyEncoding;
01316         if ( $text !== false && $wgLegacyEncoding
01317             && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
01318         ) {
01319             # Old revisions kept around in a legacy encoding?
01320             # Upconvert on demand.
01321             # ("utf8" checked for compatibility with some broken
01322             #  conversion scripts 2008-12-30)
01323             global $wgContLang;
01324             $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
01325         }
01326 
01327         return $text;
01328     }
01329 
01338     public function insertOn( $dbw ) {
01339         global $wgDefaultExternalStore, $wgContentHandlerUseDB;
01340 
01341         wfProfileIn( __METHOD__ );
01342 
01343         $this->checkContentModel();
01344 
01345         $data = $this->mText;
01346         $flags = self::compressRevisionText( $data );
01347 
01348         # Write to external storage if required
01349         if ( $wgDefaultExternalStore ) {
01350             // Store and get the URL
01351             $data = ExternalStore::insertToDefault( $data );
01352             if ( !$data ) {
01353                 wfProfileOut( __METHOD__ );
01354                 throw new MWException( "Unable to store text to external storage" );
01355             }
01356             if ( $flags ) {
01357                 $flags .= ',';
01358             }
01359             $flags .= 'external';
01360         }
01361 
01362         # Record the text (or external storage URL) to the text table
01363         if ( $this->mTextId === null ) {
01364             $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
01365             $dbw->insert( 'text',
01366                 array(
01367                     'old_id' => $old_id,
01368                     'old_text' => $data,
01369                     'old_flags' => $flags,
01370                 ), __METHOD__
01371             );
01372             $this->mTextId = $dbw->insertId();
01373         }
01374 
01375         if ( $this->mComment === null ) {
01376             $this->mComment = "";
01377         }
01378 
01379         # Record the edit in revisions
01380         $rev_id = $this->mId !== null
01381             ? $this->mId
01382             : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
01383         $row = array(
01384             'rev_id'         => $rev_id,
01385             'rev_page'       => $this->mPage,
01386             'rev_text_id'    => $this->mTextId,
01387             'rev_comment'    => $this->mComment,
01388             'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
01389             'rev_user'       => $this->mUser,
01390             'rev_user_text'  => $this->mUserText,
01391             'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
01392             'rev_deleted'    => $this->mDeleted,
01393             'rev_len'        => $this->mSize,
01394             'rev_parent_id'  => $this->mParentId === null
01395                 ? $this->getPreviousRevisionId( $dbw )
01396                 : $this->mParentId,
01397             'rev_sha1'       => $this->mSha1 === null
01398                 ? Revision::base36Sha1( $this->mText )
01399                 : $this->mSha1,
01400         );
01401 
01402         if ( $wgContentHandlerUseDB ) {
01403             //NOTE: Store null for the default model and format, to save space.
01404             //XXX: Makes the DB sensitive to changed defaults.
01405             // Make this behavior optional? Only in miser mode?
01406 
01407             $model = $this->getContentModel();
01408             $format = $this->getContentFormat();
01409 
01410             $title = $this->getTitle();
01411 
01412             if ( $title === null ) {
01413                 wfProfileOut( __METHOD__ );
01414                 throw new MWException( "Insufficient information to determine the title of the "
01415                     . "revision's page!" );
01416             }
01417 
01418             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01419             $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
01420 
01421             $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
01422             $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
01423         }
01424 
01425         $dbw->insert( 'revision', $row, __METHOD__ );
01426 
01427         $this->mId = $rev_id !== null ? $rev_id : $dbw->insertId();
01428 
01429         wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
01430 
01431         wfProfileOut( __METHOD__ );
01432         return $this->mId;
01433     }
01434 
01435     protected function checkContentModel() {
01436         global $wgContentHandlerUseDB;
01437 
01438         $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
01439 
01440         $model = $this->getContentModel();
01441         $format = $this->getContentFormat();
01442         $handler = $this->getContentHandler();
01443 
01444         if ( !$handler->isSupportedFormat( $format ) ) {
01445             $t = $title->getPrefixedDBkey();
01446 
01447             throw new MWException( "Can't use format $format with content model $model on $t" );
01448         }
01449 
01450         if ( !$wgContentHandlerUseDB && $title ) {
01451             // if $wgContentHandlerUseDB is not set,
01452             // all revisions must use the default content model and format.
01453 
01454             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01455             $defaultHandler = ContentHandler::getForModelID( $defaultModel );
01456             $defaultFormat = $defaultHandler->getDefaultFormat();
01457 
01458             if ( $this->getContentModel() != $defaultModel ) {
01459                 $t = $title->getPrefixedDBkey();
01460 
01461                 throw new MWException( "Can't save non-default content model with "
01462                     . "\$wgContentHandlerUseDB disabled: model is $model, "
01463                     . "default for $t is $defaultModel" );
01464             }
01465 
01466             if ( $this->getContentFormat() != $defaultFormat ) {
01467                 $t = $title->getPrefixedDBkey();
01468 
01469                 throw new MWException( "Can't use non-default content format with "
01470                     . "\$wgContentHandlerUseDB disabled: format is $format, "
01471                     . "default for $t is $defaultFormat" );
01472             }
01473         }
01474 
01475         $content = $this->getContent( Revision::RAW );
01476 
01477         if ( !$content || !$content->isValid() ) {
01478             $t = $title->getPrefixedDBkey();
01479 
01480             throw new MWException( "Content of $t is not valid! Content model is $model" );
01481         }
01482     }
01483 
01489     public static function base36Sha1( $text ) {
01490         return wfBaseConvert( sha1( $text ), 16, 36, 31 );
01491     }
01492 
01499     protected function loadText() {
01500         wfProfileIn( __METHOD__ );
01501 
01502         // Caching may be beneficial for massive use of external storage
01503         global $wgRevisionCacheExpiry, $wgMemc;
01504         $textId = $this->getTextId();
01505         $key = wfMemcKey( 'revisiontext', 'textid', $textId );
01506         if ( $wgRevisionCacheExpiry ) {
01507             $text = $wgMemc->get( $key );
01508             if ( is_string( $text ) ) {
01509                 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
01510                 wfProfileOut( __METHOD__ );
01511                 return $text;
01512             }
01513         }
01514 
01515         // If we kept data for lazy extraction, use it now...
01516         if ( $this->mTextRow !== null ) {
01517             $row = $this->mTextRow;
01518             $this->mTextRow = null;
01519         } else {
01520             $row = null;
01521         }
01522 
01523         if ( !$row ) {
01524             // Text data is immutable; check slaves first.
01525             $dbr = wfGetDB( DB_SLAVE );
01526             $row = $dbr->selectRow( 'text',
01527                 array( 'old_text', 'old_flags' ),
01528                 array( 'old_id' => $textId ),
01529                 __METHOD__ );
01530         }
01531 
01532         // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was
01533         // used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
01534         $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
01535         if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) {
01536             $dbw = wfGetDB( DB_MASTER );
01537             $row = $dbw->selectRow( 'text',
01538                 array( 'old_text', 'old_flags' ),
01539                 array( 'old_id' => $textId ),
01540                 __METHOD__,
01541                 $forUpdate ? array( 'FOR UPDATE' ) : array() );
01542         }
01543 
01544         if ( !$row ) {
01545             wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
01546         }
01547 
01548         $text = self::getRevisionText( $row );
01549         if ( $row && $text === false ) {
01550             wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
01551         }
01552 
01553         # No negative caching -- negative hits on text rows may be due to corrupted slave servers
01554         if ( $wgRevisionCacheExpiry && $text !== false ) {
01555             $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
01556         }
01557 
01558         wfProfileOut( __METHOD__ );
01559 
01560         return $text;
01561     }
01562 
01578     public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
01579         global $wgContentHandlerUseDB;
01580 
01581         wfProfileIn( __METHOD__ );
01582 
01583         $fields = array( 'page_latest', 'page_namespace', 'page_title',
01584                         'rev_text_id', 'rev_len', 'rev_sha1' );
01585 
01586         if ( $wgContentHandlerUseDB ) {
01587             $fields[] = 'rev_content_model';
01588             $fields[] = 'rev_content_format';
01589         }
01590 
01591         $current = $dbw->selectRow(
01592             array( 'page', 'revision' ),
01593             $fields,
01594             array(
01595                 'page_id' => $pageId,
01596                 'page_latest=rev_id',
01597                 ),
01598             __METHOD__ );
01599 
01600         if ( $current ) {
01601             if ( !$user ) {
01602                 global $wgUser;
01603                 $user = $wgUser;
01604             }
01605 
01606             $row = array(
01607                 'page'       => $pageId,
01608                 'user_text'  => $user->getName(),
01609                 'user'       => $user->getId(),
01610                 'comment'    => $summary,
01611                 'minor_edit' => $minor,
01612                 'text_id'    => $current->rev_text_id,
01613                 'parent_id'  => $current->page_latest,
01614                 'len'        => $current->rev_len,
01615                 'sha1'       => $current->rev_sha1
01616             );
01617 
01618             if ( $wgContentHandlerUseDB ) {
01619                 $row['content_model'] = $current->rev_content_model;
01620                 $row['content_format'] = $current->rev_content_format;
01621             }
01622 
01623             $revision = new Revision( $row );
01624             $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
01625         } else {
01626             $revision = null;
01627         }
01628 
01629         wfProfileOut( __METHOD__ );
01630         return $revision;
01631     }
01632 
01643     public function userCan( $field, User $user = null ) {
01644         return self::userCanBitfield( $this->mDeleted, $field, $user );
01645     }
01646 
01661     public static function userCanBitfield( $bitfield, $field, User $user = null,
01662         Title $title = null
01663     ) {
01664         if ( $bitfield & $field ) { // aspect is deleted
01665             if ( $user === null ) {
01666                 global $wgUser;
01667                 $user = $wgUser;
01668             }
01669             if ( $bitfield & self::DELETED_RESTRICTED ) {
01670                 $permissions = array( 'suppressrevision', 'viewsuppressed' );
01671             } elseif ( $field & self::DELETED_TEXT ) {
01672                 $permissions = array( 'deletedtext' );
01673             } else {
01674                 $permissions = array( 'deletedhistory' );
01675             }
01676             $permissionlist = implode( ', ', $permissions );
01677             if ( $title === null ) {
01678                 wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
01679                 return call_user_func_array( array( $user, 'isAllowedAny' ), $permissions );
01680             } else {
01681                 $text = $title->getPrefixedText();
01682                 wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
01683                 foreach ( $permissions as $perm ) {
01684                     if ( $title->userCan( $perm, $user ) ) {
01685                         return true;
01686                     }
01687                 }
01688                 return false;
01689             }
01690         } else {
01691             return true;
01692         }
01693     }
01694 
01702     static function getTimestampFromId( $title, $id ) {
01703         $dbr = wfGetDB( DB_SLAVE );
01704         // Casting fix for databases that can't take '' for rev_id
01705         if ( $id == '' ) {
01706             $id = 0;
01707         }
01708         $conds = array( 'rev_id' => $id );
01709         $conds['rev_page'] = $title->getArticleID();
01710         $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01711         if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
01712             # Not in slave, try master
01713             $dbw = wfGetDB( DB_MASTER );
01714             $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01715         }
01716         return wfTimestamp( TS_MW, $timestamp );
01717     }
01718 
01726     static function countByPageId( $db, $id ) {
01727         $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
01728             array( 'rev_page' => $id ), __METHOD__ );
01729         if ( $row ) {
01730             return $row->revCount;
01731         }
01732         return 0;
01733     }
01734 
01742     static function countByTitle( $db, $title ) {
01743         $id = $title->getArticleID();
01744         if ( $id ) {
01745             return self::countByPageId( $db, $id );
01746         }
01747         return 0;
01748     }
01749 
01766     public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
01767         if ( !$userId ) {
01768             return false;
01769         }
01770 
01771         if ( is_int( $db ) ) {
01772             $db = wfGetDB( $db );
01773         }
01774 
01775         $res = $db->select( 'revision',
01776             'rev_user',
01777             array(
01778                 'rev_page' => $pageId,
01779                 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
01780             ),
01781             __METHOD__,
01782             array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
01783         foreach ( $res as $row ) {
01784             if ( $row->rev_user != $userId ) {
01785                 return false;
01786             }
01787         }
01788         return true;
01789     }
01790 }