MediaWiki  REL1_21
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(
00258                                 "rev_id=$matchId",
00259                                 'page_namespace' => $title->getNamespace(),
00260                                 'page_title'     => $title->getDBkey()
00261                         )
00262                 );
00263         }
00264 
00275         public static function loadFromTimestamp( $db, $title, $timestamp ) {
00276                 return self::loadFromConds( $db,
00277                         array(
00278                                 'rev_timestamp'  => $db->timestamp( $timestamp ),
00279                                 'page_namespace' => $title->getNamespace(),
00280                                 'page_title'     => $title->getDBkey()
00281                         )
00282                 );
00283         }
00284 
00292         private static function newFromConds( $conditions, $flags = 0 ) {
00293                 $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00294                 $rev = self::loadFromConds( $db, $conditions, $flags );
00295                 if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
00296                         if ( !( $flags & self::READ_LATEST ) ) {
00297                                 $dbw = wfGetDB( DB_MASTER );
00298                                 $rev = self::loadFromConds( $dbw, $conditions, $flags );
00299                         }
00300                 }
00301                 return $rev;
00302         }
00303 
00313         private static function loadFromConds( $db, $conditions, $flags = 0 ) {
00314                 $res = self::fetchFromConds( $db, $conditions, $flags );
00315                 if( $res ) {
00316                         $row = $res->fetchObject();
00317                         if( $row ) {
00318                                 $ret = new Revision( $row );
00319                                 return $ret;
00320                         }
00321                 }
00322                 $ret = null;
00323                 return $ret;
00324         }
00325 
00334         public static function fetchRevision( $title ) {
00335                 return self::fetchFromConds(
00336                         wfGetDB( DB_SLAVE ),
00337                         array(
00338                                 'rev_id=page_latest',
00339                                 'page_namespace' => $title->getNamespace(),
00340                                 'page_title'     => $title->getDBkey()
00341                         )
00342                 );
00343         }
00344 
00355         private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
00356                 $fields = array_merge(
00357                         self::selectFields(),
00358                         self::selectPageFields(),
00359                         self::selectUserFields()
00360                 );
00361                 $options = array( 'LIMIT' => 1 );
00362                 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
00363                         $options[] = 'FOR UPDATE';
00364                 }
00365                 return $db->select(
00366                         array( 'revision', 'page', 'user' ),
00367                         $fields,
00368                         $conditions,
00369                         __METHOD__,
00370                         $options,
00371                         array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
00372                 );
00373         }
00374 
00381         public static function userJoinCond() {
00382                 return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
00383         }
00384 
00391         public static function pageJoinCond() {
00392                 return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
00393         }
00394 
00400         public static function selectFields() {
00401                 global $wgContentHandlerUseDB;
00402 
00403                 $fields = array(
00404                         'rev_id',
00405                         'rev_page',
00406                         'rev_text_id',
00407                         'rev_timestamp',
00408                         'rev_comment',
00409                         'rev_user_text',
00410                         'rev_user',
00411                         'rev_minor_edit',
00412                         'rev_deleted',
00413                         'rev_len',
00414                         'rev_parent_id',
00415                         'rev_sha1',
00416                 );
00417 
00418                 if ( $wgContentHandlerUseDB ) {
00419                         $fields[] = 'rev_content_format';
00420                         $fields[] = 'rev_content_model';
00421                 }
00422 
00423                 return $fields;
00424         }
00425 
00431         public static function selectTextFields() {
00432                 return array(
00433                         'old_text',
00434                         'old_flags'
00435                 );
00436         }
00437 
00442         public static function selectPageFields() {
00443                 return array(
00444                         'page_namespace',
00445                         'page_title',
00446                         'page_id',
00447                         'page_latest',
00448                         'page_is_redirect',
00449                         'page_len',
00450                 );
00451         }
00452 
00457         public static function selectUserFields() {
00458                 return array( 'user_name' );
00459         }
00460 
00467         public static function getParentLengths( $db, array $revIds ) {
00468                 $revLens = array();
00469                 if ( !$revIds ) {
00470                         return $revLens; // empty
00471                 }
00472                 wfProfileIn( __METHOD__ );
00473                 $res = $db->select( 'revision',
00474                         array( 'rev_id', 'rev_len' ),
00475                         array( 'rev_id' => $revIds ),
00476                         __METHOD__ );
00477                 foreach ( $res as $row ) {
00478                         $revLens[$row->rev_id] = $row->rev_len;
00479                 }
00480                 wfProfileOut( __METHOD__ );
00481                 return $revLens;
00482         }
00483 
00491         function __construct( $row ) {
00492                 if( is_object( $row ) ) {
00493                         $this->mId        = intval( $row->rev_id );
00494                         $this->mPage      = intval( $row->rev_page );
00495                         $this->mTextId    = intval( $row->rev_text_id );
00496                         $this->mComment   =         $row->rev_comment;
00497                         $this->mUser      = intval( $row->rev_user );
00498                         $this->mMinorEdit = intval( $row->rev_minor_edit );
00499                         $this->mTimestamp =         $row->rev_timestamp;
00500                         $this->mDeleted   = intval( $row->rev_deleted );
00501 
00502                         if ( !isset( $row->rev_parent_id ) ) {
00503                                 $this->mParentId = null;
00504                         } else {
00505                                 $this->mParentId = intval( $row->rev_parent_id );
00506                         }
00507 
00508                         if ( !isset( $row->rev_len ) ) {
00509                                 $this->mSize = null;
00510                         } else {
00511                                 $this->mSize = intval( $row->rev_len );
00512                         }
00513 
00514                         if ( !isset( $row->rev_sha1 ) ) {
00515                                 $this->mSha1 = null;
00516                         } else {
00517                                 $this->mSha1 = $row->rev_sha1;
00518                         }
00519 
00520                         if( isset( $row->page_latest ) ) {
00521                                 $this->mCurrent = ( $row->rev_id == $row->page_latest );
00522                                 $this->mTitle = Title::newFromRow( $row );
00523                         } else {
00524                                 $this->mCurrent = false;
00525                                 $this->mTitle = null;
00526                         }
00527 
00528                         if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
00529                                 $this->mContentModel = null; # determine on demand if needed
00530                         } else {
00531                                 $this->mContentModel = strval( $row->rev_content_model );
00532                         }
00533 
00534                         if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
00535                                 $this->mContentFormat = null; # determine on demand if needed
00536                         } else {
00537                                 $this->mContentFormat = strval( $row->rev_content_format );
00538                         }
00539 
00540                         // Lazy extraction...
00541                         $this->mText = null;
00542                         if( isset( $row->old_text ) ) {
00543                                 $this->mTextRow = $row;
00544                         } else {
00545                                 // 'text' table row entry will be lazy-loaded
00546                                 $this->mTextRow = null;
00547                         }
00548 
00549                         // Use user_name for users and rev_user_text for IPs...
00550                         $this->mUserText = null; // lazy load if left null
00551                         if ( $this->mUser == 0 ) {
00552                                 $this->mUserText = $row->rev_user_text; // IP user
00553                         } elseif ( isset( $row->user_name ) ) {
00554                                 $this->mUserText = $row->user_name; // logged-in user
00555                         }
00556                         $this->mOrigUserText = $row->rev_user_text;
00557                 } elseif( is_array( $row ) ) {
00558                         // Build a new revision to be saved...
00559                         global $wgUser; // ugh
00560 
00561                         # if we have a content object, use it to set the model and type
00562                         if ( !empty( $row['content'] ) ) {
00563                                 //@todo: when is that set? test with external store setup! check out insertOn() [dk]
00564                                 if ( !empty( $row['text_id'] ) ) {
00565                                         throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
00566                                                 "can't serialize content object" );
00567                                 }
00568 
00569                                 $row['content_model'] = $row['content']->getModel();
00570                                 # note: mContentFormat is initializes later accordingly
00571                                 # note: content is serialized later in this method!
00572                                 # also set text to null?
00573                         }
00574 
00575                         $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
00576                         $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
00577                         $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
00578                         $this->mUserText  = isset( $row['user_text']  ) ? strval( $row['user_text']  ) : $wgUser->getName();
00579                         $this->mUser      = isset( $row['user']       ) ? intval( $row['user']       ) : $wgUser->getId();
00580                         $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
00581                         $this->mTimestamp = isset( $row['timestamp']  ) ? strval( $row['timestamp']  ) : wfTimestampNow();
00582                         $this->mDeleted   = isset( $row['deleted']    ) ? intval( $row['deleted']    ) : 0;
00583                         $this->mSize      = isset( $row['len']        ) ? intval( $row['len']        ) : null;
00584                         $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
00585                         $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
00586 
00587                         $this->mContentModel   = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
00588                         $this->mContentFormat  = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
00589 
00590                         // Enforce spacing trimming on supplied text
00591                         $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
00592                         $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
00593                         $this->mTextRow   = null;
00594 
00595                         $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
00596 
00597                         // if we have a Content object, override mText and mContentModel
00598                         if ( !empty( $row['content'] ) ) {
00599                                 if ( !( $row['content'] instanceof Content ) ) {
00600                                         throw new MWException( '`content` field must contain a Content object.' );
00601                                 }
00602 
00603                                 $handler = $this->getContentHandler();
00604                                 $this->mContent = $row['content'];
00605 
00606                                 $this->mContentModel = $this->mContent->getModel();
00607                                 $this->mContentHandler = null;
00608 
00609                                 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
00610                         } elseif ( !is_null( $this->mText ) ) {
00611                                 $handler = $this->getContentHandler();
00612                                 $this->mContent = $handler->unserializeContent( $this->mText );
00613                         }
00614 
00615                         // If we have a Title object, make sure it is consistent with mPage.
00616                         if ( $this->mTitle && $this->mTitle->exists() ) {
00617                                 if ( $this->mPage === null ) {
00618                                         // if the page ID wasn't known, set it now
00619                                         $this->mPage = $this->mTitle->getArticleID();
00620                                 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
00621                                         // Got different page IDs. This may be legit (e.g. during undeletion),
00622                                         // but it seems worth mentioning it in the log.
00623                                         wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
00624                                                 $this->mTitle->getArticleID() . " provided by the Title object." );
00625                                 }
00626                         }
00627 
00628                         $this->mCurrent = false;
00629 
00630                         // If we still have no length, see it we have the text to figure it out
00631                         if ( !$this->mSize ) {
00632                                 if ( !is_null( $this->mContent ) ) {
00633                                         $this->mSize = $this->mContent->getSize();
00634                                 } else {
00635                                         #NOTE: this should never happen if we have either text or content object!
00636                                         $this->mSize = null;
00637                                 }
00638                         }
00639 
00640                         // Same for sha1
00641                         if ( $this->mSha1 === null ) {
00642                                 $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
00643                         }
00644 
00645                         // force lazy init
00646                         $this->getContentModel();
00647                         $this->getContentFormat();
00648                 } else {
00649                         throw new MWException( 'Revision constructor passed invalid row format.' );
00650                 }
00651                 $this->mUnpatrolled = null;
00652         }
00653 
00659         public function getId() {
00660                 return $this->mId;
00661         }
00662 
00669         public function setId( $id ) {
00670                 $this->mId = $id;
00671         }
00672 
00678         public function getTextId() {
00679                 return $this->mTextId;
00680         }
00681 
00687         public function getParentId() {
00688                 return $this->mParentId;
00689         }
00690 
00696         public function getSize() {
00697                 return $this->mSize;
00698         }
00699 
00705         public function getSha1() {
00706                 return $this->mSha1;
00707         }
00708 
00716         public function getTitle() {
00717                 if( isset( $this->mTitle ) ) {
00718                         return $this->mTitle;
00719                 }
00720                 if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
00721                         $dbr = wfGetDB( DB_SLAVE );
00722                         $row = $dbr->selectRow(
00723                                 array( 'page', 'revision' ),
00724                                 self::selectPageFields(),
00725                                 array( 'page_id=rev_page',
00726                                         'rev_id' => $this->mId ),
00727                                 __METHOD__ );
00728                         if ( $row ) {
00729                                 $this->mTitle = Title::newFromRow( $row );
00730                         }
00731                 }
00732 
00733                 if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
00734                         $this->mTitle = Title::newFromID( $this->mPage );
00735                 }
00736 
00737                 return $this->mTitle;
00738         }
00739 
00745         public function setTitle( $title ) {
00746                 $this->mTitle = $title;
00747         }
00748 
00754         public function getPage() {
00755                 return $this->mPage;
00756         }
00757 
00771         public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
00772                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00773                         return 0;
00774                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00775                         return 0;
00776                 } else {
00777                         return $this->mUser;
00778                 }
00779         }
00780 
00786         public function getRawUser() {
00787                 return $this->mUser;
00788         }
00789 
00803         public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
00804                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00805                         return '';
00806                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00807                         return '';
00808                 } else {
00809                         return $this->getRawUserText();
00810                 }
00811         }
00812 
00818         public function getRawUserText() {
00819                 if ( $this->mUserText === null ) {
00820                         $this->mUserText = User::whoIs( $this->mUser ); // load on demand
00821                         if ( $this->mUserText === false ) {
00822                                 # This shouldn't happen, but it can if the wiki was recovered
00823                                 # via importing revs and there is no user table entry yet.
00824                                 $this->mUserText = $this->mOrigUserText;
00825                         }
00826                 }
00827                 return $this->mUserText;
00828         }
00829 
00843         function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
00844                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
00845                         return '';
00846                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
00847                         return '';
00848                 } else {
00849                         return $this->mComment;
00850                 }
00851         }
00852 
00858         public function getRawComment() {
00859                 return $this->mComment;
00860         }
00861 
00865         public function isMinor() {
00866                 return (bool)$this->mMinorEdit;
00867         }
00868 
00872         public function isUnpatrolled() {
00873                 if( $this->mUnpatrolled !== null ) {
00874                         return $this->mUnpatrolled;
00875                 }
00876                 $dbr = wfGetDB( DB_SLAVE );
00877                 $this->mUnpatrolled = $dbr->selectField( 'recentchanges',
00878                         'rc_id',
00879                         array( // Add redundant user,timestamp condition so we can use the existing index
00880                                 'rc_user_text'  => $this->getRawUserText(),
00881                                 'rc_timestamp'  => $dbr->timestamp( $this->getTimestamp() ),
00882                                 'rc_this_oldid' => $this->getId(),
00883                                 'rc_patrolled'  => 0
00884                         ),
00885                         __METHOD__
00886                 );
00887                 return (int)$this->mUnpatrolled;
00888         }
00889 
00895         public function isDeleted( $field ) {
00896                 return ( $this->mDeleted & $field ) == $field;
00897         }
00898 
00904         public function getVisibility() {
00905                 return (int)$this->mDeleted;
00906         }
00907 
00924         public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
00925                 ContentHandler::deprecated( __METHOD__, '1.21' );
00926 
00927                 $content = $this->getContent( $audience, $user );
00928                 return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
00929         }
00930 
00945         public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
00946                 if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
00947                         return null;
00948                 } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
00949                         return null;
00950                 } else {
00951                         return $this->getContentInternal();
00952                 }
00953         }
00954 
00961         public function revText() {
00962                 wfDeprecated( __METHOD__, '1.17' );
00963                 return $this->getText( self::FOR_THIS_USER );
00964         }
00965 
00974         public function getRawText() {
00975                 ContentHandler::deprecated( __METHOD__, "1.21" );
00976                 return $this->getText( self::RAW );
00977         }
00978 
00985         public function getSerializedData() {
00986                 return $this->mText;
00987         }
00988 
00998         protected function getContentInternal() {
00999                 if( is_null( $this->mContent ) ) {
01000                         // Revision is immutable. Load on demand:
01001                         if( is_null( $this->mText ) ) {
01002                                 $this->mText = $this->loadText();
01003                         }
01004 
01005                         if ( $this->mText !== null && $this->mText !== false ) {
01006                                 // Unserialize content
01007                                 $handler = $this->getContentHandler();
01008                                 $format = $this->getContentFormat();
01009 
01010                                 $this->mContent = $handler->unserializeContent( $this->mText, $format );
01011                         } else {
01012                                 $this->mContent = false; // negative caching!
01013                         }
01014                 }
01015 
01016                 // NOTE: copy() will return $this for immutable content objects
01017                 return $this->mContent ? $this->mContent->copy() : null;
01018         }
01019 
01029         public function getContentModel() {
01030                 if ( !$this->mContentModel ) {
01031                         $title = $this->getTitle();
01032                         $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
01033 
01034                         assert( !empty( $this->mContentModel ) );
01035                 }
01036 
01037                 return $this->mContentModel;
01038         }
01039 
01048         public function getContentFormat() {
01049                 if ( !$this->mContentFormat ) {
01050                         $handler = $this->getContentHandler();
01051                         $this->mContentFormat = $handler->getDefaultFormat();
01052 
01053                         assert( !empty( $this->mContentFormat ) );
01054                 }
01055 
01056                 return $this->mContentFormat;
01057         }
01058 
01065         public function getContentHandler() {
01066                 if ( !$this->mContentHandler ) {
01067                         $model = $this->getContentModel();
01068                         $this->mContentHandler = ContentHandler::getForModelID( $model );
01069 
01070                         $format = $this->getContentFormat();
01071 
01072                         if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
01073                                 throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
01074                         }
01075                 }
01076 
01077                 return $this->mContentHandler;
01078         }
01079 
01083         public function getTimestamp() {
01084                 return wfTimestamp( TS_MW, $this->mTimestamp );
01085         }
01086 
01090         public function isCurrent() {
01091                 return $this->mCurrent;
01092         }
01093 
01099         public function getPrevious() {
01100                 if( $this->getTitle() ) {
01101                         $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
01102                         if( $prev ) {
01103                                 return self::newFromTitle( $this->getTitle(), $prev );
01104                         }
01105                 }
01106                 return null;
01107         }
01108 
01114         public function getNext() {
01115                 if( $this->getTitle() ) {
01116                         $next = $this->getTitle()->getNextRevisionID( $this->getId() );
01117                         if ( $next ) {
01118                                 return self::newFromTitle( $this->getTitle(), $next );
01119                         }
01120                 }
01121                 return null;
01122         }
01123 
01131         private function getPreviousRevisionId( $db ) {
01132                 if( is_null( $this->mPage ) ) {
01133                         return 0;
01134                 }
01135                 # Use page_latest if ID is not given
01136                 if( !$this->mId ) {
01137                         $prevId = $db->selectField( 'page', 'page_latest',
01138                                 array( 'page_id' => $this->mPage ),
01139                                 __METHOD__ );
01140                 } else {
01141                         $prevId = $db->selectField( 'revision', 'rev_id',
01142                                 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
01143                                 __METHOD__,
01144                                 array( 'ORDER BY' => 'rev_id DESC' ) );
01145                 }
01146                 return intval( $prevId );
01147         }
01148 
01162         public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
01163                 wfProfileIn( __METHOD__ );
01164 
01165                 # Get data
01166                 $textField = $prefix . 'text';
01167                 $flagsField = $prefix . 'flags';
01168 
01169                 if( isset( $row->$flagsField ) ) {
01170                         $flags = explode( ',', $row->$flagsField );
01171                 } else {
01172                         $flags = array();
01173                 }
01174 
01175                 if( isset( $row->$textField ) ) {
01176                         $text = $row->$textField;
01177                 } else {
01178                         wfProfileOut( __METHOD__ );
01179                         return false;
01180                 }
01181 
01182                 # Use external methods for external objects, text in table is URL-only then
01183                 if ( in_array( 'external', $flags ) ) {
01184                         $url = $text;
01185                         $parts = explode( '://', $url, 2 );
01186                         if( count( $parts ) == 1 || $parts[1] == '' ) {
01187                                 wfProfileOut( __METHOD__ );
01188                                 return false;
01189                         }
01190                         $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) );
01191                 }
01192 
01193                 // If the text was fetched without an error, convert it
01194                 if ( $text !== false ) {
01195                         if( in_array( 'gzip', $flags ) ) {
01196                                 # Deal with optional compression of archived pages.
01197                                 # This can be done periodically via maintenance/compressOld.php, and
01198                                 # as pages are saved if $wgCompressRevisions is set.
01199                                 $text = gzinflate( $text );
01200                         }
01201 
01202                         if( in_array( 'object', $flags ) ) {
01203                                 # Generic compressed storage
01204                                 $obj = unserialize( $text );
01205                                 if ( !is_object( $obj ) ) {
01206                                         // Invalid object
01207                                         wfProfileOut( __METHOD__ );
01208                                         return false;
01209                                 }
01210                                 $text = $obj->getText();
01211                         }
01212 
01213                         global $wgLegacyEncoding;
01214                         if( $text !== false && $wgLegacyEncoding
01215                                 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
01216                         {
01217                                 # Old revisions kept around in a legacy encoding?
01218                                 # Upconvert on demand.
01219                                 # ("utf8" checked for compatibility with some broken
01220                                 #  conversion scripts 2008-12-30)
01221                                 global $wgContLang;
01222                                 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
01223                         }
01224                 }
01225                 wfProfileOut( __METHOD__ );
01226                 return $text;
01227         }
01228 
01239         public static function compressRevisionText( &$text ) {
01240                 global $wgCompressRevisions;
01241                 $flags = array();
01242 
01243                 # Revisions not marked this way will be converted
01244                 # on load if $wgLegacyCharset is set in the future.
01245                 $flags[] = 'utf-8';
01246 
01247                 if( $wgCompressRevisions ) {
01248                         if( function_exists( 'gzdeflate' ) ) {
01249                                 $text = gzdeflate( $text );
01250                                 $flags[] = 'gzip';
01251                         } else {
01252                                 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
01253                         }
01254                 }
01255                 return implode( ',', $flags );
01256         }
01257 
01266         public function insertOn( $dbw ) {
01267                 global $wgDefaultExternalStore, $wgContentHandlerUseDB;
01268 
01269                 wfProfileIn( __METHOD__ );
01270 
01271                 $this->checkContentModel();
01272 
01273                 $data = $this->mText;
01274                 $flags = self::compressRevisionText( $data );
01275 
01276                 # Write to external storage if required
01277                 if( $wgDefaultExternalStore ) {
01278                         // Store and get the URL
01279                         $data = ExternalStore::insertToDefault( $data );
01280                         if( !$data ) {
01281                                 throw new MWException( "Unable to store text to external storage" );
01282                         }
01283                         if( $flags ) {
01284                                 $flags .= ',';
01285                         }
01286                         $flags .= 'external';
01287                 }
01288 
01289                 # Record the text (or external storage URL) to the text table
01290                 if( !isset( $this->mTextId ) ) {
01291                         $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
01292                         $dbw->insert( 'text',
01293                                 array(
01294                                         'old_id'    => $old_id,
01295                                         'old_text'  => $data,
01296                                         'old_flags' => $flags,
01297                                 ), __METHOD__
01298                         );
01299                         $this->mTextId = $dbw->insertId();
01300                 }
01301 
01302                 if ( $this->mComment === null ) $this->mComment = "";
01303 
01304                 # Record the edit in revisions
01305                 $rev_id = isset( $this->mId )
01306                         ? $this->mId
01307                         : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
01308                 $row = array(
01309                         'rev_id'         => $rev_id,
01310                         'rev_page'       => $this->mPage,
01311                         'rev_text_id'    => $this->mTextId,
01312                         'rev_comment'    => $this->mComment,
01313                         'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
01314                         'rev_user'       => $this->mUser,
01315                         'rev_user_text'  => $this->mUserText,
01316                         'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
01317                         'rev_deleted'    => $this->mDeleted,
01318                         'rev_len'        => $this->mSize,
01319                         'rev_parent_id'  => is_null( $this->mParentId )
01320                                 ? $this->getPreviousRevisionId( $dbw )
01321                                 : $this->mParentId,
01322                         'rev_sha1'       => is_null( $this->mSha1 )
01323                                 ? Revision::base36Sha1( $this->mText )
01324                                 : $this->mSha1,
01325                 );
01326 
01327                 if ( $wgContentHandlerUseDB ) {
01328                         //NOTE: Store null for the default model and format, to save space.
01329                         //XXX: Makes the DB sensitive to changed defaults. Make this behavior optional? Only in miser mode?
01330 
01331                         $model = $this->getContentModel();
01332                         $format = $this->getContentFormat();
01333 
01334                         $title = $this->getTitle();
01335 
01336                         if ( $title === null ) {
01337                                 throw new MWException( "Insufficient information to determine the title of the revision's page!" );
01338                         }
01339 
01340                         $defaultModel = ContentHandler::getDefaultModelFor( $title );
01341                         $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
01342 
01343                         $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
01344                         $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
01345                 }
01346 
01347                 $dbw->insert( 'revision', $row, __METHOD__ );
01348 
01349                 $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
01350 
01351                 wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
01352 
01353                 wfProfileOut( __METHOD__ );
01354                 return $this->mId;
01355         }
01356 
01357         protected function checkContentModel() {
01358                 global $wgContentHandlerUseDB;
01359 
01360                 $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
01361 
01362                 $model = $this->getContentModel();
01363                 $format = $this->getContentFormat();
01364                 $handler = $this->getContentHandler();
01365 
01366                 if ( !$handler->isSupportedFormat( $format ) ) {
01367                         $t = $title->getPrefixedDBkey();
01368 
01369                         throw new MWException( "Can't use format $format with content model $model on $t" );
01370                 }
01371 
01372                 if ( !$wgContentHandlerUseDB && $title ) {
01373                         // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
01374 
01375                         $defaultModel = ContentHandler::getDefaultModelFor( $title );
01376                         $defaultHandler = ContentHandler::getForModelID( $defaultModel );
01377                         $defaultFormat = $defaultHandler->getDefaultFormat();
01378 
01379                         if ( $this->getContentModel() != $defaultModel ) {
01380                                 $t = $title->getPrefixedDBkey();
01381 
01382                                 throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
01383                                                                                 . "model is $model , default for $t is $defaultModel" );
01384                         }
01385 
01386                         if ( $this->getContentFormat() != $defaultFormat ) {
01387                                 $t = $title->getPrefixedDBkey();
01388 
01389                                 throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
01390                                                                                 . "format is $format, default for $t is $defaultFormat" );
01391                         }
01392                 }
01393 
01394                 $content = $this->getContent( Revision::RAW );
01395 
01396                 if ( !$content || !$content->isValid() ) {
01397                         $t = $title->getPrefixedDBkey();
01398 
01399                         throw new MWException( "Content of $t is not valid! Content model is $model" );
01400                 }
01401         }
01402 
01408         public static function base36Sha1( $text ) {
01409                 return wfBaseConvert( sha1( $text ), 16, 36, 31 );
01410         }
01411 
01418         protected function loadText() {
01419                 wfProfileIn( __METHOD__ );
01420 
01421                 // Caching may be beneficial for massive use of external storage
01422                 global $wgRevisionCacheExpiry, $wgMemc;
01423                 $textId = $this->getTextId();
01424                 $key = wfMemcKey( 'revisiontext', 'textid', $textId );
01425                 if( $wgRevisionCacheExpiry ) {
01426                         $text = $wgMemc->get( $key );
01427                         if( is_string( $text ) ) {
01428                                 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
01429                                 wfProfileOut( __METHOD__ );
01430                                 return $text;
01431                         }
01432                 }
01433 
01434                 // If we kept data for lazy extraction, use it now...
01435                 if ( isset( $this->mTextRow ) ) {
01436                         $row = $this->mTextRow;
01437                         $this->mTextRow = null;
01438                 } else {
01439                         $row = null;
01440                 }
01441 
01442                 if( !$row ) {
01443                         // Text data is immutable; check slaves first.
01444                         $dbr = wfGetDB( DB_SLAVE );
01445                         $row = $dbr->selectRow( 'text',
01446                                 array( 'old_text', 'old_flags' ),
01447                                 array( 'old_id' => $this->getTextId() ),
01448                                 __METHOD__ );
01449                 }
01450 
01451                 if( !$row && wfGetLB()->getServerCount() > 1 ) {
01452                         // Possible slave lag!
01453                         $dbw = wfGetDB( DB_MASTER );
01454                         $row = $dbw->selectRow( 'text',
01455                                 array( 'old_text', 'old_flags' ),
01456                                 array( 'old_id' => $this->getTextId() ),
01457                                 __METHOD__ );
01458                 }
01459 
01460                 $text = self::getRevisionText( $row );
01461 
01462                 # No negative caching -- negative hits on text rows may be due to corrupted slave servers
01463                 if( $wgRevisionCacheExpiry && $text !== false ) {
01464                         $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
01465                 }
01466 
01467                 wfProfileOut( __METHOD__ );
01468 
01469                 return $text;
01470         }
01471 
01486         public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
01487                 global $wgContentHandlerUseDB;
01488 
01489                 wfProfileIn( __METHOD__ );
01490 
01491                 $fields = array( 'page_latest', 'page_namespace', 'page_title',
01492                                                 'rev_text_id', 'rev_len', 'rev_sha1' );
01493 
01494                 if ( $wgContentHandlerUseDB ) {
01495                         $fields[] = 'rev_content_model';
01496                         $fields[] = 'rev_content_format';
01497                 }
01498 
01499                 $current = $dbw->selectRow(
01500                         array( 'page', 'revision' ),
01501                         $fields,
01502                         array(
01503                                 'page_id' => $pageId,
01504                                 'page_latest=rev_id',
01505                                 ),
01506                         __METHOD__ );
01507 
01508                 if( $current ) {
01509                         $row = array(
01510                                 'page'       => $pageId,
01511                                 'comment'    => $summary,
01512                                 'minor_edit' => $minor,
01513                                 'text_id'    => $current->rev_text_id,
01514                                 'parent_id'  => $current->page_latest,
01515                                 'len'        => $current->rev_len,
01516                                 'sha1'       => $current->rev_sha1
01517                         );
01518 
01519                         if ( $wgContentHandlerUseDB ) {
01520                                 $row[ 'content_model' ] = $current->rev_content_model;
01521                                 $row[ 'content_format' ] = $current->rev_content_format;
01522                         }
01523 
01524                         $revision = new Revision( $row );
01525                         $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
01526                 } else {
01527                         $revision = null;
01528                 }
01529 
01530                 wfProfileOut( __METHOD__ );
01531                 return $revision;
01532         }
01533 
01544         public function userCan( $field, User $user = null ) {
01545                 return self::userCanBitfield( $this->mDeleted, $field, $user );
01546         }
01547 
01560         public static function userCanBitfield( $bitfield, $field, User $user = null ) {
01561                 if( $bitfield & $field ) { // aspect is deleted
01562                         if ( $bitfield & self::DELETED_RESTRICTED ) {
01563                                 $permission = 'suppressrevision';
01564                         } elseif ( $field & self::DELETED_TEXT ) {
01565                                 $permission = 'deletedtext';
01566                         } else {
01567                                 $permission = 'deletedhistory';
01568                         }
01569                         wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
01570                         if ( $user === null ) {
01571                                 global $wgUser;
01572                                 $user = $wgUser;
01573                         }
01574                         return $user->isAllowed( $permission );
01575                 } else {
01576                         return true;
01577                 }
01578         }
01579 
01587         static function getTimestampFromId( $title, $id ) {
01588                 $dbr = wfGetDB( DB_SLAVE );
01589                 // Casting fix for databases that can't take '' for rev_id
01590                 if ( $id == '' ) {
01591                         $id = 0;
01592                 }
01593                 $conds = array( 'rev_id' => $id );
01594                 $conds['rev_page'] = $title->getArticleID();
01595                 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01596                 if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
01597                         # Not in slave, try master
01598                         $dbw = wfGetDB( DB_MASTER );
01599                         $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01600                 }
01601                 return wfTimestamp( TS_MW, $timestamp );
01602         }
01603 
01611         static function countByPageId( $db, $id ) {
01612                 $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
01613                         array( 'rev_page' => $id ), __METHOD__ );
01614                 if( $row ) {
01615                         return $row->revCount;
01616                 }
01617                 return 0;
01618         }
01619 
01627         static function countByTitle( $db, $title ) {
01628                 $id = $title->getArticleID();
01629                 if( $id ) {
01630                         return self::countByPageId( $db, $id );
01631                 }
01632                 return 0;
01633         }
01634 
01650         public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
01651                 if ( !$userId ) return false;
01652 
01653                 if ( is_int( $db ) ) {
01654                         $db = wfGetDB( $db );
01655                 }
01656 
01657                 $res = $db->select( 'revision',
01658                         'rev_user',
01659                         array(
01660                                 'rev_page' => $pageId,
01661                                 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
01662                         ),
01663                         __METHOD__,
01664                         array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
01665                 foreach ( $res as $row ) {
01666                         if ( $row->rev_user != $userId ) {
01667                                 return false;
01668                         }
01669                 }
01670                 return true;
01671         }
01672 }