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