* to make bot-flagged actions through certain special pages.
* Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
* via ScopedCallback::consume(), the temporary rights are revoked.
+ *
+ * @since 1.34
+ *
* @param UserIdentity $user
* @param string|string[] $rights
* @return ScopedCallback
*/
public function addTemporaryUserRights( UserIdentity $user, $rights ) {
- $nextKey = count( $this->temporaryUserRights[$user->getId()] ?? [] );
- $this->temporaryUserRights[$user->getId()][$nextKey] = (array)$rights;
- return new ScopedCallback( [ $this, 'revokeTemporaryUserRights' ], [ $user->getId(), $nextKey ] );
- }
-
- /**
- * Revoke rights added by addTemporaryUserRights().
- * @param int $userId
- * @param int $rightsGroupKey Key in self::$temporaryUserRights
- * @internal For use by addTemporaryUserRights() only.
- */
- public function revokeTemporaryUserRights( $userId, $rightsGroupKey ) {
- unset( $this->temporaryUserRights[$userId][$rightsGroupKey] );
+ $userId = $user->getId();
+ $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
+ $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
+ return new ScopedCallback( function () use ( $userId, $nextKey ) {
+ unset( $this->temporaryUserRights[$userId][$nextKey] );
+ } );
}
/**
"$method: Prepared output has vary-revision-exists..."
);
return true;
+ } elseif (
+ $out->getFlag( 'vary-revision-sha1' ) &&
+ $out->getRevisionUsedSha1Base36() !== $this->revision->getSha1()
+ ) {
+ // If a self-transclusion used the proposed page text, it must match the final
+ // page content after PST transformations and automatically merged edit conflicts
+ $this->saveParseLogger->info(
+ "$method: Prepared output has vary-revision-sha1 with wrong SHA-1..."
+ );
+ return true;
} else {
// NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was
// set for a null-edit. The reason was that the original rendering in that case was
if ( $editInfo->output->getFlag( 'vary-revision' ) ) {
// This can be used for the initial parse, e.g. for filters or doEditContent(),
- // but a second parse will be triggered in doEditUpdates(). This is not optimal.
+ // but a second parse will be triggered in doEditUpdates() no matter what
$logger->info(
- "Cache for key '{key}' has vary_revision; post-insertion parse inevitable.",
- $context
- );
- } elseif ( $editInfo->output->getFlag( 'vary-revision-id' ) ) {
- // Similar to the above if we didn't guess the ID correctly.
- $logger->debug(
- "Cache for key '{key}' has vary_revision_id; post-insertion parse possible.",
- $context
- );
- } elseif ( $editInfo->output->getFlag( 'vary-revision-timestamp' ) ) {
- // Similar to the above if we didn't guess the timestamp correctly.
- $logger->debug(
- "Cache for key '{key}' has vary_revision_timestamp; post-insertion parse possible.",
+ "Cache for key '{key}' has 'vary-revision'; post-insertion parse inevitable.",
$context
);
+ } else {
+ static $flagsMaybeReparse = [
+ // Similar to the above if we didn't guess the ID correctly
+ 'vary-revision-id',
+ // Similar to the above if we didn't guess the timestamp correctly
+ 'vary-revision-timestamp',
+ // Similar to the above if we didn't guess the content correctly
+ 'vary-revision-sha1'
+ ];
+ foreach ( $flagsMaybeReparse as $flag ) {
+ if ( $editInfo->output->getFlag( $flag ) ) {
+ $logger->debug(
+ "Cache for key '{key}' has $flag; post-insertion parse possible.",
+ $context
+ );
+ }
+ }
}
return $editInfo;
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$casToken = null;
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
+
+ $e = null;
try {
$value = $conn->get( $key );
$casToken = $value;
$this->handleException( $conn, $e );
}
- $this->logRequest( 'get', $key, $server, $result );
+ $this->logRequest( 'get', $key, $conn->getServer(), $e );
+
return $result;
}
protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
+
$ttl = $this->convertToRelative( $exptime );
+
+ $e = null;
try {
if ( $ttl ) {
$result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
$this->handleException( $conn, $e );
}
- $this->logRequest( 'set', $key, $server, $result );
+ $this->logRequest( 'set', $key, $conn->getServer(), $e );
+
return $result;
}
protected function doDelete( $key, $flags = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
+
+ $e = null;
try {
- $conn->delete( $key );
- // Return true even if the key didn't exist
- $result = true;
+ // Note that redis does not return false if the key was not there
+ $result = ( $conn->delete( $key ) !== false );
} catch ( RedisException $e ) {
$result = false;
$this->handleException( $conn, $e );
}
- $this->logRequest( 'delete', $key, $server, $result );
+ $this->logRequest( 'delete', $key, $conn->getServer(), $e );
+
return $result;
}
protected function doGetMulti( array $keys, $flags = 0 ) {
- $batches = [];
+ /** @var RedisConnRef[]|Redis[] $conns */
$conns = [];
+ $batches = [];
foreach ( $keys as $key ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- continue;
+ $conn = $this->getConnection( $key );
+ if ( $conn ) {
+ $server = $conn->getServer();
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
}
- $conns[$server] = $conn;
- $batches[$server][] = $key;
}
+
$result = [];
foreach ( $batches as $server => $batchKeys ) {
$conn = $conns[$server];
+
+ $e = null;
try {
+ // Avoid mget() to reduce CPU hogging from a single request
$conn->multi( Redis::PIPELINE );
foreach ( $batchKeys as $key ) {
$conn->get( $key );
}
$batchResult = $conn->exec();
if ( $batchResult === false ) {
- $this->debug( "multi request to $server failed" );
+ $this->logRequest( 'get', implode( ',', $batchKeys ), $server, true );
continue;
}
+
foreach ( $batchResult as $i => $value ) {
if ( $value !== false ) {
$result[$batchKeys[$i]] = $this->unserialize( $value );
} catch ( RedisException $e ) {
$this->handleException( $conn, $e );
}
+
+ $this->logRequest( 'get', implode( ',', $batchKeys ), $server, $e );
}
- $this->debug( "getMulti for " . count( $keys ) . " keys " .
- "returned " . count( $result ) . " results" );
return $result;
}
- protected function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
- $batches = [];
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ /** @var RedisConnRef[]|Redis[] $conns */
$conns = [];
+ $batches = [];
foreach ( $data as $key => $value ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- continue;
+ $conn = $this->getConnection( $key );
+ if ( $conn ) {
+ $server = $conn->getServer();
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
}
- $conns[$server] = $conn;
- $batches[$server][] = $key;
}
- $expiry = $this->convertToRelative( $expiry );
+ $ttl = $this->convertToRelative( $exptime );
+ $op = $ttl ? 'setex' : 'set';
+
$result = true;
foreach ( $batches as $server => $batchKeys ) {
$conn = $conns[$server];
+
+ $e = null;
try {
+ // Avoid mset() to reduce CPU hogging from a single request
$conn->multi( Redis::PIPELINE );
foreach ( $batchKeys as $key ) {
- if ( $expiry ) {
- $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) );
+ if ( $ttl ) {
+ $conn->setex( $key, $ttl, $this->serialize( $data[$key] ) );
} else {
$conn->set( $key, $this->serialize( $data[$key] ) );
}
}
$batchResult = $conn->exec();
if ( $batchResult === false ) {
- $this->debug( "setMulti request to $server failed" );
+ $this->logRequest( $op, implode( ',', $batchKeys ), $server, true );
continue;
}
$result = $result && !in_array( false, $batchResult, true );
$this->handleException( $conn, $e );
$result = false;
}
+
+ $this->logRequest( $op, implode( ',', $batchKeys ), $server, $e );
}
return $result;
}
protected function doDeleteMulti( array $keys, $flags = 0 ) {
- $batches = [];
+ /** @var RedisConnRef[]|Redis[] $conns */
$conns = [];
+ $batches = [];
foreach ( $keys as $key ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- continue;
+ $conn = $this->getConnection( $key );
+ if ( $conn ) {
+ $server = $conn->getServer();
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
}
- $conns[$server] = $conn;
- $batches[$server][] = $key;
}
$result = true;
foreach ( $batches as $server => $batchKeys ) {
$conn = $conns[$server];
+
+ $e = null;
try {
+ // Avoid delete() with array to reduce CPU hogging from a single request
$conn->multi( Redis::PIPELINE );
foreach ( $batchKeys as $key ) {
$conn->delete( $key );
}
$batchResult = $conn->exec();
if ( $batchResult === false ) {
- $this->debug( "deleteMulti request to $server failed" );
+ $this->logRequest( 'delete', implode( ',', $batchKeys ), $server, true );
continue;
}
+ // Note that redis does not return false if the key was not there
$result = $result && !in_array( false, $batchResult, true );
} catch ( RedisException $e ) {
$this->handleException( $conn, $e );
$result = false;
}
+
+ $this->logRequest( 'delete', implode( ',', $batchKeys ), $server, $e );
+ }
+
+ return $result;
+ }
+
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ /** @var RedisConnRef[]|Redis[] $conns */
+ $conns = [];
+ $batches = [];
+ foreach ( $keys as $key ) {
+ $conn = $this->getConnection( $key );
+ if ( $conn ) {
+ $server = $conn->getServer();
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
+ }
+ }
+
+ $relative = $this->expiryIsRelative( $exptime );
+ $op = ( $exptime == 0 ) ? 'persist' : ( $relative ? 'expire' : 'expireAt' );
+
+ $result = true;
+ foreach ( $batches as $server => $batchKeys ) {
+ $conn = $conns[$server];
+
+ $e = null;
+ try {
+ $conn->multi( Redis::PIPELINE );
+ foreach ( $batchKeys as $key ) {
+ if ( $exptime == 0 ) {
+ $conn->persist( $key );
+ } elseif ( $relative ) {
+ $conn->expire( $key, $this->convertToRelative( $exptime ) );
+ } else {
+ $conn->expireAt( $key, $this->convertToExpiry( $exptime ) );
+ }
+ }
+ $batchResult = $conn->exec();
+ if ( $batchResult === false ) {
+ $this->logRequest( $op, implode( ',', $batchKeys ), $server, true );
+ continue;
+ }
+ $result = in_array( false, $batchResult, true ) ? false : $result;
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ $result = false;
+ }
+
+ $this->logRequest( $op, implode( ',', $batchKeys ), $server, $e );
}
return $result;
}
public function add( $key, $value, $expiry = 0, $flags = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
$this->handleException( $conn, $e );
}
- $this->logRequest( 'add', $key, $server, $result );
+ $this->logRequest( 'add', $key, $conn->getServer(), $result );
return $result;
}
public function incr( $key, $value = 1 ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
$this->handleException( $conn, $e );
}
- $this->logRequest( 'incr', $key, $server, $result );
+ $this->logRequest( 'incr', $key, $conn->getServer(), $result );
return $result;
}
public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
$batchResult = $conn->exec();
if ( $batchResult === false ) {
$result = false;
- $this->debug( "incrWithInit request to $server failed" );
+ $this->debug( "incrWithInit request to {$conn->getServer()} failed" );
} else {
$result = end( $batchResult );
}
$this->handleException( $conn, $e );
}
- $this->logRequest( 'incr', $key, $server, $result );
+ $this->logRequest( 'incr', $key, $conn->getServer(), $result );
return $result;
}
protected function doChangeTTL( $key, $exptime, $flags ) {
- list( $server, $conn ) = $this->getConnection( $key );
+ $conn = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
try {
if ( $exptime == 0 ) {
$result = $conn->persist( $key );
- $this->logRequest( 'persist', $key, $server, $result );
+ $this->logRequest( 'persist', $key, $conn->getServer(), $result );
} elseif ( $relative ) {
$result = $conn->expire( $key, $this->convertToRelative( $exptime ) );
- $this->logRequest( 'expire', $key, $server, $result );
+ $this->logRequest( 'expire', $key, $conn->getServer(), $result );
} else {
$result = $conn->expireAt( $key, $this->convertToExpiry( $exptime ) );
- $this->logRequest( 'expireAt', $key, $server, $result );
+ $this->logRequest( 'expireAt', $key, $conn->getServer(), $result );
}
} catch ( RedisException $e ) {
$result = false;
}
/**
- * Get a Redis object with a connection suitable for fetching the specified key
* @param string $key
- * @return array (server, RedisConnRef) or (false, false)
+ * @return RedisConnRef|Redis|null Redis handle wrapper for the key or null on failure
*/
protected function getConnection( $key ) {
$candidates = array_keys( $this->serverTagMap );
// by now in such cases.
if ( $this->automaticFailover && $candidates ) {
try {
- if ( $this->getMasterLinkStatus( $conn ) === 'down' ) {
+ /** @var string[] $info */
+ $info = $conn->info();
+ if ( ( $info['master_link_status'] ?? null ) === 'down' ) {
// If the master cannot be reached, fail-over to the next server.
// If masters are in data-center A, and replica DBs in data-center B,
// this helps avoid the case were fail-over happens in A but not
}
} catch ( RedisException $e ) {
// Server is not accepting commands
- $this->handleException( $conn, $e );
+ $this->redisPool->handleError( $conn, $e );
continue;
}
}
- return [ $server, $conn ];
+ return $conn;
}
$this->setLastError( BagOStuff::ERR_UNREACHABLE );
- return [ false, false ];
- }
-
- /**
- * Check the master link status of a Redis server that is configured as a replica DB.
- * @param RedisConnRef $conn
- * @return string|null Master link status (either 'up' or 'down'), or null
- * if the server is not a replica DB.
- */
- protected function getMasterLinkStatus( RedisConnRef $conn ) {
- $info = $conn->info();
- return $info['master_link_status'] ?? null;
+ return null;
}
/**
* @param RedisConnRef $conn
* @param RedisException $e
*/
- protected function handleException( RedisConnRef $conn, $e ) {
+ protected function handleException( RedisConnRef $conn, RedisException $e ) {
$this->setLastError( BagOStuff::ERR_UNEXPECTED );
$this->redisPool->handleError( $conn, $e );
}
/**
* Send information about a single request to the debug log
- * @param string $method
- * @param string $key
+ * @param string $op
+ * @param string $keys
* @param string $server
- * @param bool $result
+ * @param Exception|bool|null $e
*/
- public function logRequest( $method, $key, $server, $result ) {
- $this->debug( "$method $key on $server: " .
- ( $result === false ? "failure" : "success" ) );
+ public function logRequest( $op, $keys, $server, $e = null ) {
+ $this->debug( "$op($keys) on $server: " . ( $e ? "failure" : "success" ) );
}
}
$this->id = $id;
}
- /**
- * @param LoggerInterface $logger
- * @return null
- */
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
* @param string $server A hostname/port combination or the absolute path of a UNIX socket.
* If a hostname is specified but no port, port 6379 will be used.
* @param LoggerInterface|null $logger PSR-3 logger intance. [optional]
- * @return RedisConnRef|bool Returns false on failure
+ * @return RedisConnRef|Redis|bool Returns false on failure
* @throws MWException
*/
public function getConnection( $server, LoggerInterface $logger = null ) {
+ // The above @return also documents 'Redis' for convenience with IDEs.
+ // RedisConnRef uses PHP magic methods, which wouldn't be recognised.
+
$logger = $logger ?: $this->logger;
// Check the listing "dead" servers which have had a connection errors.
// Servers are marked dead for a limited period of time, to
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $title );
+ $rev = self::getCachedRevisionObject( $parser, $title, 'vary-revision-sha1' );
$length = $rev ? $rev->getSize() : 0;
if ( $length === null ) {
// We've had bugs where rev_len was not being recorded for empty pages, see T135414
*
* @param Parser $parser
* @param Title $title
+ * @param string $vary ParserOuput vary-* flag
* @return Revision
* @since 1.23
*/
- private static function getCachedRevisionObject( $parser, $title = null ) {
- if ( is_null( $title ) ) {
+ private static function getCachedRevisionObject( $parser, $title, $vary ) {
+ if ( !$title ) {
return null;
}
- // Use the revision from the parser itself, when param is the current page
- // and the revision is the current one
- if ( $title->equals( $parser->getTitle() ) ) {
- $parserRev = $parser->getRevisionObject();
- if ( $parserRev && $parserRev->isCurrent() ) {
- // force reparse after edit with vary-revision flag
- $parser->getOutput()->setFlag( 'vary-revision' );
- wfDebug( __METHOD__ . ": use current revision from parser, setting vary-revision...\n" );
- return $parserRev;
+ $revision = null;
+
+ $isSelfReferential = $title->equals( $parser->getTitle() );
+ if ( $isSelfReferential ) {
+ // Revision is for the same title that is currently being parsed. Only use the last
+ // saved revision, regardless of Parser::getRevisionId() or fake revision injection
+ // callbacks against the current title.
+ $parserRevision = $parser->getRevisionObject();
+ if ( $parserRevision && $parserRevision->isCurrent() ) {
+ $revision = $parserRevision;
+ wfDebug( __METHOD__ . ": used current revision, setting $vary" );
}
}
- // Normalize name for cache
- $page = $title->getPrefixedDBkey();
-
- if ( !( $parser->currentRevisionCache && $parser->currentRevisionCache->has( $page ) )
- && !$parser->incrementExpensiveFunctionCount() ) {
- return null;
+ $parserOutput = $parser->getOutput();
+ if ( !$revision ) {
+ if (
+ !$parser->isCurrentRevisionOfTitleCached( $title ) &&
+ !$parser->incrementExpensiveFunctionCount()
+ ) {
+ return null; // not allowed
+ }
+ // Get the current revision, ignoring Parser::getRevisionId() being null/old
+ $revision = $parser->fetchCurrentRevisionOfTitle( $title );
+ // Register dependency in templatelinks
+ $parserOutput->addTemplate(
+ $title,
+ $revision ? $revision->getPage() : 0,
+ $revision ? $revision->getId() : 0
+ );
}
- $rev = $parser->fetchCurrentRevisionOfTitle( $title );
- $pageID = $rev ? $rev->getPage() : 0;
- $revID = $rev ? $rev->getId() : 0;
- // Register dependency in templatelinks
- $parser->getOutput()->addTemplate( $title, $pageID, $revID );
+ if ( $isSelfReferential ) {
+ // Upon page save, the result of the parser function using this might change
+ $parserOutput->setFlag( $vary );
+ if ( $vary === 'vary-revision-sha1' && $revision ) {
+ $parserOutput->setRevisionUsedSha1Base36( $revision->getSha1() );
+ }
+ }
- return $rev;
+ return $revision;
}
/**
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-id' );
return $rev ? $rev->getId() : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'j' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'd' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'm' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'n' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'Y' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-revision-timestamp' );
return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'YmdHis' ) : '';
}
return '';
}
// fetch revision from cache/database and return the value
- $rev = self::getCachedRevisionObject( $parser, $t );
+ $rev = self::getCachedRevisionObject( $parser, $t, 'vary-user' );
return $rev ? $rev->getUserText() : '';
}
return $this->currentRevisionCache->get( $cacheKey );
}
+ /**
+ * @param Title $title
+ * @return bool
+ * @since 1.34
+ */
+ public function isCurrentRevisionOfTitleCached( $title ) {
+ return (
+ $this->currentRevisionCache &&
+ $this->currentRevisionCache->has( $title->getPrefixedText() )
+ );
+ }
+
/**
* Wrapper around Revision::newFromTitle to allow passing additional parameters
* without passing them on to it.
foreach ( $stuff['deps'] as $dep ) {
$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
if ( $dep['title']->equals( $this->getTitle() ) ) {
- // If we transclude ourselves, the final result
- // will change based on the new version of the page
+ // Self-transclusion; final result may change based on the new page version
$this->mOutput->setFlag( 'vary-revision' );
wfDebug( __METHOD__ . ": self transclusion, setting vary-revision" );
}
/** @var int|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */
private $revisionTimestampUsed;
+ /** @var string|null SHA-1 base 36 hash of any self-transclusion */
+ private $revisionUsedSha1Base36;
+
/** string CSS classes to use for the wrapping div, stored in the array keys.
* If no class is given, no wrapper is added.
*/
return $this->revisionTimestampUsed;
}
+ /**
+ * @param string $hash Lowercase SHA-1 base 36 hash
+ * @since 1.34
+ */
+ public function setRevisionUsedSha1Base36( $hash ) {
+ if ( $hash === null ) {
+ return; // e.g. RevisionRecord::getSha1() returned null
+ }
+
+ if (
+ $this->revisionUsedSha1Base36 !== null &&
+ $this->revisionUsedSha1Base36 !== $hash
+ ) {
+ $this->revisionUsedSha1Base36 = ''; // mismatched
+ } else {
+ $this->revisionUsedSha1Base36 = $hash;
+ }
+ }
+
+ /**
+ * @return string|null Lowercase SHA-1 base 36 hash, null if unused, or "" on inconsistency
+ * @since 1.34
+ */
+ public function getRevisionUsedSha1Base36() {
+ return $this->revisionUsedSha1Base36;
+ }
+
public function &getLanguageLinks() {
return $this->mLanguageLinks;
}
$success = $maintenance->execute();
} catch ( Exception $ex ) {
$success = false;
+ $exReportMessage = '';
while ( $ex ) {
$cls = get_class( $ex );
- print "$cls from line {$ex->getLine()} of {$ex->getFile()}: {$ex->getMessage()}\n";
- print $ex->getTraceAsString() . "\n";
+ $exReportMessage .= "$cls from line {$ex->getLine()} of {$ex->getFile()}: {$ex->getMessage()}\n";
+ $exReportMessage .= $ex->getTraceAsString() . "\n";
$ex = $ex->getPrevious();
}
+ // Print the exception to stderr if possible, don't mix it in
+ // with stdout output.
+ if ( defined( 'STDERR' ) ) {
+ fwrite( STDERR, $exReportMessage );
+ } else {
+ echo $exReportMessage;
+ }
}
// Potentially debug globals
* @file
* @ingroup Testing
*/
+
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
use MediaWiki\Tidy\TidyDriverBase;
*/
private $keepUploads;
+ /** @var Title */
+ private $defaultTitle;
+
/**
* @param TestRecorder $recorder
* @param array $options
if ( isset( $options['upload-dir'] ) ) {
$this->uploadDir = $options['upload-dir'];
}
+
+ $this->defaultTitle = Title::newFromText( 'Parser test' );
}
/**
$options->setTidy( true );
}
- if ( isset( $opts['title'] ) ) {
- $titleText = $opts['title'];
- } else {
- $titleText = 'Parser test';
+ $revId = 1337; // see Parser::getRevisionId()
+ $title = isset( $opts['title'] )
+ ? Title::newFromText( $opts['title'] )
+ : $this->defaultTitle;
+
+ if ( isset( $opts['lastsavedrevision'] ) ) {
+ $content = new WikitextContent( $test['input'] );
+ $title = Title::newFromRow( (object)[
+ 'page_id' => 187,
+ 'page_len' => $content->getSize(),
+ 'page_latest' => 1337,
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey(),
+ 'page_is_redirect' => 0
+ ] );
+ $rev = new Revision(
+ [
+ 'id' => $title->getLatestRevID(),
+ 'page' => $title->getArticleID(),
+ 'user' => $user,
+ 'content' => $content,
+ 'timestamp' => $this->getFakeTimestamp(),
+ 'title' => $title
+ ],
+ Revision::READ_LATEST,
+ $title
+ );
+ $oldCallback = $options->getCurrentRevisionCallback();
+ $options->setCurrentRevisionCallback(
+ function ( Title $t, $parser ) use ( $title, $rev, $oldCallback ) {
+ if ( $t->equals( $title ) ) {
+ return $rev;
+ } else {
+ return call_user_func( $oldCallback, $t, $parser );
+ }
+ }
+ );
}
if ( isset( $opts['maxincludesize'] ) ) {
$local = isset( $opts['local'] );
$preprocessor = $opts['preprocessor'] ?? null;
$parser = $this->getParser( $preprocessor );
- $title = Title::newFromText( $titleText );
if ( isset( $opts['styletag'] ) ) {
// For testing the behavior of <style> (including those deduplicated
} elseif ( isset( $opts['preload'] ) ) {
$out = $parser->getPreloadText( $test['input'], $title, $options );
} else {
- $output = $parser->parse( $test['input'], $title, $options, true, true, 1337 );
+ $output = $parser->parse( $test['input'], $title, $options, true, true, $revId );
$out = $output->getText( [
'allowTOC' => !isset( $opts['notoc'] ),
'unwrap' => !isset( $opts['wrap'] ),
!! end
!! test
-Magic Word: {{REVISIONID}}
+Magic Word: {{REVISIONID}} on latest revision
!! options
+lastsavedrevision
parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
showflags
!! wikitext
flags=vary-revision-id
!! end
+!! test
+Magic Word: {{REVISIONID}} on non-latest revision
+!! options
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONID}}
+!! html/*
+<p>1337
+</p>
+flags=vary-revision-id
+!! end
+
+!! test
+Magic Word: {{REVISIONTIMESTAMP}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONTIMESTAMP}}
+!! html/*
+<p>19700101000203
+</p>
+flags=
+!! end
+
+!! test
+Magic Word: {{REVISIONTIMESTAMP}} on non-existing page
+!! options
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONTIMESTAMP}}
+!! html/*
+<p>123
+</p>
+flags=vary-revision-timestamp
+!! end
+
+!! test
+Magic Word: {{REVISIONUSER}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONUSER}}
+!! html/*
+<p>127.0.0.1
+</p>
+flags=vary-user
+!! end
+
+!! test
+Parser Function: {{REVISIONID:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONID:{{PAGENAME}}}}
+!! html/*
+<p>1337
+</p>
+flags=vary-revision-id
+!! end
+
+!! test
+Parser Function: {{REVISIONID:{{PAGENAME}}}} on non-saved revision
+!! options
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONID:{{PAGENAME}}}}
+!! html/*
+
+flags=vary-revision-id
+!! end
+
+!! test
+Parser Function: {{REVISIONTIMESTAMP:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONTIMESTAMP:{{PAGENAME}}}}
+!! html/*
+<p>19700101000203
+</p>
+flags=vary-revision-timestamp
+!! end
+
+!! test
+Parser Function: {{REVISIONDAY:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONDAY:{{PAGENAME}}}}
+!! html/*
+<p>1
+</p>
+flags=vary-revision-timestamp
+!! end
+
+!! test
+Parser Function: {{REVISIONMONTH:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONMONTH:{{PAGENAME}}}}
+!! html/*
+<p>01
+</p>
+flags=vary-revision-timestamp
+!! end
+
+!! test
+Parser Function: {{REVISIONYEAR:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{REVISIONYEAR:{{PAGENAME}}}}
+!! html/*
+<p>1970
+</p>
+flags=vary-revision-timestamp
+!! end
+
+!! test
+Parser Function: {{PAGESIZE:{{PAGENAME}}}} on latest revision
+!! options
+lastsavedrevision
+parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
+!! wikitext
+{{PAGESIZE:{{PAGENAME}}}}
+!! html/*
+<p>25
+</p>
+flags=vary-revision-sha1
+!! end
+
!! test
Magic Word: {{SCRIPTPATH}}
!! options
/**
* @covers \MediaWiki\Permissions\PermissionManager::addTemporaryUserRights
- * @covers \MediaWiki\Permissions\PermissionManager::revokeTemporaryUserRights
*/
- public function testTemporaryUserRights() {
+ public function testAddTemporaryUserRights() {
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
$this->overrideUserPermissions( $this->user, [ 'read', 'edit' ] );
// sanity checks
$this->assertFalse( $this->cache->get( $key2 ) );
$this->assertFalse( $this->cache->get( $key3 ) );
- $this->cache->setMulti( [
- $key1 => 1,
- $key2 => 2,
- $key3 => 3
- ] );
+ $ok = $this->cache->setMulti( [ $key1 => 1, $key2 => 2, $key3 => 3 ] );
+
+ $this->assertTrue( $ok, "setMulti() succeeded" );
+ $this->assertEquals(
+ 3,
+ count( $this->cache->getMulti( [ $key1, $key2, $key3 ] ) ),
+ "setMulti() succeeded via getMulti() check"
+ );
$ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], 300 );
$this->assertTrue( $ok, "TTL bumped for all keys" );