* @todo document
*/
class Revision implements IDBAccessObject {
+ /** @var int|null */
protected $mId;
-
- /**
- * @var int|null
- */
+ /** @var int|null */
protected $mPage;
+ /** @var string */
protected $mUserText;
+ /** @var string */
protected $mOrigUserText;
+ /** @var int */
protected $mUser;
+ /** @var bool */
protected $mMinorEdit;
+ /** @var string */
protected $mTimestamp;
+ /** @var int */
protected $mDeleted;
+ /** @var int */
protected $mSize;
+ /** @var string */
protected $mSha1;
+ /** @var int */
protected $mParentId;
+ /** @var string */
protected $mComment;
+ /** @var string */
protected $mText;
+ /** @var int */
protected $mTextId;
+ /** @var int */
+ protected $mUnpatrolled;
- /**
- * @var stdClass|null
- */
+ /** @var stdClass|null */
protected $mTextRow;
- /**
- * @var null|Title
- */
+ /** @var null|Title */
protected $mTitle;
+ /** @var bool */
protected $mCurrent;
+ /** @var string */
protected $mContentModel;
+ /** @var string */
protected $mContentFormat;
- /**
- * @var Content|null|bool
- */
+ /** @var Content|null|bool */
protected $mContent;
-
- /**
- * @var null|ContentHandler
- */
+ /** @var null|ContentHandler */
protected $mContentHandler;
- /**
- * @var int
- */
+ /** @var int */
protected $mQueryFlags = 0;
+ /** @var bool Used for cached values to reload user text and rev_deleted */
+ protected $mRefreshMutableFields = false;
+ /** @var string Wiki ID; false means the current wiki */
+ protected $mWiki = false;
// Revision deletion constants
const DELETED_TEXT = 1;
const FOR_THIS_USER = 2;
const RAW = 3;
+ const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
+
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
} else {
// Use a join to get the latest revision
$conds[] = 'rev_id=page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
return self::loadFromConds( $db, $conds, $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 );
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
return self::loadFromConds( $db, $conds, $flags );
}
}
* Given a set of conditions, fetch a revision
*
* This method is used then a revision ID is qualified and
- * will incorporate some basic slave/master fallback logic
+ * will incorporate some basic replica DB/master fallback logic
*
* @param array $conditions
* @param int $flags (optional)
* @return Revision|null
*/
private static function newFromConds( $conditions, $flags = 0 ) {
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
$rev = self::loadFromConds( $db, $conditions, $flags );
// Make sure new pending/committed revision are visibile later on
*/
private static function loadFromConds( $db, $conditions, $flags = 0 ) {
$row = self::fetchFromConds( $db, $conditions, $flags );
+ if ( $row ) {
+ $rev = new Revision( $row );
+ $rev->mWiki = $db->getWikiID();
+
+ return $rev;
+ }
- return $row ? new Revision( $row ) : null;
+ return null;
}
/**
*/
public static function fetchRevision( LinkTarget $title ) {
$row = self::fetchFromConds(
- wfGetDB( DB_SLAVE ),
+ wfGetDB( DB_REPLICA ),
[
'rev_id=page_latest',
'page_namespace' => $title->getNamespace(),
}
// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
if ( $this->mId !== null ) {
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
$row = $dbr->selectRow(
[ 'page', 'revision' ],
self::selectPageFields(),
- [ 'page_id=rev_page',
- 'rev_id' => $this->mId ],
- __METHOD__ );
+ [ 'page_id=rev_page', 'rev_id' => $this->mId ],
+ __METHOD__
+ );
if ( $row ) {
+ // @TODO: better foreign title handling
$this->mTitle = Title::newFromRow( $row );
}
}
- if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
- $this->mTitle = Title::newFromID( $this->mPage );
+ if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
+ // Loading by ID is best, though not possible for foreign titles
+ if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
+ $this->mTitle = Title::newFromID( $this->mPage );
+ }
}
return $this->mTitle;
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
+ $this->loadMutableFields();
+
if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
return '';
} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
* @return RecentChange|null
*/
public function getRecentChange( $flags = 0 ) {
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
* @return bool
*/
public function isDeleted( $field ) {
- return ( $this->mDeleted & $field ) == $field;
+ if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+ // Current revisions of pages cannot have the content hidden. Skipping this
+ // check is very useful for Parser as it fetches templates using newKnownCurrent().
+ // Calling getVisibility() in that case triggers a verification database query.
+ return false; // no need to check
+ }
+
+ return ( $this->getVisibility() & $field ) == $field;
}
/**
* @return int
*/
public function getVisibility() {
+ $this->loadMutableFields();
+
return (int)$this->mDeleted;
}
}
/**
- * Fetch original serialized data without regard for view restrictions
+ * Get original serialized data (without checking view restrictions)
*
* @since 1.21
* @return string
*/
public function getSerializedData() {
if ( $this->mText === null ) {
+ // Revision is immutable. Load on demand.
$this->mText = $this->loadText();
}
*/
protected function getContentInternal() {
if ( $this->mContent === null ) {
- // Revision is immutable. Load on demand:
- if ( $this->mText === null ) {
- $this->mText = $this->loadText();
- }
+ $text = $this->getSerializedData();
- if ( $this->mText !== null && $this->mText !== false ) {
+ if ( $text !== null && $text !== false ) {
// Unserialize content
$handler = $this->getContentHandler();
$format = $this->getContentFormat();
- $this->mContent = $handler->unserializeContent( $this->mText, $format );
+ $this->mContent = $handler->unserializeContent( $text, $format );
}
}
*
* @return string|bool The revision's text, or false on failure
*/
- protected function loadText() {
- // Caching may be beneficial for massive use of external storage
+ private function loadText() {
global $wgRevisionCacheExpiry;
- static $processCache = null;
- if ( !$processCache ) {
- $processCache = new MapCacheLRU( 10 );
+ $cache = ObjectCache::getMainWANInstance();
+ if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
+ // Do not cache RDBMs blobs in...the RDBMs store
+ $ttl = $cache::TTL_UNCACHEABLE;
+ } else {
+ $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
}
- $cache = ObjectCache::getMainWANInstance();
- $textId = $this->getTextId();
- $key = wfMemcKey( 'revisiontext', 'textid', $textId );
+ // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
+ $ttl,
+ function () {
+ return $this->fetchText();
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
+ );
+ }
- if ( $wgRevisionCacheExpiry ) {
- if ( $processCache->has( $key ) ) {
- return $processCache->get( $key );
- }
- $text = $cache->get( $key );
- if ( is_string( $text ) ) {
- $processCache->set( $key, $text );
- return $text;
- }
- }
+ private function fetchText() {
+ $textId = $this->getTextId();
// If we kept data for lazy extraction, use it now...
if ( $this->mTextRow !== null ) {
$row = null;
}
+ // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
+ // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
+ $flags = $this->mQueryFlags;
+ $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
+ ? self::READ_LATEST_IMMUTABLE
+ : 0;
+
+ list( $index, $options, $fallbackIndex, $fallbackOptions ) =
+ DBAccessObjectUtils::getDBOptions( $flags );
+
if ( !$row ) {
- // Text data is immutable; check slaves first.
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow( 'text',
+ // Text data is immutable; check replica DBs first.
+ $row = wfGetDB( $index )->selectRow(
+ 'text',
[ 'old_text', 'old_flags' ],
[ 'old_id' => $textId ],
- __METHOD__ );
+ __METHOD__,
+ $options
+ );
}
- // 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',
+ // Fallback to DB_MASTER in some cases if the row was not found
+ if ( !$row && $fallbackIndex !== null ) {
+ // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
+ // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
+ $row = wfGetDB( $fallbackIndex )->selectRow(
+ 'text',
[ 'old_text', 'old_flags' ],
[ 'old_id' => $textId ],
__METHOD__,
- $forUpdate ? [ 'FOR UPDATE' ] : [] );
+ $fallbackOptions
+ );
}
if ( !$row ) {
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 ) {
- $processCache->set( $key, $text );
- $cache->set( $key, $text, $wgRevisionCacheExpiry );
- }
-
- return $text;
+ return is_string( $text ) ? $text : false;
}
/**
* @return bool
*/
public function userCan( $field, User $user = null ) {
- return self::userCanBitfield( $this->mDeleted, $field, $user );
+ return self::userCanBitfield( $this->getVisibility(), $field, $user );
}
/**
static function getTimestampFromId( $title, $id, $flags = 0 ) {
$db = ( $flags & self::READ_LATEST )
? wfGetDB( DB_MASTER )
- : wfGetDB( DB_SLAVE );
+ : wfGetDB( DB_REPLICA );
// Casting fix for databases that can't take '' for rev_id
if ( $id == '' ) {
$id = 0;
}
return true;
}
+
+ /**
+ * Load a revision based on a known page ID and current revision ID from the DB
+ *
+ * This method allows for the use of caching, though accessing anything that normally
+ * requires permission checks (aside from the text) will trigger a small DB lookup.
+ * The title will also be lazy loaded, though setTitle() can be used to preload it.
+ *
+ * @param IDatabase $db
+ * @param int $pageId Page ID
+ * @param int $revId Known current revision of this page
+ * @return Revision|bool Returns false if missing
+ * @since 1.28
+ */
+ public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
+ $cache = ObjectCache::getMainWANInstance();
+ return $cache->getWithSetCallback(
+ // Page/rev IDs passed in from DB to reflect history merges
+ $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ),
+ $cache::TTL_WEEK,
+ function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ $rev = Revision::loadFromPageId( $db, $pageId, $revId );
+ // Reflect revision deletion and user renames
+ if ( $rev ) {
+ $rev->mTitle = null; // mutable; lazy-load
+ $rev->mRefreshMutableFields = true;
+ }
+
+ return $rev ?: false; // don't cache negatives
+ }
+ );
+ }
+
+ /**
+ * For cached revisions, make sure the user name and rev_deleted is up-to-date
+ */
+ private function loadMutableFields() {
+ if ( !$this->mRefreshMutableFields ) {
+ return; // not needed
+ }
+
+ $this->mRefreshMutableFields = false;
+ $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
+ $row = $dbr->selectRow(
+ [ 'revision', 'user' ],
+ [ 'rev_deleted', 'user_name' ],
+ [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
+ __METHOD__
+ );
+ if ( $row ) { // update values
+ $this->mDeleted = (int)$row->rev_deleted;
+ $this->mUserText = $row->user_name;
+ }
+ }
}