diff options
Diffstat (limited to 'includes/Revision.php')
-rw-r--r-- | includes/Revision.php | 265 |
1 files changed, 160 insertions, 105 deletions
diff --git a/includes/Revision.php b/includes/Revision.php index 2b34984b..233eac01 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -61,6 +61,11 @@ class Revision implements IDBAccessObject { */ protected $mContentHandler; + /** + * @var int + */ + protected $mQueryFlags = 0; + // Revision deletion constants const DELETED_TEXT = 1; const DELETED_COMMENT = 2; @@ -106,16 +111,18 @@ class Revision implements IDBAccessObject { public static function newFromTitle( $title, $id = 0, $flags = 0 ) { $conds = array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() + 'page_title' => $title->getDBkey() ); if ( $id ) { // Use the specified ID $conds['rev_id'] = $id; + return self::newFromConds( $conds, (int)$flags ); } else { // Use a join to get the latest revision $conds[] = 'rev_id=page_latest'; + $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); + return self::loadFromConds( $db, $conds, $flags ); } - return self::newFromConds( $conds, (int)$flags ); } /** @@ -228,8 +235,8 @@ class Revision implements IDBAccessObject { * @return Revision or null */ public static function loadFromPageId( $db, $pageid, $id = 0 ) { - $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ); - if( $id ) { + $conds = array( 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ); + if ( $id ) { $conds['rev_id'] = intval( $id ); } else { $conds[] = 'rev_id=page_latest'; @@ -248,7 +255,7 @@ class Revision implements IDBAccessObject { * @return Revision or null */ public static function loadFromTitle( $db, $title, $id = 0 ) { - if( $id ) { + if ( $id ) { $matchId = intval( $id ); } else { $matchId = 'page_latest'; @@ -257,7 +264,7 @@ class Revision implements IDBAccessObject { array( "rev_id=$matchId", 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() + 'page_title' => $title->getDBkey() ) ); } @@ -275,9 +282,9 @@ class Revision implements IDBAccessObject { public static function loadFromTimestamp( $db, $title, $timestamp ) { return self::loadFromConds( $db, array( - 'rev_timestamp' => $db->timestamp( $timestamp ), + 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() + 'page_title' => $title->getDBkey() ) ); } @@ -298,6 +305,9 @@ class Revision implements IDBAccessObject { $rev = self::loadFromConds( $dbw, $conditions, $flags ); } } + if ( $rev ) { + $rev->mQueryFlags = $flags; + } return $rev; } @@ -312,9 +322,9 @@ class Revision implements IDBAccessObject { */ private static function loadFromConds( $db, $conditions, $flags = 0 ) { $res = self::fetchFromConds( $db, $conditions, $flags ); - if( $res ) { + if ( $res ) { $row = $res->fetchObject(); - if( $row ) { + if ( $row ) { $ret = new Revision( $row ); return $ret; } @@ -337,7 +347,7 @@ class Revision implements IDBAccessObject { array( 'rev_id=page_latest', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() + 'page_title' => $title->getDBkey() ) ); } @@ -489,7 +499,7 @@ class Revision implements IDBAccessObject { * @access private */ function __construct( $row ) { - if( is_object( $row ) ) { + if ( is_object( $row ) ) { $this->mId = intval( $row->rev_id ); $this->mPage = intval( $row->rev_page ); $this->mTextId = intval( $row->rev_text_id ); @@ -517,7 +527,7 @@ class Revision implements IDBAccessObject { $this->mSha1 = $row->rev_sha1; } - if( isset( $row->page_latest ) ) { + if ( isset( $row->page_latest ) ) { $this->mCurrent = ( $row->rev_id == $row->page_latest ); $this->mTitle = Title::newFromRow( $row ); } else { @@ -525,13 +535,13 @@ class Revision implements IDBAccessObject { $this->mTitle = null; } - if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) { + if ( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) { $this->mContentModel = null; # determine on demand if needed } else { $this->mContentModel = strval( $row->rev_content_model ); } - if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) { + if ( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) { $this->mContentFormat = null; # determine on demand if needed } else { $this->mContentFormat = strval( $row->rev_content_format ); @@ -539,7 +549,7 @@ class Revision implements IDBAccessObject { // Lazy extraction... $this->mText = null; - if( isset( $row->old_text ) ) { + if ( isset( $row->old_text ) ) { $this->mTextRow = $row; } else { // 'text' table row entry will be lazy-loaded @@ -554,13 +564,13 @@ class Revision implements IDBAccessObject { $this->mUserText = $row->user_name; // logged-in user } $this->mOrigUserText = $row->rev_user_text; - } elseif( is_array( $row ) ) { + } elseif ( is_array( $row ) ) { // Build a new revision to be saved... global $wgUser; // ugh # if we have a content object, use it to set the model and type if ( !empty( $row['content'] ) ) { - //@todo: when is that set? test with external store setup! check out insertOn() [dk] + // @todo when is that set? test with external store setup! check out insertOn() [dk] if ( !empty( $row['text_id'] ) ) { throw new MWException( "Text already stored in external store (id {$row['text_id']}), " . "can't serialize content object" ); @@ -714,10 +724,10 @@ class Revision implements IDBAccessObject { * @return Title|null */ public function getTitle() { - if( isset( $this->mTitle ) ) { + if ( isset( $this->mTitle ) ) { return $this->mTitle; } - if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted. + if ( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted. $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( array( 'page', 'revision' ), @@ -769,9 +779,9 @@ class Revision implements IDBAccessObject { * @return Integer */ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) { - if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { return 0; - } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { + } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { return 0; } else { return $this->mUser; @@ -801,9 +811,9 @@ class Revision implements IDBAccessObject { * @return string */ public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { - if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { return ''; - } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { + } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { return ''; } else { return $this->getRawUserText(); @@ -841,9 +851,9 @@ class Revision implements IDBAccessObject { * @return String */ function getComment( $audience = self::FOR_PUBLIC, User $user = null ) { - if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { return ''; - } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) { + } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) { return ''; } else { return $this->mComment; @@ -867,24 +877,37 @@ class Revision implements IDBAccessObject { } /** - * @return Integer rcid of the unpatrolled row, zero if there isn't one + * @return integer rcid of the unpatrolled row, zero if there isn't one */ public function isUnpatrolled() { - if( $this->mUnpatrolled !== null ) { + if ( $this->mUnpatrolled !== null ) { return $this->mUnpatrolled; } + $rc = $this->getRecentChange(); + if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) { + $this->mUnpatrolled = $rc->getAttribute( 'rc_id' ); + } else { + $this->mUnpatrolled = 0; + } + return $this->mUnpatrolled; + } + + /** + * Get the RC object belonging to the current revision, if there's one + * + * @since 1.22 + * @return RecentChange|null + */ + public function getRecentChange() { $dbr = wfGetDB( DB_SLAVE ); - $this->mUnpatrolled = $dbr->selectField( 'recentchanges', - 'rc_id', - array( // Add redundant user,timestamp condition so we can use the existing index - 'rc_user_text' => $this->getRawUserText(), - 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ), - 'rc_this_oldid' => $this->getId(), - 'rc_patrolled' => 0 + return RecentChange::newFromConds( + array( + 'rc_user_text' => $this->getRawUserText(), + 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ), + 'rc_this_oldid' => $this->getId() ), __METHOD__ ); - return (int)$this->mUnpatrolled; } /** @@ -918,7 +941,7 @@ class Revision implements IDBAccessObject { * to the $audience parameter * * @deprecated in 1.21, use getContent() instead - * @todo: replace usage in core + * @todo Replace usage in core * @return String */ public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { @@ -943,9 +966,9 @@ class Revision implements IDBAccessObject { * @return Content|null */ public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { - if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { return null; - } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { + } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { return null; } else { return $this->getContentInternal(); @@ -983,6 +1006,10 @@ class Revision implements IDBAccessObject { * @return String */ public function getSerializedData() { + if ( is_null( $this->mText ) ) { + $this->mText = $this->loadText(); + } + return $this->mText; } @@ -996,9 +1023,9 @@ class Revision implements IDBAccessObject { * @return Content|null the Revision's content, or null on failure. */ protected function getContentInternal() { - if( is_null( $this->mContent ) ) { + if ( is_null( $this->mContent ) ) { // Revision is immutable. Load on demand: - if( is_null( $this->mText ) ) { + if ( is_null( $this->mText ) ) { $this->mText = $this->loadText(); } @@ -1097,9 +1124,9 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public function getPrevious() { - if( $this->getTitle() ) { + if ( $this->getTitle() ) { $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); - if( $prev ) { + if ( $prev ) { return self::newFromTitle( $this->getTitle(), $prev ); } } @@ -1112,7 +1139,7 @@ class Revision implements IDBAccessObject { * @return Revision or null */ public function getNext() { - if( $this->getTitle() ) { + if ( $this->getTitle() ) { $next = $this->getTitle()->getNextRevisionID( $this->getId() ); if ( $next ) { return self::newFromTitle( $this->getTitle(), $next ); @@ -1129,11 +1156,11 @@ class Revision implements IDBAccessObject { * @return Integer */ private function getPreviousRevisionId( $db ) { - if( is_null( $this->mPage ) ) { + if ( is_null( $this->mPage ) ) { return 0; } # Use page_latest if ID is not given - if( !$this->mId ) { + if ( !$this->mId ) { $prevId = $db->selectField( 'page', 'page_latest', array( 'page_id' => $this->mPage ), __METHOD__ ); @@ -1166,13 +1193,13 @@ class Revision implements IDBAccessObject { $textField = $prefix . 'text'; $flagsField = $prefix . 'flags'; - if( isset( $row->$flagsField ) ) { + if ( isset( $row->$flagsField ) ) { $flags = explode( ',', $row->$flagsField ); } else { $flags = array(); } - if( isset( $row->$textField ) ) { + if ( isset( $row->$textField ) ) { $text = $row->$textField; } else { wfProfileOut( __METHOD__ ); @@ -1183,7 +1210,7 @@ class Revision implements IDBAccessObject { if ( in_array( 'external', $flags ) ) { $url = $text; $parts = explode( '://', $url, 2 ); - if( count( $parts ) == 1 || $parts[1] == '' ) { + if ( count( $parts ) == 1 || $parts[1] == '' ) { wfProfileOut( __METHOD__ ); return false; } @@ -1192,35 +1219,7 @@ class Revision implements IDBAccessObject { // If the text was fetched without an error, convert it if ( $text !== false ) { - if( in_array( 'gzip', $flags ) ) { - # Deal with optional compression of archived pages. - # This can be done periodically via maintenance/compressOld.php, and - # as pages are saved if $wgCompressRevisions is set. - $text = gzinflate( $text ); - } - - if( in_array( 'object', $flags ) ) { - # Generic compressed storage - $obj = unserialize( $text ); - if ( !is_object( $obj ) ) { - // Invalid object - wfProfileOut( __METHOD__ ); - return false; - } - $text = $obj->getText(); - } - - global $wgLegacyEncoding; - if( $text !== false && $wgLegacyEncoding - && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) - { - # Old revisions kept around in a legacy encoding? - # Upconvert on demand. - # ("utf8" checked for compatibility with some broken - # conversion scripts 2008-12-30) - global $wgContLang; - $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text ); - } + $text = self::decompressRevisionText( $text, $flags ); } wfProfileOut( __METHOD__ ); return $text; @@ -1244,8 +1243,8 @@ class Revision implements IDBAccessObject { # on load if $wgLegacyCharset is set in the future. $flags[] = 'utf-8'; - if( $wgCompressRevisions ) { - if( function_exists( 'gzdeflate' ) ) { + if ( $wgCompressRevisions ) { + if ( function_exists( 'gzdeflate' ) ) { $text = gzdeflate( $text ); $flags[] = 'gzip'; } else { @@ -1256,6 +1255,46 @@ class Revision implements IDBAccessObject { } /** + * Re-converts revision text according to it's flags. + * + * @param $text Mixed: reference to a text + * @param $flags array: compression flags + * @return String|bool decompressed text, or false on failure + */ + public static function decompressRevisionText( $text, $flags ) { + if ( in_array( 'gzip', $flags ) ) { + # Deal with optional compression of archived pages. + # This can be done periodically via maintenance/compressOld.php, and + # as pages are saved if $wgCompressRevisions is set. + $text = gzinflate( $text ); + } + + if ( in_array( 'object', $flags ) ) { + # Generic compressed storage + $obj = unserialize( $text ); + if ( !is_object( $obj ) ) { + // Invalid object + return false; + } + $text = $obj->getText(); + } + + global $wgLegacyEncoding; + if ( $text !== false && $wgLegacyEncoding + && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) + { + # Old revisions kept around in a legacy encoding? + # Upconvert on demand. + # ("utf8" checked for compatibility with some broken + # conversion scripts 2008-12-30) + global $wgContLang; + $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text ); + } + + return $text; + } + + /** * Insert a new revision into the database, returning the new revision ID * number on success and dies horribly on failure. * @@ -1274,32 +1313,35 @@ class Revision implements IDBAccessObject { $flags = self::compressRevisionText( $data ); # Write to external storage if required - if( $wgDefaultExternalStore ) { + if ( $wgDefaultExternalStore ) { // Store and get the URL $data = ExternalStore::insertToDefault( $data ); - if( !$data ) { + if ( !$data ) { + wfProfileOut( __METHOD__ ); throw new MWException( "Unable to store text to external storage" ); } - if( $flags ) { + if ( $flags ) { $flags .= ','; } $flags .= 'external'; } # Record the text (or external storage URL) to the text table - if( !isset( $this->mTextId ) ) { + if ( !isset( $this->mTextId ) ) { $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' ); $dbw->insert( 'text', array( - 'old_id' => $old_id, - 'old_text' => $data, + 'old_id' => $old_id, + 'old_text' => $data, 'old_flags' => $flags, ), __METHOD__ ); $this->mTextId = $dbw->insertId(); } - if ( $this->mComment === null ) $this->mComment = ""; + if ( $this->mComment === null ) { + $this->mComment = ""; + } # Record the edit in revisions $rev_id = isset( $this->mId ) @@ -1334,14 +1376,15 @@ class Revision implements IDBAccessObject { $title = $this->getTitle(); if ( $title === null ) { + wfProfileOut( __METHOD__ ); throw new MWException( "Insufficient information to determine the title of the revision's page!" ); } $defaultModel = ContentHandler::getDefaultModelFor( $title ); $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat(); - $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model; - $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format; + $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model; + $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format; } $dbw->insert( 'revision', $row, __METHOD__ ); @@ -1422,9 +1465,9 @@ class Revision implements IDBAccessObject { global $wgRevisionCacheExpiry, $wgMemc; $textId = $this->getTextId(); $key = wfMemcKey( 'revisiontext', 'textid', $textId ); - if( $wgRevisionCacheExpiry ) { + if ( $wgRevisionCacheExpiry ) { $text = $wgMemc->get( $key ); - if( is_string( $text ) ) { + if ( is_string( $text ) ) { wfDebug( __METHOD__ . ": got id $textId from cache\n" ); wfProfileOut( __METHOD__ ); return $text; @@ -1439,28 +1482,38 @@ class Revision implements IDBAccessObject { $row = null; } - if( !$row ) { + if ( !$row ) { // Text data is immutable; check slaves first. $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'text', array( 'old_text', 'old_flags' ), - array( 'old_id' => $this->getTextId() ), + array( 'old_id' => $textId ), __METHOD__ ); } - if( !$row && wfGetLB()->getServerCount() > 1 ) { - // Possible slave lag! + // Fallback to the master in case of slave lag. Also use FOR UPDATE if it was + // used to fetch this revision to avoid missing the row due to REPEATABLE-READ. + $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING ); + if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) { $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), - array( 'old_id' => $this->getTextId() ), - __METHOD__ ); + array( 'old_id' => $textId ), + __METHOD__, + $forUpdate ? array( 'FOR UPDATE' ) : array() ); + } + + if ( !$row ) { + wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." ); } $text = self::getRevisionText( $row ); + if ( $row && $text === false ) { + wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." ); + } # No negative caching -- negative hits on text rows may be due to corrupted slave servers - if( $wgRevisionCacheExpiry && $text !== false ) { + if ( $wgRevisionCacheExpiry && $text !== false ) { $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); } @@ -1505,7 +1558,7 @@ class Revision implements IDBAccessObject { ), __METHOD__ ); - if( $current ) { + if ( $current ) { $row = array( 'page' => $pageId, 'comment' => $summary, @@ -1517,8 +1570,8 @@ class Revision implements IDBAccessObject { ); if ( $wgContentHandlerUseDB ) { - $row[ 'content_model' ] = $current->rev_content_model; - $row[ 'content_format' ] = $current->rev_content_format; + $row['content_model'] = $current->rev_content_model; + $row['content_format'] = $current->rev_content_format; } $revision = new Revision( $row ); @@ -1558,7 +1611,7 @@ class Revision implements IDBAccessObject { * @return Boolean */ public static function userCanBitfield( $bitfield, $field, User $user = null ) { - if( $bitfield & $field ) { // aspect is deleted + if ( $bitfield & $field ) { // aspect is deleted if ( $bitfield & self::DELETED_RESTRICTED ) { $permission = 'suppressrevision'; } elseif ( $field & self::DELETED_TEXT ) { @@ -1611,7 +1664,7 @@ class Revision implements IDBAccessObject { static function countByPageId( $db, $id ) { $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ), array( 'rev_page' => $id ), __METHOD__ ); - if( $row ) { + if ( $row ) { return $row->revCount; } return 0; @@ -1626,7 +1679,7 @@ class Revision implements IDBAccessObject { */ static function countByTitle( $db, $title ) { $id = $title->getArticleID(); - if( $id ) { + if ( $id ) { return self::countByPageId( $db, $id ); } return 0; @@ -1648,7 +1701,9 @@ class Revision implements IDBAccessObject { * @return bool True if the given user was the only one to edit since the given timestamp */ public static function userWasLastToEdit( $db, $pageId, $userId, $since ) { - if ( !$userId ) return false; + if ( !$userId ) { + return false; + } if ( is_int( $db ) ) { $db = wfGetDB( $db ); |