* BagOStuff::modifySimpleRelayEvent() method has been removed.
* ParserOutput::getLegacyOptions, deprecated in 1.30, has been removed.
Use ParserOutput::allCacheVaryingOptions instead.
+* CdnCacheUpdate::newSimplePurge, deprecated in 1.27, has been removed.
+ Use CdnCacheUpdate::newFromTitles() instead.
=== Deprecations in 1.33 ===
* The configuration option $wgUseESI has been deprecated, and is expected
* The implementation of buildStringCast() in Wikimedia\Rdbms\Database has
changed to explicitly cast. Subclasses relying on the base-class
implementation should check whether they need to override it now.
+* BagOStuff::add is now abstract and must explicitly be defined in subclasses.
== Compatibility ==
MediaWiki 1.33 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php',
'RefreshLinks' => __DIR__ . '/maintenance/refreshLinks.php',
'RefreshLinksJob' => __DIR__ . '/includes/jobqueue/jobs/RefreshLinksJob.php',
+ 'RefreshSecondaryDataUpdate' => __DIR__ . '/includes/deferred/RefreshSecondaryDataUpdate.php',
'RegexlikeReplacer' => __DIR__ . '/includes/libs/replacers/RegexlikeReplacer.php',
'RemexStripTagHandler' => __DIR__ . '/includes/parser/RemexStripTagHandler.php',
'RemoveInvalidEmails' => __DIR__ . '/maintenance/removeInvalidEmails.php',
$from,
RecursiveDirectoryIterator::SKIP_DOTS
) );
+ /** @var SplFileInfo $file */
foreach ( $rii as $file ) {
$remote = $file->getPathname();
$local = strtr( $remote, [ $from => $to ] );
use ApiStashEdit;
use CategoryMembershipChangeJob;
+use RefreshSecondaryDataUpdate;
use Content;
use ContentHandler;
use DataUpdate;
$update->setRevision( $legacyRevision );
$update->setTriggeringUser( $triggeringUser );
}
- if ( $options['defer'] === false ) {
- if ( $options['transactionTicket'] !== null ) {
+ }
+
+ if ( $options['defer'] === false ) {
+ foreach ( $updates as $update ) {
+ if ( $update instanceof DataUpdate && $options['transactionTicket'] !== null ) {
$update->setTransactionTicket( $options['transactionTicket'] );
}
$update->doUpdate();
- } else {
- DeferredUpdates::addUpdate( $update, $options['defer'] );
}
+ } else {
+ $cacheTime = $this->getCanonicalParserOutput()->getCacheTime();
+ // Bundle all of the data updates into a single deferred update wrapper so that
+ // any failure will cause at most one refreshLinks job to be enqueued by
+ // DeferredUpdates::doUpdates(). This is hard to do when there are many separate
+ // updates that are not defined as being related.
+ $update = new RefreshSecondaryDataUpdate(
+ $this->wikiPage,
+ $updates,
+ $options,
+ $cacheTime,
+ $this->loadbalancerFactory->getLocalDomainID()
+ );
+ $update->setRevision( $legacyRevision );
+ $update->setTriggeringUser( $triggeringUser );
+ DeferredUpdates::addUpdate( $update, $options['defer'] );
}
}
return new CdnCacheUpdate( $urlArr );
}
- /**
- * @param Title $title
- * @return CdnCacheUpdate
- * @deprecated since 1.27
- */
- public static function newSimplePurge( Title $title ) {
- return new CdnCacheUpdate( $title->getCdnUrls() );
- }
-
/**
* Purges the list of URLs passed to the constructor.
*/
*
* See docs/deferred.txt
*/
-class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
+class LinksUpdate extends DataUpdate {
// @todo make members protected, but make sure extensions don't break
/** @var int Page ID of the article linked from */
return $this->db;
}
-
- public function getAsJobSpecification() {
- if ( $this->user ) {
- $userInfo = [
- 'userId' => $this->user->getId(),
- 'userName' => $this->user->getName(),
- ];
- } else {
- $userInfo = false;
- }
-
- if ( $this->mRevision ) {
- $triggeringRevisionId = $this->mRevision->getId();
- } else {
- $triggeringRevisionId = false;
- }
-
- return [
- 'wiki' => WikiMap::getWikiIdFromDbDomain( $this->getDB()->getDomainID() ),
- 'job' => new JobSpecification(
- 'refreshLinksPrioritized',
- [
- // Reuse the parser cache if it was saved
- 'rootJobTimestamp' => $this->mParserOutput->getCacheTime(),
- 'useRecursiveLinksUpdate' => $this->mRecursive,
- 'triggeringUser' => $userInfo,
- 'triggeringRevisionId' => $triggeringRevisionId,
- 'causeAction' => $this->getCauseAction(),
- 'causeAgent' => $this->getCauseAgent()
- ],
- [ 'removeDuplicates' => true ],
- $this->getTitle()
- )
- ];
- }
}
--- /dev/null
+<?php
+/**
+ * Updater for secondary data after a page edit.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Update object handling the cleanup of secondary data after a page was edited.
+ *
+ * This makes makes it possible for DeferredUpdates to have retry logic using a single
+ * refreshLinks job if any of the bundled updates fail.
+ */
+class RefreshSecondaryDataUpdate extends DataUpdate implements EnqueueableDataUpdate {
+ /** @var WikiPage */
+ private $page;
+ /** @var DeferrableUpdate[] */
+ private $updates;
+ /** @var bool */
+ private $recursive;
+ /** @var string */
+ private $cacheTimestamp;
+ /** @var string Database domain ID */
+ private $domain;
+
+ /** @var Revision|null */
+ private $revision;
+ /** @var User|null */
+ private $user;
+
+ /**
+ * @param WikiPage $page Page we are updating
+ * @param DeferrableUpdate[] $updates Updates from DerivedPageDataUpdater::getSecondaryUpdates()
+ * @param array $options Options map (causeAction, causeAgent, recursive)
+ * @param string $cacheTime Result of ParserOutput::getCacheTime() for the source output
+ * @param string $domain The database domain ID of the wiki the update is for
+ */
+ function __construct(
+ WikiPage $page,
+ array $updates,
+ array $options,
+ $cacheTime,
+ $domain
+ ) {
+ parent::__construct();
+
+ $this->page = $page;
+ $this->updates = $updates;
+ $this->causeAction = $options['causeAction'] ?? 'unknown';
+ $this->causeAgent = $options['causeAgent'] ?? 'unknown';
+ $this->recursive = !empty( $options['recursive'] );
+ $this->cacheTimestamp = $cacheTime;
+ $this->domain = $domain;
+ }
+
+ public function doUpdate() {
+ foreach ( $this->updates as $update ) {
+ $update->doUpdate();
+ }
+ }
+
+ /**
+ * Set the revision corresponding to this LinksUpdate
+ * @param Revision $revision
+ */
+ public function setRevision( Revision $revision ) {
+ $this->revision = $revision;
+ }
+
+ /**
+ * Set the User who triggered this LinksUpdate
+ * @param User $user
+ */
+ public function setTriggeringUser( User $user ) {
+ $this->user = $user;
+ }
+
+ public function getAsJobSpecification() {
+ return [
+ 'wiki' => WikiMap::getWikiIdFromDomain( $this->domain ),
+ 'job' => new JobSpecification(
+ 'refreshLinksPrioritized',
+ [
+ // Reuse the parser cache if it was saved
+ 'rootJobTimestamp' => $this->cacheTimestamp,
+ 'useRecursiveLinksUpdate' => $this->recursive,
+ 'triggeringUser' => $this->user
+ ? [
+ 'userId' => $this->user->getId(),
+ 'userName' => $this->user->getName()
+ ]
+ : false,
+ 'triggeringRevisionId' => $this->revision ? $this->revision->getId() : false,
+ 'causeAction' => $this->getCauseAction(),
+ 'causeAgent' => $this->getCauseAgent()
+ ],
+ [ 'removeDuplicates' => true ],
+ $this->page->getTitle()
+ )
+ ];
+ }
+}
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return apc_add(
+ $key . self::KEY_SUFFIX,
+ $this->setSerialize( $value ),
+ $exptime
+ );
+ }
+
protected function setSerialize( $value ) {
if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) {
$value = serialize( $value );
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return apcu_add(
+ $key . self::KEY_SUFFIX,
+ $this->setSerialize( $value ),
+ $exptime
+ );
+ }
+
public function delete( $key, $flags = 0 ) {
apcu_delete( $key . self::KEY_SUFFIX );
* (which will be false if not present), and takes the arguments:
* (this BagOStuff, cache key, current value, TTL).
* The TTL parameter is reference set to $exptime. It can be overriden in the callback.
+ * If the callback returns false, then the current value will be unchanged (including TTL).
*
* @param string $key
* @param callable $callback Callback method to be executed
* @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
*/
- public function add( $key, $value, $exptime = 0, $flags = 0 ) {
- // @note: avoid lock() here since that method uses *this* method by default
- if ( $this->get( $key ) === false ) {
- return $this->set( $key, $value, $exptime, $flags );
- }
-
- return false; // key already set
- }
+ abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
/**
* Increase stored value of $key by $value while preserving its TTL
// These just call the backend (tested elsewhere)
// @codeCoverageIgnoreStart
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
return $this->backend->lock( $key, $timeout, $expiry, $rclass );
}
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function delete( $key, $flags = 0 ) {
unset( $this->bag[$key] );
return $this->handleError( "Failed to store $key", $rcode, $rerr );
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function delete( $key, $flags = 0 ) {
// @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
$req = [
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
- return $this->writeStore->add( $key, $value, $exptime );
+ return $this->writeStore->add( $key, $value, $exptime, $flags );
}
public function incr( $key, $value = 1 ) {
public function set( $key, $value, $expire = 0, $flags = 0 ) {
$result = wincache_ucache_set( $key, serialize( $value ), $expire );
- /* wincache_ucache_set returns an empty array on success if $value
- * was an array, bool otherwise */
- return ( is_array( $result ) && $result === [] ) || $result;
+ return ( $result === [] || $result === true );
+ }
+
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ $result = wincache_ucache_add( $key, serialize( $value ), $exptime );
+
+ return ( $result === [] || $result === true );
}
public function delete( $key, $flags = 0 ) {
}
/**
- * Make sure isOpen() returns true as a sanity check
+ * Make sure there is an open connection handle (alive or not) as a sanity check
+ *
+ * This guards against fatal errors to the binding handle not being defined
+ * in cases where open() was never called or close() was already called
*
* @throws DBUnexpectedError
*/
- protected function assertOpen() {
+ protected function assertHasConnectionHandle() {
if ( !$this->isOpen() ) {
throw new DBUnexpectedError( $this, "DB connection was already closed." );
}
}
+ /**
+ * Make sure that this server is not marked as a replica nor read-only as a sanity check
+ *
+ * @throws DBUnexpectedError
+ */
+ protected function assertIsWritableMaster() {
+ if ( $this->getLBInfo( 'replica' ) === true ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'Write operations are not allowed on replica database connections.'
+ );
+ }
+ $reason = $this->getReadOnlyReason();
+ if ( $reason !== false ) {
+ throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
+ }
+ }
+
/**
* Closes underlying database connection
* @since 1.20
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$this->assertTransactionStatus( $sql, $fname );
+ $this->assertHasConnectionHandle();
- # Avoid fatals if close() was called
- $this->assertOpen();
-
+ $priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
- $isWrite = $this->isWriteQuery( $sql );
- if ( $isWrite ) {
- $isNonTempWrite = !$this->registerTempTableOperation( $sql );
- } else {
- $isNonTempWrite = false;
- }
-
- if ( $isWrite ) {
- if ( $this->getLBInfo( 'replica' ) === true ) {
- throw new DBError(
- $this,
- 'Write operations are not allowed on replica database connections.'
- );
- }
+ if ( $this->isWriteQuery( $sql ) ) {
# In theory, non-persistent writes are allowed in read-only mode, but due to things
# like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
- $reason = $this->getReadOnlyReason();
- if ( $reason !== false ) {
- throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
- }
- # Set a flag indicating that writes have been done
- $this->lastWriteTime = microtime( true );
+ $this->assertIsWritableMaster();
+ # Avoid treating temporary table operations as meaningful "writes"
+ $isEffectiveWrite = !$this->registerTempTableOperation( $sql );
+ } else {
+ $isEffectiveWrite = false;
}
# Add trace comment to the begin of the sql string, right after the operator.
# Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
- # Start implicit transactions that wrap the request if DBO_TRX is enabled
- if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
- && $this->isTransactableQuery( $sql )
- ) {
- $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
- $this->trxAutomatic = true;
- }
-
- # Keep track of whether the transaction has write queries pending
- if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
- $this->trxDoneWrites = true;
- $this->trxProfiler->transactionWritingIn(
- $this->server, $this->getDomainID(), $this->trxShortId );
- }
-
- if ( $this->getFlag( self::DBO_DEBUG ) ) {
- $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
- }
-
# Send the query to the server and fetch any corresponding errors
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
- # Try reconnecting if the connection was lost
+ $recoverableSR = false; // recoverable statement rollback?
+ $recoverableCL = false; // recoverable connection loss?
+
if ( $ret === false && $this->wasConnectionLoss() ) {
- # Check if any meaningful session state was lost
- $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
+ # Check if no meaningful session state was lost
+ $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
# Update session state tracking and try to restore the connection
$reconnected = $this->replaceLostConnection( __METHOD__ );
# Silently resend the query to the server if it is safe and possible
- if ( $reconnected && $recoverable ) {
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ if ( $recoverableCL && $reconnected ) {
+ $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
if ( $ret === false && $this->wasConnectionLoss() ) {
# Query probably causes disconnects; reconnect and do not re-run it
$this->replaceLostConnection( __METHOD__ );
+ } else {
+ $recoverableCL = false; // connection does not need recovering
+ $recoverableSR = $this->wasKnownStatementRollbackError();
}
}
+ } else {
+ $recoverableSR = $this->wasKnownStatementRollbackError();
}
if ( $ret === false ) {
- if ( $this->trxLevel ) {
- if ( $this->wasKnownStatementRollbackError() ) {
+ if ( $priorTransaction ) {
+ if ( $recoverableSR ) {
# We're ignoring an error that caused just the current query to be aborted.
# But log the cause so we can log a deprecation notice if a caller actually
# does ignore it.
$this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
- } else {
+ } elseif ( !$recoverableCL ) {
# Either the query was aborted or all queries after BEGIN where aborted.
# In the first case, the only options going forward are (a) ROLLBACK, or
# (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
*
* @param string $sql Original SQL query
* @param string $commentedSql SQL query with debugging/trace comment
- * @param bool $isWrite Whether the query is a (non-temporary) write operation
+ * @param bool $isEffectiveWrite Whether the query is a (non-temporary table) write
* @param string $fname Name of the calling function
* @return bool|ResultWrapper True for a successful write query, ResultWrapper
* object for a successful read query, or false on failure
*/
- private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+ private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) {
+ $this->beginIfImplied( $sql, $fname );
+
+ # Keep track of whether the transaction has write queries pending
+ if ( $isEffectiveWrite ) {
+ $this->lastWriteTime = microtime( true );
+ if ( $this->trxLevel && !$this->trxDoneWrites ) {
+ $this->trxDoneWrites = true;
+ $this->trxProfiler->transactionWritingIn(
+ $this->server, $this->getDomainID(), $this->trxShortId );
+ }
+ }
+
+ if ( $this->getFlag( self::DBO_DEBUG ) ) {
+ $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
+ }
+
$isMaster = !is_null( $this->getLBInfo( 'master' ) );
# generalizeSQL() will probably cut down the query to reasonable
# logging size most of the time. The substr is really just a sanity check.
if ( $ret !== false ) {
$this->lastPing = $startTime;
- if ( $isWrite && $this->trxLevel ) {
+ if ( $isEffectiveWrite && $this->trxLevel ) {
$this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
$this->trxWriteCallers[] = $fname;
}
$this->trxProfiler->recordQueryCompletion(
$queryProf,
$startTime,
- $isWrite,
- $isWrite ? $this->affectedRows() : $this->numRows( $ret )
+ $isEffectiveWrite,
+ $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret )
);
$this->queryLogger->debug( $sql, [
'method' => $fname,
return $ret;
}
+ /**
+ * Start an implicit transaction if DBO_TRX is enabled and no transaction is active
+ *
+ * @param string $sql
+ * @param string $fname
+ */
+ private function beginIfImplied( $sql, $fname ) {
+ if (
+ !$this->trxLevel &&
+ $this->getFlag( self::DBO_TRX ) &&
+ $this->isTransactableQuery( $sql )
+ ) {
+ $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
+ $this->trxAutomatic = true;
+ }
+ }
+
/**
* Update the estimated run-time of a query, not counting large row lock times
*
}
/**
- * Determine whether or not it is safe to retry queries after a database
- * connection is lost
+ * Determine whether it is safe to retry queries after a database connection is lost
*
* @param string $sql SQL query
* @param bool $priorWritesPending Whether there is a transaction open with
* Clean things up after transaction loss
*/
private function handleTransactionLoss() {
+ if ( $this->trxDoneWrites ) {
+ $this->trxProfiler->transactionWritingOut(
+ $this->server,
+ $this->getDomainID(),
+ $this->trxShortId,
+ $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
+ $this->trxWriteAffectedRows
+ );
+ }
$this->trxLevel = 0;
$this->trxAtomicCounter = 0;
$this->trxIdleCallbacks = []; // T67263; transaction already lost
}
/**
- * @return bool Whether it is safe to assume the given error only caused statement rollback
+ * @return bool Whether it is known that the last query error only caused statement rollback
* @note This is for backwards compatibility for callers catching DBError exceptions in
* order to ignore problems like duplicate key errors or foriegn key violations
* @since 1.31
throw new DBUnexpectedError( $this, $msg );
}
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->doBegin( $fname );
$this->trxStatus = self::STATUS_TRX_OK;
}
}
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->runOnTransactionPreCommitCallbacks();
}
if ( $trxActive ) {
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->doRollback( $fname );
$this->trxStatus = self::STATUS_TRX_NONE;
*/
public $mTitle = null;
- /**@{{
+ /**
+ * @var bool
+ * @protected
+ */
+ public $mDataLoaded = false;
+
+ /**
+ * @var bool
+ * @protected
+ */
+ public $mIsRedirect = false;
+
+ /**
+ * @var int|false False means "not loaded"
* @protected
*/
- public $mDataLoaded = false; // !< Boolean
- public $mIsRedirect = false; // !< Boolean
- public $mLatest = false; // !< Integer (false means "not loaded")
- /**@}}*/
+ public $mLatest = false;
/** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */
public $mPreparedEdit = false;
protected static $limitPreferenceName = 'wllimit';
protected static $collapsedPreferenceName = 'rcfilters-wl-collapsed';
+ /** @var float|int */
private $maxDays;
+ /** WatchedItemStore */
+ private $watchStore;
public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
parent::__construct( $page, $restriction );
$this->maxDays = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
+ $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
}
public function doesWrites() {
'label' => 'rcfilters-filter-watchlistactivity-unseen-label',
'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
'cssClassSuffix' => 'watchedunseen',
- 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ 'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
$changeTs = $rc->getAttribute( 'rc_timestamp' );
- $lastVisitTs = $rc->getAttribute( 'wl_notificationtimestamp' );
+ $lastVisitTs = $this->watchStore->getLatestNotificationTimestamp(
+ $rc->getAttribute( 'wl_notificationtimestamp' ),
+ $rc->getPerformer(),
+ $rc->getTitle()
+ );
return $lastVisitTs !== null && $changeTs >= $lastVisitTs;
},
],
}
}
+ // Get the timestamp (TS_MW) of this revision to track the latest one seen
+ $seenTime = call_user_func(
+ $this->revisionGetTimestampFromIdCallback,
+ $title,
+ $oldid ?: $title->getLatestRevID()
+ );
+
// Mark the item as read immediately in lightweight storage
$this->stash->merge(
$this->getPageSeenTimestampsKey( $user ),
- function ( $cache, $key, $current ) use ( $time, $title ) {
+ function ( $cache, $key, $current ) use ( $title, $seenTime ) {
$value = $current ?: new MapCacheLRU( 300 );
- $value->set( $this->getPageSeenKey( $title ), wfTimestamp( TS_MW, $time ) );
-
- $this->latestUpdateCache->set( $key, $value, IExpiringStore::TTL_PROC_LONG );
+ $subKey = $this->getPageSeenKey( $title );
+
+ if ( $seenTime > $value->get( $subKey ) ) {
+ // Revision is newer than the last one seen
+ $value->set( $subKey, $seenTime );
+ $this->latestUpdateCache->set( $key, $value, IExpiringStore::TTL_PROC_LONG );
+ } elseif ( $seenTime === false ) {
+ // Revision does not exist
+ $value->set( $subKey, wfTimestamp( TS_MW ) );
+ $this->latestUpdateCache->set( $key, $value, IExpiringStore::TTL_PROC_LONG );
+ } else {
+ return false; // nothing to update
+ }
return $value;
},
/**
* @param User $user
- * @return MapCacheLRU|null
+ * @return MapCacheLRU|null The map contains prefixed title keys and TS_MW values
*/
private function getPageSeenTimestamps( User $user ) {
$key = $this->getPageSeenTimestampsKey( $user );
}
}
- /**
- *
- */
private function deleteByWhere( $dbw, $startMessage, $where ) {
$this->output( $startMessage . "...\n" );
$total = 0;
$iterator = new DirectoryIterator( $dir );
}
+ /** @var SplFileInfo $info */
foreach ( $iterator as $info ) {
// Ignore directories, work only on php files,
if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] )
$dir_iterator = new RecursiveDirectoryIterator( dirname( $phpfile ) );
$iterator = new RecursiveIteratorIterator(
$dir_iterator, RecursiveIteratorIterator::LEAVES_ONLY );
+ /** @var SplFileInfo $fileObject */
foreach ( $iterator as $path => $fileObject ) {
if ( fnmatch( "*.i18n.php", $fileObject->getFilename() ) ) {
$this->output( "Converting $path.\n" );
),
RecursiveIteratorIterator::LEAVES_ONLY
);
+ /** @var SplFileInfo $fileInfo */
foreach ( $iter as $file => $fileInfo ) {
if ( $fileInfo->isFile() ) {
$files[] = $file;
}
}
- /**
- */
private function assertFileLength( $type, $fileName ) {
$file = file( $fileName, FILE_IGNORE_NEW_LINES );
<?php
-
/**
* @group Database
*/
'ctd_user_defined' => 1
],
];
- $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
+ $res = $dbr->select(
+ 'change_tag_def',
+ [ 'ctd_name', 'ctd_user_defined' ],
+ '',
+ __METHOD__,
+ [ 'ORDER BY' => 'ctd_name' ]
+ );
$this->assertEquals( $expected, iterator_to_array( $res, false ) );
}
}
// Handle some internal calls from the Database class
$check = $fname;
- if ( preg_match( '/^Wikimedia\\\\Rdbms\\\\Database::query \((.+)\)$/', $fname, $m ) ) {
+ if ( preg_match(
+ '/^Wikimedia\\\\Rdbms\\\\Database::(?:query|beginIfImplied) \((.+)\)$/',
+ $fname,
+ $m
+ ) ) {
$check = $m[1];
}
<?php
-
/**
* @group large
* @covers Pbkdf2Password
$oldid
)
);
- $this->assertEquals( 1, $getTimestampCallCounter );
+ $this->assertEquals( 2, $getTimestampCallCounter );
ScopedCallback::consume( $scopedOverrideRevision );
}
$oldid
)
);
- $this->assertEquals( 1, $getTimestampCallCounter );
+ $this->assertEquals( 2, $getTimestampCallCounter );
ScopedCallback::consume( $scopedOverrideRevision );
}
$oldid
)
);
- $this->assertEquals( 1, $getTimestampCallCounter );
+ $this->assertEquals( 2, $getTimestampCallCounter );
ScopedCallback::consume( $scopedOverrideRevision );
}