in JavaScript, use mw.log.deprecate() instead.
* The 'user.groups' module, deprecated in 1.28, was removed.
Use the 'user' module instead.
+* The ability to override User::$mRights has been removed.
* …
=== Deprecations in 1.34 ===
template option 'searchaction' instead.
* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
been deprecated.
+* User::getRights() and User::$mRights have been deprecated. Use
+ PermissionManager::getUserPermissions() instead.
=== Other changes in 1.34 ===
* …
/* WHERE */ [ 'cl_from' => $this->next ],
__METHOD__ . '-1'
);
- foreach ( $res as $o ) {
- $k = $o->cl_to;
+ foreach ( $res as $row ) {
+ $k = $row->cl_to;
# Update parent tree
- if ( !isset( $this->parents[$o->cl_from] ) ) {
- $this->parents[$o->cl_from] = [];
+ if ( !isset( $this->parents[$row->cl_from] ) ) {
+ $this->parents[$row->cl_from] = [];
}
- $this->parents[$o->cl_from][$k] = $o;
+ $this->parents[$row->cl_from][$k] = $row;
# Ignore those we already have
if ( in_array( $k, $this->deadend ) ) {
/* WHERE */ [ 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ],
__METHOD__ . '-2'
);
- foreach ( $res as $o ) {
- $id = $o->page_id;
- $name = $o->page_title;
+ foreach ( $res as $row ) {
+ $id = $row->page_id;
+ $name = $row->page_title;
$this->name2id[$name] = $id;
$this->next[] = $id;
unset( $layer[$name] );
$wgEnableWANCacheReaper = false;
/**
- * Main object stash type. This should be a fast storage system for storing
- * lightweight data like hit counters and user activity. Sites with multiple
- * data-centers should have this use a store that replicates all writes. The
- * store should have enough consistency for CAS operations to be usable.
- * Reads outside of those needed for merge() may be eventually consistent.
+ * The object store type of the main stash.
+ *
+ * This store should be a very fast storage system optimized for holding lightweight data
+ * like incrementable hit counters and current user activity. The store should replicate the
+ * dataset among all data-centers. Any add(), merge(), lock(), and unlock() operations should
+ * maintain "best effort" linearizability; as long as connectivity is strong, latency is low,
+ * and there is no eviction pressure prompted by low free space, those operations should be
+ * linearizable. In terms of PACELC (https://en.wikipedia.org/wiki/PACELC_theorem), the store
+ * should act as a PA/EL distributed system for these operations. One optimization for these
+ * operations is to route them to a "primary" data-center (e.g. one that serves HTTP POST) for
+ * synchronous execution and then replicate to the others asynchronously. This means that at
+ * least calls to these operations during HTTP POST requests would quickly return.
+ *
+ * All other operations, such as get(), set(), delete(), changeTTL(), incr(), and decr(),
+ * should be synchronous in the local data-center, replicating asynchronously to the others.
+ * This behavior can be overriden by the use of the WRITE_SYNC and READ_LATEST flags.
+ *
+ * The store should *preferably* have eventual consistency to handle network partitions.
+ *
+ * Modules that rely on the stash should be prepared for:
+ * - add(), merge(), lock(), and unlock() to be slower than other write operations,
+ * at least in "secondary" data-centers (e.g. one that only serves HTTP GET/HEAD)
+ * - Other write operations to have race conditions accross data-centers
+ * - Read operations to have race conditions accross data-centers
+ * - Consistency to be either eventual (with Last-Write-Wins) or just "best effort"
+ *
+ * In general, this means avoiding updates during idempotent HTTP requests (GET/HEAD) and
+ * avoiding assumptions of true linearizability (e.g. accepting anomalies). Modules that need
+ * these kind of guarantees should use other storage mediums.
*
* The options are:
* - db: Store cache objects in the DB
* Returns an HTML link element in a string styled as a button
* (when $wgUseMediaWikiUIEverywhere is enabled).
*
- * @param string $contents The raw HTML contents of the element: *not*
- * escaped!
+ * @param string $text The text of the element. Will be escaped (not raw HTML)
* @param array $attrs Associative array of attributes, e.g., [
* 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
* further documentation.
* @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
* @return string Raw HTML
*/
- public static function linkButton( $contents, array $attrs, array $modifiers = [] ) {
+ public static function linkButton( $text, array $attrs, array $modifiers = [] ) {
return self::element( 'a',
self::buttonAttributes( $attrs, $modifiers ),
- $contents
+ $text
);
}
* See DataUpdate::getCauseAction(). (default 'unknown')
* - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent().
* (string, default 'unknown')
+ * - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+ * from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+ * matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+ * for the time until caches have been changed to store RenderedRevision states instead
+ * of ParserOutput objects. (default: null) (since 1.33)
*/
public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
Assert::parameter(
if ( $this->renderedRevision ) {
$this->renderedRevision->updateRevision( $revision );
} else {
-
// NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
// NOTE: the revision is either new or current, so we can bypass audience checks.
$this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
$this->revision,
null,
null,
- [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
+ [
+ 'use-master' => $this->useMaster(),
+ 'audience' => RevisionRecord::RAW,
+ 'known-revision-output' => $options['known-revision-output'] ?? null
+ ]
);
// XXX: Since we presumably are dealing with the current revision,
*/
function formatRow( $row ) {
if ( $this->lastRow ) {
- $latest = ( $this->counter == 1 && $this->mIsFirst );
$firstInList = $this->counter == 1;
$this->counter++;
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $row, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
$s .= Html::hidden( 'type', 'revision' ) . "\n";
// Button container stored in $this->buttons for re-use in getEndBody()
- $this->buttons = Html::openElement( 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
- $className = 'historysubmit mw-history-compareselectedversions-button';
- $attrs = [ 'class' => $className ]
- + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
- $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
- $attrs
- ) . "\n";
-
- $user = $this->getUser();
- $actionButtons = '';
- if ( $user->isAllowed( 'deleterevision' ) ) {
- $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' );
- }
- if ( $this->showTagEditUI ) {
- $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' );
- }
- if ( $actionButtons ) {
- $this->buttons .= Xml::tags( 'div', [ 'class' =>
- 'mw-history-revisionactions' ], $actionButtons );
- }
+ $this->buttons = '';
+ if ( $this->getNumRows() > 0 ) {
+ $this->buttons .= Html::openElement(
+ 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
+ $className = 'historysubmit mw-history-compareselectedversions-button';
+ $attrs = [ 'class' => $className ]
+ + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
+ $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
+ $attrs
+ ) . "\n";
+
+ $user = $this->getUser();
+ $actionButtons = '';
+ if ( $user->isAllowed( 'deleterevision' ) ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'revisiondelete', 'showhideselectedversions' );
+ }
+ if ( $this->showTagEditUI ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'editchangetags', 'history-edit-tags' );
+ }
+ if ( $actionButtons ) {
+ $this->buttons .= Xml::tags( 'div', [ 'class' =>
+ 'mw-history-revisionactions' ], $actionButtons );
+ }
- if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
- $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
- }
+ if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
+ $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
+ }
- $this->buttons .= '</div>';
+ $this->buttons .= '</div>';
- $s .= $this->buttons;
+ $s .= $this->buttons;
+ }
$s .= '<ul id="pagehistory">' . "\n";
return $s;
protected function getEndBody() {
if ( $this->lastRow ) {
- $latest = $this->counter == 1 && $this->mIsFirst;
$firstInList = $this->counter == 1;
if ( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $next, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
* @param mixed $next The database row corresponding to the next line
* (chronologically previous)
* @param bool|string $notificationtimestamp
- * @param bool $latest Whether this row corresponds to the page's latest revision.
+ * @param bool $dummy Unused.
* @param bool $firstInList Whether this row corresponds to the first
* displayed on this history page.
* @return string HTML output for the row
*/
function historyLine( $row, $next, $notificationtimestamp = false,
- $latest = false, $firstInList = false ) {
+ $dummy = false, $firstInList = false ) {
$rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
$prevRev = null;
}
- $curlink = $this->curLink( $rev, $latest );
+ $latest = $rev->getId() === $this->getWikiPage()->getLatest();
+ $curlink = $this->curLink( $rev );
$lastlink = $this->lastLink( $rev, $next );
$curLastlinks = Html::rawElement( 'span', [], $curlink ) .
Html::rawElement( 'span', [], $lastlink );
* Create a diff-to-current link for this revision for this page
*
* @param Revision $rev
- * @param bool $latest This is the latest revision of the page?
* @return string
*/
- function curLink( $rev, $latest ) {
+ function curLink( $rev ) {
$cur = $this->historyPage->message['cur'];
- if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $latest = $this->getWikiPage()->getLatest();
+ if ( $latest === $rev->getId() || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
return $cur;
} else {
return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
new HtmlArmor( $cur ),
[],
[
- 'diff' => $this->getWikiPage()->getLatest(),
+ 'diff' => $latest,
'oldid' => $rev->getId()
]
);
use CLDRPluralRuleParser\Evaluator;
use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
+use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
/**
*/
private $store;
+ /**
+ * @var \Psr\Log\LoggerInterface
+ */
+ private $logger;
+
/**
* A 2-d associative array, code/key, where presence indicates that the item
* is loaded. Value arbitrary.
global $wgCacheDirectory;
$this->conf = $conf;
+ $this->logger = LoggerFactory::getInstance( 'localisation' );
$directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
$storeArg = [];
);
}
}
-
- wfDebugLog( 'caches', static::class . ": using store $storeClass" );
+ $this->logger->debug( static::class . ": using store $storeClass" );
$this->store = new $storeClass( $storeArg );
foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
*/
public function isExpired( $code ) {
if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
- wfDebug( __METHOD__ . "($code): forced reload\n" );
+ $this->logger->debug( __METHOD__ . "($code): forced reload\n" );
return true;
}
$preload = $this->store->get( $code, 'preload' );
// Different keys may expire separately for some stores
if ( $deps === null || $keys === null || $preload === null ) {
- wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
+ $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one\n" );
return true;
}
// anymore (e.g. uninstalled extensions)
// When this happens, always expire the cache
if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
- wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
+ $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
get_class( $dep ) . "\n" );
return true;
try {
$compiledRules = Evaluator::compile( $rules );
} catch ( CLDRPluralRuleError $e ) {
- wfDebugLog( 'l10n', $e->getMessage() );
+ $this->logger->debug( $e->getMessage() );
return [];
}
# Load the primary localisation from the source file
$data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
if ( $data === false ) {
- wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
+ $this->logger->debug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
$coreData['fallback'] = 'en';
} else {
- wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
+ $this->logger->debug( __METHOD__ . ": got localisation for $code from source\n" );
# Merge primary localisation
foreach ( $data as $key => $value ) {
}
/**
- * Acquire a lock for performing link table updates for a page on a DB
+ * Acquire a session-level lock for performing link table updates for a page on a DB
*
* @param IDatabase $dbw
* @param int $pageId
"config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis 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.\n\nThis 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'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]",
"config-sidebar-readme": "Прачытай мяне",
+ "config-sidebar-relnotes": "Заўвагі да выпуску",
"config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
"config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
"config-env-php": "Усталяваны PHP $1.",
"config-page-existingwiki": "Eksisterende wiki",
"config-help-restart": "Vil du rydde alle gemte data, du har indtastet og genstarte installationen?",
"config-restart": "Ja, genstart den",
+ "config-sidebar-upgrade": "Opgraderer",
"config-env-php": "PHP $1 er installeret.",
"config-env-hhvm": "HHVM $1 er installeret.",
"config-apc": "[https://www.php.net/apc APC] er installeret",
"Tosky",
"Selven",
"Sarah Bernabei",
- "ArTrix"
+ "ArTrix",
+ "Annibale covini gerolamo"
]
},
"config-desc": "Programma di installazione per MediaWiki",
"config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/Special:MyLanguage/Help:Contents Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:Contents Guida ai contenuti per admin]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:FAQ FAQ]",
"config-sidebar-readme": "Leggimi",
"config-sidebar-relnotes": "Note di versione",
+ "config-sidebar-license": "copiando",
"config-sidebar-upgrade": "Aggiornamento",
"config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
"config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
*
* @file
*/
+
+use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
/**
"Non-daemonized mode is no longer supported. Please install the " .
"mediawiki/services/jobrunner service and update \$wgJobTypeConf as needed." );
}
- $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
+ $this->logger = LoggerFactory::getInstance( 'redis' );
}
protected function supportedOrders() {
try {
return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
return array_sum( $conn->exec() );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
throw new RedisException( $err );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$job = $this->getJobFromFields( $item ); // may be false
} while ( !$job ); // job may be false if invalid
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $job;
$this->incrStats( 'acks', $this->type );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return true;
// Update the timestamp of the last root job started at the location...
return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
// Get the last time this root job was enqueued
$timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
} catch ( RedisException $e ) {
- $timestamp = false;
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
// Check if a new root job was started at the location after this one's...
return $ok;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
$uids = $conn->lRange( $this->getQueueKey( 'l-unclaimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-delayed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-claimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-abandoned' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
}
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $sizes;
* This function should not be called outside JobQueueRedis
*
* @param string $uid
- * @param RedisConnRef $conn
+ * @param RedisConnRef|Redis $conn
* @return RunnableJob|bool Returns false if the job does not exist
* @throws JobQueueError
* @throws UnexpectedValueException
*/
- public function getJobFromUidInternal( $uid, RedisConnRef $conn ) {
+ public function getJobFromUidInternal( $uid, $conn ) {
try {
$data = $conn->hGet( $this->getQueueKey( 'h-data' ), $uid );
if ( $data === false ) {
return $job;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$queues[] = $this->decodeQueueName( $queue );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $queues;
/**
* Get a connection to the server that handles all sub-queues for this queue
*
- * @return RedisConnRef
+ * @return RedisConnRef|Redis
* @throws JobQueueConnectionError
*/
protected function getConnection() {
/**
* @param RedisConnRef $conn
* @param RedisException $e
- * @throws JobQueueError
+ * @return JobQueueError
*/
- protected function throwRedisException( RedisConnRef $conn, $e ) {
+ protected function handleErrorAndMakeException( RedisConnRef $conn, $e ) {
$this->redisPool->handleError( $conn, $e );
- throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
+ return new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
}
/**
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionRenderer;
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
/**
* Job to update link tables for pages
* @ingroup JobQueue
*/
class RefreshLinksJob extends Job {
- /** @var float Cache parser output when it takes this long to render */
- const PARSE_THRESHOLD_SEC = 1.0;
/** @var int Lag safety margin when comparing root job times to last-refresh times */
- const CLOCK_FUDGE = 10;
+ const NORMAL_MAX_LAG = 10;
/** @var int How many seconds to wait for replica DBs to catch up */
const LAG_WAIT_TIMEOUT = 15;
!( isset( $params['pages'] ) && count( $params['pages'] ) != 1 )
);
$this->params += [ 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
- // This will control transaction rounds in order to run DataUpdates
+ // Tell JobRunner to not automatically wrap run() in a transaction round.
+ // Each runForTitle() call will manage its own rounds in order to run DataUpdates
+ // and to avoid contention as well.
$this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
}
}
function run() {
- global $wgUpdateRowsPerJob;
-
$ok = true;
+
// Job to update all (or a range of) backlink pages for a page
if ( !empty( $this->params['recursive'] ) ) {
+ $services = MediaWikiServices::getInstance();
// When the base job branches, wait for the replica DBs to catch up to the master.
// From then on, we know that any template changes at the time the base job was
// enqueued will be reflected in backlink page parses when the leaf jobs run.
if ( !isset( $this->params['range'] ) ) {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory = $services->getDBLoadBalancerFactory();
if ( !$lbFactory->waitForReplication( [
- 'domain' => $lbFactory->getLocalDomainID(),
- 'timeout' => self::LAG_WAIT_TIMEOUT
+ 'domain' => $lbFactory->getLocalDomainID(),
+ 'timeout' => self::LAG_WAIT_TIMEOUT
] ) ) { // only try so hard
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats = $services->getStatsdDataFactory();
$stats->increment( 'refreshlinks.lag_wait_failed' );
}
}
// jobs and possibly a recursive RefreshLinks job for the rest of the backlinks
$jobs = BacklinkJobUtils::partitionBacklinkJob(
$this,
- $wgUpdateRowsPerJob,
+ $services->getMainConfig()->get( 'UpdateRowsPerJob' ),
1, // job-per-title
[ 'params' => $extraParams ]
);
foreach ( $this->params['pages'] as list( $ns, $dbKey ) ) {
$title = Title::makeTitleSafe( $ns, $dbKey );
if ( $title ) {
- $this->runForTitle( $title );
+ $ok = $this->runForTitle( $title ) && $ok;
} else {
$ok = false;
$this->setLastError( "Invalid title ($ns,$dbKey)." );
}
// Job to update link tables for a given title
} else {
- $this->runForTitle( $this->title );
+ $ok = $this->runForTitle( $this->title );
}
return $ok;
protected function runForTitle( Title $title ) {
$services = MediaWikiServices::getInstance();
$stats = $services->getStatsdDataFactory();
- $lbFactory = $services->getDBLoadBalancerFactory();
- $revisionStore = $services->getRevisionStore();
$renderer = $services->getRevisionRenderer();
+ $parserCache = $services->getParserCache();
+ $lbFactory = $services->getDBLoadBalancerFactory();
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $lbFactory->beginMasterChanges( __METHOD__ );
-
+ // Load the page from the master DB
$page = WikiPage::factory( $title );
$page->loadPageData( WikiPage::READ_LATEST );
- // Serialize links updates by page ID so they see each others' changes
+ // Serialize link update job by page ID so they see each others' changes.
+ // The page ID and latest revision ID will be queried again after the lock
+ // is acquired to bail if they are changed from that of loadPageData() above.
$dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
- /** @noinspection PhpUnusedLocalVariableInspection */
$scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' );
if ( $scopedLock === null ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
- // Another job is already updating the page, likely for an older revision (T170596).
+ // Another job is already updating the page, likely for a prior revision (T170596)
$this->setLastError( 'LinksUpdate already running for this page, try again later.' );
+ $stats->increment( 'refreshlinks.lock_failure' );
+
+ return false;
+ }
+
+ if ( $this->isAlreadyRefreshed( $page ) ) {
+ $stats->increment( 'refreshlinks.update_skipped' );
+
+ return true;
+ }
+
+ // Parse during a fresh transaction round for better read consistency
+ $lbFactory->beginMasterChanges( __METHOD__ );
+ $output = $this->getParserOutput( $renderer, $parserCache, $page, $stats );
+ $options = $this->getDataUpdateOptions();
+ $lbFactory->commitMasterChanges( __METHOD__ );
+
+ if ( !$output ) {
+ return false; // raced out?
+ }
+
+ // Tell DerivedPageDataUpdater to use this parser output
+ $options['known-revision-output'] = $output;
+ // Execute corresponding DataUpdates immediately
+ $page->doSecondaryDataUpdates( $options );
+ InfoAction::invalidateCache( $title );
+
+ // Commit any writes here in case this method is called in a loop.
+ // In that case, the scoped lock will fail to be acquired.
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+
+ return true;
+ }
+
+ /**
+ * @param WikiPage $page
+ * @return bool Whether something updated the backlinks with data newer than this job
+ */
+ private function isAlreadyRefreshed( WikiPage $page ) {
+ // Get the timestamp of the change that triggered this job
+ $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+ if ( $rootTimestamp === null ) {
return false;
}
- // Get the latest ID *after* acquirePageLock() flushed the transaction.
+
+ if ( !empty( $this->params['isOpportunistic'] ) ) {
+ // Neither clock skew nor DB snapshot/replica DB lag matter much for
+ // such updates; focus on reusing the (often recently updated) cache
+ $lagAwareTimestamp = $rootTimestamp;
+ } else {
+ // For transclusion updates, the template changes must be reflected
+ $lagAwareTimestamp = wfTimestamp(
+ TS_MW,
+ wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
+ );
+ }
+
+ return ( $page->getLinksTimestamp() > $lagAwareTimestamp );
+ }
+
+ /**
+ * Get the parser output if the page is unchanged from what was loaded in $page
+ *
+ * @param RevisionRenderer $renderer
+ * @param ParserCache $parserCache
+ * @param WikiPage $page Page already loaded with READ_LATEST
+ * @param StatsdDataFactoryInterface $stats
+ * @return ParserOutput|null Combined output for all slots; might only contain metadata
+ */
+ private function getParserOutput(
+ RevisionRenderer $renderer,
+ ParserCache $parserCache,
+ WikiPage $page,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $revision = $this->getCurrentRevisionIfUnchanged( $page, $stats );
+ if ( !$revision ) {
+ return null; // race condition?
+ }
+
+ $cachedOutput = $this->getParserOutputFromCache( $parserCache, $page, $revision, $stats );
+ if ( $cachedOutput ) {
+ return $cachedOutput;
+ }
+
+ $renderedRevision = $renderer->getRenderedRevision(
+ $revision,
+ $page->makeParserOptions( 'canonical' ),
+ null,
+ [ 'audience' => $revision::RAW ]
+ );
+
+ $parseTimestamp = wfTimestampNow(); // timestamp that parsing started
+ $output = $renderedRevision->getRevisionParserOutput( [ 'generate-html' => false ] );
+ $output->setCacheTime( $parseTimestamp ); // notify LinksUpdate::doUpdate()
+
+ return $output;
+ }
+
+ /**
+ * Get the current revision record if it is unchanged from what was loaded in $page
+ *
+ * @param WikiPage $page Page already loaded with READ_LATEST
+ * @param StatsdDataFactoryInterface $stats
+ * @return RevisionRecord|null The same instance that $page->getRevisionRecord() uses
+ */
+ private function getCurrentRevisionIfUnchanged(
+ WikiPage $page,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $title = $page->getTitle();
+ // Get the latest ID since acquirePageLock() in runForTitle() flushed the transaction.
// This is used to detect edits/moves after loadPageData() but before the scope lock.
- // The works around the chicken/egg problem of determining the scope lock key.
+ // The works around the chicken/egg problem of determining the scope lock key name.
$latest = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
- if ( !empty( $this->params['triggeringRevisionId'] ) ) {
- // Fetch the specified revision; lockAndGetLatest() below detects if the page
- // was edited since and aborts in order to avoid corrupting the link tables
- $revision = $revisionStore->getRevisionById(
- (int)$this->params['triggeringRevisionId'],
- Revision::READ_LATEST
- );
- } else {
- // Fetch current revision; READ_LATEST reduces lockAndGetLatest() check failures
- $revision = $revisionStore->getRevisionByTitle( $title, 0, Revision::READ_LATEST );
+ $triggeringRevisionId = $this->params['triggeringRevisionId'] ?? null;
+ if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) {
+ // This job is obsolete and one for the latest revision will handle updates
+ $stats->increment( 'refreshlinks.rev_not_current' );
+ $this->setLastError( "Revision $triggeringRevisionId is not current" );
+
+ return null;
}
+ // Load the current revision. Note that $page should have loaded with READ_LATEST.
+ // This instance will be reused in WikiPage::doSecondaryDataUpdates() later on.
+ $revision = $page->getRevisionRecord();
if ( !$revision ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
$stats->increment( 'refreshlinks.rev_not_found' );
$this->setLastError( "Revision not found for {$title->getPrefixedDBkey()}" );
- return false; // just deleted?
- } elseif ( $revision->getId() != $latest || $revision->getPageId() !== $page->getId() ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
+
+ return null; // just deleted?
+ } elseif ( $revision->getId() !== $latest || $revision->getPageId() !== $page->getId() ) {
// Do not clobber over newer updates with older ones. If all jobs where FIFO and
// serialized, it would be OK to update links based on older revisions since it
// would eventually get to the latest. Since that is not the case (by design),
// only update the link tables to a state matching the current revision's output.
$stats->increment( 'refreshlinks.rev_not_current' );
$this->setLastError( "Revision {$revision->getId()} is not current" );
- return false;
+
+ return null;
}
- $parserOutput = false;
- $parserOptions = $page->makeParserOptions( 'canonical' );
+ return $revision;
+ }
+
+ /**
+ * Get the parser output from cache if it reflects the change that triggered this job
+ *
+ * @param ParserCache $parserCache
+ * @param WikiPage $page
+ * @param RevisionRecord $currentRevision
+ * @param StatsdDataFactoryInterface $stats
+ * @return ParserOutput|null
+ */
+ private function getParserOutputFromCache(
+ ParserCache $parserCache,
+ WikiPage $page,
+ RevisionRecord $currentRevision,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $cachedOutput = null;
// If page_touched changed after this root job, then it is likely that
// any views of the pages already resulted in re-parses which are now in
// cache. The cache can be reused to avoid expensive parsing in some cases.
- if ( isset( $this->params['rootJobTimestamp'] ) ) {
+ $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+ if ( $rootTimestamp !== null ) {
$opportunistic = !empty( $this->params['isOpportunistic'] );
-
- $skewedTimestamp = $this->params['rootJobTimestamp'];
if ( $opportunistic ) {
- // Neither clock skew nor DB snapshot/replica DB lag matter much for such
- // updates; focus on reusing the (often recently updated) cache
+ // Neither clock skew nor DB snapshot/replica DB lag matter much for
+ // such updates; focus on reusing the (often recently updated) cache
+ $lagAwareTimestamp = $rootTimestamp;
} else {
// For transclusion updates, the template changes must be reflected
- $skewedTimestamp = wfTimestamp( TS_MW,
- wfTimestamp( TS_UNIX, $skewedTimestamp ) + self::CLOCK_FUDGE
+ $lagAwareTimestamp = wfTimestamp(
+ TS_MW,
+ wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
);
}
- if ( $page->getLinksTimestamp() > $skewedTimestamp ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
- // Something already updated the backlinks since this job was made
- $stats->increment( 'refreshlinks.update_skipped' );
- return true;
- }
-
- if ( $page->getTouched() >= $this->params['rootJobTimestamp'] || $opportunistic ) {
- // Cache is suspected to be up-to-date. As long as the cache rev ID matches
- // and it reflects the job's triggering change, then it is usable.
- $parserOutput = $services->getParserCache()->getDirty( $page, $parserOptions );
- if ( !$parserOutput
- || $parserOutput->getCacheRevisionId() != $revision->getId()
- || $parserOutput->getCacheTime() < $skewedTimestamp
+ if ( $page->getTouched() >= $rootTimestamp || $opportunistic ) {
+ // Cache is suspected to be up-to-date so it's worth the I/O of checking.
+ // As long as the cache rev ID matches the current rev ID and it reflects
+ // the job's triggering change, then it is usable.
+ $parserOptions = $page->makeParserOptions( 'canonical' );
+ $output = $parserCache->getDirty( $page, $parserOptions );
+ if (
+ $output &&
+ $output->getCacheRevisionId() == $currentRevision->getId() &&
+ $output->getCacheTime() >= $lagAwareTimestamp
) {
- $parserOutput = false; // too stale
+ $cachedOutput = $output;
}
}
}
- // Fetch the current revision and parse it if necessary...
- if ( $parserOutput ) {
+ if ( $cachedOutput ) {
$stats->increment( 'refreshlinks.parser_cached' );
} else {
- $start = microtime( true );
-
- $checkCache = $page->shouldCheckParserCache( $parserOptions, $revision->getId() );
-
- // Revision ID must be passed to the parser output to get revision variables correct
- $renderedRevision = $renderer->getRenderedRevision(
- $revision,
- $parserOptions,
- null,
- [
- // use master, for consistency with the getRevisionByTitle call above.
- 'use-master' => true,
- // bypass audience checks, since we know that this is the current revision.
- 'audience' => RevisionRecord::RAW
- ]
- );
- $parserOutput = $renderedRevision->getRevisionParserOutput(
- // HTML is only needed if the output is to be placed in the parser cache
- [ 'generate-html' => $checkCache ]
- );
-
- // If it took a long time to render, then save this back to the cache to avoid
- // wasted CPU by other apaches or job runners. We don't want to always save to
- // cache as this can cause high cache I/O and LRU churn when a template changes.
- $elapsed = microtime( true ) - $start;
-
- $parseThreshold = $this->params['parseThreshold'] ?? self::PARSE_THRESHOLD_SEC;
-
- if ( $checkCache && $elapsed >= $parseThreshold && $parserOutput->isCacheable() ) {
- $ctime = wfTimestamp( TS_MW, (int)$start ); // cache time
- $services->getParserCache()->save(
- $parserOutput, $page, $parserOptions, $ctime, $revision->getId()
- );
- }
$stats->increment( 'refreshlinks.parser_uncached' );
}
+ return $cachedOutput;
+ }
+
+ /**
+ * @return array
+ */
+ private function getDataUpdateOptions() {
$options = [
'recursive' => !empty( $this->params['useRecursiveLinksUpdate'] ),
// Carry over cause so the update can do extra logging
}
}
- $lbFactory->commitMasterChanges( __METHOD__ );
-
- $page->doSecondaryDataUpdates( $options );
-
- InfoAction::invalidateCache( $title );
-
- // Commit any writes here in case this method is called in a loop.
- // In that case, the scoped lock will fail to be acquired.
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
-
- return true;
+ return $options;
}
public function getDeduplicationInfo() {
/** @var float|null */
private $wallClockOverride;
+ /** @var float */
const RANK_TOP = 1.0;
/** @var int Array key that holds the entry's main timestamp (flat key use) */
*
* @param string $key
* @param mixed $value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
* @return void
*/
public function set( $key, $value, $rank = self::RANK_TOP ) {
* Check if a key exists
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function has( $key, $maxAge = 0.0 ) {
+ public function has( $key, $maxAge = INF ) {
if ( !is_int( $key ) && !is_string( $key ) ) {
throw new UnexpectedValueException(
__METHOD__ . ': invalid key; must be string or integer.' );
* If the item is already set, it will be pushed to the top of the cache.
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
- * @return mixed Returns null if the key was not found or is older than $maxAge
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
+ * @param mixed|null $default Value to return if no key is found [default: null]
+ * @return mixed Returns $default if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
+ * @since 1.34 Added $default
*/
- public function get( $key, $maxAge = 0.0 ) {
+ public function get( $key, $maxAge = INF, $default = null ) {
if ( !$this->has( $key, $maxAge ) ) {
- return null;
+ return $default;
}
$this->ping( $key );
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function hasField( $key, $field, $maxAge = 0.0 ) {
+ public function hasField( $key, $field, $maxAge = INF ) {
$value = $this->get( $key );
if ( !is_int( $field ) && !is_string( $field ) ) {
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed Returns null if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
*/
- public function getField( $key, $field, $maxAge = 0.0 ) {
+ public function getField( $key, $field, $maxAge = INF ) {
if ( !$this->hasField( $key, $field, $maxAge ) ) {
return null;
}
* @since 1.28
* @param string $key
* @param callable $callback Callback that will produce the value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
- * @param float $maxAge Ignore items older than this many seconds [Default: 0.0] (since 1.32)
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed The cached value if found or the result of $callback otherwise
+ * @since 1.32 Added $maxAge
*/
public function getWithSetCallback(
- $key, callable $callback, $rank = self::RANK_TOP, $maxAge = 0.0
+ $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
) {
if ( $this->has( $key, $maxAge ) ) {
$value = $this->get( $key );
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
do {
$casToken = null; // passed by reference
// Get the old value and CAS token from cache
/**
* Delete all objects expiring before a certain date.
* @param string|int $timestamp The reference date in MW or TS_UNIX format
- * @param callable|null $progressCallback Optional, a function which will be called
+ * @param callable|null $progress Optional, a function which will be called
* regularly during long-running operations with the percentage progress
* as the first parameter. [optional]
* @param int $limit Maximum number of keys to delete [default: INF]
*
- * @return bool Success, false if unimplemented
+ * @return bool Success; false if unimplemented
*/
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- // stub
return false;
}
* @return bool Success
* @since 1.24
*/
- final public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
-
return $this->doSetMulti( $data, $exptime, $flags );
}
foreach ( $data as $key => $value ) {
$res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
}
-
return $res;
}
* @return bool Success
* @since 1.33
*/
- final public function deleteMulti( array $keys, $flags = 0 ) {
+ public function deleteMulti( array $keys, $flags = 0 ) {
if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
-
return $this->doDeleteMulti( $keys, $flags );
}
foreach ( $keys as $key ) {
$res = $this->doDelete( $key, $flags ) && $res;
}
-
return $res;
}
* @param mixed $mainValue
* @return string|null|bool The combined string, false if missing, null on error
*/
- protected function resolveSegments( $key, $mainValue ) {
+ final protected function resolveSegments( $key, $mainValue ) {
if ( SerializedValueContainer::isUnified( $mainValue ) ) {
return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
}
* @param callable $workCallback
* @since 1.28
*/
- public function addBusyCallback( callable $workCallback ) {
+ final public function addBusyCallback( callable $workCallback ) {
$this->busyCallbacks[] = $workCallback;
}
*/
protected function debug( $text ) {
if ( $this->debugMode ) {
- $this->logger->debug( "{class} debug: $text", [
- 'class' => static::class,
- ] );
+ $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
}
}
* @param int $exptime
* @return bool
*/
- protected function expiryIsRelative( $exptime ) {
+ final protected function expiryIsRelative( $exptime ) {
return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
}
* @param int $exptime Absolute TTL or 0 for indefinite
* @return int
*/
- protected function convertToExpiry( $exptime ) {
- $exptime = (int)$exptime; // sanity
-
+ final protected function convertToExpiry( $exptime ) {
return $this->expiryIsRelative( $exptime )
? (int)$this->getCurrentTime() + $exptime
: $exptime;
* @param int $exptime
* @return int
*/
- protected function convertToRelative( $exptime ) {
- if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
- $exptime -= (int)$this->getCurrentTime();
- if ( $exptime <= 0 ) {
- $exptime = 1;
- }
- return $exptime;
- } else {
- return $exptime;
- }
+ final protected function convertToRelative( $exptime ) {
+ return $this->expiryIsRelative( $exptime )
+ ? (int)$exptime
+ : max( $exptime - (int)$this->getCurrentTime(), 1 );
}
/**
* @param mixed $value
* @return bool
*/
- protected function isInteger( $value ) {
+ final protected function isInteger( $value ) {
if ( is_int( $value ) ) {
return true;
} elseif ( !is_string( $value ) ) {
* @param BagOStuff[] $bags
* @return int[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
*/
- protected function mergeFlagMaps( array $bags ) {
+ final protected function mergeFlagMaps( array $bags ) {
$map = [];
foreach ( $bags as $bag ) {
foreach ( $bag->attrMap as $attr => $rank ) {
* up going to the HashBagOStuff used for the in-memory cache).
*
* @ingroup Cache
- * @TODO: Make this class use composition instead of calling super
*/
-class CachedBagOStuff extends HashBagOStuff {
+class CachedBagOStuff extends BagOStuff {
/** @var BagOStuff */
protected $backend;
+ /** @var HashBagOStuff */
+ protected $procCache;
/**
* @param BagOStuff $backend Permanent backend to use
* @param array $params Parameters for HashBagOStuff
*/
- function __construct( BagOStuff $backend, $params = [] ) {
+ public function __construct( BagOStuff $backend, $params = [] ) {
unset( $params['reportDupes'] ); // useless here
parent::__construct( $params );
$this->backend = $backend;
+ $this->procCache = new HashBagOStuff( $params );
$this->attrMap = $backend->attrMap;
}
- public function get( $key, $flags = 0 ) {
- $ret = parent::get( $key, $flags );
- if ( $ret === false && !$this->hasKey( $key ) ) {
+ protected function doGet( $key, $flags = 0, &$casToken = null ) {
+ $ret = $this->procCache->get( $key, $flags );
+ if ( $ret === false && !$this->procCache->hasKey( $key ) ) {
$ret = $this->backend->get( $key, $flags );
- $this->set( $key, $ret, 0, self::WRITE_CACHE_ONLY );
+ $this->set( $key, $ret, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
}
+
return $ret;
}
- public function set( $key, $value, $exptime = 0, $flags = 0 ) {
- parent::set( $key, $value, $exptime, $flags );
- if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
- $this->backend->set( $key, $value, $exptime, $flags & ~self::WRITE_CACHE_ONLY );
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+ $this->procCache->set( $key, $value, $exptime, $flags );
+ if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+ $this->backend->set( $key, $value, $exptime, $flags );
}
+
return true;
}
- public function delete( $key, $flags = 0 ) {
- parent::delete( $key, $flags );
- if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
- $this->backend->delete( $key );
+ protected function doDelete( $key, $flags = 0 ) {
+ $this->procCache->delete( $key, $flags );
+ if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+ $this->backend->delete( $key, $flags );
}
return true;
}
- public function setDebug( $bool ) {
- parent::setDebug( $bool );
- $this->backend->setDebug( $bool );
- }
-
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- parent::deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit );
+ $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
- return $this->backend->deleteObjectsExpiringBefore(
- $timestamp,
- $progressCallback,
- $limit
- );
- }
-
- public function makeKeyInternal( $keyspace, $args ) {
- return $this->backend->makeKeyInternal( ...func_get_args() );
- }
-
- public function makeKey( $class, $component = null ) {
- return $this->backend->makeKey( ...func_get_args() );
- }
-
- public function makeGlobalKey( $class, $component = null ) {
- return $this->backend->makeGlobalKey( ...func_get_args() );
+ return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
}
// These just call the backend (tested elsewhere)
public function incr( $key, $value = 1 ) {
$n = $this->backend->incr( $key, $value );
- parent::delete( $key );
+
+ $this->procCache->delete( $key );
return $n;
}
return $this->backend->unlock( $key );
}
+ public function makeKeyInternal( $keyspace, $args ) {
+ return $this->backend->makeKeyInternal( ...func_get_args() );
+ }
+
+ public function makeKey( $class, $component = null ) {
+ return $this->backend->makeKey( ...func_get_args() );
+ }
+
+ public function makeGlobalKey( $class, $component = null ) {
+ return $this->backend->makeGlobalKey( ...func_get_args() );
+ }
+
+ public function setDebug( $bool ) {
+ parent::setDebug( $bool );
+ $this->backend->setDebug( $bool );
+ }
+
public function getLastError() {
return $this->backend->getLastError();
}
return false;
}
- protected function doSet( $key, $value, $exp = 0, $flags = 0 ) {
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
return true;
}
* @return bool
* @since 1.27
*/
- protected function hasKey( $key ) {
+ public function hasKey( $key ) {
return isset( $this->bag[$key] );
}
}
$this->segmentationSize = $params['maxPreferedKeySize'] ?? 917504; // < 1MiB
}
- /**
- * Fill in some defaults for missing keys in $params.
- *
- * @param array $params
- * @return array
- */
- protected function applyDefaultParams( $params ) {
- return $params + [
- 'compress_threshold' => 1500,
- 'connect_timeout' => 0.5,
- 'debug' => false
- ];
- }
-
/**
* Construct a cache key.
*
/**
* Available parameters are:
- * - servers: The list of IP:port combinations holding the memcached servers.
- * - persistent: Whether to use a persistent connection
- * - compress_threshold: The minimum size an object must be before it is compressed
- * - timeout: The read timeout in microseconds
- * - connect_timeout: The connect timeout in seconds
- * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
- * - server_failure_limit: Limit for server connect failures before it is removed
- * - serializer: May be either "php" or "igbinary". Igbinary produces more compact
- * values, but serialization is much slower unless the php.ini option
- * igbinary.compact_strings is off.
- * - use_binary_protocol Whether to enable the binary protocol (default is ASCII) (boolean)
+ * - servers: List of IP:port combinations holding the memcached servers.
+ * - persistent: Whether to use a persistent connection
+ * - compress_threshold: The minimum size an object must be before it is compressed
+ * - timeout: The read timeout in microseconds
+ * - connect_timeout: The connect timeout in seconds
+ * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
+ * - server_failure_limit: Limit for server connect failures before it is removed
+ * - serializer: Either "php" or "igbinary". Igbinary produces more compact
+ * values, but serialization is much slower unless the php.ini
+ * option igbinary.compact_strings is off.
+ * - use_binary_protocol Whether to enable the binary protocol (default is ASCII)
+ * - allow_tcp_nagle_delay Whether to permit Nagle's algorithm for reducing packet count
* @param array $params
- * @throws InvalidArgumentException
*/
function __construct( $params ) {
parent::__construct( $params );
- $params = $this->applyDefaultParams( $params );
+
+ // Default class-specific parameters
+ $params += [
+ 'compress_threshold' => 1500,
+ 'connect_timeout' => 0.5,
+ 'serializer' => 'php',
+ 'use_binary_protocol' => false,
+ 'allow_tcp_nagle_delay' => true
+ ];
if ( $params['persistent'] ) {
// The pool ID must be unique to the server/option combination.
// The Memcached object is essentially shared for each pool ID.
// We can only reuse a pool ID if we keep the config consistent.
- $this->client = new Memcached( md5( serialize( $params ) ) );
- if ( count( $this->client->getServerList() ) ) {
- $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
- return; // already initialized; don't add duplicate servers
- }
+ $connectionPoolId = md5( serialize( $params ) );
+ $client = new Memcached( $connectionPoolId );
+ $this->initializeClient( $client, $params );
} else {
- $this->client = new Memcached;
+ $client = new Memcached;
+ $this->initializeClient( $client, $params );
}
- if ( $params['use_binary_protocol'] ) {
- $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
- }
-
- if ( isset( $params['retry_timeout'] ) ) {
- $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
- }
-
- if ( isset( $params['server_failure_limit'] ) ) {
- $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
- }
+ $this->client = $client;
// The compression threshold is an undocumented php.ini option for some
// reason. There's probably not much harm in setting it globally, for
// compatibility with the settings for the PHP client.
ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
+ }
- // Set timeouts
- $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
- $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
+ /**
+ * Initialize the client only if needed and reuse it otherwise.
+ * This avoids duplicate servers in the list and new connections.
+ *
+ * @param Memcached $client
+ * @param array $params
+ * @throws RuntimeException
+ */
+ private function initializeClient( Memcached $client, array $params ) {
+ if ( $client->getServerList() ) {
+ $this->logger->debug( __METHOD__ . ": pre-initialized client instance." );
- // Set libketama mode since it's recommended by the documentation and
- // is as good as any. There's no way to configure libmemcached to use
- // hashes identical to the ones currently in use by the PHP client, and
- // even implementing one of the libmemcached hashes in pure PHP for
- // forwards compatibility would require MemcachedClient::get_sock() to be
- // rewritten.
- $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
+ return; // preserve persistent handle
+ }
- // Set the serializer
- $ok = false;
+ $this->logger->debug( __METHOD__ . ": initializing new client instance." );
+
+ $options = [
+ // Network protocol (ASCII or binary)
+ Memcached::OPT_BINARY_PROTOCOL => $params['use_binary_protocol'],
+ // Set various network timeouts
+ Memcached::OPT_CONNECT_TIMEOUT => $params['connect_timeout'] * 1000,
+ Memcached::OPT_SEND_TIMEOUT => $params['timeout'],
+ Memcached::OPT_RECV_TIMEOUT => $params['timeout'],
+ Memcached::OPT_POLL_TIMEOUT => $params['timeout'] / 1000,
+ // Avoid pointless delay when sending/fetching large blobs
+ Memcached::OPT_TCP_NODELAY => !$params['allow_tcp_nagle_delay'],
+ // Set libketama mode since it's recommended by the documentation
+ Memcached::OPT_LIBKETAMA_COMPATIBLE => true
+ ];
+ if ( isset( $params['retry_timeout'] ) ) {
+ $options[Memcached::OPT_RETRY_TIMEOUT] = $params['retry_timeout'];
+ }
+ if ( isset( $params['server_failure_limit'] ) ) {
+ $options[Memcached::OPT_SERVER_FAILURE_LIMIT] = $params['server_failure_limit'];
+ }
if ( $params['serializer'] === 'php' ) {
- $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+ $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_PHP;
} elseif ( $params['serializer'] === 'igbinary' ) {
if ( !Memcached::HAVE_IGBINARY ) {
- throw new InvalidArgumentException(
+ throw new RuntimeException(
__CLASS__ . ': the igbinary extension is not available ' .
'but igbinary serialization was requested.'
);
}
- $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
+ $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_IGBINARY;
}
- if ( !$ok ) {
- throw new InvalidArgumentException( __CLASS__ . ': invalid serializer parameter' );
+
+ if ( !$client->setOptions( $options ) ) {
+ throw new RuntimeException(
+ "Invalid options: " . json_encode( $options, JSON_PRETTY_PRINT )
+ );
}
$servers = [];
$servers[] = [ $host, false ]; // (ip or path, port)
}
}
- $this->client->addServers( $servers );
- }
-
- protected function applyDefaultParams( $params ) {
- $params = parent::applyDefaultParams( $params );
-
- if ( !isset( $params['use_binary_protocol'] ) ) {
- $params['use_binary_protocol'] = false;
- }
- if ( !isset( $params['serializer'] ) ) {
- $params['serializer'] = 'php';
+ if ( !$client->addServers( $servers ) ) {
+ throw new RuntimeException( "Failed to inject server address list" );
}
-
- return $params;
}
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$this->debug( "get($key)" );
if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
+ /** @noinspection PhpUndefinedClassConstantInspection */
$flags = Memcached::GET_EXTENDED;
$res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags );
if ( is_array( $res ) ) {
return $result;
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
$this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
return $this->checkResult( false, $result );
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
foreach ( array_keys( $data ) as $key ) {
$this->validateKeyEncoding( $key );
return $this->checkResult( false, $result );
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
$this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
/**
* Available parameters are:
* - servers: The list of IP:port combinations holding the memcached servers.
- * - debug: Whether to set the debug flag in the underlying client.
* - persistent: Whether to use a persistent connection
* - compress_threshold: The minimum size an object must be before it is compressed
* - timeout: The read timeout in microseconds
*/
function __construct( $params ) {
parent::__construct( $params );
- $params = $this->applyDefaultParams( $params );
+
+ // Default class-specific parameters
+ $params += [
+ 'compress_threshold' => 1500,
+ 'connect_timeout' => 0.5
+ ];
$this->client = new MemcachedClient( $params );
$this->client->set_servers( $params['servers'] );
- $this->client->set_debug( $params['debug'] );
}
public function setDebug( $debug ) {
);
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
$ret = false;
foreach ( $this->caches as $cache ) {
- if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit ) ) {
+ if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
$ret = true;
}
}
return $res;
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->doWrite(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
);
}
- public function doDeleteMulti( array $data, $flags = 0 ) {
+ public function deleteMulti( array $data, $flags = 0 ) {
+ return $this->doWrite(
+ $this->cacheIndexes,
+ $this->usesAsyncWritesGivenFlags( $flags ),
+ __FUNCTION__,
+ func_get_args()
+ );
+ }
+
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->doWrite(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
+ protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
protected function serialize( $value ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
- protected function unserialize( $value ) {
+ protected function unserialize( $blob ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
}
return $result;
}
- protected function doSet( $key, $value, $expiry = 0, $flags = 0 ) {
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
- $expiry = $this->convertToRelative( $expiry );
+ $ttl = $this->convertToRelative( $exptime );
try {
- if ( $expiry ) {
- $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
+ if ( $ttl ) {
+ $result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
} else {
// No expiry, that is very different from zero expiry in Redis
$result = $conn->set( $key, $this->serialize( $value ) );
return $result;
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $keys as $key ) {
return $result;
}
- public function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $data as $key => $value ) {
return $result;
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $keys as $key ) {
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- return $this->writeStore->deleteObjectsExpiringBefore(
- $timestamp,
- $progressCallback,
- $limit
- );
+ return $this->writeStore->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
}
public function getMulti( array $keys, $flags = 0 ) {
: $this->readStore->getMulti( $keys, $flags );
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->writeStore->setMulti( $data, $exptime, $flags );
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ public function deleteMulti( array $keys, $flags = 0 ) {
return $this->writeStore->deleteMulti( $keys, $flags );
}
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
+ }
+
public function incr( $key, $value = 1 ) {
return $this->writeStore->incr( $key, $value );
}
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
+ protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
protected function serialize( $value ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
return $success;
}
- protected function doSet( $key, $value, $expire = 0, $flags = 0 ) {
- $result = wincache_ucache_set( $key, $this->serialize( $value ), $expire );
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+ $result = wincache_ucache_set( $key, $this->serialize( $value ), $exptime );
// false positive, wincache_ucache_set returns an empty array
// in some circumstances.
$this->server,
$this->user,
$this->password,
- $this->getDBname(),
- $this->dbSchema(),
+ $this->currentDomain->getDatabase(),
+ $this->currentDomain->getSchema(),
$this->tablePrefix()
);
$this->lastPing = microtime( true );
$this->server,
$this->user,
$this->password,
- $this->getDBname(),
- $this->dbSchema(),
+ $this->currentDomain->getDatabase(),
+ $this->currentDomain->getSchema(),
$this->tablePrefix()
);
$this->lastPing = microtime( true );
/**
* Get the cache object for the main stash.
*
- * Stash objects are BagOStuff instances suitable for storing light
- * weight data that is not canonically stored elsewhere (such as RDBMS).
- * Stashes should be configured to propagate changes to all data-centers.
- *
- * Callers should be prepared for:
- * - a) Writes to be slower in non-"primary" (e.g. HTTP GET/HEAD only) DCs
- * - b) Reads to be eventually consistent, e.g. for get()/getMulti()
- * In general, this means avoiding updates on idempotent HTTP requests and
- * avoiding an assumption of perfect serializability (or accepting anomalies).
- * Reads may be eventually consistent or data might rollback as nodes flap.
- * Callers can use BagOStuff:READ_LATEST to see the latest available data.
- *
* @return BagOStuff
* @since 1.26
* @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainObjectStash()
return $values;
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->modifyMulti( $data, $exptime, $flags, self::$OP_SET );
}
return (bool)$db->affectedRows();
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
return $this->modifyMulti(
array_fill_keys( $keys, null ),
0,
return $ok;
}
- public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->modifyMulti(
array_fill_keys( $keys, null ),
$exptime,
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
/** @noinspection PhpUnusedLocalVariableInspection */
$this->deleteServerObjectsExpiringBefore(
$db,
$timestamp,
- $progressCallback,
+ $progress,
$limit,
$numServersDone,
$keysDeletedCount
* - defer: one of the DeferredUpdates constants, or false to run immediately (default: false).
* Note that even when this is set to false, some updates might still get deferred (as
* some update might directly add child updates to DeferredUpdates).
+ * - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+ * from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+ * matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+ * for the time until caches have been changed to store RenderedRevision states instead
+ * of ParserOutput objects. (default: null) (since 1.33)
* @since 1.32
*/
public function doSecondaryDataUpdates( array $options = [] ) {
'image' => $this->getName(),
'variant' => $variant,
'format' => $format,
- 'lang' => $context->getLanguage(),
- 'skin' => $context->getSkin(),
- 'version' => $context->getVersion(),
];
+ if ( $this->varyOnLanguage() ) {
+ $query['lang'] = $context->getLanguage();
+ }
+ // The following parameters are at the end to keep the original order of the parameters.
+ $query['skin'] = $context->getSkin();
+ $query['version'] = $context->getVersion();
return wfAppendQuery( $script, $query );
}
return $png ?: false;
}
}
+
+ /**
+ * Check if the image depends on the language.
+ *
+ * @return bool
+ */
+ private function varyOnLanguage() {
+ return is_array( $this->descriptor ) && (
+ isset( $this->descriptor['ltr'] ) ||
+ isset( $this->descriptor['rtl'] ) ||
+ isset( $this->descriptor['lang'] ) );
+ }
}
/** @var bool */
protected $warn = true;
- /** @var SessionManager|null */
+ /** @var SessionManagerInterface|null */
protected $manager;
/** @var BagOStuff|null */
/** @var array Track original session fields for later modification check */
protected $sessionFieldCache = [];
- protected function __construct( SessionManager $manager ) {
+ protected function __construct( SessionManagerInterface $manager ) {
$this->setEnableFlags(
\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
);
/**
* Install a session handler for the current web request
- * @param SessionManager $manager
+ * @param SessionManagerInterface $manager
*/
- public static function install( SessionManager $manager ) {
+ public static function install( SessionManagerInterface $manager ) {
if ( self::$instance ) {
$manager->setupPHPSessionHandler( self::$instance );
return;
/**
* Set the manager, store, and logger
* @private Use self::install().
- * @param SessionManager $manager
+ * @param SessionManagerInterface $manager
* @param BagOStuff $store
* @param LoggerInterface $logger
*/
public function setManager(
- SessionManager $manager, BagOStuff $store, LoggerInterface $logger
+ SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger
) {
if ( $this->manager !== $manager ) {
// Close any existing session before we change stores
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Diff' );
$form = HTMLForm::factory( 'ooui', [
'Page1' => [
$this->setHeaders();
$this->outputHeader();
$this->checkPermissions();
+ $this->addHelpLink( 'Help:User contributions' );
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'deletedcontributions-title' ) );
$out = $this->getOutput();
$out->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:User_rights_and_groups' );
$out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Manual:Tags' );
$request = $this->getRequest();
switch ( $par ) {
class UncategorizedImagesPage extends ImageQueryPage {
function __construct( $name = 'Uncategorizedimages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
function __construct( $name = 'Uncategorizedpages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Deletion_and_undeletion' );
$this->loadRequest( $par );
$this->checkPermissions(); // Needs to be after mTargetObj is set
"delete-legend": "Выдаліць",
"historywarning": "<strong>Папярэджаньне</strong>: старонка, якую Вы зьбіраецеся выдаліць, мае гісторыю з $1 {{PLURAL:$1|вэрсіі|вэрсіяў|вэрсіяў}}:",
"historyaction-submit": "Паказаць вэрсіі",
- "confirmdeletetext": "Ð\97аÑ\80аз Ð\92Ñ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о Ð\92ы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
+ "confirmdeletetext": "Ð\97аÑ\80аз вÑ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о вÑ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о вы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
"actioncomplete": "Дзеяньне выкананае",
"actionfailed": "Дзеяньне ня выкананае",
"deletedtext": "«$1» была выдаленая.\nЗапісы пра выдаленыя старонкі зьмяшчаюцца ў $2.",
"autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
"systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
"blockednoreason": "keine Begründung angegeben",
+ "blockedtext-composite": "<strong>Dein Benutzername oder deine IP-Adresse wurde gesperrt.</strong>\n\nDer Angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der längsten Sperre: $6\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
+ "blockedtext-composite-reason": "Es gibt mehrere Sperren gegen dein Benutzerkonto und/oder deine IP-Adresse",
"whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
"confirmedittext": "Du musst deine E-Mail-Adresse erst bestätigen, bevor du Bearbeitungen durchführen kannst. Bitte ergänze und bestätige deine E-Mail in den [[Special:Preferences|Einstellungen]].",
"nosuchsectiontitle": "Abschnitt nicht gefunden",
"mw-widgets-abandonedit-title": "Bist du sicher?",
"mw-widgets-copytextlayout-copy": "Kopieren",
"mw-widgets-copytextlayout-copy-fail": "Der Text konnte nicht in die Zwischenablage kopiert werden.",
+ "mw-widgets-copytextlayout-copy-success": "Text in die Zwischenablage kopiert.",
"mw-widgets-dateinput-no-date": "Kein Datum ausgewählt",
"mw-widgets-dateinput-placeholder-day": "JJJJ-MM-TT",
"mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
"restrictionsfield-help": "Eine IP-Adresse oder ein CIDR-Bereich pro Zeile. Um alles zu aktivieren, verwende:\n<pre>\n0.0.0.0/0\n::/0\n</pre>",
"edit-error-short": "Fehler: $1",
"edit-error-long": "Fehler:\n\n$1",
+ "specialmute": "Stumm",
+ "specialmute-success": "Deine Stummschaltungseinstellungen wurden aktualisiert. Schau dir alle stummgeschalteten Benutzer in [[Special:Preferences|deinen Einstellungen]] an.",
+ "specialmute-submit": "Bestätigen",
+ "specialmute-label-mute-email": "E-Mails von diesem Benutzer stummschalten",
+ "specialmute-header": "Bitte wähle deine Stummschaltungseinstellungen für {{BIDI:[[User:$1]]}}.",
+ "specialmute-error-invalid-user": "Der gesuchte Benutzername konnte nicht gefunden werden.",
+ "specialmute-error-email-blacklist-disabled": "Das Stummschalten von E-Mails von Benutzern ist nicht aktiviert.",
+ "specialmute-error-email-preferences": "Du musst deine E-Mail Adresse bestätigen bevor du einen Benutzer bestätigen kannst. Du kannst dies [[Special:Preferences|in deinen Einstellungen]] tun.",
+ "specialmute-email-footer": "Um deine E-Mail Einstellungen für {{BIDI:$2}} zu verwalten besuche bitte $1.",
+ "specialmute-login-required": "Bitte melde dich an um deine Stummschaltungseinstellungen zu ändern.",
"revid": "Version $1",
"pageid": "Seitenkennung $1",
"interfaceadmin-info": "$1\n\nBerechtigungen für das Bearbeiten von wikiweiten CSS/JS/JSON-Dateien wurden kürzlich vom Recht <code>editinterface</code> getrennt. Falls du nicht verstehst, warum du diesen Fehler erhältst, siehe [[mw:MediaWiki_1.32/interface-admin]].",
"logentry-pagelang-pagelang": "$1 {{GENDER:$2|promijenio|promijenila}} je jezik stranice $3 iz $4 u $5.",
"mediastatistics": "Statistika datoteka",
"mediastatistics-summary": "Slijede statistike postavljenih datoteka koje pokazuju zadnju inačicu datoteke. Starije ili izbrisane inačice nisu prikazane.",
- "mediastatistics-nfiles": "$1 ($2%)",
+ "mediastatistics-nfiles": "$1 ($2 %)",
"mediastatistics-nbytes": "{{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3 %)",
"mediastatistics-bytespertype": "Ukupna veličina datoteka za ovaj odlomak: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3%).",
"mediastatistics-allbytes": "Ukupna veličina svih datoteka: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2).",
"specialmute-success": "Vaše postavke utišavanja su uspješno ažurirane. Vidite sve utišane korisnike ovdje: [[Special:Preferences]].",
"specialmute-submit": "Potvrdi",
"specialmute-error-invalid-user": "Korisničko ime koje ste tražili nije moguće pronaći.",
- "specialmute-error-email-preferences": "Morate potvrditi svoju email adresu prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
- "specialmute-login-required": "Molimo Vas prijavite se da biste promijenili postavke.",
+ "specialmute-error-email-preferences": "Morate potvrditi svoju adresu e-pošte prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
+ "specialmute-login-required": "Molimo Vas, prijavite se da biste promijenili postavke.",
"gotointerwiki": "Napuštate projekt {{SITENAME}}",
"gotointerwiki-invalid": "Navedeni naslov nije valjan.",
"gotointerwiki-external": "Napuštate projekt {{SITENAME}} da biste posjetili zasebno mrežno mjesto [[$2]].\n\n<strong>[$1 Nastavljate na $1]</strong>",
"history": "Riwayat halaman",
"history_short": "Versi terdahulu",
"history_small": "riwayat",
- "updatedmarker": "diubah sejak kunjungan terakhir saya",
+ "updatedmarker": "berubah sejak kunjungan terakhir saya",
"printableversion": "Versi cetak",
"permalink": "Pranala permanen",
"print": "Cetak",
"autoblockedtext": "Alamat IP Anda telah terblokir secara otomatis karena digunakan oleh pengguna lain, yang diblokir oleh $1. Pemblokiran dilakukan dengan alasan:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAnda dapat menghubungi $1 atau [[{{MediaWiki:Grouppage-sysop}}|pengurus]] lainnya untuk membicarakan pemblokiran ini.\n\nAnda tidak dapat menggunakan fitur \"{{int:emailuser}}\" kecuali Anda telah memasukkan alamat surel yang sah di [[Special:Preferences|preferensi akun]] Anda dan Anda tidak diblokir untuk menggunakannya.\n\nAlamat IP Anda saat ini adalah $3, dan ID pemblokiran adalah #$5.\nTolong sertakan informasi-informasi ini dalam setiap pertanyaan Anda.",
"systemblockedtext": "Nama pengguna atau alamat IP Anda telah diblokir secara otomatis oleh MediaWiki.\nAlasan yang diberikan adalah:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAlamat IP Anda saat ini adalah $3\nMohon sertakan semua perincian di atas dalam setiap pertanyaan yang Anda ajukan.",
"blockednoreason": "tidak ada alasan yang diberikan",
+ "blockedtext-composite-reason": "Ada pemblokiran berganda terhadap akun Anda dan/atau alamat IP Anda.",
"whitelistedittext": "Anda harus $1 untuk dapat menyunting halaman.",
"confirmedittext": "Anda harus mengkonfirmasikan dulu alamat surel Anda sebelum menyunting halaman.\nHarap masukkan dan validasikan alamat surel Anda melalui [[Special:Preferences|halaman preferensi pengguna]] Anda.",
"nosuchsectiontitle": "Bagian tidak ditemukan",
"mw-widgets-abandonedit-discard": "Buang suntingan",
"mw-widgets-abandonedit-keep": "Lanjutkan penyuntingan",
"mw-widgets-abandonedit-title": "Apakah Anda yakin?",
+ "mw-widgets-copytextlayout-copy": "Salin",
+ "mw-widgets-copytextlayout-copy-fail": "Gagal menyalin ke papan klip.",
+ "mw-widgets-copytextlayout-copy-success": "Salin ke papan klip.",
"mw-widgets-dateinput-no-date": "Tanggal tidak ada yang terpilih",
"mw-widgets-dateinput-placeholder-day": "TTTT-BB-HH",
"mw-widgets-dateinput-placeholder-month": "TTTT-BB",
"restrictionsfield-help": "Satu alamat IP atau rentang CIDR per baris. Untuk mengaktifkan semuanya, gunakan:\n<pre>0.0.0.0/0\n::/0</pre>",
"edit-error-short": "Galat: $1",
"edit-error-long": "Galat:\n\n$1",
+ "specialmute": "Diam",
+ "specialmute-submit": "Konfirmasi",
"revid": "revisi $1",
"pageid": "ID halaman $1",
"rawhtml-notallowed": "Tag <html> tidak dapat digunakan di luar halaman normal.",
"log-action-filter-suppress-block": "Сокрытие пользователя через блокировки",
"log-action-filter-suppress-reblock": "Сокрытие пользователя через повторное блокирование",
"log-action-filter-upload-upload": "Новая загрузка",
- "log-action-filter-upload-overwrite": "Ð\9fовÑ\82оÑ\80но загÑ\80Ñ\83зиÑ\82Ñ\8c",
- "log-action-filter-upload-revert": "Ð\9eÑ\82каÑ\82иÑ\82Ñ\8c",
+ "log-action-filter-upload-overwrite": "Ð\9fеÑ\80езапиÑ\81Ñ\8c Ñ\84айла",
+ "log-action-filter-upload-revert": "Ð\92озвÑ\80аÑ\82 Ñ\81Ñ\82аÑ\80ой веÑ\80Ñ\81ии Ñ\84айла",
"authmanager-authn-not-in-progress": "Проверка подлинности не выполняется или данные сессии были утеряны. Пожалуйста, начните снова с самого начала.",
"authmanager-authn-no-primary": "Предоставленные учётные данные не могут быть проверены на подлинность.",
"authmanager-authn-no-local-user": "Предоставленные учётные данные не связаны ни с одним участником этой вики.",
"revertmerge": "растави",
"mergelogpagetext": "Испод се налази списак најновијих обједињавања историја једне странице у другу.",
"history-title": "Историја измена странице „$1”",
- "difference-title": "Разлика између измена на страници „$1”",
+ "difference-title": "$1 — разлика између измена",
"difference-title-multipage": "Разлика између страница „$1“ и „$2“",
"difference-multipage": "(разлике између страница)",
"lineno": "Ред $1:",
"svg-long-desc": "SVG датотека, номинално $1 × $2 пиксела, величина: $3",
"svg-long-desc-animated": "Анимирана SVG датотека, номинално: $1 × $2 пиксела, величина: $3",
"svg-long-error": "Неважећа SVG датотека: $1",
- "show-big-image": "Ð\9fÑ\80вобиÑ\82на датотека",
+ "show-big-image": "Ð\9eÑ\80игинална датотека",
"show-big-image-preview": "Величина овог приказа: $1.",
"show-big-image-preview-differ": "Величина $3 прегледа за ову $2 датотеку је $1.",
"show-big-image-other": "$2 {{PLURAL:$2|друга резолуција|друге резолуције|других резолуција}}: $1.",
public function execute() {
global $wgMainCacheType, $wgMemCachedTimeout, $wgObjectCaches;
+ $memcachedTypes = [ CACHE_MEMCACHED, 'memcached-php', 'memcached-pecl' ];
+
$cache = $this->getOption( 'cache' );
$iterations = $this->getOption( 'i', 100 );
if ( $cache ) {
$servers = $wgObjectCaches[$cache]['servers'];
} elseif ( $this->hasArg( 0 ) ) {
$servers = [ $this->getArg( 0 ) ];
- } elseif ( $wgMainCacheType === CACHE_MEMCACHED ) {
+ } elseif ( in_array( $wgMainCacheType, $memcachedTypes, true ) ) {
global $wgMemCachedServers;
$servers = $wgMemCachedServers;
} elseif ( isset( $wgObjectCaches[$wgMainCacheType]['servers'] ) ) {
/**
* Rebuild pass 1: Insert `recentchanges` entries for page revisions.
+ *
+ * @param ILBFactory $lbFactory
*/
private function rebuildRecentChangesTablePass1( ILBFactory $lbFactory ) {
$dbw = $this->getDB( DB_MASTER );
/**
* Rebuild pass 2: Enhance entries for page revisions with references to the previous revision
* (rc_last_oldid, rc_new etc.) and size differences (rc_old_len, rc_new_len).
+ *
+ * @param ILBFactory $lbFactory
*/
private function rebuildRecentChangesTablePass2( ILBFactory $lbFactory ) {
$dbw = $this->getDB( DB_MASTER );
$lastOldId = 0;
$lastSize = null;
$updated = 0;
- foreach ( $res as $obj ) {
+ foreach ( $res as $row ) {
$new = 0;
- if ( $obj->rc_cur_id != $lastCurId ) {
+ if ( $row->rc_cur_id != $lastCurId ) {
# Switch! Look up the previous last edit, if any
- $lastCurId = intval( $obj->rc_cur_id );
- $emit = $obj->rc_timestamp;
+ $lastCurId = intval( $row->rc_cur_id );
+ $emit = $row->rc_timestamp;
- $row = $dbw->selectRow(
+ $revRow = $dbw->selectRow(
'revision',
[ 'rev_id', 'rev_len' ],
[ 'rev_page' => $lastCurId, "rev_timestamp < " . $dbw->addQuotes( $emit ) ],
__METHOD__,
[ 'ORDER BY' => 'rev_timestamp DESC' ]
);
- if ( $row ) {
- $lastOldId = intval( $row->rev_id );
+ if ( $revRow ) {
+ $lastOldId = intval( $revRow->rev_id );
# Grab the last text size if available
- $lastSize = !is_null( $row->rev_len ) ? intval( $row->rev_len ) : null;
+ $lastSize = !is_null( $revRow->rev_len ) ? intval( $revRow->rev_len ) : null;
} else {
# No previous edit
$lastOldId = 0;
$size = (int)$dbw->selectField(
'revision',
'rev_len',
- [ 'rev_id' => $obj->rc_this_oldid ],
+ [ 'rev_id' => $row->rc_this_oldid ],
__METHOD__
);
],
[
'rc_cur_id' => $lastCurId,
- 'rc_this_oldid' => $obj->rc_this_oldid,
- 'rc_timestamp' => $obj->rc_timestamp // index usage
+ 'rc_this_oldid' => $row->rc_this_oldid,
+ 'rc_timestamp' => $row->rc_timestamp // index usage
],
__METHOD__
);
- $lastOldId = intval( $obj->rc_this_oldid );
+ $lastOldId = intval( $row->rc_this_oldid );
$lastSize = $size;
if ( ( ++$updated % $this->getBatchSize() ) == 0 ) {
/**
* Rebuild pass 3: Insert `recentchanges` entries for action logs.
+ *
+ * @param ILBFactory $lbFactory
*/
private function rebuildRecentChangesTablePass3( ILBFactory $lbFactory ) {
global $wgLogRestrictions, $wgFilterLogTypes;
/**
* Rebuild pass 4: Mark bot and autopatrolled entries.
+ *
+ * @param ILBFactory $lbFactory
*/
private function rebuildRecentChangesTablePass4( ILBFactory $lbFactory ) {
global $wgUseRCPatrol, $wgMiserMode;
);
$botusers = [];
- foreach ( $res as $obj ) {
- $botusers[] = User::newFromRow( $obj );
+ foreach ( $res as $row ) {
+ $botusers[] = User::newFromRow( $row );
}
# Fill in the rc_bot field
[ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
);
- foreach ( $res as $obj ) {
- $patrolusers[] = User::newFromRow( $obj );
+ foreach ( $res as $row ) {
+ $patrolusers[] = User::newFromRow( $row );
}
# Fill in the rc_patrolled field
}
/**
- * Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log entry
- * for a single action (upload only, at the moment, but potentially also move, protect, ...).
+ * Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log
+ * entry for a single action (upload only, at the moment, but potentially move, protect, ...).
+ *
+ * @param ILBFactory $lbFactory
*/
private function rebuildRecentChangesTablePass5( ILBFactory $lbFactory ) {
$dbw = wfGetDB( DB_MASTER );
);
$updates = 0;
- foreach ( $res as $obj ) {
- $rev_id = $obj->ls_value;
- $log_id = $obj->ls_log_id;
+ foreach ( $res as $row ) {
+ $rev_id = $row->ls_value;
+ $log_id = $row->ls_log_id;
// Mark the logging row as having an associated rev id
$dbw->update(
$hashes = [];
$maxSize = 0;
- foreach ( $res as $boRow ) {
- $extDB = $this->getDB( $boRow->bo_cluster );
+ foreach ( $res as $row ) {
+ $extDB = $this->getDB( $row->bo_cluster );
$blobRow = $extDB->selectRow(
'blobs',
'*',
- [ 'blob_id' => $boRow->bo_blob_id ],
+ [ 'blob_id' => $row->bo_blob_id ],
__METHOD__
);
];
}
+ /**
+ * @throws FatalError
+ * @throws MWException
+ * @suppress PhanPluginDuplicateConditionalNullCoalescing
+ */
public function validateParamsAndArgs() {
// Allow extensions to add additional params.
$params = [];
Hooks::run( 'MaintenanceUpdateAddParams', [ &$params ] );
+
+ // This executes before the PHP version check, so don't use null coalesce (??).
+ // Keeping this compatible with older PHP versions lets us reach the code that
+ // displays a more helpful error.
foreach ( $params as $name => $param ) {
$this->addOption(
$name,
$param['desc'],
- $param['require'] ?? false,
- $param['withArg'] ?? false,
- $param['shortName'] ?? false,
- $param['multiOccurrence'] ?? false
+ isset( $param['require'] ) ? $param['require'] : false,
+ isset( $param['withArg'] ) ? $param['withArg'] : false,
+ isset( $param['shortName'] ) ? $param['shortName'] : false,
+ isset( $param['multiOccurrence'] ) ? $param['multiOccurrence'] : false
);
}
</exclude>
</groups>
<filter>
- <whitelist addUncoveredFilesFromWhitelist="true">
+ <whitelist addUncoveredFilesFromWhitelist="false">
<directory suffix=".php">includes</directory>
<directory suffix=".php">languages</directory>
<directory suffix=".php">maintenance</directory>
+ <directory suffix=".php">extensions</directory>
+ <directory suffix=".php">skins</directory>
<exclude>
<directory suffix=".php">languages/messages</directory>
<file>languages/data/normalize-ar.php</file>
$job = new RefreshLinksJob( $page->getTitle(), [ 'parseThreshold' => 0 ] );
$job->run();
- // assert state
- $options = ParserOptions::newCanonical( 'canonical' );
- $out = $parserCache->get( $page, $options );
- $this->assertNotFalse( $out, 'parser cache entry' );
-
- $text = $out->getText();
- $this->assertContains( 'MAIN', $text );
- $this->assertContains( 'AUX', $text );
-
$this->assertSelect(
'pagelinks',
'pl_title',
);
}
+ public function testRunForMultiPage() {
+ MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+ 'aux',
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $fname = __METHOD__;
+
+ $mainContent = new WikitextContent( 'MAIN [[Kittens]]' );
+ $auxContent = new WikitextContent( 'AUX [[Category:Goats]]' );
+ $page1 = $this->createPage( "$fname-1", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+ $mainContent = new WikitextContent( 'MAIN [[Dogs]]' );
+ $auxContent = new WikitextContent( 'AUX [[Category:Hamsters]]' );
+ $page2 = $this->createPage( "$fname-2", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+ // clear state
+ $parserCache = MediaWikiServices::getInstance()->getParserCache();
+ $parserCache->deleteOptionsKey( $page1 );
+ $parserCache->deleteOptionsKey( $page2 );
+
+ $this->db->delete( 'pagelinks', '*', __METHOD__ );
+ $this->db->delete( 'categorylinks', '*', __METHOD__ );
+
+ // run job
+ $job = new RefreshLinksJob(
+ Title::newMainPage(),
+ [ 'pages' => [ [ 0, "$fname-1" ], [ 0, "$fname-2" ] ] ]
+ );
+ $job->run();
+
+ $this->assertSelect(
+ 'pagelinks',
+ 'pl_title',
+ [ 'pl_from' => $page1->getId() ],
+ [ [ 'Kittens' ] ]
+ );
+ $this->assertSelect(
+ 'categorylinks',
+ 'cl_to',
+ [ 'cl_from' => $page1->getId() ],
+ [ [ 'Goats' ] ]
+ );
+ $this->assertSelect(
+ 'pagelinks',
+ 'pl_title',
+ [ 'pl_from' => $page2->getId() ],
+ [ [ 'Dogs' ] ]
+ );
+ $this->assertSelect(
+ 'categorylinks',
+ 'cl_to',
+ [ 'cl_from' => $page2->getId() ],
+ [ [ 'Hamsters' ] ]
+ );
+ }
}
);
}
+ /**
+ * @covers MapCacheLRU::has()
+ * @covers MapCacheLRU::get()
+ * @covers MapCacheLRU::set()
+ */
+ function testMissing() {
+ $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+ $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+ $this->assertFalse( $cache->has( 'd' ) );
+ $this->assertNull( $cache->get( 'd' ) );
+ $this->assertNull( $cache->get( 'd', 0.0, null ) );
+ $this->assertFalse( $cache->get( 'd', 0.0, false ) );
+ }
+
/**
* @covers MapCacheLRU::has()
* @covers MapCacheLRU::get()
--- /dev/null
+<?php
+
+/**
+ * Holds tests for FakeResultWrapper MediaWiki class.
+ *
+ * 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
+ */
+
+use Wikimedia\Rdbms\FakeResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\FakeResultWrapper
+ */
+class FakeResultWrapperTest extends PHPUnit\Framework\TestCase {
+ public function testIteration() {
+ $res = new FakeResultWrapper( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ (object)[ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ (object)[ 'colA' => 6, 'colB' => 'f' ],
+ (object)[ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Holds tests for ResultWrapper MediaWiki class.
+ *
+ * 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
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\ResultWrapper
+ */
+class ResultWrapperTest extends PHPUnit\Framework\TestCase {
+ /**
+ * @return IDatabase
+ * @param array[] $rows
+ */
+ private function getDatabaseMock( array $rows ) {
+ $db = $this->getMockBuilder( IDatabase::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->method( 'select' )->willReturnCallback(
+ function () use ( $db, $rows ) {
+ return new ResultWrapper( $db, $rows );
+ }
+ );
+ $db->method( 'dataSeek' )->willReturnCallback(
+ function ( ResultWrapper $res, $pos ) use ( $db ) {
+ // Position already set in ResultWrapper
+ }
+ );
+ $db->method( 'fetchRow' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row;
+ }
+ );
+ $db->method( 'fetchObject' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row ? (object)$row : false;
+ }
+ );
+ $db->method( 'numRows' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ return count( $res::unwrap( $res ) );
+ }
+ );
+
+ return $db;
+ }
+
+ public function testIteration() {
+ $db = $this->getDatabaseMock( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ [ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ [ 'colA' => 6, 'colB' => 'f' ],
+ [ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $res = $db->select( 'faketable', [ 'colA', 'colB' ], '1 = 1', __METHOD__ );
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}
+++ /dev/null
-<?php
-
-/**
- * @covers LanguageCode
- * @group Language
- *
- * @author Thiemo Kreuz
- */
-class LanguageCodeTest extends PHPUnit\Framework\TestCase {
-
- use MediaWikiCoversValidator;
-
- public function testConstructor() {
- $instance = new LanguageCode();
-
- $this->assertInstanceOf( LanguageCode::class, $instance );
- }
-
- public function testGetDeprecatedCodeMapping() {
- $map = LanguageCode::getDeprecatedCodeMapping();
-
- $this->assertInternalType( 'array', $map );
- $this->assertContainsOnly( 'string', array_keys( $map ) );
- $this->assertArrayNotHasKey( '', $map );
- $this->assertContainsOnly( 'string', $map );
- $this->assertNotContains( '', $map );
-
- // Codes special to MediaWiki should never appear in a map of "deprecated" codes
- $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
- $this->assertNotContains( 'qqq', $map, 'documentation' );
- $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
- $this->assertNotContains( 'qqx', $map, 'debug code' );
-
- // Valid language codes that are currently not "deprecated"
- $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
- $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
- $this->assertArrayNotHasKey( 'simple', $map );
- }
-
- public function testReplaceDeprecatedCodes() {
- $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
- $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
- $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
- }
-
- /**
- * test @see LanguageCode::bcp47().
- * Please note the BCP 47 explicitly state that language codes are case
- * insensitive, there are some exceptions to the rule :)
- * This test is used to verify our formatting against all lower and
- * all upper cases language code.
- *
- * @see https://tools.ietf.org/html/bcp47
- * @dataProvider provideLanguageCodes()
- */
- public function testBcp47( $code, $expected ) {
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to '$code'"
- );
-
- $code = strtolower( $code );
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to lower case '$code'"
- );
-
- $code = strtoupper( $code );
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to upper case '$code'"
- );
- }
-
- /**
- * Array format is ($code, $expected)
- */
- public static function provideLanguageCodes() {
- return [
- // Extracted from BCP 47 (list not exhaustive)
- # 2.1.1
- [ 'en-ca-x-ca', 'en-CA-x-ca' ],
- [ 'sgn-be-fr', 'sgn-BE-FR' ],
- [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
- # 2.2
- [ 'sr-Latn-RS', 'sr-Latn-RS' ],
- [ 'az-arab-ir', 'az-Arab-IR' ],
-
- # 2.2.5
- [ 'sl-nedis', 'sl-nedis' ],
- [ 'de-ch-1996', 'de-CH-1996' ],
-
- # 2.2.6
- [
- 'en-latn-gb-boont-r-extended-sequence-x-private',
- 'en-Latn-GB-boont-r-extended-sequence-x-private'
- ],
-
- // Examples from BCP 47 Appendix A
- # Simple language subtag:
- [ 'DE', 'de' ],
- [ 'fR', 'fr' ],
- [ 'ja', 'ja' ],
-
- # Language subtag plus script subtag:
- [ 'zh-hans', 'zh-Hans' ],
- [ 'sr-cyrl', 'sr-Cyrl' ],
- [ 'sr-latn', 'sr-Latn' ],
-
- # Extended language subtags and their primary language subtag
- # counterparts:
- [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
- [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
- [ 'zh-yue-hk', 'zh-yue-HK' ],
- [ 'yue-hk', 'yue-HK' ],
-
- # Language-Script-Region:
- [ 'zh-hans-cn', 'zh-Hans-CN' ],
- [ 'sr-latn-RS', 'sr-Latn-RS' ],
-
- # Language-Variant:
- [ 'sl-rozaj', 'sl-rozaj' ],
- [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
- [ 'sl-nedis', 'sl-nedis' ],
-
- # Language-Region-Variant:
- [ 'de-ch-1901', 'de-CH-1901' ],
- [ 'sl-it-nedis', 'sl-IT-nedis' ],
-
- # Language-Script-Region-Variant:
- [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
-
- # Language-Region:
- [ 'de-de', 'de-DE' ],
- [ 'en-us', 'en-US' ],
- [ 'es-419', 'es-419' ],
-
- # Private use subtags:
- [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
- [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
- /**
- * Previous test does not reflect the BCP 47 which states:
- * az-Arab-x-AZE-derbend
- * AZE being private, it should be lower case, hence the test above
- * should probably be:
- * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
- */
-
- # Private use registry values:
- [ 'x-whatever', 'x-whatever' ],
- [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
- [ 'de-qaaa', 'de-Qaaa' ],
- [ 'sr-latn-qm', 'sr-Latn-QM' ],
- [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
-
- # Tags that use extensions
- [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
- [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
- [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
-
- # Invalid:
- // de-419-DE
- // a-DE
- // ar-a-aaa-b-bbb-a-ccc
-
- # Non-standard and deprecated language codes used by MediaWiki
- [ 'als', 'gsw' ],
- [ 'bat-smg', 'sgs' ],
- [ 'be-x-old', 'be-tarask' ],
- [ 'fiu-vro', 'vro' ],
- [ 'roa-rup', 'rup' ],
- [ 'zh-classical', 'lzh' ],
- [ 'zh-min-nan', 'nan' ],
- [ 'zh-yue', 'yue' ],
- [ 'cbk-zam', 'cbk' ],
- [ 'de-formal', 'de-x-formal' ],
- [ 'eml', 'egl' ],
- [ 'en-rtl', 'en-x-rtl' ],
- [ 'es-formal', 'es-x-formal' ],
- [ 'hu-formal', 'hu-x-formal' ],
- [ 'kk-Arab', 'kk-Arab' ],
- [ 'kk-Cyrl', 'kk-Cyrl' ],
- [ 'kk-Latn', 'kk-Latn' ],
- [ 'map-bms', 'jv-x-bms' ],
- [ 'mo', 'ro-Cyrl-MD' ],
- [ 'nrm', 'nrf' ],
- [ 'nl-informal', 'nl-x-informal' ],
- [ 'roa-tara', 'nap-x-tara' ],
- [ 'simple', 'en-simple' ],
- [ 'sr-ec', 'sr-Cyrl' ],
- [ 'sr-el', 'sr-Latn' ],
- [ 'zh-cn', 'zh-Hans-CN' ],
- [ 'zh-sg', 'zh-Hans-SG' ],
- [ 'zh-my', 'zh-Hans-MY' ],
- [ 'zh-tw', 'zh-Hant-TW' ],
- [ 'zh-hk', 'zh-Hant-HK' ],
- [ 'zh-mo', 'zh-Hant-MO' ],
- [ 'zh-hans', 'zh-Hans' ],
- [ 'zh-hant', 'zh-Hant' ],
- ];
- }
-
-}
--- /dev/null
+<?php
+
+/**
+ * @covers LanguageCode
+ * @group Language
+ *
+ * @author Thiemo Kreuz
+ */
+class LanguageCodeTest extends MediaWikiUnitTestCase {
+
+ public function testConstructor() {
+ $instance = new LanguageCode();
+
+ $this->assertInstanceOf( LanguageCode::class, $instance );
+ }
+
+ public function testGetDeprecatedCodeMapping() {
+ $map = LanguageCode::getDeprecatedCodeMapping();
+
+ $this->assertInternalType( 'array', $map );
+ $this->assertContainsOnly( 'string', array_keys( $map ) );
+ $this->assertArrayNotHasKey( '', $map );
+ $this->assertContainsOnly( 'string', $map );
+ $this->assertNotContains( '', $map );
+
+ // Codes special to MediaWiki should never appear in a map of "deprecated" codes
+ $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
+ $this->assertNotContains( 'qqq', $map, 'documentation' );
+ $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
+ $this->assertNotContains( 'qqx', $map, 'debug code' );
+
+ // Valid language codes that are currently not "deprecated"
+ $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
+ $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
+ $this->assertArrayNotHasKey( 'simple', $map );
+ }
+
+ public function testReplaceDeprecatedCodes() {
+ $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
+ $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
+ $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
+ }
+
+ /**
+ * test @see LanguageCode::bcp47().
+ * Please note the BCP 47 explicitly state that language codes are case
+ * insensitive, there are some exceptions to the rule :)
+ * This test is used to verify our formatting against all lower and
+ * all upper cases language code.
+ *
+ * @see https://tools.ietf.org/html/bcp47
+ * @dataProvider provideLanguageCodes()
+ */
+ public function testBcp47( $code, $expected ) {
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to '$code'"
+ );
+
+ $code = strtolower( $code );
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to lower case '$code'"
+ );
+
+ $code = strtoupper( $code );
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to upper case '$code'"
+ );
+ }
+
+ /**
+ * Array format is ($code, $expected)
+ */
+ public static function provideLanguageCodes() {
+ return [
+ // Extracted from BCP 47 (list not exhaustive)
+ # 2.1.1
+ [ 'en-ca-x-ca', 'en-CA-x-ca' ],
+ [ 'sgn-be-fr', 'sgn-BE-FR' ],
+ [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
+ # 2.2
+ [ 'sr-Latn-RS', 'sr-Latn-RS' ],
+ [ 'az-arab-ir', 'az-Arab-IR' ],
+
+ # 2.2.5
+ [ 'sl-nedis', 'sl-nedis' ],
+ [ 'de-ch-1996', 'de-CH-1996' ],
+
+ # 2.2.6
+ [
+ 'en-latn-gb-boont-r-extended-sequence-x-private',
+ 'en-Latn-GB-boont-r-extended-sequence-x-private'
+ ],
+
+ // Examples from BCP 47 Appendix A
+ # Simple language subtag:
+ [ 'DE', 'de' ],
+ [ 'fR', 'fr' ],
+ [ 'ja', 'ja' ],
+
+ # Language subtag plus script subtag:
+ [ 'zh-hans', 'zh-Hans' ],
+ [ 'sr-cyrl', 'sr-Cyrl' ],
+ [ 'sr-latn', 'sr-Latn' ],
+
+ # Extended language subtags and their primary language subtag
+ # counterparts:
+ [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
+ [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
+ [ 'zh-yue-hk', 'zh-yue-HK' ],
+ [ 'yue-hk', 'yue-HK' ],
+
+ # Language-Script-Region:
+ [ 'zh-hans-cn', 'zh-Hans-CN' ],
+ [ 'sr-latn-RS', 'sr-Latn-RS' ],
+
+ # Language-Variant:
+ [ 'sl-rozaj', 'sl-rozaj' ],
+ [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
+ [ 'sl-nedis', 'sl-nedis' ],
+
+ # Language-Region-Variant:
+ [ 'de-ch-1901', 'de-CH-1901' ],
+ [ 'sl-it-nedis', 'sl-IT-nedis' ],
+
+ # Language-Script-Region-Variant:
+ [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
+
+ # Language-Region:
+ [ 'de-de', 'de-DE' ],
+ [ 'en-us', 'en-US' ],
+ [ 'es-419', 'es-419' ],
+
+ # Private use subtags:
+ [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
+ [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
+ /**
+ * Previous test does not reflect the BCP 47 which states:
+ * az-Arab-x-AZE-derbend
+ * AZE being private, it should be lower case, hence the test above
+ * should probably be:
+ * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
+ */
+
+ # Private use registry values:
+ [ 'x-whatever', 'x-whatever' ],
+ [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
+ [ 'de-qaaa', 'de-Qaaa' ],
+ [ 'sr-latn-qm', 'sr-Latn-QM' ],
+ [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
+
+ # Tags that use extensions
+ [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
+ [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
+ [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
+
+ # Invalid:
+ // de-419-DE
+ // a-DE
+ // ar-a-aaa-b-bbb-a-ccc
+
+ # Non-standard and deprecated language codes used by MediaWiki
+ [ 'als', 'gsw' ],
+ [ 'bat-smg', 'sgs' ],
+ [ 'be-x-old', 'be-tarask' ],
+ [ 'fiu-vro', 'vro' ],
+ [ 'roa-rup', 'rup' ],
+ [ 'zh-classical', 'lzh' ],
+ [ 'zh-min-nan', 'nan' ],
+ [ 'zh-yue', 'yue' ],
+ [ 'cbk-zam', 'cbk' ],
+ [ 'de-formal', 'de-x-formal' ],
+ [ 'eml', 'egl' ],
+ [ 'en-rtl', 'en-x-rtl' ],
+ [ 'es-formal', 'es-x-formal' ],
+ [ 'hu-formal', 'hu-x-formal' ],
+ [ 'kk-Arab', 'kk-Arab' ],
+ [ 'kk-Cyrl', 'kk-Cyrl' ],
+ [ 'kk-Latn', 'kk-Latn' ],
+ [ 'map-bms', 'jv-x-bms' ],
+ [ 'mo', 'ro-Cyrl-MD' ],
+ [ 'nrm', 'nrf' ],
+ [ 'nl-informal', 'nl-x-informal' ],
+ [ 'roa-tara', 'nap-x-tara' ],
+ [ 'simple', 'en-simple' ],
+ [ 'sr-ec', 'sr-Cyrl' ],
+ [ 'sr-el', 'sr-Latn' ],
+ [ 'zh-cn', 'zh-Hans-CN' ],
+ [ 'zh-sg', 'zh-Hans-SG' ],
+ [ 'zh-my', 'zh-Hans-MY' ],
+ [ 'zh-tw', 'zh-Hant-TW' ],
+ [ 'zh-hk', 'zh-Hant-HK' ],
+ [ 'zh-mo', 'zh-Hant-MO' ],
+ [ 'zh-hans', 'zh-Hans' ],
+ [ 'zh-hant', 'zh-Hant' ],
+ ];
+ }
+
+}