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