MediaWiki
REL1_24
|
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 $mTextId; 00045 00049 protected $mTextRow; 00050 00054 protected $mTitle; 00055 protected $mCurrent; 00056 protected $mContentModel; 00057 protected $mContentFormat; 00058 00062 protected $mContent; 00063 00067 protected $mContentHandler; 00068 00072 protected $mQueryFlags = 0; 00073 00074 // Revision deletion constants 00075 const DELETED_TEXT = 1; 00076 const DELETED_COMMENT = 2; 00077 const DELETED_USER = 4; 00078 const DELETED_RESTRICTED = 8; 00079 const SUPPRESSED_USER = 12; // convenience 00080 00081 // Audience options for accessors 00082 const FOR_PUBLIC = 1; 00083 const FOR_THIS_USER = 2; 00084 const RAW = 3; 00085 00098 public static function newFromId( $id, $flags = 0 ) { 00099 return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags ); 00100 } 00101 00116 public static function newFromTitle( $title, $id = 0, $flags = 0 ) { 00117 $conds = array( 00118 'page_namespace' => $title->getNamespace(), 00119 'page_title' => $title->getDBkey() 00120 ); 00121 if ( $id ) { 00122 // Use the specified ID 00123 $conds['rev_id'] = $id; 00124 return self::newFromConds( $conds, (int)$flags ); 00125 } else { 00126 // Use a join to get the latest revision 00127 $conds[] = 'rev_id=page_latest'; 00128 $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); 00129 return self::loadFromConds( $db, $conds, $flags ); 00130 } 00131 } 00132 00147 public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) { 00148 $conds = array( 'page_id' => $pageId ); 00149 if ( $revId ) { 00150 $conds['rev_id'] = $revId; 00151 } else { 00152 // Use a join to get the latest revision 00153 $conds[] = 'rev_id = page_latest'; 00154 } 00155 return self::newFromConds( $conds, (int)$flags ); 00156 } 00157 00169 public static function newFromArchiveRow( $row, $overrides = array() ) { 00170 global $wgContentHandlerUseDB; 00171 00172 $attribs = $overrides + array( 00173 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null, 00174 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null, 00175 'comment' => $row->ar_comment, 00176 'user' => $row->ar_user, 00177 'user_text' => $row->ar_user_text, 00178 'timestamp' => $row->ar_timestamp, 00179 'minor_edit' => $row->ar_minor_edit, 00180 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null, 00181 'deleted' => $row->ar_deleted, 00182 'len' => $row->ar_len, 00183 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null, 00184 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null, 00185 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null, 00186 ); 00187 00188 if ( !$wgContentHandlerUseDB ) { 00189 unset( $attribs['content_model'] ); 00190 unset( $attribs['content_format'] ); 00191 } 00192 00193 if ( !isset( $attribs['title'] ) 00194 && isset( $row->ar_namespace ) 00195 && isset( $row->ar_title ) ) { 00196 00197 $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title ); 00198 } 00199 00200 if ( isset( $row->ar_text ) && !$row->ar_text_id ) { 00201 // Pre-1.5 ar_text row 00202 $attribs['text'] = self::getRevisionText( $row, 'ar_' ); 00203 if ( $attribs['text'] === false ) { 00204 throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' ); 00205 } 00206 } 00207 return new self( $attribs ); 00208 } 00209 00216 public static function newFromRow( $row ) { 00217 return new self( $row ); 00218 } 00219 00228 public static function loadFromId( $db, $id ) { 00229 return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) ); 00230 } 00231 00242 public static function loadFromPageId( $db, $pageid, $id = 0 ) { 00243 $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ); 00244 if ( $id ) { 00245 $conds['rev_id'] = intval( $id ); 00246 } else { 00247 $conds[] = 'rev_id=page_latest'; 00248 } 00249 return self::loadFromConds( $db, $conds ); 00250 } 00251 00262 public static function loadFromTitle( $db, $title, $id = 0 ) { 00263 if ( $id ) { 00264 $matchId = intval( $id ); 00265 } else { 00266 $matchId = 'page_latest'; 00267 } 00268 return self::loadFromConds( $db, 00269 array( 00270 "rev_id=$matchId", 00271 'page_namespace' => $title->getNamespace(), 00272 'page_title' => $title->getDBkey() 00273 ) 00274 ); 00275 } 00276 00287 public static function loadFromTimestamp( $db, $title, $timestamp ) { 00288 return self::loadFromConds( $db, 00289 array( 00290 'rev_timestamp' => $db->timestamp( $timestamp ), 00291 'page_namespace' => $title->getNamespace(), 00292 'page_title' => $title->getDBkey() 00293 ) 00294 ); 00295 } 00296 00304 private static function newFromConds( $conditions, $flags = 0 ) { 00305 $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); 00306 $rev = self::loadFromConds( $db, $conditions, $flags ); 00307 if ( $rev === null && wfGetLB()->getServerCount() > 1 ) { 00308 if ( !( $flags & self::READ_LATEST ) ) { 00309 $dbw = wfGetDB( DB_MASTER ); 00310 $rev = self::loadFromConds( $dbw, $conditions, $flags ); 00311 } 00312 } 00313 if ( $rev ) { 00314 $rev->mQueryFlags = $flags; 00315 } 00316 return $rev; 00317 } 00318 00328 private static function loadFromConds( $db, $conditions, $flags = 0 ) { 00329 $res = self::fetchFromConds( $db, $conditions, $flags ); 00330 if ( $res ) { 00331 $row = $res->fetchObject(); 00332 if ( $row ) { 00333 $ret = new Revision( $row ); 00334 return $ret; 00335 } 00336 } 00337 $ret = null; 00338 return $ret; 00339 } 00340 00349 public static function fetchRevision( $title ) { 00350 return self::fetchFromConds( 00351 wfGetDB( DB_SLAVE ), 00352 array( 00353 'rev_id=page_latest', 00354 'page_namespace' => $title->getNamespace(), 00355 'page_title' => $title->getDBkey() 00356 ) 00357 ); 00358 } 00359 00370 private static function fetchFromConds( $db, $conditions, $flags = 0 ) { 00371 $fields = array_merge( 00372 self::selectFields(), 00373 self::selectPageFields(), 00374 self::selectUserFields() 00375 ); 00376 $options = array( 'LIMIT' => 1 ); 00377 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) { 00378 $options[] = 'FOR UPDATE'; 00379 } 00380 return $db->select( 00381 array( 'revision', 'page', 'user' ), 00382 $fields, 00383 $conditions, 00384 __METHOD__, 00385 $options, 00386 array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ) 00387 ); 00388 } 00389 00396 public static function userJoinCond() { 00397 return array( 'LEFT JOIN', array( 'rev_user != 0', 'user_id = rev_user' ) ); 00398 } 00399 00406 public static function pageJoinCond() { 00407 return array( 'INNER JOIN', array( 'page_id = rev_page' ) ); 00408 } 00409 00415 public static function selectFields() { 00416 global $wgContentHandlerUseDB; 00417 00418 $fields = array( 00419 'rev_id', 00420 'rev_page', 00421 'rev_text_id', 00422 'rev_timestamp', 00423 'rev_comment', 00424 'rev_user_text', 00425 'rev_user', 00426 'rev_minor_edit', 00427 'rev_deleted', 00428 'rev_len', 00429 'rev_parent_id', 00430 'rev_sha1', 00431 ); 00432 00433 if ( $wgContentHandlerUseDB ) { 00434 $fields[] = 'rev_content_format'; 00435 $fields[] = 'rev_content_model'; 00436 } 00437 00438 return $fields; 00439 } 00440 00446 public static function selectArchiveFields() { 00447 global $wgContentHandlerUseDB; 00448 $fields = array( 00449 'ar_id', 00450 'ar_page_id', 00451 'ar_rev_id', 00452 'ar_text', 00453 'ar_text_id', 00454 'ar_timestamp', 00455 'ar_comment', 00456 'ar_user_text', 00457 'ar_user', 00458 'ar_minor_edit', 00459 'ar_deleted', 00460 'ar_len', 00461 'ar_parent_id', 00462 'ar_sha1', 00463 ); 00464 00465 if ( $wgContentHandlerUseDB ) { 00466 $fields[] = 'ar_content_format'; 00467 $fields[] = 'ar_content_model'; 00468 } 00469 return $fields; 00470 } 00471 00477 public static function selectTextFields() { 00478 return array( 00479 'old_text', 00480 'old_flags' 00481 ); 00482 } 00483 00488 public static function selectPageFields() { 00489 return array( 00490 'page_namespace', 00491 'page_title', 00492 'page_id', 00493 'page_latest', 00494 'page_is_redirect', 00495 'page_len', 00496 ); 00497 } 00498 00503 public static function selectUserFields() { 00504 return array( 'user_name' ); 00505 } 00506 00513 public static function getParentLengths( $db, array $revIds ) { 00514 $revLens = array(); 00515 if ( !$revIds ) { 00516 return $revLens; // empty 00517 } 00518 wfProfileIn( __METHOD__ ); 00519 $res = $db->select( 'revision', 00520 array( 'rev_id', 'rev_len' ), 00521 array( 'rev_id' => $revIds ), 00522 __METHOD__ ); 00523 foreach ( $res as $row ) { 00524 $revLens[$row->rev_id] = $row->rev_len; 00525 } 00526 wfProfileOut( __METHOD__ ); 00527 return $revLens; 00528 } 00529 00537 function __construct( $row ) { 00538 if ( is_object( $row ) ) { 00539 $this->mId = intval( $row->rev_id ); 00540 $this->mPage = intval( $row->rev_page ); 00541 $this->mTextId = intval( $row->rev_text_id ); 00542 $this->mComment = $row->rev_comment; 00543 $this->mUser = intval( $row->rev_user ); 00544 $this->mMinorEdit = intval( $row->rev_minor_edit ); 00545 $this->mTimestamp = $row->rev_timestamp; 00546 $this->mDeleted = intval( $row->rev_deleted ); 00547 00548 if ( !isset( $row->rev_parent_id ) ) { 00549 $this->mParentId = null; 00550 } else { 00551 $this->mParentId = intval( $row->rev_parent_id ); 00552 } 00553 00554 if ( !isset( $row->rev_len ) ) { 00555 $this->mSize = null; 00556 } else { 00557 $this->mSize = intval( $row->rev_len ); 00558 } 00559 00560 if ( !isset( $row->rev_sha1 ) ) { 00561 $this->mSha1 = null; 00562 } else { 00563 $this->mSha1 = $row->rev_sha1; 00564 } 00565 00566 if ( isset( $row->page_latest ) ) { 00567 $this->mCurrent = ( $row->rev_id == $row->page_latest ); 00568 $this->mTitle = Title::newFromRow( $row ); 00569 } else { 00570 $this->mCurrent = false; 00571 $this->mTitle = null; 00572 } 00573 00574 if ( !isset( $row->rev_content_model ) ) { 00575 $this->mContentModel = null; # determine on demand if needed 00576 } else { 00577 $this->mContentModel = strval( $row->rev_content_model ); 00578 } 00579 00580 if ( !isset( $row->rev_content_format ) ) { 00581 $this->mContentFormat = null; # determine on demand if needed 00582 } else { 00583 $this->mContentFormat = strval( $row->rev_content_format ); 00584 } 00585 00586 // Lazy extraction... 00587 $this->mText = null; 00588 if ( isset( $row->old_text ) ) { 00589 $this->mTextRow = $row; 00590 } else { 00591 // 'text' table row entry will be lazy-loaded 00592 $this->mTextRow = null; 00593 } 00594 00595 // Use user_name for users and rev_user_text for IPs... 00596 $this->mUserText = null; // lazy load if left null 00597 if ( $this->mUser == 0 ) { 00598 $this->mUserText = $row->rev_user_text; // IP user 00599 } elseif ( isset( $row->user_name ) ) { 00600 $this->mUserText = $row->user_name; // logged-in user 00601 } 00602 $this->mOrigUserText = $row->rev_user_text; 00603 } elseif ( is_array( $row ) ) { 00604 // Build a new revision to be saved... 00605 global $wgUser; // ugh 00606 00607 # if we have a content object, use it to set the model and type 00608 if ( !empty( $row['content'] ) ) { 00609 // @todo when is that set? test with external store setup! check out insertOn() [dk] 00610 if ( !empty( $row['text_id'] ) ) { 00611 throw new MWException( "Text already stored in external store (id {$row['text_id']}), " . 00612 "can't serialize content object" ); 00613 } 00614 00615 $row['content_model'] = $row['content']->getModel(); 00616 # note: mContentFormat is initializes later accordingly 00617 # note: content is serialized later in this method! 00618 # also set text to null? 00619 } 00620 00621 $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; 00622 $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; 00623 $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; 00624 $this->mUserText = isset( $row['user_text'] ) 00625 ? strval( $row['user_text'] ) : $wgUser->getName(); 00626 $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); 00627 $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; 00628 $this->mTimestamp = isset( $row['timestamp'] ) 00629 ? strval( $row['timestamp'] ) : wfTimestampNow(); 00630 $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; 00631 $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; 00632 $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; 00633 $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; 00634 00635 $this->mContentModel = isset( $row['content_model'] ) 00636 ? strval( $row['content_model'] ) : null; 00637 $this->mContentFormat = isset( $row['content_format'] ) 00638 ? strval( $row['content_format'] ) : null; 00639 00640 // Enforce spacing trimming on supplied text 00641 $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; 00642 $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; 00643 $this->mTextRow = null; 00644 00645 $this->mTitle = isset( $row['title'] ) ? $row['title'] : null; 00646 00647 // if we have a Content object, override mText and mContentModel 00648 if ( !empty( $row['content'] ) ) { 00649 if ( !( $row['content'] instanceof Content ) ) { 00650 throw new MWException( '`content` field must contain a Content object.' ); 00651 } 00652 00653 $handler = $this->getContentHandler(); 00654 $this->mContent = $row['content']; 00655 00656 $this->mContentModel = $this->mContent->getModel(); 00657 $this->mContentHandler = null; 00658 00659 $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() ); 00660 } elseif ( $this->mText !== null ) { 00661 $handler = $this->getContentHandler(); 00662 $this->mContent = $handler->unserializeContent( $this->mText ); 00663 } 00664 00665 // If we have a Title object, make sure it is consistent with mPage. 00666 if ( $this->mTitle && $this->mTitle->exists() ) { 00667 if ( $this->mPage === null ) { 00668 // if the page ID wasn't known, set it now 00669 $this->mPage = $this->mTitle->getArticleID(); 00670 } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) { 00671 // Got different page IDs. This may be legit (e.g. during undeletion), 00672 // but it seems worth mentioning it in the log. 00673 wfDebug( "Page ID " . $this->mPage . " mismatches the ID " . 00674 $this->mTitle->getArticleID() . " provided by the Title object." ); 00675 } 00676 } 00677 00678 $this->mCurrent = false; 00679 00680 // If we still have no length, see it we have the text to figure it out 00681 if ( !$this->mSize ) { 00682 if ( $this->mContent !== null ) { 00683 $this->mSize = $this->mContent->getSize(); 00684 } else { 00685 #NOTE: this should never happen if we have either text or content object! 00686 $this->mSize = null; 00687 } 00688 } 00689 00690 // Same for sha1 00691 if ( $this->mSha1 === null ) { 00692 $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText ); 00693 } 00694 00695 // force lazy init 00696 $this->getContentModel(); 00697 $this->getContentFormat(); 00698 } else { 00699 throw new MWException( 'Revision constructor passed invalid row format.' ); 00700 } 00701 $this->mUnpatrolled = null; 00702 } 00703 00709 public function getId() { 00710 return $this->mId; 00711 } 00712 00719 public function setId( $id ) { 00720 $this->mId = $id; 00721 } 00722 00728 public function getTextId() { 00729 return $this->mTextId; 00730 } 00731 00737 public function getParentId() { 00738 return $this->mParentId; 00739 } 00740 00746 public function getSize() { 00747 return $this->mSize; 00748 } 00749 00755 public function getSha1() { 00756 return $this->mSha1; 00757 } 00758 00766 public function getTitle() { 00767 if ( $this->mTitle !== null ) { 00768 return $this->mTitle; 00769 } 00770 //rev_id is defined as NOT NULL, but this revision may not yet have been inserted. 00771 if ( $this->mId !== null ) { 00772 $dbr = wfGetDB( DB_SLAVE ); 00773 $row = $dbr->selectRow( 00774 array( 'page', 'revision' ), 00775 self::selectPageFields(), 00776 array( 'page_id=rev_page', 00777 'rev_id' => $this->mId ), 00778 __METHOD__ ); 00779 if ( $row ) { 00780 $this->mTitle = Title::newFromRow( $row ); 00781 } 00782 } 00783 00784 if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) { 00785 $this->mTitle = Title::newFromID( $this->mPage ); 00786 } 00787 00788 return $this->mTitle; 00789 } 00790 00796 public function setTitle( $title ) { 00797 $this->mTitle = $title; 00798 } 00799 00805 public function getPage() { 00806 return $this->mPage; 00807 } 00808 00822 public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) { 00823 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { 00824 return 0; 00825 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { 00826 return 0; 00827 } else { 00828 return $this->mUser; 00829 } 00830 } 00831 00837 public function getRawUser() { 00838 return $this->mUser; 00839 } 00840 00854 public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { 00855 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { 00856 return ''; 00857 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { 00858 return ''; 00859 } else { 00860 return $this->getRawUserText(); 00861 } 00862 } 00863 00869 public function getRawUserText() { 00870 if ( $this->mUserText === null ) { 00871 $this->mUserText = User::whoIs( $this->mUser ); // load on demand 00872 if ( $this->mUserText === false ) { 00873 # This shouldn't happen, but it can if the wiki was recovered 00874 # via importing revs and there is no user table entry yet. 00875 $this->mUserText = $this->mOrigUserText; 00876 } 00877 } 00878 return $this->mUserText; 00879 } 00880 00894 function getComment( $audience = self::FOR_PUBLIC, User $user = null ) { 00895 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { 00896 return ''; 00897 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) { 00898 return ''; 00899 } else { 00900 return $this->mComment; 00901 } 00902 } 00903 00909 public function getRawComment() { 00910 return $this->mComment; 00911 } 00912 00916 public function isMinor() { 00917 return (bool)$this->mMinorEdit; 00918 } 00919 00923 public function isUnpatrolled() { 00924 if ( $this->mUnpatrolled !== null ) { 00925 return $this->mUnpatrolled; 00926 } 00927 $rc = $this->getRecentChange(); 00928 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) { 00929 $this->mUnpatrolled = $rc->getAttribute( 'rc_id' ); 00930 } else { 00931 $this->mUnpatrolled = 0; 00932 } 00933 return $this->mUnpatrolled; 00934 } 00935 00942 public function getRecentChange() { 00943 $dbr = wfGetDB( DB_SLAVE ); 00944 return RecentChange::newFromConds( 00945 array( 00946 'rc_user_text' => $this->getRawUserText(), 00947 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ), 00948 'rc_this_oldid' => $this->getId() 00949 ), 00950 __METHOD__ 00951 ); 00952 } 00953 00959 public function isDeleted( $field ) { 00960 return ( $this->mDeleted & $field ) == $field; 00961 } 00962 00968 public function getVisibility() { 00969 return (int)$this->mDeleted; 00970 } 00971 00988 public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { 00989 ContentHandler::deprecated( __METHOD__, '1.21' ); 00990 00991 $content = $this->getContent( $audience, $user ); 00992 return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable 00993 } 00994 01009 public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { 01010 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { 01011 return null; 01012 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { 01013 return null; 01014 } else { 01015 return $this->getContentInternal(); 01016 } 01017 } 01018 01027 public function getRawText() { 01028 ContentHandler::deprecated( __METHOD__, "1.21" ); 01029 return $this->getText( self::RAW ); 01030 } 01031 01038 public function getSerializedData() { 01039 if ( $this->mText === null ) { 01040 $this->mText = $this->loadText(); 01041 } 01042 01043 return $this->mText; 01044 } 01045 01055 protected function getContentInternal() { 01056 if ( $this->mContent === null ) { 01057 // Revision is immutable. Load on demand: 01058 if ( $this->mText === null ) { 01059 $this->mText = $this->loadText(); 01060 } 01061 01062 if ( $this->mText !== null && $this->mText !== false ) { 01063 // Unserialize content 01064 $handler = $this->getContentHandler(); 01065 $format = $this->getContentFormat(); 01066 01067 $this->mContent = $handler->unserializeContent( $this->mText, $format ); 01068 } else { 01069 $this->mContent = false; // negative caching! 01070 } 01071 } 01072 01073 // NOTE: copy() will return $this for immutable content objects 01074 return $this->mContent ? $this->mContent->copy() : null; 01075 } 01076 01087 public function getContentModel() { 01088 if ( !$this->mContentModel ) { 01089 $title = $this->getTitle(); 01090 $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT ); 01091 01092 assert( !empty( $this->mContentModel ) ); 01093 } 01094 01095 return $this->mContentModel; 01096 } 01097 01107 public function getContentFormat() { 01108 if ( !$this->mContentFormat ) { 01109 $handler = $this->getContentHandler(); 01110 $this->mContentFormat = $handler->getDefaultFormat(); 01111 01112 assert( !empty( $this->mContentFormat ) ); 01113 } 01114 01115 return $this->mContentFormat; 01116 } 01117 01124 public function getContentHandler() { 01125 if ( !$this->mContentHandler ) { 01126 $model = $this->getContentModel(); 01127 $this->mContentHandler = ContentHandler::getForModelID( $model ); 01128 01129 $format = $this->getContentFormat(); 01130 01131 if ( !$this->mContentHandler->isSupportedFormat( $format ) ) { 01132 throw new MWException( "Oops, the content format $format is not supported for " 01133 . "this content model, $model" ); 01134 } 01135 } 01136 01137 return $this->mContentHandler; 01138 } 01139 01143 public function getTimestamp() { 01144 return wfTimestamp( TS_MW, $this->mTimestamp ); 01145 } 01146 01150 public function isCurrent() { 01151 return $this->mCurrent; 01152 } 01153 01159 public function getPrevious() { 01160 if ( $this->getTitle() ) { 01161 $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); 01162 if ( $prev ) { 01163 return self::newFromTitle( $this->getTitle(), $prev ); 01164 } 01165 } 01166 return null; 01167 } 01168 01174 public function getNext() { 01175 if ( $this->getTitle() ) { 01176 $next = $this->getTitle()->getNextRevisionID( $this->getId() ); 01177 if ( $next ) { 01178 return self::newFromTitle( $this->getTitle(), $next ); 01179 } 01180 } 01181 return null; 01182 } 01183 01191 private function getPreviousRevisionId( $db ) { 01192 if ( $this->mPage === null ) { 01193 return 0; 01194 } 01195 # Use page_latest if ID is not given 01196 if ( !$this->mId ) { 01197 $prevId = $db->selectField( 'page', 'page_latest', 01198 array( 'page_id' => $this->mPage ), 01199 __METHOD__ ); 01200 } else { 01201 $prevId = $db->selectField( 'revision', 'rev_id', 01202 array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ), 01203 __METHOD__, 01204 array( 'ORDER BY' => 'rev_id DESC' ) ); 01205 } 01206 return intval( $prevId ); 01207 } 01208 01222 public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) { 01223 wfProfileIn( __METHOD__ ); 01224 01225 # Get data 01226 $textField = $prefix . 'text'; 01227 $flagsField = $prefix . 'flags'; 01228 01229 if ( isset( $row->$flagsField ) ) { 01230 $flags = explode( ',', $row->$flagsField ); 01231 } else { 01232 $flags = array(); 01233 } 01234 01235 if ( isset( $row->$textField ) ) { 01236 $text = $row->$textField; 01237 } else { 01238 wfProfileOut( __METHOD__ ); 01239 return false; 01240 } 01241 01242 # Use external methods for external objects, text in table is URL-only then 01243 if ( in_array( 'external', $flags ) ) { 01244 $url = $text; 01245 $parts = explode( '://', $url, 2 ); 01246 if ( count( $parts ) == 1 || $parts[1] == '' ) { 01247 wfProfileOut( __METHOD__ ); 01248 return false; 01249 } 01250 $text = ExternalStore::fetchFromURL( $url, array( 'wiki' => $wiki ) ); 01251 } 01252 01253 // If the text was fetched without an error, convert it 01254 if ( $text !== false ) { 01255 $text = self::decompressRevisionText( $text, $flags ); 01256 } 01257 wfProfileOut( __METHOD__ ); 01258 return $text; 01259 } 01260 01271 public static function compressRevisionText( &$text ) { 01272 global $wgCompressRevisions; 01273 $flags = array(); 01274 01275 # Revisions not marked this way will be converted 01276 # on load if $wgLegacyCharset is set in the future. 01277 $flags[] = 'utf-8'; 01278 01279 if ( $wgCompressRevisions ) { 01280 if ( function_exists( 'gzdeflate' ) ) { 01281 $text = gzdeflate( $text ); 01282 $flags[] = 'gzip'; 01283 } else { 01284 wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" ); 01285 } 01286 } 01287 return implode( ',', $flags ); 01288 } 01289 01297 public static function decompressRevisionText( $text, $flags ) { 01298 if ( in_array( 'gzip', $flags ) ) { 01299 # Deal with optional compression of archived pages. 01300 # This can be done periodically via maintenance/compressOld.php, and 01301 # as pages are saved if $wgCompressRevisions is set. 01302 $text = gzinflate( $text ); 01303 } 01304 01305 if ( in_array( 'object', $flags ) ) { 01306 # Generic compressed storage 01307 $obj = unserialize( $text ); 01308 if ( !is_object( $obj ) ) { 01309 // Invalid object 01310 return false; 01311 } 01312 $text = $obj->getText(); 01313 } 01314 01315 global $wgLegacyEncoding; 01316 if ( $text !== false && $wgLegacyEncoding 01317 && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) 01318 ) { 01319 # Old revisions kept around in a legacy encoding? 01320 # Upconvert on demand. 01321 # ("utf8" checked for compatibility with some broken 01322 # conversion scripts 2008-12-30) 01323 global $wgContLang; 01324 $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text ); 01325 } 01326 01327 return $text; 01328 } 01329 01338 public function insertOn( $dbw ) { 01339 global $wgDefaultExternalStore, $wgContentHandlerUseDB; 01340 01341 wfProfileIn( __METHOD__ ); 01342 01343 $this->checkContentModel(); 01344 01345 $data = $this->mText; 01346 $flags = self::compressRevisionText( $data ); 01347 01348 # Write to external storage if required 01349 if ( $wgDefaultExternalStore ) { 01350 // Store and get the URL 01351 $data = ExternalStore::insertToDefault( $data ); 01352 if ( !$data ) { 01353 wfProfileOut( __METHOD__ ); 01354 throw new MWException( "Unable to store text to external storage" ); 01355 } 01356 if ( $flags ) { 01357 $flags .= ','; 01358 } 01359 $flags .= 'external'; 01360 } 01361 01362 # Record the text (or external storage URL) to the text table 01363 if ( $this->mTextId === null ) { 01364 $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' ); 01365 $dbw->insert( 'text', 01366 array( 01367 'old_id' => $old_id, 01368 'old_text' => $data, 01369 'old_flags' => $flags, 01370 ), __METHOD__ 01371 ); 01372 $this->mTextId = $dbw->insertId(); 01373 } 01374 01375 if ( $this->mComment === null ) { 01376 $this->mComment = ""; 01377 } 01378 01379 # Record the edit in revisions 01380 $rev_id = $this->mId !== null 01381 ? $this->mId 01382 : $dbw->nextSequenceValue( 'revision_rev_id_seq' ); 01383 $row = array( 01384 'rev_id' => $rev_id, 01385 'rev_page' => $this->mPage, 01386 'rev_text_id' => $this->mTextId, 01387 'rev_comment' => $this->mComment, 01388 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, 01389 'rev_user' => $this->mUser, 01390 'rev_user_text' => $this->mUserText, 01391 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), 01392 'rev_deleted' => $this->mDeleted, 01393 'rev_len' => $this->mSize, 01394 'rev_parent_id' => $this->mParentId === null 01395 ? $this->getPreviousRevisionId( $dbw ) 01396 : $this->mParentId, 01397 'rev_sha1' => $this->mSha1 === null 01398 ? Revision::base36Sha1( $this->mText ) 01399 : $this->mSha1, 01400 ); 01401 01402 if ( $wgContentHandlerUseDB ) { 01403 //NOTE: Store null for the default model and format, to save space. 01404 //XXX: Makes the DB sensitive to changed defaults. 01405 // Make this behavior optional? Only in miser mode? 01406 01407 $model = $this->getContentModel(); 01408 $format = $this->getContentFormat(); 01409 01410 $title = $this->getTitle(); 01411 01412 if ( $title === null ) { 01413 wfProfileOut( __METHOD__ ); 01414 throw new MWException( "Insufficient information to determine the title of the " 01415 . "revision's page!" ); 01416 } 01417 01418 $defaultModel = ContentHandler::getDefaultModelFor( $title ); 01419 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat(); 01420 01421 $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model; 01422 $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format; 01423 } 01424 01425 $dbw->insert( 'revision', $row, __METHOD__ ); 01426 01427 $this->mId = $rev_id !== null ? $rev_id : $dbw->insertId(); 01428 01429 wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) ); 01430 01431 wfProfileOut( __METHOD__ ); 01432 return $this->mId; 01433 } 01434 01435 protected function checkContentModel() { 01436 global $wgContentHandlerUseDB; 01437 01438 $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted. 01439 01440 $model = $this->getContentModel(); 01441 $format = $this->getContentFormat(); 01442 $handler = $this->getContentHandler(); 01443 01444 if ( !$handler->isSupportedFormat( $format ) ) { 01445 $t = $title->getPrefixedDBkey(); 01446 01447 throw new MWException( "Can't use format $format with content model $model on $t" ); 01448 } 01449 01450 if ( !$wgContentHandlerUseDB && $title ) { 01451 // if $wgContentHandlerUseDB is not set, 01452 // all revisions must use the default content model and format. 01453 01454 $defaultModel = ContentHandler::getDefaultModelFor( $title ); 01455 $defaultHandler = ContentHandler::getForModelID( $defaultModel ); 01456 $defaultFormat = $defaultHandler->getDefaultFormat(); 01457 01458 if ( $this->getContentModel() != $defaultModel ) { 01459 $t = $title->getPrefixedDBkey(); 01460 01461 throw new MWException( "Can't save non-default content model with " 01462 . "\$wgContentHandlerUseDB disabled: model is $model, " 01463 . "default for $t is $defaultModel" ); 01464 } 01465 01466 if ( $this->getContentFormat() != $defaultFormat ) { 01467 $t = $title->getPrefixedDBkey(); 01468 01469 throw new MWException( "Can't use non-default content format with " 01470 . "\$wgContentHandlerUseDB disabled: format is $format, " 01471 . "default for $t is $defaultFormat" ); 01472 } 01473 } 01474 01475 $content = $this->getContent( Revision::RAW ); 01476 01477 if ( !$content || !$content->isValid() ) { 01478 $t = $title->getPrefixedDBkey(); 01479 01480 throw new MWException( "Content of $t is not valid! Content model is $model" ); 01481 } 01482 } 01483 01489 public static function base36Sha1( $text ) { 01490 return wfBaseConvert( sha1( $text ), 16, 36, 31 ); 01491 } 01492 01499 protected function loadText() { 01500 wfProfileIn( __METHOD__ ); 01501 01502 // Caching may be beneficial for massive use of external storage 01503 global $wgRevisionCacheExpiry, $wgMemc; 01504 $textId = $this->getTextId(); 01505 $key = wfMemcKey( 'revisiontext', 'textid', $textId ); 01506 if ( $wgRevisionCacheExpiry ) { 01507 $text = $wgMemc->get( $key ); 01508 if ( is_string( $text ) ) { 01509 wfDebug( __METHOD__ . ": got id $textId from cache\n" ); 01510 wfProfileOut( __METHOD__ ); 01511 return $text; 01512 } 01513 } 01514 01515 // If we kept data for lazy extraction, use it now... 01516 if ( $this->mTextRow !== null ) { 01517 $row = $this->mTextRow; 01518 $this->mTextRow = null; 01519 } else { 01520 $row = null; 01521 } 01522 01523 if ( !$row ) { 01524 // Text data is immutable; check slaves first. 01525 $dbr = wfGetDB( DB_SLAVE ); 01526 $row = $dbr->selectRow( 'text', 01527 array( 'old_text', 'old_flags' ), 01528 array( 'old_id' => $textId ), 01529 __METHOD__ ); 01530 } 01531 01532 // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was 01533 // used to fetch this revision to avoid missing the row due to REPEATABLE-READ. 01534 $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING ); 01535 if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) { 01536 $dbw = wfGetDB( DB_MASTER ); 01537 $row = $dbw->selectRow( 'text', 01538 array( 'old_text', 'old_flags' ), 01539 array( 'old_id' => $textId ), 01540 __METHOD__, 01541 $forUpdate ? array( 'FOR UPDATE' ) : array() ); 01542 } 01543 01544 if ( !$row ) { 01545 wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." ); 01546 } 01547 01548 $text = self::getRevisionText( $row ); 01549 if ( $row && $text === false ) { 01550 wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." ); 01551 } 01552 01553 # No negative caching -- negative hits on text rows may be due to corrupted slave servers 01554 if ( $wgRevisionCacheExpiry && $text !== false ) { 01555 $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); 01556 } 01557 01558 wfProfileOut( __METHOD__ ); 01559 01560 return $text; 01561 } 01562 01578 public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) { 01579 global $wgContentHandlerUseDB; 01580 01581 wfProfileIn( __METHOD__ ); 01582 01583 $fields = array( 'page_latest', 'page_namespace', 'page_title', 01584 'rev_text_id', 'rev_len', 'rev_sha1' ); 01585 01586 if ( $wgContentHandlerUseDB ) { 01587 $fields[] = 'rev_content_model'; 01588 $fields[] = 'rev_content_format'; 01589 } 01590 01591 $current = $dbw->selectRow( 01592 array( 'page', 'revision' ), 01593 $fields, 01594 array( 01595 'page_id' => $pageId, 01596 'page_latest=rev_id', 01597 ), 01598 __METHOD__ ); 01599 01600 if ( $current ) { 01601 if ( !$user ) { 01602 global $wgUser; 01603 $user = $wgUser; 01604 } 01605 01606 $row = array( 01607 'page' => $pageId, 01608 'user_text' => $user->getName(), 01609 'user' => $user->getId(), 01610 'comment' => $summary, 01611 'minor_edit' => $minor, 01612 'text_id' => $current->rev_text_id, 01613 'parent_id' => $current->page_latest, 01614 'len' => $current->rev_len, 01615 'sha1' => $current->rev_sha1 01616 ); 01617 01618 if ( $wgContentHandlerUseDB ) { 01619 $row['content_model'] = $current->rev_content_model; 01620 $row['content_format'] = $current->rev_content_format; 01621 } 01622 01623 $revision = new Revision( $row ); 01624 $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) ); 01625 } else { 01626 $revision = null; 01627 } 01628 01629 wfProfileOut( __METHOD__ ); 01630 return $revision; 01631 } 01632 01643 public function userCan( $field, User $user = null ) { 01644 return self::userCanBitfield( $this->mDeleted, $field, $user ); 01645 } 01646 01661 public static function userCanBitfield( $bitfield, $field, User $user = null, 01662 Title $title = null 01663 ) { 01664 if ( $bitfield & $field ) { // aspect is deleted 01665 if ( $user === null ) { 01666 global $wgUser; 01667 $user = $wgUser; 01668 } 01669 if ( $bitfield & self::DELETED_RESTRICTED ) { 01670 $permissions = array( 'suppressrevision', 'viewsuppressed' ); 01671 } elseif ( $field & self::DELETED_TEXT ) { 01672 $permissions = array( 'deletedtext' ); 01673 } else { 01674 $permissions = array( 'deletedhistory' ); 01675 } 01676 $permissionlist = implode( ', ', $permissions ); 01677 if ( $title === null ) { 01678 wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); 01679 return call_user_func_array( array( $user, 'isAllowedAny' ), $permissions ); 01680 } else { 01681 $text = $title->getPrefixedText(); 01682 wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" ); 01683 foreach ( $permissions as $perm ) { 01684 if ( $title->userCan( $perm, $user ) ) { 01685 return true; 01686 } 01687 } 01688 return false; 01689 } 01690 } else { 01691 return true; 01692 } 01693 } 01694 01702 static function getTimestampFromId( $title, $id ) { 01703 $dbr = wfGetDB( DB_SLAVE ); 01704 // Casting fix for databases that can't take '' for rev_id 01705 if ( $id == '' ) { 01706 $id = 0; 01707 } 01708 $conds = array( 'rev_id' => $id ); 01709 $conds['rev_page'] = $title->getArticleID(); 01710 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); 01711 if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) { 01712 # Not in slave, try master 01713 $dbw = wfGetDB( DB_MASTER ); 01714 $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); 01715 } 01716 return wfTimestamp( TS_MW, $timestamp ); 01717 } 01718 01726 static function countByPageId( $db, $id ) { 01727 $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ), 01728 array( 'rev_page' => $id ), __METHOD__ ); 01729 if ( $row ) { 01730 return $row->revCount; 01731 } 01732 return 0; 01733 } 01734 01742 static function countByTitle( $db, $title ) { 01743 $id = $title->getArticleID(); 01744 if ( $id ) { 01745 return self::countByPageId( $db, $id ); 01746 } 01747 return 0; 01748 } 01749 01766 public static function userWasLastToEdit( $db, $pageId, $userId, $since ) { 01767 if ( !$userId ) { 01768 return false; 01769 } 01770 01771 if ( is_int( $db ) ) { 01772 $db = wfGetDB( $db ); 01773 } 01774 01775 $res = $db->select( 'revision', 01776 'rev_user', 01777 array( 01778 'rev_page' => $pageId, 01779 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) ) 01780 ), 01781 __METHOD__, 01782 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) ); 01783 foreach ( $res as $row ) { 01784 if ( $row->rev_user != $userId ) { 01785 return false; 01786 } 01787 } 01788 return true; 01789 } 01790 }