MediaWiki
REL1_21
|
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 }