MediaWiki  REL1_23
Revision.php
Go to the documentation of this file.
00001 <?php
00026 class Revision implements IDBAccessObject {
00027     protected $mId;
00028 
00032     protected $mPage;
00033     protected $mUserText;
00034     protected $mOrigUserText;
00035     protected $mUser;
00036     protected $mMinorEdit;
00037     protected $mTimestamp;
00038     protected $mDeleted;
00039     protected $mSize;
00040     protected $mSha1;
00041     protected $mParentId;
00042     protected $mComment;
00043     protected $mText;
00044     protected $mTextRow;
00045 
00049     protected $mTitle;
00050     protected $mCurrent;
00051     protected $mContentModel;
00052     protected $mContentFormat;
00053 
00057     protected $mContent;
00058 
00062     protected $mContentHandler;
00063 
00067     protected $mQueryFlags = 0;
00068 
00069     // Revision deletion constants
00070     const DELETED_TEXT = 1;
00071     const DELETED_COMMENT = 2;
00072     const DELETED_USER = 4;
00073     const DELETED_RESTRICTED = 8;
00074     const SUPPRESSED_USER = 12; // convenience
00075 
00076     // Audience options for accessors
00077     const FOR_PUBLIC = 1;
00078     const FOR_THIS_USER = 2;
00079     const RAW = 3;
00080 
00093     public static function newFromId( $id, $flags = 0 ) {
00094         return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
00095     }
00096 
00111     public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
00112         $conds = array(
00113             'page_namespace' => $title->getNamespace(),
00114             'page_title' => $title->getDBkey()
00115         );
00116         if ( $id ) {
00117             // Use the specified ID
00118             $conds['rev_id'] = $id;
00119             return self::newFromConds( $conds, (int)$flags );
00120         } else {
00121             // Use a join to get the latest revision
00122             $conds[] = 'rev_id=page_latest';
00123             $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00124             return self::loadFromConds( $db, $conds, $flags );
00125         }
00126     }
00127 
00142     public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
00143         $conds = array( 'page_id' => $pageId );
00144         if ( $revId ) {
00145             $conds['rev_id'] = $revId;
00146         } else {
00147             // Use a join to get the latest revision
00148             $conds[] = 'rev_id = page_latest';
00149         }
00150         return self::newFromConds( $conds, (int)$flags );
00151     }
00152 
00164     public static function newFromArchiveRow( $row, $overrides = array() ) {
00165         global $wgContentHandlerUseDB;
00166 
00167         $attribs = $overrides + array(
00168             'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
00169             'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
00170             'comment'    => $row->ar_comment,
00171             'user'       => $row->ar_user,
00172             'user_text'  => $row->ar_user_text,
00173             'timestamp'  => $row->ar_timestamp,
00174             'minor_edit' => $row->ar_minor_edit,
00175             'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
00176             'deleted'    => $row->ar_deleted,
00177             'len'        => $row->ar_len,
00178             'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
00179             'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
00180             'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
00181         );
00182 
00183         if ( !$wgContentHandlerUseDB ) {
00184             unset( $attribs['content_model'] );
00185             unset( $attribs['content_format'] );
00186         }
00187 
00188         if ( !isset( $attribs['title'] )
00189             && isset( $row->ar_namespace )
00190             && isset( $row->ar_title ) ) {
00191 
00192             $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
00193         }
00194 
00195         if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
00196             // Pre-1.5 ar_text row
00197             $attribs['text'] = self::getRevisionText( $row, 'ar_' );
00198             if ( $attribs['text'] === false ) {
00199                 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
00200             }
00201         }
00202         return new self( $attribs );
00203     }
00204 
00211     public static function newFromRow( $row ) {
00212         return new self( $row );
00213     }
00214 
00223     public static function loadFromId( $db, $id ) {
00224         return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
00225     }
00226 
00237     public static function loadFromPageId( $db, $pageid, $id = 0 ) {
00238         $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) );
00239         if ( $id ) {
00240             $conds['rev_id'] = intval( $id );
00241         } else {
00242             $conds[] = 'rev_id=page_latest';
00243         }
00244         return self::loadFromConds( $db, $conds );
00245     }
00246 
00257     public static function loadFromTitle( $db, $title, $id = 0 ) {
00258         if ( $id ) {
00259             $matchId = intval( $id );
00260         } else {
00261             $matchId = 'page_latest';
00262         }
00263         return self::loadFromConds( $db,
00264             array(
00265                 "rev_id=$matchId",
00266                 'page_namespace' => $title->getNamespace(),
00267                 'page_title' => $title->getDBkey()
00268             )
00269         );
00270     }
00271 
00282     public static function loadFromTimestamp( $db, $title, $timestamp ) {
00283         return self::loadFromConds( $db,
00284             array(
00285                 'rev_timestamp' => $db->timestamp( $timestamp ),
00286                 'page_namespace' => $title->getNamespace(),
00287                 'page_title' => $title->getDBkey()
00288             )
00289         );
00290     }
00291 
00299     private static function newFromConds( $conditions, $flags = 0 ) {
00300         $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
00301         $rev = self::loadFromConds( $db, $conditions, $flags );
00302         if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
00303             if ( !( $flags & self::READ_LATEST ) ) {
00304                 $dbw = wfGetDB( DB_MASTER );
00305                 $rev = self::loadFromConds( $dbw, $conditions, $flags );
00306             }
00307         }
00308         if ( $rev ) {
00309             $rev->mQueryFlags = $flags;
00310         }
00311         return $rev;
00312     }
00313 
00323     private static function loadFromConds( $db, $conditions, $flags = 0 ) {
00324         $res = self::fetchFromConds( $db, $conditions, $flags );
00325         if ( $res ) {
00326             $row = $res->fetchObject();
00327             if ( $row ) {
00328                 $ret = new Revision( $row );
00329                 return $ret;
00330             }
00331         }
00332         $ret = null;
00333         return $ret;
00334     }
00335 
00344     public static function fetchRevision( $title ) {
00345         return self::fetchFromConds(
00346             wfGetDB( DB_SLAVE ),
00347             array(
00348                 'rev_id=page_latest',
00349                 'page_namespace' => $title->getNamespace(),
00350                 'page_title' => $title->getDBkey()
00351             )
00352         );
00353     }
00354 
00365     private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
00366         $fields = array_merge(
00367             self::selectFields(),
00368             self::selectPageFields(),
00369             self::selectUserFields()
00370         );
00371         $options = array( 'LIMIT' => 1 );
00372         if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
00373             $options[] = 'FOR UPDATE';
00374         }
00375         return $db->select(
00376             array( 'revision', 'page', 'user' ),
00377             $fields,
00378             $conditions,
00379             __METHOD__,
00380             $options,
00381             array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
00382         );
00383     }
00384 
00391     public static function userJoinCond() {
00392         return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) );
00393     }
00394 
00401     public static function pageJoinCond() {
00402         return array( 'INNER JOIN', array( 'page_id = rev_page' ) );
00403     }
00404 
00410     public static function selectFields() {
00411         global $wgContentHandlerUseDB;
00412 
00413         $fields = array(
00414             'rev_id',
00415             'rev_page',
00416             'rev_text_id',
00417             'rev_timestamp',
00418             'rev_comment',
00419             'rev_user_text',
00420             'rev_user',
00421             'rev_minor_edit',
00422             'rev_deleted',
00423             'rev_len',
00424             'rev_parent_id',
00425             'rev_sha1',
00426         );
00427 
00428         if ( $wgContentHandlerUseDB ) {
00429             $fields[] = 'rev_content_format';
00430             $fields[] = 'rev_content_model';
00431         }
00432 
00433         return $fields;
00434     }
00435 
00441     public static function selectArchiveFields() {
00442         global $wgContentHandlerUseDB;
00443         $fields = array(
00444             'ar_id',
00445             'ar_page_id',
00446             'ar_rev_id',
00447             'ar_text',
00448             'ar_text_id',
00449             'ar_timestamp',
00450             'ar_comment',
00451             'ar_user_text',
00452             'ar_user',
00453             'ar_minor_edit',
00454             'ar_deleted',
00455             'ar_len',
00456             'ar_parent_id',
00457             'ar_sha1',
00458         );
00459 
00460         if ( $wgContentHandlerUseDB ) {
00461             $fields[] = 'ar_content_format';
00462             $fields[] = 'ar_content_model';
00463         }
00464         return $fields;
00465     }
00466 
00472     public static function selectTextFields() {
00473         return array(
00474             'old_text',
00475             'old_flags'
00476         );
00477     }
00478 
00483     public static function selectPageFields() {
00484         return array(
00485             'page_namespace',
00486             'page_title',
00487             'page_id',
00488             'page_latest',
00489             'page_is_redirect',
00490             'page_len',
00491         );
00492     }
00493 
00498     public static function selectUserFields() {
00499         return array( 'user_name' );
00500     }
00501 
00508     public static function getParentLengths( $db, array $revIds ) {
00509         $revLens = array();
00510         if ( !$revIds ) {
00511             return $revLens; // empty
00512         }
00513         wfProfileIn( __METHOD__ );
00514         $res = $db->select( 'revision',
00515             array( 'rev_id', 'rev_len' ),
00516             array( 'rev_id' => $revIds ),
00517             __METHOD__ );
00518         foreach ( $res as $row ) {
00519             $revLens[$row->rev_id] = $row->rev_len;
00520         }
00521         wfProfileOut( __METHOD__ );
00522         return $revLens;
00523     }
00524 
00532     function __construct( $row ) {
00533         if ( is_object( $row ) ) {
00534             $this->mId = intval( $row->rev_id );
00535             $this->mPage = intval( $row->rev_page );
00536             $this->mTextId = intval( $row->rev_text_id );
00537             $this->mComment = $row->rev_comment;
00538             $this->mUser = intval( $row->rev_user );
00539             $this->mMinorEdit = intval( $row->rev_minor_edit );
00540             $this->mTimestamp = $row->rev_timestamp;
00541             $this->mDeleted = intval( $row->rev_deleted );
00542 
00543             if ( !isset( $row->rev_parent_id ) ) {
00544                 $this->mParentId = null;
00545             } else {
00546                 $this->mParentId = intval( $row->rev_parent_id );
00547             }
00548 
00549             if ( !isset( $row->rev_len ) ) {
00550                 $this->mSize = null;
00551             } else {
00552                 $this->mSize = intval( $row->rev_len );
00553             }
00554 
00555             if ( !isset( $row->rev_sha1 ) ) {
00556                 $this->mSha1 = null;
00557             } else {
00558                 $this->mSha1 = $row->rev_sha1;
00559             }
00560 
00561             if ( isset( $row->page_latest ) ) {
00562                 $this->mCurrent = ( $row->rev_id == $row->page_latest );
00563                 $this->mTitle = Title::newFromRow( $row );
00564             } else {
00565                 $this->mCurrent = false;
00566                 $this->mTitle = null;
00567             }
00568 
00569             if ( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
00570                 $this->mContentModel = null; # determine on demand if needed
00571             } else {
00572                 $this->mContentModel = strval( $row->rev_content_model );
00573             }
00574 
00575             if ( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
00576                 $this->mContentFormat = null; # determine on demand if needed
00577             } else {
00578                 $this->mContentFormat = strval( $row->rev_content_format );
00579             }
00580 
00581             // Lazy extraction...
00582             $this->mText = null;
00583             if ( isset( $row->old_text ) ) {
00584                 $this->mTextRow = $row;
00585             } else {
00586                 // 'text' table row entry will be lazy-loaded
00587                 $this->mTextRow = null;
00588             }
00589 
00590             // Use user_name for users and rev_user_text for IPs...
00591             $this->mUserText = null; // lazy load if left null
00592             if ( $this->mUser == 0 ) {
00593                 $this->mUserText = $row->rev_user_text; // IP user
00594             } elseif ( isset( $row->user_name ) ) {
00595                 $this->mUserText = $row->user_name; // logged-in user
00596             }
00597             $this->mOrigUserText = $row->rev_user_text;
00598         } elseif ( is_array( $row ) ) {
00599             // Build a new revision to be saved...
00600             global $wgUser; // ugh
00601 
00602             # if we have a content object, use it to set the model and type
00603             if ( !empty( $row['content'] ) ) {
00604                 // @todo when is that set? test with external store setup! check out insertOn() [dk]
00605                 if ( !empty( $row['text_id'] ) ) {
00606                     throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
00607                         "can't serialize content object" );
00608                 }
00609 
00610                 $row['content_model'] = $row['content']->getModel();
00611                 # note: mContentFormat is initializes later accordingly
00612                 # note: content is serialized later in this method!
00613                 # also set text to null?
00614             }
00615 
00616             $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
00617             $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
00618             $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
00619             $this->mUserText = isset( $row['user_text'] )
00620                 ? strval( $row['user_text'] ) : $wgUser->getName();
00621             $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
00622             $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
00623             $this->mTimestamp = isset( $row['timestamp'] )
00624                 ? strval( $row['timestamp'] ) : wfTimestampNow();
00625             $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
00626             $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
00627             $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
00628             $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
00629 
00630             $this->mContentModel = isset( $row['content_model'] )
00631                 ? strval( $row['content_model'] ) : null;
00632             $this->mContentFormat = isset( $row['content_format'] )
00633                 ? strval( $row['content_format'] ) : null;
00634 
00635             // Enforce spacing trimming on supplied text
00636             $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
00637             $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
00638             $this->mTextRow = null;
00639 
00640             $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
00641 
00642             // if we have a Content object, override mText and mContentModel
00643             if ( !empty( $row['content'] ) ) {
00644                 if ( !( $row['content'] instanceof Content ) ) {
00645                     throw new MWException( '`content` field must contain a Content object.' );
00646                 }
00647 
00648                 $handler = $this->getContentHandler();
00649                 $this->mContent = $row['content'];
00650 
00651                 $this->mContentModel = $this->mContent->getModel();
00652                 $this->mContentHandler = null;
00653 
00654                 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
00655             } elseif ( !is_null( $this->mText ) ) {
00656                 $handler = $this->getContentHandler();
00657                 $this->mContent = $handler->unserializeContent( $this->mText );
00658             }
00659 
00660             // If we have a Title object, make sure it is consistent with mPage.
00661             if ( $this->mTitle && $this->mTitle->exists() ) {
00662                 if ( $this->mPage === null ) {
00663                     // if the page ID wasn't known, set it now
00664                     $this->mPage = $this->mTitle->getArticleID();
00665                 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
00666                     // Got different page IDs. This may be legit (e.g. during undeletion),
00667                     // but it seems worth mentioning it in the log.
00668                     wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
00669                         $this->mTitle->getArticleID() . " provided by the Title object." );
00670                 }
00671             }
00672 
00673             $this->mCurrent = false;
00674 
00675             // If we still have no length, see it we have the text to figure it out
00676             if ( !$this->mSize ) {
00677                 if ( !is_null( $this->mContent ) ) {
00678                     $this->mSize = $this->mContent->getSize();
00679                 } else {
00680                     #NOTE: this should never happen if we have either text or content object!
00681                     $this->mSize = null;
00682                 }
00683             }
00684 
00685             // Same for sha1
00686             if ( $this->mSha1 === null ) {
00687                 $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
00688             }
00689 
00690             // force lazy init
00691             $this->getContentModel();
00692             $this->getContentFormat();
00693         } else {
00694             throw new MWException( 'Revision constructor passed invalid row format.' );
00695         }
00696         $this->mUnpatrolled = null;
00697     }
00698 
00704     public function getId() {
00705         return $this->mId;
00706     }
00707 
00714     public function setId( $id ) {
00715         $this->mId = $id;
00716     }
00717 
00723     public function getTextId() {
00724         return $this->mTextId;
00725     }
00726 
00732     public function getParentId() {
00733         return $this->mParentId;
00734     }
00735 
00741     public function getSize() {
00742         return $this->mSize;
00743     }
00744 
00750     public function getSha1() {
00751         return $this->mSha1;
00752     }
00753 
00761     public function getTitle() {
00762         if ( isset( $this->mTitle ) ) {
00763             return $this->mTitle;
00764         }
00765         //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
00766         if ( !is_null( $this->mId ) ) {
00767             $dbr = wfGetDB( DB_SLAVE );
00768             $row = $dbr->selectRow(
00769                 array( 'page', 'revision' ),
00770                 self::selectPageFields(),
00771                 array( 'page_id=rev_page',
00772                     'rev_id' => $this->mId ),
00773                 __METHOD__ );
00774             if ( $row ) {
00775                 $this->mTitle = Title::newFromRow( $row );
00776             }
00777         }
00778 
00779         if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
00780             $this->mTitle = Title::newFromID( $this->mPage );
00781         }
00782 
00783         return $this->mTitle;
00784     }
00785 
00791     public function setTitle( $title ) {
00792         $this->mTitle = $title;
00793     }
00794 
00800     public function getPage() {
00801         return $this->mPage;
00802     }
00803 
00817     public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
00818         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00819             return 0;
00820         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00821             return 0;
00822         } else {
00823             return $this->mUser;
00824         }
00825     }
00826 
00832     public function getRawUser() {
00833         return $this->mUser;
00834     }
00835 
00849     public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
00850         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
00851             return '';
00852         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
00853             return '';
00854         } else {
00855             return $this->getRawUserText();
00856         }
00857     }
00858 
00864     public function getRawUserText() {
00865         if ( $this->mUserText === null ) {
00866             $this->mUserText = User::whoIs( $this->mUser ); // load on demand
00867             if ( $this->mUserText === false ) {
00868                 # This shouldn't happen, but it can if the wiki was recovered
00869                 # via importing revs and there is no user table entry yet.
00870                 $this->mUserText = $this->mOrigUserText;
00871             }
00872         }
00873         return $this->mUserText;
00874     }
00875 
00889     function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
00890         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
00891             return '';
00892         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
00893             return '';
00894         } else {
00895             return $this->mComment;
00896         }
00897     }
00898 
00904     public function getRawComment() {
00905         return $this->mComment;
00906     }
00907 
00911     public function isMinor() {
00912         return (bool)$this->mMinorEdit;
00913     }
00914 
00918     public function isUnpatrolled() {
00919         if ( $this->mUnpatrolled !== null ) {
00920             return $this->mUnpatrolled;
00921         }
00922         $rc = $this->getRecentChange();
00923         if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
00924             $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
00925         } else {
00926             $this->mUnpatrolled = 0;
00927         }
00928         return $this->mUnpatrolled;
00929     }
00930 
00937     public function getRecentChange() {
00938         $dbr = wfGetDB( DB_SLAVE );
00939         return RecentChange::newFromConds(
00940             array(
00941                 'rc_user_text' => $this->getRawUserText(),
00942                 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
00943                 'rc_this_oldid' => $this->getId()
00944             ),
00945             __METHOD__
00946         );
00947     }
00948 
00954     public function isDeleted( $field ) {
00955         return ( $this->mDeleted & $field ) == $field;
00956     }
00957 
00963     public function getVisibility() {
00964         return (int)$this->mDeleted;
00965     }
00966 
00983     public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
00984         ContentHandler::deprecated( __METHOD__, '1.21' );
00985 
00986         $content = $this->getContent( $audience, $user );
00987         return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
00988     }
00989 
01004     public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
01005         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
01006             return null;
01007         } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
01008             return null;
01009         } else {
01010             return $this->getContentInternal();
01011         }
01012     }
01013 
01022     public function getRawText() {
01023         ContentHandler::deprecated( __METHOD__, "1.21" );
01024         return $this->getText( self::RAW );
01025     }
01026 
01033     public function getSerializedData() {
01034         if ( is_null( $this->mText ) ) {
01035             $this->mText = $this->loadText();
01036         }
01037 
01038         return $this->mText;
01039     }
01040 
01050     protected function getContentInternal() {
01051         if ( is_null( $this->mContent ) ) {
01052             // Revision is immutable. Load on demand:
01053             if ( is_null( $this->mText ) ) {
01054                 $this->mText = $this->loadText();
01055             }
01056 
01057             if ( $this->mText !== null && $this->mText !== false ) {
01058                 // Unserialize content
01059                 $handler = $this->getContentHandler();
01060                 $format = $this->getContentFormat();
01061 
01062                 $this->mContent = $handler->unserializeContent( $this->mText, $format );
01063             } else {
01064                 $this->mContent = false; // negative caching!
01065             }
01066         }
01067 
01068         // NOTE: copy() will return $this for immutable content objects
01069         return $this->mContent ? $this->mContent->copy() : null;
01070     }
01071 
01082     public function getContentModel() {
01083         if ( !$this->mContentModel ) {
01084             $title = $this->getTitle();
01085             $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
01086 
01087             assert( !empty( $this->mContentModel ) );
01088         }
01089 
01090         return $this->mContentModel;
01091     }
01092 
01102     public function getContentFormat() {
01103         if ( !$this->mContentFormat ) {
01104             $handler = $this->getContentHandler();
01105             $this->mContentFormat = $handler->getDefaultFormat();
01106 
01107             assert( !empty( $this->mContentFormat ) );
01108         }
01109 
01110         return $this->mContentFormat;
01111     }
01112 
01119     public function getContentHandler() {
01120         if ( !$this->mContentHandler ) {
01121             $model = $this->getContentModel();
01122             $this->mContentHandler = ContentHandler::getForModelID( $model );
01123 
01124             $format = $this->getContentFormat();
01125 
01126             if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
01127                 throw new MWException( "Oops, the content format $format is not supported for "
01128                     . "this content model, $model" );
01129             }
01130         }
01131 
01132         return $this->mContentHandler;
01133     }
01134 
01138     public function getTimestamp() {
01139         return wfTimestamp( TS_MW, $this->mTimestamp );
01140     }
01141 
01145     public function isCurrent() {
01146         return $this->mCurrent;
01147     }
01148 
01154     public function getPrevious() {
01155         if ( $this->getTitle() ) {
01156             $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
01157             if ( $prev ) {
01158                 return self::newFromTitle( $this->getTitle(), $prev );
01159             }
01160         }
01161         return null;
01162     }
01163 
01169     public function getNext() {
01170         if ( $this->getTitle() ) {
01171             $next = $this->getTitle()->getNextRevisionID( $this->getId() );
01172             if ( $next ) {
01173                 return self::newFromTitle( $this->getTitle(), $next );
01174             }
01175         }
01176         return null;
01177     }
01178 
01186     private function getPreviousRevisionId( $db ) {
01187         if ( is_null( $this->mPage ) ) {
01188             return 0;
01189         }
01190         # Use page_latest if ID is not given
01191         if ( !$this->mId ) {
01192             $prevId = $db->selectField( 'page', 'page_latest',
01193                 array( 'page_id' => $this->mPage ),
01194                 __METHOD__ );
01195         } else {
01196             $prevId = $db->selectField( 'revision', 'rev_id',
01197                 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
01198                 __METHOD__,
01199                 array( 'ORDER BY' => 'rev_id DESC' ) );
01200         }
01201         return intval( $prevId );
01202     }
01203 
01217     public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
01218         wfProfileIn( __METHOD__ );
01219 
01220         # Get data
01221         $textField = $prefix . 'text';
01222         $flagsField = $prefix . 'flags';
01223 
01224         if ( isset( $row->$flagsField ) ) {
01225             $flags = explode( ',', $row->$flagsField );
01226         } else {
01227             $flags = array();
01228         }
01229 
01230         if ( isset( $row->$textField ) ) {
01231             $text = $row->$textField;
01232         } else {
01233             wfProfileOut( __METHOD__ );
01234             return false;
01235         }
01236 
01237         # Use external methods for external objects, text in table is URL-only then
01238         if ( in_array( 'external', $flags ) ) {
01239             $url = $text;
01240             $parts = explode( '://', $url, 2 );
01241             if ( count( $parts ) == 1 || $parts[1] == '' ) {
01242                 wfProfileOut( __METHOD__ );
01243                 return false;
01244             }
01245             $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) );
01246         }
01247 
01248         // If the text was fetched without an error, convert it
01249         if ( $text !== false ) {
01250             $text = self::decompressRevisionText( $text, $flags );
01251         }
01252         wfProfileOut( __METHOD__ );
01253         return $text;
01254     }
01255 
01266     public static function compressRevisionText( &$text ) {
01267         global $wgCompressRevisions;
01268         $flags = array();
01269 
01270         # Revisions not marked this way will be converted
01271         # on load if $wgLegacyCharset is set in the future.
01272         $flags[] = 'utf-8';
01273 
01274         if ( $wgCompressRevisions ) {
01275             if ( function_exists( 'gzdeflate' ) ) {
01276                 $text = gzdeflate( $text );
01277                 $flags[] = 'gzip';
01278             } else {
01279                 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
01280             }
01281         }
01282         return implode( ',', $flags );
01283     }
01284 
01292     public static function decompressRevisionText( $text, $flags ) {
01293         if ( in_array( 'gzip', $flags ) ) {
01294             # Deal with optional compression of archived pages.
01295             # This can be done periodically via maintenance/compressOld.php, and
01296             # as pages are saved if $wgCompressRevisions is set.
01297             $text = gzinflate( $text );
01298         }
01299 
01300         if ( in_array( 'object', $flags ) ) {
01301             # Generic compressed storage
01302             $obj = unserialize( $text );
01303             if ( !is_object( $obj ) ) {
01304                 // Invalid object
01305                 return false;
01306             }
01307             $text = $obj->getText();
01308         }
01309 
01310         global $wgLegacyEncoding;
01311         if ( $text !== false && $wgLegacyEncoding
01312             && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
01313         ) {
01314             # Old revisions kept around in a legacy encoding?
01315             # Upconvert on demand.
01316             # ("utf8" checked for compatibility with some broken
01317             #  conversion scripts 2008-12-30)
01318             global $wgContLang;
01319             $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
01320         }
01321 
01322         return $text;
01323     }
01324 
01333     public function insertOn( $dbw ) {
01334         global $wgDefaultExternalStore, $wgContentHandlerUseDB;
01335 
01336         wfProfileIn( __METHOD__ );
01337 
01338         $this->checkContentModel();
01339 
01340         $data = $this->mText;
01341         $flags = self::compressRevisionText( $data );
01342 
01343         # Write to external storage if required
01344         if ( $wgDefaultExternalStore ) {
01345             // Store and get the URL
01346             $data = ExternalStore::insertToDefault( $data );
01347             if ( !$data ) {
01348                 wfProfileOut( __METHOD__ );
01349                 throw new MWException( "Unable to store text to external storage" );
01350             }
01351             if ( $flags ) {
01352                 $flags .= ',';
01353             }
01354             $flags .= 'external';
01355         }
01356 
01357         # Record the text (or external storage URL) to the text table
01358         if ( !isset( $this->mTextId ) ) {
01359             $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
01360             $dbw->insert( 'text',
01361                 array(
01362                     'old_id' => $old_id,
01363                     'old_text' => $data,
01364                     'old_flags' => $flags,
01365                 ), __METHOD__
01366             );
01367             $this->mTextId = $dbw->insertId();
01368         }
01369 
01370         if ( $this->mComment === null ) {
01371             $this->mComment = "";
01372         }
01373 
01374         # Record the edit in revisions
01375         $rev_id = isset( $this->mId )
01376             ? $this->mId
01377             : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
01378         $row = array(
01379             'rev_id'         => $rev_id,
01380             'rev_page'       => $this->mPage,
01381             'rev_text_id'    => $this->mTextId,
01382             'rev_comment'    => $this->mComment,
01383             'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
01384             'rev_user'       => $this->mUser,
01385             'rev_user_text'  => $this->mUserText,
01386             'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
01387             'rev_deleted'    => $this->mDeleted,
01388             'rev_len'        => $this->mSize,
01389             'rev_parent_id'  => is_null( $this->mParentId )
01390                 ? $this->getPreviousRevisionId( $dbw )
01391                 : $this->mParentId,
01392             'rev_sha1'       => is_null( $this->mSha1 )
01393                 ? Revision::base36Sha1( $this->mText )
01394                 : $this->mSha1,
01395         );
01396 
01397         if ( $wgContentHandlerUseDB ) {
01398             //NOTE: Store null for the default model and format, to save space.
01399             //XXX: Makes the DB sensitive to changed defaults.
01400             // Make this behavior optional? Only in miser mode?
01401 
01402             $model = $this->getContentModel();
01403             $format = $this->getContentFormat();
01404 
01405             $title = $this->getTitle();
01406 
01407             if ( $title === null ) {
01408                 wfProfileOut( __METHOD__ );
01409                 throw new MWException( "Insufficient information to determine the title of the "
01410                     . "revision's page!" );
01411             }
01412 
01413             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01414             $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
01415 
01416             $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
01417             $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
01418         }
01419 
01420         $dbw->insert( 'revision', $row, __METHOD__ );
01421 
01422         $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
01423 
01424         wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
01425 
01426         wfProfileOut( __METHOD__ );
01427         return $this->mId;
01428     }
01429 
01430     protected function checkContentModel() {
01431         global $wgContentHandlerUseDB;
01432 
01433         $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
01434 
01435         $model = $this->getContentModel();
01436         $format = $this->getContentFormat();
01437         $handler = $this->getContentHandler();
01438 
01439         if ( !$handler->isSupportedFormat( $format ) ) {
01440             $t = $title->getPrefixedDBkey();
01441 
01442             throw new MWException( "Can't use format $format with content model $model on $t" );
01443         }
01444 
01445         if ( !$wgContentHandlerUseDB && $title ) {
01446             // if $wgContentHandlerUseDB is not set,
01447             // all revisions must use the default content model and format.
01448 
01449             $defaultModel = ContentHandler::getDefaultModelFor( $title );
01450             $defaultHandler = ContentHandler::getForModelID( $defaultModel );
01451             $defaultFormat = $defaultHandler->getDefaultFormat();
01452 
01453             if ( $this->getContentModel() != $defaultModel ) {
01454                 $t = $title->getPrefixedDBkey();
01455 
01456                 throw new MWException( "Can't save non-default content model with "
01457                     . "\$wgContentHandlerUseDB disabled: model is $model, "
01458                     . "default for $t is $defaultModel" );
01459             }
01460 
01461             if ( $this->getContentFormat() != $defaultFormat ) {
01462                 $t = $title->getPrefixedDBkey();
01463 
01464                 throw new MWException( "Can't use non-default content format with "
01465                     . "\$wgContentHandlerUseDB disabled: format is $format, "
01466                     . "default for $t is $defaultFormat" );
01467             }
01468         }
01469 
01470         $content = $this->getContent( Revision::RAW );
01471 
01472         if ( !$content || !$content->isValid() ) {
01473             $t = $title->getPrefixedDBkey();
01474 
01475             throw new MWException( "Content of $t is not valid! Content model is $model" );
01476         }
01477     }
01478 
01484     public static function base36Sha1( $text ) {
01485         return wfBaseConvert( sha1( $text ), 16, 36, 31 );
01486     }
01487 
01494     protected function loadText() {
01495         wfProfileIn( __METHOD__ );
01496 
01497         // Caching may be beneficial for massive use of external storage
01498         global $wgRevisionCacheExpiry, $wgMemc;
01499         $textId = $this->getTextId();
01500         $key = wfMemcKey( 'revisiontext', 'textid', $textId );
01501         if ( $wgRevisionCacheExpiry ) {
01502             $text = $wgMemc->get( $key );
01503             if ( is_string( $text ) ) {
01504                 wfDebug( __METHOD__ . ": got id $textId from cache\n" );
01505                 wfProfileOut( __METHOD__ );
01506                 return $text;
01507             }
01508         }
01509 
01510         // If we kept data for lazy extraction, use it now...
01511         if ( isset( $this->mTextRow ) ) {
01512             $row = $this->mTextRow;
01513             $this->mTextRow = null;
01514         } else {
01515             $row = null;
01516         }
01517 
01518         if ( !$row ) {
01519             // Text data is immutable; check slaves first.
01520             $dbr = wfGetDB( DB_SLAVE );
01521             $row = $dbr->selectRow( 'text',
01522                 array( 'old_text', 'old_flags' ),
01523                 array( 'old_id' => $textId ),
01524                 __METHOD__ );
01525         }
01526 
01527         // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was
01528         // used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
01529         $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
01530         if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) {
01531             $dbw = wfGetDB( DB_MASTER );
01532             $row = $dbw->selectRow( 'text',
01533                 array( 'old_text', 'old_flags' ),
01534                 array( 'old_id' => $textId ),
01535                 __METHOD__,
01536                 $forUpdate ? array( 'FOR UPDATE' ) : array() );
01537         }
01538 
01539         if ( !$row ) {
01540             wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
01541         }
01542 
01543         $text = self::getRevisionText( $row );
01544         if ( $row && $text === false ) {
01545             wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
01546         }
01547 
01548         # No negative caching -- negative hits on text rows may be due to corrupted slave servers
01549         if ( $wgRevisionCacheExpiry && $text !== false ) {
01550             $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
01551         }
01552 
01553         wfProfileOut( __METHOD__ );
01554 
01555         return $text;
01556     }
01557 
01572     public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
01573         global $wgContentHandlerUseDB;
01574 
01575         wfProfileIn( __METHOD__ );
01576 
01577         $fields = array( 'page_latest', 'page_namespace', 'page_title',
01578                         'rev_text_id', 'rev_len', 'rev_sha1' );
01579 
01580         if ( $wgContentHandlerUseDB ) {
01581             $fields[] = 'rev_content_model';
01582             $fields[] = 'rev_content_format';
01583         }
01584 
01585         $current = $dbw->selectRow(
01586             array( 'page', 'revision' ),
01587             $fields,
01588             array(
01589                 'page_id' => $pageId,
01590                 'page_latest=rev_id',
01591                 ),
01592             __METHOD__ );
01593 
01594         if ( $current ) {
01595             $row = array(
01596                 'page'       => $pageId,
01597                 'comment'    => $summary,
01598                 'minor_edit' => $minor,
01599                 'text_id'    => $current->rev_text_id,
01600                 'parent_id'  => $current->page_latest,
01601                 'len'        => $current->rev_len,
01602                 'sha1'       => $current->rev_sha1
01603             );
01604 
01605             if ( $wgContentHandlerUseDB ) {
01606                 $row['content_model'] = $current->rev_content_model;
01607                 $row['content_format'] = $current->rev_content_format;
01608             }
01609 
01610             $revision = new Revision( $row );
01611             $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
01612         } else {
01613             $revision = null;
01614         }
01615 
01616         wfProfileOut( __METHOD__ );
01617         return $revision;
01618     }
01619 
01630     public function userCan( $field, User $user = null ) {
01631         return self::userCanBitfield( $this->mDeleted, $field, $user );
01632     }
01633 
01646     public static function userCanBitfield( $bitfield, $field, User $user = null ) {
01647         if ( $bitfield & $field ) { // aspect is deleted
01648             if ( $bitfield & self::DELETED_RESTRICTED ) {
01649                 $permission = 'suppressrevision';
01650             } elseif ( $field & self::DELETED_TEXT ) {
01651                 $permission = 'deletedtext';
01652             } else {
01653                 $permission = 'deletedhistory';
01654             }
01655             wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
01656             if ( $user === null ) {
01657                 global $wgUser;
01658                 $user = $wgUser;
01659             }
01660             return $user->isAllowed( $permission );
01661         } else {
01662             return true;
01663         }
01664     }
01665 
01673     static function getTimestampFromId( $title, $id ) {
01674         $dbr = wfGetDB( DB_SLAVE );
01675         // Casting fix for databases that can't take '' for rev_id
01676         if ( $id == '' ) {
01677             $id = 0;
01678         }
01679         $conds = array( 'rev_id' => $id );
01680         $conds['rev_page'] = $title->getArticleID();
01681         $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01682         if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
01683             # Not in slave, try master
01684             $dbw = wfGetDB( DB_MASTER );
01685             $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
01686         }
01687         return wfTimestamp( TS_MW, $timestamp );
01688     }
01689 
01697     static function countByPageId( $db, $id ) {
01698         $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
01699             array( 'rev_page' => $id ), __METHOD__ );
01700         if ( $row ) {
01701             return $row->revCount;
01702         }
01703         return 0;
01704     }
01705 
01713     static function countByTitle( $db, $title ) {
01714         $id = $title->getArticleID();
01715         if ( $id ) {
01716             return self::countByPageId( $db, $id );
01717         }
01718         return 0;
01719     }
01720 
01736     public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
01737         if ( !$userId ) {
01738             return false;
01739         }
01740 
01741         if ( is_int( $db ) ) {
01742             $db = wfGetDB( $db );
01743         }
01744 
01745         $res = $db->select( 'revision',
01746             'rev_user',
01747             array(
01748                 'rev_page' => $pageId,
01749                 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
01750             ),
01751             __METHOD__,
01752             array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
01753         foreach ( $res as $row ) {
01754             if ( $row->rev_user != $userId ) {
01755                 return false;
01756             }
01757         }
01758         return true;
01759     }
01760 }