MediaWiki  master
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 
00064         // Revision deletion constants
00065         const DELETED_TEXT = 1;
00066         const DELETED_COMMENT = 2;
00067         const DELETED_USER = 4;
00068         const DELETED_RESTRICTED = 8;
00069         const SUPPRESSED_USER = 12; // convenience
00070 
00071         // Audience options for accessors
00072         const FOR_PUBLIC = 1;
00073         const FOR_THIS_USER = 2;
00074         const RAW = 3;
00075 
00088         public static function newFromId( $id, $flags = 0 ) {
00089                 return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
00090         }
00091 
00106         public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
00107                 $conds = array(
00108                         'page_namespace' => $title->getNamespace(),
00109                         'page_title'     => $title->getDBkey()
00110                 );
00111                 if ( $id ) {
00112                         // Use the specified ID
00113                         $conds['rev_id'] = $id;
00114                 } else {
00115                         // Use a join to get the latest revision
00116                         $conds[] = 'rev_id=page_latest';
00117                 }
00118                 return self::newFromConds( $conds, (int)$flags );
00119         }
00120 
00135         public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
00136                 $conds = array( 'page_id' => $pageId );
00137                 if ( $revId ) {
00138                         $conds['rev_id'] = $revId;
00139                 } else {
00140                         // Use a join to get the latest revision
00141                         $conds[] = 'rev_id = page_latest';
00142                 }
00143                 return self::newFromConds( $conds, (int)$flags );
00144         }
00145 
00157         public static function newFromArchiveRow( $row, $overrides = array() ) {
00158                 global $wgContentHandlerUseDB;
00159 
00160                 $attribs = $overrides + array(
00161                         'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
00162                         'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
00163                         'comment'    => $row->ar_comment,
00164                         'user'       => $row->ar_user,
00165                         'user_text'  => $row->ar_user_text,
00166                         'timestamp'  => $row->ar_timestamp,
00167                         'minor_edit' => $row->ar_minor_edit,
00168                         'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
00169                         'deleted'    => $row->ar_deleted,
00170                         'len'        => $row->ar_len,
00171                         'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
00172                         'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
00173                         'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
00174                 );
00175 
00176                 if ( !$wgContentHandlerUseDB ) {
00177                         unset( $attribs['content_model'] );
00178                         unset( $attribs['content_format'] );
00179                 }
00180 
00181                 if ( !isset( $attribs['title'] )
00182                         && isset( $row->ar_namespace )
00183                         && isset( $row->ar_title ) ) {
00184 
00185                         $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
00186                 }
00187 
00188                 if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
00189                         // Pre-1.5 ar_text row
00190                         $attribs['text'] = self::getRevisionText( $row, 'ar_' );
00191                         if ( $attribs['text'] === false ) {
00192                                 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
00193                         }
00194                 }
00195                 return new self( $attribs );
00196         }
00197 
00204         public static function newFromRow( $row ) {
00205                 return new self( $row );
00206         }
00207 
00216         public static function loadFromId( $db, $id ) {
00217                 return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
00218         }
00219 
00230         public static function loadFromPageId( $db, $pageid, $id = 0 ) {
00231                 $conds = array( 'rev_page' => intval( $pageid ), 'page_id'  => intval( $pageid ) );
00232                 if( $id ) {
00233                         $conds['rev_id'] = intval( $id );
00234                 } else {
00235                         $conds[] = 'rev_id=page_latest';
00236                 }
00237                 return self::loadFromConds( $db, $conds );
00238         }
00239 
00250         public static function loadFromTitle( $db, $title, $id = 0 ) {
00251                 if( $id ) {
00252                         $matchId = intval( $id );
00253                 } else {
00254                         $matchId = 'page_latest';
00255                 }
00256                 return self::loadFromConds( $db,
00257                         array( "rev_id=$matchId",
00258                                    'page_namespace' => $title->getNamespace(),
00259                                    'page_title'     => $title->getDBkey() )
00260                 );
00261         }
00262 
00273         public static function loadFromTimestamp( $db, $title, $timestamp ) {
00274                 return self::loadFromConds( $db,
00275                         array( 'rev_timestamp'  => $db->timestamp( $timestamp ),
00276                                    'page_namespace' => $title->getNamespace(),
00277                                    'page_title'     => $title->getDBkey() )
00278                 );
00279         }
00280 
00288         private static function newFromConds( $conditions, $flags = 0 ) {
00289                 $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00290                 $rev = self::loadFromConds( $db, $conditions, $flags );
00291                 if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
00292                         if ( !( $flags & self::READ_LATEST ) ) {
00293                                 $dbw = wfGetDB( DB_MASTER );
00294                                 $rev = self::loadFromConds( $dbw, $conditions, $flags );
00295                         }
00296                 }
00297                 return $rev;
00298         }
00299 
00309         private static function loadFromConds( $db, $conditions, $flags = 0 ) {
00310                 $res = self::fetchFromConds( $db, $conditions, $flags );
00311                 if( $res ) {
00312                         $row = $res->fetchObject();
00313                         if( $row ) {
00314                                 $ret = new Revision( $row );
00315                                 return $ret;
00316                         }
00317                 }
00318                 $ret = null;
00319                 return $ret;
00320         }
00321 
00330         public static function fetchRevision( $title ) {
00331                 return self::fetchFromConds(
00332                         wfGetDB( DB_SLAVE ),
00333                         array( 'rev_id=page_latest',
00334                                    'page_namespace' => $title->getNamespace(),
00335                                    'page_title'     => $title->getDBkey() )
00336                 );
00337         }
00338 
00349         private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
00350                 $fields = array_merge(
00351                         self::selectFields(),
00352                         self::selectPageFields(),
00353                         self::selectUserFields()
00354                 );
00355                 $options = array( 'LIMIT' => 1 );
00356                 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
00357                         $options[] = 'FOR UPDATE';
00358                 }
00359                 return $db->select(
00360                         array( 'revision', 'page', 'user' ),
00361                         $fields,
00362                         $conditions,
00363                         __METHOD__,
00364                         $options,
00365                         array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
00366                 );
00367         }
00368 
00375         public static function userJoinCond() {
00376                 return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
00377         }
00378 
00385         public static function pageJoinCond() {
00386                 return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
00387         }
00388 
00394         public static function selectFields() {
00395                 global $wgContentHandlerUseDB;
00396 
00397                 $fields = array(
00398                         'rev_id',
00399                         'rev_page',
00400                         'rev_text_id',
00401                         'rev_timestamp',
00402                         'rev_comment',
00403                         'rev_user_text',
00404                         'rev_user',
00405                         'rev_minor_edit',
00406                         'rev_deleted',
00407                         'rev_len',
00408                         'rev_parent_id',
00409                         'rev_sha1',
00410                 );
00411 
00412                 if ( $wgContentHandlerUseDB ) {
00413                         $fields[] = 'rev_content_format';
00414                         $fields[] = 'rev_content_model';
00415                 }
00416 
00417                 return $fields;
00418         }
00419 
00425         public static function selectTextFields() {
00426                 return array(
00427                         'old_text',
00428                         'old_flags'
00429                 );
00430         }
00431 
00436         public static function selectPageFields() {
00437                 return array(
00438                         'page_namespace',
00439                         'page_title',
00440                         'page_id',
00441                         'page_latest',
00442                         'page_is_redirect',
00443                         'page_len',
00444                 );
00445         }
00446 
00451         public static function selectUserFields() {
00452                 return array( 'user_name' );
00453         }
00454 
00461         public static function getParentLengths( $db, array $revIds ) {
00462                 $revLens = array();
00463                 if ( !$revIds ) {
00464                         return $revLens; // empty
00465                 }
00466                 wfProfileIn( __METHOD__ );
00467                 $res = $db->select( 'revision',
00468                         array( 'rev_id', 'rev_len' ),
00469                         array( 'rev_id' => $revIds ),
00470                         __METHOD__ );
00471                 foreach ( $res as $row ) {
00472                         $revLens[$row->rev_id] = $row->rev_len;
00473                 }
00474                 wfProfileOut( __METHOD__ );
00475                 return $revLens;
00476         }
00477 
00485         function __construct( $row ) {
00486                 if( is_object( $row ) ) {
00487                         $this->mId        = intval( $row->rev_id );
00488                         $this->mPage      = intval( $row->rev_page );
00489                         $this->mTextId    = intval( $row->rev_text_id );
00490                         $this->mComment   =         $row->rev_comment;
00491                         $this->mUser      = intval( $row->rev_user );
00492                         $this->mMinorEdit = intval( $row->rev_minor_edit );
00493                         $this->mTimestamp =         $row->rev_timestamp;
00494                         $this->mDeleted   = intval( $row->rev_deleted );
00495 
00496                         if( !isset( $row->rev_parent_id ) ) {
00497                                 $this->mParentId = is_null( $row->rev_parent_id ) ? null : 0;
00498                         } else {
00499                                 $this->mParentId  = intval( $row->rev_parent_id );
00500                         }
00501 
00502                         if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) {
00503                                 $this->mSize = null;
00504                         } else {
00505                                 $this->mSize = intval( $row->rev_len );
00506                         }
00507 
00508                         if ( !isset( $row->rev_sha1 ) ) {
00509                                 $this->mSha1 = null;
00510                         } else {
00511                                 $this->mSha1 = $row->rev_sha1;
00512                         }
00513 
00514                         if( isset( $row->page_latest ) ) {
00515                                 $this->mCurrent = ( $row->rev_id == $row->page_latest );
00516                                 $this->mTitle = Title::newFromRow( $row );
00517                         } else {
00518                                 $this->mCurrent = false;
00519                                 $this->mTitle = null;
00520                         }
00521 
00522                         if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
00523                                 $this->mContentModel = null; # determine on demand if needed
00524                         } else {
00525                                 $this->mContentModel = strval( $row->rev_content_model );
00526                         }
00527 
00528                         if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
00529                                 $this->mContentFormat = null; # determine on demand if needed
00530                         } else {
00531                                 $this->mContentFormat = strval( $row->rev_content_format );
00532                         }
00533 
00534                         // Lazy extraction...
00535                         $this->mText      = null;
00536                         if( isset( $row->old_text ) ) {
00537                                 $this->mTextRow = $row;
00538                         } else {
00539                                 // 'text' table row entry will be lazy-loaded
00540                                 $this->mTextRow = null;
00541                         }
00542 
00543                         // Use user_name for users and rev_user_text for IPs...
00544                         $this->mUserText = null; // lazy load if left null
00545                         if ( $this->mUser == 0 ) {
00546                                 $this->mUserText = $row->rev_user_text; // IP user
00547                         } elseif ( isset( $row->user_name ) ) {
00548                                 $this->mUserText = $row->user_name; // logged-in user
00549                         }
00550                         $this->mOrigUserText = $row->rev_user_text;
00551                 } elseif( is_array( $row ) ) {
00552                         // Build a new revision to be saved...
00553                         global $wgUser; // ugh
00554 
00555 
00556                         # if we have a content object, use it to set the model and type
00557                         if ( !empty( $row['content'] ) ) {
00558                                 //@todo: when is that set? test with external store setup! check out insertOn() [dk]
00559                                 if ( !empty( $row['text_id'] ) ) {
00560                                         throw new MWException( "Text already stored in external store (id {$row['text_id']}), "
00561                                                                                         . "can't serialize content object" );
00562                                 }
00563 
00564                                 $row['content_model'] = $row['content']->getModel();
00565                                 # note: mContentFormat is initializes later accordingly
00566                                 # note: content is serialized later in this method!
00567                                 # also set text to null?
00568                         }
00569 
00570                         $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
00571                         $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
00572                         $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
00573                         $this->mUserText  = isset( $row['user_text']  ) ? strval( $row['user_text']  ) : $wgUser->getName();
00574                         $this->mUser      = isset( $row['user']       ) ? intval( $row['user']       ) : $wgUser->getId();
00575                         $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
00576                         $this->mTimestamp = isset( $row['timestamp']  ) ? strval( $row['timestamp']  ) : wfTimestampNow();
00577                         $this->mDeleted   = isset( $row['deleted']    ) ? intval( $row['deleted']    ) : 0;
00578                         $this->mSize      = isset( $row['len']        ) ? intval( $row['len']        ) : null;
00579                         $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
00580                         $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
00581 
00582                         $this->mContentModel   = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
00583                         $this->mContentFormat  = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
00584 
00585                         // Enforce spacing trimming on supplied text
00586                         $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
00587                         $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
00588                         $this->mTextRow   = null;
00589 
00590                         $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
00591 
00592                         // if we have a Content object, override mText and mContentModel
00593                         if ( !empty( $row['content'] ) ) {
00594                                 if ( !( $row['content'] instanceof Content ) ) {
00595                                         throw new MWException( '`content` field must contain a Content object.' );
00596                                 }
00597 
00598                                 $handler = $this->getContentHandler();
00599                                 $this->mContent = $row['content'];
00600 
00601                                 $this->mContentModel = $this->mContent->getModel();
00602                                 $this->mContentHandler = null;
00603 
00604                                 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
00605                         } elseif ( !is_null( $this->mText ) ) {
00606                                 $handler = $this->getContentHandler();
00607                                 $this->mContent = $handler->unserializeContent( $this->mText );
00608                         }
00609 
00610                         // If we have a Title object, make sure it is consistent with mPage.
00611                         if ( $this->mTitle && $this->mTitle->exists() ) {
00612                                 if ( $this->mPage === null ) {
00613                                         // if the page ID wasn't known, set it now
00614                                         $this->mPage = $this->mTitle->getArticleID();
00615                                 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
00616                                         // Got different page IDs. This may be legit (e.g. during undeletion),
00617                                         // but it seems worth mentioning it in the log.
00618                                         wfDebug( "Page ID " . $this->mPage . " mismatches the ID "
00619                                                         . $this->mTitle->getArticleID() . " provided by the Title object." );
00620                                 }
00621                         }
00622 
00623                         $this->mCurrent   = false;
00624 
00625                         // If we still have no length, see it we have the text to figure it out
00626                         if ( !$this->mSize ) {
00627                                 if ( !is_null( $this->mContent ) ) {
00628                                         $this->mSize = $this->mContent->getSize();
00629                                 } else {
00630                                         #NOTE: this should never happen if we have either text or content object!
00631                                         $this->mSize = null;
00632                                 }
00633                         }
00634 
00635                         // Same for sha1
00636                         if ( $this->mSha1 === null ) {
00637                                 $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
00638                         }
00639 
00640                         // force lazy init
00641                         $this->getContentModel();
00642                         $this->getContentFormat();
00643                 } else {
00644                         throw new MWException( 'Revision constructor passed invalid row format.' );
00645                 }
00646                 $this->mUnpatrolled = null;
00647         }
00648 
00654         public function getId() {
00655                 return $this->mId;
00656         }
00657 
00664         public function setId( $id ) {
00665                 $this->mId = $id;
00666         }
00667 
00673         public function getTextId() {
00674                 return $this->mTextId;
00675         }
00676 
00682         public function getParentId() {
00683                 return $this->mParentId;
00684         }
00685 
00691         public function getSize() {
00692                 return $this->mSize;
00693         }
00694 
00700         public function getSha1() {
00701                 return $this->mSha1;
00702         }
00703 
00711         public function getTitle() {
00712                 if( isset( $this->mTitle ) ) {
00713                         return $this->mTitle;
00714                 }
00715                 if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
00716                         $dbr = wfGetDB( DB_SLAVE );
00717                         $row = $dbr->selectRow(
00718                                 array( 'page', 'revision' ),
00719                                 self::selectPageFields(),
00720                                 array( 'page_id=rev_page',
00721                                            'rev_id' => $this->mId ),
00722                                 __METHOD__ );
00723                         if ( $row ) {
00724                                 $this->mTitle = Title::newFromRow( $row );
00725                         }
00726                 }
00727 
00728                 if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
00729                         $this->mTitle = Title::newFromID( $this->mPage );
00730                 }
00731 
00732                 return $this->mTitle;
00733         }
00734 
00740         public function setTitle( $title ) {
00741                 $this->mTitle = $title;
00742         }
00743 
00749         public function getPage() {
00750                 return $this->mPage;
00751         }
00752 
00766         public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
00767                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00768                         return 0;
00769                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00770                         return 0;
00771                 } else {
00772                         return $this->mUser;
00773                 }
00774         }
00775 
00781         public function getRawUser() {
00782                 return $this->mUser;
00783         }
00784 
00798         public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
00799                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00800                         return '';
00801                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00802                         return '';
00803                 } else {
00804                         return $this->getRawUserText();
00805                 }
00806         }
00807 
00813         public function getRawUserText() {
00814                 if ( $this->mUserText === null ) {
00815                         $this->mUserText = User::whoIs( $this->mUser ); // load on demand
00816                         if ( $this->mUserText === false ) {
00817                                 # This shouldn't happen, but it can if the wiki was recovered
00818                                 # via importing revs and there is no user table entry yet.
00819                                 $this->mUserText = $this->mOrigUserText;
00820                         }
00821                 }
00822                 return $this->mUserText;
00823         }
00824 
00838         function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
00839                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
00840                         return '';
00841                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
00842                         return '';
00843                 } else {
00844                         return $this->mComment;
00845                 }
00846         }
00847 
00853         public function getRawComment() {
00854                 return $this->mComment;
00855         }
00856 
00860         public function isMinor() {
00861                 return (bool)$this->mMinorEdit;
00862         }
00863 
00867         public function isUnpatrolled() {
00868                 if( $this->mUnpatrolled !== null ) {
00869                         return $this->mUnpatrolled;
00870                 }
00871                 $dbr = wfGetDB( DB_SLAVE );
00872                 $this->mUnpatrolled = $dbr->selectField( 'recentchanges',
00873                         'rc_id',
00874                         array( // Add redundant user,timestamp condition so we can use the existing index
00875                                 'rc_user_text'  => $this->getRawUserText(),
00876                                 'rc_timestamp'  => $dbr->timestamp( $this->getTimestamp() ),
00877                                 'rc_this_oldid' => $this->getId(),
00878                                 'rc_patrolled'  => 0
00879                         ),
00880                         __METHOD__
00881                 );
00882                 return (int)$this->mUnpatrolled;
00883         }
00884 
00890         public function isDeleted( $field ) {
00891                 return ( $this->mDeleted & $field ) == $field;
00892         }
00893 
00899         public function getVisibility() {
00900                 return (int)$this->mDeleted;
00901         }
00902 
00919         public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
00920                 ContentHandler::deprecated( __METHOD__, '1.21' );
00921 
00922                 $content = $this->getContent( $audience, $user );
00923                 return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
00924         }
00925 
00940         public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
00941                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
00942                         return null;
00943                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
00944                         return null;
00945                 } else {
00946                         return $this->getContentInternal();
00947                 }
00948         }
00949 
00956         public function revText() {
00957                 wfDeprecated( __METHOD__, '1.17' );
00958                 return $this->getText( self::FOR_THIS_USER );
00959         }
00960 
00969         public function getRawText() {
00970                 ContentHandler::deprecated( __METHOD__, "1.21" );
00971                 return $this->getText( self::RAW );
00972         }
00973 
00980         public function getSerializedData() {
00981                 return $this->mText;
00982         }
00983 
00993         protected function getContentInternal() {
00994                 if( is_null( $this->mContent ) ) {
00995                         // Revision is immutable. Load on demand:
00996                         if( is_null( $this->mText ) ) {
00997                                 $this->mText = $this->loadText();
00998                         }
00999 
01000                         if ( $this->mText !== null && $this->mText !== false ) {
01001                                 // Unserialize content
01002                                 $handler = $this->getContentHandler();
01003                                 $format = $this->getContentFormat();
01004 
01005                                 $this->mContent = $handler->unserializeContent( $this->mText, $format );
01006                         } else {
01007                                 $this->mContent = false; // negative caching!
01008                         }
01009                 }
01010 
01011                 // NOTE: copy() will return $this for immutable content objects
01012                 return $this->mContent ? $this->mContent->copy() : null;
01013         }
01014 
01024         public function getContentModel() {
01025                 if ( !$this->mContentModel ) {
01026                         $title = $this->getTitle();
01027                         $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
01028 
01029                         assert( !empty( $this->mContentModel ) );
01030                 }
01031 
01032                 return $this->mContentModel;
01033         }
01034 
01043         public function getContentFormat() {
01044                 if ( !$this->mContentFormat ) {
01045                         $handler = $this->getContentHandler();
01046                         $this->mContentFormat = $handler->getDefaultFormat();
01047 
01048                         assert( !empty( $this->mContentFormat ) );
01049                 }
01050 
01051                 return $this->mContentFormat;
01052         }
01053 
01060         public function getContentHandler() {
01061                 if ( !$this->mContentHandler ) {
01062                         $model = $this->getContentModel();
01063                         $this->mContentHandler = ContentHandler::getForModelID( $model );
01064 
01065                         $format = $this->getContentFormat();
01066 
01067                         if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
01068                                 throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
01069                         }
01070                 }
01071 
01072                 return $this->mContentHandler;
01073         }
01074 
01078         public function getTimestamp() {
01079                 return wfTimestamp( TS_MW, $this->mTimestamp );
01080         }
01081 
01085         public function isCurrent() {
01086                 return $this->mCurrent;
01087         }
01088 
01094         public function getPrevious() {
01095                 if( $this->getTitle() ) {
01096                         $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
01097                         if( $prev ) {
01098                                 return self::newFromTitle( $this->getTitle(), $prev );
01099                         }
01100                 }
01101                 return null;
01102         }
01103 
01109         public function getNext() {
01110                 if( $this->getTitle() ) {
01111                         $next = $this->getTitle()->getNextRevisionID( $this->getId() );
01112                         if ( $next ) {
01113                                 return self::newFromTitle( $this->getTitle(), $next );
01114                         }
01115                 }
01116                 return null;
01117         }
01118 
01126         private function getPreviousRevisionId( $db ) {
01127                 if( is_null( $this->mPage ) ) {
01128                         return 0;
01129                 }
01130                 # Use page_latest if ID is not given
01131                 if( !$this->mId ) {
01132                         $prevId = $db->selectField( 'page', 'page_latest',
01133                                 array( 'page_id' => $this->mPage ),
01134                                 __METHOD__ );
01135                 } else {
01136                         $prevId = $db->selectField( 'revision', 'rev_id',
01137                                 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
01138                                 __METHOD__,
01139                                 array( 'ORDER BY' => 'rev_id DESC' ) );
01140                 }
01141                 return intval( $prevId );
01142         }
01143 
01153         public static function getRevisionText( $row, $prefix = 'old_' ) {
01154                 wfProfileIn( __METHOD__ );
01155 
01156                 # Get data
01157                 $textField = $prefix . 'text';
01158                 $flagsField = $prefix . 'flags';
01159 
01160                 if( isset( $row->$flagsField ) ) {
01161                         $flags = explode( ',', $row->$flagsField );
01162                 } else {
01163                         $flags = array();
01164                 }
01165 
01166                 if( isset( $row->$textField ) ) {
01167                         $text = $row->$textField;
01168                 } else {
01169                         wfProfileOut( __METHOD__ );
01170                         return false;
01171                 }
01172 
01173                 # Use external methods for external objects, text in table is URL-only then
01174                 if ( in_array( 'external', $flags ) ) {
01175                         $url = $text;
01176                         $parts = explode( '://', $url, 2 );
01177                         if( count( $parts ) == 1 || $parts[1] == '' ) {
01178                                 wfProfileOut( __METHOD__ );
01179                                 return false;
01180                         }
01181                         $text = ExternalStore::fetchFromURL( $url );
01182                 }
01183 
01184                 // If the text was fetched without an error, convert it
01185                 if ( $text !== false ) {
01186                         if( in_array( 'gzip', $flags ) ) {
01187                                 # Deal with optional compression of archived pages.
01188                                 # This can be done periodically via maintenance/compressOld.php, and
01189                                 # as pages are saved if $wgCompressRevisions is set.
01190                                 $text = gzinflate( $text );
01191                         }
01192 
01193                         if( in_array( 'object', $flags ) ) {
01194                                 # Generic compressed storage
01195                                 $obj = unserialize( $text );
01196                                 if ( !is_object( $obj ) ) {
01197                                         // Invalid object
01198                                         wfProfileOut( __METHOD__ );
01199                                         return false;
01200                                 }
01201                                 $text = $obj->getText();
01202                         }
01203 
01204                         global $wgLegacyEncoding;
01205                         if( $text !== false && $wgLegacyEncoding
01206                                 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
01207                         {
01208                                 # Old revisions kept around in a legacy encoding?
01209                                 # Upconvert on demand.
01210                                 # ("utf8" checked for compatibility with some broken
01211                                 #  conversion scripts 2008-12-30)
01212                                 global $wgContLang;
01213                                 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
01214                         }
01215                 }
01216                 wfProfileOut( __METHOD__ );
01217                 return $text;
01218         }
01219 
01230         public static function compressRevisionText( &$text ) {
01231                 global $wgCompressRevisions;
01232                 $flags = array();
01233 
01234                 # Revisions not marked this way will be converted
01235                 # on load if $wgLegacyCharset is set in the future.
01236                 $flags[] = 'utf-8';
01237 
01238                 if( $wgCompressRevisions ) {
01239                         if( function_exists( 'gzdeflate' ) ) {
01240                                 $text = gzdeflate( $text );
01241                                 $flags[] = 'gzip';
01242                         } else {
01243                                 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
01244                         }
01245                 }
01246                 return implode( ',', $flags );
01247         }
01248 
01257         public function insertOn( $dbw ) {
01258                 global $wgDefaultExternalStore, $wgContentHandlerUseDB;
01259 
01260                 wfProfileIn( __METHOD__ );
01261 
01262                 $this->checkContentModel();
01263 
01264                 $data = $this->mText;
01265                 $flags = self::compressRevisionText( $data );
01266 
01267                 # Write to external storage if required
01268                 if( $wgDefaultExternalStore ) {
01269                         // Store and get the URL
01270                         $data = ExternalStore::insertToDefault( $data );
01271                         if( !$data ) {
01272                                 throw new MWException( "Unable to store text to external storage" );
01273                         }
01274                         if( $flags ) {
01275                                 $flags .= ',';
01276                         }
01277                         $flags .= 'external';
01278                 }
01279 
01280                 # Record the text (or external storage URL) to the text table
01281                 if( !isset( $this->mTextId ) ) {
01282                         $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
01283                         $dbw->insert( 'text',
01284                                 array(
01285                                         'old_id'    => $old_id,
01286                                         'old_text'  => $data,
01287                                         'old_flags' => $flags,
01288                                 ), __METHOD__
01289                         );
01290                         $this->mTextId = $dbw->insertId();
01291                 }
01292 
01293                 if ( $this->mComment === null ) $this->mComment = "";
01294 
01295                 # Record the edit in revisions
01296                 $rev_id = isset( $this->mId )
01297                         ? $this->mId
01298                         : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
01299                 $row = array(
01300                         'rev_id'         => $rev_id,
01301                         'rev_page'       => $this->mPage,
01302                         'rev_text_id'    => $this->mTextId,
01303                         'rev_comment'    => $this->mComment,
01304                         'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
01305                         'rev_user'       => $this->mUser,
01306                         'rev_user_text'  => $this->mUserText,
01307                         'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
01308                         'rev_deleted'    => $this->mDeleted,
01309                         'rev_len'        => $this->mSize,
01310                         'rev_parent_id'  => is_null( $this->mParentId )
01311                                 ? $this->getPreviousRevisionId( $dbw )
01312                                 : $this->mParentId,
01313                         'rev_sha1'       => is_null( $this->mSha1 )
01314                                 ? Revision::base36Sha1( $this->mText )
01315                                 : $this->mSha1,
01316                 );
01317 
01318                 if ( $wgContentHandlerUseDB ) {
01319                         //NOTE: Store null for the default model and format, to save space.
01320                         //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
01321 
01322                         $model = $this->getContentModel();
01323                         $format = $this->getContentFormat();
01324 
01325                         $title = $this->getTitle();
01326 
01327                         if ( $title === null ) {
01328                                 throw new MWException( "Insufficient information to determine the title of the revision's page!" );
01329                         }
01330 
01331                         $defaultModel = ContentHandler::getDefaultModelFor( $title );
01332                         $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
01333 
01334                         $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
01335                         $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
01336                 }
01337 
01338                 $dbw->insert( 'revision', $row, __METHOD__ );
01339 
01340                 $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
01341 
01342                 wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
01343 
01344                 wfProfileOut( __METHOD__ );
01345                 return $this->mId;
01346         }
01347 
01348         protected function checkContentModel() {
01349                 global $wgContentHandlerUseDB;
01350 
01351                 $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
01352 
01353                 $model = $this->getContentModel();
01354                 $format = $this->getContentFormat();
01355                 $handler = $this->getContentHandler();
01356 
01357                 if ( !$handler->isSupportedFormat( $format ) ) {
01358                         $t = $title->getPrefixedDBkey();
01359 
01360                         throw new MWException( "Can't use format $format with content model $model on $t" );
01361                 }
01362 
01363                 if ( !$wgContentHandlerUseDB && $title ) {
01364                         // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
01365 
01366                         $defaultModel = ContentHandler::getDefaultModelFor( $title );
01367                         $defaultHandler = ContentHandler::getForModelID( $defaultModel );
01368                         $defaultFormat = $defaultHandler->getDefaultFormat();
01369 
01370                         if ( $this->getContentModel() != $defaultModel ) {
01371                                 $t = $title->getPrefixedDBkey();
01372 
01373                                 throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
01374                                                                                 . "model is $model , default for $t is $defaultModel" );
01375                         }
01376 
01377                         if ( $this->getContentFormat() != $defaultFormat ) {
01378                                 $t = $title->getPrefixedDBkey();
01379 
01380                                 throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
01381                                                                                 . "format is $format, default for $t is $defaultFormat" );
01382                         }
01383                 }
01384 
01385                 $content = $this->getContent( Revision::RAW );
01386 
01387                 if ( !$content || !$content->isValid() ) {
01388                         $t = $title->getPrefixedDBkey();
01389 
01390                         throw new MWException( "Content of $t is not valid! Content model is $model" );
01391                 }
01392         }
01393 
01399         public static function base36Sha1( $text ) {
01400                 return wfBaseConvert( sha1( $text ), 16, 36, 31 );
01401         }
01402 
01413         protected function loadText() {
01414                 wfProfileIn( __METHOD__ );
01415 
01416                 // Caching may be beneficial for massive use of external storage
01417                 global $wgRevisionCacheExpiry, $wgMemc;
01418                 $textId = $this->getTextId();
01419                 $key = wfMemcKey( 'revisiontext', 'textid', $textId );
01420                 if( $wgRevisionCacheExpiry ) {
01421                         $text = $wgMemc->get( $key );
01422                         if( is_string( $text ) ) {
01423                                 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
01424                                 wfProfileOut( __METHOD__ );
01425                                 return $text;
01426                         }
01427                 }
01428 
01429                 // If we kept data for lazy extraction, use it now...
01430                 if ( isset( $this->mTextRow ) ) {
01431                         $row = $this->mTextRow;
01432                         $this->mTextRow = null;
01433                 } else {
01434                         $row = null;
01435                 }
01436 
01437                 if( !$row ) {
01438                         // Text data is immutable; check slaves first.
01439                         $dbr = wfGetDB( DB_SLAVE );
01440                         $row = $dbr->selectRow( 'text',
01441                                 array( 'old_text', 'old_flags' ),
01442                                 array( 'old_id' => $this->getTextId() ),
01443                                 __METHOD__ );
01444                 }
01445 
01446                 if( !$row && wfGetLB()->getServerCount() > 1 ) {
01447                         // Possible slave lag!
01448                         $dbw = wfGetDB( DB_MASTER );
01449                         $row = $dbw->selectRow( 'text',
01450                                 array( 'old_text', 'old_flags' ),
01451                                 array( 'old_id' => $this->getTextId() ),
01452                                 __METHOD__ );
01453                 }
01454 
01455                 $text = self::getRevisionText( $row );
01456 
01457                 # No negative caching -- negative hits on text rows may be due to corrupted slave servers
01458                 if( $wgRevisionCacheExpiry && $text !== false ) {
01459                         $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
01460                 }
01461 
01462                 wfProfileOut( __METHOD__ );
01463 
01464                 return $text;
01465         }
01466 
01481         public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
01482                 global $wgContentHandlerUseDB;
01483 
01484                 wfProfileIn( __METHOD__ );
01485 
01486                 $fields = array( 'page_latest', 'page_namespace', 'page_title',
01487                                                 'rev_text_id', 'rev_len', 'rev_sha1' );
01488 
01489                 if ( $wgContentHandlerUseDB ) {
01490                         $fields[] = 'rev_content_model';
01491                         $fields[] = 'rev_content_format';
01492                 }
01493 
01494                 $current = $dbw->selectRow(
01495                         array( 'page', 'revision' ),
01496                         $fields,
01497                         array(
01498                                 'page_id' => $pageId,
01499                                 'page_latest=rev_id',
01500                                 ),
01501                         __METHOD__ );
01502 
01503                 if( $current ) {
01504                         $row = array(
01505                                 'page'       => $pageId,
01506                                 'comment'    => $summary,
01507                                 'minor_edit' => $minor,
01508                                 'text_id'    => $current->rev_text_id,
01509                                 'parent_id'  => $current->page_latest,
01510                                 'len'        => $current->rev_len,
01511                                 'sha1'       => $current->rev_sha1
01512                         );
01513 
01514                         if ( $wgContentHandlerUseDB ) {
01515                                 $row[ 'content_model' ] = $current->rev_content_model;
01516                                 $row[ 'content_format' ] = $current->rev_content_format;
01517                         }
01518 
01519                         $revision = new Revision( $row );
01520                         $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
01521                 } else {
01522                         $revision = null;
01523                 }
01524 
01525                 wfProfileOut( __METHOD__ );
01526                 return $revision;
01527         }
01528 
01539         public function userCan( $field, User $user = null ) {
01540                 return self::userCanBitfield( $this->mDeleted, $field, $user );
01541         }
01542 
01555         public static function userCanBitfield( $bitfield, $field, User $user = null ) {
01556                 if( $bitfield & $field ) { // aspect is deleted
01557                         if ( $bitfield & self::DELETED_RESTRICTED ) {
01558                                 $permission = 'suppressrevision';
01559                         } elseif ( $field & self::DELETED_TEXT ) {
01560                                 $permission = 'deletedtext';
01561                         } else {
01562                                 $permission = 'deletedhistory';
01563                         }
01564                         wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
01565                         if ( $user === null ) {
01566                                 global $wgUser;
01567                                 $user = $wgUser;
01568                         }
01569                         return $user->isAllowed( $permission );
01570                 } else {
01571                         return true;
01572                 }
01573         }
01574 
01582         static function getTimestampFromId( $title, $id ) {
01583                 $dbr = wfGetDB( DB_SLAVE );
01584                 // Casting fix for DB2
01585                 if ( $id == '' ) {
01586                         $id = 0;
01587                 }
01588                 $conds = array( 'rev_id' => $id );
01589                 $conds['rev_page'] = $title->getArticleID();
01590                 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01591                 if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
01592                         # Not in slave, try master
01593                         $dbw = wfGetDB( DB_MASTER );
01594                         $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01595                 }
01596                 return wfTimestamp( TS_MW, $timestamp );
01597         }
01598 
01606         static function countByPageId( $db, $id ) {
01607                 $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
01608                         array( 'rev_page' => $id ), __METHOD__ );
01609                 if( $row ) {
01610                         return $row->revCount;
01611                 }
01612                 return 0;
01613         }
01614 
01622         static function countByTitle( $db, $title ) {
01623                 $id = $title->getArticleID();
01624                 if( $id ) {
01625                         return self::countByPageId( $db, $id );
01626                 }
01627                 return 0;
01628         }
01629 
01645         public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
01646                 if ( !$userId ) return false;
01647 
01648                 if ( is_int( $db ) ) {
01649                         $db = wfGetDB( $db );
01650                 }
01651 
01652                 $res = $db->select( 'revision',
01653                         'rev_user',
01654                         array(
01655                                 'rev_page' => $pageId,
01656                                 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
01657                         ),
01658                         __METHOD__,
01659                         array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
01660                 foreach ( $res as $row ) {
01661                         if ( $row->rev_user != $userId ) {
01662                                 return false;
01663                         }
01664                 }
01665                 return true;
01666         }
01667 }