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