MediaWiki  REL1_22
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 $mTextRow;
00045 
00049     protected $mTitle;
00050     protected $mCurrent;
00051     protected $mContentModel;
00052     protected $mContentFormat;
00053 
00057     protected $mContent;
00058 
00062     protected $mContentHandler;
00063 
00067     protected $mQueryFlags = 0;
00068 
00069     // Revision deletion constants
00070     const DELETED_TEXT = 1;
00071     const DELETED_COMMENT = 2;
00072     const DELETED_USER = 4;
00073     const DELETED_RESTRICTED = 8;
00074     const SUPPRESSED_USER = 12; // convenience
00075 
00076     // Audience options for accessors
00077     const FOR_PUBLIC = 1;
00078     const FOR_THIS_USER = 2;
00079     const RAW = 3;
00080 
00093     public static function newFromId( $id, $flags = 0 ) {
00094         return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
00095     }
00096 
00111     public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
00112         $conds = array(
00113             'page_namespace' => $title->getNamespace(),
00114             'page_title' => $title->getDBkey()
00115         );
00116         if ( $id ) {
00117             // Use the specified ID
00118             $conds['rev_id'] = $id;
00119             return self::newFromConds( $conds, (int)$flags );
00120         } else {
00121             // Use a join to get the latest revision
00122             $conds[] = 'rev_id=page_latest';
00123             $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00124             return self::loadFromConds( $db, $conds, $flags );
00125         }
00126     }
00127 
00142     public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
00143         $conds = array( 'page_id' => $pageId );
00144         if ( $revId ) {
00145             $conds['rev_id'] = $revId;
00146         } else {
00147             // Use a join to get the latest revision
00148             $conds[] = 'rev_id = page_latest';
00149         }
00150         return self::newFromConds( $conds, (int)$flags );
00151     }
00152 
00164     public static function newFromArchiveRow( $row, $overrides = array() ) {
00165         global $wgContentHandlerUseDB;
00166 
00167         $attribs = $overrides + array(
00168             'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
00169             'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
00170             'comment'    => $row->ar_comment,
00171             'user'       => $row->ar_user,
00172             'user_text'  => $row->ar_user_text,
00173             'timestamp'  => $row->ar_timestamp,
00174             'minor_edit' => $row->ar_minor_edit,
00175             'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
00176             'deleted'    => $row->ar_deleted,
00177             'len'        => $row->ar_len,
00178             'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
00179             'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
00180             'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
00181         );
00182 
00183         if ( !$wgContentHandlerUseDB ) {
00184             unset( $attribs['content_model'] );
00185             unset( $attribs['content_format'] );
00186         }
00187 
00188         if ( !isset( $attribs['title'] )
00189             && isset( $row->ar_namespace )
00190             && isset( $row->ar_title ) ) {
00191 
00192             $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
00193         }
00194 
00195         if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
00196             // Pre-1.5 ar_text row
00197             $attribs['text'] = self::getRevisionText( $row, 'ar_' );
00198             if ( $attribs['text'] === false ) {
00199                 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
00200             }
00201         }
00202         return new self( $attribs );
00203     }
00204 
00211     public static function newFromRow( $row ) {
00212         return new self( $row );
00213     }
00214 
00223     public static function loadFromId( $db, $id ) {
00224         return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
00225     }
00226 
00237     public static function loadFromPageId( $db, $pageid, $id = 0 ) {
00238         $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
00239         if ( $id ) {
00240             $conds['rev_id'] = intval( $id );
00241         } else {
00242             $conds[] = 'rev_id=page_latest';
00243         }
00244         return self::loadFromConds( $db, $conds );
00245     }
00246 
00257     public static function loadFromTitle( $db, $title, $id = 0 ) {
00258         if ( $id ) {
00259             $matchId = intval( $id );
00260         } else {
00261             $matchId = 'page_latest';
00262         }
00263         return self::loadFromConds( $db,
00264             array(
00265                 "rev_id=$matchId",
00266                 'page_namespace' => $title->getNamespace(),
00267                 'page_title' => $title->getDBkey()
00268             )
00269         );
00270     }
00271 
00282     public static function loadFromTimestamp( $db, $title, $timestamp ) {
00283         return self::loadFromConds( $db,
00284             array(
00285                 'rev_timestamp' => $db->timestamp( $timestamp ),
00286                 'page_namespace' => $title->getNamespace(),
00287                 'page_title' => $title->getDBkey()
00288             )
00289         );
00290     }
00291 
00299     private static function newFromConds( $conditions, $flags = 0 ) {
00300         $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00301         $rev = self::loadFromConds( $db, $conditions, $flags );
00302         if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
00303             if ( !( $flags & self::READ_LATEST ) ) {
00304                 $dbw = wfGetDB( DB_MASTER );
00305                 $rev = self::loadFromConds( $dbw, $conditions, $flags );
00306             }
00307         }
00308         if ( $rev ) {
00309             $rev->mQueryFlags = $flags;
00310         }
00311         return $rev;
00312     }
00313 
00323     private static function loadFromConds( $db, $conditions, $flags = 0 ) {
00324         $res = self::fetchFromConds( $db, $conditions, $flags );
00325         if ( $res ) {
00326             $row = $res->fetchObject();
00327             if ( $row ) {
00328                 $ret = new Revision( $row );
00329                 return $ret;
00330             }
00331         }
00332         $ret = null;
00333         return $ret;
00334     }
00335 
00344     public static function fetchRevision( $title ) {
00345         return self::fetchFromConds(
00346             wfGetDB( DB_SLAVE ),
00347             array(
00348                 'rev_id=page_latest',
00349                 'page_namespace' => $title->getNamespace(),
00350                 'page_title' => $title->getDBkey()
00351             )
00352         );
00353     }
00354 
00365     private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
00366         $fields = array_merge(
00367             self::selectFields(),
00368             self::selectPageFields(),
00369             self::selectUserFields()
00370         );
00371         $options = array( 'LIMIT' => 1 );
00372         if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
00373             $options[] = 'FOR UPDATE';
00374         }
00375         return $db->select(
00376             array( 'revision', 'page', 'user' ),
00377             $fields,
00378             $conditions,
00379             __METHOD__,
00380             $options,
00381             array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
00382         );
00383     }
00384 
00391     public static function userJoinCond() {
00392         return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
00393     }
00394 
00401     public static function pageJoinCond() {
00402         return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
00403     }
00404 
00410     public static function selectFields() {
00411         global $wgContentHandlerUseDB;
00412 
00413         $fields = array(
00414             'rev_id',
00415             'rev_page',
00416             'rev_text_id',
00417             'rev_timestamp',
00418             'rev_comment',
00419             'rev_user_text',
00420             'rev_user',
00421             'rev_minor_edit',
00422             'rev_deleted',
00423             'rev_len',
00424             'rev_parent_id',
00425             'rev_sha1',
00426         );
00427 
00428         if ( $wgContentHandlerUseDB ) {
00429             $fields[] = 'rev_content_format';
00430             $fields[] = 'rev_content_model';
00431         }
00432 
00433         return $fields;
00434     }
00435 
00441     public static function selectTextFields() {
00442         return array(
00443             'old_text',
00444             'old_flags'
00445         );
00446     }
00447 
00452     public static function selectPageFields() {
00453         return array(
00454             'page_namespace',
00455             'page_title',
00456             'page_id',
00457             'page_latest',
00458             'page_is_redirect',
00459             'page_len',
00460         );
00461     }
00462 
00467     public static function selectUserFields() {
00468         return array( 'user_name' );
00469     }
00470 
00477     public static function getParentLengths( $db, array $revIds ) {
00478         $revLens = array();
00479         if ( !$revIds ) {
00480             return $revLens; // empty
00481         }
00482         wfProfileIn( __METHOD__ );
00483         $res = $db->select( 'revision',
00484             array( 'rev_id', 'rev_len' ),
00485             array( 'rev_id' => $revIds ),
00486             __METHOD__ );
00487         foreach ( $res as $row ) {
00488             $revLens[$row->rev_id] = $row->rev_len;
00489         }
00490         wfProfileOut( __METHOD__ );
00491         return $revLens;
00492     }
00493 
00501     function __construct( $row ) {
00502         if ( is_object( $row ) ) {
00503             $this->mId        = intval( $row->rev_id );
00504             $this->mPage      = intval( $row->rev_page );
00505             $this->mTextId    = intval( $row->rev_text_id );
00506             $this->mComment   =         $row->rev_comment;
00507             $this->mUser      = intval( $row->rev_user );
00508             $this->mMinorEdit = intval( $row->rev_minor_edit );
00509             $this->mTimestamp =         $row->rev_timestamp;
00510             $this->mDeleted   = intval( $row->rev_deleted );
00511 
00512             if ( !isset( $row->rev_parent_id ) ) {
00513                 $this->mParentId = null;
00514             } else {
00515                 $this->mParentId = intval( $row->rev_parent_id );
00516             }
00517 
00518             if ( !isset( $row->rev_len ) ) {
00519                 $this->mSize = null;
00520             } else {
00521                 $this->mSize = intval( $row->rev_len );
00522             }
00523 
00524             if ( !isset( $row->rev_sha1 ) ) {
00525                 $this->mSha1 = null;
00526             } else {
00527                 $this->mSha1 = $row->rev_sha1;
00528             }
00529 
00530             if ( isset( $row->page_latest ) ) {
00531                 $this->mCurrent = ( $row->rev_id == $row->page_latest );
00532                 $this->mTitle = Title::newFromRow( $row );
00533             } else {
00534                 $this->mCurrent = false;
00535                 $this->mTitle = null;
00536             }
00537 
00538             if ( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
00539                 $this->mContentModel = null; # determine on demand if needed
00540             } else {
00541                 $this->mContentModel = strval( $row->rev_content_model );
00542             }
00543 
00544             if ( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
00545                 $this->mContentFormat = null; # determine on demand if needed
00546             } else {
00547                 $this->mContentFormat = strval( $row->rev_content_format );
00548             }
00549 
00550             // Lazy extraction...
00551             $this->mText = null;
00552             if ( isset( $row->old_text ) ) {
00553                 $this->mTextRow = $row;
00554             } else {
00555                 // 'text' table row entry will be lazy-loaded
00556                 $this->mTextRow = null;
00557             }
00558 
00559             // Use user_name for users and rev_user_text for IPs...
00560             $this->mUserText = null; // lazy load if left null
00561             if ( $this->mUser == 0 ) {
00562                 $this->mUserText = $row->rev_user_text; // IP user
00563             } elseif ( isset( $row->user_name ) ) {
00564                 $this->mUserText = $row->user_name; // logged-in user
00565             }
00566             $this->mOrigUserText = $row->rev_user_text;
00567         } elseif ( is_array( $row ) ) {
00568             // Build a new revision to be saved...
00569             global $wgUser; // ugh
00570 
00571             # if we have a content object, use it to set the model and type
00572             if ( !empty( $row['content'] ) ) {
00573                 // @todo when is that set? test with external store setup! check out insertOn() [dk]
00574                 if ( !empty( $row['text_id'] ) ) {
00575                     throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
00576                         "can't serialize content object" );
00577                 }
00578 
00579                 $row['content_model'] = $row['content']->getModel();
00580                 # note: mContentFormat is initializes later accordingly
00581                 # note: content is serialized later in this method!
00582                 # also set text to null?
00583             }
00584 
00585             $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
00586             $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
00587             $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
00588             $this->mUserText  = isset( $row['user_text']  ) ? strval( $row['user_text']  ) : $wgUser->getName();
00589             $this->mUser      = isset( $row['user']       ) ? intval( $row['user']       ) : $wgUser->getId();
00590             $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
00591             $this->mTimestamp = isset( $row['timestamp']  ) ? strval( $row['timestamp']  ) : wfTimestampNow();
00592             $this->mDeleted   = isset( $row['deleted']    ) ? intval( $row['deleted']    ) : 0;
00593             $this->mSize      = isset( $row['len']        ) ? intval( $row['len']        ) : null;
00594             $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
00595             $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
00596 
00597             $this->mContentModel   = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
00598             $this->mContentFormat  = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
00599 
00600             // Enforce spacing trimming on supplied text
00601             $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
00602             $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
00603             $this->mTextRow   = null;
00604 
00605             $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
00606 
00607             // if we have a Content object, override mText and mContentModel
00608             if ( !empty( $row['content'] ) ) {
00609                 if ( !( $row['content'] instanceof Content ) ) {
00610                     throw new MWException( '`content` field must contain a Content object.' );
00611                 }
00612 
00613                 $handler = $this->getContentHandler();
00614                 $this->mContent = $row['content'];
00615 
00616                 $this->mContentModel = $this->mContent->getModel();
00617                 $this->mContentHandler = null;
00618 
00619                 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
00620             } elseif ( !is_null( $this->mText ) ) {
00621                 $handler = $this->getContentHandler();
00622                 $this->mContent = $handler->unserializeContent( $this->mText );
00623             }
00624 
00625             // If we have a Title object, make sure it is consistent with mPage.
00626             if ( $this->mTitle && $this->mTitle->exists() ) {
00627                 if ( $this->mPage === null ) {
00628                     // if the page ID wasn't known, set it now
00629                     $this->mPage = $this->mTitle->getArticleID();
00630                 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
00631                     // Got different page IDs. This may be legit (e.g. during undeletion),
00632                     // but it seems worth mentioning it in the log.
00633                     wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
00634                         $this->mTitle->getArticleID() . " provided by the Title object." );
00635                 }
00636             }
00637 
00638             $this->mCurrent = false;
00639 
00640             // If we still have no length, see it we have the text to figure it out
00641             if ( !$this->mSize ) {
00642                 if ( !is_null( $this->mContent ) ) {
00643                     $this->mSize = $this->mContent->getSize();
00644                 } else {
00645                     #NOTE: this should never happen if we have either text or content object!
00646                     $this->mSize = null;
00647                 }
00648             }
00649 
00650             // Same for sha1
00651             if ( $this->mSha1 === null ) {
00652                 $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
00653             }
00654 
00655             // force lazy init
00656             $this->getContentModel();
00657             $this->getContentFormat();
00658         } else {
00659             throw new MWException( 'Revision constructor passed invalid row format.' );
00660         }
00661         $this->mUnpatrolled = null;
00662     }
00663 
00669     public function getId() {
00670         return $this->mId;
00671     }
00672 
00679     public function setId( $id ) {
00680         $this->mId = $id;
00681     }
00682 
00688     public function getTextId() {
00689         return $this->mTextId;
00690     }
00691 
00697     public function getParentId() {
00698         return $this->mParentId;
00699     }
00700 
00706     public function getSize() {
00707         return $this->mSize;
00708     }
00709 
00715     public function getSha1() {
00716         return $this->mSha1;
00717     }
00718 
00726     public function getTitle() {
00727         if ( isset( $this->mTitle ) ) {
00728             return $this->mTitle;
00729         }
00730         if ( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
00731             $dbr = wfGetDB( DB_SLAVE );
00732             $row = $dbr->selectRow(
00733                 array( 'page', 'revision' ),
00734                 self::selectPageFields(),
00735                 array( 'page_id=rev_page',
00736                     'rev_id' => $this->mId ),
00737                 __METHOD__ );
00738             if ( $row ) {
00739                 $this->mTitle = Title::newFromRow( $row );
00740             }
00741         }
00742 
00743         if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
00744             $this->mTitle = Title::newFromID( $this->mPage );
00745         }
00746 
00747         return $this->mTitle;
00748     }
00749 
00755     public function setTitle( $title ) {
00756         $this->mTitle = $title;
00757     }
00758 
00764     public function getPage() {
00765         return $this->mPage;
00766     }
00767 
00781     public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
00782         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00783             return 0;
00784         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00785             return 0;
00786         } else {
00787             return $this->mUser;
00788         }
00789     }
00790 
00796     public function getRawUser() {
00797         return $this->mUser;
00798     }
00799 
00813     public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
00814         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00815             return '';
00816         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00817             return '';
00818         } else {
00819             return $this->getRawUserText();
00820         }
00821     }
00822 
00828     public function getRawUserText() {
00829         if ( $this->mUserText === null ) {
00830             $this->mUserText = User::whoIs( $this->mUser ); // load on demand
00831             if ( $this->mUserText === false ) {
00832                 # This shouldn't happen, but it can if the wiki was recovered
00833                 # via importing revs and there is no user table entry yet.
00834                 $this->mUserText = $this->mOrigUserText;
00835             }
00836         }
00837         return $this->mUserText;
00838     }
00839 
00853     function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
00854         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
00855             return '';
00856         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
00857             return '';
00858         } else {
00859             return $this->mComment;
00860         }
00861     }
00862 
00868     public function getRawComment() {
00869         return $this->mComment;
00870     }
00871 
00875     public function isMinor() {
00876         return (bool)$this->mMinorEdit;
00877     }
00878 
00882     public function isUnpatrolled() {
00883         if ( $this->mUnpatrolled !== null ) {
00884             return $this->mUnpatrolled;
00885         }
00886         $rc = $this->getRecentChange();
00887         if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
00888             $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
00889         } else {
00890             $this->mUnpatrolled = 0;
00891         }
00892         return $this->mUnpatrolled;
00893     }
00894 
00901     public function getRecentChange() {
00902         $dbr = wfGetDB( DB_SLAVE );
00903         return RecentChange::newFromConds(
00904             array(
00905                 'rc_user_text' => $this->getRawUserText(),
00906                 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
00907                 'rc_this_oldid' => $this->getId()
00908             ),
00909             __METHOD__
00910         );
00911     }
00912 
00918     public function isDeleted( $field ) {
00919         return ( $this->mDeleted & $field ) == $field;
00920     }
00921 
00927     public function getVisibility() {
00928         return (int)$this->mDeleted;
00929     }
00930 
00947     public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
00948         ContentHandler::deprecated( __METHOD__, '1.21' );
00949 
00950         $content = $this->getContent( $audience, $user );
00951         return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
00952     }
00953 
00968     public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
00969         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
00970             return null;
00971         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
00972             return null;
00973         } else {
00974             return $this->getContentInternal();
00975         }
00976     }
00977 
00984     public function revText() {
00985         wfDeprecated( __METHOD__, '1.17' );
00986         return $this->getText( self::FOR_THIS_USER );
00987     }
00988 
00997     public function getRawText() {
00998         ContentHandler::deprecated( __METHOD__, "1.21" );
00999         return $this->getText( self::RAW );
01000     }
01001 
01008     public function getSerializedData() {
01009         if ( is_null( $this->mText ) ) {
01010             $this->mText = $this->loadText();
01011         }
01012 
01013         return $this->mText;
01014     }
01015 
01025     protected function getContentInternal() {
01026         if ( is_null( $this->mContent ) ) {
01027             // Revision is immutable. Load on demand:
01028             if ( is_null( $this->mText ) ) {
01029                 $this->mText = $this->loadText();
01030             }
01031 
01032             if ( $this->mText !== null && $this->mText !== false ) {
01033                 // Unserialize content
01034                 $handler = $this->getContentHandler();
01035                 $format = $this->getContentFormat();
01036 
01037                 $this->mContent = $handler->unserializeContent( $this->mText, $format );
01038             } else {
01039                 $this->mContent = false; // negative caching!
01040             }
01041         }
01042 
01043         // NOTE: copy() will return $this for immutable content objects
01044         return $this->mContent ? $this->mContent->copy() : null;
01045     }
01046 
01056     public function getContentModel() {
01057         if ( !$this->mContentModel ) {
01058             $title = $this->getTitle();
01059             $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
01060 
01061             assert( !empty( $this->mContentModel ) );
01062         }
01063 
01064         return $this->mContentModel;
01065     }
01066 
01075     public function getContentFormat() {
01076         if ( !$this->mContentFormat ) {
01077             $handler = $this->getContentHandler();
01078             $this->mContentFormat = $handler->getDefaultFormat();
01079 
01080             assert( !empty( $this->mContentFormat ) );
01081         }
01082 
01083         return $this->mContentFormat;
01084     }
01085 
01092     public function getContentHandler() {
01093         if ( !$this->mContentHandler ) {
01094             $model = $this->getContentModel();
01095             $this->mContentHandler = ContentHandler::getForModelID( $model );
01096 
01097             $format = $this->getContentFormat();
01098 
01099             if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
01100                 throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
01101             }
01102         }
01103 
01104         return $this->mContentHandler;
01105     }
01106 
01110     public function getTimestamp() {
01111         return wfTimestamp( TS_MW, $this->mTimestamp );
01112     }
01113 
01117     public function isCurrent() {
01118         return $this->mCurrent;
01119     }
01120 
01126     public function getPrevious() {
01127         if ( $this->getTitle() ) {
01128             $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
01129             if ( $prev ) {
01130                 return self::newFromTitle( $this->getTitle(), $prev );
01131             }
01132         }
01133         return null;
01134     }
01135 
01141     public function getNext() {
01142         if ( $this->getTitle() ) {
01143             $next = $this->getTitle()->getNextRevisionID( $this->getId() );
01144             if ( $next ) {
01145                 return self::newFromTitle( $this->getTitle(), $next );
01146             }
01147         }
01148         return null;
01149     }
01150 
01158     private function getPreviousRevisionId( $db ) {
01159         if ( is_null( $this->mPage ) ) {
01160             return 0;
01161         }
01162         # Use page_latest if ID is not given
01163         if ( !$this->mId ) {
01164             $prevId = $db->selectField( 'page', 'page_latest',
01165                 array( 'page_id' => $this->mPage ),
01166                 __METHOD__ );
01167         } else {
01168             $prevId = $db->selectField( 'revision', 'rev_id',
01169                 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
01170                 __METHOD__,
01171                 array( 'ORDER BY' => 'rev_id DESC' ) );
01172         }
01173         return intval( $prevId );
01174     }
01175 
01189     public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
01190         wfProfileIn( __METHOD__ );
01191 
01192         # Get data
01193         $textField = $prefix . 'text';
01194         $flagsField = $prefix . 'flags';
01195 
01196         if ( isset( $row->$flagsField ) ) {
01197             $flags = explode( ',', $row->$flagsField );
01198         } else {
01199             $flags = array();
01200         }
01201 
01202         if ( isset( $row->$textField ) ) {
01203             $text = $row->$textField;
01204         } else {
01205             wfProfileOut( __METHOD__ );
01206             return false;
01207         }
01208 
01209         # Use external methods for external objects, text in table is URL-only then
01210         if ( in_array( 'external', $flags ) ) {
01211             $url = $text;
01212             $parts = explode( '://', $url, 2 );
01213             if ( count( $parts ) == 1 || $parts[1] == '' ) {
01214                 wfProfileOut( __METHOD__ );
01215                 return false;
01216             }
01217             $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) );
01218         }
01219 
01220         // If the text was fetched without an error, convert it
01221         if ( $text !== false ) {
01222             $text = self::decompressRevisionText( $text, $flags );
01223         }
01224         wfProfileOut( __METHOD__ );
01225         return $text;
01226     }
01227 
01238     public static function compressRevisionText( &$text ) {
01239         global $wgCompressRevisions;
01240         $flags = array();
01241 
01242         # Revisions not marked this way will be converted
01243         # on load if $wgLegacyCharset is set in the future.
01244         $flags[] = 'utf-8';
01245 
01246         if ( $wgCompressRevisions ) {
01247             if ( function_exists( 'gzdeflate' ) ) {
01248                 $text = gzdeflate( $text );
01249                 $flags[] = 'gzip';
01250             } else {
01251                 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
01252             }
01253         }
01254         return implode( ',', $flags );
01255     }
01256 
01264     public static function decompressRevisionText( $text, $flags ) {
01265         if ( in_array( 'gzip', $flags ) ) {
01266             # Deal with optional compression of archived pages.
01267             # This can be done periodically via maintenance/compressOld.php, and
01268             # as pages are saved if $wgCompressRevisions is set.
01269             $text = gzinflate( $text );
01270         }
01271 
01272         if ( in_array( 'object', $flags ) ) {
01273             # Generic compressed storage
01274             $obj = unserialize( $text );
01275             if ( !is_object( $obj ) ) {
01276                 // Invalid object
01277                 return false;
01278             }
01279             $text = $obj->getText();
01280         }
01281 
01282         global $wgLegacyEncoding;
01283         if ( $text !== false && $wgLegacyEncoding
01284             && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
01285         {
01286             # Old revisions kept around in a legacy encoding?
01287             # Upconvert on demand.
01288             # ("utf8" checked for compatibility with some broken
01289             #  conversion scripts 2008-12-30)
01290             global $wgContLang;
01291             $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
01292         }
01293 
01294         return $text;
01295     }
01296 
01305     public function insertOn( $dbw ) {
01306         global $wgDefaultExternalStore, $wgContentHandlerUseDB;
01307 
01308         wfProfileIn( __METHOD__ );
01309 
01310         $this->checkContentModel();
01311 
01312         $data = $this->mText;
01313         $flags = self::compressRevisionText( $data );
01314 
01315         # Write to external storage if required
01316         if ( $wgDefaultExternalStore ) {
01317             // Store and get the URL
01318             $data = ExternalStore::insertToDefault( $data );
01319             if ( !$data ) {
01320                 wfProfileOut( __METHOD__ );
01321                 throw new MWException( "Unable to store text to external storage" );
01322             }
01323             if ( $flags ) {
01324                 $flags .= ',';
01325             }
01326             $flags .= 'external';
01327         }
01328 
01329         # Record the text (or external storage URL) to the text table
01330         if ( !isset( $this->mTextId ) ) {
01331             $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
01332             $dbw->insert( 'text',
01333                 array(
01334                     'old_id' => $old_id,
01335                     'old_text' => $data,
01336                     'old_flags' => $flags,
01337                 ), __METHOD__
01338             );
01339             $this->mTextId = $dbw->insertId();
01340         }
01341 
01342         if ( $this->mComment === null ) {
01343             $this->mComment = "";
01344         }
01345 
01346         # Record the edit in revisions
01347         $rev_id = isset( $this->mId )
01348             ? $this->mId
01349             : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
01350         $row = array(
01351             'rev_id'         => $rev_id,
01352             'rev_page'       => $this->mPage,
01353             'rev_text_id'    => $this->mTextId,
01354             'rev_comment'    => $this->mComment,
01355             'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
01356             'rev_user'       => $this->mUser,
01357             'rev_user_text'  => $this->mUserText,
01358             'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
01359             'rev_deleted'    => $this->mDeleted,
01360             'rev_len'        => $this->mSize,
01361             'rev_parent_id'  => is_null( $this->mParentId )
01362                 ? $this->getPreviousRevisionId( $dbw )
01363                 : $this->mParentId,
01364             'rev_sha1'       => is_null( $this->mSha1 )
01365                 ? Revision::base36Sha1( $this->mText )
01366                 : $this->mSha1,
01367         );
01368 
01369         if ( $wgContentHandlerUseDB ) {
01370             //NOTE: Store null for the default model and format, to save space.
01371             //XXX: Makes the DB sensitive to changed defaults. Make this behavior optional? Only in miser mode?
01372 
01373             $model = $this->getContentModel();
01374             $format = $this->getContentFormat();
01375 
01376             $title = $this->getTitle();
01377 
01378             if ( $title === null ) {
01379                 wfProfileOut( __METHOD__ );
01380                 throw new MWException( "Insufficient information to determine the title of the revision's page!" );
01381             }
01382 
01383             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01384             $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
01385 
01386             $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
01387             $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
01388         }
01389 
01390         $dbw->insert( 'revision', $row, __METHOD__ );
01391 
01392         $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
01393 
01394         wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
01395 
01396         wfProfileOut( __METHOD__ );
01397         return $this->mId;
01398     }
01399 
01400     protected function checkContentModel() {
01401         global $wgContentHandlerUseDB;
01402 
01403         $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
01404 
01405         $model = $this->getContentModel();
01406         $format = $this->getContentFormat();
01407         $handler = $this->getContentHandler();
01408 
01409         if ( !$handler->isSupportedFormat( $format ) ) {
01410             $t = $title->getPrefixedDBkey();
01411 
01412             throw new MWException( "Can't use format $format with content model $model on $t" );
01413         }
01414 
01415         if ( !$wgContentHandlerUseDB && $title ) {
01416             // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
01417 
01418             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01419             $defaultHandler = ContentHandler::getForModelID( $defaultModel );
01420             $defaultFormat = $defaultHandler->getDefaultFormat();
01421 
01422             if ( $this->getContentModel() != $defaultModel ) {
01423                 $t = $title->getPrefixedDBkey();
01424 
01425                 throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
01426                                         . "model is $model , default for $t is $defaultModel" );
01427             }
01428 
01429             if ( $this->getContentFormat() != $defaultFormat ) {
01430                 $t = $title->getPrefixedDBkey();
01431 
01432                 throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
01433                                         . "format is $format, default for $t is $defaultFormat" );
01434             }
01435         }
01436 
01437         $content = $this->getContent( Revision::RAW );
01438 
01439         if ( !$content || !$content->isValid() ) {
01440             $t = $title->getPrefixedDBkey();
01441 
01442             throw new MWException( "Content of $t is not valid! Content model is $model" );
01443         }
01444     }
01445 
01451     public static function base36Sha1( $text ) {
01452         return wfBaseConvert( sha1( $text ), 16, 36, 31 );
01453     }
01454 
01461     protected function loadText() {
01462         wfProfileIn( __METHOD__ );
01463 
01464         // Caching may be beneficial for massive use of external storage
01465         global $wgRevisionCacheExpiry, $wgMemc;
01466         $textId = $this->getTextId();
01467         $key = wfMemcKey( 'revisiontext', 'textid', $textId );
01468         if ( $wgRevisionCacheExpiry ) {
01469             $text = $wgMemc->get( $key );
01470             if ( is_string( $text ) ) {
01471                 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
01472                 wfProfileOut( __METHOD__ );
01473                 return $text;
01474             }
01475         }
01476 
01477         // If we kept data for lazy extraction, use it now...
01478         if ( isset( $this->mTextRow ) ) {
01479             $row = $this->mTextRow;
01480             $this->mTextRow = null;
01481         } else {
01482             $row = null;
01483         }
01484 
01485         if ( !$row ) {
01486             // Text data is immutable; check slaves first.
01487             $dbr = wfGetDB( DB_SLAVE );
01488             $row = $dbr->selectRow( 'text',
01489                 array( 'old_text', 'old_flags' ),
01490                 array( 'old_id' => $textId ),
01491                 __METHOD__ );
01492         }
01493 
01494         // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was
01495         // used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
01496         $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
01497         if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) {
01498             $dbw = wfGetDB( DB_MASTER );
01499             $row = $dbw->selectRow( 'text',
01500                 array( 'old_text', 'old_flags' ),
01501                 array( 'old_id' => $textId ),
01502                 __METHOD__,
01503                 $forUpdate ? array( 'FOR UPDATE' ) : array() );
01504         }
01505 
01506         if ( !$row ) {
01507             wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
01508         }
01509 
01510         $text = self::getRevisionText( $row );
01511         if ( $row && $text === false ) {
01512             wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
01513         }
01514 
01515         # No negative caching -- negative hits on text rows may be due to corrupted slave servers
01516         if ( $wgRevisionCacheExpiry && $text !== false ) {
01517             $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
01518         }
01519 
01520         wfProfileOut( __METHOD__ );
01521 
01522         return $text;
01523     }
01524 
01539     public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
01540         global $wgContentHandlerUseDB;
01541 
01542         wfProfileIn( __METHOD__ );
01543 
01544         $fields = array( 'page_latest', 'page_namespace', 'page_title',
01545                         'rev_text_id', 'rev_len', 'rev_sha1' );
01546 
01547         if ( $wgContentHandlerUseDB ) {
01548             $fields[] = 'rev_content_model';
01549             $fields[] = 'rev_content_format';
01550         }
01551 
01552         $current = $dbw->selectRow(
01553             array( 'page', 'revision' ),
01554             $fields,
01555             array(
01556                 'page_id' => $pageId,
01557                 'page_latest=rev_id',
01558                 ),
01559             __METHOD__ );
01560 
01561         if ( $current ) {
01562             $row = array(
01563                 'page'       => $pageId,
01564                 'comment'    => $summary,
01565                 'minor_edit' => $minor,
01566                 'text_id'    => $current->rev_text_id,
01567                 'parent_id'  => $current->page_latest,
01568                 'len'        => $current->rev_len,
01569                 'sha1'       => $current->rev_sha1
01570             );
01571 
01572             if ( $wgContentHandlerUseDB ) {
01573                 $row['content_model'] = $current->rev_content_model;
01574                 $row['content_format'] = $current->rev_content_format;
01575             }
01576 
01577             $revision = new Revision( $row );
01578             $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
01579         } else {
01580             $revision = null;
01581         }
01582 
01583         wfProfileOut( __METHOD__ );
01584         return $revision;
01585     }
01586 
01597     public function userCan( $field, User $user = null ) {
01598         return self::userCanBitfield( $this->mDeleted, $field, $user );
01599     }
01600 
01613     public static function userCanBitfield( $bitfield, $field, User $user = null ) {
01614         if ( $bitfield & $field ) { // aspect is deleted
01615             if ( $bitfield & self::DELETED_RESTRICTED ) {
01616                 $permission = 'suppressrevision';
01617             } elseif ( $field & self::DELETED_TEXT ) {
01618                 $permission = 'deletedtext';
01619             } else {
01620                 $permission = 'deletedhistory';
01621             }
01622             wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
01623             if ( $user === null ) {
01624                 global $wgUser;
01625                 $user = $wgUser;
01626             }
01627             return $user->isAllowed( $permission );
01628         } else {
01629             return true;
01630         }
01631     }
01632 
01640     static function getTimestampFromId( $title, $id ) {
01641         $dbr = wfGetDB( DB_SLAVE );
01642         // Casting fix for databases that can't take '' for rev_id
01643         if ( $id == '' ) {
01644             $id = 0;
01645         }
01646         $conds = array( 'rev_id' => $id );
01647         $conds['rev_page'] = $title->getArticleID();
01648         $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01649         if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
01650             # Not in slave, try master
01651             $dbw = wfGetDB( DB_MASTER );
01652             $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01653         }
01654         return wfTimestamp( TS_MW, $timestamp );
01655     }
01656 
01664     static function countByPageId( $db, $id ) {
01665         $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
01666             array( 'rev_page' => $id ), __METHOD__ );
01667         if ( $row ) {
01668             return $row->revCount;
01669         }
01670         return 0;
01671     }
01672 
01680     static function countByTitle( $db, $title ) {
01681         $id = $title->getArticleID();
01682         if ( $id ) {
01683             return self::countByPageId( $db, $id );
01684         }
01685         return 0;
01686     }
01687 
01703     public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
01704         if ( !$userId ) {
01705             return false;
01706         }
01707 
01708         if ( is_int( $db ) ) {
01709             $db = wfGetDB( $db );
01710         }
01711 
01712         $res = $db->select( 'revision',
01713             'rev_user',
01714             array(
01715                 'rev_page' => $pageId,
01716                 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
01717             ),
01718             __METHOD__,
01719             array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
01720         foreach ( $res as $row ) {
01721             if ( $row->rev_user != $userId ) {
01722                 return false;
01723             }
01724         }
01725         return true;
01726     }
01727 }