Adam Roses Wight <awight@wikimedia.org>
Adam Roses Wight <awight@wikimedia.org> <spam@ludd.net>
addshore <addshorewiki@gmail.com>
+addshore <addshorewiki@gmail.com> <adamshorland@gmail.com>
Aditya Sastry <ganeshaditya1@gmail.com>
Adrian Heine <adrian.heine@wikimedia.de>
Alex Z. <mrzmanwiki@gmail.com> <mrzman@users.mediawiki.org>
Daniel Friesen <mediawiki@danielfriesen.name> <daniel@nadir-seen-fire.com>
Daniel Friesen <mediawiki@danielfriesen.name> <dantman@users.mediawiki.org>
Daniel Friesen <mediawiki@danielfriesen.name> <pub-github@nadir-seen-fire.com>
-Daniel Kinzler <daniel.kinzler@wikimedia.de>
-Daniel Kinzler <daniel.kinzler@wikimedia.de> <daniel@users.mediawiki.org>
+Daniel Kinzler <dkinzler@wikimedia.org>
+Daniel Kinzler <dkinzler@wikimedia.org> <daniel.kinzler@wikimedia.de>
+Daniel Kinzler <dkinzler@wikimedia.org> <daniel@users.mediawiki.org>
Daniel Renfro <bluecurio@gmail.com> <drenfro@vistaprint.com>
Danny B. <Wikipedia.Danny.B@email.cz>
Danny B. <Wikipedia.Danny.B@email.cz> <danny.b@email.cz>
Darkdragon09 <ubuntu@ip-172-31-39-38.us-west-2.compute.internal>
David Causse <dcausse@wikimedia.org>
David Chan <david@sheetmusic.org.uk>
+Dayllan Maza <dmaza@wikimedia.org>
+Dayllan Maza <dmaza@wikimedia.org> <dayllan.maza@gmail.com>
Dereckson <dereckson@espace-win.org>
Derk-Jan Hartman <hartman@videolan.org>
Derk-Jan Hartman <hartman@videolan.org> <hartman.wiki@gmail.com>
Katie Filbert <aude.wiki@gmail.com>
Katie Filbert <aude.wiki@gmail.com> <aude@users.mediawiki.org>
Kevin Israel <pleasestand@live.com>
+Kosta Harlan <kharlan@wikimedia.org>
+Kosta Harlan <kharlan@wikimedia.org> <kosta@fastmail.com>
Kunal Grover <kunalgrover05@gmail.com>
Kunal Mehta <legoktm@member.fsf.org>
Kunal Mehta <legoktm@member.fsf.org> <legoktm.wikipedia@gmail.com>
Patrick Reilly <preilly@wikimedia.org> <preilly@users.mediawiki.org>
Patrick Westerhoff <PatrickWesterhoff@gmail.com>
Paul Copperman <paul.copperman@gmail.com> <pcopp@users.mediawiki.org>
+Petar Petković <ppetkovic@wikimedia.org>
Peter Coombe <pcoombe@wikimedia.org>
Peter Coti <petercoti@gmail.com>
Peter Potrowl <peter017@gmail.com> <peter17@users.mediawiki.org>
* Another two OutputPage methods, setPageTitleActionText() and
getPageTitleActionText(), were removed. They did nothing since 1.15 (almost
ten years). Use setHTMLTitle() directly.
+* The return value of OutputPage::adaptCdnTTL() has been removed. The
+ value returned was misleading and probably not what any caller would
+ have wanted.
* All MagicWord static member variables have been removed. Use appropriate
hooks or MagicWordFactory methods instead.
* MagicWord::clearCache() has been removed. Instead, create a new
* 'uppercase-se' (NorthernSamiUppercaseCollation) - use 'uca-se' instead
* 'xx-uca-et' (CollationEt) - use 'uca-et' instead
* 'xx-uca-fa' (CollationFa) - use 'uca-fa' instead
+* LanguageCode::bcp47() now always returns a valid BCP 47 code. This means
+ that some MediaWiki-specific language codes, such as `simple`, are mapped
+ into valid BCP 47 codes (eg `en-simple`).
* The hooks 'SpecialRecentChangesFilters' & 'SpecialWatchlistFilters' deprecated
in 1.23 were removed. Instead, use 'ChangesListSpecialPageStructuredFilters'.
The ChangesListSpecialPage code for these legacy hooks, and their use in
removed. Use the 'ChangesListSpecialPageStructuredFilters' hook instead.
* DeferredUpdates::setImmediateMode(), deprecated since 1.29, has been removed.
* File / MediaHandler::getStreamHeaders(), deprecated since 1.30, was removed.
+* The hook 'DoEditSectionLink', deprecated since 1.25, has been removed. Use
+ the hook 'SkinEditSectionLinks' instead.
+* The hook 'UserGetImplicitGroups', deprecated since 1.25, has been removed.
+* The global function wfRunHooks, deprecated since 1.25, has now been removed.
+ Use Hooks::run().
+* The hook 'UnknownAction', deprecated since 1.19, has now been removed.
+* The hook 'ParserLimitReport', deprecated since 1.22, has been removed. Use
+ the hooks 'ParserLimitReportPrepare' and 'ParserLimitReportFormat' instead.
=== Deprecations in 1.32 ===
* HTMLForm::setSubmitProgressive() is deprecated. No need to call it. Submit
as a string. They should be given as a OOUI\FieldLayout object instead.
Notably, this affects fields defined in the 'GetPreferences' hook, because
Special:Preferences uses an OOUI form now. (If possible, don't use 'rawrow'.)
+* In Skin::doEditSectionLink omitting the parameters $tooltip and $lang is
+ deprecated. For the $lang parameter, types other than Language are
+ deprecated.
+* The $wgUseKeyHeader configuration option and the
+ OutputPage::getKeyHeader() method have been deprecated; the relevant
+ draft IETF spec expired without becoming a standard.
=== Other changes in 1.32 ===
* (T198811) The following tables have had their UNIQUE indexes turned into
&$article: article (object) being viewed
&$oldid: oldid (int) being viewed
-'DoEditSectionLink': DEPRECATED since 1.25! Use SkinEditSectionLinks instead.
-Override the HTML generated for section edit links
-$skin: Skin object rendering the UI
-$title: Title object for the title being linked to (may not be the same as
- the page title, if the section is included from a template)
-$section: The designation of the section being pointed to, to be included in
- the link, like "§ion=$section"
-$tooltip: The default tooltip. Escape before using.
- By default, this is wrapped in the 'editsectionhint' message.
-&$result: The HTML to return, prefilled with the default plus whatever other
- changes earlier hooks have made
-$lang: The language code to use for the link in the wfMessage function
-
'EditFilter': Perform checks on an edit
$editor: EditPage instance (object). The edit form (see includes/EditPage.php)
$text: Contents of the edit box
&$parser: Parser object
&$varCache: variable cache (array)
-'ParserLimitReport': DEPRECATED since 1.22! Use ParserLimitReportPrepare and
-ParserLimitReportFormat instead.
-Called at the end of Parser:parse() when the parser will
-include comments about size of the text parsed.
-$parser: Parser object
-&$limitReport: text that will be included (without comment tags)
-
'ParserLimitReportFormat': Called for each row in the parser limit report that
needs formatting. If nothing handles this hook, the default is to use "$key" to
get the label, and "$key-value" or "$key-value-text"/"$key-value-html" to
test case files matching the suffix "Test.php".
&$paths: list of test cases and directories to search.
-'UnknownAction': DEPRECATED since 1.19! To add an action in an extension,
-create a subclass of Action, and add a new key to $wgActions.
-An unknown "action" has occurred (useful for defining your own actions).
-$action: action name
-$article: article "acted on"
-
'UnwatchArticle': Before a watch is removed from an article.
&$user: user watching
&$page: WikiPage object to be removed
&$timestamp: timestamp, change this to override local email authentication
timestamp
-'UserGetImplicitGroups': DEPRECATED since 1.25!
-Called in User::getImplicitGroups().
-&$groups: List of implicit (automatically-assigned) groups
-
'UserGetLanguageObject': Called when getting user's interface language object.
$user: User object
&$code: Language code that will be used to create the object
/**
* Send the Key HTTP header for better caching.
- * See https://datatracker.ietf.org/doc/draft-fielding-http-key/ for details.
+ * See https://datatracker.ietf.org/doc/draft-ietf-httpbis-key/ for details.
* @since 1.27
+ * @deprecated in 1.32, the IETF spec expired without becoming a standard.
*/
$wgUseKeyHeader = false;
return ObjectCache::getInstance( $wgMessageCacheType );
}
-/**
- * Call hook functions defined in $wgHooks
- *
- * @param string $event Event name
- * @param array $args Parameters passed to hook functions
- * @param string|null $deprecatedVersion Optionally mark hook as deprecated with version number
- *
- * @return bool True if no handler aborted the hook
- * @deprecated since 1.25 - use Hooks::run
- */
-function wfRunHooks( $event, array $args = [], $deprecatedVersion = null ) {
- wfDeprecated( __METHOD__, '1.25' );
- return Hooks::run( $event, $args, $deprecatedVersion );
-}
-
/**
* Wrapper around php's unpack.
*
$action->show();
return;
}
- // NOTE: deprecated hook. Add to $wgActions instead
- if ( Hooks::run(
- 'UnknownAction',
- [
- $request->getVal( 'action', 'view' ),
- $page
- ],
- '1.19'
- ) ) {
- $output->setStatusCode( 404 );
- $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
- }
+
+ // If we've not found out which action it is by now, it's unknown
+ $output->setStatusCode( 404 );
+ $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
}
/**
*/
private $CSPNonce;
+ /**
+ * @var array A cache of the names of the cookies that will influence the cache
+ */
+ private static $cacheVaryCookies = null;
+
/**
* Constructor for OutputPage. This should not be called directly.
* Instead a new RequestContext should be created and it will implicitly create
/**
* Parse wikitext, strip paragraphs, and return the HTML.
*
+ * @todo This doesn't work as expected at all. If $interface is false, there's always a
+ * wrapping <div>, so stripOuterParagraph() does nothing.
+ *
* @param string $text
* @param bool $linestart Is this the start of a line?
* @param bool $interface Use interface language (instead of content language) while parsing
* @param string|int|float|bool|null $mtime Last-Modified timestamp
* @param int $minTTL Minimum TTL in seconds [default: 1 minute]
* @param int $maxTTL Maximum TTL in seconds [default: $wgSquidMaxage]
- * @return int TTL in seconds passed to lowerCdnMaxage() (may not be the same as the new
- * s-maxage)
* @since 1.28
*/
public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
return $minTTL; // entity does not exist
}
- $age = time() - wfTimestamp( TS_UNIX, $mtime );
+ $age = MWTimestamp::time() - wfTimestamp( TS_UNIX, $mtime );
$adaptiveTTL = max( 0.9 * $age, $minTTL );
$adaptiveTTL = min( $adaptiveTTL, $maxTTL );
$this->lowerCdnMaxage( (int)$adaptiveTTL );
-
- return $adaptiveTTL;
}
/**
* @return array
*/
function getCacheVaryCookies() {
- static $cookies;
- if ( $cookies === null ) {
+ if ( self::$cacheVaryCookies === null ) {
$config = $this->getConfig();
- $cookies = array_merge(
+ self::$cacheVaryCookies = array_values( array_unique( array_merge(
SessionManager::singleton()->getVaryCookies(),
[
'forceHTTPS',
],
$config->get( 'CacheVaryCookies' )
- );
- Hooks::run( 'GetCacheVaryCookies', [ $this, &$cookies ] );
+ ) ) );
+ Hooks::run( 'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
}
- return $cookies;
+ return self::$cacheVaryCookies;
}
/**
* Get a complete Key header
*
* @return string
+ * @deprecated in 1.32; the IETF spec for this header expired w/o becoming
+ * a standard.
*/
public function getKeyHeader() {
+ wfDeprecated( '$wgUseKeyHeader', '1.32' );
+
$cvCookies = $this->getCacheVaryCookies();
$cookiesOption = [];
continue;
}
+ // XXX Note that this code is not strictly correct: we
+ // do a case-insensitive match in
+ // LanguageConverter::getHeaderVariant() while the
+ // (abandoned, draft) spec for the `Key` header only
+ // allows case-sensitive matches. To match the logic
+ // in LanguageConverter::getHeaderVariant() we should
+ // also be looking at fallback variants and deprecated
+ // mediawiki-internal codes, as well as BCP 47
+ // normalized forms.
+
$aloption[] = "substr=$variant";
// IE and some other browsers use BCP 47 standards in their Accept-Language header,
use Revision;
use Title;
use User;
+use Content;
use Wikimedia\Assert\Assert;
/**
'Access to the content has been suppressed for this audience'
);
} else {
- $output = $content->getParserOutput(
- $this->title,
- $this->revision->getId(),
- $this->options,
- $withHtml
- );
+ $output = $this->getSlotParserOutputUncached( $content, $withHtml );
if ( $withHtml && !$output->hasText() ) {
throw new LogicException(
return $this->slotsOutput[$role];
}
+ /**
+ * @note This method exist to make duplicate parses easier to see during profiling
+ * @param Content $content
+ * @param bool $withHtml
+ * @return ParserOutput
+ */
+ private function getSlotParserOutputUncached( Content $content, $withHtml ) {
+ return $content->getParserOutput(
+ $this->title,
+ $this->revision->getId(),
+ $this->options,
+ $withHtml
+ );
+ }
+
/**
* Updates the RevisionRecord after the revision has been saved. This can be used to discard
* and cached ParserOutput so parser functions like {{REVISIONTIMESTAMP}} or {{REVISIONID}}
exit( 1 );
}
+// Check to see if we are at the file scope
+$wgScopeTest = 'MediaWiki Setup.php scope test';
+if ( !isset( $GLOBALS['wgScopeTest'] ) || $GLOBALS['wgScopeTest'] !== $wgScopeTest ) {
+ echo "Error, Setup.php must be included from the file scope.\n";
+ die( 1 );
+}
+unset( $wgScopeTest );
+
/**
* Pre-config setup: Before loading LocalSettings.php
*/
// Don't let any other extensions load
ExtensionRegistry::getInstance()->finish();
-// Check to see if we are at the file scope
-if ( !isset( $wgVersion ) ) {
- echo "Error, Setup.php must be included from the file scope, after DefaultSettings.php\n";
- die( 1 );
-}
-
mb_internal_encoding( 'UTF-8' );
// Set the configured locale on all requests for consisteny
* are distinct from Special Pages because an action must apply to exactly one page.
*
* To add an action in an extension, create a subclass of Action, and add the key to
- * $wgActions. There is also the deprecated UnknownAction hook
+ * $wgActions.
*
* Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input
* format (protect, delete, move, etc), and the just-do-something format (watch, rollback,
$data = [];
foreach ( $langNames as $code => $name ) {
- $lang = [ 'code' => $code ];
+ $lang = [
+ 'code' => $code,
+ 'bcp47' => LanguageCode::bcp47( $code ),
+ ];
ApiResult::setContentValue( $lang, 'name', $name );
$data[] = $lang;
}
$cache = [];
- # Common conditions
- $conds = [
- 'page_is_redirect' => 0,
- 'page_namespace' => NS_MEDIAWIKI,
- ];
-
- $mostused = [];
+ $mostused = []; // list of "<cased message key>/<code>"
if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
if ( !$this->cache->has( $wgLanguageCode ) ) {
$this->load( $wgLanguageCode );
}
}
+ // Get the list of software-defined messages in core/extensions
+ $overridable = array_flip( Language::getMessageKeysFor( $wgLanguageCode ) );
+
+ // Common conditions
+ $conds = [
+ 'page_is_redirect' => 0,
+ 'page_namespace' => NS_MEDIAWIKI,
+ ];
if ( count( $mostused ) ) {
$conds['page_title'] = $mostused;
} elseif ( $code !== $wgLanguageCode ) {
$dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
}
- # Conditions to fetch oversized pages to ignore them
- $bigConds = $conds;
- $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
-
- # Load titles for all oversized pages in the MediaWiki namespace
+ // Set the stubs for oversized software-defined messages in the main cache map
$res = $dbr->select(
'page',
[ 'page_title', 'page_latest' ],
- $bigConds,
+ array_merge( $conds, [ 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
__METHOD__ . "($code)-big"
);
foreach ( $res as $row ) {
- $cache[$row->page_title] = '!TOO BIG';
+ $name = $this->contLang->lcfirst( $row->page_title );
+ // Include entries/stubs for all keys in $mostused in adaptive mode
+ if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+ $cache[$row->page_title] = '!TOO BIG';
+ }
// At least include revision ID so page changes are reflected in the hash
$cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
}
- # Conditions to load the remaining pages with their contents
- $smallConds = $conds;
- $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
-
+ // Set the text for small software-defined messages in the main cache map
$res = $dbr->select(
[ 'page', 'revision', 'text' ],
- [ 'page_title', 'old_id', 'old_text', 'old_flags' ],
- $smallConds,
+ [ 'page_title', 'page_latest', 'old_id', 'old_text', 'old_flags' ],
+ array_merge( $conds, [ 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
__METHOD__ . "($code)-small",
[],
[
'text' => [ 'JOIN', 'rev_text_id=old_id' ],
]
);
-
foreach ( $res as $row ) {
- $text = Revision::getRevisionText( $row );
- if ( $text === false ) {
- // Failed to fetch data; possible ES errors?
- // Store a marker to fetch on-demand as a workaround...
- // TODO Use a differnt marker
- $entry = '!TOO BIG';
- wfDebugLog(
- 'MessageCache',
- __METHOD__
- . ": failed to load message page text for {$row->page_title} ($code)"
- );
+ $name = $this->contLang->lcfirst( $row->page_title );
+ // Include entries/stubs for all keys in $mostused in adaptive mode
+ if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+ $text = Revision::getRevisionText( $row );
+ if ( $text === false ) {
+ // Failed to fetch data; possible ES errors?
+ // Store a marker to fetch on-demand as a workaround...
+ // TODO Use a differnt marker
+ $entry = '!TOO BIG';
+ wfDebugLog(
+ 'MessageCache',
+ __METHOD__
+ . ": failed to load message page text for {$row->page_title} ($code)"
+ );
+ } else {
+ $entry = ' ' . $text;
+ }
+ $cache[$row->page_title] = $entry;
} else {
- $entry = ' ' . $text;
+ // T193271: cache object gets too big and slow to generate.
+ // At least include revision ID so page changes are reflected in the hash.
+ $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
}
- $cache[$row->page_title] = $entry;
}
$cache['VERSION'] = MSG_CACHE_VERSION;
return;
}
- // Reload messages from the database and pre-populate dc-local caches
- // as optimisation. Use the master DB to avoid race conditions.
- $cache = $this->loadFromDB( $code, self::FOR_UPDATE );
+ // Load the existing cache to update it in the local DC cache.
+ // The other DCs will see a hash mismatch.
+ if ( $this->load( $code, self::FOR_UPDATE ) ) {
+ $cache = $this->cache->get( $code );
+ } else {
+ // Err? Fall back to loading from the database.
+ $cache = $this->loadFromDB( $code, self::FOR_UPDATE );
+ }
// Check if individual cache keys should exist and update cache accordingly
$newTextByTitle = []; // map of (title => content)
+ $newBigTitles = []; // map of (title => latest revision ID), like EXCESSIVE in loadFromDB()
foreach ( $replacements as list( $title ) ) {
$page = WikiPage::factory( Title::makeTitle( NS_MEDIAWIKI, $title ) );
$page->loadPageData( $page::READ_LATEST );
// Remember the text for the blob store update later on
$newTextByTitle[$title] = $text;
// Note that if $text is false, then $cache should have a !NONEXISTANT entry
- if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- // Match logic of loadCachedMessagePageEntry()
- $this->wanCache->set(
- $this->bigMessageCacheKey( $cache['HASH'], $title ),
- ' ' . $text,
- $this->mExpiry
- );
+ if ( !is_string( $text ) ) {
+ $cache[$title] = '!NONEXISTENT';
+ } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
+ $cache[$title] = '!TOO BIG';
+ $newBigTitles[$title] = $page->getLatest();
+ } else {
+ $cache[$title] = ' ' . $text;
}
}
+ // Update HASH for the new key. Incorporates various administrative keys,
+ // including the old HASH (and thereby the EXCESSIVE value from loadFromDB()
+ // and previous replace() calls), but that doesn't really matter since we
+ // only ever compare it for equality with a copy saved by saveToCaches().
+ $cache['HASH'] = md5( serialize( $cache + [ 'EXCESSIVE' => $newBigTitles ] ) );
+ // Update the too-big WAN cache entries now that we have the new HASH
+ foreach ( $newBigTitles as $title => $id ) {
+ // Match logic of loadCachedMessagePageEntry()
+ $this->wanCache->set(
+ $this->bigMessageCacheKey( $cache['HASH'], $title ),
+ ' ' . $newTextByTitle[$title],
+ $this->mExpiry
+ );
+ }
// Mark this cache as definitely being "latest" (non-volatile) so
// load() calls do not try to refresh the cache with replica DB data
$cache['LATEST'] = time();
Hooks::run( 'MessageCache::get', [ &$lckey ] );
// Loop through each language in the fallback list until we find something useful
- $lang = wfGetLangObj( $langcode );
$message = $this->getMessageFromFallbackChain(
- $lang,
+ wfGetLangObj( $langcode ),
$lckey,
!$this->mDisable && $useDB
);
$this->getMessagePageName( $langcode, $uckey ),
$langcode
);
-
if ( $message !== false ) {
return $message;
}
$this->load( $code );
$entry = $this->cache->getField( $code, $title );
+
if ( $entry !== null ) {
+ // Message page exists as an override of a software messages
if ( substr( $entry, 0, 1 ) === ' ' ) {
// The message exists and is not '!TOO BIG'
return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
+ // The text might be '-' or missing due to some data loss
return false;
}
- // Fall through and try invididual message cache below
- } else {
- // Message does not have a MediaWiki page definition
- $message = false;
- Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
- if ( $message !== false ) {
- $this->cache->setField( $code, $title, ' ' . $message );
- } else {
- $this->cache->setField( $code, $title, '!NONEXISTENT' );
- }
-
- return $message;
- }
-
- if ( $this->cacheVolatile[$code] ) {
- $entry = false;
- // Make sure that individual keys respect the WAN cache holdoff period too
- LoggerFactory::getInstance( 'MessageCache' )->debug(
- __METHOD__ . ': loading volatile key \'{titleKey}\'',
- [ 'titleKey' => $title, 'code' => $code ] );
+ // Load the message page, utilizing the individual message cache.
+ // If the page does not exist, there will be no hook handler fallbacks.
+ $entry = $this->loadCachedMessagePageEntry(
+ $title,
+ $code,
+ $this->cache->getField( $code, 'HASH' )
+ );
} else {
- // Try the individual message cache
+ // Message page does not exist or does not override a software message.
+ // Load the message page, utilizing the individual message cache.
$entry = $this->loadCachedMessagePageEntry(
$title,
$code,
$this->cache->getField( $code, 'HASH' )
);
+ if ( substr( $entry, 0, 1 ) !== ' ' ) {
+ // Message does not have a MediaWiki page definition; try hook handlers
+ $message = false;
+ Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
+ if ( $message !== false ) {
+ $this->cache->setField( $code, $title, ' ' . $message );
+ } else {
+ $this->cache->setField( $code, $title, '!NONEXISTENT' );
+ }
+
+ return $message;
+ }
}
if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
- $this->cache->setField( $code, $title, $entry );
+ if ( $this->cacheVolatile[$code] ) {
+ // Make sure that individual keys respect the WAN cache holdoff period too
+ LoggerFactory::getInstance( 'MessageCache' )->debug(
+ __METHOD__ . ': loading volatile key \'{titleKey}\'',
+ [ 'titleKey' => $title, 'code' => $code ] );
+ } else {
+ $this->cache->setField( $code, $title, $entry );
+ }
// The message exists, so make sure a string is returned
return (string)substr( $entry, 1 );
}
throw new MWException( __CLASS__ . ': must call startWrite() before finishWrite()' );
}
- $dbw = $this->getWriteConnection();
- $dbw->startAtomic( __METHOD__ );
+ $trxProfiler = Profiler::instance()->getTransactionProfiler();
+ $oldSilenced = $trxProfiler->setSilenced( true );
try {
- $dbw->delete( 'l10n_cache', [ 'lc_lang' => $this->currentLang ], __METHOD__ );
- foreach ( array_chunk( $this->batch, 500 ) as $rows ) {
- $dbw->insert( 'l10n_cache', $rows, __METHOD__ );
- }
- $this->writesDone = true;
- } catch ( DBQueryError $e ) {
- if ( $dbw->wasReadOnlyError() ) {
- $this->readOnly = true; // just avoid site down time
- } else {
- throw $e;
+ $dbw = $this->getWriteConnection();
+ $dbw->startAtomic( __METHOD__ );
+ try {
+ $dbw->delete( 'l10n_cache', [ 'lc_lang' => $this->currentLang ], __METHOD__ );
+ foreach ( array_chunk( $this->batch, 500 ) as $rows ) {
+ $dbw->insert( 'l10n_cache', $rows, __METHOD__ );
+ }
+ $this->writesDone = true;
+ } catch ( DBQueryError $e ) {
+ if ( $dbw->wasReadOnlyError() ) {
+ $this->readOnly = true; // just avoid site down time
+ } else {
+ throw $e;
+ }
}
+ $dbw->endAtomic( __METHOD__ );
+ } finally {
+ $trxProfiler->setSilenced( $oldSilenced );
}
- $dbw->endAtomic( __METHOD__ );
$this->currentLang = null;
$this->batch = [];
public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
global $wgCommandLineMode;
- if ( self::$executeContext && self::$executeContext['stage'] >= $stage ) {
+ if (
+ self::$executeContext &&
+ self::$executeContext['stage'] >= $stage &&
+ !( $update instanceof MergeableUpdate )
+ ) {
// This is a sub-DeferredUpdate; run it right after its parent update.
// Also, while post-send updates are running, push any "pre-send" jobs to the
// active post-send queue to make sure they get run this round (or at all).
*/
public static function doUpdates( $mode = 'run', $stage = self::ALL ) {
$stageEffective = ( $stage === self::ALL ) ? self::POSTSEND : $stage;
+ // For ALL mode, make sure that any PRESEND updates added along the way get run.
+ // Normally, these use the subqueue, but that isn't true for MergeableUpdate items.
+ do {
+ if ( $stage === self::ALL || $stage === self::PRESEND ) {
+ self::execute( self::$preSendUpdates, $mode, $stageEffective );
+ }
- if ( $stage === self::ALL || $stage === self::PRESEND ) {
- self::execute( self::$preSendUpdates, $mode, $stageEffective );
- }
-
- if ( $stage === self::ALL || $stage == self::POSTSEND ) {
- self::execute( self::$postSendUpdates, $mode, $stageEffective );
- }
+ if ( $stage === self::ALL || $stage == self::POSTSEND ) {
+ self::execute( self::$postSendUpdates, $mode, $stageEffective );
+ }
+ } while ( $stage === self::ALL && self::$preSendUpdates );
}
/**
/** @var MergeableUpdate $existingUpdate */
$existingUpdate = $queue[$class];
$existingUpdate->merge( $update );
+ // Move the update to the end to handle things like mergeable purge
+ // updates that might depend on the prior updates in the queue running
+ unset( $queue[$class] );
+ $queue[$class] = $existingUpdate;
} else {
$queue[$class] = $update;
}
"config-skins-screenshots": "$1 (لقطات شاشة: $2)",
"config-extensions-requires": "$1 (يتطلب $2)",
"config-screenshot": "لقطة شاشة",
+ "config-extension-not-found": "لم يمكن العثور على ملف التسجيل للامتداد \"$1\"",
+ "config-extension-dependency": "خطأ اعتماد حدث أثناء تنصيب الامتداد \"$1\": $2",
"mainpagetext": "<strong>تم تثبيت ميدياويكي بنجاح.</strong>",
"mainpagedocfooter": "استشر [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents دليل المستخدم] لمعلومات حول استخدام برنامج الويكي.\n\n== البداية ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings قائمة إعدادات الضبط]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ أسئلة متكررة حول ميدياويكي]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة بإصدار ميدياويكي]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam تعلم كيفية مكافحة السخام في الويكي الخاص بك]"
}
"config-skins-screenshot": "$1 ($2)",
"config-extensions-requires": "$1 (erfordert $2)",
"config-screenshot": "Bildschirmfoto",
+ "config-extension-not-found": "Die Registrierungsdatei für die Erweiterung „$1“ konnte nicht gefunden werden",
+ "config-extension-dependency": "Bei der Installation der Erweiterung „$1“ ist ein Abhängigkeitsfehler aufgetreten: $2",
"mainpagetext": "<strong>MediaWiki wurde installiert.</strong>",
"mainpagedocfooter": "Hilfe zur Benutzung und Konfiguration der Wiki-Software findest du im [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Benutzerhandbuch].\n\n== Starthilfen ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste der Konfigurationsvariablen]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki-FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailingliste neuer MediaWiki-Versionen]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Übersetze MediaWiki für deine Sprache]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Erfahre, wie du Spam auf deinem Wiki bekämpfen kannst]"
}
"config-skins-screenshots": "$1 (captures d’écran : $2)",
"config-extensions-requires": "$1 (nécessite $2)",
"config-screenshot": "Captures d’écrans",
+ "config-extension-not-found": "Impossible de trouver le fichier d’inscription pour l’extension « $1 »",
+ "config-extension-dependency": "Une erreur de dépendance s’est produite en installant l’extension « $1 » : $2",
"mainpagetext": "<strong>MediaWiki a été installé.</strong>",
"mainpagedocfooter": "Consultez le [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]"
}
"config-nofile": "Il file \"$1\" non può essere trovato. È stato eliminato?",
"config-extension-link": "Sapevi che il tuo wiki supporta le [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions estensioni]?\n\nPuoi navigare tra le [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category estensioni per categoria].",
"config-extensions-requires": "$1 (richiesto $2)",
+ "config-extension-not-found": "Impossibile trovare il file di registrazione per l'estensione \"$1\"",
+ "config-extension-dependency": "Si è verificato un errore di dipendenza durante l'installazione dell'estensione \"$1\": $2",
"mainpagetext": "<strong>MediaWiki è stato installato.</strong>",
"mainpagedocfooter": "Consulta la [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents guida utente] per maggiori informazioni sull'uso di questo software wiki.\n\n== Per iniziare ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Impostazioni di configurazione]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Domande frequenti su MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list annunci MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Trova MediaWiki nella tua lingua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Imparare a combattere lo spam sul tuo wiki]"
}
"config-skins-screenshots": "$1 (screenshots: $2)",
"config-extensions-requires": "$1 (requer $2)",
"config-screenshot": "screenshot",
+ "config-extension-not-found": "Não foi possível encontrar o arquivo de registo da extensão \"$1\"",
+ "config-extension-dependency": "Foi encontrado um erro de dependências ao instalar a extensão \"$1\": $2",
"mainpagetext": "<strong>O MediaWiki foi instalado.</strong>",
"mainpagedocfooter": "Consulte o [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Manual de Usuário] para informações de como usar o software wiki.\n\n== Começando ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de opções de configuração]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ do MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de discussão com avisos de novas versões do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traduza o MediaWiki para seu idioma]"
}
"config-install-mainpage-failed": "Não foi possível inserir a página principal: $1",
"config-install-done": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não o descarregar agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
"config-install-done-path": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório <code>$4</code>. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando a hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
- "config-install-success": "O MediaWiki foi instalado. Já pode visitar <$1$2> para ver a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes,\n<https://www.mediawiki.org/wiki/Manual:FAQ/pt>, ou utilize um dos fóruns de suporte indicados nessa página.",
+ "config-install-success": "O MediaWiki foi instalado. Já pode visitar <$1$2> para ver a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes,\n<https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ>, ou utilize um dos fóruns de suporte indicados nessa página.",
"config-download-localsettings": "Descarregar <code>LocalSettings.php</code>",
"config-help": "ajuda",
"config-help-tooltip": "clique para expandir",
"config-skins-screenshots": "$1 (capturas de ecrã: $2)",
"config-extensions-requires": "$1 (requer $2)",
"config-screenshot": "captura de ecrã",
+ "config-extension-not-found": "Não foi possível encontrar o ficheiro de registo da extensão \"$1\"",
+ "config-extension-dependency": "Foi encontrado um erro de dependências ao instalar a extensão \"$1\": $2",
"mainpagetext": "<strong>O MediaWiki foi instalado.</strong>",
"mainpagedocfooter": "Consulte a [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Ajuda do MediaWiki] para informações sobre o uso do software wiki.\n\n== Onde começar ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de opções de configuração]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Perguntas e respostas frequentes sobre o MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Subscreva a lista de divulgação de novas versões do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Regionalize o MediaWiki para a sua língua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Aprenda a combater <i>spam</i> na sua wiki]"
}
"config-skins-screenshot": "$1 ($2)",
"config-extensions-requires": "$1 (захтева $2)",
"config-screenshot": "снимак екрана",
+ "config-extension-not-found": "Није могуће пронаћи датотеку регистрације за додатак „$1”",
"mainpagetext": "<strong>Медијавики је инсталиран.</strong>",
"mainpagedocfooter": "Погледајте [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents кориснички водич] за коришћење програма.\n\n== Увод ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Помоћ у вези са подешавањима]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Често постављана питања]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописни списак о издањима Медијавикија]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научите како да се борите против спама на свом викију]"
}
"config-skins-screenshots": "$1 (螢幕截圖: $2)",
"config-extensions-requires": "$1(需要 $2)",
"config-screenshot": "螢幕截圖",
+ "config-extension-not-found": "查無用於擴充功能「$1」的註冊檔案",
+ "config-extension-dependency": "當安裝擴充功能「$1」時發生相依性錯誤:$2",
"mainpagetext": "<strong>已安裝 MediaWiki。</strong>",
"mainpagedocfooter": "有關使用wiki的訊息,請參閱[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 使用者指南]。\n\n== 新手入門 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 系統設定]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki常見問題]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki郵寄清單]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources 將MediaWiki翻譯至您的語言]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam 了解如何在您的wiki上防禦破壞]"
}
use Wikimedia\Rdbms\DBQueryError;
use Wikimedia\Rdbms\DBConnectionError;
use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\TransactionProfiler;
+use Wikimedia\ScopedCallback;
use Wikimedia\WaitConditionLoop;
/**
$type = $info['type'] ?? 'mysql';
$host = $info['host'] ?? '[unknown]';
$this->logger->debug( __CLASS__ . ": connecting to $host" );
- // Use a blank trx profiler to ignore expections as this is a cache
- $info['trxProfiler'] = new TransactionProfiler();
$db = Database::factory( $type, $info );
$db->clearFlag( DBO_TRX ); // auto-commit mode
} else {
if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
// Keep a separate connection to avoid contention and deadlocks
$db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
- // @TODO: Use a blank trx profiler to ignore expections as this is a cache
} else {
// However, SQLite has the opposite behavior due to DB-level locking.
// Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
$result = true;
$exptime = (int)$expiry;
+ $silenceScope = $this->silenceTransactionProfiler();
foreach ( $keysByTable as $serverIndex => $serverKeys ) {
$db = null;
try {
protected function cas( $casToken, $key, $value, $exptime = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
+ $silenceScope = $this->silenceTransactionProfiler();
try {
$db = $this->getDB( $serverIndex );
$exptime = intval( $exptime );
public function delete( $key ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
+ $silenceScope = $this->silenceTransactionProfiler();
try {
$db = $this->getDB( $serverIndex );
$db->delete(
public function incr( $key, $step = 1 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
+ $silenceScope = $this->silenceTransactionProfiler();
try {
$db = $this->getDB( $serverIndex );
$step = intval( $step );
public function changeTTL( $key, $expiry = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
+ $silenceScope = $this->silenceTransactionProfiler();
try {
$db = $this->getDB( $serverIndex );
$db->update(
* @return bool
*/
public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
+ $silenceScope = $this->silenceTransactionProfiler();
for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
$db = null;
try {
* @return bool
*/
public function deleteAll() {
+ $silenceScope = $this->silenceTransactionProfiler();
for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
$db = null;
try {
return ( $loop->invoke() === $loop::CONDITION_REACHED );
}
+
+ /**
+ * Returns a ScopedCallback which resets the silence flag in the transaction profiler when it is
+ * destroyed on the end of a scope, for example on return or throw
+ * @return ScopedCallback
+ * @since 1.32
+ */
+ protected function silenceTransactionProfiler() {
+ $trxProfiler = Profiler::instance()->getTransactionProfiler();
+ $oldSilenced = $trxProfiler->setSilenced( true );
+ return new ScopedCallback( function () use ( $trxProfiler, $oldSilenced ) {
+ $trxProfiler->setSilenced( $oldSilenced );
+ } );
+ }
}
// Since we're not really outputting HTML, decode the entities and
// then re-encode the things that need hiding inside HTML comments.
$limitReport = htmlspecialchars_decode( $limitReport );
- // Run deprecated hook
- Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
// Sanitize for comment. Note '‐' in the replacement is U+2010,
// which looks much like the problematic '-'.
return $skin->doEditSectionLink( $editsectionPage,
$editsectionSection,
$editsectionContent,
- $wgLang->getCode()
+ $wgLang
);
},
$text
*
* @param array $configuration List of configuration values keyed by variable name
* @return string JavaScript code
+ * @throws Exception
*/
public static function makeConfigSetScript( array $configuration ) {
- return Xml::encodeJsCall(
+ $js = Xml::encodeJsCall(
'mw.config.set',
[ $configuration ],
self::inDebugMode()
);
+ if ( $js === false ) {
+ throw new Exception(
+ 'JSON serialization of config data failed. ' .
+ 'This usually means the config data is not valid UTF-8.'
+ );
+ }
+ return $js;
}
/**
'pluralRules' => $language->getPluralRules(),
'digitGroupingPattern' => $language->digitGroupingPattern(),
'fallbackLanguages' => $language->getFallbackLanguages(),
+ 'bcp47Map' => LanguageCode::getNonstandardLanguageCodeMapping(),
];
}
* @param string $section The designation of the section being pointed to,
* to be included in the link, like "§ion=$section"
* @param string|null $tooltip The tooltip to use for the link: will be escaped
- * and wrapped in the 'editsectionhint' message
- * @param string $lang Language code
+ * and wrapped in the 'editsectionhint' message.
+ * Not setting this parameter is deprecated.
+ * @param Language|string $lang Language object or language code string.
+ * Type string is deprecated. Not setting this parameter is deprecated.
* @return string HTML to use for edit link
*/
public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) {
// HTML generated here should probably have userlangattributes
// added to it for LTR text on RTL pages
- $lang = wfGetLangObj( $lang );
+ if ( !$lang instanceof Language ) {
+ wfDeprecated( __METHOD__ . ' with other type than Language for $lang', '1.32' );
+ $lang = wfGetLangObj( $lang );
+ }
$attribs = [];
if ( !is_null( $tooltip ) ) {
);
$result .= '<span class="mw-editsection-bracket">]</span></span>';
- // Deprecated, use SkinEditSectionLinks hook instead
- Hooks::run(
- 'DoEditSectionLink',
- [ $this, $nt, $section, $tooltip, &$result, $lang ],
- '1.25'
- );
return $result;
}
$activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400;
$timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
- $tables = [ 'querycachetwo', 'user', 'recentchanges' ] + $rcQuery['tables'];
- $jconds = $rcQuery['joins'];
+ $tables = [ 'querycachetwo', 'user', 'rc' => [ 'recentchanges' ] + $rcQuery['tables'] ];
+ $jconds = [
+ 'user' => [ 'JOIN', 'user_name = qcc_title' ],
+ 'rc' => [ 'JOIN', $rcQuery['fields']['rc_user_text'] . ' = qcc_title' ],
+ ] + $rcQuery['joins'];
$conds = [
'qcc_type' => 'activeusers',
'qcc_namespace' => NS_USER,
- 'user_name = qcc_title',
- $rcQuery['fields']['rc_user_text'] . ' = qcc_title',
'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
}
if ( $this->groups !== [] ) {
$tables[] = 'user_groups';
- $conds[] = 'ug_user = user_id';
+ $jconds['user_groups'] = [ 'JOIN', [ 'ug_user = user_id' ] ];
$conds['ug_group'] = $this->groups;
$conds[] = 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() );
}
/**
* Get a list of implicit groups
+ * TODO: Should we deprecate this? It's trivial, but we don't want to encourage use of globals.
+ *
* @return array Array of Strings Array of internal group names
*/
public static function getImplicitGroups() {
global $wgImplicitGroups;
-
- $groups = $wgImplicitGroups;
- # Deprecated, use $wgImplicitGroups instead
- Hooks::run( 'UserGetImplicitGroups', [ &$groups ], '1.25' );
-
- return $groups;
+ return $wgImplicitGroups;
}
/**
}
function validateVariant( $variant = null ) {
+ if ( $variant === null ) {
+ return null;
+ }
+ $variant = strtolower( $variant );
return $variant === $this->mLang->getCode() ? $variant : null;
}
/**
* Mapping of deprecated language codes that were used in previous
* versions of MediaWiki to up-to-date, current language codes.
+ * These may or may not be valid BCP 47 codes; they are included here
+ * because MediaWiki remapped these particular codes at some point.
*
* @var array Mapping from language code to language code
*
* @since 1.30
+ * @see https://meta.wikimedia.org/wiki/Special_language_codes
*/
private static $deprecatedLanguageCodeMapping = [
// Note that als is actually a valid ISO 639 code (Tosk Albanian), but it
// was previously used in MediaWiki for Alsatian, which comes under gsw
- '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',
+ 'als' => 'gsw', // T25215
+ 'bat-smg' => 'sgs', // T27522
+ 'be-x-old' => 'be-tarask', // T11823
+ 'fiu-vro' => 'vro', // T31186
+ 'roa-rup' => 'rup', // T17988
+ 'zh-classical' => 'lzh', // T30443
+ 'zh-min-nan' => 'nan', // T30442
+ 'zh-yue' => 'yue', // T30441
+ ];
+
+ /**
+ * Mapping of non-standard language codes used in MediaWiki to
+ * standardized BCP 47 codes. These are not deprecated (yet?):
+ * IANA may eventually recognize the subtag, in which case the `-x-`
+ * infix could be removed, or else we could rename the code in
+ * MediaWiki, in which case they'd move up to the above mapping
+ * of deprecated codes.
+ *
+ * As a rule, we preserve all distinctions made by MediaWiki
+ * internally. For example, `de-formal` becomes `de-x-formal`
+ * instead of just `de` because MediaWiki distinguishes `de-formal`
+ * from `de` (for example, for interface translations). Similarly,
+ * BCP 47 indicates that `kk-Cyrl` SHOULD not be used because it
+ * "typically does not add information", but in our case MediaWiki
+ * LanguageConverter distinguishes `kk` (render content in a mix of
+ * Kurdish variants) from `kk-Cyrl` (convert content to be uniformly
+ * Cyrillic). As the BCP 47 requirement is a SHOULD not a MUST,
+ * `kk-Cyrl` is a valid code, although some validators may emit
+ * a warning note.
+ *
+ * @var array Mapping from nonstandard codes to BCP 47 codes
+ *
+ * @since 1.32
+ * @see https://meta.wikimedia.org/wiki/Special_language_codes
+ * @see https://phabricator.wikimedia.org/T125073
+ */
+ private static $nonstandardLanguageCodeMapping = [
+ // All codes returned by Language::fetchLanguageNames() validated
+ // against IANA registry at
+ // https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
+ // with help of validator at
+ // http://schneegans.de/lv/
+ 'cbk-zam' => 'cbk', // T124657
+ 'de-formal' => 'de-x-formal',
+ 'eml' => 'egl', // T36217
+ 'en-rtl' => 'en-x-rtl',
+ 'es-formal' => 'es-x-formal',
+ 'hu-formal' => 'hu-x-formal',
+ 'map-bms' => 'jv-x-bms', // [[en:Banyumasan_dialect]] T125073
+ 'mo' => 'ro-Cyrl-MD', // T125073
+ 'nrm' => 'nrf', // [[en:Norman_language]] T25216
+ 'nl-informal' => 'nl-x-informal',
+ 'roa-tara' => 'nap-x-tara', // [[en:Tarantino_dialect]]
+ 'simple' => 'en-simple',
+ 'sr-ec' => 'sr-Cyrl', // T117845
+ 'sr-el' => 'sr-Latn', // T117845
+
+ // Although these next codes aren't *wrong* per se, including
+ // both the script and the country code helps compatibility with
+ // other BCP 47 users. Note that MW also uses `zh-Hans`/`zh-Hant`,
+ // without a country code, and those should be left alone.
+ // (See $variantfallbacks in LanguageZh.php for Hans/Hant id.)
+ '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',
];
/**
return self::$deprecatedLanguageCodeMapping;
}
+ /**
+ * Returns a mapping of non-standard language codes used by
+ * (current and previous version of) MediaWiki, mapped to standard
+ * BCP 47 names.
+ *
+ * This array is exported to JavaScript to ensure
+ * mediawiki.language.bcp47 stays in sync with LanguageCode::bcp47().
+ *
+ * @return string[]
+ *
+ * @since 1.32
+ */
+ public static function getNonstandardLanguageCodeMapping() {
+ $result = [];
+ foreach ( self::$deprecatedLanguageCodeMapping as $code => $ignore ) {
+ $result[$code] = self::bcp47( $code );
+ }
+ foreach ( self::$nonstandardLanguageCodeMapping as $code => $ignore ) {
+ $result[$code] = self::bcp47( $code );
+ }
+ return $result;
+ }
+
/**
* Replace deprecated language codes that were used in previous
* versions of MediaWiki to up-to-date, current language codes.
* See mediawiki.language.bcp47 for the JavaScript implementation.
*
* @param string $code The language code.
- * @return string The language code which complying with BCP 47 standards.
+ * @return string A language code complying with BCP 47 standards.
*
* @since 1.31
*/
public static function bcp47( $code ) {
+ $code = self::replaceDeprecatedCodes( strtolower( $code ) );
+ if ( isset( self::$nonstandardLanguageCodeMapping[$code] ) ) {
+ $code = self::$nonstandardLanguageCodeMapping[$code];
+ }
$codeSegment = explode( '-', $code );
$codeBCP = [];
foreach ( $codeSegment as $segNo => $seg ) {
$req = $this->validateVariant( $wgDefaultLanguageVariant );
}
+ $req = $this->validateVariant( $req );
+
// This function, unlike the other get*Variant functions, is
// not memoized (i.e. there return value is not cached) since
// new information might appear during processing after this
// is first called.
- if ( $this->validateVariant( $req ) ) {
+ if ( $req ) {
return $req;
}
return $this->mMainLanguageCode;
* @return mixed Returns the variant if it is valid, null otherwise
*/
public function validateVariant( $variant = null ) {
- if ( $variant !== null && in_array( $variant, $this->mVariants ) ) {
+ if ( $variant === null ) {
+ return null;
+ }
+ // Our internal variants are always lower-case; the variant we
+ // are validating may have mixed case.
+ $variant = LanguageCode::replaceDeprecatedCodes( strtolower( $variant ) );
+ if ( in_array( $variant, $this->mVariants ) ) {
return $variant;
}
+ // Browsers are supposed to use BCP 47 standard in the
+ // Accept-Language header, but not all of our internal
+ // mediawiki variant codes are BCP 47. Map BCP 47 code
+ // to our internal code.
+ foreach ( $this->mVariants as $v ) {
+ // Case-insensitive match (BCP 47 is mixed case)
+ if ( strtolower( LanguageCode::bcp47( $v ) ) === $variant ) {
+ return $v;
+ }
+ }
return null;
}
return $this->mHeaderVariant;
}
- // see if some supported language variant is set in the
+ // See if some supported language variant is set in the
// HTTP header.
$languages = array_keys( $wgRequest->getAcceptLang() );
if ( empty( $languages ) ) {
$convTable = $convRule->getConvTable();
$action = $convRule->getRulesAction();
foreach ( $convTable as $variant => $pair ) {
- if ( !$this->validateVariant( $variant ) ) {
+ $v = $this->validateVariant( $variant );
+ if ( !$v ) {
continue;
}
if ( $action == 'add' ) {
// More efficient than array_merge(), about 2.5 times.
foreach ( $pair as $from => $to ) {
- $this->mTables[$variant]->setPair( $from, $to );
+ $this->mTables[$v]->setPair( $from, $to );
}
} elseif ( $action == 'remove' ) {
- $this->mTables[$variant]->removeArray( $pair );
+ $this->mTables[$v]->removeArray( $pair );
}
}
}
'ba' => 'башҡортса', # Bashkir
'ban' => 'Basa Bali', # Balinese
'bar' => 'Boarisch', # Bavarian (Austro-Bavarian and South Tyrolean)
- 'bat-smg' => 'žemaitėška', # Samogitian (deprecated code, 'sgs' in ISO 693-3 since 2010-06-30 )
+ 'bat-smg' => 'žemaitėška', # Samogitian (deprecated code, 'sgs' in ISO 639-3 since 2010-06-30 )
'bbc' => 'Batak Toba', # Batak Toba (falls back to bbc-latn)
'bbc-latn' => 'Batak Toba', # Batak Toba
'bcc' => 'جهلسری بلوچی', # Southern Balochi
'lzh' => '文言', # Literary Chinese, T10217
'lzz' => 'Lazuri', # Laz
'mai' => 'मैथिली', # Maithili
- 'map-bms' => 'Basa Banyumasan', # Banyumasan
+ 'map-bms' => 'Basa Banyumasan', # Banyumasan ('jv-x-bms')
'mdf' => 'мокшень', # Moksha
'mg' => 'Malagasy', # Malagasy
'mh' => 'Ebon', # Marshallese
'mn' => 'монгол', # Halh Mongolian (Cyrillic) (ISO 639-3: khk)
'mni' => 'মেইতেই লোন্', # Manipuri/Meitei
'mnw' => 'ဘာသာ မန်', # Mon, T201583
- 'mo' => 'молдовеняскэ', # Moldovan, deprecated
+ 'mo' => 'молдовеняскэ', # Moldovan, deprecated (ISO 639-2: ro-Cyrl-MD)
'mr' => 'मराठी', # Marathi
'mrj' => 'кырык мары', # Hill Mari
'ms' => 'Bahasa Melayu', # Malay
'myv' => 'эрзянь', # Erzya
'mzn' => 'مازِرونی', # Mazanderani
'na' => 'Dorerin Naoero', # Nauruan
- 'nah' => 'Nāhuatl', # Nahuatl (not in ISO 639-3)
+ 'nah' => 'Nāhuatl', # Nahuatl (added to ISO 639-3 on 2006-10-31)
'nan' => 'Bân-lâm-gú', # Min-nan, T10217
'nap' => 'Napulitano', # Neapolitan, T45793
'nb' => 'norsk bokmål', # Norwegian (Bokmal)
'nn' => 'norsk nynorsk', # Norwegian (Nynorsk)
'no' => 'norsk', # Norwegian macro language (falls back to nb).
'nov' => 'Novial', # Novial
- 'nrm' => 'Nouormand', # Norman
+ 'nrm' => 'Nouormand', # Norman (invalid code; 'nrf' in ISO 639 since 2014)
'nso' => 'Sesotho sa Leboa', # Northern Sotho
'nv' => 'Diné bizaad', # Navajo
'ny' => 'Chi-Chewa', # Chichewa
'rmy' => 'Romani', # Vlax Romany
'rn' => 'Kirundi', # Rundi/Kirundi/Urundi
'ro' => 'română', # Romanian
- 'roa-rup' => 'armãneashti', # Aromanian (deprecated code, 'rup' exists in ISO 693-3)
- 'roa-tara' => 'tarandíne', # Tarantino
+ 'roa-rup' => 'armãneashti', # Aromanian (deprecated code, 'rup' exists in ISO 639-3)
+ 'roa-tara' => 'tarandíne', # Tarantino ('nap-x-tara')
'ru' => 'русский', # Russian
'rue' => 'русиньскый', # Rusyn
'rup' => 'armãneashti', # Aromanian
'tt-cyrl' => 'татарча', # Tatar (Cyrillic script) (default)
'tt-latn' => 'tatarça', # Tatar (Latin script)
'tum' => 'chiTumbuka', # Tumbuka
- 'tw' => 'Twi', # Twi, (FIXME!)
+ 'tw' => 'Twi', # Twi
'ty' => 'reo tahiti', # Tahitian
'tyv' => 'тыва дыл', # Tyvan
'tzm' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ', # Tamazight
"uploadstash-errclear": "Не атрымалася ачысьціць файлы.",
"uploadstash-refresh": "Абнавіць сьпіс файлаў",
"uploadstash-thumbnail": "прагляд мініятуры",
- "uploadstash-exception": "Ð\9dе магÑ\83 заÑ\85аваÑ\86Ñ\8c загÑ\80Ñ\83зкÑ\83 Ñ\9e Ñ\81Ñ\85овÑ\96Ñ\88Ñ\87Ñ\8b ($1): «$2».",
+ "uploadstash-exception": "Ð\9dе аÑ\82Ñ\80Ñ\8bмалаÑ\81Ñ\8f заÑ\85аваÑ\86Ñ\8c загÑ\80Ñ\83зкÑ\83 Ñ\9e Ñ\85ованкÑ\83 ($1): «$2».",
"uploadstash-bad-path": "Шлях не існуе.",
"uploadstash-bad-path-invalid": "Шлях не зьяўляецца слушным.",
"uploadstash-bad-path-unknown-type": "Невядомы тып «$1».",
"movepage-moved": "'''Старонка «$1» была перанесеная ў «$2»'''",
"movepage-moved-redirect": "Перанакіраваньне было створана.",
"movepage-moved-noredirect": "Перанакіраваньне не было створанае.",
+ "movepage-delete-first": "Мэтавая старонка мае зашмат вэрсіяў, каб выдаліць яе пры пераносе. Калі ласка, спачатку выдаліце старонку ўручную, а потым паспрабуйце яшчэ раз.",
"articleexists": "Старонка з такой назвай ужо існуе, альбо абраная Вамі назва недапушчальная. Калі ласка, абярыце іншую назву.",
"cantmove-titleprotected": "Немагчыма перанесьці старонку, таму што новая назва знаходзіцца ў сьпісе забароненых",
"movetalk": "Перанесьці таксама старонку абмеркаваньня",
"badarticleerror": "Cette action ne peut pas être effectuée sur cette page.",
"cannotdelete": "Impossible de supprimer la page ou le fichier « $1 ».\nLa suppression a peut-être déjà été effectuée par quelqu’un d’autre.",
"cannotdelete-title": "Impossible de supprimer la page « $1 »",
- "delete-scheduled": "La page « $1 » st programmée pour être supprimée.\nVeuillez patienter.",
+ "delete-scheduled": "La page « $1 » est programmée pour être supprimée.\nVeuillez patienter.",
"delete-hook-aborted": "Suppression annulée par une extension.\nAucune explication n’a été fournie.",
"no-null-revision": "Impossible de créer une nouvelle révision vide pour la page « $1 »",
"badtitle": "Mauvais titre",
"movepage-moved": "<strong>« $1 » a été renommée en « $2 »</strong>",
"movepage-moved-redirect": "Une redirection depuis l’ancien nom a été créée.",
"movepage-moved-noredirect": "La création d’une redirection depuis l’ancien nom a été annulée.",
- "movepage-delete-first": "La page cible a trop de révisions à supprimer en tant que faisant partie du déplacement d’une page.\nVeuillez d’abord supprimer la page manuellement, puis réessayer.",
+ "movepage-delete-first": "La page cible a trop de révisions à supprimer pour le déplacement de page. Veuillez d’abord supprimer la page manuellement, puis réessayer.",
"articleexists": "Il existe déjà une page portant ce titre, ou le titre que vous avez choisi n'est pas correct.\nVeuillez en choisir un autre.",
"cantmove-titleprotected": "Vous ne pouvez pas déplacer une page vers cet emplacement car la création de page avec ce nouveau titre a été protégée.",
"movetalk": "Renommer aussi la page de discussion associée",
"page-atom-feed": "Atom feed $1 ဍူ",
"red-link-title": "$1 (လိက်မေံ လ်ုအ်ှဍေၜး)",
"nstab-main": "လက်မေံသး",
- "nstab-user": "á\80\86á\80ºá\80¯á\80\9eá\80¯á\80²á\80¸á\80\80á\80ºá\80¯á\80\86á\80¬ á\80\9cá\80\80á\80ºá\80\99á\80±á\80¶",
+ "nstab-user": "á\80\86á\80ºá\80¯á\80\9eá\80¯á\80¶á\82\8bá\80\80á\80ºá\80¯á\80\86á\80¬á\82\8b á\80\9cá\80á\80\80á\80ºá\80\99á\80±á\80¶á\81\9cá\81 á\80¬á\80º",
"nstab-special": "လိက်မေံခေါဟ်",
"nstab-project": "ပ်ုရောဴဂျက်လိက်မေံၜၠါ်",
"nstab-image": "ဖိုင်",
"revdelete-unrestricted": "Limitatioune fir Administrateuren opgehuewen",
"logentry-block-block": "$1 {{GENDER:$2|huet}} {{GENDER:$4|$3}} fir eng Zäit vu(n) $5 $6 gespaart",
"logentry-block-unblock": "$1 {{GENDER:$2|huet}} d'Spär vum {{GENDER:$4|$3}} opgehuewen",
+ "logentry-block-reblock": "$1 {{GENDER:$2|huet}} d'Spärastellunge fir {{GENDER:$4|$3}} mat enger Spärdauer vu(n) $5 $6 geännert",
+ "logentry-suppress-reblock": "$1 {{GENDER:$2|huet}} d'Spärastellunge fir {{GENDER:$4|$3}} mat enger Spärdauer vu(n) $5 $6 geännert",
"logentry-import-upload": "$1 {{GENDER:$2|huet}} $3 duerch Eropluede vun engem Fichier importéiert",
"logentry-import-interwiki": "$1 huet $3 vun enger anerer Wiki {{GENDER:$2|importéiert}}",
"logentry-import-interwiki-details": "$1 {{GENDER:$2|huet}} $3 vu(n) $5 importéiert ({{PLURAL:$4|Eng Versioun|$4 Versiounen}})",
"action-editmyprivateinfo": "уредување на вашите лични податоци",
"action-editcontentmodel": "уредување на содржинскиот модел на страница",
"action-managechangetags": "создавање или (де)активирање на ознаки",
- "action-applychangetags": "Ñ\81Ñ\82аваÑ\9aе на ознаки заедно Ñ\81о напÑ\80евените промени",
+ "action-applychangetags": "Ñ\81Ñ\82аваÑ\9aе на ознаки заедно Ñ\81о ваÑ\88ите промени",
"action-changetags": "додавање и отстранување на произволни ознаки во поединечни преработки и дневнички записи",
"action-deletechangetags": "бришење ознаки од базата",
"action-purge": "превчитување на оваа страница",
"recentchangeslinked-page": "Sidenamn:",
"recentchangeslinked-to": "Vis endringar på sider som lenkjar til den gitte sida i staden",
"recentchanges-page-added-to-category": "[[:$1]] vart lagd til kategorien",
+ "recentchanges-page-added-to-category-bundled": "[[:$1]] lagt til i kategorien; [[Special:WhatLinksHere/$1|denne sida er inkludert i andre sider]]",
"recentchanges-page-removed-from-category": "[[:$1]] fjerna frå kategori",
+ "recentchanges-page-removed-from-category-bundled": "[[:$1]] fjernat frå kategori, [[Special:WhatLinksHere/$1|denne sida er inkludert i andre sider]]",
"upload": "Last opp fil",
"uploadbtn": "Last opp fil",
"reuploaddesc": "Attende til opplastingsskjemaet.",
"badarticleerror": "Esta ação não pode ser realizada nesta página.",
"cannotdelete": "Não foi possível eliminar a página ou arquivo $1.\nÉ possível que ele já tenha sido eliminado por outra pessoa.",
"cannotdelete-title": "Não é possível eliminar a página \"$1\"",
+ "delete-scheduled": "A página \"$1\" está agendada para eliminação.\nAguarde a mesma, por favor.",
"delete-hook-aborted": "A eliminação foi cancelada por um \"hook\".\nNão foi dada nenhuma explicação.",
"no-null-revision": "Não foi possível criar nova revisão nula para a página \"$1\"",
"badtitle": "Título inválido",
"movepage-moved": "'''\"$1\" foi movida para \"$2\"'''",
"movepage-moved-redirect": "Um redirecionamento foi criado.",
"movepage-moved-noredirect": "A criação de um redirecionamento foi suprimida.",
+ "movepage-delete-first": "A página de destino tem muitas revisões para excluir como parte de uma movimentação de página. Primeiro, exclua a página manualmente e tente novamente.",
"articleexists": "Uma página com este título já existe, ou o título que escolheu é inválido.\nPor favor, escolha outro nome.",
"cantmove-titleprotected": "Você não pode mover uma página para tal denominação uma vez que o novo título se encontra protegido contra criação",
"movetalk": "Mover também a página de discussão associada",
"badarticleerror": "Esta operação não pode ser realizada nesta página.",
"cannotdelete": "Não foi possível eliminar a página ou ficheiro \"$1\".\nPode já ter sido eliminado por outro utilizador.",
"cannotdelete-title": "Não é possível eliminar a página \"$1\"",
+ "delete-scheduled": "A página \"$1\" está agendada para eliminação.\nAguarde a mesma, por favor.",
"delete-hook-aborted": "A eliminação foi cancelada por um \"hook\".\nNão foi dada nenhuma explicação.",
"no-null-revision": "Não foi possível criar uma nova revisão nula para a página \"$1\"",
"badtitle": "Título inválido",
"movepage-moved": "<strong>\"$1\" foi movida para \"$2\"</strong>",
"movepage-moved-redirect": "Foi criado um redirecionamento.",
"movepage-moved-noredirect": "A criação de um redirecionamento foi suprimida.",
+ "movepage-delete-first": "A página de destino tem demasiadas revisões para apagá-las como parte de uma movimentação de página. Elimine-a primeiro manualmente e depois tente novamente, por favor.",
"articleexists": "Ou já existe uma página com este nome ou o nome que escolheu é inválido.\nEscolha outro nome, por favor.",
"cantmove-titleprotected": "Não pode mover uma página para esse destino, porque o novo título foi protegido para evitar a sua criação",
"movetalk": "Mover também a página de discussão associada",
"variantname-gan-hans": "{{Optional}}\n\nVariant option for wikis with variants conversion enabled.",
"variantname-gan-hant": "{{Optional}}\n\nVariant option for wikis with variants conversion enabled.",
"variantname-gan": "{{Optional}}\n\nVariant option for wikis with variants conversion enabled.",
- "variantname-sr-ec": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-ec</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-cyrl</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
- "variantname-sr-el": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-el</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-latn</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
+ "variantname-sr-ec": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-ec</code> is not a conforming BCP 47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-cyrl</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
+ "variantname-sr-el": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-el</code> is not a conforming BCP 47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-latn</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
"variantname-sr": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
"variantname-kk-kz": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
"variantname-kk-tr": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
"action-editmyprivateinfo": "редактирование вашей частной информации",
"action-editcontentmodel": "редактирование контентной модели страницы",
"action-managechangetags": "создание и (де)активацию меток",
- "action-applychangetags": " применять теги наряду с Вашими изменениями",
+ "action-applychangetags": "применение меток вместе с Вашими изменениями",
"action-changetags": "добавление и удаление произвольных меток на отдельных изменениях и записях в журнале",
"action-deletechangetags": "удаление меток из базы данных",
"action-purge": "очистку кэша этой страницы",
"externaldberror": "Дошло је до грешке при потврди идентитета базе података или вам није дозвољено да ажурирате свој спољни налог.",
"login": "Пријава",
"login-security": "Потврда вашег индентитета",
- "nav-login-createaccount": "Ð\9fÑ\80иÑ\98ави ме / оÑ\82воÑ\80и налог",
+ "nav-login-createaccount": "Ð\9fÑ\80иÑ\98ава / Ñ\80егиÑ\81Ñ\82Ñ\80аÑ\86иÑ\98а",
"logout": "Одјава",
"userlogout": "Одјава",
"notloggedin": "Нисте пријављени",
}
function execute() {
- global $wgVersion, $wgLang, $wgAllowSchemaUpdates;
+ global $wgVersion, $wgLang, $wgAllowSchemaUpdates, $wgMessagesDirs;
if ( !$wgAllowSchemaUpdates
&& !( $this->hasOption( 'force' )
}
}
+ // T206765: We need to load the installer i18n files as some of errors come installer/updater code
+ $wgMessagesDirs['MediawikiInstaller'] = dirname( __DIR__ ) . '/includes/installer/i18n';
+
$lang = Language::factory( 'en' );
// Set global language to ensure localised errors are in English (T22633)
RequestContext::getMain()->setLanguage( $lang );
* - `pluralRules`
* - `digitGroupingPattern`
* - `fallbackLanguages`
+ * - `bcp47Map`
+ * - `languageNames`
*
* @property
*/
},
/**
- * Formats language tags according the BCP47 standard.
+ * Formats language tags according the BCP 47 standard.
* See LanguageCode::bcp47 for the PHP implementation.
*
* @param {string} languageTag Well-formed language tag
* @return {string}
*/
bcp47: function ( languageTag ) {
- var formatted,
+ var bcp47Map,
+ formatted,
+ segments,
isFirstSegment = true,
- isPrivate = false,
- segments = languageTag.split( '-' );
+ isPrivate = false;
+ languageTag = languageTag.toLowerCase();
+
+ bcp47Map = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'bcp47Map' );
+ if ( bcp47Map && Object.prototype.hasOwnProperty.call( bcp47Map, languageTag ) ) {
+ languageTag = bcp47Map[ languageTag ];
+ }
+
+ segments = languageTag.split( '-' );
formatted = segments.map( function ( segment ) {
var newSegment;
*/
private $mwGlobalsToUnset = [];
+ /**
+ * Holds original values of ini settings to be restored
+ * in tearDown().
+ * @see setIniSettings()
+ * @var array
+ */
+ private $iniSettings = [];
+
/**
* Holds original loggers which have been replaced by setLogger()
* @var LoggerInterface[]
foreach ( $this->mwGlobalsToUnset as $value ) {
unset( $GLOBALS[$value] );
}
+ foreach ( $this->iniSettings as $name => $value ) {
+ ini_set( $name, $value );
+ }
if (
array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
}
}
+ /**
+ * Set an ini setting for the duration of the test
+ * @param string $name Name of the setting
+ * @param string $value Value to set
+ * @since 1.32
+ */
+ protected function setIniSetting( $name, $value ) {
+ $original = ini_get( $name );
+ $this->iniSettings[$name] = $original;
+ ini_set( $name, $value );
+ }
+
/**
* Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
* Otherwise old namespace data will lurk and cause bugs.
];
}
- /**
- * @dataProvider provideHooks
- * @covers ::wfRunHooks
- */
- public function testOldStyleHooks( $msg, array $hook, $expectedFoo, $expectedBar ) {
- global $wgHooks;
-
- $this->hideDeprecated( 'wfRunHooks' );
- $foo = $bar = 'original';
-
- $wgHooks['MediaWikiHooksTest001'][] = $hook;
- wfRunHooks( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
-
- $this->assertSame( $expectedFoo, $foo, $msg );
- $this->assertSame( $expectedBar, $bar, $msg );
- }
-
/**
* @dataProvider provideHooks
* @covers Hooks::register
'wgUseMediaWikiUIEverywhere' => false,
] );
- $langObj = Language::factory( 'en' );
+ $contLangObj = Language::factory( 'en' );
// Hardcode namespaces during test runs,
// so that html output based on existing namespaces
// can be properly evaluated.
- $langObj->setNamespaces( [
+ $contLangObj->setNamespaces( [
-2 => 'Media',
-1 => 'Special',
0 => '',
100 => 'Custom',
101 => 'Custom_talk',
] );
- $this->setUserLang( $langObj );
- $this->setContentLang( $langObj );
+ $this->setContentLang( $contLangObj );
+
+ $userLangObj = Language::factory( 'es' );
+ $userLangObj->setNamespaces( [
+ -2 => "Medio",
+ -1 => "Especial",
+ 0 => "",
+ 1 => "Discusión",
+ 2 => "Usuario",
+ 3 => "Usuario discusión",
+ 4 => "Wiki",
+ 5 => "Wiki discusión",
+ 6 => "Archivo",
+ 7 => "Archivo discusión",
+ 8 => "MediaWiki",
+ 9 => "MediaWiki discusión",
+ 10 => "Plantilla",
+ 11 => "Plantilla discusión",
+ 12 => "Ayuda",
+ 13 => "Ayuda discusión",
+ 14 => "Categoría",
+ 15 => "Categoría discusión",
+ 100 => "Personalizado",
+ 101 => "Personalizado discusión",
+ ] );
+ $this->setUserLang( $userLangObj );
+
$this->restoreWarnings = false;
}
public function testNamespaceSelector() {
$this->assertEquals(
'<select id="namespace" name="namespace">' . "\n" .
- '<option value="0">(Main)</option>' . "\n" .
+ '<option value="0">(Principal)</option>' . "\n" .
'<option value="1">Talk</option>' . "\n" .
'<option value="2">User</option>' . "\n" .
'<option value="3">User talk</option>' . "\n" .
$this->assertEquals(
'<label for="mw-test-namespace">Select a namespace:</label>' . "\u{00A0}" .
'<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
- '<option value="all">all</option>' . "\n" .
- '<option value="0">(Main)</option>' . "\n" .
+ '<option value="all">todos</option>' . "\n" .
+ '<option value="0">(Principal)</option>' . "\n" .
'<option value="1">Talk</option>' . "\n" .
'<option value="2" selected="">User</option>' . "\n" .
'<option value="3">User talk</option>' . "\n" .
$this->assertEquals(
'<label for="namespace">Select a namespace:</label>' . "\u{00A0}" .
'<select id="namespace" name="namespace">' . "\n" .
- '<option value="0">(Main)</option>' . "\n" .
+ '<option value="0">(Principal)</option>' . "\n" .
'<option value="1">Talk</option>' . "\n" .
'<option value="2">User</option>' . "\n" .
'<option value="3">User talk</option>' . "\n" .
public function testCanDisableANamespaces() {
$this->assertEquals(
'<select id="namespace" name="namespace">' . "\n" .
- '<option disabled="" value="0">(Main)</option>' . "\n" .
+ '<option disabled="" value="0">(Principal)</option>' . "\n" .
'<option disabled="" value="1">Talk</option>' . "\n" .
'<option disabled="" value="2">User</option>' . "\n" .
'<option disabled="" value="3">User talk</option>' . "\n" .
/**
* @covers OutputPage::getHeadItemsArray
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testHeadItemsParserOutput() {
$op = $this->newInstance();
[ 'c' => '<d>&', 'e' => 'f', 'a' => 'q' ] );
$op->addParserOutputMetadata( $stubPO2 );
$stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
- $op->addParserOutputMetadata( $stubPO3 );
+ $op->addParserOutput( $stubPO3 );
$stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
$op->addParserOutputMetadata( $stubPO4 );
/**
* @covers OutputPage::showNewSectionLink
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testShowNewSectionLink() {
$op = $this->newInstance();
$this->assertFalse( $op->showNewSectionLink() );
- $po = new ParserOutput();
- $po->setNewSection( true );
- $op->addParserOutputMetadata( $po );
-
+ $pOut1 = $this->createParserOutputStub( 'getNewSection', true );
+ $op->addParserOutputMetadata( $pOut1 );
$this->assertTrue( $op->showNewSectionLink() );
+
+ $pOut2 = $this->createParserOutputStub( 'getNewSection', false );
+ $op->addParserOutput( $pOut2 );
+ $this->assertFalse( $op->showNewSectionLink() );
}
/**
* @covers OutputPage::forceHideNewSectionLink
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testForceHideNewSectionLink() {
$op = $this->newInstance();
$this->assertFalse( $op->forceHideNewSectionLink() );
- $po = new ParserOutput();
- $po->hideNewSection( true );
- $op->addParserOutputMetadata( $po );
-
+ $pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
+ $op->addParserOutputMetadata( $pOut1 );
$this->assertTrue( $op->forceHideNewSectionLink() );
+
+ $pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
+ $op->addParserOutput( $pOut2 );
+ $this->assertFalse( $op->forceHideNewSectionLink() );
}
/**
* @covers OutputPage::setLanguageLinks
* @covers OutputPage::getLanguageLinks
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
function testLanguageLinks() {
$op = $this->newInstance();
$op->setLanguageLinks( [ 'pt:E' ] );
$this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
- $po = new ParserOutput();
- $po->setLanguageLinks( [ 'he:F', 'ar:G' ] );
- $op->addParserOutputMetadata( $po );
+ $pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
+ $op->addParserOutputMetadata( $pOut1 );
$this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
+
+ $pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
+ $op->addParserOutput( $pOut2 );
+ $this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
}
// @todo Are these category links tests too abstract and complicated for what they test? Would
* @dataProvider provideGetCategories
*
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
* @covers OutputPage::getCategories
* @covers OutputPage::getCategoryLinks
*/
$stubPO = $this->createParserOutputStub( 'getCategories', $args );
- $op->addParserOutputMetadata( $stubPO );
+ // addParserOutput and addParserOutputMetadata should behave identically for us, so
+ // alternate to get coverage for both without adding extra tests
+ static $idx = 0;
+ $idx++;
+ $method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
+ $op->$method( $stubPO );
$this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
* @covers OutputPage::setIndicators
* @covers OutputPage::getIndicators
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testIndicators() {
$op = $this->newInstance();
$op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
$this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
- // Test with ParserOutput
- $stubPO = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
- $op->addParserOutputMetadata( $stubPO );
+ // Test with addParserOutputMetadata
+ $pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
+ $op->addParserOutputMetadata( $pOut1 );
$this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
$op->getIndicators() );
+
+ // Test with addParserOutput
+ $pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
+ $op->addParserOutput( $pOut2 );
+ $this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
+ $op->getIndicators() );
}
/**
$this->assertNull( $op->getFileVersion() );
}
- private function createParserOutputStub( $method = '', $retVal = [] ) {
+ /**
+ * Call either with arguments $methodName, $returnValue; or an array
+ * [ $methodName => $returnValue, $methodName => $returnValue, ... ]
+ */
+ private function createParserOutputStub( ...$args ) {
+ if ( count( $args ) === 0 ) {
+ $retVals = [];
+ } elseif ( count( $args ) === 1 ) {
+ $retVals = $args[0];
+ } elseif ( count( $args ) === 2 ) {
+ $retVals = [ $args[0] => $args[1] ];
+ }
$pOut = $this->getMock( ParserOutput::class );
- if ( $method !== '' ) {
+ foreach ( $retVals as $method => $retVal ) {
$pOut->method( $method )->willReturn( $retVal );
}
/**
* @covers OutputPage::getTemplateIds
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testTemplateIds() {
$op = $this->newInstance();
NS_PROJECT => [ 'F' => 5678 ],
];
- $op->addParserOutputMetadata( $stubPO2 );
+ $op->addParserOutput( $stubPO2 );
$this->assertSame( $finalIds, $op->getTemplateIds() );
// Test merging with an empty set of id's
/**
* @covers OutputPage::getFileSearchOptions
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testFileSearchOptions() {
$op = $this->newInstance();
$stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
- $op->addParserOutputMetadata( $stubPO1 );
+ $op->addParserOutput( $stubPO1 );
$this->assertSame( $files1, $op->getFileSearchOptions() );
// Test merging with a second set of files
$this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
// Test merging with an empty set of files
- $op->addParserOutputMetadata( $stubPOEmpty );
+ $op->addParserOutput( $stubPOEmpty );
$this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
}
/**
* @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
*/
public function testNoGallery() {
$op = $this->newInstance();
$this->assertTrue( $op->mNoGallery );
$stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
- $op->addParserOutputMetadata( $stubPO2 );
+ $op->addParserOutput( $stubPO2 );
$this->assertFalse( $op->mNoGallery );
}
+ private static $parserOutputHookCalled;
+
+ /**
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testParserOutputHooks() {
+ $op = $this->newInstance();
+ $pOut = $this->createParserOutputStub( 'getOutputHooks', [
+ [ 'myhook', 'banana' ],
+ [ 'yourhook', 'kumquat' ],
+ [ 'theirhook', 'hippopotamus' ],
+ ] );
+
+ self::$parserOutputHookCalled = [];
+
+ $this->setMwGlobals( 'wgParserOutputHooks', [
+ 'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
+ use ( $op, $pOut ) {
+ $this->assertSame( $op, $innerOp );
+ $this->assertSame( $pOut, $innerPOut );
+ $this->assertSame( 'banana', $data );
+ self::$parserOutputHookCalled[] = 'closure';
+ },
+ 'yourhook' => [ $this, 'parserOutputHookCallback' ],
+ 'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
+ 'uncalled' => function () {
+ $this->assertTrue( false );
+ },
+ ] );
+
+ $op->addParserOutputMetadata( $pOut );
+
+ $this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
+ }
+
+ public function parserOutputHookCallback(
+ OutputPage $op, ParserOutput $pOut, $data
+ ) {
+ $this->assertSame( 'kumquat', $data );
+
+ self::$parserOutputHookCalled[] = 'callback';
+ }
+
+ public static function parserOutputHookCallbackStatic(
+ OutputPage $op, ParserOutput $pOut, $data
+ ) {
+ // All the assert methods are actually static, who knew!
+ self::assertSame( 'hippopotamus', $data );
+
+ self::$parserOutputHookCalled[] = 'static';
+ }
+
// @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
// for them:
- // * enableClientCache()
// * addModules()
// * addModuleScripts()
// * addModuleStyles()
// * addJsConfigVars()
- // * preventClickJacking()
+ // * enableOOUI()
// Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
// be testing they actually work.
+ /**
+ * @covers OutputPage::addParserOutputText
+ */
+ public function testAddParserOutputText() {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+
+ $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
+
+ $op->addParserOutputMetadata( $pOut );
+ $this->assertSame( '', $op->getHTML() );
+
+ $op->addParserOutputText( $pOut );
+ $this->assertSame( '<some text>', $op->getHTML() );
+ }
+
+ /**
+ * @covers OutputPage::addParserOutput
+ */
+ public function testAddParserOutput() {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+ $this->assertFalse( $op->showNewSectionLink() );
+
+ $pOut = $this->createParserOutputStub( [
+ 'getText' => '<some text>',
+ 'getNewSection' => true,
+ ] );
+
+ $op->addParserOutput( $pOut );
+ $this->assertSame( '<some text>', $op->getHTML() );
+ $this->assertTrue( $op->showNewSectionLink() );
+ }
+
+ /**
+ * @covers OutputPage::addTemplate
+ */
+ public function testAddTemplate() {
+ $template = $this->getMock( QuickTemplate::class );
+ $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
+
+ $op = $this->newInstance();
+ $op->addTemplate( $template );
+
+ $this->assertSame( '<abc>&def;', $op->getHTML() );
+ }
+
+ /**
+ * @dataProvider provideParse
+ * @covers OutputPage::parse
+ * @param array $args To pass to parse()
+ * @param string $expectedHTML Expected return value for parse()
+ * @param string $expectedHTML Expected return value for parseInline(), if different
+ */
+ public function testParse( array $args, $expectedHTML ) {
+ $op = $this->newInstance();
+ $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
+ }
+
+ /**
+ * @dataProvider provideParse
+ * @covers OutputPage::parseInline
+ */
+ public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
+ if ( count( $args ) > 3 ) {
+ // $language param not supported
+ $this->assertTrue( true );
+ return;
+ }
+ $op = $this->newInstance();
+ $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
+ }
+
+ public function provideParse() {
+ return [
+ 'List at start of line' => [
+ [ '* List' ],
+ "<div class=\"mw-parser-output\"><ul><li>List</li></ul>\n</div>",
+ ],
+ 'List not at start' => [
+ [ "* ''Not'' list", false ],
+ '<div class="mw-parser-output">* <i>Not</i> list</div>',
+ ],
+ 'Interface' => [
+ [ "''Italic''", true, true ],
+ "<p><i>Italic</i>\n</p>",
+ '<i>Italic</i>',
+ ],
+ 'formatnum' => [
+ [ '{{formatnum:123456.789}}' ],
+ "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
+ ],
+ 'Language' => [
+ [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
+ "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
+ ],
+ 'Language with interface' => [
+ [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
+ "<p>123.456,789\n</p>",
+ '123.456,789',
+ ],
+ 'No section edit links' => [
+ [ '== Header ==' ],
+ '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
+ "Header</span></h2>\n</div>",
+ ]
+ ];
+ }
+
+ /**
+ * @covers OutputPage::parse
+ */
+ public function testParseNullTitle() {
+ $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parse' );
+ $op = $this->newInstance( [], null, 'notitle' );
+ $op->parse( '' );
+ }
+
+ /**
+ * @covers OutputPage::parse
+ */
+ public function testParseInlineNullTitle() {
+ $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parse' );
+ $op = $this->newInstance( [], null, 'notitle' );
+ $op->parseInline( '' );
+ }
+
+ /**
+ * @covers OutputPage::setCdnMaxage
+ * @covers OutputPage::lowerCdnMaxage
+ */
+ public function testCdnMaxage() {
+ $op = $this->newInstance();
+ $wrapper = TestingAccessWrapper::newFromObject( $op );
+ $this->assertSame( 0, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( -1 );
+ $this->assertSame( -1, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 120 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 60 );
+ $this->assertSame( 60, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 180 );
+ $this->assertSame( 180, $wrapper->mCdnMaxage );
+
+ $op->lowerCdnMaxage( 240 );
+ $this->assertSame( 180, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 300 );
+ $this->assertSame( 240, $wrapper->mCdnMaxage );
+
+ $op->lowerCdnMaxage( 120 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 180 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 60 );
+ $this->assertSame( 60, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 240 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+ }
+
+ /** @var int Faked time to set for tests that need it */
+ private static $fakeTime;
+
+ /**
+ * @dataProvider provideAdaptCdnTTL
+ * @covers OutputPage::adaptCdnTTL
+ * @param array $args To pass to adaptCdnTTL()
+ * @param int $expected Expected new value of mCdnMaxageLimit
+ * @param array $options Associative array:
+ * initialMaxage => Maxage to set before calling adaptCdnTTL() (default 86400)
+ */
+ public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
+ try {
+ MWTimestamp::setFakeTime( self::$fakeTime );
+
+ $op = $this->newInstance();
+ // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
+ // is 0, so adaptCdnTTL() won't mutate the object at all.
+ $initial = $options['initialMaxage'] ?? 86400;
+ $op->setCdnMaxage( $initial );
+
+ $op->adaptCdnTTL( ...$args );
+ } finally {
+ MWTimestamp::setFakeTime( false );
+ }
+
+ $wrapper = TestingAccessWrapper::newFromObject( $op );
+
+ // Special rules for false/null
+ if ( $args[0] === null || $args[0] === false ) {
+ $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
+ $op->setCdnMaxage( $expected + 1 );
+ $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
+ return;
+ }
+
+ $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
+
+ if ( $initial >= $expected ) {
+ $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
+ } else {
+ $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
+ }
+
+ $op->setCdnMaxage( $expected + 1 );
+ $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
+ }
+
+ public function provideAdaptCdnTTL() {
+ global $wgSquidMaxage;
+ $now = time();
+ self::$fakeTime = $now;
+ return [
+ 'Five minutes ago' => [ [ $now - 300 ], 270 ],
+ 'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
+ 'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
+ 'Five minutes ago, initial maxage four minutes' =>
+ [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
+ 'A very long time ago' => [ [ $now - 1000000000 ], $wgSquidMaxage ],
+ 'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
+
+ 'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
+ 'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
+ "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
+ 'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
+ // @todo These give incorrect results due to timezones, how to test?
+ //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
+ //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
+
+ 'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
+ 'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
+ 'A very long time ago, maxTTL even longer' =>
+ [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
+ ];
+ }
+
+ /**
+ * @covers OutputPage::enableClientCache
+ * @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
+ */
+ public function testClientCache() {
+ $op = $this->newInstance();
+
+ // Test initial value
+ $this->assertSame( true, $op->enableClientCache( null ) );
+ // Test that calling with null doesn't change the value
+ $this->assertSame( true, $op->enableClientCache( null ) );
+
+ // Test setting to false
+ $this->assertSame( true, $op->enableClientCache( false ) );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+ // Test that calling with null doesn't change the value
+ $this->assertSame( false, $op->enableClientCache( null ) );
+
+ // Test that a cacheable ParserOutput doesn't set to true
+ $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
+ $op->addParserOutputMetadata( $pOutCacheable );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+
+ // Test setting back to true
+ $this->assertSame( false, $op->enableClientCache( true ) );
+ $this->assertSame( true, $op->enableClientCache( null ) );
+
+ // Test that an uncacheable ParserOutput does set to false
+ $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
+ $op->addParserOutput( $pOutUncacheable );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+ }
+
+ /**
+ * @covers OutputPage::getCacheVaryCookies
+ */
+ public function testGetCacheVaryCookies() {
+ global $wgCookiePrefix, $wgDBname;
+ $op = $this->newInstance();
+ $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
+ $expectedCookies = [
+ "{$prefix}Token",
+ "{$prefix}LoggedOut",
+ "{$prefix}_session",
+ 'forceHTTPS',
+ 'cookie1',
+ 'cookie2',
+ ];
+
+ // We have to reset the cookies because getCacheVaryCookies may have already been called
+ TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
+
+ $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
+ $this->setTemporaryHook( 'GetCacheVaryCookies',
+ function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
+ $this->assertSame( $op, $innerOP );
+ $cookies[] = 'cookie2';
+ $this->assertSame( $expectedCookies, $cookies );
+ }
+ );
+
+ $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
+ }
+
/**
* @covers OutputPage::haveCacheVaryCookies
*/
public function testHaveCacheVaryCookies() {
$request = new FauxRequest();
- $context = new RequestContext();
- $context->setRequest( $request );
- $op = new OutputPage( $context );
+ $op = $this->newInstance( [], $request );
// No cookies are set.
$this->assertFalse( $op->haveCacheVaryCookies() );
* @covers OutputPage::addVaryHeader
* @covers OutputPage::getVaryHeader
* @covers OutputPage::getKeyHeader
+ *
+ * @param array[] $calls For each array, call addVaryHeader() with those arguments
+ * @param string[] $cookies Array of cookie names to vary on
+ * @param string $vary Text of expected Vary header (including the 'Vary: ')
+ * @param string $key Text of expected Key header (including the 'Key: ')
*/
- public function testVaryHeaders( $calls, $vary, $key ) {
- // get rid of default Vary fields
+ public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
+ // Get rid of default Vary fields
$op = $this->getMockBuilder( OutputPage::class )
->setConstructorArgs( [ new RequestContext() ] )
->setMethods( [ 'getCacheVaryCookies' ] )
->getMock();
$op->expects( $this->any() )
->method( 'getCacheVaryCookies' )
- ->will( $this->returnValue( [] ) );
+ ->will( $this->returnValue( $cookies ) );
TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
+ $this->hideDeprecated( '$wgUseKeyHeader' );
foreach ( $calls as $call ) {
- call_user_func_array( [ $op, 'addVaryHeader' ], $call );
+ $op->addVaryHeader( ...$call );
}
$this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
$this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
public function provideVaryHeaders() {
// note: getKeyHeader() automatically adds Vary: Cookie
return [
- [ // single header
+ 'No header' => [
+ [],
+ [],
+ 'Vary: ',
+ 'Key: Cookie',
+ ],
+ 'Single header' => [
[
[ 'Cookie' ],
],
+ [],
'Vary: Cookie',
'Key: Cookie',
],
- [ // non-unique headers
+ 'Non-unique headers' => [
[
[ 'Cookie' ],
[ 'Accept-Language' ],
[ 'Cookie' ],
],
+ [],
'Vary: Cookie, Accept-Language',
'Key: Cookie,Accept-Language',
],
- [ // two headers with single options
+ 'Two headers with single options' => [
[
[ 'Cookie', [ 'param=phpsessid' ] ],
[ 'Accept-Language', [ 'substr=en' ] ],
],
+ [],
'Vary: Cookie, Accept-Language',
'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
],
- [ // one header with multiple options
+ 'One header with multiple options' => [
[
[ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
],
+ [],
'Vary: Cookie',
'Key: Cookie;param=phpsessid;param=userId',
],
- [ // Duplicate option
+ 'Duplicate option' => [
[
[ 'Cookie', [ 'param=phpsessid' ] ],
[ 'Cookie', [ 'param=phpsessid' ] ],
[ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
],
+ [],
'Vary: Cookie, Accept-Language',
'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
],
- [ // Same header, different options
+ 'Same header, different options' => [
[
[ 'Cookie', [ 'param=phpsessid' ] ],
[ 'Cookie', [ 'param=userId' ] ],
],
+ [],
'Vary: Cookie',
'Key: Cookie;param=phpsessid;param=userId',
],
+ 'No header, vary cookies' => [
+ [],
+ [ 'cookie1', 'cookie2' ],
+ 'Vary: Cookie',
+ 'Key: Cookie;param=cookie1;param=cookie2',
+ ],
+ 'Cookie header with option plus vary cookies' => [
+ [
+ [ 'Cookie', [ 'param=cookie1' ] ],
+ ],
+ [ 'cookie2', 'cookie3' ],
+ 'Vary: Cookie',
+ 'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
+ ],
+ 'Non-cookie header plus vary cookies' => [
+ [
+ [ 'Accept-Language' ],
+ ],
+ [ 'cookie' ],
+ 'Vary: Accept-Language, Cookie',
+ 'Key: Accept-Language,Cookie;param=cookie',
+ ],
+ 'Cookie and non-cookie headers plus vary cookies' => [
+ [
+ [ 'Cookie', [ 'param=cookie1' ] ],
+ [ 'Accept-Language' ],
+ ],
+ [ 'cookie2' ],
+ 'Vary: Cookie, Accept-Language',
+ 'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
+ ],
];
}
+ /**
+ * @covers OutputPage::getVaryHeader
+ */
+ public function testVaryHeaderDefault() {
+ $op = $this->newInstance();
+ $this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
+ }
+
/**
* @dataProvider provideLinkHeaders
*
* @covers OutputPage::addLinkHeader
* @covers OutputPage::getLinkHeader
*/
- public function testLinkHeaders( $headers, $result ) {
+ public function testLinkHeaders( array $headers, $result ) {
$op = $this->newInstance();
foreach ( $headers as $header ) {
'Link: <https://foo/bar.jpg>;rel=preload;as=image',
],
[
- [ '<https://foo/bar.jpg>;rel=preload;as=image','<https://foo/baz.jpg>;rel=preload;as=image' ],
- 'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;rel=preload;as=image',
+ [
+ '<https://foo/bar.jpg>;rel=preload;as=image',
+ '<https://foo/baz.jpg>;rel=preload;as=image'
+ ],
+ 'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
+ 'rel=preload;as=image',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAddAcceptLanguage
+ * @covers OutputPage::addAcceptLanguage
+ * @covers OutputPage::getKeyHeader
+ */
+ public function testAddAcceptLanguage(
+ $code, array $variants, array $expected, array $options = []
+ ) {
+ $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
+ $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
+
+ if ( !in_array( 'notitle', $options ) ) {
+ $mockLang = $this->getMock( Language::class );
+
+ if ( in_array( 'varianturl', $options ) ) {
+ $mockLang->expects( $this->never() )->method( $this->anything() );
+ } else {
+ $mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
+ $mockLang->method( 'getVariants' )->willReturn( $variants );
+ $mockLang->method( 'getCode' )->willReturn( $code );
+ }
+
+ $mockTitle = $this->getMock( Title::class );
+ $mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
+
+ $op->setTitle( $mockTitle );
+ }
+
+ // This will run addAcceptLanguage()
+ $op->sendCacheControl();
+
+ $this->hideDeprecated( '$wgUseKeyHeader' );
+ $keyHeader = $op->getKeyHeader();
+
+ if ( !$expected ) {
+ $this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
+ return;
+ }
+
+ $keyHeader = explode( ' ', $keyHeader, 2 )[1];
+ $keyHeader = explode( ',', $keyHeader );
+
+ $acceptLanguage = null;
+ foreach ( $keyHeader as $item ) {
+ if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
+ $acceptLanguage = $item;
+ break;
+ }
+ }
+
+ $expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
+ $this->assertSame( $expectedString, $acceptLanguage );
+ }
+
+ public function provideAddAcceptLanguage() {
+ return [
+ 'No variants' => [ 'en', [ 'en' ], [] ],
+ 'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
+ 'Multiple variants with BCP47 alternatives' => [
+ 'zh',
+ [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
+ [ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
],
+ 'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
+ 'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
+ ];
+ }
+
+ /**
+ * @covers OutputPage::preventClickjacking
+ * @covers OutputPage::allowClickjacking
+ * @covers OutputPage::getPreventClickjacking
+ * @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
+ */
+ public function testClickjacking() {
+ $op = $this->newInstance();
+ $this->assertTrue( $op->getPreventClickjacking() );
+
+ $op->allowClickjacking();
+ $this->assertFalse( $op->getPreventClickjacking() );
+
+ $op->preventClickjacking();
+ $this->assertTrue( $op->getPreventClickjacking() );
+
+ $op->preventClickjacking( false );
+ $this->assertFalse( $op->getPreventClickjacking() );
+
+ $pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
+ $op->addParserOutputMetadata( $pOut1 );
+ $this->assertTrue( $op->getPreventClickjacking() );
+
+ // The ParserOutput can't allow, only prevent
+ $pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
+ $op->addParserOutputMetadata( $pOut2 );
+ $this->assertTrue( $op->getPreventClickjacking() );
+
+ // Reset to test with addParserOutput()
+ $op->allowClickjacking();
+ $this->assertFalse( $op->getPreventClickjacking() );
+
+ $op->addParserOutput( $pOut1 );
+ $this->assertTrue( $op->getPreventClickjacking() );
+
+ $op->addParserOutput( $pOut2 );
+ $this->assertTrue( $op->getPreventClickjacking() );
+ }
+
+ /**
+ * @dataProvider provideGetFrameOptions
+ * @covers OutputPage::getFrameOptions
+ * @covers OutputPage::preventClickjacking
+ */
+ public function testGetFrameOptions(
+ $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
+ ) {
+ $op = $this->newInstance( [
+ 'BreakFrames' => $breakFrames,
+ 'EditPageFrameOptions' => $editPageFrameOptions,
+ ] );
+ $op->preventClickjacking( $preventClickjacking );
+
+ $this->assertSame( $expected, $op->getFrameOptions() );
+ }
+
+ public function provideGetFrameOptions() {
+ return [
+ 'BreakFrames true' => [ true, false, false, 'DENY' ],
+ 'Allow clickjacking locally' => [ false, false, 'DENY', false ],
+ 'Allow clickjacking globally' => [ false, true, false, false ],
+ 'DENY globally' => [ false, true, 'DENY', 'DENY' ],
+ 'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
+ 'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
];
}
] );
}
+ /**
+ * @covers OutputPage::isTOCEnabled
+ * @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
+ */
+ public function testIsTOCEnabled() {
+ $op = $this->newInstance();
+ $this->assertFalse( $op->isTOCEnabled() );
+
+ $pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
+ $op->addParserOutputMetadata( $pOut1 );
+ $this->assertFalse( $op->isTOCEnabled() );
+
+ $pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
+ $op->addParserOutput( $pOut2 );
+ $this->assertTrue( $op->isTOCEnabled() );
+
+ // The parser output doesn't disable the TOC after it was enabled
+ $op->addParserOutputMetadata( $pOut1 );
+ $this->assertTrue( $op->isTOCEnabled() );
+ }
+
+ /**
+ * @dataProvider providePreloadLinkHeaders
+ * @covers ResourceLoaderSkinModule::getPreloadLinks
+ * @covers ResourceLoaderSkinModule::getLogoPreloadlinks
+ */
+ public function testPreloadLinkHeaders( $config, $result ) {
+ $this->setMwGlobals( $config );
+ $ctx = $this->getMockBuilder( ResourceLoaderContext::class )
+ ->disableOriginalConstructor()->getMock();
+ $module = new ResourceLoaderSkinModule();
+
+ $this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
+ }
+
+ public function providePreloadLinkHeaders() {
+ return [
+ [
+ [
+ 'wgResourceBasePath' => '/w',
+ 'wgLogo' => '/img/default.png',
+ 'wgLogoHD' => [
+ '1.5x' => '/img/one-point-five.png',
+ '2x' => '/img/two-x.png',
+ ],
+ ],
+ 'Link: </img/default.png>;rel=preload;as=image;media=' .
+ 'not all and (min-resolution: 1.5dppx),' .
+ '</img/one-point-five.png>;rel=preload;as=image;media=' .
+ '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
+ '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
+ ],
+ [
+ [
+ 'wgResourceBasePath' => '/w',
+ 'wgLogo' => '/img/default.png',
+ 'wgLogoHD' => false,
+ ],
+ 'Link: </img/default.png>;rel=preload;as=image'
+ ],
+ [
+ [
+ 'wgResourceBasePath' => '/w',
+ 'wgLogo' => '/img/default.png',
+ 'wgLogoHD' => [
+ '2x' => '/img/two-x.png',
+ ],
+ ],
+ 'Link: </img/default.png>;rel=preload;as=image;media=' .
+ 'not all and (min-resolution: 2dppx),' .
+ '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
+ ],
+ [
+ [
+ 'wgResourceBasePath' => '/w',
+ 'wgLogo' => '/img/default.png',
+ 'wgLogoHD' => [
+ 'svg' => '/img/vector.svg',
+ ],
+ ],
+ 'Link: </img/vector.svg>;rel=preload;as=image'
+
+ ],
+ [
+ [
+ 'wgResourceBasePath' => '/w',
+ 'wgLogo' => '/w/test.jpg',
+ 'wgLogoHD' => false,
+ 'wgUploadPath' => '/w/images',
+ 'IP' => dirname( __DIR__ ) . '/data/media',
+ ],
+ 'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
+ ],
+ ];
+ }
+
/**
* @return OutputPage
*/
function ( $code, $name ) {
return [
'code' => $code,
+ 'bcp47' => LanguageCode::bcp47( $code ),
'name' => $name
];
},
$this->assertEquals( $oldText, $messageCache->get( $message ), 'Content restored' );
}
+ public function testReplaceCache() {
+ global $wgWANObjectCaches;
+
+ // We need a WAN cache for this.
+ $this->setMwGlobals( [
+ 'wgMainWANCache' => 'hash',
+ 'wgWANObjectCaches' => $wgWANObjectCaches + [
+ 'hash' => [
+ 'class' => WANObjectCache::class,
+ 'cacheId' => 'hash',
+ 'channels' => []
+ ]
+ ]
+ ] );
+ $this->overrideMwServices();
+
+ MessageCache::destroyInstance();
+ $messageCache = MessageCache::singleton();
+ $messageCache->enable();
+
+ // Populate one key
+ $this->makePage( 'Key1', 'de', 'Value1' );
+ $this->assertEquals( 0,
+ DeferredUpdates::pendingUpdatesCount(),
+ 'Post-commit deferred update triggers a run of all updates' );
+ $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ), 'Key1 was successfully edited' );
+
+ // Screw up the database so MessageCache::loadFromDB() will
+ // produce the wrong result for reloading Key1
+ $this->db->delete(
+ 'page', [ 'page_namespace' => NS_MEDIAWIKI, 'page_title' => 'Key1' ], __METHOD__
+ );
+
+ // Populate the second key
+ $this->makePage( 'Key2', 'de', 'Value2' );
+ $this->assertEquals( 0,
+ DeferredUpdates::pendingUpdatesCount(),
+ 'Post-commit deferred update triggers a run of all updates' );
+ $this->assertEquals( 'Value2', $messageCache->get( 'Key2' ), 'Key2 was successfully edited' );
+
+ // Now test that the second edit didn't reload Key1
+ $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ),
+ 'Key1 wasn\'t reloaded by edit of Key2' );
+ }
+
/**
* @dataProvider provideNormalizeKey
*/
$urls1[] = $title->getCanonicalURL( '?x=1' );
$urls1[] = $title->getCanonicalURL( '?x=2' );
$urls1[] = $title->getCanonicalURL( '?x=3' );
- $update1 = new CdnCacheUpdate( $urls1 );
+ $update1 = $this->newCdnCacheUpdate( $urls1 );
DeferredUpdates::addUpdate( $update1 );
$urls2 = [];
$urls2[] = $title->getCanonicalURL( '?x=2' );
$urls2[] = $title->getCanonicalURL( '?x=3' );
$urls2[] = $title->getCanonicalURL( '?x=4' );
- $update2 = new CdnCacheUpdate( $urls2 );
+ $update2 = $this->newCdnCacheUpdate( $urls2 );
DeferredUpdates::addUpdate( $update2 );
$wrapper = TestingAccessWrapper::newFromObject( $update1 );
$this->assertEquals( array_merge( $urls1, $urls2 ), $wrapper->urls );
+
+ $update = null;
+ DeferredUpdates::clearPendingUpdates();
+ DeferredUpdates::addCallableUpdate( function () use ( $urls1, $urls2, &$update ) {
+ $update = $this->newCdnCacheUpdate( $urls1 );
+ DeferredUpdates::addUpdate( $update );
+ DeferredUpdates::addUpdate( $this->newCdnCacheUpdate( $urls2 ) );
+ DeferredUpdates::addUpdate(
+ $this->newCdnCacheUpdate( $urls2 ), DeferredUpdates::PRESEND );
+ } );
+ DeferredUpdates::doUpdates();
+
+ $wrapper = TestingAccessWrapper::newFromObject( $update );
+ $this->assertEquals( array_merge( $urls1, $urls2 ), $wrapper->urls );
+
+ $this->assertEquals( DeferredUpdates::pendingUpdatesCount(), 0, 'PRESEND update run' );
+ }
+
+ /**
+ * @param array $urls
+ * @return CdnCacheUpdate
+ */
+ private function newCdnCacheUpdate( array $urls ) {
+ return $this->getMockBuilder( CdnCacheUpdate::class )
+ ->setConstructorArgs( [ $urls ] )
+ ->setMethods( [ 'doUpdate' ] )
+ ->getMock();
}
}
* @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 BCP47 standard to lower case '$code'"
+ "Applying BCP 47 standard to lower case '$code'"
);
$code = strtoupper( $code );
$this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP47 standard to upper case '$code'"
+ "Applying BCP 47 standard to upper case '$code'"
);
}
// 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' ],
];
}
$this->lang = new LanguageToTest();
$this->lc = new TestConverter(
$this->lang, 'tg',
- [ 'tg', 'tg-latn' ]
+ # Adding 'sgs' as a variant to ensure we handle deprecated codes
+ # adding 'simple' as a variant to ensure we handle non BCP 47 codes
+ [ 'tg', 'tg-latn', 'sgs', 'simple' ]
);
}
$this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
}
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getURLVariant
+ */
+ public function testGetPreferredVariantUrl() {
+ global $wgRequest;
+ $wgRequest->setVal( 'variant', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getURLVariant
+ */
+ public function testGetPreferredVariantUrlDeprecated() {
+ global $wgRequest;
+ $wgRequest->setVal( 'variant', 'bat-smg' );
+
+ $this->assertEquals( 'sgs', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getURLVariant
+ */
+ public function testGetPreferredVariantUrlBCP47() {
+ global $wgRequest;
+ $wgRequest->setVal( 'variant', 'en-simple' );
+
+ $this->assertEquals( 'simple', $this->lc->getPreferredVariant() );
+ }
+
/**
* @covers LanguageConverter::getPreferredVariant
* @covers LanguageConverter::getHeaderVariant
$this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
}
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getHeaderVariant
+ */
+ public function testGetPreferredVariantHeadersBCP47() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'en-simple' );
+
+ $this->assertEquals( 'simple', $this->lc->getPreferredVariant() );
+ }
+
/**
* @covers LanguageConverter::getPreferredVariant
* @covers LanguageConverter::getHeaderVariant
$this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
}
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantUserOptionDeprecated() {
+ global $wgUser;
+
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'bat-smg' );
+
+ $this->assertEquals( 'sgs', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantUserOptionBCP47() {
+ global $wgUser;
+
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'en-simple' );
+
+ $this->assertEquals( 'simple', $this->lc->getPreferredVariant() );
+ }
+
/**
* @covers LanguageConverter::getPreferredVariant
* @covers LanguageConverter::getUserVariant
$this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
}
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getUserVariant
+ */
+ public function testGetPreferredVariantUserOptionForForeignLanguageDeprecated() {
+ global $wgUser;
+
+ $this->setContentLang( 'en' );
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant-tg', 'bat-smg' );
+
+ $this->assertEquals( 'sgs', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getUserVariant
+ */
+ public function testGetPreferredVariantUserOptionForForeignLanguageBCP47() {
+ global $wgUser;
+
+ $this->setContentLang( 'en' );
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant-tg', 'en-simple' );
+
+ $this->assertEquals( 'simple', $this->lc->getPreferredVariant() );
+ }
+
/**
* @covers LanguageConverter::getPreferredVariant
* @covers LanguageConverter::getUserVariant
$this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
}
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantDefaultLanguageVariantDeprecated() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'bat-smg';
+ $this->assertEquals( 'sgs', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantDefaultLanguageVariantBCP47() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'en-simple';
+ $this->assertEquals( 'simple', $this->lc->getPreferredVariant() );
+ }
+
/**
* @covers LanguageConverter::getPreferredVariant
* @covers LanguageConverter::getURLVariant
$testString .= 'xxx xxx xxx';
}
$testString .= "\n<big id='в'></big>";
- $old = ini_set( 'pcre.backtrack_limit', 200 );
+ $this->setIniSetting( 'pcre.backtrack_limit', 200 );
$result = $this->lc->autoConvert( $testString, 'tg-latn' );
- ini_set( 'pcre.backtrack_limit', $old );
// The в in the id attribute should not get converted to a v
$this->assertFalse(
strpos( $result, 'v' ),
function loadDefaultTables() {
$this->mTables = [
+ 'sgs' => new ReplacementArray(),
+ 'simple' => new ReplacementArray(),
'tg-latn' => new ReplacementArray( $this->table ),
'tg' => new ReplacementArray()
];
// # 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' ]
+ [ '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' ]
];
QUnit.test( 'mw.language.bcp47', function ( assert ) {
+ mw.language.data = this.liveLangData;
bcp47Tests.forEach( function ( data ) {
var input = data[ 0 ],
expected = data[ 1 ];
assert.strictEqual( mw.language.bcp47( input ), expected );
+ assert.strictEqual( mw.language.bcp47( input.toLowerCase() ), expected );
+ assert.strictEqual( mw.language.bcp47( input.toUpperCase() ), expected );
} );
} );
}() );