Merge "maintenance: Sanity check that --replicadb isn't using the master in sql.php"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 24 Aug 2019 20:30:14 +0000 (20:30 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 24 Aug 2019 20:30:14 +0000 (20:30 +0000)
145 files changed:
.mailmap
RELEASE-NOTES-1.34
autoload.php
docs/hooks.txt
img_auth.php
includes/Autopromote.php
includes/BadFileLookup.php [new file with mode: 0644]
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/MWNamespace.php
includes/MediaWikiServices.php
includes/OutputPage.php
includes/Permissions/PermissionManager.php
includes/ProtectionForm.php
includes/ServiceWiring.php
includes/WebRequest.php
includes/api/ApiBase.php
includes/api/ApiComparePages.php
includes/api/ApiHelp.php
includes/api/ApiMain.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllRevisions.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryContributors.php
includes/api/ApiQueryDeletedRevisions.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryUserContribs.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/auth/AuthManager.php
includes/cache/localisation/LocalisationCache.php
includes/content/AbstractContent.php
includes/content/Content.php
includes/content/ContentHandler.php
includes/content/WikitextContent.php
includes/deferred/DeferrableCallback.php
includes/deferred/DeferredUpdates.php
includes/editpage/TextboxBuilder.php
includes/exception/PermissionsError.php
includes/gallery/TraditionalImageGallery.php
includes/installer/Installer.php
includes/language/LanguageCode.php
includes/language/LanguageNameUtils.php [new file with mode: 0644]
includes/libs/filebackend/FSFileBackend.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/HTTPFileStreamer.php
includes/libs/filebackend/MemoryFileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fileop/FileOp.php
includes/libs/filebackend/fileop/StoreFileOp.php
includes/libs/filebackend/fsfile/FSFile.php
includes/libs/filebackend/fsfile/TempFSFile.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/MediumSpecificBagOStuff.php
includes/libs/objectcache/MemcachedClient.php [deleted file]
includes/libs/objectcache/MemcachedPeclBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/utils/MemcachedClient.php [new file with mode: 0644]
includes/libs/rdbms/ChronologyProtector.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/domain/DatabaseDomain.php
includes/logging/LogEventsList.php
includes/logging/LogPager.php
includes/media/ThumbnailImage.php
includes/objectcache/SqlBagOStuff.php
includes/page/ImageHistoryList.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/parser/ParserFactory.php
includes/skins/Skin.php
includes/skins/SkinTemplate.php
includes/specialpage/SpecialPage.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialEditTags.php
includes/specials/SpecialImport.php
includes/specials/SpecialLog.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialWatchlist.php
includes/specials/forms/PreferencesFormOOUI.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/DeletedContribsPager.php
includes/specials/pagers/NewFilesPager.php
includes/specials/pagers/NewPagesPager.php
includes/title/NamespaceInfo.php
includes/user/User.php
languages/Language.php
languages/data/Names.php
languages/i18n/ace.json
languages/i18n/cs.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/ig.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/mk.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nqo.json
languages/i18n/pt.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/szl.json
languages/i18n/tl.json
languages/i18n/tr.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/rebuildLocalisationCache.php
maintenance/rebuildrecentchanges.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiTestCaseTrait.php [new file with mode: 0644]
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Permissions/PermissionManagerTest.php
tests/phpunit/includes/api/ApiOptionsTest.php
tests/phpunit/includes/api/ApiQuerySiteinfoTest.php
tests/phpunit/includes/cache/LocalisationCacheTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/deferred/DeferredUpdatesTest.php
tests/phpunit/includes/filebackend/lockmanager/LockManagerGroupIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/logging/LogFormatterTest.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/parser/ParserMethodsTest.php
tests/phpunit/includes/specials/SpecialPreferencesTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/languages/LanguageTest.php
tests/phpunit/unit/includes/BadFileLookupTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php [new file with mode: 0644]
thumb.php

index 1265bd2..0f5413e 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -59,7 +59,7 @@ Ariel Glenn <ariel@wikimedia.org> <ariel@wikimedia.org>
 Arlo Breault <abreault@wikimedia.org>
 Arthur Richards <arichards@wikimedia.org>
 Arthur Richards <arichards@wikimedia.org> <awjrichards@users.mediawiki.org>
-Aryeh Gregor <simetrical+mw@gmail.com> <simetrical@users.mediawiki.org>
+Aryeh Gregor <ayg@aryeh.name> <simetrical@users.mediawiki.org>
 Asher Feldman <afeldman@wikimedia.org>
 Asher Feldman <afeldman@wikimedia.org> <asher@users.mediawiki.org>
 aude <aude.wiki@gmail.com>
index 00e4aad..df25d30 100644 (file)
@@ -100,6 +100,8 @@ For notes on 1.33.x and older releases, see HISTORY.
   See <https://www.mediawiki.org/wiki/OOUI/Themes> for details.
 * (T229035) The GetUserBlock hook was added. Use this instead of
   GetBlockedStatus.
+* ObjectFactory is available as a service. When used as a service, the object
+  specs can now specify needed DI services.
 
 === External library changes in 1.34 ===
 
@@ -352,6 +354,8 @@ because of Phabricator reports.
 * The UserIsBlockedFrom hook is only called if a block is found first, and
   should only be used to unblock a blocked user.
 * …
+* Language::$dataCache has been removed (without prior deprecation, for
+  practical reasons). Use MediaWikiServices instead to get a LocalisationCache.
 
 === Deprecations in 1.34 ===
 * The MWNamespace class is deprecated. Use NamespaceInfo.
@@ -427,6 +431,8 @@ because of Phabricator reports.
   engines.
 * Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin
   template option 'searchaction' instead.
+* Skin::getRevisionId() and Skin::isRevisionCurrent() have been deprecated.
+  Use OutputPage::getRevisionId() and OutputPage::isRevisionCurrent() instead.
 * LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
   been deprecated.
 * FileBackend::getWikiId() has been deprecated.
@@ -458,6 +464,13 @@ because of Phabricator reports.
   MediaWikiServices::getMessageCache().
 * Constructing MovePage directly is deprecated. Use MovePageFactory.
 * TempFSFile::factory() has been deprecated. Use TempFSFileFactory instead.
+* wfIsBadImage() is deprecated. Use the BadFileLookup service instead.
+* Language::getLocalisationCache() is deprecated. Use MediaWikiServices.
+* The following Language methods are deprecated: isSupportedLanguage,
+  isValidCode, isValidBuiltInCode, isKnownLanguageTag, fetchLanguageNames,
+  fetchLanguageName, getFileName, getMessagesFileName, getJsonMessagesFileName.
+  Use the new LanguageNameUtils class instead. (Note that fetchLanguageName(s)
+  are called getLanguageName(s) in the new class.)
 
 === Other changes in 1.34 ===
 * …
index 20c19e5..9b122cb 100644 (file)
@@ -874,6 +874,7 @@ $wgAutoloadLocalClasses = [
        'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
        'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
        'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
+       'MediaWiki\\BadFileLookup' => __DIR__ . '/includes/BadFileLookup.php',
        'MediaWiki\\ChangeTags\\Taggable' => __DIR__ . '/includes/changetags/Taggable.php',
        'MediaWiki\\Config\\ConfigRepository' => __DIR__ . '/includes/config/ConfigRepository.php',
        'MediaWiki\\Config\\ServiceOptions' => __DIR__ . '/includes/config/ServiceOptions.php',
@@ -891,6 +892,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Languages\\Data\\CrhExceptions' => __DIR__ . '/languages/data/CrhExceptions.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
+       'MediaWiki\\Languages\\LanguageNameUtils' => __DIR__ . '/includes/language/LanguageNameUtils.php',
        'MediaWiki\\Logger\\ConsoleLogger' => __DIR__ . '/includes/debug/logger/ConsoleLogger.php',
        'MediaWiki\\Logger\\ConsoleSpi' => __DIR__ . '/includes/debug/logger/ConsoleSpi.php',
        'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
@@ -973,7 +975,7 @@ $wgAutoloadLocalClasses = [
        'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php',
        'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
-       'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
+       'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/utils/MemcachedClient.php',
        'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
        'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
index 719c60f..b7ea02c 100644 (file)
@@ -3504,6 +3504,12 @@ processing.
 &$archive: PageArchive object
 $title: Title object of the page that we're about to undelete
 
+'UndeletePageToolLinks': Add one or more links to edit page subtitle when a page
+has been previously deleted.
+$context: IContextSource (object)
+$linkRenderer: LinkRenderer instance
+&$links: Array of HTML strings
+
 'UndeleteShowRevision': Called when showing a revision in Special:Undelete.
 $title: title object related to the revision
 $rev: revision (object) that will be viewed
index 914014d..6e45e4e 100644 (file)
@@ -53,9 +53,10 @@ $mediawiki->doPostOutputShutdown( 'fast' );
 
 function wfImageAuthMain() {
        global $wgImgAuthUrlPathMap;
+       $permissionManager = \MediaWiki\MediaWikiServices::getInstance()->getPermissionManager();
 
        $request = RequestContext::getMain()->getRequest();
-       $publicWiki = in_array( 'read', User::getGroupPermissions( [ '*' ] ), true );
+       $publicWiki = in_array( 'read', $permissionManager->getGroupPermissions( [ '*' ] ), true );
 
        // Get the requested file path (source file or thumbnail)
        $matches = WebRequest::getPathInfo();
@@ -160,7 +161,6 @@ function wfImageAuthMain() {
 
                // Check user authorization for this title
                // Checks Whitelist too
-               $permissionManager = \MediaWiki\MediaWikiServices::getInstance()->getPermissionManager();
 
                if ( !$permissionManager->userCan( 'read', $user, $title ) ) {
                        wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $name );
index b17f1ab..2156787 100644 (file)
@@ -21,6 +21,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * This class checks if user can get extra rights
  * because of conditions specified in $wgAutopromote
@@ -200,7 +202,9 @@ class Autopromote {
                        case APCOND_BLOCKED:
                                return $user->getBlock() && $user->getBlock()->isSitewide();
                        case APCOND_ISBOT:
-                               return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) );
+                               return in_array( 'bot', MediaWikiServices::getInstance()
+                                       ->getPermissionManager()
+                                       ->getGroupPermissions( $user->getGroups() ) );
                        default:
                                $result = null;
                                Hooks::run( 'AutopromoteCondition', [ $cond[0],
diff --git a/includes/BadFileLookup.php b/includes/BadFileLookup.php
new file mode 100644 (file)
index 0000000..2f7c0ea
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+
+namespace MediaWiki;
+
+use BagOStuff;
+use Hooks;
+use MalformedTitleException;
+use MediaWiki\Linker\LinkTarget;
+use RepoGroup;
+use TitleParser;
+
+class BadFileLookup {
+       /** @var callable Returns contents of blacklist (see comment for isBadFile()) */
+       private $blacklistCallback;
+
+       /** @var BagOStuff Cache of parsed bad image list */
+       private $cache;
+
+       /** @var RepoGroup */
+       private $repoGroup;
+
+       /** @var TitleParser */
+       private $titleParser;
+
+       /** @var array|null Parsed blacklist */
+       private $badFiles;
+
+       /**
+        * Do not call directly. Use MediaWikiServices.
+        *
+        * @param callable $blacklistCallback Callback that returns wikitext of a file blacklist
+        * @param BagOStuff $cache For caching parsed versions of the blacklist
+        * @param RepoGroup $repoGroup
+        * @param TitleParser $titleParser
+        */
+       public function __construct(
+               callable $blacklistCallback,
+               BagOStuff $cache,
+               RepoGroup $repoGroup,
+               TitleParser $titleParser
+       ) {
+               $this->blacklistCallback = $blacklistCallback;
+               $this->cache = $cache;
+               $this->repoGroup = $repoGroup;
+               $this->titleParser = $titleParser;
+       }
+
+       /**
+        * Determine if a file exists on the 'bad image list'.
+        *
+        * The format of MediaWiki:Bad_image_list is as follows:
+        *    * Only list items (lines starting with "*") are considered
+        *    * The first link on a line must be a link to a bad file
+        *    * Any subsequent links on the same line are considered to be exceptions,
+        *      i.e. articles where the file may occur inline.
+        *
+        * @param string $name The file name to check
+        * @param LinkTarget|null $contextTitle The page on which the file occurs, if known
+        * @return bool
+        */
+       public function isBadFile( $name, LinkTarget $contextTitle = null ) {
+               // Handle redirects; callers almost always hit wfFindFile() anyway, so just use that method
+               // because it has a fast process cache.
+               $file = $this->repoGroup->findFile( $name );
+               // XXX If we don't find the file we also don't replace spaces by underscores or otherwise
+               // validate or normalize the title, is this right?
+               if ( $file ) {
+                       $name = $file->getTitle()->getDBkey();
+               }
+
+               // Run the extension hook
+               $bad = false;
+               if ( !Hooks::run( 'BadImage', [ $name, &$bad ] ) ) {
+                       return (bool)$bad;
+               }
+
+               if ( $this->badFiles === null ) {
+                       // Not used before in this request, try the cache
+                       $blacklist = ( $this->blacklistCallback )();
+                       $key = $this->cache->makeKey( 'bad-image-list', sha1( $blacklist ) );
+                       $this->badFiles = $this->cache->get( $key ) ?: null;
+               }
+
+               if ( $this->badFiles === null ) {
+                       // Cache miss, build the list now
+                       $this->badFiles = [];
+                       $lines = explode( "\n", $blacklist );
+                       foreach ( $lines as $line ) {
+                               // List items only
+                               if ( substr( $line, 0, 1 ) !== '*' ) {
+                                       continue;
+                               }
+
+                               // Find all links
+                               $m = [];
+                               // XXX What is the ':?' doing in the regex? Why not let the TitleParser strip it?
+                               if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
+                                       continue;
+                               }
+
+                               $fileDBkey = null;
+                               $exceptions = [];
+                               foreach ( $m[1] as $i => $titleText ) {
+                                       try {
+                                               $title = $this->titleParser->parseTitle( $titleText );
+                                       } catch ( MalformedTitleException $e ) {
+                                               continue;
+                                       }
+                                       if ( $i == 0 ) {
+                                               $fileDBkey = $title->getDBkey();
+                                       } else {
+                                               $exceptions[$title->getNamespace()][$title->getDBkey()] = true;
+                                       }
+                               }
+
+                               if ( $fileDBkey !== null ) {
+                                       $this->badFiles[$fileDBkey] = $exceptions;
+                               }
+                       }
+                       $this->cache->set( $key, $this->badFiles, 24 * 60 * 60 );
+               }
+
+               return isset( $this->badFiles[$name] ) && ( !$contextTitle ||
+                       !isset( $this->badFiles[$name][$contextTitle->getNamespace()]
+                               [$contextTitle->getDBkey()] ) );
+       }
+}
index 98ffe71..8341dac 100644 (file)
@@ -2636,6 +2636,8 @@ $wgLocalisationCacheConf = [
        'store' => 'detect',
        'storeClass' => false,
        'storeDirectory' => false,
+       'storeServer' => [],
+       'forceRecache' => false,
        'manualRecache' => false,
 ];
 
@@ -9108,6 +9110,16 @@ $wgFeaturePolicyReportOnly = [];
  */
 $wgSpecialSearchFormOptions = [];
 
+/**
+ * Toggles native image lazy loading, via the "loading" attribute.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var array
+ */
+$wgNativeImageLazyLoading = false;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 550a018..d0a5080 100644 (file)
@@ -4447,8 +4447,8 @@ ERROR;
        protected function addPageProtectionWarningHeaders() {
                $out = $this->context->getOutput();
                if ( $this->mTitle->isProtected( 'edit' ) &&
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
-                               $this->mTitle->getNamespace()
+                       MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
+                               $this->getTitle()->getNamespace()
                        ) !== [ '' ]
                ) {
                        # Is the title semi-protected?
index 1741958..cc998c7 100644 (file)
@@ -24,14 +24,15 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        die( "This file is part of MediaWiki, it is not a valid entry point" );
 }
 
+use MediaWiki\BadFileLookup;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\ProcOpenError;
 use MediaWiki\Session\SessionManager;
 use MediaWiki\Shell\Shell;
-use Wikimedia\WrappedString;
 use Wikimedia\AtEase\AtEase;
+use Wikimedia\WrappedString;
 
 /**
  * Load an extension
@@ -2907,72 +2908,27 @@ function wfUnpack( $format, $data, $length = false ) {
  *    * Any subsequent links on the same line are considered to be exceptions,
  *      i.e. articles where the image may occur inline.
  *
+ * @deprecated since 1.34, use the BadFileLookup service directly instead
+ *
  * @param string $name The image name to check
  * @param Title|bool $contextTitle The page on which the image occurs, if known
  * @param string|null $blacklist Wikitext of a file blacklist
  * @return bool
  */
 function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
-       # Handle redirects; callers almost always hit wfFindFile() anyway,
-       # so just use that method because it has a fast process cache.
-       $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name ); // get the final name
-       $name = $file ? $file->getTitle()->getDBkey() : $name;
-
-       # Run the extension hook
-       $bad = false;
-       if ( !Hooks::run( 'BadImage', [ $name, &$bad ] ) ) {
-               return (bool)$bad;
-       }
-
-       $cache = ObjectCache::getLocalServerInstance( 'hash' );
-       $key = $cache->makeKey(
-               'bad-image-list', ( $blacklist === null ) ? 'default' : md5( $blacklist )
-       );
-       $badImages = $cache->get( $key );
-
-       if ( $badImages === false ) { // cache miss
-               if ( $blacklist === null ) {
-                       $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list
-               }
-               # Build the list now
-               $badImages = [];
-               $lines = explode( "\n", $blacklist );
-               foreach ( $lines as $line ) {
-                       # List items only
-                       if ( substr( $line, 0, 1 ) !== '*' ) {
-                               continue;
-                       }
-
-                       # Find all links
-                       $m = [];
-                       if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
-                               continue;
-                       }
-
-                       $exceptions = [];
-                       $imageDBkey = false;
-                       foreach ( $m[1] as $i => $titleText ) {
-                               $title = Title::newFromText( $titleText );
-                               if ( !is_null( $title ) ) {
-                                       if ( $i == 0 ) {
-                                               $imageDBkey = $title->getDBkey();
-                                       } else {
-                                               $exceptions[$title->getPrefixedDBkey()] = true;
-                                       }
-                               }
-                       }
-
-                       if ( $imageDBkey !== false ) {
-                               $badImages[$imageDBkey] = $exceptions;
-                       }
-               }
-               $cache->set( $key, $badImages, 60 );
-       }
-
-       $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false;
-       $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] );
-
-       return $bad;
+       $services = MediaWikiServices::getInstance();
+       if ( $blacklist !== null ) {
+               wfDeprecated( __METHOD__ . ' with $blacklist parameter', '1.34' );
+               return ( new BadFileLookup(
+                       function () use ( $blacklist ) {
+                               return $blacklist;
+                       },
+                       $services->getLocalServerObjectCache(),
+                       $services->getRepoGroup(),
+                       $services->getTitleParser()
+               ) )->isBadFile( $name, $contextTitle ?: null );
+       }
+       return $services->getBadFileLookup()->isBadFile( $name, $contextTitle ?: null );
 }
 
 /**
index 0121bd5..4a911b0 100644 (file)
@@ -318,8 +318,9 @@ class MWNamespace {
         * @return array
         */
        public static function getRestrictionLevels( $index, User $user = null ) {
-               return MediaWikiServices::getInstance()->getNamespaceInfo()->
-                       getRestrictionLevels( $index, $user );
+               return MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->getNamespaceRestrictionLevels( $index, $user );
        }
 
        /**
index ec82f8e..e926c32 100644 (file)
@@ -14,10 +14,12 @@ use GlobalVarConfig;
 use Hooks;
 use IBufferingStatsdDataFactory;
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use LocalisationCache;
 use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use MediaWiki\Http\HttpRequestFactory;
+use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
@@ -64,6 +66,7 @@ use SkinFactory;
 use TitleFormatter;
 use TitleParser;
 use VirtualRESTServiceClient;
+use Wikimedia\ObjectFactory;
 use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Services\SalvageableService;
 use Wikimedia\Services\ServiceContainer;
@@ -429,6 +432,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'ActorMigration' );
        }
 
+       /**
+        * @since 1.34
+        * @return BadFileLookup
+        */
+       public function getBadFileLookup() : BadFileLookup {
+               return $this->getService( 'BadFileLookup' );
+       }
+
        /**
         * @since 1.31
         * @return BlobStore
@@ -614,6 +625,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'InterwikiLookup' );
        }
 
+       /**
+        * @since 1.34
+        * @return LanguageNameUtils
+        */
+       public function getLanguageNameUtils() {
+               return $this->getService( 'LanguageNameUtils' );
+       }
+
        /**
         * @since 1.28
         * @return LinkCache
@@ -641,6 +660,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'LinkRendererFactory' );
        }
 
+       /**
+        * @since 1.34
+        * @return LocalisationCache
+        */
+       public function getLocalisationCache() : LocalisationCache {
+               return $this->getService( 'LocalisationCache' );
+       }
+
        /**
         * @since 1.28
         * @return \BagOStuff
@@ -732,6 +759,17 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'NameTableStoreFactory' );
        }
 
+       /**
+        * ObjectFactory is intended for instantiating "handlers" from declarative definitions,
+        * such as Action API modules, special pages, or REST API handlers.
+        *
+        * @since 1.34
+        * @return ObjectFactory
+        */
+       public function getObjectFactory() {
+               return $this->getService( 'ObjectFactory' );
+       }
+
        /**
         * @since 1.32
         * @return OldRevisionImporter
index b7341e3..3f2dcf7 100644 (file)
@@ -1661,6 +1661,16 @@ class OutputPage extends ContextSource {
                return $this->mRevisionId;
        }
 
+       /**
+        * Whether the revision displayed is the latest revision of the page
+        *
+        * @since 1.34
+        * @return bool
+        */
+       public function isRevisionCurrent() {
+               return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
+       }
+
        /**
         * Set the timestamp of the revision which will be displayed. This is used
         * to avoid a extra DB call in Skin::lastModified().
@@ -2666,6 +2676,8 @@ class OutputPage extends ContextSource {
         * @param string|null $action Action that was denied or null if unknown
         */
        public function showPermissionsErrorPage( array $errors, $action = null ) {
+               $services = MediaWikiServices::getInstance();
+               $permissionManager = $services->getPermissionManager();
                foreach ( $errors as $key => $error ) {
                        $errors[$key] = (array)$error;
                }
@@ -2675,11 +2687,12 @@ class OutputPage extends ContextSource {
                // 1. the user is not logged in
                // 2. the only error is insufficient permissions (i.e. no block or something else)
                // 3. the error can be avoided simply by logging in
+
                if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
                        && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
                        && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
-                       && ( User::groupHasPermission( 'user', $action )
-                       || User::groupHasPermission( 'autoconfirmed', $action ) )
+                       && ( $permissionManager->groupHasPermission( 'user', $action )
+                               || $permissionManager->groupHasPermission( 'autoconfirmed', $action ) )
                ) {
                        $displayReturnto = null;
 
@@ -2715,8 +2728,6 @@ class OutputPage extends ContextSource {
                                }
                        }
 
-                       $services = MediaWikiServices::getInstance();
-
                        $title = SpecialPage::getTitleFor( 'Userlogin' );
                        $linkRenderer = $services->getLinkRenderer();
                        $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
@@ -2730,8 +2741,6 @@ class OutputPage extends ContextSource {
                        $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
                        $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
 
-                       $permissionManager = $services->getPermissionManager();
-
                        # Don't return to a page the user can't read otherwise
                        # we'll end up in a pointless loop
                        if ( $displayReturnto && $permissionManager->userCan(
index 2d4885e..ec0157b 100644 (file)
@@ -68,7 +68,9 @@ class PermissionManager {
                'BlockDisablesLogin',
                'GroupPermissions',
                'RevokePermissions',
-               'AvailableRights'
+               'AvailableRights',
+               'NamespaceProtection',
+               'RestrictionLevels'
        ];
 
        /** @var ServiceOptions */
@@ -827,7 +829,7 @@ class PermissionManager {
         * Check restrictions on cascading pages.
         *
         * @param string $action The action to check
-        * @param User $user User to check
+        * @param UserIdentity $user User to check
         * @param array $errors List of current errors
         * @param string $rigor One of PermissionManager::RIGOR_ constants
         *   - RIGOR_QUICK  : does cheap permission checks from replica DBs (usable for GUI creation)
@@ -841,7 +843,7 @@ class PermissionManager {
         */
        private function checkCascadingSourcesRestrictions(
                $action,
-               User $user,
+               UserIdentity $user,
                $errors,
                $rigor,
                $short,
@@ -870,7 +872,7 @@ class PermissionManager {
                                        if ( $right == 'autoconfirmed' ) {
                                                $right = 'editsemiprotected';
                                        }
-                                       if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
+                                       if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) {
                                                $wikiPages = '';
                                                /** @var Title $wikiPage */
                                                foreach ( $cascadingSources as $wikiPage ) {
@@ -1086,7 +1088,7 @@ class PermissionManager {
         * Check CSS/JSON/JS sub-page permissions
         *
         * @param string $action The action to check
-        * @param User $user User to check
+        * @param UserIdentity $user User to check
         * @param array $errors List of current errors
         * @param string $rigor One of PermissionManager::RIGOR_ constants
         *   - RIGOR_QUICK  : does cheap permission checks from replica DBs (usable for GUI creation)
@@ -1100,7 +1102,7 @@ class PermissionManager {
         */
        private function checkUserConfigPermissions(
                $action,
-               User $user,
+               UserIdentity $user,
                $errors,
                $rigor,
                $short,
@@ -1120,22 +1122,22 @@ class PermissionManager {
                        // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
                        if (
                                $title->isUserCssConfigPage()
-                               && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
+                               && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' )
                        ) {
                                $errors[] = [ 'mycustomcssprotected', $action ];
                        } elseif (
                                $title->isUserJsonConfigPage()
-                               && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
+                               && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' )
                        ) {
                                $errors[] = [ 'mycustomjsonprotected', $action ];
                        } elseif (
                                $title->isUserJsConfigPage()
-                               && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
+                               && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' )
                        ) {
                                $errors[] = [ 'mycustomjsprotected', $action ];
                        } elseif (
                                $title->isUserJsConfigPage()
-                               && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
+                               && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' )
                        ) {
                                // T207750 - do not allow users to edit a redirect if they couldn't edit the target
                                $rev = $this->revisionLookup->getRevisionByTitle( $title );
@@ -1195,6 +1197,42 @@ class PermissionManager {
                return in_array( $action, $this->getUserPermissions( $user ), true );
        }
 
+       /**
+        * Check if user is allowed to make any action
+        *
+        * @param UserIdentity $user
+        * // TODO: HHVM can't create mocks with variable params @param string ...$actions
+        * @return bool True if user is allowed to perform *any* of the given actions
+        * @since 1.34
+        */
+       public function userHasAnyRight( UserIdentity $user ) {
+               $actions = array_slice( func_get_args(), 1 );
+               foreach ( $actions as $action ) {
+                       if ( $this->userHasRight( $user, $action ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Check if user is allowed to make all actions
+        *
+        * @param UserIdentity $user
+        * // TODO: HHVM can't create mocks with variable params @param string ...$actions
+        * @return bool True if user is allowed to perform *all* of the given actions
+        * @since 1.34
+        */
+       public function userHasAllRights( UserIdentity $user ) {
+               $actions = array_slice( func_get_args(), 1 );
+               foreach ( $actions as $action ) {
+                       if ( !$this->userHasRight( $user, $action ) ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
        /**
         * Get the permissions this user has.
         *
@@ -1415,6 +1453,85 @@ class PermissionManager {
                return $this->allRights;
        }
 
+       /**
+        * Determine which restriction levels it makes sense to use in a namespace,
+        * optionally filtered by a user's rights.
+        *
+        * @param int $index Index to check
+        * @param UserIdentity|null $user User to check
+        * @return array
+        */
+       public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) {
+               if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
+                       // All levels are valid if there's no namespace restriction.
+                       // But still filter by user, if necessary
+                       $levels = $this->options->get( 'RestrictionLevels' );
+                       if ( $user ) {
+                               $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
+                                       $right = $level;
+                                       if ( $right == 'sysop' ) {
+                                               $right = 'editprotected'; // BC
+                                       }
+                                       if ( $right == 'autoconfirmed' ) {
+                                               $right = 'editsemiprotected'; // BC
+                                       }
+                                       return $this->userHasRight( $user, $right );
+                               } ) );
+                       }
+                       return $levels;
+               }
+
+               // $wgNamespaceProtection can require one or more rights to edit the namespace, which
+               // may be satisfied by membership in multiple groups each giving a subset of those rights.
+               // A restriction level is redundant if, for any one of the namespace rights, all groups
+               // giving that right also give the restriction level's right. Or, conversely, a
+               // restriction level is not redundant if, for every namespace right, there's at least one
+               // group giving that right without the restriction level's right.
+               //
+               // First, for each right, get a list of groups with that right.
+               $namespaceRightGroups = [];
+               foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
+                       if ( $right == 'sysop' ) {
+                               $right = 'editprotected'; // BC
+                       }
+                       if ( $right == 'autoconfirmed' ) {
+                               $right = 'editsemiprotected'; // BC
+                       }
+                       if ( $right != '' ) {
+                               $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
+                       }
+               }
+
+               // Now, go through the protection levels one by one.
+               $usableLevels = [ '' ];
+               foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
+                       $right = $level;
+                       if ( $right == 'sysop' ) {
+                               $right = 'editprotected'; // BC
+                       }
+                       if ( $right == 'autoconfirmed' ) {
+                               $right = 'editsemiprotected'; // BC
+                       }
+
+                       if ( $right != '' &&
+                                !isset( $namespaceRightGroups[$right] ) &&
+                                ( !$user || $this->userHasRight( $user, $right ) )
+                       ) {
+                               // Do any of the namespace rights imply the restriction right? (see explanation above)
+                               foreach ( $namespaceRightGroups as $groups ) {
+                                       if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
+                                               // Yes, this one does.
+                                               continue 2;
+                                       }
+                               }
+                               // No, keep the restriction level
+                               $usableLevels[] = $level;
+                       }
+               }
+
+               return $usableLevels;
+       }
+
        /**
         * Add temporary user rights, only valid for the current scope.
         * This is meant for making it possible to programatically trigger certain actions that
index adca805..8b5d995 100644 (file)
@@ -90,7 +90,7 @@ class ProtectionForm {
         * Loads the current state of protection into the object.
         */
        function loadData() {
-               $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+               $levels = MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
                        $this->mTitle->getNamespace(), $this->mContext->getUser()
                );
                $this->mCascade = $this->mTitle->areRestrictionsCascading();
@@ -180,7 +180,7 @@ class ProtectionForm {
         */
        function execute() {
                if (
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+                       MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
                                $this->mTitle->getNamespace()
                        ) === [ '' ]
                ) {
@@ -586,10 +586,12 @@ class ProtectionForm {
        function buildSelector( $action, $selected ) {
                // If the form is disabled, display all relevant levels. Otherwise,
                // just show the ones this user can use.
-               $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
-                       $this->mTitle->getNamespace(),
-                       $this->disabled ? null : $this->mContext->getUser()
-               );
+               $levels = MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->getNamespaceRestrictionLevels(
+                                       $this->mTitle->getNamespace(),
+                                       $this->disabled ? null : $this->mContext->getUser()
+                               );
 
                $id = 'mwProtect-level-' . $action;
 
index 0ae35c3..21a66cd 100644 (file)
@@ -39,6 +39,7 @@
 
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Auth\AuthManager;
+use MediaWiki\BadFileLookup;
 use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\Config\ConfigRepository;
@@ -47,6 +48,7 @@ use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use MediaWiki\Http\HttpRequestFactory;
 use MediaWiki\Interwiki\ClassicInterwikiLookup;
 use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Logger\LoggerFactory;
@@ -69,6 +71,7 @@ use MediaWiki\Storage\BlobStoreFactory;
 use MediaWiki\Storage\NameTableStoreFactory;
 use MediaWiki\Storage\SqlBlobStore;
 use MediaWiki\Storage\PageEditStash;
+use Wikimedia\ObjectFactory;
 
 return [
        'ActorMigration' => function ( MediaWikiServices $services ) : ActorMigration {
@@ -77,6 +80,17 @@ return [
                );
        },
 
+       'BadFileLookup' => function ( MediaWikiServices $services ) : BadFileLookup {
+               return new BadFileLookup(
+                       function () {
+                               return wfMessage( 'bad_image_list' )->inContentLanguage()->plain();
+                       },
+                       $services->getLocalServerObjectCache(),
+                       $services->getRepoGroup(),
+                       $services->getTitleParser()
+               );
+       },
+
        'BlobStore' => function ( MediaWikiServices $services ) : BlobStore {
                return $services->getService( '_SqlBlobStore' );
        },
@@ -243,6 +257,13 @@ return [
                );
        },
 
+       'LanguageNameUtils' => function ( MediaWikiServices $services ) : LanguageNameUtils {
+               return new LanguageNameUtils( new ServiceOptions(
+                       LanguageNameUtils::$constructorOptions,
+                       $services->getMainConfig()
+               ) );
+       },
+
        'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
                return new LinkCache(
                        $services->getTitleFormatter(),
@@ -269,6 +290,56 @@ return [
                );
        },
 
+       'LocalisationCache' => function ( MediaWikiServices $services ) : LocalisationCache {
+               $conf = $services->getMainConfig()->get( 'LocalisationCacheConf' );
+
+               $logger = LoggerFactory::getInstance( 'localisation' );
+
+               // Figure out what class to use for the LCStore
+               $storeArg = [];
+               $storeArg['directory'] =
+                       $conf['storeDirectory'] ?? $services->getMainConfig()->get( 'CacheDirectory' );
+
+               if ( !empty( $conf['storeClass'] ) ) {
+                       $storeClass = $conf['storeClass'];
+               } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
+                       ( $conf['store'] === 'detect' && $storeArg['directory'] )
+               ) {
+                       $storeClass = LCStoreCDB::class;
+               } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
+                       $storeClass = LCStoreDB::class;
+                       $storeArg['server'] = $conf['storeServer'] ?? [];
+               } elseif ( $conf['store'] === 'array' ) {
+                       $storeClass = LCStoreStaticArray::class;
+               } else {
+                       throw new MWException(
+                               'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
+                       );
+               }
+               $logger->debug( "LocalisationCache: using store $storeClass" );
+
+               return new $conf['class'](
+                       new ServiceOptions(
+                               LocalisationCache::$constructorOptions,
+                               // Two of the options are stored in $wgLocalisationCacheConf
+                               $conf,
+                               // In case someone set that config variable and didn't reset all keys, set defaults.
+                               [
+                                       'forceRecache' => false,
+                                       'manualRecache' => false,
+                               ],
+                               // Some other options come from config itself
+                               $services->getMainConfig()
+                       ),
+                       new $storeClass( $storeArg ),
+                       $logger,
+                       [ function () use ( $services ) {
+                               $services->getResourceLoader()->getMessageBlobStore()->clear();
+                       } ],
+                       $services->getLanguageNameUtils()
+               );
+       },
+
        'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff {
                $config = $services->getMainConfig();
                $cacheId = \ObjectCache::detectLocalServerCache();
@@ -419,6 +490,10 @@ return [
                );
        },
 
+       'ObjectFactory' => function ( MediaWikiServices $services ) : ObjectFactory {
+               return new ObjectFactory( $services );
+       },
+
        'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter {
                return new ImportableOldRevisionImporter(
                        true,
index 6593e49..defe07e 100644 (file)
@@ -395,18 +395,25 @@ class WebRequest {
                # https://www.php.net/variables.external#language.variables.external.dot-in-names
                # Work around PHP *feature* to avoid *bugs* elsewhere.
                $name = strtr( $name, '.', '_' );
-               if ( isset( $arr[$name] ) ) {
-                       $data = $arr[$name];
+
+               if ( !isset( $arr[$name] ) ) {
+                       return $default;
+               }
+
+               $data = $arr[$name];
+               # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings.
+               $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 );
+               if ( !$isAsciiStr ) {
                        if ( isset( $_GET[$name] ) && is_string( $data ) ) {
                                # Check for alternate/legacy character encoding.
-                               $contLang = MediaWikiServices::getInstance()->getContentLanguage();
-                               $data = $contLang->checkTitleEncoding( $data );
+                               $data = MediaWikiServices::getInstance()
+                                       ->getContentLanguage()
+                                       ->checkTitleEncoding( $data );
                        }
                        $data = $this->normalizeUnicode( $data );
-                       return $data;
-               } else {
-                       return $default;
                }
+
+               return $data;
        }
 
        /**
index a7b872c..8b6a3e5 100644 (file)
@@ -2126,7 +2126,9 @@ abstract class ApiBase extends ContextSource {
                        $user = $this->getUser();
                }
                $rights = (array)$rights;
-               if ( !$user->isAllowedAny( ...$rights ) ) {
+               if ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $user, ...$rights )
+               ) {
                        $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] );
                }
        }
index e096915..05eb438 100644 (file)
@@ -231,7 +231,9 @@ class ApiComparePages extends ApiBase {
         */
        private function getRevisionById( $id ) {
                $rev = $this->revisionStore->getRevisionById( $id );
-               if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
+               if ( !$rev && $this->getPermissionManager()
+                               ->userHasAnyRight( $this->getUser(), 'deletedtext', 'undelete' )
+               ) {
                        // Try the 'archive' table
                        $arQuery = $this->revisionStore->getArchiveQueryInfo();
                        $row = $this->getDB()->selectRow(
index 988957b..2627715 100644 (file)
@@ -66,6 +66,23 @@ class ApiHelp extends ApiBase {
                        ApiResult::setSubelementsList( $data, 'help' );
                        $result->addValue( null, $this->getModuleName(), $data );
                } else {
+                       // Show any errors at the top of the HTML
+                       $transform = [
+                               'Types' => [ 'AssocAsObject' => true ],
+                               'Strip' => 'all',
+                       ];
+                       $errors = array_filter( [
+                               'errors' => $this->getResult()->getResultData( [ 'errors' ], $transform ),
+                               'warnings' => $this->getResult()->getResultData( [ 'warnings' ], $transform ),
+                       ] );
+                       if ( $errors ) {
+                               $json = FormatJson::encode( $errors, true, FormatJson::UTF8_OK );
+                               // Escape any "--", some parsers might interpret that as end-of-comment.
+                               // The above already escaped any "<" and ">".
+                               $json = str_replace( '--', '-\u002D', $json );
+                               $html = "<!-- API warnings and errors:\n$json\n-->\n$html";
+                       }
+
                        $result->reset();
                        $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
index 6b9e4ac..641aa9f 100644 (file)
@@ -1539,6 +1539,12 @@ class ApiMain extends ApiBase {
                        $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
                }
 
+               if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) {
+                       $this->addDeprecation(
+                               'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
+                       );
+               }
+
                // See if custom printer is used
                $this->mPrinter = $module->getCustomPrinter();
                if ( is_null( $this->mPrinter ) ) {
@@ -1939,7 +1945,7 @@ class ApiMain extends ApiBase {
 
                        $groups = array_map( function ( $group ) {
                                return $group == '*' ? 'all' : $group;
-                       }, User::getGroupsWithPermission( $right ) );
+                       }, $this->getPermissionManager()->getGroupsWithPermission( $right ) );
 
                        $help['permissions'] .= Html::rawElement( 'dd', null,
                                $this->msg( 'api-help-permissions-granted-to' )
index 4eead4c..d713b3a 100644 (file)
@@ -239,7 +239,9 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                        // check it again just in case)
                        if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                                $bitmask = RevisionRecord::DELETED_USER;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
index 40cd149..b181710 100644 (file)
@@ -205,7 +205,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                                $this->addJoinConds( [ 'user_groups' => [
                                        'LEFT JOIN',
                                        [
-                                               'ug_group' => User::getGroupsWithPermission( 'bot' ),
+                                               'ug_group' => $this->getPermissionManager()->getGroupsWithPermission( 'bot' ),
                                                'ug_user = ' . $actorQuery['fields']['img_user'],
                                                'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
                                        ]
index 17a6e00..3751102 100644 (file)
@@ -156,7 +156,9 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
                        // Paranoia: avoid brute force searches (T19342)
                        if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
                                $bitmask = RevisionRecord::DELETED_USER;
-                       } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
index 59e92e1..023b88f 100644 (file)
@@ -90,7 +90,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
                if ( !is_null( $params['rights'] ) && count( $params['rights'] ) ) {
                        $groups = [];
                        foreach ( $params['rights'] as $r ) {
-                               $groups = array_merge( $groups, User::getGroupsWithPermission( $r ) );
+                               $groups = array_merge( $groups, $this->getPermissionManager()
+                                       ->getGroupsWithPermission( $r ) );
                        }
 
                        // no group with the given right(s) exists, no need for a query
@@ -312,7 +313,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                }
 
                                if ( $fld_rights ) {
-                                       $data['rights'] = User::getGroupPermissions( $groups );
+                                       $data['rights'] = $this->getPermissionManager()->getGroupPermissions( $groups );
                                        ApiResult::setIndexedTagName( $data['rights'], 'r' );
                                        ApiResult::setArrayType( $data['rights'], 'array' );
                                }
index 846a8b1..10db848 100644 (file)
@@ -600,7 +600,8 @@ abstract class ApiQueryBase extends ApiBase {
         * @return bool
         */
        public function userCanSeeRevDel() {
-               return $this->getUser()->isAllowedAny(
+               return $this->getPermissionManager()->userHasAnyRight(
+                       $this->getUser(),
                        'deletedhistory',
                        'deletedtext',
                        'suppressrevision',
index 9057f10..fd2d199 100644 (file)
@@ -152,7 +152,8 @@ class ApiQueryContributors extends ApiQueryBase {
                } elseif ( $params['rights'] ) {
                        $excludeGroups = false;
                        foreach ( $params['rights'] as $r ) {
-                               $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) );
+                               $limitGroups = array_merge( $limitGroups, $this->getPermissionManager()
+                                       ->getGroupsWithPermission( $r ) );
                        }
 
                        // If no group has the rights requested, no need to query
@@ -168,7 +169,8 @@ class ApiQueryContributors extends ApiQueryBase {
                } elseif ( $params['excluderights'] ) {
                        $excludeGroups = true;
                        foreach ( $params['excluderights'] as $r ) {
-                               $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) );
+                               $limitGroups = array_merge( $limitGroups, $this->getPermissionManager()
+                                       ->getGroupsWithPermission( $r ) );
                        }
                }
 
index ac12b47..fc88499 100644 (file)
@@ -134,7 +134,9 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
                        // check it again just in case)
                        if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                                $bitmask = RevisionRecord::DELETED_USER;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
index aa88a51..1af4d95 100644 (file)
@@ -199,7 +199,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        // check it again just in case)
                        if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                                $bitmask = RevisionRecord::DELETED_USER;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
index fe484a8..f9087eb 100644 (file)
@@ -116,7 +116,9 @@ class ApiQueryFilearchive extends ApiQueryBase {
                // Exclude files this user can't view.
                if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedtext' ) ) {
                        $bitmask = File::DELETED_FILE;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !$this->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED;
                } else {
                        $bitmask = 0;
index 0791426..b97ab3c 100644 (file)
@@ -144,7 +144,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                        $info['imagerepository'] = $img->getRepoName();
                                }
                                if ( isset( $prop['badfile'] ) ) {
-                                       $info['badfile'] = (bool)wfIsBadImage( $title, $badFileContextTitle );
+                                       $info['badfile'] = (bool)MediaWikiServices::getInstance()->getBadFileLookup()
+                                               ->isBadFile( $title, $badFileContextTitle );
                                }
 
                                $fit = $result->addValue( [ 'query', 'pages' ], (int)$pageId, $info );
index 50bd63f..ac7e5cc 100644 (file)
@@ -250,7 +250,9 @@ class ApiQueryInfo extends ApiQueryBase {
         */
        public static function getImportToken( $pageid, $title ) {
                global $wgUser;
-               if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) {
+               if ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $wgUser, 'import', 'importupload' ) ) {
                        return false;
                }
 
index c995ec5..47a6f87 100644 (file)
@@ -223,7 +223,9 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
                                $titleBits = LogPage::DELETED_ACTION;
                                $userBits = LogPage::DELETED_USER;
-                       } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
                                $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED;
                        } else {
index a74faf2..143d466 100644 (file)
@@ -363,7 +363,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
                        if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                                $bitmask = RevisionRecord::DELETED_USER;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
@@ -376,7 +378,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        // LogPage::DELETED_ACTION hides the affected page, too.
                        if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                                $bitmask = LogPage::DELETED_ACTION;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       } elseif ( !$this->getPermissionManager()
+                               ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+                       ) {
                                $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
                        } else {
                                $bitmask = 0;
index 3a06e36..d616ad4 100644 (file)
@@ -335,7 +335,9 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
                                // Paranoia: avoid brute force searches (T19342)
                                if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
                                        $bitmask = RevisionRecord::DELETED_USER;
-                               } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                               } elseif ( !$this->getPermissionManager()
+                                       ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' )
+                               ) {
                                        $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                                } else {
                                        $bitmask = 0;
index cfefcb2..919c763 100644 (file)
@@ -410,7 +410,9 @@ class ApiQueryUserContribs extends ApiQueryBase {
                $user = $this->getUser();
                if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
                        $bitmask = RevisionRecord::DELETED_USER;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !$this->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                } else {
                        $bitmask = 0;
index 6625863..8b42a07 100644 (file)
        "apiwarn-deprecation-missingparam": "Because <var>$1</var> was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used.",
        "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.",
        "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.",
+       "apiwarn-deprecation-post-without-content-type": "A POST request was made without a <code>Content-Type</code> header. This does not work reliably.",
        "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
        "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
index d5de23f..87f056b 100644 (file)
        "apiwarn-deprecation-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
        "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
        "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}",
+       "apiwarn-deprecation-post-without-content-type": "{{doc-apierror}}",
        "apiwarn-deprecation-purge-get": "{{doc-apierror}}",
        "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
        "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.",
index c871ce1..4fcaf4e 100644 (file)
@@ -1639,8 +1639,9 @@ class AuthManager implements LoggerAwareInterface {
 
                // Is the IP user able to create accounts?
                $anon = new User;
-               if ( $source !== self::AUTOCREATE_SOURCE_MAINT &&
-                       !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
+               if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->userHasAnyRight( $anon, 'createaccount', 'autocreateaccount' )
                ) {
                        $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
                                'username' => $username,
index ffc7cd0..fb4675e 100644 (file)
 
 use CLDRPluralRuleParser\Evaluator;
 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
-use MediaWiki\Logger\LoggerFactory;
-use MediaWiki\MediaWikiServices;
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
+use Psr\Log\LoggerInterface;
 
 /**
  * Class for caching the contents of localisation files, Messages*.php
  * and *.i18n.php.
  *
- * An instance of this class is available using Language::getLocalisationCache().
+ * An instance of this class is available using MediaWikiServices.
  *
  * The values retrieved from here are merged, containing items from extension
  * files, core messages files and the language fallback sequence (e.g. zh-cn ->
@@ -40,8 +41,8 @@ use MediaWiki\MediaWikiServices;
 class LocalisationCache {
        const VERSION = 4;
 
-       /** Configuration associative array */
-       private $conf;
+       /** @var ServiceOptions */
+       private $options;
 
        /**
         * True if recaching should only be done on an explicit call to recache().
@@ -50,11 +51,6 @@ class LocalisationCache {
         */
        private $manualRecache = false;
 
-       /**
-        * True to treat all files as expired until they are regenerated by this object.
-        */
-       private $forceRecache = false;
-
        /**
         * The cache data. 3-d array, where the first key is the language code,
         * the second key is the item key e.g. 'messages', and the third key is
@@ -71,10 +67,16 @@ class LocalisationCache {
        private $store;
 
        /**
-        * @var \Psr\Log\LoggerInterface
+        * @var LoggerInterface
         */
        private $logger;
 
+       /** @var callable[] See comment for parameter in constructor */
+       private $clearStoreCallbacks;
+
+       /** @var LanguageNameUtils */
+       private $langNameUtils;
+
        /**
         * A 2-d associative array, code/key, where presence indicates that the item
         * is loaded. Value arbitrary.
@@ -188,60 +190,52 @@ class LocalisationCache {
 
        private $mergeableKeys = null;
 
+       /**
+        * @todo Make this a const when HHVM support is dropped (T192166)
+        *
+        * @var array
+        * @since 1.34
+        */
+       public static $constructorOptions = [
+               // True to treat all files as expired until they are regenerated by this object.
+               'forceRecache',
+               'manualRecache',
+               'ExtensionMessagesFiles',
+               'MessagesDirs',
+       ];
+
        /**
         * For constructor parameters, see the documentation in DefaultSettings.php
         * for $wgLocalisationCacheConf.
         *
-        * @param array $conf
+        * Do not construct this directly. Use MediaWikiServices.
+        *
+        * @param ServiceOptions $options
+        * @param LCStore $store What backend to use for storage
+        * @param LoggerInterface $logger
+        * @param callable[] $clearStoreCallbacks To be called whenever the cache is cleared. Can be
+        *   used to clear other caches that depend on this one, such as ResourceLoader's
+        *   MessageBlobStore.
+        * @param LanguageNameUtils $langNameUtils
         * @throws MWException
         */
-       function __construct( $conf ) {
-               global $wgCacheDirectory;
-
-               $this->conf = $conf;
-               $this->logger = LoggerFactory::getInstance( 'localisation' );
-
-               $directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
-               $storeArg = [];
-               $storeArg['directory'] = $directory;
-
-               if ( !empty( $conf['storeClass'] ) ) {
-                       $storeClass = $conf['storeClass'];
-               } else {
-                       switch ( $conf['store'] ) {
-                               case 'files':
-                               case 'file':
-                                       $storeClass = LCStoreCDB::class;
-                                       break;
-                               case 'db':
-                                       $storeClass = LCStoreDB::class;
-                                       $storeArg['server'] = $conf['storeServer'] ?? [];
-                                       break;
-                               case 'array':
-                                       $storeClass = LCStoreStaticArray::class;
-                                       break;
-                               case 'detect':
-                                       if ( $directory ) {
-                                               $storeClass = LCStoreCDB::class;
-                                       } else {
-                                               $storeClass = LCStoreDB::class;
-                                               $storeArg['server'] = $conf['storeServer'] ?? [];
-                                       }
-                                       break;
-                               default:
-                                       throw new MWException(
-                                               'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
-                                       );
-                       }
-               }
-               $this->logger->debug( static::class . ": using store $storeClass" );
-
-               $this->store = new $storeClass( $storeArg );
-               foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
-                       if ( isset( $conf[$var] ) ) {
-                               $this->$var = $conf[$var];
-                       }
-               }
+       function __construct(
+               ServiceOptions $options,
+               LCStore $store,
+               LoggerInterface $logger,
+               array $clearStoreCallbacks,
+               LanguageNameUtils $langNameUtils
+       ) {
+               $options->assertRequiredOptions( self::$constructorOptions );
+
+               $this->options = $options;
+               $this->store = $store;
+               $this->logger = $logger;
+               $this->clearStoreCallbacks = $clearStoreCallbacks;
+               $this->langNameUtils = $langNameUtils;
+
+               // Keep this separate from $this->options so it can be mutable
+               $this->manualRecache = $options->get( 'manualRecache' );
        }
 
        /**
@@ -406,7 +400,7 @@ class LocalisationCache {
         * @return bool
         */
        public function isExpired( $code ) {
-               if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
+               if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
                        $this->logger->debug( __METHOD__ . "($code): forced reload" );
 
                        return true;
@@ -451,7 +445,7 @@ class LocalisationCache {
                $this->initialisedLangs[$code] = true;
 
                # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
-               if ( !Language::isValidBuiltInCode( $code ) ) {
+               if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
                        $this->initShallowFallback( $code, 'en' );
 
                        return;
@@ -459,7 +453,7 @@ class LocalisationCache {
 
                # Recache the data if necessary
                if ( !$this->manualRecache && $this->isExpired( $code ) ) {
-                       if ( Language::isSupportedLanguage( $code ) ) {
+                       if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
                                $this->recache( $code );
                        } elseif ( $code === 'en' ) {
                                throw new MWException( 'MessagesEn.php is missing.' );
@@ -697,7 +691,7 @@ class LocalisationCache {
                global $IP;
 
                // This reads in the PHP i18n file with non-messages l10n data
-               $fileName = Language::getMessagesFileName( $code );
+               $fileName = $this->langNameUtils->getMessagesFileName( $code );
                if ( !file_exists( $fileName ) ) {
                        $data = [];
                } else {
@@ -804,14 +798,12 @@ class LocalisationCache {
        public function getMessagesDirs() {
                global $IP;
 
-               $config = MediaWikiServices::getInstance()->getMainConfig();
-               $messagesDirs = $config->get( 'MessagesDirs' );
                return [
                        'core' => "$IP/languages/i18n",
                        'exif' => "$IP/languages/i18n/exif",
                        'api' => "$IP/includes/api/i18n",
                        'oojs-ui' => "$IP/resources/lib/ooui/i18n",
-               ] + $messagesDirs;
+               ] + $this->options->get( 'MessagesDirs' );
        }
 
        /**
@@ -821,8 +813,6 @@ class LocalisationCache {
         * @throws MWException
         */
        public function recache( $code ) {
-               global $wgExtensionMessagesFiles;
-
                if ( !$code ) {
                        throw new MWException( "Invalid language code requested" );
                }
@@ -874,7 +864,7 @@ class LocalisationCache {
 
                # Load non-JSON localisation data for extensions
                $extensionData = array_fill_keys( $codeSequence, $initialData );
-               foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) {
+               foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
                        if ( isset( $messageDirs[$extension] ) ) {
                                # This extension has JSON message data; skip the PHP shim
                                continue;
@@ -1038,8 +1028,9 @@ class LocalisationCache {
                # HACK: If using a null (i.e. disabled) storage backend, we
                # can't write to the MessageBlobStore either
                if ( !$this->store instanceof LCStoreNull ) {
-                       $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
-                       $blobStore->clear();
+                       foreach ( $this->clearStoreCallbacks as $callback ) {
+                               $callback();
+                       }
                }
        }
 
@@ -1100,5 +1091,4 @@ class LocalisationCache {
                $this->store = new LCStoreNull;
                $this->manualRecache = false;
        }
-
 }
index c82b473..eb9ef85 100644 (file)
@@ -529,7 +529,7 @@ abstract class AbstractContent implements Content {
         * @since 1.24
         *
         * @param Title $title Context title for parsing
-        * @param int|null $revId Revision ID (for {{REVISIONID}})
+        * @param int|null $revId Revision ID being rendered
         * @param ParserOptions|null $options
         * @param bool $generateHtml Whether or not to generate HTML
         *
@@ -575,7 +575,8 @@ abstract class AbstractContent implements Content {
         * @since 1.24
         *
         * @param Title $title Context title for parsing
-        * @param int|null $revId Revision ID (for {{REVISIONID}})
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications.
         * @param ParserOptions $options
         * @param bool $generateHtml Whether or not to generate HTML
         * @param ParserOutput &$output The output object to fill (reference).
index 2637aa6..8596619 100644 (file)
@@ -269,7 +269,8 @@ interface Content {
         *       may call ParserOutput::recordOption() on the output object.
         *
         * @param Title $title The page title to use as a context for rendering.
-        * @param int|null $revId Optional revision ID being rendered.
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications. (default: null)
         * @param ParserOptions|null $options Any parser options.
         * @param bool $generateHtml Whether to generate HTML (default: true). If false,
         *        the result of calling getText() on the ParserOutput object returned by
index 48dfc70..ea5ab78 100644 (file)
@@ -1077,7 +1077,8 @@ abstract class ContentHandler {
                }
 
                // Max content length = max comment length - length of the comment (excl. $1)
-               $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
+               $maxLength = CommentStore::COMMENT_CHARACTER_LIMIT - ( strlen( $reason ) - 2 );
+               $text = $content ? $content->getTextForSummary( $maxLength ) : '';
 
                // Now replace the '$1' placeholder
                $reason = str_replace( '$1', $text, $reason );
index 8e5e0a8..70b638b 100644 (file)
@@ -329,7 +329,8 @@ class WikitextContent extends TextContent {
         * using the global Parser service.
         *
         * @param Title $title
-        * @param int|null $revId Revision to pass to the parser (default: null)
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications. (default: null)
         * @param ParserOptions $options (default: null)
         * @param bool $generateHtml (default: true)
         * @param ParserOutput &$output ParserOutput representing the HTML form of the text,
index 2eb0d5d..33961ed 100644 (file)
@@ -9,5 +9,5 @@ interface DeferrableCallback {
        /**
         * @return string Originating method name
         */
-       function getOrigin();
+       public function getOrigin();
 }
index d43ffbc..3380364 100644 (file)
@@ -362,11 +362,16 @@ class DeferredUpdates {
                        $update->setTransactionTicket( $ticket );
                }
 
-               $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+               // Designate $update::doUpdate() as the write round owner
+               $fnameTrxOwner = ( $update instanceof DeferrableCallback )
+                       ? $update->getOrigin()
+                       : get_class( $update ) . '::doUpdate';
+               // Determine whether the write round will be explicit or implicit
                $useExplicitTrxRound = !(
                        $update instanceof TransactionRoundAwareUpdate &&
                        $update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
                );
+
                // Flush any pending changes left over from an implicit transaction round
                if ( $useExplicitTrxRound ) {
                        $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round
index 103b3e5..8161251 100644 (file)
@@ -75,8 +75,8 @@ class TextboxBuilder {
        public function getTextboxProtectionCSSClasses( Title $title ) {
                $classes = []; // Textarea CSS
                if ( $title->isProtected( 'edit' ) &&
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->
-                       getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
+                       MediaWikiServices::getInstance()->getPermissionManager()
+                               ->getNamespaceRestrictionLevels( $title->getNamespace() ) !== [ '' ]
                ) {
                        # Is the title semi-protected?
                        if ( $title->isSemiProtected() ) {
index cc69a76..87a3dc2 100644 (file)
@@ -18,6 +18,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Show an error when a user tries to do something they do not have the necessary
  * permissions for.
@@ -46,7 +48,9 @@ class PermissionsError extends ErrorPageError {
 
                if ( !count( $errors ) ) {
                        $groups = [];
-                       foreach ( User::getGroupsWithPermission( $this->permission ) as $group ) {
+                       foreach ( MediaWikiServices::getInstance()
+                                                 ->getPermissionManager()
+                                                 ->getGroupsWithPermission( $this->permission ) as $group ) {
                                $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
                        }
 
index d25d9aa..fadd587 100644 (file)
@@ -111,8 +111,8 @@ class TraditionalImageGallery extends ImageGalleryBase {
                                if ( $this->mParser instanceof Parser ) {
                                        $this->mParser->addTrackingCategory( 'broken-file-category' );
                                }
-                       } elseif ( $this->mHideBadImages
-                               && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() )
+                       } elseif ( $this->mHideBadImages && MediaWikiServices::getInstance()->getBadFileLookup()
+                               ->isBadFile( $nt->getDBkey(), $this->getContextTitle() )
                        ) {
                                # The image is blacklisted, just show it as a text link.
                                $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' .
index de15456..414222b 100644 (file)
@@ -412,14 +412,17 @@ abstract class Installer {
                // This will be overridden in the web installer with the user-specified language
                RequestContext::getMain()->setLanguage( 'en' );
 
-               // Disable the i18n cache
-               // TODO: manage LocalisationCache singleton in MediaWikiServices
-               Language::getLocalisationCache()->disableBackend();
-
                // Disable all global services, since we don't have any configuration yet!
                MediaWikiServices::disableStorageBackend();
 
                $mwServices = MediaWikiServices::getInstance();
+
+               // Disable i18n cache
+               $mwServices->getLocalisationCache()->disableBackend();
+
+               // Clear language cache so the old i18n cache doesn't sneak back in
+               Language::clearCaches();
+
                // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
                // SqlBagOStuff will then throw since we just disabled wfGetDB)
                $wgObjectCaches = $mwServices->getMainConfig()->get( 'ObjectCaches' );
index 7d954d3..1d2f0b4 100644 (file)
@@ -21,7 +21,6 @@
 
 /**
  * Methods for dealing with language codes.
- * @todo Move some of the code-related static methods out of Language into this class
  *
  * @since 1.29
  * @ingroup Language
diff --git a/includes/language/LanguageNameUtils.php b/includes/language/LanguageNameUtils.php
new file mode 100644 (file)
index 0000000..08d9ab3
--- /dev/null
@@ -0,0 +1,319 @@
+<?php
+/**
+ * Internationalisation code.
+ * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Language
+ */
+
+/**
+ * @defgroup Language Language
+ */
+
+namespace MediaWiki\Languages;
+
+use HashBagOStuff;
+use Hooks;
+use MediaWiki\Config\ServiceOptions;
+use MediaWikiTitleCodec;
+use MWException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * @ingroup Language
+ *
+ * A service that provides utilities to do with language names and codes.
+ *
+ * @since 1.34
+ */
+class LanguageNameUtils {
+       /**
+        * Return autonyms in getLanguageName(s).
+        */
+       const AUTONYMS = null;
+
+       /**
+        * Return all known languages in getLanguageName(s).
+        */
+       const ALL = 'all';
+
+       /**
+        * Return in getLanguageName(s) only the languages that are defined by MediaWiki.
+        */
+       const DEFINED = 'mw';
+
+       /**
+        * Return in getLanguageName(s) only the languages for which we have at least some localisation.
+        */
+       const SUPPORTED = 'mwfile';
+
+       /** @var ServiceOptions */
+       private $options;
+
+       /**
+        * Cache for language names
+        * @var HashBagOStuff|null
+        */
+       private $languageNameCache;
+
+       /**
+        * Cache for validity of language codes
+        * @var array
+        */
+       private $validCodeCache = [];
+
+       public static $constructorOptions = [
+               'ExtraLanguageNames',
+               'UsePigLatinVariant',
+       ];
+
+       /**
+        * @param ServiceOptions $options
+        */
+       public function __construct( ServiceOptions $options ) {
+               $options->assertRequiredOptions( self::$constructorOptions );
+               $this->options = $options;
+       }
+
+       /**
+        * Checks whether any localisation is available for that language tag in MediaWiki
+        * (MessagesXx.php or xx.json exists).
+        *
+        * @param string $code Language tag (in lower case)
+        * @return bool Whether language is supported
+        */
+       public function isSupportedLanguage( $code ) {
+               if ( !$this->isValidBuiltInCode( $code ) ) {
+                       return false;
+               }
+
+               if ( $code === 'qqq' ) {
+                       // Special code for internal use, not supported even though there is a qqq.json
+                       return false;
+               }
+
+               return is_readable( $this->getMessagesFileName( $code ) ) ||
+                       is_readable( $this->getJsonMessagesFileName( $code ) );
+       }
+
+       /**
+        * Returns true if a language code string is of a valid form, whether or not it exists. This
+        * includes codes which are used solely for customisation via the MediaWiki namespace.
+        *
+        * @param string $code
+        *
+        * @return bool
+        */
+       public function isValidCode( $code ) {
+               Assert::parameterType( 'string', $code, '$code' );
+               if ( !isset( $this->validCodeCache[$code] ) ) {
+                       // People think language codes are HTML-safe, so enforce it.  Ideally we should only
+                       // allow a-zA-Z0-9- but .+ and other chars are often used for {{int:}} hacks.  See bugs
+                       // T39564, T39587, T38938.
+                       $this->validCodeCache[$code] =
+                               // Protect against path traversal
+                               strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code ) &&
+                               !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
+               }
+               return $this->validCodeCache[$code];
+       }
+
+       /**
+        * Returns true if a language code is of a valid form for the purposes of internal customisation
+        * of MediaWiki, via Messages*.php or *.json.
+        *
+        * @param string $code
+        * @return bool
+        */
+       public function isValidBuiltInCode( $code ) {
+               Assert::parameterType( 'string', $code, '$code' );
+
+               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+       }
+
+       /**
+        * Returns true if a language code is an IETF tag known to MediaWiki.
+        *
+        * @param string $tag
+        *
+        * @return bool
+        */
+       public function isKnownLanguageTag( $tag ) {
+               // Quick escape for invalid input to avoid exceptions down the line when code tries to
+               // process tags which are not valid at all.
+               if ( !$this->isValidBuiltInCode( $tag ) ) {
+                       return false;
+               }
+
+               if ( isset( Data\Names::$names[$tag] ) || $this->getLanguageName( $tag, $tag ) !== '' ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Get an array of language names, indexed by code.
+        * @param null|string $inLanguage Code of language in which to return the names
+        *   Use self::AUTONYMS for autonyms (native names)
+        * @param string $include One of:
+        *   self::ALL all available languages
+        *   self::DEFINED only if the language is defined in MediaWiki or wgExtraLanguageNames
+        *     (default)
+        *   self::SUPPORTED only if the language is in self::DEFINED *and* has a message file
+        * @return array Language code => language name (sorted by key)
+        */
+       public function getLanguageNames( $inLanguage = self::AUTONYMS, $include = self::DEFINED ) {
+               $cacheKey = $inLanguage === self::AUTONYMS ? 'null' : $inLanguage;
+               $cacheKey .= ":$include";
+               if ( !$this->languageNameCache ) {
+                       $this->languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
+               }
+
+               $ret = $this->languageNameCache->get( $cacheKey );
+               if ( !$ret ) {
+                       $ret = $this->getLanguageNamesUncached( $inLanguage, $include );
+                       $this->languageNameCache->set( $cacheKey, $ret );
+               }
+               return $ret;
+       }
+
+       /**
+        * Uncached helper for getLanguageNames
+        * @param null|string $inLanguage As getLanguageNames
+        * @param string $include As getLanguageNames
+        * @return array Language code => language name (sorted by key)
+        */
+       private function getLanguageNamesUncached( $inLanguage, $include ) {
+               // If passed an invalid language code to use, fallback to en
+               if ( $inLanguage !== self::AUTONYMS && !$this->isValidCode( $inLanguage ) ) {
+                       $inLanguage = 'en';
+               }
+
+               $names = [];
+
+               if ( $inLanguage !== self::AUTONYMS ) {
+                       # TODO: also include for self::AUTONYMS, when this code is more efficient
+                       Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
+               }
+
+               $mwNames = $this->options->get( 'ExtraLanguageNames' ) + Data\Names::$names;
+               if ( $this->options->get( 'UsePigLatinVariant' ) ) {
+                       // Pig Latin (for variant development)
+                       $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
+               }
+
+               foreach ( $mwNames as $mwCode => $mwName ) {
+                       # - Prefer own MediaWiki native name when not using the hook
+                       # - For other names just add if not added through the hook
+                       if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
+                               $names[$mwCode] = $mwName;
+                       }
+               }
+
+               if ( $include === self::ALL ) {
+                       ksort( $names );
+                       return $names;
+               }
+
+               $returnMw = [];
+               $coreCodes = array_keys( $mwNames );
+               foreach ( $coreCodes as $coreCode ) {
+                       $returnMw[$coreCode] = $names[$coreCode];
+               }
+
+               if ( $include === self::SUPPORTED ) {
+                       $namesMwFile = [];
+                       # We do this using a foreach over the codes instead of a directory loop so that messages
+                       # files in extensions will work correctly.
+                       foreach ( $returnMw as $code => $value ) {
+                               if ( is_readable( $this->getMessagesFileName( $code ) ) ||
+                                       is_readable( $this->getJsonMessagesFileName( $code ) )
+                               ) {
+                                       $namesMwFile[$code] = $names[$code];
+                               }
+                       }
+
+                       ksort( $namesMwFile );
+                       return $namesMwFile;
+               }
+
+               ksort( $returnMw );
+               # self::DEFINED option; default if it's not one of the other two options
+               # (self::ALL/self::SUPPORTED)
+               return $returnMw;
+       }
+
+       /**
+        * @param string $code The code of the language for which to get the name
+        * @param null|string $inLanguage Code of language in which to return the name (self::AUTONYMS
+        *   for autonyms)
+        * @param string $include See getLanguageNames(), except this defaults to self::ALL instead of
+        *   self::DEFINED
+        * @return string Language name or empty
+        * @since 1.20
+        */
+       public function getLanguageName( $code, $inLanguage = self::AUTONYMS, $include = self::ALL ) {
+               $code = strtolower( $code );
+               $array = $this->getLanguageNames( $inLanguage, $include );
+               return $array[$code] ?? '';
+       }
+
+       /**
+        * Get the name of a file for a certain language code
+        * @param string $prefix Prepend this to the filename
+        * @param string $code Language code
+        * @param string $suffix Append this to the filename
+        * @throws MWException
+        * @return string $prefix . $mangledCode . $suffix
+        */
+       public function getFileName( $prefix, $code, $suffix = '.php' ) {
+               if ( !$this->isValidBuiltInCode( $code ) ) {
+                       throw new MWException( "Invalid language code \"$code\"" );
+               }
+
+               return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
+       }
+
+       /**
+        * @param string $code
+        * @return string
+        */
+       public function getMessagesFileName( $code ) {
+               global $IP;
+               $file = $this->getFileName( "$IP/languages/messages/Messages", $code, '.php' );
+               Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
+               return $file;
+       }
+
+       /**
+        * @param string $code
+        * @return string
+        * @throws MWException
+        */
+       public function getJsonMessagesFileName( $code ) {
+               global $IP;
+
+               if ( !$this->isValidBuiltInCode( $code ) ) {
+                       throw new MWException( "Invalid language code \"$code\"" );
+               }
+
+               return "$IP/languages/i18n/$code.json";
+       }
+}
index e6a49c7..c05dc28 100644 (file)
@@ -795,8 +795,16 @@ class FSFileBackend extends FileBackendStore {
         * Listen for E_WARNING errors and track whether any happen
         */
        protected function trapWarnings() {
-               $this->hadWarningErrors[] = false; // push to stack
-               set_error_handler( [ $this, 'handleWarning' ], E_WARNING );
+               // push to stack
+               $this->hadWarningErrors[] = false;
+               set_error_handler( function ( $errno, $errstr ) {
+                       // more detailed error logging
+                       $this->logger->error( $errstr );
+                       $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
+
+                       // suppress from PHP handler
+                       return true;
+               }, E_WARNING );
        }
 
        /**
@@ -805,20 +813,9 @@ class FSFileBackend extends FileBackendStore {
         * @return bool
         */
        protected function untrapWarnings() {
-               restore_error_handler(); // restore previous handler
-               return array_pop( $this->hadWarningErrors ); // pop from stack
-       }
-
-       /**
-        * @param int $errno
-        * @param string $errstr
-        * @return bool
-        * @private
-        */
-       public function handleWarning( $errno, $errstr ) {
-               $this->logger->error( $errstr ); // more detailed error logging
-               $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
-
-               return true; // suppress from PHP handler
+               // restore previous handler
+               restore_error_handler();
+               // pop from stack
+               return array_pop( $this->hadWarningErrors );
        }
 }
index e2a25fc..aa95ee4 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup FileBackend
  */
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
@@ -376,9 +377,9 @@ abstract class FileBackendStore extends FileBackend {
                unset( $params['latest'] ); // sanity
 
                // Check that the specified temp file is valid...
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( !$ok ) { // not present or not empty
                        $status->fatal( 'backend-fail-opentemp', $tmpPath );
 
@@ -714,9 +715,9 @@ abstract class FileBackendStore extends FileBackend {
        protected function doGetFileContentsMulti( array $params ) {
                $contents = [];
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                }
 
                return $contents;
index 7a11aeb..653a102 100644 (file)
@@ -19,6 +19,8 @@
  *
  * @file
  */
+
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
@@ -100,9 +102,9 @@ class HTTPFileStreamer {
                                is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
                        };
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $info = stat( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( !is_array( $info ) ) {
                        if ( $sendErrors ) {
index 3932f34..f0cbf3b 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Simulation of a backend storage in memory.
  *
@@ -70,9 +72,9 @@ class MemoryFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $data = file_get_contents( $params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $data === false ) { // source doesn't exist?
                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
 
index d6a0c6c..afd1688 100644 (file)
@@ -22,6 +22,8 @@
  * @author Russ Nelson
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
  *
@@ -326,9 +328,9 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $sha1Hash = sha1_file( $params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $sha1Hash === false ) { // source doesn't exist?
                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
 
index 206048b..961fdb9 100644 (file)
@@ -77,7 +77,7 @@ abstract class FileOp {
         * @param FileBackendStore $backend
         * @param array $params
         * @param LoggerInterface $logger PSR logger instance
-        * @throws FileBackendError
+        * @throws InvalidArgumentException
         */
        final public function __construct(
                FileBackendStore $backend, array $params, LoggerInterface $logger
index 69ae47f..5783a82 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Store a file into the backend from a file on the file system.
  * Parameters for this operation are outlined in FileBackend::doOperations().
@@ -77,9 +79,9 @@ class StoreFileOp extends FileOp {
        }
 
        protected function getSourceSha1Base36() {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $hash = sha1_file( $this->params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $hash !== false ) {
                        $hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
                }
index 553c9aa..1937e37 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Class representing a non-directory file on the file system
  *
@@ -75,9 +77,9 @@ class FSFile {
         * @return string|bool TS_MW timestamp or false on failure
         */
        public function getTimestamp() {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $timestamp = filemtime( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $timestamp !== false ) {
                        $timestamp = wfTimestamp( TS_MW, $timestamp );
                }
@@ -168,9 +170,9 @@ class FSFile {
                        return $this->sha1Base36;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $this->sha1Base36 = sha1_file( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( $this->sha1Base36 !== false ) {
                        $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
index 378dbe7..46fa5e1 100644 (file)
@@ -24,6 +24,8 @@ use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * This class is used to hold the location and do limited manipulation
  * of files stored temporarily (this will be whatever wfTempDir() returns)
@@ -38,7 +40,9 @@ class TempFSFile extends FSFile {
        protected static $pathsCollect = null;
 
        /**
-        * Do not call directly. Use TempFSFileFactory.
+        * Do not call directly. Use TempFSFileFactory
+        *
+        * @param string $path
         */
        public function __construct( $path ) {
                parent::__construct( $path );
@@ -110,9 +114,9 @@ class TempFSFile extends FSFile {
         */
        public function purge() {
                $this->canDelete = false; // done
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = unlink( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                unset( self::$pathsCollect[$this->path] );
 
@@ -172,9 +176,9 @@ class TempFSFile extends FSFile {
         */
        public static function purgeAllOnShutdown() {
                foreach ( self::$pathsCollect as $path => $unused ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        unlink( $path );
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                }
        }
 
index da60c01..73904e4 100644 (file)
@@ -478,6 +478,16 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                return INF;
        }
 
+       /**
+        * @param int $field
+        * @param int $flags
+        * @return bool
+        * @since 1.34
+        */
+       final protected function fieldHasFlags( $field, $flags ) {
+               return ( ( $field & $flags ) === $flags );
+       }
+
        /**
         * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
         *
index 9fa9a89..b1d2b6c 100644 (file)
@@ -87,7 +87,7 @@ class CachedBagOStuff extends BagOStuff {
 
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->procCache->set( $key, $value, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        $this->backend->set( $key, $value, $exptime, $flags );
                }
 
@@ -96,7 +96,7 @@ class CachedBagOStuff extends BagOStuff {
 
        public function delete( $key, $flags = 0 ) {
                $this->procCache->delete( $key, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        $this->backend->delete( $key, $flags );
                }
 
@@ -166,7 +166,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
                $this->procCache->setMulti( $data, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->setMulti( $data, $exptime, $flags );
                }
 
@@ -175,7 +176,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function deleteMulti( array $keys, $flags = 0 ) {
                $this->procCache->deleteMulti( $keys, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->deleteMulti( $keys, $flags );
                }
 
@@ -184,7 +186,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
                $this->procCache->changeTTLMulti( $keys, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->changeTTLMulti( $keys, $exptime, $flags );
                }
 
index 329e600..1242501 100644 (file)
@@ -188,7 +188,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @return bool True if the item was deleted or not found, false on failure
         */
        public function delete( $key, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) {
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) {
                        return $this->doDelete( $key, $flags );
                }
 
@@ -598,9 +598,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @since 1.24
         */
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
                        throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
                }
+
                return $this->doSetMulti( $data, $exptime, $flags );
        }
 
@@ -615,6 +616,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                foreach ( $data as $key => $value ) {
                        $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
                }
+
                return $res;
        }
 
@@ -629,9 +631,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @since 1.33
         */
        public function deleteMulti( array $keys, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
-                       throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+               if ( $this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ' got WRITE_PRUNE_SEGMENTS' );
                }
+
                return $this->doDeleteMulti( $keys, $flags );
        }
 
@@ -807,7 +810,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                $usable = true;
 
                if (
-                       ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS &&
+                       $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) &&
                        !is_int( $value ) && // avoid breaking incr()/decr()
                        is_finite( $this->segmentationSize )
                ) {
diff --git a/includes/libs/objectcache/MemcachedClient.php b/includes/libs/objectcache/MemcachedClient.php
deleted file mode 100644 (file)
index 2c40854..0000000
+++ /dev/null
@@ -1,1311 +0,0 @@
-<?php
-// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
-/**
- * Memcached client for PHP.
- *
- * +---------------------------------------------------------------------------+
- * | memcached client, PHP                                                     |
- * +---------------------------------------------------------------------------+
- * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
- * | All rights reserved.                                                      |
- * |                                                                           |
- * | Redistribution and use in source and binary forms, with or without        |
- * | modification, are permitted provided that the following conditions        |
- * | are met:                                                                  |
- * |                                                                           |
- * | 1. Redistributions of source code must retain the above copyright         |
- * |    notice, this list of conditions and the following disclaimer.          |
- * | 2. Redistributions in binary form must reproduce the above copyright      |
- * |    notice, this list of conditions and the following disclaimer in the    |
- * |    documentation and/or other materials provided with the distribution.   |
- * |                                                                           |
- * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
- * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
- * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
- * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
- * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
- * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
- * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
- * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
- * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
- * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
- * +---------------------------------------------------------------------------+
- * | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
- * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
- * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
- * |   client logic under 2-clause BSD license.                                |
- * +---------------------------------------------------------------------------+
- *
- * @file
- * $TCAnet$
- */
-
-/**
- * This is a PHP client for memcached - a distributed memory cache daemon.
- *
- * More information is available at http://www.danga.com/memcached/
- *
- * Usage example:
- *
- *     $mc = new MemcachedClient(array(
- *         'servers' => array(
- *             '127.0.0.1:10000',
- *             array( '192.0.0.1:10010', 2 ),
- *             '127.0.0.1:10020'
- *         ),
- *         'debug'   => false,
- *         'compress_threshold' => 10240,
- *         'persistent' => true
- *     ));
- *
- *     $mc->add( 'key', array( 'some', 'array' ) );
- *     $mc->replace( 'key', 'some random string' );
- *     $val = $mc->get( 'key' );
- *
- * @author Ryan T. Dean <rtdean@cytherianage.net>
- * @version 0.1.2
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-// {{{ class MemcachedClient
-/**
- * memcached client class implemented using (p)fsockopen()
- *
- * @author  Ryan T. Dean <rtdean@cytherianage.net>
- * @ingroup Cache
- */
-class MemcachedClient {
-       // {{{ properties
-       // {{{ public
-
-       // {{{ constants
-       // {{{ flags
-
-       /**
-        * Flag: indicates data is serialized
-        */
-       const SERIALIZED = 1;
-
-       /**
-        * Flag: indicates data is compressed
-        */
-       const COMPRESSED = 2;
-
-       /**
-        * Flag: indicates data is an integer
-        */
-       const INTVAL = 4;
-
-       // }}}
-
-       /**
-        * Minimum savings to store data compressed
-        */
-       const COMPRESSION_SAVINGS = 0.20;
-
-       // }}}
-
-       /**
-        * Command statistics
-        *
-        * @var array
-        * @access public
-        */
-       public $stats;
-
-       // }}}
-       // {{{ private
-
-       /**
-        * Cached Sockets that are connected
-        *
-        * @var array
-        * @access private
-        */
-       public $_cache_sock;
-
-       /**
-        * Current debug status; 0 - none to 9 - profiling
-        *
-        * @var bool
-        * @access private
-        */
-       public $_debug;
-
-       /**
-        * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
-        *
-        * @var array
-        * @access private
-        */
-       public $_host_dead;
-
-       /**
-        * Is compression available?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_have_zlib;
-
-       /**
-        * Do we want to use compression?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_compress_enable;
-
-       /**
-        * At how many bytes should we compress?
-        *
-        * @var int
-        * @access private
-        */
-       public $_compress_threshold;
-
-       /**
-        * Are we using persistent links?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_persistent;
-
-       /**
-        * If only using one server; contains ip:port to connect to
-        *
-        * @var string
-        * @access private
-        */
-       public $_single_sock;
-
-       /**
-        * Array containing ip:port or array(ip:port, weight)
-        *
-        * @var array
-        * @access private
-        */
-       public $_servers;
-
-       /**
-        * Our bit buckets
-        *
-        * @var array
-        * @access private
-        */
-       public $_buckets;
-
-       /**
-        * Total # of bit buckets we have
-        *
-        * @var int
-        * @access private
-        */
-       public $_bucketcount;
-
-       /**
-        * # of total servers we have
-        *
-        * @var int
-        * @access private
-        */
-       public $_active;
-
-       /**
-        * Stream timeout in seconds. Applies for example to fread()
-        *
-        * @var int
-        * @access private
-        */
-       public $_timeout_seconds;
-
-       /**
-        * Stream timeout in microseconds
-        *
-        * @var int
-        * @access private
-        */
-       public $_timeout_microseconds;
-
-       /**
-        * Connect timeout in seconds
-        */
-       public $_connect_timeout;
-
-       /**
-        * Number of connection attempts for each server
-        */
-       public $_connect_attempts;
-
-       /**
-        * @var LoggerInterface
-        */
-       private $_logger;
-
-       // }}}
-       // }}}
-       // {{{ methods
-       // {{{ public functions
-       // {{{ memcached()
-
-       /**
-        * Memcache initializer
-        *
-        * @param array $args Associative array of settings
-        */
-       public function __construct( $args ) {
-               $this->set_servers( $args['servers'] ?? array() );
-               $this->_debug = $args['debug'] ?? false;
-               $this->stats = array();
-               $this->_compress_threshold = $args['compress_threshold'] ?? 0;
-               $this->_persistent = $args['persistent'] ?? false;
-               $this->_compress_enable = true;
-               $this->_have_zlib = function_exists( 'gzcompress' );
-
-               $this->_cache_sock = array();
-               $this->_host_dead = array();
-
-               $this->_timeout_seconds = 0;
-               $this->_timeout_microseconds = $args['timeout'] ?? 500000;
-
-               $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
-               $this->_connect_attempts = 2;
-
-               $this->_logger = $args['logger'] ?? new NullLogger();
-       }
-
-       // }}}
-
-       /**
-        * @param mixed $value
-        * @return string|integer
-        */
-       public function serialize( $value ) {
-               return serialize( $value );
-       }
-
-       /**
-        * @param string $value
-        * @return mixed
-        */
-       public function unserialize( $value ) {
-               return unserialize( $value );
-       }
-
-       // {{{ add()
-
-       /**
-        * Adds a key/value to the memcache server if one isn't already set with
-        * that key
-        *
-        * @param string $key Key to set with data
-        * @param mixed $val Value to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of expiration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool
-        */
-       public function add( $key, $val, $exp = 0 ) {
-               return $this->_set( 'add', $key, $val, $exp );
-       }
-
-       // }}}
-       // {{{ decr()
-
-       /**
-        * Decrease a value stored on the memcache server
-        *
-        * @param string $key Key to decrease
-        * @param int $amt (optional) amount to decrease
-        *
-        * @return mixed False on failure, value on success
-        */
-       public function decr( $key, $amt = 1 ) {
-               return $this->_incrdecr( 'decr', $key, $amt );
-       }
-
-       // }}}
-       // {{{ delete()
-
-       /**
-        * Deletes a key from the server, optionally after $time
-        *
-        * @param string $key Key to delete
-        * @param int $time (optional) how long to wait before deleting
-        *
-        * @return bool True on success, false on failure
-        */
-       public function delete( $key, $time = 0 ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-
-               if ( isset( $this->stats['delete'] ) ) {
-                       $this->stats['delete']++;
-               } else {
-                       $this->stats['delete'] = 1;
-               }
-               $cmd = "delete $key $time\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-               $res = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
-               }
-
-               if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       /**
-        * Changes the TTL on a key from the server to $time
-        *
-        * @param string $key
-        * @param int $time TTL in seconds
-        *
-        * @return bool True on success, false on failure
-        */
-       public function touch( $key, $time = 0 ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-
-               if ( isset( $this->stats['touch'] ) ) {
-                       $this->stats['touch']++;
-               } else {
-                       $this->stats['touch'] = 1;
-               }
-               $cmd = "touch $key $time\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-               $res = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
-               }
-
-               if ( $res == "TOUCHED" ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ disconnect_all()
-
-       /**
-        * Disconnects all connected sockets
-        */
-       public function disconnect_all() {
-               foreach ( $this->_cache_sock as $sock ) {
-                       fclose( $sock );
-               }
-
-               $this->_cache_sock = array();
-       }
-
-       // }}}
-       // {{{ enable_compress()
-
-       /**
-        * Enable / Disable compression
-        *
-        * @param bool $enable True to enable, false to disable
-        */
-       public function enable_compress( $enable ) {
-               $this->_compress_enable = $enable;
-       }
-
-       // }}}
-       // {{{ forget_dead_hosts()
-
-       /**
-        * Forget about all of the dead hosts
-        */
-       public function forget_dead_hosts() {
-               $this->_host_dead = array();
-       }
-
-       // }}}
-       // {{{ get()
-
-       /**
-        * Retrieves the value associated with the key from the memcache server
-        *
-        * @param array|string $key key to retrieve
-        * @param float $casToken [optional]
-        *
-        * @return mixed
-        */
-       public function get( $key, &$casToken = null ) {
-               if ( $this->_debug ) {
-                       $this->_debugprint( "get($key)" );
-               }
-
-               if ( !is_array( $key ) && strval( $key ) === '' ) {
-                       $this->_debugprint( "Skipping key which equals to an empty string" );
-                       return false;
-               }
-
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-               if ( isset( $this->stats['get'] ) ) {
-                       $this->stats['get']++;
-               } else {
-                       $this->stats['get'] = 1;
-               }
-
-               $cmd = "gets $key\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-
-               $val = array();
-               $this->_load_items( $sock, $val, $casToken );
-
-               if ( $this->_debug ) {
-                       foreach ( $val as $k => $v ) {
-                               $this->_debugprint(
-                                       sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
-                       }
-               }
-
-               $value = false;
-               if ( isset( $val[$key] ) ) {
-                       $value = $val[$key];
-               }
-               return $value;
-       }
-
-       // }}}
-       // {{{ get_multi()
-
-       /**
-        * Get multiple keys from the server(s)
-        *
-        * @param array $keys Keys to retrieve
-        *
-        * @return array
-        */
-       public function get_multi( $keys ) {
-               if ( !$this->_active ) {
-                       return array();
-               }
-
-               if ( isset( $this->stats['get_multi'] ) ) {
-                       $this->stats['get_multi']++;
-               } else {
-                       $this->stats['get_multi'] = 1;
-               }
-               $sock_keys = array();
-               $socks = array();
-               foreach ( $keys as $key ) {
-                       $sock = $this->get_sock( $key );
-                       if ( !is_resource( $sock ) ) {
-                               continue;
-                       }
-                       $key = is_array( $key ) ? $key[1] : $key;
-                       if ( !isset( $sock_keys[$sock] ) ) {
-                               $sock_keys[intval( $sock )] = array();
-                               $socks[] = $sock;
-                       }
-                       $sock_keys[intval( $sock )][] = $key;
-               }
-
-               $gather = array();
-               // Send out the requests
-               foreach ( $socks as $sock ) {
-                       $cmd = 'gets';
-                       foreach ( $sock_keys[intval( $sock )] as $key ) {
-                               $cmd .= ' ' . $key;
-                       }
-                       $cmd .= "\r\n";
-
-                       if ( $this->_fwrite( $sock, $cmd ) ) {
-                               $gather[] = $sock;
-                       }
-               }
-
-               // Parse responses
-               $val = array();
-               foreach ( $gather as $sock ) {
-                       $this->_load_items( $sock, $val, $casToken );
-               }
-
-               if ( $this->_debug ) {
-                       foreach ( $val as $k => $v ) {
-                               $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
-                       }
-               }
-
-               return $val;
-       }
-
-       // }}}
-       // {{{ incr()
-
-       /**
-        * Increments $key (optionally) by $amt
-        *
-        * @param string $key Key to increment
-        * @param int $amt (optional) amount to increment
-        *
-        * @return int|null Null if the key does not exist yet (this does NOT
-        * create new mappings if the key does not exist). If the key does
-        * exist, this returns the new value for that key.
-        */
-       public function incr( $key, $amt = 1 ) {
-               return $this->_incrdecr( 'incr', $key, $amt );
-       }
-
-       // }}}
-       // {{{ replace()
-
-       /**
-        * Overwrites an existing value for key; only works if key is already set
-        *
-        * @param string $key Key to set value as
-        * @param mixed $value Value to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool
-        */
-       public function replace( $key, $value, $exp = 0 ) {
-               return $this->_set( 'replace', $key, $value, $exp );
-       }
-
-       // }}}
-       // {{{ run_command()
-
-       /**
-        * Passes through $cmd to the memcache server connected by $sock; returns
-        * output as an array (null array if no output)
-        *
-        * @param Resource $sock Socket to send command on
-        * @param string $cmd Command to run
-        *
-        * @return array Output array
-        */
-       public function run_command( $sock, $cmd ) {
-               if ( !is_resource( $sock ) ) {
-                       return array();
-               }
-
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return array();
-               }
-
-               $ret = array();
-               while ( true ) {
-                       $res = $this->_fgets( $sock );
-                       $ret[] = $res;
-                       if ( preg_match( '/^END/', $res ) ) {
-                               break;
-                       }
-                       if ( strlen( $res ) == 0 ) {
-                               break;
-                       }
-               }
-               return $ret;
-       }
-
-       // }}}
-       // {{{ set()
-
-       /**
-        * Unconditionally sets a key to a given value in the memcache.  Returns true
-        * if set successfully.
-        *
-        * @param string $key Key to set value as
-        * @param mixed $value Value to set
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool True on success
-        */
-       public function set( $key, $value, $exp = 0 ) {
-               return $this->_set( 'set', $key, $value, $exp );
-       }
-
-       // }}}
-       // {{{ cas()
-
-       /**
-        * Sets a key to a given value in the memcache if the current value still corresponds
-        * to a known, given value.  Returns true if set successfully.
-        *
-        * @param float $casToken Current known value
-        * @param string $key Key to set value as
-        * @param mixed $value Value to set
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool True on success
-        */
-       public function cas( $casToken, $key, $value, $exp = 0 ) {
-               return $this->_set( 'cas', $key, $value, $exp, $casToken );
-       }
-
-       // }}}
-       // {{{ set_compress_threshold()
-
-       /**
-        * Set the compression threshold
-        *
-        * @param int $thresh Threshold to compress if larger than
-        */
-       public function set_compress_threshold( $thresh ) {
-               $this->_compress_threshold = $thresh;
-       }
-
-       // }}}
-       // {{{ set_debug()
-
-       /**
-        * Set the debug flag
-        *
-        * @see __construct()
-        * @param bool $dbg True for debugging, false otherwise
-        */
-       public function set_debug( $dbg ) {
-               $this->_debug = $dbg;
-       }
-
-       // }}}
-       // {{{ set_servers()
-
-       /**
-        * Set the server list to distribute key gets and puts between
-        *
-        * @see __construct()
-        * @param array $list Array of servers to connect to
-        */
-       public function set_servers( $list ) {
-               $this->_servers = $list;
-               $this->_active = count( $list );
-               $this->_buckets = null;
-               $this->_bucketcount = 0;
-
-               $this->_single_sock = null;
-               if ( $this->_active == 1 ) {
-                       $this->_single_sock = $this->_servers[0];
-               }
-       }
-
-       /**
-        * Sets the timeout for new connections
-        *
-        * @param int $seconds Number of seconds
-        * @param int $microseconds Number of microseconds
-        */
-       public function set_timeout( $seconds, $microseconds ) {
-               $this->_timeout_seconds = $seconds;
-               $this->_timeout_microseconds = $microseconds;
-       }
-
-       // }}}
-       // }}}
-       // {{{ private methods
-       // {{{ _close_sock()
-
-       /**
-        * Close the specified socket
-        *
-        * @param string $sock Socket to close
-        *
-        * @access private
-        */
-       function _close_sock( $sock ) {
-               $host = array_search( $sock, $this->_cache_sock );
-               fclose( $this->_cache_sock[$host] );
-               unset( $this->_cache_sock[$host] );
-       }
-
-       // }}}
-       // {{{ _connect_sock()
-
-       /**
-        * Connects $sock to $host, timing out after $timeout
-        *
-        * @param int $sock Socket to connect
-        * @param string $host Host:IP to connect to
-        *
-        * @return bool
-        * @access private
-        */
-       function _connect_sock( &$sock, $host ) {
-               list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
-               $sock = false;
-               $timeout = $this->_connect_timeout;
-               $errno = $errstr = null;
-               for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
-                       Wikimedia\suppressWarnings();
-                       if ( $this->_persistent == 1 ) {
-                               $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
-                       } else {
-                               $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
-                       }
-                       Wikimedia\restoreWarnings();
-               }
-               if ( !$sock ) {
-                       $this->_error_log( "Error connecting to $host: $errstr" );
-                       $this->_dead_host( $host );
-                       return false;
-               }
-
-               // Initialise timeout
-               stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
-
-               // If the connection was persistent, flush the read buffer in case there
-               // was a previous incomplete request on this connection
-               if ( $this->_persistent ) {
-                       $this->_flush_read_buffer( $sock );
-               }
-               return true;
-       }
-
-       // }}}
-       // {{{ _dead_sock()
-
-       /**
-        * Marks a host as dead until 30-40 seconds in the future
-        *
-        * @param string $sock Socket to mark as dead
-        *
-        * @access private
-        */
-       function _dead_sock( $sock ) {
-               $host = array_search( $sock, $this->_cache_sock );
-               $this->_dead_host( $host );
-       }
-
-       /**
-        * @param string $host
-        */
-       function _dead_host( $host ) {
-               $ip = explode( ':', $host )[0];
-               $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
-               $this->_host_dead[$host] = $this->_host_dead[$ip];
-               unset( $this->_cache_sock[$host] );
-       }
-
-       // }}}
-       // {{{ get_sock()
-
-       /**
-        * get_sock
-        *
-        * @param string $key Key to retrieve value for;
-        *
-        * @return Resource|bool Resource on success, false on failure
-        * @access private
-        */
-       function get_sock( $key ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               if ( $this->_single_sock !== null ) {
-                       return $this->sock_to_host( $this->_single_sock );
-               }
-
-               $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
-               if ( $this->_buckets === null ) {
-                       $bu = array();
-                       foreach ( $this->_servers as $v ) {
-                               if ( is_array( $v ) ) {
-                                       for ( $i = 0; $i < $v[1]; $i++ ) {
-                                               $bu[] = $v[0];
-                                       }
-                               } else {
-                                       $bu[] = $v;
-                               }
-                       }
-                       $this->_buckets = $bu;
-                       $this->_bucketcount = count( $bu );
-               }
-
-               $realkey = is_array( $key ) ? $key[1] : $key;
-               for ( $tries = 0; $tries < 20; $tries++ ) {
-                       $host = $this->_buckets[$hv % $this->_bucketcount];
-                       $sock = $this->sock_to_host( $host );
-                       if ( is_resource( $sock ) ) {
-                               return $sock;
-                       }
-                       $hv = $this->_hashfunc( $hv . $realkey );
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ _hashfunc()
-
-       /**
-        * Creates a hash integer based on the $key
-        *
-        * @param string $key Key to hash
-        *
-        * @return int Hash value
-        * @access private
-        */
-       function _hashfunc( $key ) {
-               # Hash function must be in [0,0x7ffffff]
-               # We take the first 31 bits of the MD5 hash, which unlike the hash
-               # function used in a previous version of this client, works
-               return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
-       }
-
-       // }}}
-       // {{{ _incrdecr()
-
-       /**
-        * Perform increment/decriment on $key
-        *
-        * @param string $cmd Command to perform
-        * @param string|array $key Key to perform it on
-        * @param int $amt Amount to adjust
-        *
-        * @return int New value of $key
-        * @access private
-        */
-       function _incrdecr( $cmd, $key, $amt = 1 ) {
-               if ( !$this->_active ) {
-                       return null;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return null;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-               if ( isset( $this->stats[$cmd] ) ) {
-                       $this->stats[$cmd]++;
-               } else {
-                       $this->stats[$cmd] = 1;
-               }
-               if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
-                       return null;
-               }
-
-               $line = $this->_fgets( $sock );
-               $match = array();
-               if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
-                       return null;
-               }
-               return $match[1];
-       }
-
-       // }}}
-       // {{{ _load_items()
-
-       /**
-        * Load items into $ret from $sock
-        *
-        * @param Resource $sock Socket to read from
-        * @param array $ret returned values
-        * @param float $casToken [optional]
-        * @return bool True for success, false for failure
-        *
-        * @access private
-        */
-       function _load_items( $sock, &$ret, &$casToken = null ) {
-               $results = array();
-
-               while ( 1 ) {
-                       $decl = $this->_fgets( $sock );
-
-                       if ( $decl === false ) {
-                               /*
-                                * If nothing can be read, something is wrong because we know exactly when
-                                * to stop reading (right after "END") and we return right after that.
-                                */
-                               return false;
-                       } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
-                               /*
-                                * Read all data returned. This can be either one or multiple values.
-                                * Save all that data (in an array) to be processed later: we'll first
-                                * want to continue reading until "END" before doing anything else,
-                                * to make sure that we don't leave our client in a state where it's
-                                * output is not yet fully read.
-                                */
-                               $results[] = array(
-                                       $match[1], // rkey
-                                       $match[2], // flags
-                                       $match[3], // len
-                                       $match[4], // casToken
-                                       $this->_fread( $sock, $match[3] + 2 ), // data
-                               );
-                       } elseif ( $decl == "END" ) {
-                               if ( count( $results ) == 0 ) {
-                                       return false;
-                               }
-
-                               /**
-                                * All data has been read, time to process the data and build
-                                * meaningful return values.
-                                */
-                               foreach ( $results as $vars ) {
-                                       list( $rkey, $flags, $len, $casToken, $data ) = $vars;
-
-                                       if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
-                                               $this->_handle_error( $sock,
-                                                       'line ending missing from data block from $1' );
-                                               return false;
-                                       }
-                                       $data = substr( $data, 0, -2 );
-                                       $ret[$rkey] = $data;
-
-                                       if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
-                                               $ret[$rkey] = gzuncompress( $ret[$rkey] );
-                                       }
-
-                                       /*
-                                        * This unserialize is the exact reason that we only want to
-                                        * process data after having read until "END" (instead of doing
-                                        * this right away): "unserialize" can trigger outside code:
-                                        * in the event that $ret[$rkey] is a serialized object,
-                                        * unserializing it will trigger __wakeup() if present. If that
-                                        * function attempted to read from memcached (while we did not
-                                        * yet read "END"), these 2 calls would collide.
-                                        */
-                                       if ( $flags & self::SERIALIZED ) {
-                                               $ret[$rkey] = $this->unserialize( $ret[$rkey] );
-                                       } elseif ( $flags & self::INTVAL ) {
-                                               $ret[$rkey] = intval( $ret[$rkey] );
-                                       }
-                               }
-
-                               return true;
-                       } else {
-                               $this->_handle_error( $sock, 'Error parsing response from $1' );
-                               return false;
-                       }
-               }
-       }
-
-       // }}}
-       // {{{ _set()
-
-       /**
-        * Performs the requested storage operation to the memcache server
-        *
-        * @param string $cmd Command to perform
-        * @param string $key Key to act on
-        * @param mixed $val What we need to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        * @param float $casToken [optional]
-        *
-        * @return bool
-        * @access private
-        */
-       function _set( $cmd, $key, $val, $exp, $casToken = null ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               if ( isset( $this->stats[$cmd] ) ) {
-                       $this->stats[$cmd]++;
-               } else {
-                       $this->stats[$cmd] = 1;
-               }
-
-               $flags = 0;
-
-               if ( is_int( $val ) ) {
-                       $flags |= self::INTVAL;
-               } elseif ( !is_scalar( $val ) ) {
-                       $val = $this->serialize( $val );
-                       $flags |= self::SERIALIZED;
-                       if ( $this->_debug ) {
-                               $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
-                       }
-               }
-
-               $len = strlen( $val );
-
-               if ( $this->_have_zlib && $this->_compress_enable
-                       && $this->_compress_threshold && $len >= $this->_compress_threshold
-               ) {
-                       $c_val = gzcompress( $val, 9 );
-                       $c_len = strlen( $c_val );
-
-                       if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
-                               if ( $this->_debug ) {
-                                       $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
-                               }
-                               $val = $c_val;
-                               $len = $c_len;
-                               $flags |= self::COMPRESSED;
-                       }
-               }
-
-               $command = "$cmd $key $flags $exp $len";
-               if ( $casToken ) {
-                       $command .= " $casToken";
-               }
-
-               if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
-                       return false;
-               }
-
-               $line = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
-               }
-               if ( $line === "STORED" ) {
-                       return true;
-               } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
-                       // "Not stored" is always used as the mcrouter response with AllAsyncRoute
-                       return true;
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ sock_to_host()
-
-       /**
-        * Returns the socket for the host
-        *
-        * @param string $host Host:IP to get socket for
-        *
-        * @return Resource|bool IO Stream or false
-        * @access private
-        */
-       function sock_to_host( $host ) {
-               if ( isset( $this->_cache_sock[$host] ) ) {
-                       return $this->_cache_sock[$host];
-               }
-
-               $sock = null;
-               $now = time();
-               list( $ip, /* $port */) = explode( ':', $host );
-               if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
-                       isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
-               ) {
-                       return null;
-               }
-
-               if ( !$this->_connect_sock( $sock, $host ) ) {
-                       return null;
-               }
-
-               // Do not buffer writes
-               stream_set_write_buffer( $sock, 0 );
-
-               $this->_cache_sock[$host] = $sock;
-
-               return $this->_cache_sock[$host];
-       }
-
-       /**
-        * @param string $text
-        */
-       function _debugprint( $text ) {
-               $this->_logger->debug( $text );
-       }
-
-       /**
-        * @param string $text
-        */
-       function _error_log( $text ) {
-               $this->_logger->error( "Memcached error: $text" );
-       }
-
-       /**
-        * Write to a stream. If there is an error, mark the socket dead.
-        *
-        * @param Resource $sock The socket
-        * @param string $buf The string to write
-        * @return bool True on success, false on failure
-        */
-       function _fwrite( $sock, $buf ) {
-               $bytesWritten = 0;
-               $bufSize = strlen( $buf );
-               while ( $bytesWritten < $bufSize ) {
-                       $result = fwrite( $sock, $buf );
-                       $data = stream_get_meta_data( $sock );
-                       if ( $data['timed_out'] ) {
-                               $this->_handle_error( $sock, 'timeout writing to $1' );
-                               return false;
-                       }
-                       // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
-                       if ( $result === false || $result === 0 ) {
-                               $this->_handle_error( $sock, 'error writing to $1' );
-                               return false;
-                       }
-                       $bytesWritten += $result;
-               }
-
-               return true;
-       }
-
-       /**
-        * Handle an I/O error. Mark the socket dead and log an error.
-        *
-        * @param Resource $sock
-        * @param string $msg
-        */
-       function _handle_error( $sock, $msg ) {
-               $peer = stream_socket_get_name( $sock, true /** remote **/ );
-               if ( strval( $peer ) === '' ) {
-                       $peer = array_search( $sock, $this->_cache_sock );
-                       if ( $peer === false ) {
-                               $peer = '[unknown host]';
-                       }
-               }
-               $msg = str_replace( '$1', $peer, $msg );
-               $this->_error_log( "$msg" );
-               $this->_dead_sock( $sock );
-       }
-
-       /**
-        * Read the specified number of bytes from a stream. If there is an error,
-        * mark the socket dead.
-        *
-        * @param Resource $sock The socket
-        * @param int $len The number of bytes to read
-        * @return string|bool The string on success, false on failure.
-        */
-       function _fread( $sock, $len ) {
-               $buf = '';
-               while ( $len > 0 ) {
-                       $result = fread( $sock, $len );
-                       $data = stream_get_meta_data( $sock );
-                       if ( $data['timed_out'] ) {
-                               $this->_handle_error( $sock, 'timeout reading from $1' );
-                               return false;
-                       }
-                       if ( $result === false ) {
-                               $this->_handle_error( $sock, 'error reading buffer from $1' );
-                               return false;
-                       }
-                       if ( $result === '' ) {
-                               // This will happen if the remote end of the socket is shut down
-                               $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
-                               return false;
-                       }
-                       $len -= strlen( $result );
-                       $buf .= $result;
-               }
-               return $buf;
-       }
-
-       /**
-        * Read a line from a stream. If there is an error, mark the socket dead.
-        * The \r\n line ending is stripped from the response.
-        *
-        * @param Resource $sock The socket
-        * @return string|bool The string on success, false on failure
-        */
-       function _fgets( $sock ) {
-               $result = fgets( $sock );
-               // fgets() may return a partial line if there is a select timeout after
-               // a successful recv(), so we have to check for a timeout even if we
-               // got a string response.
-               $data = stream_get_meta_data( $sock );
-               if ( $data['timed_out'] ) {
-                       $this->_handle_error( $sock, 'timeout reading line from $1' );
-                       return false;
-               }
-               if ( $result === false ) {
-                       $this->_handle_error( $sock, 'error reading line from $1' );
-                       return false;
-               }
-               if ( substr( $result, -2 ) === "\r\n" ) {
-                       $result = substr( $result, 0, -2 );
-               } elseif ( substr( $result, -1 ) === "\n" ) {
-                       $result = substr( $result, 0, -1 );
-               } else {
-                       $this->_handle_error( $sock, 'line ending missing in response from $1' );
-                       return false;
-               }
-               return $result;
-       }
-
-       /**
-        * Flush the read buffer of a stream
-        * @param Resource $f
-        */
-       function _flush_read_buffer( $f ) {
-               if ( !is_resource( $f ) ) {
-                       return;
-               }
-               $r = array( $f );
-               $w = null;
-               $e = null;
-               $n = stream_select( $r, $w, $e, 0, 0 );
-               while ( $n == 1 && !feof( $f ) ) {
-                       fread( $f, 1024 );
-                       $r = array( $f );
-                       $w = null;
-                       $e = null;
-                       $n = stream_select( $r, $w, $e, 0, 0 );
-               }
-       }
-
-       // }}}
-       // }}}
-       // }}}
-}
-
-// }}}
index 3df483d..12c1a7a 100644 (file)
@@ -332,7 +332,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
                // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
                // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852
-               if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) {
                        $client = $this->acquireAsyncClient();
                        $result = $client->setMulti( $data, $exptime );
                        $this->releaseAsyncClient( $client );
@@ -352,7 +352,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
                // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
                // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852
-               if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) {
                        $client = $this->acquireAsyncClient();
                        $resultArray = $client->deleteMulti( $keys ) ?: [];
                        $this->releaseAsyncClient( $client );
index d150880..31d73e1 100644 (file)
@@ -106,7 +106,7 @@ class MultiWriteBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) {
+               if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
                        // If the latest write was a delete(), we do NOT want to fallback
                        // to the other tiers and possibly see the old value. Also, this
                        // is used by merge(), which only needs to hit the primary.
@@ -125,7 +125,7 @@ class MultiWriteBagOStuff extends BagOStuff {
 
                if ( $value !== false
                        && $missIndexes
-                       && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
+                       && $this->fieldHasFlags( $flags, self::READ_VERIFIED )
                ) {
                        // Backfill the value to the higher (and often faster/smaller) cache tiers
                        $this->doWrite(
@@ -346,7 +346,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        protected function usesAsyncWritesGivenFlags( $flags ) {
-               return ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) ? false : $this->asyncWrites;
+               return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
        }
 
        public function makeKeyInternal( $keyspace, $args ) {
index 504d515..e3ced0d 100644 (file)
@@ -76,7 +76,7 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
+               return $this->fieldHasFlags( $flags, self::READ_LATEST )
                        ? $this->writeStore->get( $key, $flags )
                        : $this->readStore->get( $key, $flags );
        }
@@ -118,7 +118,7 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
-               return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
+               return $this->fieldHasFlags( $flags, self::READ_LATEST )
                        ? $this->writeStore->getMulti( $keys, $flags )
                        : $this->readStore->getMulti( $keys, $flags );
        }
diff --git a/includes/libs/objectcache/utils/MemcachedClient.php b/includes/libs/objectcache/utils/MemcachedClient.php
new file mode 100644 (file)
index 0000000..2c40854
--- /dev/null
@@ -0,0 +1,1311 @@
+<?php
+// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
+/**
+ * Memcached client for PHP.
+ *
+ * +---------------------------------------------------------------------------+
+ * | memcached client, PHP                                                     |
+ * +---------------------------------------------------------------------------+
+ * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
+ * | All rights reserved.                                                      |
+ * |                                                                           |
+ * | Redistribution and use in source and binary forms, with or without        |
+ * | modification, are permitted provided that the following conditions        |
+ * | are met:                                                                  |
+ * |                                                                           |
+ * | 1. Redistributions of source code must retain the above copyright         |
+ * |    notice, this list of conditions and the following disclaimer.          |
+ * | 2. Redistributions in binary form must reproduce the above copyright      |
+ * |    notice, this list of conditions and the following disclaimer in the    |
+ * |    documentation and/or other materials provided with the distribution.   |
+ * |                                                                           |
+ * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
+ * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+ * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
+ * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
+ * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
+ * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+ * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
+ * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
+ * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
+ * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
+ * +---------------------------------------------------------------------------+
+ * | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
+ * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
+ * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
+ * |   client logic under 2-clause BSD license.                                |
+ * +---------------------------------------------------------------------------+
+ *
+ * @file
+ * $TCAnet$
+ */
+
+/**
+ * This is a PHP client for memcached - a distributed memory cache daemon.
+ *
+ * More information is available at http://www.danga.com/memcached/
+ *
+ * Usage example:
+ *
+ *     $mc = new MemcachedClient(array(
+ *         'servers' => array(
+ *             '127.0.0.1:10000',
+ *             array( '192.0.0.1:10010', 2 ),
+ *             '127.0.0.1:10020'
+ *         ),
+ *         'debug'   => false,
+ *         'compress_threshold' => 10240,
+ *         'persistent' => true
+ *     ));
+ *
+ *     $mc->add( 'key', array( 'some', 'array' ) );
+ *     $mc->replace( 'key', 'some random string' );
+ *     $val = $mc->get( 'key' );
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @version 0.1.2
+ */
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+// {{{ class MemcachedClient
+/**
+ * memcached client class implemented using (p)fsockopen()
+ *
+ * @author  Ryan T. Dean <rtdean@cytherianage.net>
+ * @ingroup Cache
+ */
+class MemcachedClient {
+       // {{{ properties
+       // {{{ public
+
+       // {{{ constants
+       // {{{ flags
+
+       /**
+        * Flag: indicates data is serialized
+        */
+       const SERIALIZED = 1;
+
+       /**
+        * Flag: indicates data is compressed
+        */
+       const COMPRESSED = 2;
+
+       /**
+        * Flag: indicates data is an integer
+        */
+       const INTVAL = 4;
+
+       // }}}
+
+       /**
+        * Minimum savings to store data compressed
+        */
+       const COMPRESSION_SAVINGS = 0.20;
+
+       // }}}
+
+       /**
+        * Command statistics
+        *
+        * @var array
+        * @access public
+        */
+       public $stats;
+
+       // }}}
+       // {{{ private
+
+       /**
+        * Cached Sockets that are connected
+        *
+        * @var array
+        * @access private
+        */
+       public $_cache_sock;
+
+       /**
+        * Current debug status; 0 - none to 9 - profiling
+        *
+        * @var bool
+        * @access private
+        */
+       public $_debug;
+
+       /**
+        * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
+        *
+        * @var array
+        * @access private
+        */
+       public $_host_dead;
+
+       /**
+        * Is compression available?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_have_zlib;
+
+       /**
+        * Do we want to use compression?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_compress_enable;
+
+       /**
+        * At how many bytes should we compress?
+        *
+        * @var int
+        * @access private
+        */
+       public $_compress_threshold;
+
+       /**
+        * Are we using persistent links?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_persistent;
+
+       /**
+        * If only using one server; contains ip:port to connect to
+        *
+        * @var string
+        * @access private
+        */
+       public $_single_sock;
+
+       /**
+        * Array containing ip:port or array(ip:port, weight)
+        *
+        * @var array
+        * @access private
+        */
+       public $_servers;
+
+       /**
+        * Our bit buckets
+        *
+        * @var array
+        * @access private
+        */
+       public $_buckets;
+
+       /**
+        * Total # of bit buckets we have
+        *
+        * @var int
+        * @access private
+        */
+       public $_bucketcount;
+
+       /**
+        * # of total servers we have
+        *
+        * @var int
+        * @access private
+        */
+       public $_active;
+
+       /**
+        * Stream timeout in seconds. Applies for example to fread()
+        *
+        * @var int
+        * @access private
+        */
+       public $_timeout_seconds;
+
+       /**
+        * Stream timeout in microseconds
+        *
+        * @var int
+        * @access private
+        */
+       public $_timeout_microseconds;
+
+       /**
+        * Connect timeout in seconds
+        */
+       public $_connect_timeout;
+
+       /**
+        * Number of connection attempts for each server
+        */
+       public $_connect_attempts;
+
+       /**
+        * @var LoggerInterface
+        */
+       private $_logger;
+
+       // }}}
+       // }}}
+       // {{{ methods
+       // {{{ public functions
+       // {{{ memcached()
+
+       /**
+        * Memcache initializer
+        *
+        * @param array $args Associative array of settings
+        */
+       public function __construct( $args ) {
+               $this->set_servers( $args['servers'] ?? array() );
+               $this->_debug = $args['debug'] ?? false;
+               $this->stats = array();
+               $this->_compress_threshold = $args['compress_threshold'] ?? 0;
+               $this->_persistent = $args['persistent'] ?? false;
+               $this->_compress_enable = true;
+               $this->_have_zlib = function_exists( 'gzcompress' );
+
+               $this->_cache_sock = array();
+               $this->_host_dead = array();
+
+               $this->_timeout_seconds = 0;
+               $this->_timeout_microseconds = $args['timeout'] ?? 500000;
+
+               $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
+               $this->_connect_attempts = 2;
+
+               $this->_logger = $args['logger'] ?? new NullLogger();
+       }
+
+       // }}}
+
+       /**
+        * @param mixed $value
+        * @return string|integer
+        */
+       public function serialize( $value ) {
+               return serialize( $value );
+       }
+
+       /**
+        * @param string $value
+        * @return mixed
+        */
+       public function unserialize( $value ) {
+               return unserialize( $value );
+       }
+
+       // {{{ add()
+
+       /**
+        * Adds a key/value to the memcache server if one isn't already set with
+        * that key
+        *
+        * @param string $key Key to set with data
+        * @param mixed $val Value to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of expiration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool
+        */
+       public function add( $key, $val, $exp = 0 ) {
+               return $this->_set( 'add', $key, $val, $exp );
+       }
+
+       // }}}
+       // {{{ decr()
+
+       /**
+        * Decrease a value stored on the memcache server
+        *
+        * @param string $key Key to decrease
+        * @param int $amt (optional) amount to decrease
+        *
+        * @return mixed False on failure, value on success
+        */
+       public function decr( $key, $amt = 1 ) {
+               return $this->_incrdecr( 'decr', $key, $amt );
+       }
+
+       // }}}
+       // {{{ delete()
+
+       /**
+        * Deletes a key from the server, optionally after $time
+        *
+        * @param string $key Key to delete
+        * @param int $time (optional) how long to wait before deleting
+        *
+        * @return bool True on success, false on failure
+        */
+       public function delete( $key, $time = 0 ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+
+               if ( isset( $this->stats['delete'] ) ) {
+                       $this->stats['delete']++;
+               } else {
+                       $this->stats['delete'] = 1;
+               }
+               $cmd = "delete $key $time\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+               $res = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
+               }
+
+               if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Changes the TTL on a key from the server to $time
+        *
+        * @param string $key
+        * @param int $time TTL in seconds
+        *
+        * @return bool True on success, false on failure
+        */
+       public function touch( $key, $time = 0 ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+
+               if ( isset( $this->stats['touch'] ) ) {
+                       $this->stats['touch']++;
+               } else {
+                       $this->stats['touch'] = 1;
+               }
+               $cmd = "touch $key $time\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+               $res = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
+               }
+
+               if ( $res == "TOUCHED" ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ disconnect_all()
+
+       /**
+        * Disconnects all connected sockets
+        */
+       public function disconnect_all() {
+               foreach ( $this->_cache_sock as $sock ) {
+                       fclose( $sock );
+               }
+
+               $this->_cache_sock = array();
+       }
+
+       // }}}
+       // {{{ enable_compress()
+
+       /**
+        * Enable / Disable compression
+        *
+        * @param bool $enable True to enable, false to disable
+        */
+       public function enable_compress( $enable ) {
+               $this->_compress_enable = $enable;
+       }
+
+       // }}}
+       // {{{ forget_dead_hosts()
+
+       /**
+        * Forget about all of the dead hosts
+        */
+       public function forget_dead_hosts() {
+               $this->_host_dead = array();
+       }
+
+       // }}}
+       // {{{ get()
+
+       /**
+        * Retrieves the value associated with the key from the memcache server
+        *
+        * @param array|string $key key to retrieve
+        * @param float $casToken [optional]
+        *
+        * @return mixed
+        */
+       public function get( $key, &$casToken = null ) {
+               if ( $this->_debug ) {
+                       $this->_debugprint( "get($key)" );
+               }
+
+               if ( !is_array( $key ) && strval( $key ) === '' ) {
+                       $this->_debugprint( "Skipping key which equals to an empty string" );
+                       return false;
+               }
+
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+               if ( isset( $this->stats['get'] ) ) {
+                       $this->stats['get']++;
+               } else {
+                       $this->stats['get'] = 1;
+               }
+
+               $cmd = "gets $key\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+
+               $val = array();
+               $this->_load_items( $sock, $val, $casToken );
+
+               if ( $this->_debug ) {
+                       foreach ( $val as $k => $v ) {
+                               $this->_debugprint(
+                                       sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
+                       }
+               }
+
+               $value = false;
+               if ( isset( $val[$key] ) ) {
+                       $value = $val[$key];
+               }
+               return $value;
+       }
+
+       // }}}
+       // {{{ get_multi()
+
+       /**
+        * Get multiple keys from the server(s)
+        *
+        * @param array $keys Keys to retrieve
+        *
+        * @return array
+        */
+       public function get_multi( $keys ) {
+               if ( !$this->_active ) {
+                       return array();
+               }
+
+               if ( isset( $this->stats['get_multi'] ) ) {
+                       $this->stats['get_multi']++;
+               } else {
+                       $this->stats['get_multi'] = 1;
+               }
+               $sock_keys = array();
+               $socks = array();
+               foreach ( $keys as $key ) {
+                       $sock = $this->get_sock( $key );
+                       if ( !is_resource( $sock ) ) {
+                               continue;
+                       }
+                       $key = is_array( $key ) ? $key[1] : $key;
+                       if ( !isset( $sock_keys[$sock] ) ) {
+                               $sock_keys[intval( $sock )] = array();
+                               $socks[] = $sock;
+                       }
+                       $sock_keys[intval( $sock )][] = $key;
+               }
+
+               $gather = array();
+               // Send out the requests
+               foreach ( $socks as $sock ) {
+                       $cmd = 'gets';
+                       foreach ( $sock_keys[intval( $sock )] as $key ) {
+                               $cmd .= ' ' . $key;
+                       }
+                       $cmd .= "\r\n";
+
+                       if ( $this->_fwrite( $sock, $cmd ) ) {
+                               $gather[] = $sock;
+                       }
+               }
+
+               // Parse responses
+               $val = array();
+               foreach ( $gather as $sock ) {
+                       $this->_load_items( $sock, $val, $casToken );
+               }
+
+               if ( $this->_debug ) {
+                       foreach ( $val as $k => $v ) {
+                               $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
+                       }
+               }
+
+               return $val;
+       }
+
+       // }}}
+       // {{{ incr()
+
+       /**
+        * Increments $key (optionally) by $amt
+        *
+        * @param string $key Key to increment
+        * @param int $amt (optional) amount to increment
+        *
+        * @return int|null Null if the key does not exist yet (this does NOT
+        * create new mappings if the key does not exist). If the key does
+        * exist, this returns the new value for that key.
+        */
+       public function incr( $key, $amt = 1 ) {
+               return $this->_incrdecr( 'incr', $key, $amt );
+       }
+
+       // }}}
+       // {{{ replace()
+
+       /**
+        * Overwrites an existing value for key; only works if key is already set
+        *
+        * @param string $key Key to set value as
+        * @param mixed $value Value to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool
+        */
+       public function replace( $key, $value, $exp = 0 ) {
+               return $this->_set( 'replace', $key, $value, $exp );
+       }
+
+       // }}}
+       // {{{ run_command()
+
+       /**
+        * Passes through $cmd to the memcache server connected by $sock; returns
+        * output as an array (null array if no output)
+        *
+        * @param Resource $sock Socket to send command on
+        * @param string $cmd Command to run
+        *
+        * @return array Output array
+        */
+       public function run_command( $sock, $cmd ) {
+               if ( !is_resource( $sock ) ) {
+                       return array();
+               }
+
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return array();
+               }
+
+               $ret = array();
+               while ( true ) {
+                       $res = $this->_fgets( $sock );
+                       $ret[] = $res;
+                       if ( preg_match( '/^END/', $res ) ) {
+                               break;
+                       }
+                       if ( strlen( $res ) == 0 ) {
+                               break;
+                       }
+               }
+               return $ret;
+       }
+
+       // }}}
+       // {{{ set()
+
+       /**
+        * Unconditionally sets a key to a given value in the memcache.  Returns true
+        * if set successfully.
+        *
+        * @param string $key Key to set value as
+        * @param mixed $value Value to set
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool True on success
+        */
+       public function set( $key, $value, $exp = 0 ) {
+               return $this->_set( 'set', $key, $value, $exp );
+       }
+
+       // }}}
+       // {{{ cas()
+
+       /**
+        * Sets a key to a given value in the memcache if the current value still corresponds
+        * to a known, given value.  Returns true if set successfully.
+        *
+        * @param float $casToken Current known value
+        * @param string $key Key to set value as
+        * @param mixed $value Value to set
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool True on success
+        */
+       public function cas( $casToken, $key, $value, $exp = 0 ) {
+               return $this->_set( 'cas', $key, $value, $exp, $casToken );
+       }
+
+       // }}}
+       // {{{ set_compress_threshold()
+
+       /**
+        * Set the compression threshold
+        *
+        * @param int $thresh Threshold to compress if larger than
+        */
+       public function set_compress_threshold( $thresh ) {
+               $this->_compress_threshold = $thresh;
+       }
+
+       // }}}
+       // {{{ set_debug()
+
+       /**
+        * Set the debug flag
+        *
+        * @see __construct()
+        * @param bool $dbg True for debugging, false otherwise
+        */
+       public function set_debug( $dbg ) {
+               $this->_debug = $dbg;
+       }
+
+       // }}}
+       // {{{ set_servers()
+
+       /**
+        * Set the server list to distribute key gets and puts between
+        *
+        * @see __construct()
+        * @param array $list Array of servers to connect to
+        */
+       public function set_servers( $list ) {
+               $this->_servers = $list;
+               $this->_active = count( $list );
+               $this->_buckets = null;
+               $this->_bucketcount = 0;
+
+               $this->_single_sock = null;
+               if ( $this->_active == 1 ) {
+                       $this->_single_sock = $this->_servers[0];
+               }
+       }
+
+       /**
+        * Sets the timeout for new connections
+        *
+        * @param int $seconds Number of seconds
+        * @param int $microseconds Number of microseconds
+        */
+       public function set_timeout( $seconds, $microseconds ) {
+               $this->_timeout_seconds = $seconds;
+               $this->_timeout_microseconds = $microseconds;
+       }
+
+       // }}}
+       // }}}
+       // {{{ private methods
+       // {{{ _close_sock()
+
+       /**
+        * Close the specified socket
+        *
+        * @param string $sock Socket to close
+        *
+        * @access private
+        */
+       function _close_sock( $sock ) {
+               $host = array_search( $sock, $this->_cache_sock );
+               fclose( $this->_cache_sock[$host] );
+               unset( $this->_cache_sock[$host] );
+       }
+
+       // }}}
+       // {{{ _connect_sock()
+
+       /**
+        * Connects $sock to $host, timing out after $timeout
+        *
+        * @param int $sock Socket to connect
+        * @param string $host Host:IP to connect to
+        *
+        * @return bool
+        * @access private
+        */
+       function _connect_sock( &$sock, $host ) {
+               list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
+               $sock = false;
+               $timeout = $this->_connect_timeout;
+               $errno = $errstr = null;
+               for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
+                       Wikimedia\suppressWarnings();
+                       if ( $this->_persistent == 1 ) {
+                               $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
+                       } else {
+                               $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
+                       }
+                       Wikimedia\restoreWarnings();
+               }
+               if ( !$sock ) {
+                       $this->_error_log( "Error connecting to $host: $errstr" );
+                       $this->_dead_host( $host );
+                       return false;
+               }
+
+               // Initialise timeout
+               stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
+
+               // If the connection was persistent, flush the read buffer in case there
+               // was a previous incomplete request on this connection
+               if ( $this->_persistent ) {
+                       $this->_flush_read_buffer( $sock );
+               }
+               return true;
+       }
+
+       // }}}
+       // {{{ _dead_sock()
+
+       /**
+        * Marks a host as dead until 30-40 seconds in the future
+        *
+        * @param string $sock Socket to mark as dead
+        *
+        * @access private
+        */
+       function _dead_sock( $sock ) {
+               $host = array_search( $sock, $this->_cache_sock );
+               $this->_dead_host( $host );
+       }
+
+       /**
+        * @param string $host
+        */
+       function _dead_host( $host ) {
+               $ip = explode( ':', $host )[0];
+               $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
+               $this->_host_dead[$host] = $this->_host_dead[$ip];
+               unset( $this->_cache_sock[$host] );
+       }
+
+       // }}}
+       // {{{ get_sock()
+
+       /**
+        * get_sock
+        *
+        * @param string $key Key to retrieve value for;
+        *
+        * @return Resource|bool Resource on success, false on failure
+        * @access private
+        */
+       function get_sock( $key ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               if ( $this->_single_sock !== null ) {
+                       return $this->sock_to_host( $this->_single_sock );
+               }
+
+               $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
+               if ( $this->_buckets === null ) {
+                       $bu = array();
+                       foreach ( $this->_servers as $v ) {
+                               if ( is_array( $v ) ) {
+                                       for ( $i = 0; $i < $v[1]; $i++ ) {
+                                               $bu[] = $v[0];
+                                       }
+                               } else {
+                                       $bu[] = $v;
+                               }
+                       }
+                       $this->_buckets = $bu;
+                       $this->_bucketcount = count( $bu );
+               }
+
+               $realkey = is_array( $key ) ? $key[1] : $key;
+               for ( $tries = 0; $tries < 20; $tries++ ) {
+                       $host = $this->_buckets[$hv % $this->_bucketcount];
+                       $sock = $this->sock_to_host( $host );
+                       if ( is_resource( $sock ) ) {
+                               return $sock;
+                       }
+                       $hv = $this->_hashfunc( $hv . $realkey );
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ _hashfunc()
+
+       /**
+        * Creates a hash integer based on the $key
+        *
+        * @param string $key Key to hash
+        *
+        * @return int Hash value
+        * @access private
+        */
+       function _hashfunc( $key ) {
+               # Hash function must be in [0,0x7ffffff]
+               # We take the first 31 bits of the MD5 hash, which unlike the hash
+               # function used in a previous version of this client, works
+               return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
+       }
+
+       // }}}
+       // {{{ _incrdecr()
+
+       /**
+        * Perform increment/decriment on $key
+        *
+        * @param string $cmd Command to perform
+        * @param string|array $key Key to perform it on
+        * @param int $amt Amount to adjust
+        *
+        * @return int New value of $key
+        * @access private
+        */
+       function _incrdecr( $cmd, $key, $amt = 1 ) {
+               if ( !$this->_active ) {
+                       return null;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return null;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+               if ( isset( $this->stats[$cmd] ) ) {
+                       $this->stats[$cmd]++;
+               } else {
+                       $this->stats[$cmd] = 1;
+               }
+               if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
+                       return null;
+               }
+
+               $line = $this->_fgets( $sock );
+               $match = array();
+               if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
+                       return null;
+               }
+               return $match[1];
+       }
+
+       // }}}
+       // {{{ _load_items()
+
+       /**
+        * Load items into $ret from $sock
+        *
+        * @param Resource $sock Socket to read from
+        * @param array $ret returned values
+        * @param float $casToken [optional]
+        * @return bool True for success, false for failure
+        *
+        * @access private
+        */
+       function _load_items( $sock, &$ret, &$casToken = null ) {
+               $results = array();
+
+               while ( 1 ) {
+                       $decl = $this->_fgets( $sock );
+
+                       if ( $decl === false ) {
+                               /*
+                                * If nothing can be read, something is wrong because we know exactly when
+                                * to stop reading (right after "END") and we return right after that.
+                                */
+                               return false;
+                       } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
+                               /*
+                                * Read all data returned. This can be either one or multiple values.
+                                * Save all that data (in an array) to be processed later: we'll first
+                                * want to continue reading until "END" before doing anything else,
+                                * to make sure that we don't leave our client in a state where it's
+                                * output is not yet fully read.
+                                */
+                               $results[] = array(
+                                       $match[1], // rkey
+                                       $match[2], // flags
+                                       $match[3], // len
+                                       $match[4], // casToken
+                                       $this->_fread( $sock, $match[3] + 2 ), // data
+                               );
+                       } elseif ( $decl == "END" ) {
+                               if ( count( $results ) == 0 ) {
+                                       return false;
+                               }
+
+                               /**
+                                * All data has been read, time to process the data and build
+                                * meaningful return values.
+                                */
+                               foreach ( $results as $vars ) {
+                                       list( $rkey, $flags, $len, $casToken, $data ) = $vars;
+
+                                       if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
+                                               $this->_handle_error( $sock,
+                                                       'line ending missing from data block from $1' );
+                                               return false;
+                                       }
+                                       $data = substr( $data, 0, -2 );
+                                       $ret[$rkey] = $data;
+
+                                       if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
+                                               $ret[$rkey] = gzuncompress( $ret[$rkey] );
+                                       }
+
+                                       /*
+                                        * This unserialize is the exact reason that we only want to
+                                        * process data after having read until "END" (instead of doing
+                                        * this right away): "unserialize" can trigger outside code:
+                                        * in the event that $ret[$rkey] is a serialized object,
+                                        * unserializing it will trigger __wakeup() if present. If that
+                                        * function attempted to read from memcached (while we did not
+                                        * yet read "END"), these 2 calls would collide.
+                                        */
+                                       if ( $flags & self::SERIALIZED ) {
+                                               $ret[$rkey] = $this->unserialize( $ret[$rkey] );
+                                       } elseif ( $flags & self::INTVAL ) {
+                                               $ret[$rkey] = intval( $ret[$rkey] );
+                                       }
+                               }
+
+                               return true;
+                       } else {
+                               $this->_handle_error( $sock, 'Error parsing response from $1' );
+                               return false;
+                       }
+               }
+       }
+
+       // }}}
+       // {{{ _set()
+
+       /**
+        * Performs the requested storage operation to the memcache server
+        *
+        * @param string $cmd Command to perform
+        * @param string $key Key to act on
+        * @param mixed $val What we need to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        * @param float $casToken [optional]
+        *
+        * @return bool
+        * @access private
+        */
+       function _set( $cmd, $key, $val, $exp, $casToken = null ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               if ( isset( $this->stats[$cmd] ) ) {
+                       $this->stats[$cmd]++;
+               } else {
+                       $this->stats[$cmd] = 1;
+               }
+
+               $flags = 0;
+
+               if ( is_int( $val ) ) {
+                       $flags |= self::INTVAL;
+               } elseif ( !is_scalar( $val ) ) {
+                       $val = $this->serialize( $val );
+                       $flags |= self::SERIALIZED;
+                       if ( $this->_debug ) {
+                               $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
+                       }
+               }
+
+               $len = strlen( $val );
+
+               if ( $this->_have_zlib && $this->_compress_enable
+                       && $this->_compress_threshold && $len >= $this->_compress_threshold
+               ) {
+                       $c_val = gzcompress( $val, 9 );
+                       $c_len = strlen( $c_val );
+
+                       if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
+                               if ( $this->_debug ) {
+                                       $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
+                               }
+                               $val = $c_val;
+                               $len = $c_len;
+                               $flags |= self::COMPRESSED;
+                       }
+               }
+
+               $command = "$cmd $key $flags $exp $len";
+               if ( $casToken ) {
+                       $command .= " $casToken";
+               }
+
+               if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
+                       return false;
+               }
+
+               $line = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
+               }
+               if ( $line === "STORED" ) {
+                       return true;
+               } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
+                       // "Not stored" is always used as the mcrouter response with AllAsyncRoute
+                       return true;
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ sock_to_host()
+
+       /**
+        * Returns the socket for the host
+        *
+        * @param string $host Host:IP to get socket for
+        *
+        * @return Resource|bool IO Stream or false
+        * @access private
+        */
+       function sock_to_host( $host ) {
+               if ( isset( $this->_cache_sock[$host] ) ) {
+                       return $this->_cache_sock[$host];
+               }
+
+               $sock = null;
+               $now = time();
+               list( $ip, /* $port */) = explode( ':', $host );
+               if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
+                       isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
+               ) {
+                       return null;
+               }
+
+               if ( !$this->_connect_sock( $sock, $host ) ) {
+                       return null;
+               }
+
+               // Do not buffer writes
+               stream_set_write_buffer( $sock, 0 );
+
+               $this->_cache_sock[$host] = $sock;
+
+               return $this->_cache_sock[$host];
+       }
+
+       /**
+        * @param string $text
+        */
+       function _debugprint( $text ) {
+               $this->_logger->debug( $text );
+       }
+
+       /**
+        * @param string $text
+        */
+       function _error_log( $text ) {
+               $this->_logger->error( "Memcached error: $text" );
+       }
+
+       /**
+        * Write to a stream. If there is an error, mark the socket dead.
+        *
+        * @param Resource $sock The socket
+        * @param string $buf The string to write
+        * @return bool True on success, false on failure
+        */
+       function _fwrite( $sock, $buf ) {
+               $bytesWritten = 0;
+               $bufSize = strlen( $buf );
+               while ( $bytesWritten < $bufSize ) {
+                       $result = fwrite( $sock, $buf );
+                       $data = stream_get_meta_data( $sock );
+                       if ( $data['timed_out'] ) {
+                               $this->_handle_error( $sock, 'timeout writing to $1' );
+                               return false;
+                       }
+                       // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
+                       if ( $result === false || $result === 0 ) {
+                               $this->_handle_error( $sock, 'error writing to $1' );
+                               return false;
+                       }
+                       $bytesWritten += $result;
+               }
+
+               return true;
+       }
+
+       /**
+        * Handle an I/O error. Mark the socket dead and log an error.
+        *
+        * @param Resource $sock
+        * @param string $msg
+        */
+       function _handle_error( $sock, $msg ) {
+               $peer = stream_socket_get_name( $sock, true /** remote **/ );
+               if ( strval( $peer ) === '' ) {
+                       $peer = array_search( $sock, $this->_cache_sock );
+                       if ( $peer === false ) {
+                               $peer = '[unknown host]';
+                       }
+               }
+               $msg = str_replace( '$1', $peer, $msg );
+               $this->_error_log( "$msg" );
+               $this->_dead_sock( $sock );
+       }
+
+       /**
+        * Read the specified number of bytes from a stream. If there is an error,
+        * mark the socket dead.
+        *
+        * @param Resource $sock The socket
+        * @param int $len The number of bytes to read
+        * @return string|bool The string on success, false on failure.
+        */
+       function _fread( $sock, $len ) {
+               $buf = '';
+               while ( $len > 0 ) {
+                       $result = fread( $sock, $len );
+                       $data = stream_get_meta_data( $sock );
+                       if ( $data['timed_out'] ) {
+                               $this->_handle_error( $sock, 'timeout reading from $1' );
+                               return false;
+                       }
+                       if ( $result === false ) {
+                               $this->_handle_error( $sock, 'error reading buffer from $1' );
+                               return false;
+                       }
+                       if ( $result === '' ) {
+                               // This will happen if the remote end of the socket is shut down
+                               $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
+                               return false;
+                       }
+                       $len -= strlen( $result );
+                       $buf .= $result;
+               }
+               return $buf;
+       }
+
+       /**
+        * Read a line from a stream. If there is an error, mark the socket dead.
+        * The \r\n line ending is stripped from the response.
+        *
+        * @param Resource $sock The socket
+        * @return string|bool The string on success, false on failure
+        */
+       function _fgets( $sock ) {
+               $result = fgets( $sock );
+               // fgets() may return a partial line if there is a select timeout after
+               // a successful recv(), so we have to check for a timeout even if we
+               // got a string response.
+               $data = stream_get_meta_data( $sock );
+               if ( $data['timed_out'] ) {
+                       $this->_handle_error( $sock, 'timeout reading line from $1' );
+                       return false;
+               }
+               if ( $result === false ) {
+                       $this->_handle_error( $sock, 'error reading line from $1' );
+                       return false;
+               }
+               if ( substr( $result, -2 ) === "\r\n" ) {
+                       $result = substr( $result, 0, -2 );
+               } elseif ( substr( $result, -1 ) === "\n" ) {
+                       $result = substr( $result, 0, -1 );
+               } else {
+                       $this->_handle_error( $sock, 'line ending missing in response from $1' );
+                       return false;
+               }
+               return $result;
+       }
+
+       /**
+        * Flush the read buffer of a stream
+        * @param Resource $f
+        */
+       function _flush_read_buffer( $f ) {
+               if ( !is_resource( $f ) ) {
+                       return;
+               }
+               $r = array( $f );
+               $w = null;
+               $e = null;
+               $n = stream_select( $r, $w, $e, 0, 0 );
+               while ( $n == 1 && !feof( $f ) ) {
+                       fread( $f, 1024 );
+                       $r = array( $f );
+                       $w = null;
+                       $e = null;
+                       $n = stream_select( $r, $w, $e, 0, 0 );
+               }
+       }
+
+       // }}}
+       // }}}
+       // }}}
+}
+
+// }}}
index 8615cfc..e1398b8 100644 (file)
@@ -224,10 +224,9 @@ class ChronologyProtector implements LoggerAwareInterface {
                        implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
                );
 
-               // CP-protected writes should overwhelmingly go to the master datacenter, so use a
-               // DC-local lock to merge the values. Use a DC-local get() and a synchronous all-DC
-               // set(). This makes it possible for the BagOStuff class to write in parallel to all
-               // DCs with one RTT. The use of WRITE_SYNC avoids needing READ_LATEST for the get().
+               // CP-protected writes should overwhelmingly go to the master datacenter, so merge the
+               // positions with a DC-local lock, a DC-local get(), and an all-DC set() with WRITE_SYNC.
+               // If set() returns success, then any get() should be able to see the new positions.
                if ( $store->lock( $this->key, 3 ) ) {
                        if ( $workCallback ) {
                                // Let the store run the work before blocking on a replication sync barrier.
index 6029f77..0318022 100644 (file)
@@ -47,37 +47,6 @@ use Throwable;
  * @since 1.28
  */
 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
-       /** @var string Server that this instance is currently connected to */
-       protected $server;
-       /** @var string User that this instance is currently connected under the name of */
-       protected $user;
-       /** @var string Password used to establish the current connection */
-       protected $password;
-       /** @var array[] Map of (table => (dbname, schema, prefix) map) */
-       protected $tableAliases = [];
-       /** @var string[] Map of (index alias => index) */
-       protected $indexAliases = [];
-       /** @var bool Whether this PHP instance is for a CLI script */
-       protected $cliMode;
-       /** @var string Agent name for query profiling */
-       protected $agent;
-       /** @var int Bit field of class DBO_* constants */
-       protected $flags;
-       /** @var array LoadBalancer tracking information */
-       protected $lbInfo = [];
-       /** @var array|bool Variables use for schema element placeholders */
-       protected $schemaVars = false;
-       /** @var array Parameters used by initConnection() to establish a connection */
-       protected $connectionParams = [];
-       /** @var array SQL variables values to use for all new connections */
-       protected $connectionVariables = [];
-       /** @var string Current SQL query delimiter */
-       protected $delimiter = ';';
-       /** @var string|bool|null Stashed value of html_errors INI setting */
-       protected $htmlErrors;
-       /** @var int Row batch size to use for emulated INSERT SELECT queries */
-       protected $nonNativeInsertSelectBatchSize = 10000;
-
        /** @var BagOStuff APC cache */
        protected $srvCache;
        /** @var LoggerInterface */
@@ -92,25 +61,62 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
+
        /** @var DatabaseDomain */
        protected $currentDomain;
+
        /** @var object|resource|null Database connection */
        protected $conn;
 
        /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
        private $lazyMasterHandle;
 
+       /** @var string Server that this instance is currently connected to */
+       protected $server;
+       /** @var string User that this instance is currently connected under the name of */
+       protected $user;
+       /** @var string Password used to establish the current connection */
+       protected $password;
+       /** @var bool Whether this PHP instance is for a CLI script */
+       protected $cliMode;
+       /** @var string Agent name for query profiling */
+       protected $agent;
+       /** @var array Parameters used by initConnection() to establish a connection */
+       protected $connectionParams;
+       /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
+       protected $connectionVariables;
+       /** @var int Row batch size to use for emulated INSERT SELECT queries */
+       protected $nonNativeInsertSelectBatchSize;
+
+       /** @var int Current bit field of class DBO_* constants */
+       protected $flags;
+       /** @var array Current LoadBalancer tracking information */
+       protected $lbInfo = [];
+       /** @var string Current SQL query delimiter */
+       protected $delimiter = ';';
+       /** @var array[] Current map of (table => (dbname, schema, prefix) map) */
+       protected $tableAliases = [];
+       /** @var string[] Current map of (index alias => index) */
+       protected $indexAliases = [];
+       /** @var array|null Current variables use for schema element placeholders */
+       protected $schemaVars;
+
+       /** @var string|bool|null Stashed value of html_errors INI setting */
+       private $htmlErrors;
+       /** @var int[] Prior flags member variable values */
+       private $priorFlags = [];
+
        /** @var array Map of (name => 1) for locks obtained via lock() */
        protected $sessionNamedLocks = [];
        /** @var array Map of (table name => 1) for TEMPORARY tables */
        protected $sessionTempTables = [];
 
        /** @var string ID of the active transaction or the empty string otherwise */
-       protected $trxShortId = '';
+       private $trxShortId = '';
        /** @var int Transaction status */
-       protected $trxStatus = self::STATUS_TRX_NONE;
+       private $trxStatus = self::STATUS_TRX_NONE;
        /** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */
-       protected $trxStatusCause;
+       private $trxStatusCause;
        /** @var array|null Error details of the last statement-only rollback */
        private $trxStatusIgnoredCause;
        /** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */
@@ -154,9 +160,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var bool Whether to suppress triggering of transaction end callbacks */
        private $trxEndCallbacksSuppressed = false;
 
-       /** @var int[] Prior flags member variable values */
-       private $priorFlags = [];
-
        /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
        protected $affectedRowCount;
 
@@ -233,15 +236,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @note exceptions for missing libraries/drivers should be thrown in initConnection()
         * @param array $params Parameters passed from Database::factory()
         */
-       protected function __construct( array $params ) {
+       public function __construct( array $params ) {
+               $this->connectionParams = [];
                foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
                        $this->connectionParams[$name] = $params[$name];
                }
-
+               $this->connectionVariables = $params['variables'] ?? [];
                $this->cliMode = $params['cliMode'];
-               // Agent name is added to SQL queries in a comment, so make sure it can't break out
-               $this->agent = str_replace( '/', '-', $params['agent'] );
-
+               $this->agent = $params['agent'];
                $this->flags = $params['flags'];
                if ( $this->flags & self::DBO_DEFAULT ) {
                        if ( $this->cliMode ) {
@@ -250,11 +252,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->flags |= self::DBO_TRX;
                        }
                }
-
-               $this->connectionVariables = $params['variables'];
+               $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
 
                $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
-
                $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = $params['trxProfiler'];
                $this->connLogger = $params['connLogger'];
@@ -262,10 +262,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->errorLogger = $params['errorLogger'];
                $this->deprecationLogger = $params['deprecationLogger'];
 
-               if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
-                       $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
-               }
-
                // Set initial dummy domain until open() sets the final DB/prefix
                $this->currentDomain = new DatabaseDomain(
                        $params['dbname'] != '' ? $params['dbname'] : null,
@@ -395,6 +391,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                'cliMode' => (bool)$params['cliMode'],
                                'agent' => (string)$params['agent'],
                                // Objects and callbacks
+                               'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
                                'profiler' => $params['profiler'] ?? null,
                                'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
                                'connLogger' => $params['connLogger'] ?? new NullLogger(),
@@ -491,7 +488,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        /**
-        * @return array Map of (Database::ATTR_* constant => value
+        * @return array Map of (Database::ATTR_* constant => value)
         * @since 1.31
         */
        protected static function getAttributes() {
@@ -965,7 +962,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws DBReadOnlyError
         */
        protected function assertIsWritableMaster() {
-               if ( $this->getLBInfo( 'replica' ) === true ) {
+               if ( $this->getLBInfo( 'replica' ) ) {
                        throw new DBReadOnlyRoleError(
                                $this,
                                'Write operations are not allowed on replica database connections'
@@ -1148,7 +1145,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Send the query to the server and fetch any corresponding errors
                list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
                if ( $ret === false ) {
-                       $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+                       $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
                        // Throw an error unless both the ignore flag was set and a rollback is not needed
                        $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
                }
@@ -1188,11 +1185,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        // Do not treat temporary table writes as "meaningful writes" since they are only
                        // visible to one session and are not permanent. Profile them as reads. Integration
                        // tests can override this behavior via $flags.
-                       $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+                       $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
                        list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent );
                        $isPermWrite = ( $tmpType !== self::$TEMP_NORMAL );
                        // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
-                       if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+                       if ( $isPermWrite && $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
                                throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
                        }
                } else {
@@ -1203,8 +1200,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // Add trace comment to the begin of the sql string, right after the operator.
-               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
-               $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
+               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
+               $encAgent = str_replace( '/', '-', $this->agent );
+               $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 );
 
                // Send the query to the server and fetch any corresponding errors.
                // This also doubles as a "ping" to see if the connection was dropped.
@@ -1212,7 +1210,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
 
                // Check if the query failed due to a recoverable connection loss
-               $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY );
+               $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
                if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
                        // Silently resend the query to the server since it is safe and possible
                        list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
@@ -1283,7 +1281,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        }
                }
 
-               $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: ';
+               $prefix = $this->getLBInfo( 'master' ) ? 'query-m: ' : 'query: ';
                $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
 
                $startTime = microtime( true );
@@ -4279,10 +4277,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // This will reconnect if possible or return false if not
-               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
-               $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
-               $this->restoreFlags( self::RESTORE_PRIOR );
-
+               $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
+               $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
                if ( $ok ) {
                        $rtt = $this->lastRoundTripEstimate;
                }
@@ -4472,7 +4468,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function setSchemaVars( $vars ) {
-               $this->schemaVars = $vars;
+               $this->schemaVars = is_array( $vars ) ? $vars : null;
        }
 
        public function sourceStream(
@@ -4624,11 +4620,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @return array
         */
        protected function getSchemaVars() {
-               if ( $this->schemaVars ) {
-                       return $this->schemaVars;
-               } else {
-                       return $this->getDefaultSchemaVars();
-               }
+               return $this->schemaVars ?? $this->getDefaultSchemaVars();
        }
 
        /**
@@ -4818,8 +4810,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param int $field
         * @param int $flags
         * @return bool
+        * @since 1.34
         */
-       protected function hasFlags( $field, $flags ) {
+       final protected function fieldHasBit( $field, $flags ) {
                return ( ( $field & $flags ) === $flags );
        }
 
index a9223ac..ac8c7c3 100644 (file)
@@ -814,22 +814,16 @@ abstract class DatabaseMysqlBase extends Database {
        protected function getHeartbeatData( array $conds ) {
                // Query time and trip time are not counted
                $nowUnix = microtime( true );
-               // Do not bother starting implicit transactions here
-               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
-               try {
-                       $whereSQL = $this->makeList( $conds, self::LIST_AND );
-                       // Use ORDER BY for channel based queries since that field might not be UNIQUE.
-                       // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
-                       // percision field is not supported in MySQL <= 5.5.
-                       $res = $this->query(
-                               "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
-                               __METHOD__,
-                               self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX
-                       );
-                       $row = $res ? $res->fetchObject() : false;
-               } finally {
-                       $this->restoreFlags();
-               }
+               $whereSQL = $this->makeList( $conds, self::LIST_AND );
+               // Use ORDER BY for channel based queries since that field might not be UNIQUE.
+               // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
+               // percision field is not supported in MySQL <= 5.5.
+               $res = $this->query(
+                       "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
+                       __METHOD__,
+                       self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX
+               );
+               $row = $res ? $res->fetchObject() : false;
 
                return [ $row ? $row->ts : null, $nowUnix ];
        }
index 5698cf8..3ceb339 100644 (file)
@@ -23,7 +23,19 @@ namespace Wikimedia\Rdbms;
 use InvalidArgumentException;
 
 /**
- * Class to handle database/prefix specification for IDatabase domains
+ * Class to handle database/schema/prefix specifications for IDatabase
+ *
+ * The components of a database domain are defined as follows:
+ *   - database: name of a server-side collection of schemas that is client-selectable
+ *   - schema: name of a server-side collection of tables within the given database
+ *   - prefix: table name prefix of an application-defined table collection
+ *
+ * If an RDBMS does not support server-side collections of table collections (schemas) then
+ * the schema component should be null and the "database" component treated as a collection
+ * of exactly one table collection (the implied schema for that "database").
+ *
+ * The above criteria should determine how components should map to RDBMS specific keywords
+ * rather than "database"/"schema" always mapping to "DATABASE"/"SCHEMA" as used by the RDBMS.
  */
 class DatabaseDomain {
        /** @var string|null */
index e66bd69..66be436 100644 (file)
@@ -565,7 +565,9 @@ class LogEventsList extends ContextSource {
                        }
                        $permissionlist = implode( ', ', $permissions );
                        wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
-                       return $user->isAllowedAny( ...$permissions );
+                       return MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->userHasAnyRight( $user, ...$permissions );
                }
                return true;
        }
index 4ecc368..15b149e 100644 (file)
@@ -23,6 +23,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @ingroup Pager
  */
@@ -462,7 +464,10 @@ class LogPager extends ReverseChronologicalPager {
                $user = $this->getUser();
                if ( !$user->isAllowed( 'deletedhistory' ) ) {
                        $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
                                ' != ' . LogPage::SUPPRESSED_USER;
                }
@@ -480,7 +485,10 @@ class LogPager extends ReverseChronologicalPager {
                $user = $this->getUser();
                if ( !$user->isAllowed( 'deletedhistory' ) ) {
                        $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
                                ' != ' . LogPage::SUPPRESSED_ACTION;
                }
index 7ee6dcb..6e4412c 100644 (file)
@@ -110,7 +110,7 @@ class ThumbnailImage extends MediaTransformOutput {
         * @return string
         */
        function toHtml( $options = [] ) {
-               global $wgPriorityHints, $wgPriorityHintsRatio, $wgElementTiming;
+               global $wgPriorityHints, $wgPriorityHintsRatio, $wgElementTiming, $wgNativeImageLazyLoading;
 
                if ( func_num_args() == 2 ) {
                        throw new MWException( __METHOD__ . ' called in the old style' );
@@ -126,6 +126,10 @@ class ThumbnailImage extends MediaTransformOutput {
                        'decoding' => 'async',
                ];
 
+               if ( $wgNativeImageLazyLoading ) {
+                       $attribs['loading'] = 'lazy';
+               }
+
                $elementTimingName = 'thumbnail';
 
                if ( $wgPriorityHints
index d9fe319..a1820ab 100644 (file)
@@ -43,7 +43,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /** @var string[] (server index => tag/host name) */
        protected $serverTags;
        /** @var int */
-       protected $numServers;
+       protected $numServerShards;
        /** @var int UNIX timestamp */
        protected $lastGarbageCollect = 0;
        /** @var int */
@@ -51,7 +51,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /** @var int */
        protected $purgeLimit = 100;
        /** @var int */
-       protected $shards = 1;
+       protected $numTableShards = 1;
        /** @var string */
        protected $tableName = 'objectcache';
        /** @var bool */
@@ -126,7 +126,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                if ( isset( $params['servers'] ) ) {
                        $this->serverInfos = [];
                        $this->serverTags = [];
-                       $this->numServers = count( $params['servers'] );
+                       $this->numServerShards = count( $params['servers'] );
                        $index = 0;
                        foreach ( $params['servers'] as $tag => $info ) {
                                $this->serverInfos[$index] = $info;
@@ -139,11 +139,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        }
                } elseif ( isset( $params['server'] ) ) {
                        $this->serverInfos = [ $params['server'] ];
-                       $this->numServers = count( $this->serverInfos );
+                       $this->numServerShards = count( $this->serverInfos );
                } else {
                        // Default to using the main wiki's database servers
                        $this->serverInfos = false;
-                       $this->numServers = 1;
+                       $this->numServerShards = 1;
                        $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
                if ( isset( $params['purgePeriod'] ) ) {
@@ -156,7 +156,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        $this->tableName = $params['tableName'];
                }
                if ( isset( $params['shards'] ) ) {
-                       $this->shards = intval( $params['shards'] );
+                       $this->numTableShards = intval( $params['shards'] );
                }
                // Backwards-compatibility for < 1.34
                $this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false );
@@ -165,35 +165,35 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /**
         * Get a connection to the specified database
         *
-        * @param int $serverIndex
+        * @param int $shardIndex
         * @return IMaintainableDatabase
         * @throws MWException
         */
-       protected function getDB( $serverIndex ) {
-               if ( $serverIndex >= $this->numServers ) {
-                       throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+       private function getDB( $shardIndex ) {
+               if ( $shardIndex >= $this->numServerShards ) {
+                       throw new MWException( __METHOD__ . ": Invalid server index \"$shardIndex\"" );
                }
 
                # Don't keep timing out trying to connect for each call if the DB is down
                if (
-                       isset( $this->connFailureErrors[$serverIndex] ) &&
-                       ( $this->getCurrentTime() - $this->connFailureTimes[$serverIndex] ) < 60
+                       isset( $this->connFailureErrors[$shardIndex] ) &&
+                       ( $this->getCurrentTime() - $this->connFailureTimes[$shardIndex] ) < 60
                ) {
-                       throw $this->connFailureErrors[$serverIndex];
+                       throw $this->connFailureErrors[$shardIndex];
                }
 
                if ( $this->serverInfos ) {
-                       if ( !isset( $this->conns[$serverIndex] ) ) {
+                       if ( !isset( $this->conns[$shardIndex] ) ) {
                                // Use custom database defined by server connection info
-                               $info = $this->serverInfos[$serverIndex];
+                               $info = $this->serverInfos[$shardIndex];
                                $type = $info['type'] ?? 'mysql';
                                $host = $info['host'] ?? '[unknown]';
                                $this->logger->debug( __CLASS__ . ": connecting to $host" );
                                $db = Database::factory( $type, $info );
                                $db->clearFlag( DBO_TRX ); // auto-commit mode
-                               $this->conns[$serverIndex] = $db;
+                               $this->conns[$shardIndex] = $db;
                        }
-                       $db = $this->conns[$serverIndex];
+                       $db = $this->conns[$shardIndex];
                } else {
                        // Use the main LB database
                        $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
@@ -218,22 +218,22 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param string $key
         * @return array Server index and table name
         */
-       protected function getTableByKey( $key ) {
-               if ( $this->shards > 1 ) {
+       private function getTableByKey( $key ) {
+               if ( $this->numTableShards > 1 ) {
                        $hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
-                       $tableIndex = $hash % $this->shards;
+                       $tableIndex = $hash % $this->numTableShards;
                } else {
                        $tableIndex = 0;
                }
-               if ( $this->numServers > 1 ) {
+               if ( $this->numServerShards > 1 ) {
                        $sortedServers = $this->serverTags;
                        ArrayUtils::consistentHashSort( $sortedServers, $key );
                        reset( $sortedServers );
-                       $serverIndex = key( $sortedServers );
+                       $shardIndex = key( $sortedServers );
                } else {
-                       $serverIndex = 0;
+                       $shardIndex = 0;
                }
-               return [ $serverIndex, $this->getTableNameByShard( $tableIndex ) ];
+               return [ $shardIndex, $this->getTableNameByShard( $tableIndex ) ];
        }
 
        /**
@@ -241,9 +241,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param int $index
         * @return string
         */
-       protected function getTableNameByShard( $index ) {
-               if ( $this->shards > 1 ) {
-                       $decimals = strlen( $this->shards - 1 );
+       private function getTableNameByShard( $index ) {
+               if ( $this->numTableShards > 1 ) {
+                       $decimals = strlen( $this->numTableShards - 1 );
                        return $this->tableName .
                                sprintf( "%0{$decimals}d", $index );
                } else {
@@ -278,19 +278,19 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                return $values;
        }
 
-       protected function fetchBlobMulti( array $keys, $flags = 0 ) {
+       private function fetchBlobMulti( array $keys, $flags = 0 ) {
                $values = []; // array of (key => value)
 
                $keysByTable = [];
                foreach ( $keys as $key ) {
-                       list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
-                       $keysByTable[$serverIndex][$tableName][] = $key;
+                       list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+                       $keysByTable[$shardIndex][$tableName][] = $key;
                }
 
                $dataRows = [];
-               foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+               foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getDB( $shardIndex );
                                foreach ( $serverKeys as $tableName => $tableKeys ) {
                                        $res = $db->select( $tableName,
                                                [ 'keyname', 'value', 'exptime' ],
@@ -305,13 +305,13 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                                continue;
                                        }
                                        foreach ( $res as $row ) {
-                                               $row->serverIndex = $serverIndex;
+                                               $row->shardIndex = $shardIndex;
                                                $row->tableName = $tableName;
                                                $dataRows[$row->keyname] = $row;
                                        }
                                }
                        } catch ( DBError $e ) {
-                               $this->handleReadError( $e, $serverIndex );
+                               $this->handleReadError( $e, $shardIndex );
                        }
                }
 
@@ -321,14 +321,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
                                $db = null; // in case of connection failure
                                try {
-                                       $db = $this->getDB( $row->serverIndex );
+                                       $db = $this->getDB( $row->shardIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
                                                $this->debug( "get: key has expired" );
                                        } else { // HIT
                                                $values[$key] = $db->decodeBlob( $row->value );
                                        }
                                } catch ( DBQueryError $e ) {
-                                       $this->handleWriteError( $e, $db, $row->serverIndex );
+                                       $this->handleWriteError( $e, $db, $row->shardIndex );
                                }
                        } else { // MISS
                                $this->debug( 'get: no matching rows' );
@@ -352,8 +352,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        private function modifyMulti( array $data, $exptime, $flags, $op ) {
                $keysByTable = [];
                foreach ( $data as $key => $value ) {
-                       list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
-                       $keysByTable[$serverIndex][$tableName][] = $key;
+                       list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+                       $keysByTable[$shardIndex][$tableName][] = $key;
                }
 
                $exptime = $this->getExpirationAsTimestamp( $exptime );
@@ -361,14 +361,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $result = true;
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
-               foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+               foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getDB( $shardIndex );
                                $this->occasionallyGarbageCollect( $db ); // expire old entries if any
                                $dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $result = false;
                                continue;
                        }
@@ -384,14 +384,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                                $dbExpiry
                                        ) && $result;
                                } catch ( DBError $e ) {
-                                       $this->handleWriteError( $e, $db, $serverIndex );
+                                       $this->handleWriteError( $e, $db, $shardIndex );
                                        $result = false;
                                }
 
                        }
                }
 
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
                        $result = $this->waitForReplication() && $result;
                }
 
@@ -472,14 +472,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
-               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
                $exptime = $this->getExpirationAsTimestamp( $exptime );
 
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getDB( $shardIndex );
                        // (T26425) use a replace if the db supports it instead of
                        // delete/insert to avoid clashes with conflicting keynames
                        $db->update(
@@ -499,12 +499,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                __METHOD__
                        );
                } catch ( DBQueryError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
 
                        return false;
                }
 
-               return (bool)$db->affectedRows();
+               $success = (bool)$db->affectedRows();
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
+                       $success = $this->waitForReplication() && $success;
+               }
+
+               return $success;
        }
 
        protected function doDeleteMulti( array $keys, $flags = 0 ) {
@@ -521,14 +526,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        public function incr( $key, $step = 1 ) {
-               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
 
                $newCount = false;
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getDB( $shardIndex );
                        $encTimestamp = $db->addQuotes( $db->timestamp() );
                        $db->update(
                                $tableName,
@@ -548,21 +553,12 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                }
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
                }
 
                return $newCount;
        }
 
-       public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
-               $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
-                       $ok = $this->waitForReplication() && $ok;
-               }
-
-               return $ok;
-       }
-
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
                return $this->modifyMulti(
                        array_fill_keys( $keys, null ),
@@ -581,7 +577,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param string $exptime
         * @return bool
         */
-       protected function isExpired( $db, $exptime ) {
+       private function isExpired( $db, $exptime ) {
                return (
                        $exptime != $this->getMaxDateTime( $db ) &&
                        wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime()
@@ -592,7 +588,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param IDatabase $db
         * @return string
         */
-       protected function getMaxDateTime( $db ) {
+       private function getMaxDateTime( $db ) {
                if ( (int)$this->getCurrentTime() > 0x7fffffff ) {
                        return $db->timestamp( 1 << 62 );
                } else {
@@ -604,7 +600,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param IDatabase $db
         * @throws DBError
         */
-       protected function occasionallyGarbageCollect( IDatabase $db ) {
+       private function occasionallyGarbageCollect( IDatabase $db ) {
                if (
                        // Random purging is enabled
                        $this->purgePeriod &&
@@ -642,16 +638,16 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
 
-               $serverIndexes = range( 0, $this->numServers - 1 );
-               shuffle( $serverIndexes );
+               $shardIndexes = range( 0, $this->numServerShards - 1 );
+               shuffle( $shardIndexes );
 
                $ok = true;
 
                $keysDeletedCount = 0;
-               foreach ( $serverIndexes as $numServersDone => $serverIndex ) {
+               foreach ( $shardIndexes as $numServersDone => $shardIndex ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getDB( $shardIndex );
                                $this->deleteServerObjectsExpiringBefore(
                                        $db,
                                        $timestamp,
@@ -661,7 +657,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                        $keysDeletedCount
                                );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $ok = false;
                        }
                }
@@ -687,7 +683,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                &$keysDeletedCount = 0
        ) {
                $cutoffUnix = wfTimestamp( TS_UNIX, $timestamp );
-               $shardIndexes = range( 0, $this->shards - 1 );
+               $shardIndexes = range( 0, $this->numTableShards - 1 );
                shuffle( $shardIndexes );
 
                foreach ( $shardIndexes as $numShardsDone => $shardIndex ) {
@@ -732,13 +728,13 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                        if ( $lag ) {
                                                $remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue );
                                                $processedLag = max( $lag - $remainingLag, 0 );
-                                               $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->shards;
+                                               $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->numTableShards;
                                        } else {
                                                $doneRatio = 1;
                                        }
 
-                                       $overallRatio = ( $doneRatio / $this->numServers )
-                                               + ( $serversDoneCount / $this->numServers );
+                                       $overallRatio = ( $doneRatio / $this->numServerShards )
+                                               + ( $serversDoneCount / $this->numServerShards );
                                        call_user_func( $progressCallback, $overallRatio * 100 );
                                }
                        } while ( $res->numRows() && $keysDeletedCount < $limit );
@@ -753,15 +749,15 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        public function deleteAll() {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
-               for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+               for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
-                               for ( $i = 0; $i < $this->shards; $i++ ) {
+                               $db = $this->getDB( $shardIndex );
+                               for ( $i = 0; $i < $this->numTableShards; $i++ ) {
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                return false;
                        }
                }
@@ -779,11 +775,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        }
                }
 
-               list( $serverIndex ) = $this->getTableByKey( $key );
+               list( $shardIndex ) = $this->getTableByKey( $key );
 
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getDB( $shardIndex );
                        $ok = $db->lock( $key, __METHOD__, $timeout );
                        if ( $ok ) {
                                $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
@@ -796,7 +792,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                        return $ok;
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
                        $ok = false;
                }
 
@@ -811,11 +807,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                if ( --$this->locks[$key]['depth'] <= 0 ) {
                        unset( $this->locks[$key] );
 
-                       list( $serverIndex ) = $this->getTableByKey( $key );
+                       list( $shardIndex ) = $this->getTableByKey( $key );
 
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getDB( $shardIndex );
                                $ok = $db->unlock( $key, __METHOD__ );
                                if ( !$ok ) {
                                        $this->logger->warning(
@@ -824,7 +820,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                        );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $ok = false;
                        }
 
@@ -882,11 +878,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * Handle a DBError which occurred during a read operation.
         *
         * @param DBError $exception
-        * @param int $serverIndex
+        * @param int $shardIndex
         */
-       protected function handleReadError( DBError $exception, $serverIndex ) {
+       private function handleReadError( DBError $exception, $shardIndex ) {
                if ( $exception instanceof DBConnectionError ) {
-                       $this->markServerDown( $exception, $serverIndex );
+                       $this->markServerDown( $exception, $shardIndex );
                }
 
                $this->setAndLogDBError( $exception );
@@ -897,12 +893,12 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         *
         * @param DBError $exception
         * @param IDatabase|null $db DB handle or null if connection failed
-        * @param int $serverIndex
+        * @param int $shardIndex
         * @throws Exception
         */
-       protected function handleWriteError( DBError $exception, $db, $serverIndex ) {
+       private function handleWriteError( DBError $exception, $db, $shardIndex ) {
                if ( !( $db instanceof IDatabase ) ) {
-                       $this->markServerDown( $exception, $serverIndex );
+                       $this->markServerDown( $exception, $shardIndex );
                }
 
                $this->setAndLogDBError( $exception );
@@ -926,37 +922,37 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * Mark a server down due to a DBConnectionError exception
         *
         * @param DBError $exception
-        * @param int $serverIndex
+        * @param int $shardIndex
         */
-       protected function markServerDown( DBError $exception, $serverIndex ) {
-               unset( $this->conns[$serverIndex] ); // bug T103435
+       private function markServerDown( DBError $exception, $shardIndex ) {
+               unset( $this->conns[$shardIndex] ); // bug T103435
 
                $now = $this->getCurrentTime();
-               if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
-                       if ( $now - $this->connFailureTimes[$serverIndex] >= 60 ) {
-                               unset( $this->connFailureTimes[$serverIndex] );
-                               unset( $this->connFailureErrors[$serverIndex] );
+               if ( isset( $this->connFailureTimes[$shardIndex] ) ) {
+                       if ( $now - $this->connFailureTimes[$shardIndex] >= 60 ) {
+                               unset( $this->connFailureTimes[$shardIndex] );
+                               unset( $this->connFailureErrors[$shardIndex] );
                        } else {
-                               $this->logger->debug( __METHOD__ . ": Server #$serverIndex already down" );
+                               $this->logger->debug( __METHOD__ . ": Server #$shardIndex already down" );
                                return;
                        }
                }
-               $this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) );
-               $this->connFailureTimes[$serverIndex] = $now;
-               $this->connFailureErrors[$serverIndex] = $exception;
+               $this->logger->info( __METHOD__ . ": Server #$shardIndex down until " . ( $now + 60 ) );
+               $this->connFailureTimes[$shardIndex] = $now;
+               $this->connFailureErrors[$shardIndex] = $exception;
        }
 
        /**
         * Create shard tables. For use from eval.php.
         */
        public function createTables() {
-               for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
-                       $db = $this->getDB( $serverIndex );
+               for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
+                       $db = $this->getDB( $shardIndex );
                        if ( $db->getType() !== 'mysql' ) {
                                throw new MWException( __METHOD__ . ' is not supported on this DB server' );
                        }
 
-                       for ( $i = 0; $i < $this->shards; $i++ ) {
+                       for ( $i = 0; $i < $this->numTableShards; $i++ ) {
                                $db->query(
                                        'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
                                        ' LIKE ' . $db->tableName( 'objectcache' ),
@@ -968,11 +964,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /**
         * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
         */
-       protected function usesMainDB() {
+       private function usesMainDB() {
                return !$this->serverInfos;
        }
 
-       protected function waitForReplication() {
+       private function waitForReplication() {
                if ( !$this->usesMainDB() ) {
                        // Custom DB server list; probably doesn't use replication
                        return true;
@@ -1007,12 +1003,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        /**
-        * 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
+        * Silence the transaction profiler until the return value falls out of scope
+        *
+        * @return ScopedCallback|null
         */
-       protected function silenceTransactionProfiler() {
+       private function silenceTransactionProfiler() {
+               if ( !$this->usesMainDB() ) {
+                       // Custom DB is configured which either has no TransactionProfiler injected,
+                       // or has one specific for cache use, which we shouldn't silence
+                       return null;
+               }
+
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
                $oldSilenced = $trxProfiler->setSilenced( true );
                return new ScopedCallback( function () use ( $trxProfiler, $oldSilenced ) {
index e488b6c..2de82bf 100644 (file)
@@ -91,7 +91,9 @@ class ImageHistoryList extends ContextSource {
                . Xml::openElement( 'table', [ 'class' => 'wikitable filehistory' ] ) . "\n"
                . '<tr><th></th>'
                . ( $this->current->isLocal()
-               && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<th></th>' : '' )
+               && ( MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->userHasAnyRight( $this->getUser(), 'delete', 'deletedhistory' ) ) ? '<th></th>' : '' )
                . '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>'
                . ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' )
                . '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>'
@@ -126,7 +128,10 @@ class ImageHistoryList extends ContextSource {
                $row = $selected = '';
 
                // Deletion link
-               if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
+               if ( $local && ( MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->userHasAnyRight( $user, 'delete', 'deletedhistory' ) )
+               ) {
                        $row .= '<td>';
                        # Link to remove from history
                        if ( $user->isAllowed( 'delete' ) ) {
index 8cc5a39..4607535 100644 (file)
@@ -3249,7 +3249,10 @@ class WikiPage implements Page, IDBAccessObject {
                        $flags |= EDIT_MINOR;
                }
 
-               if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
+               if ( $bot && ( MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->userHasAnyRight( $guser, 'markbotedits', 'bot' ) )
+               ) {
                        $flags |= EDIT_FORCE_BOT;
                }
 
index b643c3f..a19f86c 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup Parser
  */
+use MediaWiki\BadFileLookup;
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
@@ -299,6 +300,9 @@ class Parser {
        /** @var LoggerInterface */
        private $logger;
 
+       /** @var BadFileLookup */
+       private $badFileLookup;
+
        /**
         * TODO Make this a const when HHVM support is dropped (T192166)
         *
@@ -339,6 +343,7 @@ class Parser {
         * @param LinkRendererFactory|null $linkRendererFactory
         * @param NamespaceInfo|null $nsInfo
         * @param LoggerInterface|null $logger
+        * @param BadFileLookup|null $badFileLookup
         */
        public function __construct(
                $svcOptions = null,
@@ -349,7 +354,8 @@ class Parser {
                SpecialPageFactory $spFactory = null,
                $linkRendererFactory = null,
                $nsInfo = null,
-               $logger = null
+               $logger = null,
+               BadFileLookup $badFileLookup = null
        ) {
                if ( !$svcOptions || is_array( $svcOptions ) ) {
                        // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
@@ -396,6 +402,8 @@ class Parser {
                        MediaWikiServices::getInstance()->getLinkRendererFactory();
                $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
                $this->logger = $logger ?: new NullLogger();
+               $this->badFileLookup = $badFileLookup ??
+                       MediaWikiServices::getInstance()->getBadFileLookup();
        }
 
        /**
@@ -530,7 +538,10 @@ class Parser {
         * @param ParserOptions $options
         * @param bool $linestart
         * @param bool $clearState
-        * @param int|null $revid Number to pass in {{REVISIONID}}
+        * @param int|null $revid ID of the revision being rendered. This is used to render
+        *  REVISION* magic words. 0 means that any current revision will be used. Null means
+        *  that {{REVISIONID}}/{{REVISIONUSER}} will be empty and {{REVISIONTIMESTAMP}} will
+        *  use the current timestamp.
         * @return ParserOutput A ParserOutput
         * @return-taint escaped
         */
@@ -2498,7 +2509,7 @@ class Parser {
                                }
 
                                if ( $ns == NS_FILE ) {
-                                       if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
+                                       if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) {
                                                if ( $wasblank ) {
                                                        # if no parameters were passed, $text
                                                        # becomes something like "File:Foo.png",
index 3d15e86..bab1f36 100644 (file)
@@ -19,6 +19,7 @@
  * @ingroup Parser
  */
 
+use MediaWiki\BadFileLookup;
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\MediaWikiServices;
@@ -54,6 +55,9 @@ class ParserFactory {
        /** @var LoggerInterface */
        private $logger;
 
+       /** @var BadFileLookup */
+       private $badFileLookup;
+
        /**
         * Old parameter list, which we support for backwards compatibility, were:
         *   array $parserConf See $wgParserConf documentation
@@ -77,6 +81,7 @@ class ParserFactory {
         * @param LinkRendererFactory $linkRendererFactory
         * @param NamespaceInfo|LinkRendererFactory|null $nsInfo
         * @param LoggerInterface|null $logger
+        * @param BadFileLookup|null $badFileLookup
         * @since 1.32
         */
        public function __construct(
@@ -87,7 +92,8 @@ class ParserFactory {
                SpecialPageFactory $spFactory,
                $linkRendererFactory,
                $nsInfo = null,
-               $logger = null
+               $logger = null,
+               BadFileLookup $badFileLookup = null
        ) {
                // @todo Do we need to retain compat for constructing this class directly?
                if ( !$nsInfo ) {
@@ -119,6 +125,8 @@ class ParserFactory {
                $this->linkRendererFactory = $linkRendererFactory;
                $this->nsInfo = $nsInfo;
                $this->logger = $logger ?: new NullLogger();
+               $this->badFileLookup = $badFileLookup ??
+                       MediaWikiServices::getInstance()->getBadFileLookup();
        }
 
        /**
@@ -135,7 +143,8 @@ class ParserFactory {
                        $this->specialPageFactory,
                        $this->linkRendererFactory,
                        $this->nsInfo,
-                       $this->logger
+                       $this->logger,
+                       $this->badFileLookup
                );
        }
 }
index bbad648..eeed05e 100644 (file)
@@ -238,7 +238,9 @@ abstract class Skin extends ContextSource {
 
                // Add various resources if required
                if ( $user->isLoggedIn()
-                       && $user->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
+                       && MediaWikiServices::getInstance()
+                                ->getPermissionManager()
+                                ->userHasAllRights( $user, 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
                        && $this->getRelevantTitle()->canExist()
                ) {
                        $modules['watch'][] = 'mediawiki.page.watch.ajax';
@@ -306,6 +308,7 @@ abstract class Skin extends ContextSource {
        /**
         * Get the current revision ID
         *
+        * @deprecated since 1.34, use OutputPage::getRevisionId instead
         * @return int
         */
        public function getRevisionId() {
@@ -315,11 +318,11 @@ abstract class Skin extends ContextSource {
        /**
         * Whether the revision displayed is the latest revision of the page
         *
+        * @deprecated since 1.34, use OutputPage::isRevisionCurrent instead
         * @return bool
         */
        public function isRevisionCurrent() {
-               $revID = $this->getRevisionId();
-               return $revID == 0 || $revID == $this->getTitle()->getLatestRevID();
+               return $this->getOutput()->isRevisionCurrent();
        }
 
        /**
@@ -699,7 +702,7 @@ abstract class Skin extends ContextSource {
         * @return string HTML text with an URL
         */
        function printSource() {
-               $oldid = $this->getRevisionId();
+               $oldid = $this->getOutput()->getRevisionId();
                if ( $oldid ) {
                        $canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid );
                        $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
@@ -733,11 +736,24 @@ abstract class Skin extends ContextSource {
                                        $msg = 'viewdeleted';
                                }
 
-                               return $this->msg( $msg )->rawParams(
+                               $subtitle = $this->msg( $msg )->rawParams(
                                        $linkRenderer->makeKnownLink(
                                                SpecialPage::getTitleFor( 'Undelete', $this->getTitle()->getPrefixedDBkey() ),
                                                $this->msg( 'restorelink' )->numParams( $n )->text() )
                                        )->escaped();
+
+                               // Allow extensions to add more links
+                               $links = [];
+                               Hooks::run( 'UndeletePageToolLinks', [ $this->getContext(), $linkRenderer, &$links ] );
+
+                               if ( $links ) {
+                                       $subtitle .= ''
+                                               . $this->msg( 'word-separator' )->escaped()
+                                               . $this->msg( 'parentheses' )
+                                                       ->rawParams( $this->getLanguage()->pipeList( $links ) )
+                                                       ->escaped();
+                               }
+                               return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
                        }
                }
 
@@ -828,7 +844,7 @@ abstract class Skin extends ContextSource {
        function getCopyright( $type = 'detect' ) {
                $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                if ( $type == 'detect' ) {
-                       if ( !$this->isRevisionCurrent()
+                       if ( !$this->getOutput()->isRevisionCurrent()
                                && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
                        ) {
                                $type = 'history';
@@ -932,7 +948,8 @@ abstract class Skin extends ContextSource {
 
                # No cached timestamp, load it from the database
                if ( $timestamp === null ) {
-                       $timestamp = Revision::getTimestampFromId( $this->getTitle(), $this->getRevisionId() );
+                       $timestamp = Revision::getTimestampFromId( $this->getTitle(),
+                               $this->getOutput()->getRevisionId() );
                }
 
                if ( $timestamp ) {
@@ -1086,8 +1103,8 @@ abstract class Skin extends ContextSource {
        function editUrlOptions() {
                $options = [ 'action' => 'edit' ];
 
-               if ( !$this->isRevisionCurrent() ) {
-                       $options['oldid'] = intval( $this->getRevisionId() );
+               if ( !$this->getOutput()->isRevisionCurrent() ) {
+                       $options['oldid'] = intval( $this->getOutput()->getRevisionId() );
                }
 
                return $options;
index af7ec29..f348135 100644 (file)
@@ -371,7 +371,7 @@ class SkinTemplate extends Skin {
                $tpl->set( 'credits', false );
                $tpl->set( 'numberofwatchingusers', false );
                if ( $title->exists() ) {
-                       if ( $out->isArticle() && $this->isRevisionCurrent() ) {
+                       if ( $out->isArticle() && $out->isRevisionCurrent() ) {
                                if ( $wgMaxCredits != 0 ) {
                                        /** @var CreditsAction $action */
                                        $action = Action::factory(
@@ -585,6 +585,7 @@ class SkinTemplate extends Skin {
                $request = $this->getRequest();
                $pageurl = $title->getLocalURL();
                $authManager = AuthManager::singleton();
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
 
                /* set up the default links for the personal toolbar */
                $personal_urls = [];
@@ -704,7 +705,7 @@ class SkinTemplate extends Skin {
                        ];
 
                        // No need to show Talk and Contributions to anons if they can't contribute!
-                       if ( User::groupHasPermission( '*', 'edit' ) ) {
+                       if ( $permissionManager->groupHasPermission( '*', 'edit' ) ) {
                                // Because of caching, we can't link directly to the IP talk and
                                // contributions pages. Instead we use the special page shortcuts
                                // (which work correctly regardless of caching). This means we can't
@@ -732,7 +733,7 @@ class SkinTemplate extends Skin {
                        }
 
                        if ( $authManager->canAuthenticateNow() ) {
-                               $key = User::groupHasPermission( '*', 'read' )
+                               $key = $permissionManager->groupHasPermission( '*', 'read' )
                                        ? 'login'
                                        : 'login-private';
                                $personal_urls[$key] = $login_url;
@@ -974,7 +975,7 @@ class SkinTemplate extends Skin {
                                        // Whether to show the "Add a new section" tab
                                        // Checks if this is a current rev of talk page and is not forced to be hidden
                                        $showNewSection = !$out->forceHideNewSectionLink()
-                                               && ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() );
+                                               && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() );
                                        $section = $request->getVal( 'section' );
 
                                        if ( $title->exists()
@@ -1068,8 +1069,8 @@ class SkinTemplate extends Skin {
                                }
 
                                if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
-                                       MediaWikiServices::getInstance()->getNamespaceInfo()->
-                                               getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
+                                       MediaWikiServices::getInstance()->getPermissionManager()
+                                               ->getNamespaceRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
                                ) {
                                        $mode = $title->isProtected() ? 'unprotect' : 'protect';
                                        $content_navigation['actions'][$mode] = [
@@ -1081,7 +1082,10 @@ class SkinTemplate extends Skin {
                                }
 
                                // Checks if the user is logged in
-                               if ( $this->loggedin && $user->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) ) {
+                               if ( $this->loggedin && MediaWikiServices::getInstance()
+                                               ->getPermissionManager()
+                                               ->userHasAllRights( $user, 'viewmywatchlist', 'editmywatchlist' )
+                               ) {
                                        /**
                                         * The following actions use messages which, if made particular to
                                         * the any specific skins, would break the Ajax code which makes this
@@ -1291,7 +1295,7 @@ class SkinTemplate extends Skin {
 
                if ( $out->isArticle() ) {
                        // Also add a "permalink" while we're at it
-                       $revid = $this->getRevisionId();
+                       $revid = $this->getOutput()->getRevisionId();
                        if ( $revid ) {
                                $nav_urls['permalink'] = [
                                        'text' => $this->msg( 'permalink' )->text(),
index d7e39d5..7d33035 100644 (file)
@@ -278,7 +278,9 @@ class SpecialPage implements MessageLocalizer {
         */
        public function isRestricted() {
                // DWIM: If anons can do something, then it is not restricted
-               return $this->mRestriction != '' && !User::groupHasPermission( '*', $this->mRestriction );
+               return $this->mRestriction != '' && !MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->groupHasPermission( '*', $this->mRestriction );
        }
 
        /**
index 9b5dd3f..cc2fc80 100644 (file)
@@ -51,7 +51,9 @@ class SpecialCreateAccount extends LoginSignupSpecialPage {
        }
 
        public function isRestricted() {
-               return !User::groupHasPermission( '*', 'createaccount' );
+               return !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->groupHasPermission( '*', 'createaccount' );
        }
 
        public function userCanExecute( User $user ) {
index 6ef6cb3..70a1bd4 100644 (file)
@@ -261,8 +261,7 @@ class SpecialEditTags extends UnlistedSpecialPage {
                                                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                                                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
                                                        // Unicode codepoints.
-                                                       // "- 155" is to leave room for the auto-generated part of the log entry.
-                                                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+                                                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                                ] ) .
                                        '</td>' .
                                "</tr><tr>\n" .
index c3aec83..f21c206 100644 (file)
@@ -24,6 +24,7 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Permissions\PermissionManager;
 
 /**
@@ -76,7 +77,10 @@ class SpecialImport extends SpecialPage {
                Hooks::run( 'ImportSources', [ &$this->importSources ] );
 
                $user = $this->getUser();
-               if ( !$user->isAllowedAny( 'import', 'importupload' ) ) {
+               if ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'import', 'importupload' )
+               ) {
                        throw new PermissionsError( 'import' );
                }
 
index 2f0c2ce..c6927c1 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Timestamp\TimestampException;
 
 /**
@@ -264,7 +265,9 @@ class SpecialLog extends SpecialPage {
 
        private function getActionButtons( $formcontents ) {
                $user = $this->getUser();
-               $canRevDelete = $user->isAllowedAll( 'deletedhistory', 'deletelogentry' );
+               $canRevDelete = MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAllRights( $user, 'deletedhistory', 'deletelogentry' );
                $showTagEditUI = ChangeTags::showTagEditingUI( $user );
                # If the user doesn't have the ability to delete log entries nor edit tags,
                # don't bother showing them the button(s).
index 711d447..493f6db 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page that list newly created pages
  *
@@ -184,7 +186,9 @@ class SpecialNewpages extends IncludableSpecialPage {
                }
 
                // Disable some if needed
-               if ( !User::groupHasPermission( '*', 'createpage' ) ) {
+               if ( !MediaWikiServices::getInstance()->getPermissionManager()
+                               ->groupHasPermission( '*', 'createpage' )
+               ) {
                        unset( $filters['hideliu'] );
                }
                if ( !$this->getUser()->useNPPatrol() ) {
index 2443470..f5239b4 100644 (file)
@@ -382,7 +382,10 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                // the necessary rights.
                if ( !$user->isAllowed( 'deletedhistory' ) ) {
                        $bitmask = LogPage::DELETED_ACTION;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
                } else {
                        $bitmask = 0;
index 5dae156..ea23973 100644 (file)
@@ -18,6 +18,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Form to edit user preferences.
  *
@@ -71,7 +73,10 @@ class PreferencesFormOOUI extends OOUIHTMLForm {
         * @return string
         */
        function getButtons() {
-               if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+               if ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $this->getModifiedUser(), 'editmyprivateinfo', 'editmyoptions' )
+               ) {
                        return '';
                }
 
index 1cb78b8..9ac7df5 100644 (file)
@@ -285,7 +285,9 @@ class ContribsPager extends RangeChronologicalPager {
                        $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
                        # ignore local groups with the bot right
                        # @todo FIXME: Global groups may have 'bot' rights
-                       $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+                       $groupsWithBotPermission = MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->getGroupsWithPermission( 'bot' );
                        if ( count( $groupsWithBotPermission ) ) {
                                $queryInfo['tables'][] = 'user_groups';
                                $queryInfo['conds'][] = 'ug_group IS NULL';
@@ -351,7 +353,10 @@ class ContribsPager extends RangeChronologicalPager {
                        $queryInfo['conds'][] = $this->mDb->bitAnd(
                                'rev_deleted', RevisionRecord::DELETED_USER
                                ) . ' = 0';
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $queryInfo['conds'][] = $this->mDb->bitAnd(
                                'rev_deleted', RevisionRecord::SUPPRESSED_USER
                                ) . ' != ' . RevisionRecord::SUPPRESSED_USER;
index 88e1ea8..2f40ace 100644 (file)
@@ -90,7 +90,10 @@ class DeletedContribsPager extends IndexPager {
                // Paranoia: avoid brute force searches (T19792)
                if ( !$user->isAllowed( 'deletedhistory' ) ) {
                        $conds[] = $this->mDb->bitAnd( 'ar_deleted', RevisionRecord::DELETED_USER ) . ' = 0';
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $conds[] = $this->mDb->bitAnd( 'ar_deleted', RevisionRecord::SUPPRESSED_USER ) .
                                ' != ' . RevisionRecord::SUPPRESSED_USER;
                }
index 88dff6e..ed86e54 100644 (file)
@@ -87,7 +87,9 @@ class NewFilesPager extends RangeChronologicalPager {
                }
 
                if ( !$opts->getValue( 'showbots' ) ) {
-                       $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+                       $groupsWithBotPermission = MediaWikiServices::getInstance()
+                               ->getPermissionManager()
+                               ->getGroupsWithPermission( 'bot' );
 
                        if ( count( $groupsWithBotPermission ) ) {
                                $dbr = wfGetDB( DB_REPLICA );
index 8131671..c50563d 100644 (file)
@@ -68,7 +68,9 @@ class NewPagesPager extends ReverseChronologicalPager {
                        $conds[] = ActorMigration::newMigration()->getWhere(
                                $this->mDb, 'rc_user', User::newFromName( $user->getText(), false ), false
                        )['conds'];
-               } elseif ( User::groupHasPermission( '*', 'createpage' ) &&
+               } elseif ( MediaWikiServices::getInstance()
+                                       ->getPermissionManager()
+                                       ->groupHasPermission( '*', 'createpage' ) &&
                        $this->opts->getValue( 'hideliu' )
                ) {
                        # If anons cannot make new pages, don't "exclude logged in users"!
index 7307cc1..105eeaa 100644 (file)
@@ -22,6 +22,7 @@
 
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
 
 /**
  * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
@@ -93,10 +94,8 @@ class NamespaceInfo {
                'ExtraNamespaces',
                'ExtraSignatureNamespaces',
                'NamespaceContentModels',
-               'NamespaceProtection',
                'NamespacesWithSubpages',
                'NonincludableNamespaces',
-               'RestrictionLevels',
        ];
 
        /**
@@ -572,82 +571,18 @@ class NamespaceInfo {
         * Determine which restriction levels it makes sense to use in a namespace,
         * optionally filtered by a user's rights.
         *
-        * @todo Move this to PermissionManager and remove the dependency here on permissions-related
-        * config settings.
-        *
+        * @deprecated since 1.34 User PermissionManager::getNamespaceRestrictionLevels instead.
         * @param int $index Index to check
         * @param User|null $user User to check
         * @return array
         */
        public function getRestrictionLevels( $index, User $user = null ) {
-               if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
-                       // All levels are valid if there's no namespace restriction.
-                       // But still filter by user, if necessary
-                       $levels = $this->options->get( 'RestrictionLevels' );
-                       if ( $user ) {
-                               $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
-                                       $right = $level;
-                                       if ( $right == 'sysop' ) {
-                                               $right = 'editprotected'; // BC
-                                       }
-                                       if ( $right == 'autoconfirmed' ) {
-                                               $right = 'editsemiprotected'; // BC
-                                       }
-                                       return ( $right == '' || $user->isAllowed( $right ) );
-                               } ) );
-                       }
-                       return $levels;
-               }
-
-               // $wgNamespaceProtection can require one or more rights to edit the namespace, which
-               // may be satisfied by membership in multiple groups each giving a subset of those rights.
-               // A restriction level is redundant if, for any one of the namespace rights, all groups
-               // giving that right also give the restriction level's right. Or, conversely, a
-               // restriction level is not redundant if, for every namespace right, there's at least one
-               // group giving that right without the restriction level's right.
-               //
-               // First, for each right, get a list of groups with that right.
-               $namespaceRightGroups = [];
-               foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
-                       if ( $right == 'sysop' ) {
-                               $right = 'editprotected'; // BC
-                       }
-                       if ( $right == 'autoconfirmed' ) {
-                               $right = 'editsemiprotected'; // BC
-                       }
-                       if ( $right != '' ) {
-                               $namespaceRightGroups[$right] = User::getGroupsWithPermission( $right );
-                       }
-               }
-
-               // Now, go through the protection levels one by one.
-               $usableLevels = [ '' ];
-               foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
-                       $right = $level;
-                       if ( $right == 'sysop' ) {
-                               $right = 'editprotected'; // BC
-                       }
-                       if ( $right == 'autoconfirmed' ) {
-                               $right = 'editsemiprotected'; // BC
-                       }
-
-                       if ( $right != '' &&
-                               !isset( $namespaceRightGroups[$right] ) &&
-                               ( !$user || $user->isAllowed( $right ) )
-                       ) {
-                               // Do any of the namespace rights imply the restriction right? (see explanation above)
-                               foreach ( $namespaceRightGroups as $groups ) {
-                                       if ( !array_diff( $groups, User::getGroupsWithPermission( $right ) ) ) {
-                                               // Yes, this one does.
-                                               continue 2;
-                                       }
-                               }
-                               // No, keep the restriction level
-                               $usableLevels[] = $level;
-                       }
-               }
-
-               return $usableLevels;
+               // PermissionManager is not injected because adding an explicit dependency
+               // breaks MW installer by adding a dependency chain on the database before
+               // it was set up. Also, the method is deprecated and will be soon removed.
+               return MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->getNamespaceRestrictionLevels( $index, $user );
        }
 
        /**
index 7c2f038..4fcf98d 100644 (file)
@@ -3601,32 +3601,28 @@ class User implements IDBAccessObject, UserIdentity {
        /**
         * Check if user is allowed to access a feature / make an action
         *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()
+        * ->getPermissionManager()->userHasAnyRights(...) instead
+        *
         * @param string $permissions,... Permissions to test
         * @return bool True if user is allowed to perform *any* of the given actions
         */
        public function isAllowedAny() {
-               $permissions = func_get_args();
-               foreach ( $permissions as $permission ) {
-                       if ( $this->isAllowed( $permission ) ) {
-                               return true;
-                       }
-               }
-               return false;
+               return MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAnyRight( $this, ...func_get_args() );
        }
 
        /**
-        *
+        * @deprecated since 1.34, use MediaWikiServices::getInstance()
+        * ->getPermissionManager()->userHasAllRights(...) instead
         * @param string $permissions,... Permissions to test
         * @return bool True if the user is allowed to perform *all* of the given actions
         */
        public function isAllowedAll() {
-               $permissions = func_get_args();
-               foreach ( $permissions as $permission ) {
-                       if ( !$this->isAllowed( $permission ) ) {
-                               return false;
-                       }
-               }
-               return true;
+               return MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->userHasAllRights( $this, ...func_get_args() );
        }
 
        /**
@@ -5351,7 +5347,9 @@ class User implements IDBAccessObject, UserIdentity {
                global $wgLang;
 
                $groups = [];
-               foreach ( self::getGroupsWithPermission( $permission ) as $group ) {
+               foreach ( MediaWikiServices::getInstance()
+                                         ->getPermissionManager()
+                                         ->getGroupsWithPermission( $permission ) as $group ) {
                        $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
                }
 
index bb256c9..872614c 100644 (file)
@@ -27,8 +27,8 @@
  */
 
 use CLDRPluralRuleParser\Evaluator;
+use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Assert\Assert;
 
 /**
  * Internationalisation code
@@ -38,21 +38,24 @@ class Language {
        /**
         * Return autonyms in fetchLanguageName(s).
         * @since 1.32
+        * @deprecated since 1.34, LanguageNameUtils::AUTONYMS
         */
-       const AS_AUTONYMS = null;
+       const AS_AUTONYMS = LanguageNameUtils::AUTONYMS;
 
        /**
         * Return all known languages in fetchLanguageName(s).
         * @since 1.32
+        * @deprecated since 1.34, use LanguageNameUtils::ALL
         */
-       const ALL = 'all';
+       const ALL = LanguageNameUtils::ALL;
 
        /**
         * Return in fetchLanguageName(s) only the languages for which we have at
         * least some localisation.
         * @since 1.32
+        * @deprecated since 1.34, use LanguageNameUtils::SUPPORTED
         */
-       const SUPPORTED = 'mwfile';
+       const SUPPORTED = LanguageNameUtils::SUPPORTED;
 
        /**
         * @var LanguageConverter
@@ -75,10 +78,11 @@ class Language {
         */
        public $transformData = [];
 
-       /**
-        * @var LocalisationCache
-        */
-       public static $dataCache;
+       /** @var LocalisationCache */
+       private $localisationCache;
+
+       /** @var LanguageNameUtils */
+       private $langNameUtils;
 
        public static $mLangObjCache = [];
 
@@ -94,6 +98,7 @@ class Language {
         */
        const STRICT_FALLBACKS = 1;
 
+       // TODO Make these const once we drop HHVM support (T192166)
        public static $mWeekdayMsgs = [
                'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
                'friday', 'saturday'
@@ -178,12 +183,6 @@ class Language {
         */
        private static $grammarTransformations;
 
-       /**
-        * Cache for language names
-        * @var HashBagOStuff|null
-        */
-       private static $languageNameCache;
-
        /**
         * Unicode directional formatting characters, for embedBidi()
         */
@@ -239,11 +238,12 @@ class Language {
         * @return Language
         */
        protected static function newFromCode( $code, $fallback = false ) {
-               if ( !self::isValidCode( $code ) ) {
+               $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
+               if ( !$langNameUtils->isValidCode( $code ) ) {
                        throw new MWException( "Invalid language code \"$code\"" );
                }
 
-               if ( !self::isValidBuiltInCode( $code ) ) {
+               if ( !$langNameUtils->isValidBuiltInCode( $code ) ) {
                        // It's not possible to customise this code with class files, so
                        // just return a Language object. This is to support uselang= hacks.
                        $lang = new Language;
@@ -262,7 +262,7 @@ class Language {
                // Keep trying the fallback list until we find an existing class
                $fallbacks = self::getFallbacksFor( $code );
                foreach ( $fallbacks as $fallbackCode ) {
-                       if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
+                       if ( !$langNameUtils->isValidBuiltInCode( $fallbackCode ) ) {
                                throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
                        }
 
@@ -283,37 +283,30 @@ class Language {
         * @since 1.32
         */
        public static function clearCaches() {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       throw new MWException( __METHOD__ . ' must not be used outside tests' );
+               if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MEDIAWIKI_INSTALL' ) ) {
+                       throw new MWException( __METHOD__ . ' must not be used outside tests/installer' );
+               }
+               if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
+                       MediaWikiServices::getInstance()->resetServiceForTesting( 'LocalisationCache' );
+                       MediaWikiServices::getInstance()->resetServiceForTesting( 'LanguageNameUtils' );
                }
-               self::$dataCache = null;
-               // Reinitialize $dataCache, since it's expected to always be available
-               self::getLocalisationCache();
                self::$mLangObjCache = [];
                self::$fallbackLanguageCache = [];
                self::$grammarTransformations = null;
-               self::$languageNameCache = null;
        }
 
        /**
         * Checks whether any localisation is available for that language tag
         * in MediaWiki (MessagesXx.php exists).
         *
+        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code Language tag (in lower case)
         * @return bool Whether language is supported
         * @since 1.21
         */
        public static function isSupportedLanguage( $code ) {
-               if ( !self::isValidBuiltInCode( $code ) ) {
-                       return false;
-               }
-
-               if ( $code === 'qqq' ) {
-                       return false;
-               }
-
-               return is_readable( self::getMessagesFileName( $code ) ) ||
-                       is_readable( self::getJsonMessagesFileName( $code ) );
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->isSupportedLanguage( $code );
        }
 
        /**
@@ -381,77 +374,55 @@ class Language {
         * not it exists. This includes codes which are used solely for
         * customisation via the MediaWiki namespace.
         *
+        * @deprecated since 1.34, use LanguageNameUtils
+        *
         * @param string $code
         *
         * @return bool
         */
        public static function isValidCode( $code ) {
-               static $cache = [];
-               Assert::parameterType( 'string', $code, '$code' );
-               if ( !isset( $cache[$code] ) ) {
-                       // People think language codes are html safe, so enforce it.
-                       // Ideally we should only allow a-zA-Z0-9-
-                       // but, .+ and other chars are often used for {{int:}} hacks
-                       // see bugs T39564, T39587, T38938
-                       $cache[$code] =
-                               // Protect against path traversal
-                               strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
-                               && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
-               }
-               return $cache[$code];
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code );
        }
 
        /**
         * Returns true if a language code is of a valid form for the purposes of
         * internal customisation of MediaWiki, via Messages*.php or *.json.
         *
+        * @deprecated since 1.34, use LanguageNameUtils
+        *
         * @param string $code
         *
         * @since 1.18
         * @return bool
         */
        public static function isValidBuiltInCode( $code ) {
-               Assert::parameterType( 'string', $code, '$code' );
-
-               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->isValidBuiltInCode( $code );
        }
 
        /**
         * Returns true if a language code is an IETF tag known to MediaWiki.
         *
+        * @deprecated since 1.34, use LanguageNameUtils
+        *
         * @param string $tag
         *
         * @since 1.21
         * @return bool
         */
        public static function isKnownLanguageTag( $tag ) {
-               // Quick escape for invalid input to avoid exceptions down the line
-               // when code tries to process tags which are not valid at all.
-               if ( !self::isValidBuiltInCode( $tag ) ) {
-                       return false;
-               }
-
-               if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
-                       || self::fetchLanguageName( $tag, $tag ) !== ''
-               ) {
-                       return true;
-               }
-
-               return false;
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->isKnownLanguageTag( $tag );
        }
 
        /**
         * Get the LocalisationCache instance
         *
+        * @deprecated since 1.34, use MediaWikiServices
         * @return LocalisationCache
         */
        public static function getLocalisationCache() {
-               if ( is_null( self::$dataCache ) ) {
-                       global $wgLocalisationCacheConf;
-                       $class = $wgLocalisationCacheConf['class'];
-                       self::$dataCache = new $class( $wgLocalisationCacheConf );
-               }
-               return self::$dataCache;
+               return MediaWikiServices::getInstance()->getLocalisationCache();
        }
 
        function __construct() {
@@ -462,7 +433,9 @@ class Language {
                } else {
                        $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
                }
-               self::getLocalisationCache();
+               $services = MediaWikiServices::getInstance();
+               $this->localisationCache = $services->getLocalisationCache();
+               $this->langNameUtils = $services->getLanguageNameUtils();
        }
 
        /**
@@ -494,7 +467,7 @@ class Language {
         * @return array
         */
        public function getBookstoreList() {
-               return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
+               return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
        }
 
        /**
@@ -511,7 +484,7 @@ class Language {
                                getCanonicalNamespaces();
 
                        $this->namespaceNames = $wgExtraNamespaces +
-                               self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
+                               $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
                        $this->namespaceNames += $validNamespaces;
 
                        $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
@@ -618,7 +591,7 @@ class Language {
                global $wgExtraGenderNamespaces;
 
                $ns = $wgExtraGenderNamespaces +
-                       (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
 
                return $ns[$index][$gender] ?? $this->getNsText( $index );
        }
@@ -640,7 +613,7 @@ class Language {
                        return false;
                } else {
                        // Check what is in i18n files
-                       $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
                        return count( $aliases ) > 0;
                }
        }
@@ -664,7 +637,7 @@ class Language {
         */
        public function getNamespaceAliases() {
                if ( is_null( $this->namespaceAliases ) ) {
-                       $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
+                       $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
                        if ( !$aliases ) {
                                $aliases = [];
                        } else {
@@ -678,8 +651,8 @@ class Language {
                        }
 
                        global $wgExtraGenderNamespaces;
-                       $genders = $wgExtraGenderNamespaces +
-                               (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       $genders = $wgExtraGenderNamespaces + (array)$this->localisationCache
+                               ->getItem( $this->mCode, 'namespaceGenderAliases' );
                        foreach ( $genders as $index => $forms ) {
                                foreach ( $forms as $alias ) {
                                        $aliases[$alias] = $index;
@@ -767,7 +740,7 @@ class Language {
                if ( $usemsg && wfMessage( $msg )->exists() ) {
                        return $this->getMessageFromDB( $msg );
                }
-               $name = self::fetchLanguageName( $code );
+               $name = $this->langNameUtils->getLanguageName( $code );
                if ( $name ) {
                        return $name; # if it's defined as a language name, show that
                } else {
@@ -780,21 +753,21 @@ class Language {
         * @return string[]|bool List of date format preference keys, or false if disabled.
         */
        public function getDatePreferences() {
-               return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
+               return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
        }
 
        /**
         * @return array
         */
        function getDateFormats() {
-               return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
+               return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
        }
 
        /**
         * @return array|string
         */
        public function getDefaultDateFormat() {
-               $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
+               $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
                if ( $df === 'dmy or mdy' ) {
                        global $wgAmericanDates;
                        return $wgAmericanDates ? 'mdy' : 'dmy';
@@ -807,7 +780,7 @@ class Language {
         * @return array
         */
        public function getDatePreferenceMigrationMap() {
-               return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
+               return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
        }
 
        /**
@@ -828,6 +801,8 @@ class Language {
 
        /**
         * Get an array of language names, indexed by code.
+        *
+        * @deprecated since 1.34, use LanguageNameUtils::getLanguageNames
         * @param null|string $inLanguage Code of language in which to return the names
         *              Use self::AS_AUTONYMS for autonyms (native names)
         * @param string $include One of:
@@ -838,95 +813,12 @@ class Language {
         * @since 1.20
         */
        public static function fetchLanguageNames( $inLanguage = self::AS_AUTONYMS, $include = 'mw' ) {
-               $cacheKey = $inLanguage === self::AS_AUTONYMS ? 'null' : $inLanguage;
-               $cacheKey .= ":$include";
-               if ( self::$languageNameCache === null ) {
-                       self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
-               }
-
-               $ret = self::$languageNameCache->get( $cacheKey );
-               if ( !$ret ) {
-                       $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
-                       self::$languageNameCache->set( $cacheKey, $ret );
-               }
-               return $ret;
-       }
-
-       /**
-        * Uncached helper for fetchLanguageNames
-        * @param null|string $inLanguage Code of language in which to return the names
-        *              Use self::AS_AUTONYMS for autonyms (native names)
-        * @param string $include One of:
-        *              self::ALL all available languages
-        *              'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
-        *              self::SUPPORTED only if the language is in 'mw' *and* has a message file
-        * @return array Language code => language name (sorted by key)
-        */
-       private static function fetchLanguageNamesUncached(
-               $inLanguage = self::AS_AUTONYMS,
-               $include = 'mw'
-       ) {
-               global $wgExtraLanguageNames, $wgUsePigLatinVariant;
-
-               // If passed an invalid language code to use, fallback to en
-               if ( $inLanguage !== self::AS_AUTONYMS && !self::isValidCode( $inLanguage ) ) {
-                       $inLanguage = 'en';
-               }
-
-               $names = [];
-
-               if ( $inLanguage ) {
-                       # TODO: also include when $inLanguage is null, when this code is more efficient
-                       Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
-               }
-
-               $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
-               if ( $wgUsePigLatinVariant ) {
-                       // Pig Latin (for variant development)
-                       $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
-               }
-
-               foreach ( $mwNames as $mwCode => $mwName ) {
-                       # - Prefer own MediaWiki native name when not using the hook
-                       # - For other names just add if not added through the hook
-                       if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
-                               $names[$mwCode] = $mwName;
-                       }
-               }
-
-               if ( $include === self::ALL ) {
-                       ksort( $names );
-                       return $names;
-               }
-
-               $returnMw = [];
-               $coreCodes = array_keys( $mwNames );
-               foreach ( $coreCodes as $coreCode ) {
-                       $returnMw[$coreCode] = $names[$coreCode];
-               }
-
-               if ( $include === self::SUPPORTED ) {
-                       $namesMwFile = [];
-                       # We do this using a foreach over the codes instead of a directory
-                       # loop so that messages files in extensions will work correctly.
-                       foreach ( $returnMw as $code => $value ) {
-                               if ( is_readable( self::getMessagesFileName( $code ) )
-                                       || is_readable( self::getJsonMessagesFileName( $code ) )
-                               ) {
-                                       $namesMwFile[$code] = $names[$code];
-                               }
-                       }
-
-                       ksort( $namesMwFile );
-                       return $namesMwFile;
-               }
-
-               ksort( $returnMw );
-               # 'mw' option; default if it's not one of the other two options (all/mwfile)
-               return $returnMw;
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->getLanguageNames( $inLanguage, $include );
        }
 
        /**
+        * @deprecated since 1.34, use LanguageNameUtils::getLanguageName
         * @param string $code The code of the language for which to get the name
         * @param null|string $inLanguage Code of language in which to return the name
         *   (SELF::AS_AUTONYMS for autonyms)
@@ -939,9 +831,8 @@ class Language {
                $inLanguage = self::AS_AUTONYMS,
                $include = self::ALL
        ) {
-               $code = strtolower( $code );
-               $array = self::fetchLanguageNames( $inLanguage, $include );
-               return !array_key_exists( $code, $array ) ? '' : $array[$code];
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->getLanguageName( $code, $inLanguage, $include );
        }
 
        /**
@@ -2274,7 +2165,8 @@ class Language {
                }
 
                if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
-                       $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
+                       $df =
+                               $this->localisationCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
 
                        if ( $type === 'pretty' && $df === null ) {
                                $df = $this->getDateFormatString( 'date', $pref );
@@ -2282,7 +2174,8 @@ class Language {
 
                        if ( !$wasDefault && $df === null ) {
                                $pref = $this->getDefaultDateFormat();
-                               $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
+                               $df = $this->getLocalisationCache()
+                                       ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                        }
 
                        $this->dateFormatStrings[$type][$pref] = $df;
@@ -2646,14 +2539,14 @@ class Language {
         * @return string|null
         */
        public function getMessage( $key ) {
-               return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
+               return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
        }
 
        /**
         * @return array
         */
        function getAllMessages() {
-               return self::$dataCache->getItem( $this->mCode, 'messages' );
+               return $this->localisationCache->getItem( $this->mCode, 'messages' );
        }
 
        /**
@@ -2895,7 +2788,7 @@ class Language {
         * @return string
         */
        function fallback8bitEncoding() {
-               return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
+               return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
        }
 
        /**
@@ -3085,7 +2978,7 @@ class Language {
         * @return bool
         */
        function isRTL() {
-               return self::$dataCache->getItem( $this->mCode, 'rtl' );
+               return $this->localisationCache->getItem( $this->mCode, 'rtl' );
        }
 
        /**
@@ -3161,7 +3054,7 @@ class Language {
         * @return array
         */
        function capitalizeAllNouns() {
-               return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
+               return $this->localisationCache->getItem( $this->mCode, 'capitalizeAllNouns' );
        }
 
        /**
@@ -3194,7 +3087,7 @@ class Language {
         * @return bool
         */
        function linkPrefixExtension() {
-               return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
+               return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
        }
 
        /**
@@ -3202,7 +3095,7 @@ class Language {
         * @return array
         */
        function getMagicWords() {
-               return self::$dataCache->getItem( $this->mCode, 'magicWords' );
+               return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
        }
 
        /**
@@ -3212,7 +3105,7 @@ class Language {
         */
        function getMagic( $mw ) {
                $rawEntry = $this->mMagicExtensions[$mw->mId] ??
-                       self::$dataCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
+                       $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
 
                if ( !is_array( $rawEntry ) ) {
                        wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
@@ -3247,7 +3140,7 @@ class Language {
                if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
                        // Initialise array
                        $this->mExtendedSpecialPageAliases =
-                               self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
+                               $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
                }
 
                return $this->mExtendedSpecialPageAliases;
@@ -3412,28 +3305,28 @@ class Language {
         * @return string
         */
        function digitGroupingPattern() {
-               return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
+               return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
        }
 
        /**
         * @return array
         */
        function digitTransformTable() {
-               return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
+               return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
        }
 
        /**
         * @return array
         */
        function separatorTransformTable() {
-               return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
+               return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
        }
 
        /**
         * @return int|null
         */
        function minimumGroupingDigits() {
-               return self::$dataCache->getItem( $this->mCode, 'minimumGroupingDigits' );
+               return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' );
        }
 
        /**
@@ -4333,7 +4226,7 @@ class Language {
         * @return string
         */
        public function linkTrail() {
-               return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
+               return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
        }
 
        /**
@@ -4343,7 +4236,7 @@ class Language {
         * @return string
         */
        public function linkPrefixCharset() {
-               return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
+               return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
        }
 
        /**
@@ -4445,6 +4338,8 @@ class Language {
 
        /**
         * Get the name of a file for a certain language code
+        *
+        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $prefix Prepend this to the filename
         * @param string $code Language code
         * @param string $suffix Append this to the filename
@@ -4452,38 +4347,30 @@ class Language {
         * @return string $prefix . $mangledCode . $suffix
         */
        public static function getFileName( $prefix, $code, $suffix = '.php' ) {
-               if ( !self::isValidBuiltInCode( $code ) ) {
-                       throw new MWException( "Invalid language code \"$code\"" );
-               }
-
-               return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->getFileName( $prefix, $code, $suffix );
        }
 
        /**
+        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code
         * @return string
         */
        public static function getMessagesFileName( $code ) {
-               global $IP;
-               $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
-               Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
-               return $file;
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->getMessagesFileName( $code );
        }
 
        /**
+        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code
         * @return string
         * @throws MWException
         * @since 1.23
         */
        public static function getJsonMessagesFileName( $code ) {
-               global $IP;
-
-               if ( !self::isValidBuiltInCode( $code ) ) {
-                       throw new MWException( "Invalid language code \"$code\"" );
-               }
-
-               return "$IP/languages/i18n/$code.json";
+               return MediaWikiServices::getInstance()->getLanguageNameUtils()
+                       ->getJsonMessagesFileName( $code );
        }
 
        /**
@@ -4933,11 +4820,13 @@ class Language {
         * @return array Associative array with plural form, and plural rule as key-value pairs
         */
        public function getCompiledPluralRules() {
-               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
+               $pluralRules =
+                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRules ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
+                               $pluralRules = $this->localisationCache
+                                       ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
                                if ( $pluralRules ) {
                                        break;
                                }
@@ -4952,11 +4841,13 @@ class Language {
         * @return array Associative array with plural form number and plural rule as key-value pairs
         */
        public function getPluralRules() {
-               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
+               $pluralRules =
+                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRules ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
+                               $pluralRules = $this->localisationCache
+                                       ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
                                if ( $pluralRules ) {
                                        break;
                                }
@@ -4971,11 +4862,13 @@ class Language {
         * @return array Associative array with plural form number and plural rule type as key-value pairs
         */
        public function getPluralRuleTypes() {
-               $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
+               $pluralRuleTypes =
+                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRuleTypes ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
+                               $pluralRuleTypes = $this->localisationCache
+                                       ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
                                if ( $pluralRuleTypes ) {
                                        break;
                                }
index 00f35b2..1d80f6b 100644 (file)
@@ -39,7 +39,7 @@ namespace MediaWiki\Languages\Data;
  * If you are adding support for such a language, add it also to
  * the relevant section in shared.css.
  *
- * Do not use this class directly. Use Language::fetchLanguageNames(), which
+ * Do not use this class directly. Use LanguageNameUtils::getLanguageNames(), which
  * includes support for the CLDR extension.
  *
  * @ingroup Language
index f052ccf..23cdb22 100644 (file)
@@ -12,7 +12,8 @@
                        "Si Gam Acèh",
                        "아라",
                        "Macofe",
-                       "Rachmat04"
+                       "Rachmat04",
+                       "Martin Urbanec"
                ]
        },
        "tog-underline": "Bôh garéh yup peunawôt:",
        "booksources-search-legend": "Mita bak nè kitab",
        "booksources-search": "Mita",
        "specialloguserlabel": "Ureuëng ngui:",
-       "speciallogtitlelabel": "Sasaran (judu atawa {{ns:ureueng ngui}}:nan ureueng ngui keu ureueng ngui)",
+       "speciallogtitlelabel": "Sasaran (judu atawa {{ns:user}}:nan ureueng ngui keu ureueng ngui)",
        "log": "Log",
        "all-logs-page": "Ban dum log umom",
        "allpages": "Ban dum laman",
index b9c8dc7..04f2722 100644 (file)
        "block-log-flags-angry-autoblock": "rozšířené automatické blokování zapnuto",
        "block-log-flags-hiddenname": "uživatelské jméno skryto",
        "range_block_disabled": "Blokování rozsahů IP adres je zakázáno.",
+       "ipb-prevent-user-talk-edit": "U částečných bloků musí být editace vlastní uživatelské diskuse povolena, pokud blok nezahrnuje omezení jmenného prostoru {{ns:3}}.",
        "ipb_expiry_invalid": "Neplatný čas vypršení.",
        "ipb_expiry_old": "Čas vypršení je v minulosti.",
        "ipb_expiry_temp": "Blokování skrytých uživatelských jmen by měla být trvalá.",
        "delete_and_move_reason": "Smazáno pro umožnění přesunu z „[[$1]]“",
        "selfmove": "Název je stejný; nelze stránku přesunout na sebe samu.",
        "immobile-source-namespace": "Stránky ve jmenném prostoru „$1“ nelze přesouvat",
+       "immobile-source-namespace-iw": "Z této wiki nelze přesouvat stránky na jiných wiki.",
        "immobile-target-namespace": "Stránky nelze přesouvat do jmenného prostoru „$1“",
        "immobile-target-namespace-iw": "Mezijazykový odkaz není validní cíl při přesouvání stránky.",
        "immobile-source-page": "Tuto stránku nelze přesouvat.",
        "permanentlink": "Trvalý odkaz",
        "permanentlink-revid": "ID revize",
        "permanentlink-submit": "Přejít na revizi",
+       "newsection": "Nová sekce",
+       "newsection-page": "Cílová stránka",
+       "newsection-submit": "Jít na stránku",
        "dberr-problems": "Promiňte! Tento server má v tuto chvíli technické problémy.",
        "dberr-again": "Zkuste několik minut počkat a poté znovu načíst stránku.",
        "dberr-info": "(Nelze se připojit k databázi: $1)",
        "restrictionsfield-help": "Jedna IP adresa nebo CIDR rozsah na řádek. Všechno povolíte pomocí:<pre>0.0.0.0/0\n::/0</pre>",
        "edit-error-short": "Chyba: $1",
        "edit-error-long": "Chyby:\n\n$1",
+       "specialmute": "Ztlumení",
+       "specialmute-success": "Požadované ztlumení bylo upraveno. Všechny ztlumené uživatele najdete ve [[Special:Preferences|svém nastavení]].",
+       "specialmute-submit": "Potvrdit",
+       "specialmute-label-mute-email": "Ignorovat e-maily od tohoto uživatele",
+       "specialmute-header": "Vyberte si prosím požadované ztlumení uživatele <b>{{BIDI:[[User:$1|$1]]}}</b>.",
+       "specialmute-error-invalid-user": "Požadované uživatelské jméno nebylo nalezeno.",
+       "specialmute-error-no-options": "Funkce ztlumení uživatele není dostupná. Důvodem může být: neověřili jste svou e-mailovou adresu nebo administrátor wiki na této wiki vypnul e-mailové funkce nebo listinu zakázaných e-mailů.",
+       "specialmute-email-footer": "Spravovat nastavení e-mailů od uživatele {{BIDI:$2}} můžete na <$1>.",
+       "specialmute-login-required": "Pro změnu ztlumení se musíte přihlásit.",
+       "mute-preferences": "Nastavení ztlumení",
        "revid": "revize $1",
        "pageid": "Stránka s ID $1",
        "interfaceadmin-info": "$1\n\nOprávnění editovat celoprojektové soubory s CSS/JS/JSON bylo nedávno odděleno z oprávnění <code>editinterface</code>. Pokud nerozumíte, proč se vám zobrazuje tato chyba, vizte [[mw:MediaWiki_1.32/interface-admin]].",
index c806122..5cae831 100644 (file)
        "move-subpages": "Renommer les sous-pages (maximum $1)",
        "move-talk-subpages": "Renommer les sous-pages de la page de discussion (maximum $1)",
        "movepage-page-exists": "La page $1 existe déjà et ne peut pas être écrasée automatiquement.",
-       "movepage-source-doesnt-exist": "La page $1 n’existe pas et n’a pas pu être supprimée.",
+       "movepage-source-doesnt-exist": "La page $1 n’existe pas et n’a pas pu être renommée.",
        "movepage-page-moved": "La page $1 a été renommée en $2.",
        "movepage-page-unmoved": "La page $1 n'a pas pu être renommée en $2.",
        "movepage-max-pages": "Le maximum de $1 {{PLURAL:$1|page renommée|pages renommées}} a été atteint et aucune autre page ne sera renommée automatiquement.",
        "delete_and_move_reason": "Page supprimée pour permettre le renommage depuis « [[$1]] »",
        "selfmove": "Le titre est le même ;\nimpossible de renommer une page sur elle-même.",
        "immobile-source-namespace": "Vous ne pouvez pas renommer les pages dans l'espace de noms « $1 »",
-       "immobile-source-namespace-iw": "Les pages sur d’autres wikis ne peuvent être déplacées depuis ce wiki.",
+       "immobile-source-namespace-iw": "Il n'est pas possible de déplacer les pages depuis ce wiki vers les autres wikis.",
        "immobile-target-namespace": "Vous ne pouvez pas renommer des pages vers l’espace de noms « $1 ».",
        "immobile-target-namespace-iw": "Un lien interwiki n’est pas une cible valide pour un renommage de page.",
        "immobile-source-page": "Cette page n'est pas renommable.",
index ebca6ef..f95fc94 100644 (file)
        "rcfilters-filter-showlinkedto-label": "הצגת שינויים בדפים שמקשרים אל",
        "rcfilters-filter-showlinkedto-option-label": "<strong>דפים שמקשרים אל</strong> הדף שנבחר",
        "rcfilters-target-page-placeholder": "יש להקליד שם דף (או קטגוריה)",
+       "rcfilters-allcontents-label": "כל התכנים",
+       "rcfilters-alldiscussions-label": "כל הדיונים",
        "rcnotefrom": "להלן {{PLURAL:$5|השינוי שבוצע|השינויים שבוצעו}} מאז <strong>$3, $4</strong> (מוצגים עד <strong>$1</strong>).",
        "rclistfromreset": "איפוס בחירת התאריך",
        "rclistfrom": "הצגת שינויים חדשים החל מ־$2, $3",
        "move-subpages": "העברת דפי המשנה (עד $1)",
        "move-talk-subpages": "העברת דפי המשנה של דף השיחה (עד $1)",
        "movepage-page-exists": "הדף $1 קיים כבר ולא ניתן לדרוס אותו אוטומטית.",
+       "movepage-source-doesnt-exist": "הדף $1 אינו קיים ולא ניתן להעבירו.",
        "movepage-page-moved": "הדף $1 הועבר לשם $2.",
        "movepage-page-unmoved": "לא ניתן להעביר את הדף $1 לשם $2.",
        "movepage-max-pages": "{{PLURAL:$1|דף אחד כבר הועבר|$1 דפים כבר הועברו}}. זה המספר המרבי ולא ניתן להעביר דפים נוספים אוטומטית.",
        "delete_and_move_reason": "מחיקה כדי לאפשר העברה מהשם \"[[$1]]\"",
        "selfmove": "הכותרת זהה;\nלא ניתן להעביר דף לעצמו.",
        "immobile-source-namespace": "לא ניתן להעביר דפים במרחב השם \"$1\".",
+       "immobile-source-namespace-iw": "לא ניתן להעביר דפים באתרי ויקי אחרים מתוך אתר הוויקי הזה.",
        "immobile-target-namespace": "לא ניתן להעביר דפים למרחב השם \"$1\".",
        "immobile-target-namespace-iw": "קישור בינוויקי אינו יעד תקין להעברת דף.",
        "immobile-source-page": "דף זה אינו ניתן להעברה.",
        "immobile-target-page": "לא ניתן להעביר אל כותרת יעד זו.",
+       "movepage-invalid-target-title": "השם המבוקש אינו תקין.",
        "bad-target-model": "היעד המבוקש משתמש במודל תוכן שונה. לא ניתן להמיר $1 ל{{grammar:תחילית|$2}}.",
        "imagenocrossnamespace": "לא ניתן להעביר קובץ למרחב שם אחר.",
        "nonfile-cannot-move-to-file": "לא ניתן להעביר דף שאינו קובץ למרחב קובץ.",
index 3b78ced..e1710b7 100644 (file)
        "right-move": "Papụ̀ ihuâ",
        "right-movefile": "Papụ̀ àfabà",
        "right-upload": "Tịnyé ihe na nsónùsòrò",
+       "right-writeapi": "Iji ede API",
        "right-delete": "Kàchafu ihü",
        "right-bigdelete": "Kàcha ihü nwéré ákíkó mbu dí ógólógó",
        "right-undelete": "Ágbakashia ótù ihü",
        "recentchanges-label-bot": "Bot deziri ihe a",
        "recentchanges-label-unpatrolled": "ebugharịbegi ndezi a",
        "recentchanges-label-plusminus": "Pegi a agbanwela na otu ọha site na ọnu ọgụgụ bayits",
+       "recentchanges-legend-heading": "<strong>Isi-okwu</strong>",
        "recentchanges-legend-newpage": "$1 - ihü ohúrù",
        "rcfilters-savedqueries-cancel-label": "Hapụ̀",
        "rclistfrom": "Zìrí ihe gbanwere ọhúrù shí $3 $2",
        "filehist-filesize": "Ívù usòrò",
        "filehist-comment": "Nkwute",
        "imagelinks": "Mgbanwe usòrò",
-       "linkstoimage": "{{PLURAL:$1|Ihü nká|Ihü nke $1}} na jikodo gá usòrò nká:",
+       "linkstoimage": "Ihe ndị na-eso {{PLURAL:$1|ihe eji Ihu akwụkwọ eme|$1 ihe eji Ihu akwụkwọ eme}} na faịlụ a:",
        "nolinkstoimage": "Ọdighi ihuakwụkwọ nwere failụ a.",
        "sharedupload": "Ákwúkwó runotu nke shì $1 na ó nwèríkí di na orürü nke ndi ozor.",
        "sharedupload-desc-here": "Failụ a si na $1,enwekwara ike iji ya eme ihe na arụmarụ ọzọ. Nkọwa na [$2 ihuakwukwọ nkọwa failụ] eziri na okpuru.",
        "undelete-show-file-submit": "Eeh",
        "namespace": "Ahàm̀bara:",
        "invert": "Tụgha ǹke ǹhọ̀rọ",
+       "tooltip-invert": "Kachie igbe a izocha mgbanwe Ihu-akwụkwọ ndị nnọ na aha-ebe ahọpụtara(yana aha-ebe jikọtara ya m'obụrụ na akachiri ya)",
+       "namespace_association": "Nyìrí aha-ebe",
+       "tooltip-namespace_association": "Kachie igbea itinye kwa okwu ma ọbụ isi-okwu aha-ebe jikọtara aha ahọpụtara",
        "blanknamespace": "(Ḿkpà)",
        "contributions": "atụmatụ metụrara Jenda.{{GENDER:$1|User}}",
        "contributions-title": "Orü ọ'bànifé nà $1",
        "svg-long-desc": "usòrò SVG, nà áhà pixel $1 × $2, ívụ usòrò: $3",
        "show-big-image": "Failụ si na nke mbu",
        "show-big-image-preview": "Otu nyochaa a ha:$1",
+       "show-big-image-other": "Ndị ọzọ {{PLURAL:$2|mkpebi|mkpebi}}:$1.",
        "show-big-image-size": "$1 × $2 piksels",
        "file-info-gif-looped": "etemte",
        "newimages-legend": "Nzàtà",
index f797c29..26532b6 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Mostra le modifiche alle pagine che collegano a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Pagine con collegamenti a</strong> la pagina selezionata",
        "rcfilters-target-page-placeholder": "Inserisci il nome di una pagina (o categoria)",
+       "rcfilters-alldiscussions-label": "Tutte le discussioni",
        "rcnotefrom": "Di seguito {{PLURAL:$5|è elencata la modifica apportata|sono elencate le modifiche apportate}} a partire da <strong>$3, $4</strong> (mostrate fino a <strong>$1</strong>).",
        "rclistfromreset": "Reimposta la selezione della data",
        "rclistfrom": "Mostra le nuove modifiche a partire daː $2, $3",
index 0f789a0..ab7f1ef 100644 (file)
        "rcfilters-filter-showlinkedto-label": "다음 문서로 링크한 문서의 변경사항 보기",
        "rcfilters-filter-showlinkedto-option-label": "<strong>선택된 문서로 링크하는</strong> 문서들",
        "rcfilters-target-page-placeholder": "문서 이름(또는 분류)을 입력하세요",
+       "rcfilters-allcontents-label": "모든 내용",
+       "rcfilters-alldiscussions-label": "모든 토론",
        "rcnotefrom": "아래는 <strong>$3, $4</strong>부터 시작하는 {{PLURAL:$5|바뀜이 있습니다}}. (최대 <strong>$1</strong>개가 표시됨)",
        "rclistfromreset": "날짜 선택 초기화",
        "rclistfrom": "$3 $2부터 시작하는 새로 바뀐 문서 보기",
index de98e58..0516e3e 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Прикажи промени во страници кои водат кон",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Страници кои води кон</strong> избраната страница",
        "rcfilters-target-page-placeholder": "Внесете име на страница (или категорија)",
+       "rcfilters-allcontents-label": "Сета содржина",
+       "rcfilters-alldiscussions-label": "Сите разговори",
        "rcnotefrom": "Подолу {{PLURAL:$5|е прикажана промената|се прикажани промените}} почнувајќи од <strong>$3, $4</strong>  (се прикажуваат до <b>$1</b>).",
        "rclistfromreset": "Нов избор на датуми",
        "rclistfrom": "Прикажи нови промени почнувајќи од $3 $2",
        "move-subpages": "Премести ги и потстраниците (највеќе до $1)",
        "move-talk-subpages": "Премести потстраници на разговорни страници (највеќе до $1)",
        "movepage-page-exists": "Страницата $1 веќе постои и не може автоматски да биде заменета.",
+       "movepage-source-doesnt-exist": "Страницата „$1“ не постои и затоа не може да се премести.",
        "movepage-page-moved": "Страницата $1 е преместена на $2.",
        "movepage-page-unmoved": "Страницата $1 не може да биде преместена во $2.",
        "movepage-max-pages": "{{PLURAL:$1|Преместен е највеќе $1 страница|Преместени се највеќе $1 страници}}. Повеќе од тоа не може да се преместува автоматски.",
        "delete_and_move_reason": "Избришано за да се ослободи место за преместувањето од „[[$1]]“",
        "selfmove": "Насловот е истоветен;\nне можам да го преместам на самиот себе.",
        "immobile-source-namespace": "Не може да се преместуваат страници во именскиот простор „$1“",
+       "immobile-source-namespace-iw": "Од ова вики не можат да се преместат страници на други викија.",
        "immobile-target-namespace": "Не може да се преместуваат страници во именскиот простор „$1“",
        "immobile-target-namespace-iw": "Меѓупроектна врска не може да се користи за преименување на страници.",
        "immobile-source-page": "Оваа страница не може да се преместува.",
        "immobile-target-page": "Не може да се премести под бараниот наслов.",
+       "movepage-invalid-target-title": "Побараното име е неважечко.",
        "bad-target-model": "Саканата одредница користи друг содржински модел. Не можам да претворам од $1 во $2.",
        "imagenocrossnamespace": "Не може да се премести податотека во неподатотечен именски простор",
        "nonfile-cannot-move-to-file": "Не можам да преместам неподатотека во податотечен именски простор",
index c75710f..250dc65 100644 (file)
        "mainpage": "Paggena prencepale",
        "mainpage-description": "Paggena prencepale",
        "policy-url": "Project:Policy",
-       "portal": "Porta d'<nowiki/>'a commonetà",
+       "portal": "Porta d'a commonetà",
        "portal-url": "Project:Porta d''a commonetà",
        "privacy": "'Nformazzione ppe a privacy",
        "privacypage": "Project:'Nfrummazione ncopp'â privacy",
        "virus-scanfailed": "scanziona fallita (codece $1)",
        "virus-unknownscanner": "antivirus scanusciuto:",
        "logouttext": "'''Site asciùte.'''\n\nNota ca arcune paggene putessero cuntinuà ad cumparì comme se 'o logout nun fosse affettuato fin quanno nun sarrà pulezzata 'a cache d\"o proprio browser.",
+       "logging-out-notify": "Staje ascenno, aspietta.",
+       "logout-failed": "Nun se può ascì mo: $1",
        "cannotlogoutnow-title": "Mo nun se pò ascì",
        "cannotlogoutnow-text": "'A disconessione nun è possibbele quanno s'ausa $1.",
        "welcomeuser": "Bemmenuto, $1!",
        "badretype": "'E passwords ch'è mis nun songe eguale.",
        "usernameinprogress": "Na criazione 'e cunto pe' st'utente è già nprugresso. Pe' piacere aspettate.",
        "userexists": "'O nomme utente ch'avete miso è già ausàto.\nPe' piacere sciglite n'atu nomme.",
+       "createacct-normalization": "'O nomme tuio sarrà cagnato a \"$2\" pe raggioni tecniche.",
        "loginerror": "Probblema 'e accièsso",
        "createacct-error": "Errore 'e criazione 'e cunto",
        "createaccounterror": "Nun se può crià nu cunto: $1",
        "resetpass-abort-generic": "'O cagnamiento d' 'a password s'è spezzato 'a na stensione.",
        "resetpass-expired": "'A pasword è ammaturata. Avite 'e ffà na password nova pe putè trasì.",
        "resetpass-expired-soft": "'A pasword vuost è ammaturata e s'adda cagnà. Avite 'e scegliere na password nova mò, o ffà click ncopp'a \"{{int:authprovider-resetpass-skip-label}}\" p' 'a cagnà aroppo.",
+       "resetpass-validity": "'A pasword toia nun è bbona: $1",
        "resetpass-validity-soft": "'A password toja nun è bbona: $1\n\nAvite 'e scegliere na password nova mò, o ffà click ncopp'a \"{{int:authprovider-resetpass-skip-label}}\" p' 'a cagnà aròppo.",
        "passwordreset": "Riabbìa 'a password",
        "passwordreset-text-one": "Ghienche stu modulo pe' ricevere na mmasciata e-mail c' 'a password temporanea.",
        "histfirst": "primma",
        "histlast": "urdema",
        "historysize": "({{PLURAL:$1|1 byte|$1 byte}})",
-       "historyempty": "(abbacante)",
+       "historyempty": "abbacante",
        "history-feed-title": "Cronologgia",
        "history-feed-description": "Cronologgia d' 'a paggena ncopp'a stu sito",
        "history-feed-item-nocomment": "$1 'o $2",
        "rcfilters-savedqueries-apply-label": "Crea filtro",
        "rcfilters-savedqueries-cancel-label": "Scancella",
        "rcfilters-clear-all-filters": "Pulezza tutt' 'e filtre",
-       "rcfilters-show-new-changes": "Vide 'e cagnamiente cchiù nnove",
+       "rcfilters-show-new-changes": "Vide 'e cagnamiente cchiù nnove 'e $1",
        "rcfilters-invalid-filter": "Filtro invalido",
        "rcfilters-filterlist-title": "Filtre",
        "rcfilters-filterlist-whatsthis": "Cumme funzionano?",
        "rcfilters-highlightmenu-help": "Piglia nu culore p'evidenzià sta proprietà",
        "rcfilters-filterlist-noresults": "Nisciuno filtro truvato",
        "rcfilters-noresults-conflict": "Nun s'hanno truvato risultati pecché 'a cerca tene nu cunflitto",
-       "rcfilters-state-message-subset": "Sto filtro nun tene effetti pecché 'e risultati suoi traseno 'int' {{{{PLURAL:$2|'e cerca|cerche}} cchiù gruosse (pruova 'a evidenzià pe verè): $1",
+       "rcfilters-state-message-subset": "Sto filtro nun tene effetti pecché 'e risultati suoi traseno 'int' {{{{PLURAL:$2|'a cerca|'e ccerche}} cchiù gruosse (pruova 'a evidenzià pe verè): $1",
        "rcfilters-filtergroup-authorship": "Autore d' 'o cuntribbuto",
        "rcfilters-filter-editsbyself-label": "Cagnamiénte d'ê tuoie",
        "rcfilters-filter-editsbyself-description": "Contribbute d'ê tuoie",
        "rcfilters-filter-watchlistactivity-seen-description": "Càgni a paggene ch'hê visto 'a cuanno facettero ll'urdimo cagnamiénto.",
        "rcfilters-filtergroup-lastrevision": "Ùrdeme verziune",
        "rcfilters-filter-lastrevision-label": "Verzione 'e mmo",
+       "rcfilters-tag-prefix-namespace-inverted": "<strong>:no</strong> $1",
        "rcfilters-watchlist-markseen-button": "Segna tutt'ê cagni comme visti",
        "rcfilters-watchlist-edit-watchlist-button": "Càgna 'e lista tuia d'ê paggene cuntrullate",
        "rcfilters-watchlist-showupdated": "'E càgne 'e ppaggene ca nun hê visto so' 'e <strong>niro</strong> e ch'ê ppalluccelle chiene.",
        "deadendpages": "Paggene ca nun spòntano",
        "deadendpagestext": "'E paggene ccà abbascio nun spontano a n'ati paggene ncopp'a {{SITENAME}}.",
        "protectedpages": "Paggene prutette",
+       "protectedpages-filters": "Filtri:",
        "protectedpages-indef": "Sulamente prutezziune a tiempo nun definito",
        "protectedpages-summary": "Sta paggena elenca 'e paggene ch'esisteno e ca mo stanne prutette. P'avé n'elenco 'e titule prutette â criazione, vedite [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "Sulamente prutezziune ricurzive",
        "deleting-backlinks-warning": "<strong>Attenzione:</strong>\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|ati paggene]] cunteneno cullegamiente o paggene appennute â n'ata paggena ca state pe' scancellà.",
        "deleting-subpages-warning": "<strong>Accuorto:</strong> 'A paggena ca staie pe scancellà tene  [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|na sottopaggena|$1 sottopaggene|51=cchiù 'e 50 sottopaggene}}]].",
        "rollback": "Ausa na revizione 'e primma",
+       "rollback-confirmation-yes": "Sfàjere",
+       "rollback-confirmation-no": "Scancella",
        "rollbacklink": "sfàjere",
        "rollbacklinkcount": "sfàje {{PLURAL:$1|nu cagnamiento|$1 cagnamiente}}",
        "rollbacklinkcount-morethan": "sfàje cchiù 'e {{PLURAL:$1|nu cagnamiento|$1 cagnamiente}}",
        "mycontris": "'E ffatiche d''e mmeje",
        "anoncontribs": "Cuntribbute",
        "contribsub2": "Ppe {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "Pe {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "'O cunto utente \"$1\" nun è riggistrato.",
        "nocontribs": "Nisciunu cagnamiento è stato truvato cu sti criterie.",
        "uctop": "attuale",
        "ipb-disableusertalk": "Nun permettere a st'utente edità 'a paggena 'e chiacchiera d' 'a soja pe' tramente ch'e bloccato",
        "ipb-change-block": "Fremma n'ata vota ll'utente cu ste mpustaziune",
        "ipb-confirm": "Cunferma 'o blocco",
+       "ipb-sitewide": "Pe tutte parte",
        "ipb-pages-label": "Paggene",
        "badipaddress": "Indirizzo IP nun valido",
        "blockipsuccesssub": "Blocco aseguito",
index 6abed76..f64aee4 100644 (file)
        "search-category": "(kategori $1)",
        "search-file-match": "(matcher filinnhold)",
        "search-suggest": "Mente du: $1",
-       "search-rewritten": "Viser resultatet for $1. Søk i stedet for $2.",
+       "search-rewritten": "Viser resultater for $1. Søk etter $2 i stedet.",
        "search-interwiki-caption": "Resultater fra søsterprosjekter",
        "search-interwiki-default": "Resultater fra $1:",
        "search-interwiki-more": "(mer)",
index 0c02b1b..aad736d 100644 (file)
        "movethispage": "यो पृष्ठ सार्नुहोस्",
        "unusedimagestext": "निम्न फाइलहरू छन्, तर कुनै पनि पृष्ठमा प्रयोग गरिएको छैन। कृपया ध्यान दें कि अन्य वेबसाइट एउटा सिधै लिङ्कको फाइलसँग जोड्न सकिन्छ, र सक्रिय उपयोगमा हुँदा पनि यहाँ देखाउन सकिन्छ।",
        "unusedcategoriestext": "तल श्रेणीका पृष्ठहरू उपलब्ध भएता पनि उक्त पृष्ठहरूलाई अन्य पृष्ठहरू तथा श्रेणीले प्रयोग गर्न सक्दैनन् ।",
-       "notargettitle": "कुनैपनि निसाना(टारगेट) छैन",
+       "notargettitle": "कुनैपनि निसाना छैन",
        "notargettext": "यो कार्यको लागि तपाईँले कुनै लक्षित पृष्ठ वा प्रयोगकर्ता निर्दिष्ट गर्नु भएको छैन ।",
        "nopagetitle": "त्यस्तो गन्तव्या पृष्ठ भेटिएन",
        "nopagetext": "तपाईंले खुलाउनु भएको गन्तव्य पृष्ठ अस्तित्वमा  छैन।",
index 0316918..91239d3 100644 (file)
        "confirmemail_success": "ߌ ߟߊ߫ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߓߘߊ߫ ߓߊ߲߫ ߟߊߛߙߋߦߊ߫ ߟߊ߫.\nߌ ߘߌ߫ ߛߴߌ  ߜߊ߲߬ߞߎ߲߬ ߠߊ߫ [[Special:UserLogin|log in]] ߡߎ߬ߕߎ߲߬ ߞߊ߬ ߛߍߥߊ߫ ߥߞߌ ߟߊ߫.",
        "confirmemail_loggedin": "ߌ ߟߊ߫ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߓߘߊ߫ ߓߊ߲߫ ߟߊߛߙߋߦߊ߫ ߟߊ߫.",
        "confirmemail_subject": "{{SITENAME}} ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ",
+       "confirmemail_invalidated": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ ߓߘߊ߫ ߘߐߛߊ߬",
+       "invalidateemail": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ ߘߐߛߊ߬",
+       "notificationemail_subject_changed": "{{SITENAME}} ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ߫ ߛߊ߲߬ߓߊ߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߓߘߊ߫ ߡߊߝߊ߬ߟߋ߲߬.",
+       "notificationemail_subject_removed": "{{SITENAME}} ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߓߊ߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߓߘߊ߫ ߓߐ߫ ߊ߬ ߟߊ߫.",
        "scarytranscludetoolong": "[URL ߖߊ߰ߡߊ߲߬ ߞߏߖߎ߰]",
        "deletedwhileediting": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</strong> ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߬ ߓߘߊ߫ ߖߏ߰ߛߌ߫ ߊ߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߡߌ߬ߣߊ ߞߐ߫ ߌ ߓߟߏ߫.",
        "recreate": "ߊ߬ ߟߊߘߊ߲߫ ߕߎ߲߯",
        "img-lang-go": "ߕߊ߯",
        "table_pager_next": "ߞߐߜߍ߫ ߣߊ߬ߕߐ",
        "table_pager_prev": "ߞߐߜߍ ߢߍߕߊ",
+       "table_pager_first": "ߞߐߜߍ ߝߟߐ",
        "table_pager_last": "ߞߐߜߍ ߞߐ߯ߟߕߊ",
        "table_pager_limit": "$1 ߞߣߐߘߐ ߟߎ߬ ߦߌ߬ߘߊ߬ ߞߐߜߍ ߡߊ߬",
        "table_pager_limit_label": "ߞߎߡߘߊ ߟߎ߬ ߞߐߜߍ ߡߊ߬",
        "autosumm-replace": "ߞߣߐߘߐ ߣߐ߬ߘߐߓߌ߬ߟߊ߬  \"$1\" ߟߊ߫",
        "autoredircomment": "ߞߐߜߍ ߓߘߊ߫ ߟߊߘߎ߲߬ߛߌ߲߫ ߦߊ߲߬ [[$1]]",
        "autosumm-removed-redirect": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߦߟߍ߬ߡߊ߲߫ ߦߊ߲߬ [[$1]]",
+       "autosumm-changed-redirect-target": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߞߎ߲߬ߕߋߟߋ߲ ߡߊߝߊ߬ߟߋ߲߫ ߞߊ߬ ߓߐ߫ [[$1]] ߞߊ߬ ߕߊ߯ [[$2]]",
+       "autosumm-new": "ߞߐߜߍ ߓߘߊ߫ ߛߌ߲ߘߌ߫ ߣߌ߲߬  \"$1\" ߡߊ߬",
+       "autosumm-newblank": "ߞߐߜߍ߫ ߘߐߞߏߟߏ߲ ߓߘߊ߫ ߛߌ߲ߘߌ߫",
+       "watchlistedit-normal-title": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "watchlistedit-normal-legend": "ߞߎ߲߬ߕߐ߮ ߛߋ߲߬ߓߐ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫",
+       "watchlistedit-normal-submit": "ߞߎ߲߬ߕߐ߮ ߛߋ߲߬ߓߐ߫",
+       "watchlistedit-normal-done": "{{PLURAL:|ߞߎ߲߬ߕߐ߰ $1 ߞߋߟߋ߲ ߕߘߍ߬ ߦߋ߫|ߞߎ߲߬ߕߐ߮ $1 ߟߎ߬ ߕߎ߲߬ ߦߋ߫}} ߟߎ߫ ߛߋ߲߬ߓߐ߫ ߌ ߟߊ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫",
+       "watchlistedit-raw-title": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߎ߰ߡߍ",
+       "watchlistedit-raw-legend": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߎ߰ߡߍ",
+       "watchlistedit-raw-titles": "ߞߎ߲߬ߕߐ߮:",
+       "watchlistedit-raw-submit": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߟߏ߲ߘߐߦߊ߫",
+       "watchlistedit-raw-done": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߓߘߊ߫ ߓߊ߲߫ ߟߏ߲ߘߐߦߊ߫ ߟߊ߫.",
+       "watchlistedit-raw-added": "{{PLURAL:$1|ߞߎ߲߬ߕߐ߮ ߁ ߕߘߍ߬ ߓߘߊ߫|ߞߎ߲߬ߕߐ߮ $1 ߟߎ߬ ߕߎ߲߬ ߓߘߊ߫ ߟߊߘߏ߲߬}} ߟߊߘߏ߲߬:",
+       "watchlistedit-raw-removed": "{{PLURAL:|ߞߎ߲߬ߕߐ߮ $1 ߕߘߍ߬ ߓߘߊ߫|ߞߎ߲߬ߕߐ߮ $1 ߟߎ߬ ߕߘߍ߬ ߓߘߊ߫}} ߛߋ߲߬ߓߐ߫:",
+       "watchlistedit-clear-title": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߖߏ߬ߛߌ߬",
+       "watchlistedit-clear-legend": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߖߏ߬ߛߌ߬",
+       "watchlistedit-clear-explain": "ߞߎ߲߬ߕߐ߮ ߟߎ߬ ߓߍ߯ ߘߌߣߊ߬ ߛߋ߲߬ߓߐ߫ ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫",
+       "watchlistedit-clear-titles": "ߞߎ߲߬ߕߐ߮ ߟߎ߬:",
+       "watchlistedit-clear-submit": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߖߏ߬ߛߌ߬ (ߣߌ߲߬ ߦߋ߫ ߓߟߏߕߍ߰ߓߊߟߌ ߟߋ߬ ߘߌ߫)",
+       "watchlistedit-clear-done": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߓߘߊ߫ ߓߊ߲߫ ߖߏ߬ߛߌ߬ ߟߊ߫.",
+       "watchlistedit-clear-jobqueue": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߖߏ߬ߛߌ߬ߟߌ ߦߴߌ ߘߐ߫. ߊ߬ ߘߏ߲߬ ߘߌ߫ ߛߋ߫ ߥߊ߯ߕߌ߫ ߕߊ߬ ߟߊ߫߹",
+       "watchlistedit-clear-removed": "{{PLURAL:|ߞߎ߲߬ߕߐ߮ $1 ߁ ߕߘߍ߬ ߓߘߊ߫|ߞߎ߲߬ߕߐ߮ $1 ߟߎ߬ ߕߘߍ߬ ߓߘߊ߫}} ߛߋ߲߬ߓߐ߫:",
+       "watchlistedit-too-many": "ߞߐߜߍ߫ ߛߌߦߊߡߊ߲ߓߊ ߠߋ߬ ߦߌ߬ߘߊ߬ߣߍ߲߫ ߦߊ߲߬.",
        "watchlisttools-clear": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߞߐߜߍ ߖߏ߬ߛߌ߬",
        "watchlisttools-view": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߫ ߕߣߐ߬ߡߊ ߟߎ߫ ߦߌ߬ߘߊ߬ߟߌ",
        "watchlisttools-edit": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߞߐߜߍ ߦߋ߫ ߞߵߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "watchlisttools-raw": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߞߐߜߍ ߡߎ߰ߡߍ ߡߊߦߟߍ߬ߡߊ߲߫",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|ߓߊ߬ߘߏ߬ߟߌ]])",
+       "timezone-local": "ߕߌ߲߬ߞߎߘߎ߲",
+       "version-skins": "ߜߟߏ߬ ߡߊߞߍߣߍ߲ ߠߎ߬",
+       "version-specialpages": "ߞߐߜߍ߫ ߞߙߍߞߙߍߣߍ߲",
+       "version-parserhooks": "ߞߐ߬ߘߙߍ߬ ߞߎߙߎ߲ߞߎߙߎ߲ߠߊ",
+       "version-variables": "ߓߐߢߐ߲߯ߡߕߊ ߟߎ߬",
+       "version-editors": "ߛߓߍߦߟߊ",
+       "version-antispam": "ߞߏ߬ߘߏ (ߛߑߔߊߡ) ߢߍߓߍ߲ߠߌ߲",
+       "version-other": "ߘߏ߫ ߜߘߍ",
+       "version-hooks": "ߘߎ߲ߓߟߐ ߟߎ߬",
        "redirect": "ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߬ ߦߋ߫ ߞߐߕߐ߮ ߓߟߏ߫߸ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߸ ߞߐߜߍ߸ ߡߛߊ߬ߦߌ߲߬ߠߌ߲߸ ߥߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ID",
        "redirect-summary": "ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߬ ߦߋ߫ ߞߐߕߐ߮ (ߞߐߕߐ߮ ߕߐ߮ ߘߌ߫),ߞߐߜߍ (ߦߋ߫ ߡߛߊ߬ߦߌ߲߬ߠߌ߲ ID ߥߟߊ߫ ߞߐߜߍ ID ߘߌ ߞߊ߲߬), ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ ߦߋ߫ (ߟߊ߬ߓߊ߰ߙߟߊ߬ ߦߙߌߞߊ ID ߘߌ ߞߊ߲߬), ߥߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߘߏ߲߬ߕߐ߬ߟߊ ߦߋ߫ (ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ID ߘߌ ߞߊ߲߬). ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ:\n[[{{#Special:Redirect}}/file/Example.jpg]], \n[[{{#Special:Redirect}}/page/64308]],\n[[{{#Special:Redirect}}/revision/328429]], \n[[{{#Special:Redirect}}/user/101]], ߥߟߊ߫ \n[[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "ߕߊ߯",
index 84e16ee..b3ee47b 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Mostrar mudanças nas páginas que contêm hiperligações para",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Páginas que contêm hiperligações</strong> para a página selecionada",
        "rcfilters-target-page-placeholder": "Introduzir o nome de uma página (ou categoria)",
+       "rcfilters-allcontents-label": "Todos os conteúdos",
+       "rcfilters-alldiscussions-label": "Todas as discussões",
        "rcnotefrom": "Abaixo {{PLURAL:$5|está a mudança|estão as mudanças}} desde <strong>$2</strong> (mostradas até <strong>$1</strong>).",
        "rclistfromreset": "Reiniciar a seleção da data",
        "rclistfrom": "Mostrar as novas mudanças a partir das $2 de $3",
        "move-subpages": "Mover subpáginas (até $1)",
        "move-talk-subpages": "Mover subpáginas da página de discussão (até $1)",
        "movepage-page-exists": "A página $1 já existe e não pode ser substituída.",
+       "movepage-source-doesnt-exist": "A página $1 não existe e não pode ser movida.",
        "movepage-page-moved": "A página $1 foi movida para $2.",
        "movepage-page-unmoved": "Não foi possível mover a página $1 para $2.",
        "movepage-max-pages": "O limite de $1 {{PLURAL:$1|página movida|páginas movidas}} foi atingido; não será possível mover mais páginas de forma automática.",
        "delete_and_move_reason": "Eliminada para poder mover \"[[$1]]\" para este título",
        "selfmove": "O título é o mesmo;\nnão é possível mover uma página para ela mesma.",
        "immobile-source-namespace": "Não é possível mover páginas no domínio \"$1\"",
+       "immobile-source-namespace-iw": "As páginas de outras wikis não podem ser movidas desta wiki.",
        "immobile-target-namespace": "Não é possível mover páginas para o domínio \"$1\"",
        "immobile-target-namespace-iw": "Uma hiperligação interwikis não é um destino válido para uma movimentação de página.",
        "immobile-source-page": "Esta página não pode ser movida.",
        "immobile-target-page": "Não é possível mover para esse título de destino.",
+       "movepage-invalid-target-title": "O nome pedido é inválido.",
        "bad-target-model": "O destino pretendido usa um modelo de conteúdo diferente. Não é possível converter de $1 para $2.",
        "imagenocrossnamespace": "Não é possível mover imagem para domínio que não de imagens",
        "nonfile-cannot-move-to-file": "Não é possível mover algo que não é um ficheiro para o domínio de ficheiros",
        "permanentlink": "Hiperligação permanente",
        "permanentlink-revid": "Identificador de revisão",
        "permanentlink-submit": "Ir para a revisão",
+       "newsection": "Secção nova",
+       "newsection-page": "Página de destino",
+       "newsection-submit": "Ir para a página",
        "dberr-problems": "Desculpe! Este sítio está com dificuldades técnicas.",
        "dberr-again": "Experimente esperar alguns minutos e atualizar.",
        "dberr-info": "(Não foi possível aceder ao servidor da base de dados: $1)",
index 60d3abc..f886c73 100644 (file)
        "uploadstash-bad-path-unknown-type": "Tipe scanusciute \"$1\".",
        "uploadstash-bad-path-unrecognized-thumb-name": "Nome d'a miniature non acchiate.",
        "uploadstash-bad-path-bad-format": "'A chiave \"$1\" non ge ste jndr'à 'nu formate appropriate.",
+       "uploadstash-file-not-found": "Chiave \"$1\" non acchiate jndr'à scorte.",
        "uploadstash-file-not-found-no-thumb": "No ge se pò avè 'a miniature.",
        "uploadstash-file-not-found-no-local-path": "Nisciune percorse locale pa vôsce in scale.",
        "uploadstash-file-not-found-no-object": "Non ge pozze ccrejà 'nu oggette file locale pa miniature.",
        "pageswithprop-legend": "Pàggene cu 'na probbietà d'a pàgene",
        "pageswithprop-text": "Sta pàgene elenghe le pàggene ca ausane 'na particolare probbietà d'a pàgene.",
        "pageswithprop-prop": "Nome d'a probbietà:",
+       "pageswithprop-reverse": "Ordenamende a smerse",
+       "pageswithprop-sortbyvalue": "Ordene pe valore d'a probbietà",
        "pageswithprop-submit": "Véje",
        "pageswithprop-prophidden-long": "valore d'a probbietà d'u teste lunghe scunnute ($1)",
        "pageswithprop-prophidden-binary": "valore probbietà binarie scunnute ($1)",
        "speciallogtitlelabel": "Destinazione (titole o {{ns:user}}:nome de l'utende pe l'utende):",
        "log": "Archivije",
        "logeventslist-submit": "Fà 'ndrucà",
+       "logeventslist-more-filters": "'Ndruche le archivije aggiundive:",
+       "logeventslist-patrol-log": "Archivije de le condrolle",
+       "logeventslist-tag-log": "Archivije de le tag",
        "all-logs-page": "Tutte l'archivije pubbleche",
        "alllogstext": "Visualizzazione combinate de tutte le archivije disponibbele sus a {{SITENAME}}.\nTu puè restringere 'a viste selezionanne 'u tipe de archivije, 'u nome utende (senzibbile a le maiuscole), o le pàggene coinvolte (pure chiste senzibbile a le maiuscole).",
        "logempty": "Non ge stè 'n'anema de priatorie jndr'à l'archivije.",
        "dellogpage": "Archivie de le scangellaminde",
        "dellogpagetext": "Sotte ste 'na liste de le cchiù recende scangellaziune.",
        "deletionlog": "Archivije de le scangellaminde",
+       "log-name-create": "Archivije d'a ccrejazione de le pàggene",
+       "log-description-create": "Sotte ste 'n'elenghe de le urteme ccrejaziune de pàgene.",
+       "logentry-create-create": "$1 pàgena {{GENDER:$2|ccrejate}} $3",
        "reverted": "Turnà a 'a revisiona cchiù recende",
        "deletecomment": "Mutive:",
        "deleteotherreason": "Otre mutive de cchiù:",
        "deleting-backlinks-warning": "<strong>Attenziò:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Otre pàggene]] appondene o vonne 'a pàgene ca tu vue ccù scangìlle.",
        "rollback": "Annulle le cangiaminde",
        "rollback-confirmation-confirm": "Pe piacere conferme:",
+       "rollback-confirmation-yes": "Annulle",
+       "rollback-confirmation-no": "Annulle",
        "rollbacklink": "annulle 'u cangiaminde",
        "rollbacklinkcount": "annulle $1 {{PLURAL:$1|cangiamende|cangiaminde}}",
        "rollbacklinkcount-morethan": "annulle cchiù de $1 {{PLURAL:$1|cangiamende|cangiaminde}}",
        "pageinfo-category-subcats": "Numere de sottocategorije",
        "pageinfo-category-files": "Numere de file",
        "pageinfo-user-id": "ID de l'utende",
+       "pageinfo-file-hash": "Valore hash",
        "pageinfo-view-protect-log": "'Ndruche l'archivije de le protezziune pe sta pàgene.",
        "markaspatrolleddiff": "Signe cumme condrollate",
        "markaspatrolledtext": "Signe sta pàgene cumme condrollate",
        "compare-revision-not-exists": "'A revisione ca è specificate non g'esiste.",
        "diff-form": "Differenze",
        "permanentlink-revid": "ID d'a revisione",
+       "newsection-submit": "Veje 'a pàgene",
        "dberr-problems": "Sime spiacende! Stu site stè 'ngondre de le difficoltà tecniche.",
        "dberr-again": "Aspitte quacche minute e pò recareche.",
        "dberr-info": "(Non ge riuscime a trasè sus a'u server d'u database: $1)",
        "htmlform-datetime-invalid": "'U valore specificate non jè 'na date. Pruéve ausanne 'u formate AAAA-MM-GG HH:MM:SS",
        "htmlform-date-toolow": "'U valore specificate avène apprime da date congesse de $1.",
        "htmlform-date-toohigh": "'U valore specificate avène apprisse da date congesse de $1.",
+       "htmlform-time-toolow": "'U valore specificate avène apprime de l'orarie congesse de $1.",
+       "htmlform-time-toohigh": "'U valore specificate avène apprisse de l'orarie congesse de $1.",
+       "htmlform-datetime-toolow": "'U valore specificate avène apprime da date e orarie congesse de $1.",
+       "htmlform-datetime-toohigh": "'U valore specificate avène apprisse da date e orarie congesse de $1.",
        "htmlform-title-badnamespace": "[[:$1]] non ge stè jndr'à 'u namespace \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" jè 'nu titole de 'na pàgene ca no se pò ccrejà",
        "htmlform-title-not-exists": "$1 non g'esiste.",
index d3f8b0a..e3dc8f5 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Показать правки на ссылающихся страницах",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Страницы, ссылающиеся</strong> на выбранную",
        "rcfilters-target-page-placeholder": "Введите имя страницы (или категории)",
+       "rcfilters-allcontents-label": "Все пространства имён",
+       "rcfilters-alldiscussions-label": "Все обсуждения",
        "rcnotefrom": "Ниже {{PLURAL:$5|указано изменение|перечислены изменения}} с <strong>$3, $4</strong> (показано не более <strong>$1</strong>).",
        "rclistfromreset": "Сбросить выбор даты",
        "rclistfrom": "Показать изменения с $3 $2.",
        "move-subpages": "Переименовать подстраницы (до $1)",
        "move-talk-subpages": "Переименовать подстраницы страницы обсуждения (до $1)",
        "movepage-page-exists": "Страница $1 уже существует и не может быть автоматически перезаписана.",
+       "movepage-source-doesnt-exist": "Страница $1 не существует, а потому не может быть переименована.",
        "movepage-page-moved": "Страница $1 была переименована в $2.",
        "movepage-page-unmoved": "Страница $1 не может быть переименована в $2.",
        "movepage-max-pages": "{{PLURAL:$1|Была переименована|Было переименовано|Были переименованы}} $1 {{PLURAL:$1|страница|страницы|страниц}} — это максимум; большее число страниц автоматически переименовать нельзя.",
        "delete_and_move_reason": "Удалено для возможности переименования «[[$1]]»",
        "selfmove": "Невозможно переименовать страницу: исходное и новое имя страницы совпадают.",
        "immobile-source-namespace": "Невозможно переименовывать страницы в пространстве имён «$1»",
+       "immobile-source-namespace-iw": "Страницы из других вики не могут быть переименованы в этой вики.",
        "immobile-target-namespace": "Невозможно переместить страницу в пространство имён «$1»",
        "immobile-target-namespace-iw": "Ссылка интервики не может быть использована для переименования.",
        "immobile-source-page": "Эту страницу нельзя переименовать.",
        "immobile-target-page": "Нельзя присвоить странице это имя.",
+       "movepage-invalid-target-title": "Запрошенное имя недопустимо.",
        "bad-target-model": "Невозможно преобразовать $1 в $2. У страниц несовместимые модели содержимого.",
        "imagenocrossnamespace": "Невозможно дать файлу имя из другого пространства имён",
        "nonfile-cannot-move-to-file": "Невозможно переименовывать не-файловые страницы в файлы",
        "specialmute-success": "Изменения по отключению уведомлений были сохранены. Просмотрите всех отключённых участников на [[Special:Preferences|ваших настройках]].",
        "specialmute-submit": "Подтвердить",
        "specialmute-label-mute-email": "Отключить эл. почту от этого участника",
-       "specialmute-header": "Пожалуйста, выберите настройки уведомлений для {{GENDER:$1|участника|участницы}} <b>{{BIDI:[[User:$1|$1]]}}</b>.",
+       "specialmute-header": "Пожалуйста, выберите настройки отключения уведомлений для {{GENDER:$1|участника|участницы}} <b>{{BIDI:[[User:$1|$1]]}}</b>.",
        "specialmute-error-invalid-user": "Указанное вами имя участника не может быть найдено.",
+       "specialmute-error-no-options": "Функции отключения уведомлений недоступны. Это вызвано либо тем, что вы не подтвердили электронную почту, либо тем, что администратор выключил в этой вики функции электронной почты и\\или функции чёрного списка.",
        "specialmute-email-footer": "Для управления настройками эл. почты {{GENDER:$2|участника|участницы}} {{BIDI:$2}}, пожалуйста, посетите <$1>.",
        "specialmute-login-required": "Пожалуйста авторизируйтесь, чтобы управлять отключением уведомлений.",
        "mute-preferences": "Настройки выключения",
index eef1d93..ccb9e42 100644 (file)
        "yourtext": "توهان جو متن",
        "storedversion": "سانڍيل مسودو",
        "yourdiff": "تفاوت",
-       "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 تحت پڌريون ڪجن ٿيون (تفصيلن لاءِ $1 ڏسندا). اوهان جي تحرير کي {{SITENAME}} جي قائدن تحت سنواري سگهجي ٿو. جيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو. پنهنجو مواد هتي جمع ڪرڻ جو مطلب هوندو تہ توهان کي جمع ڪرايل مواد جي مفت فراهمي ۽ کُليل تبديليءَ تي ڪوبہ اعتراز ناهي.<br />\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن مفت وسيلي تان ڪاپي ڪيو آهي.\n'''تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ کان سواءِ هتي جمع نہ ڪريو.'''",
+       "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 ھيٺ ڏنل ڄاتيون وڃن ٿيون (تفصيلن لاءِ $1 ڏسندا).\nجيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻيءَ کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ ان کي هتي اماڻيو.<br />\nتوهان اسان سان اھو بہ وچن ڪريو ٿا تہ ھي توهان پاڻ لکيو آھي يا وري ڪنھن مفت وسيلي يا عوامي ڊومين تان نقل ڪيو آهي.\n<strong>حق-۽-واسطا-رکندڙ ڪم کان اجازت سواءِ نہ اماڻيو.</strong>",
        "copyrightwarning2": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيدارين کي ٻيا ڀاڱيدار سنواري، بدلائي، يا ڊاهي سگھن ٿا. جيڪڏهن اوهان نہ ٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريون وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو.</br>\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن اهڙي ئي مفت عوامي وسيلي تان ڪاپي ڪيو آهي. (تفصيلن لاءِ $1 ڏسندا).\n\n<strong>تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ بنان هتي جمع نہ ڪريو.</strong>",
        "protectedpagewarning": "<strong>چتاءُ: هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط منتظمين ئي ان کي سنواري سگھن ٿا. </strong>\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
        "semiprotectedpagewarning": "<strong>نوٽ:</strong> هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط خودڪار نموني پڪ ڪيل واپرائيندڙ ئي ان کي سنواري سگھن ٿا.\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
        "version-specialpages": "خاص صفحا",
        "version-variables": "ڦِرڻا",
        "version-other": "ٻيو",
-       "version-license": "ذريعات‌وڪي لائيسنس",
-       "version-ext-license": "لائيسنس",
+       "version-license": "ذريعات‌وڪي اجازتنامو",
+       "version-ext-license": "اجازتنامو",
        "version-ext-colheader-name": "توسيع",
        "version-skin-colheader-name": "چَمَ",
        "version-ext-colheader-version": "ڀيرو",
-       "version-ext-colheader-license": "لائيسنس",
+       "version-ext-colheader-license": "اجازتنامو",
        "version-ext-colheader-description": "تشريح",
        "version-ext-colheader-credits": "ليکڪ",
-       "version-license-title": "لائيسنس براءِ $1",
+       "version-license-title": "$1 لاءِ اجازتنامو",
        "version-poweredby-others": "ٻيا",
        "version-poweredby-translators": "translatewiki.net جا ترجميڪار",
        "version-software": "تنصيب شده منطقگري",
        "version-software-version": "ڀيرو",
        "version-libraries-library": "لائبريري",
        "version-libraries-version": "ڀيرو",
-       "version-libraries-license": "لائيسنس",
+       "version-libraries-license": "اجازتنامو",
        "version-libraries-description": "تشريح",
        "version-libraries-authors": "ليکڪ",
        "redirect-submit": "ھلو",
index 4f56f2d..765dcd4 100644 (file)
        "october": "październik",
        "november": "listopad",
        "december": "grudziyń",
-       "january-gen": "styczńa",
+       "january-gen": "stycznia",
        "february-gen": "lutego",
        "march-gen": "marca",
-       "april-gen": "kwjetńa",
-       "may-gen": "moja",
+       "april-gen": "kwietnia",
+       "may-gen": "mŏja",
        "june-gen": "czyrwca",
        "july-gen": "lipca",
-       "august-gen": "śyrpńa",
-       "september-gen": "wrzyśńa",
-       "october-gen": "paźdźerńika",
+       "august-gen": "siyrpnia",
+       "september-gen": "września",
+       "october-gen": "października",
        "november-gen": "listopada",
-       "december-gen": "grudńa",
+       "december-gen": "grudnia",
        "jan": "sty",
        "feb": "lut",
        "mar": "mar",
        "october-date": "$1 paźdźyrńika",
        "december-date": "$1 grudńa",
        "pagecategories": "{{PLURAL:$1|Kategoryjŏ|Kategoryje}}",
-       "category_header": "Zajty we katygoryji \"$1\"",
-       "subcategories": "Podkatygoryje",
-       "category-media-header": "Pliki we katygoryji \"$1\"",
-       "category-empty": "''Terozki w tyj katygoryji sům żodne artikle a pliki''",
+       "category_header": "Strōny we kategoryji \"$1\"",
+       "subcategories": "Podkategoryje",
+       "category-media-header": "Zbiory we kategoryji „$1”",
+       "category-empty": "<em>We tyj kategoryji niy ma terŏz żŏdnych strōn ani mediōw.</em>",
        "hidden-categories": "{{PLURAL:$1|Skrytŏ kategoryjŏ|Skryte kategoryje|Skrytych kategoryji}}",
        "hidden-category-category": "Schowane katygoryje",
-       "category-subcat-count": "{{PLURAL:$2|Ta katygoryjo mo jyno jedno podkatygoryjo.|Ta katygoryjo mo {{PLURAL:$1|tako podkatygoryjo|$1 podkatygoryje|$1 podkatygoryj}} ze wjelośći wszyjskich katygoryj: $2.}}",
+       "category-subcat-count": "{{PLURAL:$2|Ta kategoryjŏ mŏ ino jednã podkategoryjõ.|Ta kategoryjõ mŏ {{PLURAL:$1|takõ podkategoryjõ|$1 podkategoryje|$1 podkategoryji}} ze wszyjskich $2 kategoryji.}}",
        "category-subcat-count-limited": "Ta katygoryjo mo {{PLURAL:$1|tako podkatygoryjo|$1 podkatygoryje|$1 podkatygoryji}}.",
-       "category-article-count": "{{PLURAL:$2|W tyj katygoryji je jyno jydno zajta.|W katygoryji {{PLURAL:$1|je ukozano $1 zajta|sům ukozane $1 zajty|je ukozanych $1 zajtůw}} ze cołkij wjelośći $2 zajtůw.}}",
+       "category-article-count": "{{PLURAL:$2|We tyj kategoryji je ino jedna strōna.|We kategoryji {{PLURAL:$1|je ta strōna|sōm te $1 strōny|je te $1 strōn}} ze wszyjskich $2 strōn.}}",
        "category-article-count-limited": "W katygoryji {{PLURAL:$1|je pokozano $1 zajta|sům pokozane $1 zajty|je pokazanych $1 zajtůw}}.",
-       "category-file-count": "{{PLURAL:$2|W katygoryji znojduje śe jydyn plik.|W katygoryji {{PLURAL:$1|je pokozany $1 plik|sům pokozane $1 pliki|je pokozanych $1 plikůw}} ze cołkyj liczby $2 plikůw.}}",
+       "category-file-count": "{{PLURAL:$2|We tyj kategoryji je ino jedyn zbiōr.|We tyj kategoryji {{PLURAL:$1|je tyn $1 zbiōr|sōm te $1 zbiory|je te $1 zbiorōw}} ze wszyjskich $2 zbiorōw.}}",
        "category-file-count-limited": "W katygoryji {{PLURAL:$1|je pokozany $1 plik|sům pokozane $1 pliki|je pokozanych $1 plikůw}}.",
-       "listingcontinuesabbrev": "ć.d.",
+       "listingcontinuesabbrev": "cd.",
        "index-category": "Indeksowane zajty",
        "noindex-category": "Niyindeksowane strōny",
-       "broken-file-category": "Zajty z linkami do niyôbecnych zbiorōw",
-       "about": "Uo serwiśe",
+       "broken-file-category": "Strōny ze zepsutymi linkami do zbiorōw",
+       "about": "Ô serwisie",
        "article": "zajta",
-       "newwindow": "(uodwjyro śe we nowym uokńe)",
-       "cancel": "Uodćepej",
+       "newwindow": "(ôtwiyrŏ we nowym ôknie)",
+       "cancel": "Ôdciep",
        "moredotdotdot": "Wjyncyj...",
        "morenotlisted": "Ńy je to kůmplytno lista",
        "mypage": "Zajta",
-       "mytalk": "Dyskusyjo",
+       "mytalk": "Dyskusyjŏ",
        "anontalk": "Godka tygo IP",
        "navigation": "Nawigacyjŏ",
        "and": "&#32;i",
        "variants": "Warianty",
        "navigation-heading": "Myni nawigacyje",
        "errorpagetitle": "Feler",
-       "returnto": "Nazod do zajty $1.",
+       "returnto": "Wrōć do $1.",
        "tagline": "Ze {{GRAMMAR:D.lp|{{SITENAME}}}}",
        "help": "Pōmoc",
        "search": "Szukej",
        "searchbutton": "Szukej",
        "go": "Przyńdź",
        "searcharticle": "Idź",
-       "history": "Gyszichta zajty",
+       "history": "Historyjŏ strōny",
        "history_short": "Historyjŏ",
        "updatedmarker": "pomjyńane uod uostatńij wizyty",
        "printableversion": "Wersyjŏ do durku",
        "permalink": "Link trwały",
        "print": "Drukuj",
        "view": "Pokŏż",
-       "view-foreign": "Uobejrzij we {{grammar:MS.lp|$1}}",
+       "view-foreign": "Ôbejzdrzij we {{grammar:MS.lp|$1}}",
        "edit": "Edytuj",
-       "create": "Stwůrz",
-       "create-local": "Wkludź lokalny uopis",
-       "delete": "Wyćep",
+       "create": "StwÅ\8drz",
+       "create-local": "Wkludź lokalny ôpis",
+       "delete": "Skasuj",
        "undelete_short": "Wćep nazod {{PLURAL:$1|jedna wersyjo|$1 wersyje|$1 wersyji}}",
        "viewdeleted_short": "{{PLURAL:$1|jedna wyćepano wersyjo|$1 wyćepane wersyje|$1 wyćepanych wersyjůw}}",
        "protect": "Zawrzij",
        "protect_change": "půmjyń",
        "unprotect": "Uodymkńij",
-       "newpage": "Nowy artikel",
+       "newpage": "Nowŏ strōna",
        "talkpagelinktext": "dyskusyjŏ",
        "specialpage": "Specjalnŏ strōna",
        "personaltools": "Włŏsne nŏrzyńdzia",
        "viewtalkpage": "Zajta godki",
        "otherlanguages": "We inkszych jynzykach",
        "redirectedfrom": "(Pōnkniyntŏ ze $1)",
-       "redirectpagesub": "Zajta przekerowujůnco",
-       "redirectto": "Przekerowańy do:",
+       "redirectpagesub": "Strōna przekerowaniŏ",
+       "redirectto": "Przekerowanie do:",
        "lastmodifiedat": "Ta strōna była ôstatni rŏz edytowanŏ $2, $1.",
        "viewcount": "W ta zajta filowano {{PLURAL:$1|tylko roz|$1 rozůw}}.",
        "protectedpage": "Zajta zawarto",
        "currentevents-url": "Project:Terŏźne wydarzynia",
        "disclaimers": "Prawne informacyje",
        "disclaimerpage": "Project:Prawne informacyje",
-       "edithelp": "Půmoc we půmjyÅ\84\84y",
+       "edithelp": "PÅ\8dmoc we edycyji",
        "mainpage": "Przodniŏ zajta",
        "mainpage-description": "Przodniŏ strōna",
        "policy-url": "Project:Prawidła",
        "versionrequiredtext": "Wymagano jest MediaWiki we wersji $1 coby skorzistać zr tyj zajty. Uobocz [[Special:Version]]",
        "ok": "OK",
        "retrievedfrom": "Zdrzōdło \"$1\"",
-       "youhavenewmessages": "Mosz $1 ($2).",
-       "youhavenewmessagesfromusers": "Mosz $1 uod {{PLURAL:$3|inszygo używocza|$3 używoczy}} ($2).",
+       "youhavenewmessages": "Mŏsz $1 ($2).",
+       "youhavenewmessagesfromusers": "Mŏsz $1 ôd {{PLURAL:$3|inszego używŏcza|$3 używŏczy}} ($2).",
        "youhavenewmessagesmanyusers": "Mosz $1 uod wjelu używoczy ($2).",
-       "newmessageslinkplural": "{{PLURAL:$1|jedno nowina|999=nowiny}}",
-       "newmessagesdifflinkplural": "{{PLURAL:$1|ôstatniŏ pōmiana|999=ôstatnie pōmiany}}",
+       "newmessageslinkplural": "{{PLURAL:$1|jedna nowina|999=nowiny}}",
+       "newmessagesdifflinkplural": "{{PLURAL:$1|ôstatniŏ zmiana|999=ôstatnie zmiany}}",
        "youhavenewmessagesmulti": "Mosz nowe powjadůmjyńa: $1",
        "editsection": "edytuj",
        "editold": "edytuj",
-       "viewsourceold": "pokoż zdrzůdło",
+       "viewsourceold": "pokŏż zdrzōdło",
        "editlink": "edytuj",
        "viewsourcelink": "pokŏż zdrzōdło",
        "editsectionhint": "Edytuj sekcyjõ: $1",
        "sort-descending": "Sortuj pomńijszajůnco",
        "sort-ascending": "Sortuj rosnůnco",
        "nstab-main": "Strōna",
-       "nstab-user": "{{GENDER:{{BASEPAGENAME}}|Zajta używocza|Zajta używoczki}}",
+       "nstab-user": "{{GENDER:{{BASEPAGENAME}}|Strōna ôd używŏcza|Strōna ôd używŏczki}}",
        "nstab-media": "Pliki",
        "nstab-special": "Specjalnŏ strōna",
-       "nstab-project": "Zajta projektu",
+       "nstab-project": "Strōna projektu",
        "nstab-image": "Zbiōr",
-       "nstab-mediawiki": "Komuńikat",
+       "nstab-mediawiki": "Kōmunikat",
        "nstab-template": "Muster",
        "nstab-help": "Zajta půmocy",
        "nstab-category": "Kategoryjŏ",
        "nosuchaction": "Ńy mo takij uoperacyji",
        "nosuchactiontext": "Uoprogramowańy ńy rozpoznowo uoperacyji takij kej podano w URL.",
        "nosuchspecialpage": "Niy ma takij specjalnyj strōny",
-       "nospecialpagetext": "<strong>Uoprogramowańy ńy rozpoznowo takij szpecyjalnyj zajty.</strong>\n\nLista szpecyjalnych zajtůw znojdźesz na [[Special:SpecialPages|{{int:specialpages}}]].",
+       "nospecialpagetext": "<strong>Ôbranŏ była niynŏleżnŏ specjalnŏ strōna.</strong>\n\nListã specjalnych strōn idzie znojś na [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Feler",
        "databaseerror": "Feler bazy danych",
        "databaseerror-text": "Pojawjůł śe feler przi wysyłańu zapytańa do bazy danych. Mogebność je, aże je to feler we uoprogramowańu.",
        "cannotdelete-title": "Ńy idźie wyćepać zajty \"$1\".",
        "delete-hook-aborted": "Wyćepywańe sztopńynte bez hak. Przyczyna ńyuokreślůno.",
        "no-null-revision": "Ńy je mogebne stworzyńe zerowyj wersyji zajty \"$1\"",
-       "badtitle": "Felerny titel",
-       "badtitletext": "Podano felerny titel zajty. Prawdopodańy sům w ńim znoki, kerych ńy wolno używać we titlach abo je pusty.",
+       "badtitle": "Niynŏleżny tytuł",
+       "badtitletext": "Podany tytuł strōny to je niynŏleżny, prōzny, abo źle zalinkowany tytuł metajynzykowy abo interwiki.\nMoże w nim być jedyn abo wiyncyj znakōw, co niy mogōm być używane we tytułach.",
        "perfcached": "To co sam je naszkryflane, to ino kopja ze pamjyńći podryncznyj a może ńy być aktualne. Nojwjyncyj {{PLURAL:$1|jydyn wynik je|$1 wyniki sům}} we tyj pamjyńći.",
        "perfcachedts": "To co sam je naszkryflane, to ino kopja s pamjyńći podryncznyj a bůło uaktualńůne $1. Nojwjyncyj {{PLURAL:$4|jeden wynik je|$4 wyniki sům}} dostympne.",
        "querypage-no-updates": "Uaktualńyńo lo tyj zajty sům terozki zawarte. Dane, kere sam sům, ńy zostouy uodśwjyżůne.",
-       "viewsource": "Zdrzůdłowy tekst",
-       "viewsource-title": "Uobocz zdrzůdło lo $1",
+       "viewsource": "ZdrzÅ\8ddłowy tekst",
+       "viewsource-title": "Pokŏż zdrzōdło $1",
        "actionthrottled": "Akcyjo wstrzimano",
        "actionthrottledtext": "Mechańizm uobrůny przed spamym uograńiczo liczba wykonań tyj czynnośći we jednostce czasu. Průbowołżeś go uocygańić. Prosza, sprůbuj na nowo za pora minut.",
        "protectedpagetext": "Ta zajta je zawarto przed sprowjańym.",
        "welcomeuser": "Witej, $1",
        "welcomecreation-msg": "Uotwarli my sam lo Ćebje kůnto.\nPamjyntej coby posztalować [[Special:Preferences|preferencyji]]",
        "yourname": "Mjano użytkowńika:",
-       "userlogin-yourname": "Mjano używocza",
-       "userlogin-yourname-ph": "Wkludź swoje miano używacza",
+       "userlogin-yourname": "Miano używŏcza",
+       "userlogin-yourname-ph": "Wkludź swoje miano używŏcza",
        "createacct-another-username-ph": "Wszkryflej mjano użytkowńika",
        "yourpassword": "Hasło:",
        "userlogin-yourpassword": "Hasło",
        "userlogin-yourpassword-ph": "Wkludź swoje hasło",
        "createacct-yourpassword-ph": "Wkludź hasło",
        "yourpasswordagain": "Naszkryflej ausdruk zaś",
-       "createacct-yourpasswordagain": "Potwjyrdź hasło",
+       "createacct-yourpasswordagain": "Potwiyrdź hasło",
        "createacct-yourpasswordagain-ph": "Wkludź hasło jeszcze rŏz",
-       "userlogin-remembermypassword": "Ńy wylogůwywuj mje",
+       "userlogin-remembermypassword": "Niy ôdlogowuj mie",
        "userlogin-signwithsecure": "Użyj bezpjecznygo połůnczyńa",
        "yourdomainname": "Twoja domyna",
        "password-change-forbidden": "Ńy można půmjyńać haseł na tyj wiki.",
        "externaldberror": "Je jaki feler we zewnyntrznyj baźe autentyfikacyjnyj, abo ńy mosz uprawńyń potrzebnych do aktualizacyji zewnyntrznego kůnta.",
-       "login": "Zaloguj śe",
+       "login": "Wloguj sie",
        "nav-login-createaccount": "Logowańy / Tworzyńy kůnta",
        "logout": "Wyloguj",
        "userlogout": "Uodloguj śe",
        "notloggedin": "Ńy jeżeś zalogowany",
-       "userlogin-noaccount": "Ńy mosz kůnta?",
-       "userlogin-joinproject": "Doćep śe do {{SITENAME}}",
-       "createaccount": "Twůrz nowe kůnto",
-       "userlogin-resetpassword-link": "Ńy pamjyntosz hasła?",
-       "userlogin-helplink2": "Hilfa przi logůwańu",
+       "userlogin-noaccount": "Niy mŏsz kōnta?",
+       "userlogin-joinproject": "Dołōncz do {{GRAMMAR:D.lp|{{SITENAME}}}}",
+       "createaccount": "TwÅ\8drz nowe kÅ\8dnto",
+       "userlogin-resetpassword-link": "Niy pamiyntŏsz hasła?",
+       "userlogin-helplink2": "Pōmoc przi logowaniu",
        "userlogin-loggedin": "Zalogowano kej {{GENDER:$1|$1}}. Użyj formulara půńiżyj, coby zalogować śe kej inkszy używocz.",
        "userlogin-createanother": "Twůrz inksze kůnto",
        "createacct-emailrequired": "E-brif",
-       "createacct-emailoptional": "E-brif (uopcjůnalne)",
-       "createacct-email-ph": "Wkludź swojã adresã e-brifa",
+       "createacct-emailoptional": "Adresa e-mail (niymusowo)",
+       "createacct-email-ph": "Wkludź swojã adresã e-mail",
        "createacct-another-email-ph": "Nastow e-brif",
        "createaccountmail": "Użyj chwilowygo hasła losowo genyrowanygo a wyślij je na wrychtowany adres e-brifa.",
        "createacct-realname": "Prawdźiwe imje a nazwisko (uopcjůnalńe)",
        "createacct-reason": "Powůd:",
        "createacct-reason-ph": "Pojakymu tworzisz nowe kůnta",
-       "createacct-submit": "Twůrz kůnto",
+       "createacct-submit": "Stwōrz kōnto",
        "createacct-another-submit": "Twůrz inksze kůnto",
-       "createacct-benefit-heading": "{{grammar:B.lp|{{SITENAME}}}} tworzům perzůny take kej Ty.",
+       "createacct-benefit-heading": "{{grammar:B.lp|{{SITENAME}}}} tworzÅ\8dm ludzie jak Ty.",
        "createacct-benefit-body1": "{{PLURAL:$1|edycyjo|edycyje|edycyji}}",
-       "createacct-benefit-body2": "{{PLURAL:$1|zajta|zajty|zajt}}",
-       "createacct-benefit-body3": "{{PLURAL:$1|używocz|używoczůw}} we uostatńim czaśe",
+       "createacct-benefit-body2": "{{PLURAL:$1|strōna|strōny|strōn}}",
+       "createacct-benefit-body3": "{{PLURAL:$1|nojnowszy używŏcz|nojnowsi używŏcze|nojnowszych używŏczōw}}",
        "badretype": "Hasła kere żeś naszkryfloł ńy zgodzajům śe jydne ze drugim.",
        "userexists": "Mjano użytkowńika, kere żeś wybroł, je zajynte. Wybjer, prosza, inksze mjano.",
        "loginerror": "Feler przi logowańu",
        "suspicious-userlogout": "Polecyńe wylogowańo uostoło uodćepńynte skiż tygo co wyglůnda, aże uostoło posłane bez uszkodzůna przeglůndarka abo buforujůncy serwer proxy.",
        "createacct-another-realname-tip": "Wszkryflańy twojigo mjana a nazwiska ńy je końyczne.\nKej bydźesz chćoł je podoć, bydům użyte, coby dokůmyntowoć Twoje autorstwo.",
        "pt-login": "Wloguj sie",
-       "pt-login-button": "Zaloguj śe",
+       "pt-login-button": "Wloguj sie",
        "pt-createaccount": "Twōrz nowe kōnto",
        "pt-userlogout": "Ôdloguj sie",
        "php-mail-error-unknown": "Ńyznany feler we funkcyji mail()",
        "resetpass-wrong-oldpass": "Felerne tymczasowe abo aktualne hasło.\nMożliwe co właśńy zmjyńiłżeś swoje hasło abo poprosiłżeś uo nowe tymczasowe hasło.",
        "resetpass-temp-password": "Tymczasowe hasło:",
        "resetpass-abort-generic": "Půmjyńańe hasła uostoła zatrzimane bez rozszyrzyńe.",
-       "passwordreset": "Wyczyść hasło",
+       "passwordreset": "Wysnŏż hasło",
        "passwordreset-disabled": "No tyj wiki zamkńynto resytowańy hasył.",
        "passwordreset-username": "Miano ôd używŏcza:",
        "passwordreset-domain": "Domyna:",
        "passwordreset-emailtext-ip": "Ftoś (cheba Ty, s IP $1)\npado, aże chce informacyji lo konta do {{GRAMMAR:MS.lp{{SITENAME}}}} ($4).\nZe tym ausdrukym sům powjůnzane kůnta:\n$2\n\n{{PLURAL:$3|Tymczasowygo hasła|Tymczasowych hasył}} możno użyć we {{PLURAL:$5|jedyn dźyń|$5 dńi}}.\n\nJak chćołżeś gynał to zrobjyć, to zaloguj śe terozki a podej swoje hasło.\n\nJak ftoś inkszy chćoł nowe hasło abo jak Ci śe przipůmńoło stare a ńy chcysz nowygo, to zignoruj to a używej starygo hasła.",
        "passwordreset-emailelement": "Mjano sprowjorza: \n$1\n\nTymczasowe hasło: \n$2",
        "passwordreset-emailsentemail": "E-brif posłany.",
-       "changeemail": "Pomjyno ausdruka e-mail",
+       "changeemail": "Zmiyń abo skasuj adresã e-mail",
        "changeemail-header": "Pomjyno ausduku e-mail",
        "changeemail-no-info": "Muśisz być zalogowany, coby uzyskać bezpostrzedńi dostymp do tyj zajty.",
        "changeemail-oldemail": "Uobecny ausdruk:",
        "resettokens": "Resetuj tokeny",
        "bold_sample": "Ruby tekst",
        "bold_tip": "Ruby tekst",
-       "italic_sample": "Przechylůny tekst",
-       "italic_tip": "Przechylůny tekst",
-       "link_sample": "Titel linka",
+       "italic_sample": "Kursywa",
+       "italic_tip": "Kursywa",
+       "link_sample": "Tytuł linku",
        "link_tip": "Wewnytrzny link",
-       "extlink_sample": "http://www.example.com titla linku",
-       "extlink_tip": "Eksterny link (pamjyntej uo prefikśe http:// )",
-       "headline_sample": "Tekst iberszryftu",
-       "headline_tip": "Iberszryft 2. stůpńo",
-       "nowiki_sample": "Wćepej sam tekst bez formatowańo",
-       "nowiki_tip": "Zignoruj formatowańy wiki",
-       "image_tip": "Plik uosadzůny we zajće",
-       "media_tip": "Link do plika",
-       "sig_tip": "Twojo szrajbka ze datum a czasym",
-       "hr_tip": "Poźůmo lińijo (używej mjyrńy)",
-       "summary": "Popis půmjyńań:",
+       "extlink_sample": "http://www.example.com tytuł linku",
+       "extlink_tip": "Zewnyntrzny link (pamiyntej ô prefiksie http:// )",
+       "headline_sample": "Tekst nŏgōwka",
+       "headline_tip": "Nŏgōwek 2. poziōmu",
+       "nowiki_sample": "Wraź sam niysformatowany tekst",
+       "nowiki_tip": "Ignoruj formatowanie wiki",
+       "image_tip": "Wrażōny zbiōr",
+       "media_tip": "Link do zbioru",
+       "sig_tip": "Twōj podpis ze datōm i czasym",
+       "hr_tip": "Poziōmŏ linijŏ (niy nadużywej)",
+       "summary": "Ôpis zmian:",
        "subject": "Tyjma/iberszryft:",
-       "minoredit": "To je niywielgŏ pōmiana",
-       "watchthis": "Dej pozůr",
-       "savearticle": "Spamjyntej",
-       "preview": "Uobźyrańy",
-       "showpreview": "Uobźyrej",
-       "showdiff": "Pozdrzyj na půmjyńańy",
-       "anoneditwarning": "<strong>Dej pozůr:</strong> Ńy jeżeś zalogůwany. Twůj IP ausdruk bydźe bez wszyjskich widoczny eli zrobisz egal jako půmjana. Eli <strong>[$1 zalogůjesz śe]</strong> abo <strong>[$2 stworzisz kůnto]</strong>, Twoje půmjany bydům przipisane do kůnta, wroz ze inkszymi korzyśćůma.",
+       "minoredit": "To je małŏ zmiana",
+       "watchthis": "Ôbserwuj tã strōnã",
+       "savearticle": "Spamiyntej",
+       "preview": "Podglōnd",
+       "showpreview": "Pokŏż podglōnd",
+       "showdiff": "Pokŏż zmiany",
+       "anoneditwarning": "<strong>Pozōr:</strong> Niy je żeś wlogowany(ŏ). Jak zrobisz jakeś zmiany, to Twoja adresa IP bydzie publicznie widać. Jeźli <strong>[$1 sie wlogujesz]</strong> abo <strong>[$2 stworzisz kōnto]</strong>, to Twoje zmiany bydōm przipisane do kōnta społym ze inkszymi profitami.",
        "anonpreviewwarning": "Ńy jeżeś zalogowany. Twój IP ausdruk uostańy spamjyntany, eli ty bydźesz sprowjać zajte.",
        "missingsummary": "'''Pozůr:''' Ńy wprowadźůł żeś uopisu pomjyńań. Kej go ńy chcesz wprowadzać, naćiś knefel Spamjyntej jeszcze roz.",
        "missingcommenttext": "Wćepej kůmyntorz půńiżyj.",
        "nosuchsectiontitle": "Ńy mo takij tajli",
        "nosuchsectiontext": "Průbowołżeś sprowjać tajla kero ńy istńeje.",
        "loginreqtitle": "Muśisz śe zalogować",
-       "loginreqlink": "zaloguj śe",
+       "loginreqlink": "Wloguj sie",
        "loginreqpagetext": "Muśisz $1 coby můc przeglůndać inksze zajty.",
        "accmailtitle": "Hasło posłane.",
        "accmailtext": "Cufalńe hasło lo [[User talk:$1|$1]] uostoło posłane do $2. Hasło lo tygo nowygo kůnta po zalogowańu je mogebność pomjyńić na zajće ''[[Special:ChangePassword|pomjyńańe hasła]]''.",
        "newarticle": "(Nowy)",
-       "newarticletext": "Niy ma artikla ze takim titlym. Eli chcesz go sprŏwić, napisz niżyj jego tekst (wiyncyj informacyji znojdziesz [$1 na zajcie pōmocy]). Eli jeżeś sam felernie, naciś ino knefel \"Nazŏd\" we swojij przeziyrŏczce.",
+       "newarticletext": "Prōbujesz ôtworzić link do strōny, co jeszcze niy istniyje.\nŻeby stworzić strōnã, weź wkludzać we polu niżyj (wejzdrzij na [$1 strōnã pōmocy]). Jeźliś je sam bez cufal, to kliknij knefel <strong>nazŏd</strong> we przeglōndarce.",
        "anontalkpagetext": "----\n<em>To je strōna dyskusyje anōnimowego używŏcza – takigo, co niy mŏ jeszcze swojigo kōnta abo niy chce go terŏz używać.</em>\nŻeby go idyntyfikować, używōmy adresōw IP.\nAle adresa IP może być używanŏ ôd wielu używŏczōw.\nJeźli je żeś anōnimowy używŏcz i uwŏżŏsz, iże wkludzōne sam kōmyntŏrze niy sōm do Ciebie, to [[Special:CreateAccount|stwōrz kōnto]] abo [[Special:UserLogin|wloguj sie]], żeby żŏdyn Cie niy mylōł z inkszymi anōnimowymi używŏczami.",
        "noarticletext": "Niy ma terŏz żŏdnego tekstu.\nMożesz [[Special:Search/{{PAGENAME}}|szukać tego tytułu na inkszych strōnach]],\n<span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{urlencode:{{FULLPAGENAMEE}}}}}} przeszukać regest] \nabo [{{fullurl:{{FULLPAGENAME}}|action=edit}} stworzić tã strōnã]</span>.",
-       "noarticletext-nopermission": "Ta zajta terozki je pusto.\nMogesz [[Special:Search/{{PAGENAME}}|wysznupać ta titla]] we treśćach inkszych zajtůw, abo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przesznupać powjůnzane rejery]</span>, nale ńy mosz uprowńyń coby ta zajta wćepać",
+       "noarticletext-nopermission": "Ta strōna je terŏz prōznŏ.\nMożesz [[Special:Search/{{PAGENAME}}|szukać tego tytułu]] we treściach inkszych strōn abo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przeszukać powiōnzane regesty]</span>, ale niy mŏsz praw do stworzyniŏ tyj strōny.",
        "userpage-userdoesnotexist": "Użytkowńik \"<nowiki>$1</nowiki>\" ńy je zarejesztrowany. Sprowdź eli na pewno chćołżeś stworzyć/pomjynić gynał ta zajta.",
        "userpage-userdoesnotexist-view": "Kōnto używŏcza''$1'' niy je zaregistrowane.",
        "blocked-notice-logextract": "{{GENDER:$1|Tyn sprowjorz|Ta sprowjorka}} mo zawrzite sprowjyńa.",
        "userinvalidconfigtitle": "<strong>Pozůr:</strong> Ńy mo skůrki uo mjańe \"$1\". Pamjyntej, aże zajty użytkowńika zawjyrajůnce CSS, JSON i JavaScript powinny zaczynać śe małům buchsztabům, lb. {{ns:user}}:Foo/vector.css.",
        "updated": "(Pomjyńano)",
        "note": "'''Pozůr:'''",
-       "previewnote": "'''To je ino podglůnd - artikel jeszcze ńy je spamjyntany!'''",
-       "continue-editing": "Pōdź do placu edycyje",
+       "previewnote": "<strong>Pamiyntej, iże to je ino podglōnd.</strong>\nZmiany jeszcze niy sōm spamiyntane!",
+       "continue-editing": "Idź do pola edycyje",
        "previewconflict": "Wersyjo podglůndano uodnośi śe do tekstu ze pola edycyje na wjyrchu. Tak bydźe wyglůndać zajta jeli zdecydujesz śe jům naszkryflać.",
        "session_fail_preview": "'''Pozůr! Serwer ńy może przetworzić tyj edycyji, beztuż co dane sesyji uostoły utracůne.\nPoprůbuj jeszcze roz.\nEli to tyż ńy do podpory – [[Special:UserLogout|wyloguj śe]] a zaloguj jeszcze roz.'''",
        "session_fail_preview_html": "'''Przepraszomy! Serwer ńy może przetworzić tygo sprowjyńo skuli utraty danych ze sesyji.'''\n\n''Jako iże na {{GRAMMAR:MS.lp|{{SITENAME}}}} włůnczono zostoła uopcyjo \"raw HTML\", podglůnd zostoł schrůńony coby zabezpjeczyć przed atakami JavaScript.''\n\n'''Jeli to je prawiduowo průba sprowjańo, sprůbuj ješče roz. Kejby to ńy pomoguo - wylůguj śe a zalůguj na nowo.'''",
        "token_suffix_mismatch": "'''Twoje sprowjyńy zostoło uodćepane skuli tego, co twůj klijynt pomjyszoł znaki uod interpůnkcyji we żetůńe sprowjyń. Twoje sprowjyńy zostoło uodćepane coby zapobjec zńyszczyńu tekstu zajty. Take felery zdorzajům śe w roźe korzistańo ze felernych anůnimowych śećowych usłůg proxy.'''",
        "editing": "Edytujesz $1",
-       "creating": "Tworzyńy $1",
-       "editingsection": "Edytujesz $1 (sekcyjo)",
+       "creating": "Tworzynie $1",
+       "editingsection": "Edytujesz $1 (sekcyjŏ)",
        "editingcomment": "Sprowjosz \"$1\" (nowy kůmyntorz)",
        "editconflict": "Kůnflikt sprowjyń: $1",
        "explainconflict": "Ftoś zdůnżůł wćepać swoja wersyjo artikla ńim żeś naszkryflou sprowjyńy.\nWe polu edycyji na wjyrchu mosz tekst zajty aktuelńy naszkryflany we baźe danych.\nTwoje pomjyńańo sům we polu edycyji půńiżyj.\nBy wćepać swoje pomjyńańo muśisz pomjyńać tekst we polu na wjyrchu.\n'''Ino''' tekst ze pola na wjyrchu bydźe naszkryflany we baźe jak \nwciśńesz knefel \"$1\".",
        "semiprotectedpagewarning": "'''Pozůr:''' Ta zajta zostoła zawarto a ino zaregiszterowani użytkownicy mogům jům sprowjać.\nUostotńy wpis w rejerze je ńyżej.",
        "cascadeprotectedwarning": "'''Dej pozůr:''' Ta zajta zostoła zawarto a ino użytkowńicy ze uprawńyńami admińistratora mogům jům sprowjać. Zajta ta je podpjynto pod {{PLURAL:$1|nastympujůnco zajta, kero zostoła zawarto|nastympujůncych zajtach, kere zostouy zawarte}} ze załůnczonům uopcjům dźedźiczyńo:",
        "titleprotectedwarning": "'''Dej pozůr: Zajta uo tym titlu zostoła zawarto a ino [[Special:ListGroupRights|ńykerzi użytkowńicy]] mogům jům wćepać.'''\nUostatńy wpis z rejera je ńyżej.",
-       "templatesused": "{{PLURAL:$1|Muster|Mustry}} użyte na tyj zajće:",
-       "templatesusedpreview": "{{PLURAL:$1|Muster|Mustry}} użyte na tyj zajće:",
+       "templatesused": "{{PLURAL:$1|Muster użyty|Mustry użyte}} na tyj strōnie:",
+       "templatesusedpreview": "{{PLURAL:$1|Muster użyty|Mustry użyte}} na tyj podglōńdzie:",
        "templatesusedsection": "{{PLURAL:$1|Szablon|Szablůny}} użyte we tyj tajli:",
        "template-protected": "(chrōniōny)",
-       "template-semiprotected": "(tajlowo zawarte)",
-       "hiddencategories": "Ta zajta je {{PLURAL:$1|we jednyj schrůńunyj katygoryji|we $1 schrůńunych katygoryjach}}:",
+       "template-semiprotected": "(pōłzawarte)",
+       "hiddencategories": "Ta strōna je we {{PLURAL:$1|jednyj skrytyj kategoryji|$1 skrytych kategoryjach}}:",
        "nocreatetext": "Na {{GRAMMAR:MS.lp|{{SITENAME}}}} tworzyńy nowych zajtůw uograńiczůno.\nMoges sprowjać te co już sům, abo [[Special:UserLogin|zalogować śe, abo śa zaregisztrować]].",
        "nocreate-loggedin": "Ńy mosz uprowńyń do tworzyńo nowych zajtůw.",
        "sectioneditnotsupported-title": "Sprowjańy tajli ńymogebne",
        "sectioneditnotsupported-text": "Sprowjańy tajli ńymogebne na tyj zajće.",
-       "permissionserrors": "Felerne uprawńyńo",
+       "permissionserrors": "Feler uprawniyń",
        "permissionserrorstext": "Ńy mosz uprowńyń do takij akcyje {{PLURAL:$1|skuli tego, co:|bestůż, co:}}",
-       "permissionserrorstext-withaction": "Ńy mogesz $2, ze {{PLURAL:$1|takigo powodu|takich powodůw}}:",
-       "recreate-moveddeleted-warn": "'''Uostrzeżyńy: Wćepujesz ta samo zajta, kero bůła poprzedńo wyćepano.'''\n\nZastanůw śe, czy noleżoło by śe go sam wćepywać.\nRejer wyćepań tyj zajty je podany půńiżej, cobyś mjoł wygoda:",
+       "permissionserrorstext-withaction": "Niy mŏsz przizwolyniŏ na $2, skuli {{PLURAL:$1|takigo powodu|takich powodōw}}:",
+       "recreate-moveddeleted-warn": "<strong>Pozōr: Prziwrŏcŏsz strōnã, co była przōdzij skasowanŏ.</strong>\n\nDej pozōr, czy prziwrōcynie tyj strōny je nŏleżne.\nRegesty kasowań i pōnkniyńć tyj strōny idzie ôbejzdrzeć niżyj.",
        "moveddeleted-notice": "Ta strōna była skasowanŏ.\nRegest skasowań, zabezpieczyń i pōnkniyńć tyj strōny je pokŏzany niżyj.",
        "log-fulllog": "Ukoż rejer",
        "edit-hook-aborted": "Sprowjyńy sztopńynte skiż hoka.\nŃy je wjadůme pů jakymu.",
        "parser-template-loop-warning": "Wykryto muster zapyntlyńo: [[$1]]",
        "parser-template-recursion-depth-warning": "Przekroczůno limit głymbokośći rekurencyji mustru ($1)",
        "undo-success": "Sprowjyńy zostoło wycofane. Prosza pomjarkować ukozane půniżyj dyferencyje mjyndzy wersyjůma, coby zweryfikować jejich poprawność, potym zaś naszkryflać pomjyńańo coby zakończyć uoperacyjo.",
-       "undo-failure": "Edycyjŏ niy może być cofniyntŏ skuli ôstudy ze wersyjōma postrzednimi.",
+       "undo-failure": "Ta edycyjŏ niy może być cŏfniyntŏ skuli kōnfliktu ze wersyjami postrzednimi.",
        "undo-norev": "Sprowjyńo ńy idźe cofnůńć skuli tego, co ńy istńije abo uostoło wyćepane.",
        "undo-summary": "Wycůfańy wersyji $1 naszkryflanej bez [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]])",
        "cantcreateaccount-text": "Tworzyńy kůnta s tygo adresu IP ('''$1''') uostoło zawarte bez użytkowńika [[User:$3|$3]].\n\nSkuli: ''$2''",
-       "viewpagelogs": "Uobocz rejery uoperacyji lo tyj zajty",
+       "viewpagelogs": "Ôbejzdrz regesty dlŏ tyj strōny",
        "nohistory": "Ta zajta ńy mo swojij historyje sprowjyń.",
        "currentrev": "Aktuelno wersyjo",
-       "currentrev-asof": "Aktuelno wersyjo na dźyń $1",
+       "currentrev-asof": "Teroźnŏ wersyjŏ na dziyń $1",
        "revisionasof": "Wersyjŏ ze dnia $1",
-       "revision-info": "Wersyjo ze dńo $1 autorstwa {{GENDER:$6|$2}}$7",
+       "revision-info": "Wersyjo ze dnia $1 autorstwa {{GENDER:$6|$2}}$7",
        "previousrevision": "← starszŏ wersyjŏ",
-       "nextrevision": "Nostympno wersyjo→",
-       "currentrevisionlink": "Aktualno wersyjo",
-       "cur": "akt.",
+       "nextrevision": "Nastympnŏ wersyjŏ →",
+       "currentrevisionlink": "Terŏźnŏ wersyjŏ",
+       "cur": "ter.",
        "next": "nastympno",
        "last": "poprz.",
        "page_first": "poczůnek",
        "page_last": "kůńec",
-       "histlegend": "Wybůr růżńic do porůwnańo: postow kropki we boksach a naćiś enter abo knefel na dole.<br />\nLegynda: (akt.) - růżńice s wersyjům bjeżůncům, (poprz.) - růżńice s wersyjům poprzedzajůncům, d - drobne zmjany",
+       "histlegend": "Ôbranie rōżnic: Ôznŏcz szaltry przi wersyjach do porōwnaniŏ i wziś enter abo knefel na spodku.<br />\nLegynda: <strong>({{int:cur}})</strong> = rōżnica ze ôstatniōm wersyjōm, <strong>({{int:last}})</strong> = rōżnica ze poprzedniōm wersyjōm, <strong>{{int:minoreditletter}}</strong> = małŏ edycyjŏ.",
        "history-fieldset-title": "Filtruj wersyje",
        "history-show-deleted": "Jyno wyćepane",
        "histfirst": "nojstarsze",
        "histlast": "nojnowsze",
        "historysize": "({{PLURAL:$1|1 bajt|$1 bajty|$1 bajtůw}})",
        "historyempty": "(blank)",
-       "history-feed-title": "Gyszichta wersyjůw",
-       "history-feed-description": "Historyjo wersyje tyj zajty wiki",
+       "history-feed-title": "Historyjŏ wersyji",
+       "history-feed-description": "Historyjo wersyji tyj strōny wiki",
        "history-feed-item-nocomment": "$1 uo $2",
        "history-feed-empty": "Wybrano zajta ńy istńije.\nMůgła uostać wyćepano abo przećepano pod inksze mjano.\nMożesz tyż [[Special:Search|sznupać]] za tům zajtům.",
        "rev-deleted-comment": "(kůmyntorz wyćepany)",
        "rev-deleted-event": "(szkryflańy wyćepane)",
        "rev-deleted-text-permission": "Wersyjo tyj zajty uostoua wyćepano a ńy je dostympna publičńy. Ščygůuy idźe znejść we [{{fullurl:{{#Special:Log}}/suppress|page={{PAGENAMEE}}}} rejeře wyćepań].",
        "rev-deleted-text-view": "Ta wersyjo zajty uostoua wyćepano a ńy je dostympna publičńy.\nAtoli kej admińistrator {{GRAMMAR:MS.lp|{{SITENAME}}}} možeš jům uobejřeć.\nPowody wyćepańo idźe znejść we [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} rejeře wyćepań]",
-       "rev-delundel": "ukoż/schrůń",
+       "rev-delundel": "pokŏż/skryj",
        "rev-showdeleted": "ukoż",
        "revisiondelete": "Wyćep/wćep nazod wersyje",
        "revdelete-nooldid-title": "Ńy wybrano wersyji",
        "mergehistory-comment": "Historyjo [[:$1]] skuplowano ze [[:$2]]: $3",
        "mergehistory-same-destination": "Zajta zdrzůdłowo a docelowo ńy mogům być te same.",
        "mergehistory-reason": "Kůmyntorz:",
-       "mergelog": "Skuplowane",
+       "mergelog": "Regest scalyń",
        "revertmerge": "Uodkupluj",
        "mergelogpagetext": "Půńiżyj je lista uostatńich kuplowań historyji půmjyńań zajtůw.",
-       "history-title": "Historyjŏ pōmian zajty \"$1\"",
-       "difference-title": "$1: Růżńice mjyndzy wersyjůma",
+       "history-title": "Historyjŏ wersyji strōny „$1”",
+       "difference-title": "$1: Porōwnanie wersyji",
        "difference-multipage": "(Porůwnańy zajt)",
        "lineno": "Linijŏ $1:",
-       "compareselectedversions": "zrůwnej uobrane wersyje",
+       "compareselectedversions": "Porōwnej ôbrane wersyje",
        "showhideselectedversions": "Ukoż/ukryj uobrane wersyje",
        "editundo": "cŏfnij",
        "diff-empty": "(Brak rōżnic)",
        "diff-multi-sameuser": "({{PLURAL:$1|Niyma pokŏzanŏ jedna postrzedniŏ wersyjŏ|Niy sōm pokŏzane $1 postrzednie wersyje|Niy je pokŏzane $1 postrzednich wersyji}} ôd tego samego używŏcza)",
-       "diff-multi-otherusers": "({{PLURAL:$1|Niyma pokŏzanŏ jedna postrzedniŏ wersyjŏ|Niy sōm pokŏzane $1 postrzednie wersyje|Niy je pokŏzane $1 postrzednich wersyji}} ôd {{PLURAL:$2|jednego inkszego używŏcza|$2 inkszych używŏczōw}} )",
+       "diff-multi-otherusers": "({{PLURAL:$1|Niyma pokŏzanŏ jedna postrzedniŏ wersyjŏ|Niy sōm pokŏzane $1 postrzednie wersyje|Niy je pokŏzane $1 postrzednich wersyji}} ôd {{PLURAL:$2|jednego inkszego używŏcza|$2 inkszych używŏczōw}})",
        "diff-multi-manyusers": "(Ńy pokozano {{PLURAL:$1|jydnyj wersyji postrzedńij|$1 wersyji postrzedńich}}, sprowjanej bez {{PLURAL:$2|jydnygo sprowjorza|$2 sprowjorzow}} .)",
        "difference-missing-revision": "{{PLURAL:$2|Wersyjo|$2 wersyje|$2 wersyji}} #$1 zajty \"{{PAGENAME}}\" ńy {{PLURAL:$2|uostoła znaleźůno|uostoły znaleźůne|uostoło znaleźůnych}}. Zauobycz je to skiż starygo linky do wyćępanyj zajty. Powůd wyćepańa nojdźesz we [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rejerze].",
        "searchresults": "Efekty szukaniŏ",
        "titlematches": "Znolyźono we titlach:",
        "textmatches": "Znejdźono na zajtach:",
        "notextmatches": "Ńy znejdźono we tekście zajtůw",
-       "prevn": "poprzedńe {{PLURAL:$1|$1}}",
-       "nextn": "nostympne {{PLURAL:$1|$1}}",
-       "prevn-title": "{{PLURAL:$1|Poprzedńi|Poprzedńe}} $1 {{PLURAL:$1|wyńik|wyńiki|wyńikůw}}",
-       "nextn-title": "{{PLURAL:$1|Dalszy|Dalsze|Dalszych}} $1 {{PLURAL:$1|wyńik|wyńiki|wyńikůw}}",
+       "prevn": "{{PLURAL:$1|poprzedni|poprzednie $1}}",
+       "nextn": "{{PLURAL:$1|nastympny|nastympne $1}}",
+       "prevn-title": "{{PLURAL:$1|Poprzedni|Poprzedie|Poprzednich}} $1 {{PLURAL:$1|wynik|wyniki|wynikōw}}",
+       "nextn-title": "{{PLURAL:$1|Dalszy|Dalsze|Dalszych}} $1 {{PLURAL:$1|wynik|wyniki|wynikōw}}",
        "shown-title": "Pokŏż $1 {{PLURAL:$1|wynik|wyniki|wynikōw}} na strōnã",
-       "viewprevnext": "Uobźyrej ($1 {{int:pipe-separator}} $2) ($3)",
-       "searchmenu-exists": "'''Ńy ma zajty uo mjańy \"[[:$1]]\" na tyj wiki'''",
-       "searchmenu-new": "<strong>Sprŏw zajtã „[[:$1]]” na tyj wiki!</strong> {{PLURAL:$2|0=|Ôbezdrzij tyż zajtã ze efektami podszukōnkōw.|Ôbezdrzij tyż efekty podszukōnkōw.}}",
+       "viewprevnext": "Pokŏż ($1 {{int:pipe-separator}} $2) ($3)",
+       "searchmenu-exists": "<strong>Na tyj wiki je strōna ze mianym „[[:$1]]”.</strong>",
+       "searchmenu-new": "<strong>Stwōrz strōnã „[[:$1]]” na tyj wiki!</strong> {{PLURAL:$2|0=|Ôbejzdrzij tyż strōnã ze wynikami szukaniŏ.|Ôbejzdrzij tyż wyniki szukaniŏ.}}",
        "searchprofile-articles": "Strōny",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Wszyjsko",
        "searchprofile-everything-tooltip": "Szukanie we cołkij zawartości (społym ze strōnami dyskusyje)",
        "searchprofile-advanced-tooltip": "Szukanie we ôbranych zortach mian",
        "search-result-size": "$1 ({{PLURAL:$2|1 słowo|$2 słowa|$2 słōw}})",
-       "search-result-category-size": "{{PLURAL:$1|1 element|$1 elementy|$1 elementów}} ({{PLURAL:$2|1 kategoryjo|$2 kategoryje|$2 kategoryje}}, {{PLURAL:$3|1 uobrozek|$3 uobrozki|$3 uobrozkow}})",
+       "search-result-category-size": "{{PLURAL:$1|1 elymynt|$1 elymynta|$1 elymyntōw}} ({{PLURAL:$2|1 podkategoryjŏ|$2 podkategoryje|$2 podkategoryji}}, {{PLURAL:$3|1 zbiōr|$3 zbiory|$3 zbiorōw}})",
        "search-redirect": "(pōnkniyńcie ze $1)",
-       "search-section": "(tajla $1)",
+       "search-section": "(sekcyjŏ $1)",
        "search-file-match": "(ôdpowiadŏ zawartości zbioru)",
-       "search-suggest": "Myśloł żeś: $1 ?",
+       "search-suggest": "Niy rozchodzi sie ô: $1",
        "search-interwiki-caption": "Śostrzane projekty",
        "search-interwiki-default": "$1 wyńiki:",
        "search-interwiki-more": "(wjyncyj)",
        "searchrelated": "podane",
        "searchall": "wszyjske",
        "showingresults": "To lista na keryj je {{PLURAL:$1|'''1''' wyńik|'''$1''' wyńikůw}}, sztartujůnc uod nůmery '''$2'''.",
-       "search-showingresults": "{{PLURAL:$4|Rezultat <strong>$1</strong> ze <strong>$3</strong>|Rezultaty <strong>$1 - $2</strong> ze <strong>$3</strong>}}",
-       "search-nonefound": "Å\83y mo wynikůw, kere uodpadajům kryterjům zapytaÅ\84o.",
+       "search-showingresults": "{{PLURAL:$4|Rezultat <strong>$1</strong> ze <strong>$3</strong>|Rezultaty <strong>$1  $2</strong> ze <strong>$3</strong>}}",
+       "search-nonefound": "Å»Å\8fdne wyniki niy Ã´dpowiadajÅ\8dm tymu zapytaniu.",
        "powersearch-legend": "Sznupańy zaawansowane",
        "powersearch-ns": "Sznupej we przestrzyńach mjan:",
        "powersearch-togglelabel": "Uoznocz:",
        "group-user": "Używŏcze",
        "group-autoconfirmed": "Autōmatycznie przituplowani używŏcze",
        "group-bot": "Boty",
-       "group-sysop": "Admińi",
+       "group-sysop": "Administratorzi",
        "group-bureaucrat": "Bjurokraty",
        "group-suppress": "Rewizorze",
        "group-all": "(wszyjscy)",
        "grouppage-user": "{{ns:project}}:Używŏcze",
        "grouppage-autoconfirmed": "{{ns:project}}:Autōmatycznie przituplowani używŏcze",
        "grouppage-bot": "{{ns:project}}:Boty",
-       "grouppage-sysop": "{{ns:project}}:Admińistratory",
+       "grouppage-sysop": "{{ns:project}}:Administratorzi",
        "grouppage-bureaucrat": "{{ns:project}}:Bjurokraty",
        "grouppage-suppress": "{{ns:project}}:Rewizorze",
        "right-read": "Czytej zajty",
        "right-siteadmin": "Zawjerańy i uodmykańy bazy danych",
        "newuserlogpage": "Ksiōnżka nowych używŏczōw",
        "newuserlogpagetext": "To je rejer uostatńo utworzůnych kůnt użytkowńikůw",
-       "rightslog": "Uprawńyńo",
+       "rightslog": "Regest uprawniyń używŏczōw",
        "rightslogtext": "Rejer půmjyńań uprawńyń užytkowńikůw.",
        "action-read": "přeglůndańo tyj zajty",
-       "action-edit": "edycyje tyj zajty",
+       "action-edit": "edycyje tyj strōny",
        "action-createpage": "tworzyńo zajtůw",
        "action-createtalk": "tworzyńo zajtůw godki",
        "action-createaccount": "stworzynie tego kōnta używŏcza",
        "action-userrights-interwiki": "sprowjańo uprowńyń sprowjořy na inkšych witrynach wiki",
        "action-siteadmin": "zawarćo a uodymkńyńćo bazy danych",
        "nchanges": "$1 {{PLURAL:$1|pomjyńańe|pomjyńańa|pomjyńań}}",
-       "enhancedrc-history": "gyszichta",
+       "enhancedrc-history": "historyjŏ",
        "recentchanges": "Ôstatnie zmiany",
        "recentchanges-legend": "Ôpcyje ôstatnich zmian",
        "recentchanges-summary": "Na tyj strōnie idzie śledzić ôstatnie zmiany na wiki.",
        "recentchanges-label-newpage": "Ta edycyjŏ stworziła nowõ strōnã",
        "recentchanges-label-minor": "To je małŏ zmiana",
        "recentchanges-label-bot": "To je zmiana zrobiōnŏ ôd bota",
-       "recentchanges-label-unpatrolled": "Ta edycyjŏ niy ôstała jeszcze przichwŏlōnŏ",
-       "recentchanges-label-plusminus": "Půmjyńono mjara zajty we bajtach",
+       "recentchanges-label-unpatrolled": "Ta edycyjŏ niy była jeszcze sprawdzōnŏ",
+       "recentchanges-label-plusminus": "Strōna zmiyniyła srogość ô tela bajtōw",
        "recentchanges-legend-heading": "<strong>Legynda:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (uobejrzij tyż [[Special:NewPages|lista nowych zajt]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ôbejzdrzij tyż [[Special:NewPages|listã nowych strōn]])",
        "rcnotefrom": "Niżyj {{PLURAL:$5|je zmiana|sōm zmiany}} ôd <strong>$3, $4</strong> ({{PLURAL:$5|je pokŏzanŏ|sōm pokŏzane}} nojwyżyj <strong>$1</strong>).",
-       "rclistfrom": "Ukoż půmjyńańa uod $3 $2",
-       "rcshowhideminor": "$1 drobne půmjyńańa",
-       "rcshowhideminor-show": "Pokoż",
-       "rcshowhideminor-hide": "Schrůń",
+       "rclistfrom": "Pokŏż zmiany ôd $3 $2",
+       "rcshowhideminor": "$1 małe zmiany",
+       "rcshowhideminor-show": "Pokŏż",
+       "rcshowhideminor-hide": "Skryj",
        "rcshowhidebots": "$1 boty",
-       "rcshowhidebots-show": "Pokoż",
-       "rcshowhidebots-hide": "Schrůń",
-       "rcshowhideliu": "$1 zaregisztrowanych",
+       "rcshowhidebots-show": "Pokŏż",
+       "rcshowhidebots-hide": "Skryj",
+       "rcshowhideliu": "$1 zaregistrowanych",
        "rcshowhideliu-show": "Pokŏż",
-       "rcshowhideliu-hide": "Schrůń",
-       "rcshowhideanons": "$1 anůÅ\84imowych",
-       "rcshowhideanons-show": "Pokoż",
-       "rcshowhideanons-hide": "Schrůń",
-       "rcshowhidepatr": "$1 uowjerzůne",
+       "rcshowhideliu-hide": "Skryj",
+       "rcshowhideanons": "$1 anÅ\8dnimowych używÅ\8fczÅ\8dw",
+       "rcshowhideanons-show": "Pokŏż",
+       "rcshowhideanons-hide": "Skryj",
+       "rcshowhidepatr": "$1 zweryfikowane edycyje",
        "rcshowhidemine": "$1 moje edycyje",
-       "rcshowhidemine-show": "Pokoż",
-       "rcshowhidemine-hide": "Schrůń",
-       "rclinks": "Ukŏż ôstatnie $1 mian bez ôstatnie $2 dni.",
+       "rcshowhidemine-show": "Pokŏż",
+       "rcshowhidemine-hide": "Skryj",
+       "rclinks": "Ukŏż ôstatnie $1 zmian bez ôstatnie $2 dni.",
        "diff": "rōżn.",
        "hist": "hist.",
-       "hide": "Schrůń",
-       "show": "Ukoż",
+       "hide": "Skryj",
+       "show": "Pokŏż",
        "minoreditletter": "d",
        "newpageletter": "N",
        "boteditletter": "b",
        "rc-enhanced-expand": "Pokoż szczygůły",
        "rc-enhanced-hide": "Schrůń detajle",
        "rc-old-title": "ôryginalnie stworzōne za „$1”",
-       "recentchangeslinked": "Půmjyńańa we nalinkowanych",
+       "recentchangeslinked": "Zmiany we linkowanych",
        "recentchangeslinked-feed": "Pomjyńańa we adresowanych",
        "recentchangeslinked-toolbox": "Zmiany we linkowanych",
-       "recentchangeslinked-title": "Pomjyńyńo w adrésowanych s \"$1\"",
+       "recentchangeslinked-title": "Zmiany we linkowanych z „$1”",
        "recentchangeslinked-summary": "Wkludź miano strōny, żeby ôbejzdrzeć zmiany na strōnach linkowanych do nij abo co dō nij linkujōm. (Żeby ôbejzdrzeć strōny z kategoryje, wkludź {{ns:category}}:Miano kategoryje). Strōny ze [[Special:Watchlist|Ôbserwowanych]] sōm <strong>porubiōne</strong>.",
-       "recentchangeslinked-page": "Mjano zajty",
-       "recentchangeslinked-to": "Ukoż půmjyńańa na zajtach, kere linkujům na uobrano zajta",
+       "recentchangeslinked-page": "Miano strōny:",
+       "recentchangeslinked-to": "Pokŏż zmiany na strōnach, co linkujōm do podanyj strōny",
        "upload": "Zaladuj zbiōr",
        "uploadbtn": "Wćepej sam plik",
        "reuploaddesc": "Nazod do formulařa uod wćepywańo.",
        "upload-permitted": "Dopuščalne formaty plikůw: $1.",
        "upload-preferred": "Zalecane formaty plikůw: $1.",
        "upload-prohibited": "Zakozane formaty plikůw: $1.",
-       "uploadlogpage": "Wćepane sam",
+       "uploadlogpage": "Regest przisłań",
        "uploadlogpagetext": "Půńiżyj jee lista plikůw wćepanych na uostatku.\nPrzelyź na zajta [[Special:NewFiles|galeryje nowych plikůw]], coby uobejzdrzeć pliki kej mińatůrki.",
        "filename": "Mjano pliku",
-       "filedesc": "Popis",
+       "filedesc": "Ôpis",
        "fileuploadsummary": "Uopis:",
        "filestatus": "Status prawny:",
        "filesource": "Kod zdřůduowy:",
        "upload-curl-error6-text": "Podany URL je ńyosiůngalny. Proša, sprowdź dokuadńy čy podany URL je prawidouwy i čy dano zajta dźauo.",
        "upload-curl-error28": "Překročůny čas kery bůu na wćepywańe",
        "upload-curl-error28-text": "Zajta uodpowjado za powoli. Proša, sprawdź čy zajta dźauo, uodčekej pora minut i sprůbuj zaś. Možeš tyž sprůbować wončas kej zajta bydźe mńij uobćůnžůno.",
-       "license": "Licencyjo:",
-       "license-header": "Licencyjo",
+       "license": "Licyncyjŏ:",
+       "license-header": "Licyncyjŏ",
        "nolicense": "Ńy wybrano (naškryflej rynčńy!)",
        "license-nopreview": "(Podglůnd ńydostympny)",
        "upload_source_url": " (poprowny, publičńy dostympny URL)",
        "upload_source_file": "(plik na twojym kůmputrze)",
        "listfiles-summary": "To je ekstra zajta na kery sům pokazywane wšyske pliki wćepane na serwer. Důmyślńy na wiyrchu listy wyśwjetlajům śe pliki wćepane na uostatku. Coby půmjyńić sposůb sortowańo, klikńij na naguůwek kolůmny.",
        "listfiles_search_for": "Šnupej za grafikům uo mjańe:",
-       "imgfile": "plik",
-       "listfiles": "Lista plikůw",
+       "imgfile": "zbiōr",
+       "listfiles": "Lista zbiorōw",
        "listfiles_date": "Data",
        "listfiles_name": "Mjano",
        "listfiles_user": "Užytkowńik",
        "filehist-help": "Kliknij w datã/czas, żeby ôbejzdrzeć zbiōr, jak wtynczŏs wyglōndoł.",
        "filehist-deleteall": "wyćep wszyske",
        "filehist-deleteone": "Wyćep",
-       "filehist-revert": "cofej",
+       "filehist-revert": "cŏfnij",
        "filehist-current": "terŏźnŏ",
        "filehist-datetime": "Data i czas",
        "filehist-thumb": "Miniatura",
        "filehist-thumbtext": "Miniatura wersyje $1",
-       "filehist-nothumb": "Ńy ma mińjaturki",
+       "filehist-nothumb": "Bez miniatury",
        "filehist-user": "Używŏcz",
        "filehist-dimensions": "Wymiary",
        "filehist-filesize": "Rozmior plika",
        "sharedupload-desc-here": "Tyn zbiōr je ze $1 i może być używany we inkszych projektach.\nÔpis na jego [$2 strōnie ôpisu zbioru] je pokŏzany niżyj.",
        "filepage-nofile": "Niy ma zbioru ze tym mianym.",
        "uploadnewversion-linktext": "Wćepńij nowšo wersyjo tygo plika",
-       "upload-disallowed-here": "Ńy moges nadpisać tygo plika.",
+       "upload-disallowed-here": "Niy możesz podmiynić tego zbioru.",
        "filerevert": "Přiwracańy $1",
        "filerevert-legend": "Přiwracańy poprzedńy wersje plika",
        "filerevert-intro": "Zamjeřoš přiwrůćić '''[[Media:$1|$1]]''' do wersje z [$4 $3, $2].",
        "randompage-nopages": "We przestrzyńi mjan \"$1\" ńy ma żodnych zajtůw.",
        "randomredirect": "Losowe překerowańy",
        "randomredirect-nopages": "We przestrzyńi mjan \"$1\" ńy ma przekerowań.",
-       "statistics": "Sztatystyka",
+       "statistics": "Statystyka",
        "statistics-header-pages": "Statystyka zajtůw",
        "statistics-header-edits": "Statystyka sprowjyń",
        "statistics-header-users": "Statystyka užytkowńikůw",
        "doubleredirects": "Podwůjne překierowańa",
        "doubleredirectstext": "Na tyi liśće mogům znojdować śe překerowańo pozorne. Uoznača to, aže půńižej pjyrwšej lińii artikla, zawjerajůncyj \"#REDIRECT ...\", može znojdować śe dodotkowy tekst. Koždy wjerš listy zawjero uodwouańo do pjyrwšygo i drůgygo překerowańo a pjyrwšom lińjům tekstu drůgygo překerowańo. Uůmožliwjo to na ogůu uodnaleźyńy wuaśćiwygo artikla, do kerygo powinno śe překerowywać.",
        "double-redirect-fixed-move": "zajta [[$1]] zostoła zastůmpjůno bez przekerowańy, skiż jeij przekludzyńo ku [[$2]]",
-       "double-redirect-fixer": "Korektor przekerowań",
+       "double-redirect-fixer": "Korektōr przekerowań",
        "brokenredirects": "Zuomane překerowańa",
        "brokenredirectstext": "Překerowańo půńižej wskazujům na artikle kerych sam ńy ma.",
        "brokenredirects-edit": "sprowjéj",
        "nbytes": "$1 {{PLURAL:$1|bajt|bajty|bajtōw}}",
        "ncategories": "$1 {{PLURAL:$1|kategoryja|kategorje|kategorjůw}}",
        "nlinks": "$1 {{PLURAL:$1|link|linki|linkůw}}",
-       "nmembers": "$1 {{PLURAL:$1|elyment|elymenty|elymentůw}}",
+       "nmembers": "$1 {{PLURAL:$1|elymynt|elymynta|elymyntōw}}",
        "nrevisions": "$1 {{PLURAL:$1|wersja|wersje|wersjůw}}",
        "specialpage-empty": "Ta zajta je pusto.",
        "lonelypages": "Poćepńynte zajty",
        "mostcategories": "Zajty kere majům nojwiyncyj kategoryjůw",
        "mostimages": "Nojczyńśćij adresowane pliki",
        "mostrevisions": "Nojczyńśćij sprowjane artikle",
-       "prefixindex": "Wszyskie zajty wedle prefiksa",
+       "prefixindex": "Wszyjske strōny ze prefiksym",
        "shortpages": "Nojkrůtsze zajty",
        "longpages": "Duge artikle",
        "deadendpages": "Artikle bez linkůw",
        "newpages": "Nowe strōny",
        "newpages-username": "Mjano użytkowńika:",
        "ancientpages": "Nojstarše artikle",
-       "move": "Przećep",
+       "move": "Przeniyś",
        "movethispage": "Přećepej ta zajta",
        "unusedimagestext": "Pamjyntej, proša, aže inkše witryny, np. projekty Wikimedja w inkšych godkach, můgům adresować do tych plikůw užywajůnc bezpośredńo URL. Bez tůž ńykere ze plikůw můgům sam być na tej liśće pokozane mimo, aže žodna zajta ńy adresuje do ńich.",
        "unusedcategoriestext": "Katygorje pokazane půńižej istńejům, choć ńy kořisto s ńich žadyn artikel ańi katygorja.",
        "booksources-invalid-isbn": "Podany numer ISBN zostoł rozpoznany kej felerny. Sprowdź aże podany numer je zgodny s numerym kery je we zdrzůdle.",
        "specialloguserlabel": "Fto:",
        "speciallogtitlelabel": "Cyl (nazwa abo {{ns:user}}:miano ôd używŏcza):",
-       "log": "Register dźołano",
-       "all-logs-page": "Wszyjstke uoperacyje",
-       "alllogstext": "Wspůlny rejer wszyjstkych typůw uoperacyji do {{SITENAME}}.\nMożesz zawyńźić liczba wyńikůw wybjerajůnc typ rejeru, mjano użytkowńika abo titel zajty (wjelge a mołe buchsztaby majům znoczyńy).",
+       "log": "Regest ôperacyji",
+       "all-logs-page": "Wszyjske óperacyje",
+       "alllogstext": "Spōlne pokŏzanie wszyjskich dostympnych regestōw {{SITENAME}}.\nMożesz uakuratnić widok bez ôbranie zorty regestu, miana ôd używŏcza, abo tykanyj strōny (dŏwŏ pozōr na małe i sroge litery).",
        "logempty": "Niy ma we regeście zgodliwych elymyntōw.",
        "log-title-wildcard": "Šnupej za titlami kere začynojům śe uod tygo tekstu",
        "allpages": "Wszyjske strōny",
        "prevpage": "Popředńo zajta ($1)",
        "allpagesfrom": "Zajty začynojůnce śe na:",
        "allpagesto": "Zajty uo titlach kere na zadku majům:",
-       "allarticles": "Wszyske zajty",
+       "allarticles": "Wszyjske strōny",
        "allinnamespace": "Wszyjstke zajty (we przestrzyńi mjan $1)",
        "allpagessubmit": "Idź",
        "allpagesprefix": "Ukoż artikle s prefiksym:",
        "allpagesbadtitle": "Podane mjano je felerne, zawjyro prefiks mjyndzyprojektowy abo mjyndzygodkow. Może uůne tyż zawjerać jako buchsztaba abo inksze znaki, kerych ńy wolno używać we mjanach.",
        "allpages-bad-ns": "{{GRAMMAR:MS.lp|{{SITENAME}}}} ńy mo przestrzyńi mjan „$1”.",
-       "allpages-hide-redirects": "Ukoż pukńyńća",
+       "allpages-hide-redirects": "Pokŏż przekerowania",
        "categories": "Kategoryje",
        "categoriespagetext": "Zajta przedstowjo lista katygoryji s zajtůma a plikůma.\n[[Special:UnusedCategories|Ńyużywane kategoryj]] ńy zostoły tukej pokozane.\nKukńij tyż [[Special:WantedCategories|ńyistńyjůnce kategoryje]].",
        "categoriesfrom": "Pokož kategoryje začynajůnc uod:",
        "listgrouprights-group": "Grupa",
        "listgrouprights-rights": "Uprawńyńo",
        "listgrouprights-helppage": "Help:Uprawńyńo grup użytkowńikůw",
-       "listgrouprights-members": "(listo człůnkůw grupy)",
+       "listgrouprights-members": "(lista czōnkōw grupy)",
        "listgrouprights-addgroup": "Idźe dodać do {{PLURAL:$2|grupy|grup}}: $1",
        "listgrouprights-removegroup": "Idźe wyćepać s {{PLURAL:$2|grupy|grup}}: $1",
        "listgrouprights-addgroup-all": "Idźe dodać do kożdyj grupy",
        "listgrouprights-addgroup-self": "Je mogebny dać swe konto do {{PLURAL:$2|grupy|grup:}} $1",
        "mailnologin": "Brak adresu",
        "mailnologintext": "Muśyš śe [[Special:UserLogin|zalůgować]] i mjeć wpisany aktualny adres e-brif w swojich [[Special:Preferences|preferyncyjach]], coby můc wysuać e-brif do inkšygo užytkowńika.",
-       "emailuser": "Poślij tymu używoczowi e-brif",
+       "emailuser": "Poślij tymu używŏczowi e-mail",
        "emailpagetext": "Możesz użyć půńiższygo formularza, coby wysłać wjadůmość e-brif do tygo użytkowńika.\nAdres e-brifa, kery zostoł bez Ćebje wkludzůny we [[Special:Preferences|Twojich sztalowańach]], pojawi śe we polu „Uod”, bez cůż uodbjorca bydźe můg Ći uodpedźeć.",
        "defemailsubject": "{{SITENAME}} - e-mail ôd używŏcza \"$1\"",
        "usermaildisabled": "E-mail ôd używŏcza je zastŏwiōny",
        "usermessage-editor": "Nadŏwca systymowych kōmunikatōw",
        "watchlist": "Ôbserwowane",
        "mywatchlist": "Ôbserwowane",
-       "watchlistfor2": "Lo $1 ($2)",
+       "watchlistfor2": "{{GENDER:$1|Używŏcza|Używŏczki}} $1 $2",
        "nowatchlist": "Ńy ma žodnych pozycyji na liśće zajtůw, na kere dowoš pozůr.",
        "watchlistanontext": "$1 coby uobejřeć abo sprowjać elymynty listy zajtůw, na kere dowoš pozůr",
        "watchnologin": "Ńy jest žeś zalůgowany",
        "addedwatchtext": "Zajta \"[[:$1]]\" zostoua dodano do Twojij [[Special:Watchlist|listy artiklůw, na kere dowoš pozůr]].\nNa tyi liśće bydźeš mjou rejer přišuych sprowjyń tyi zajty i jeji zajty godki, a mjano zajty bydźeš mjou škryflane '''tustym''' na [[Special:RecentChanges|liśće půmjyńanych na ůostatku]], cobyś mjou wygoda w jei pomjyńańa filować.",
        "removedwatchtext": "Artikel \"[[:$1]]\" zostou wyćepńjynty s [[Special:Watchlist|Twojij pozorlisty]].",
-       "watch": "Dej pozůr",
+       "watch": "Ôbserwuj",
        "watchthispage": "Dej pozůr",
        "unwatch": "Niy ôbserwuj",
        "unwatchthispage": "Přestoń dować pozůr",
        "notvisiblerev": "Wersyja zostoua wyćepano",
        "watchlist-details": "Na Twojij liście ôbserwowanych {{PLURAL:$1|je $1 strōna|sōm $1 strōny|je $1 strōn}} (plus strōny dyskusyje).",
        "wlheader-enotif": "Wysůuańy powjadůmjyń na adres e-brif je zouůnčůne",
-       "wlheader-showupdated": "Zajty, kere były pōmiyniane ôd twojij ôstatnij nŏwiydzki na nich ôstały ukŏzane '''na rubo'''",
+       "wlheader-showupdated": "Zajty, co były zmiyniane ôd twojij ôstatnij nŏwiydzki na nich ôstały ukŏzane <strong>na rubo</strong>.",
        "wlnote": "Niżyj {{PLURAL:$1|je ôstaniŏ zmiana|sōm ôstatnie <strong>$1</strong> zmiany|je ôstatnie <strong>$1</strong> zmian}} ze {{PLURAL:$2|ôstatnij godziny|ôstatnich <strong>$2</strong> godzin}}, na $3, $4.",
        "wlshowlast": "Pokŏż ôstatnie $1 godzin $2 dni",
-       "watchlist-options": "Uopcyje artikli na kere dowosz pozůr",
+       "watchlist-options": "Ôpcyje ôbserwowanych",
        "watching": "Dowom pozor...",
        "unwatching": "Ńy dowům pozůr.",
        "enotif_reset": "Ôznŏcz wszyjske strōny za nawiydzōne",
        "actioncomplete": "Fertig",
        "actionfailed": "Ńy udało śe.",
        "deletedtext": "Wyćepano \"$1\". Rejer uostatnio zrobiůnych wyćepań možeš uobejžyć tukej: $2.",
-       "dellogpage": "Wyćepane",
+       "dellogpage": "Regest kasowań",
        "dellogpagetext": "To je lista uostatńo wykůnanych wyćepań.",
        "deletionlog": "rejer wyćepań",
        "reverted": "Přiwrůcůno popředńo wersyja",
        "revertpage": "Wycofano sprowjyńe użytkowńika [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]]). Autor prziwrůcůnej wersyji to [[User:$1|$1]].",
        "rollback-success": "Wycofano sprowjyńa użytkowńika $1.\nPrziwrůcůno uostatńo wersyja autorstwa  $2.",
        "sessionfailure": "Feler weryfikacyji zalůgowańo.\nPolecyńy zostoło anulowane, coby ůńiknůńć przechwycyńo sesyji.\n\nNaćiś knefel „cofej”, przeładuj zajta, a potym zaś wydej polecyńy",
-       "protectlogpage": "Zawarte",
+       "protectlogpage": "Regest zawarć",
        "protectlogtext": "Půńižej znojdowo śe lista zawarć i uodymkńjyńć pojydynčych zajtůw.\nCoby přejřeć lista uobecńy zawartych zajtůw, přeńdź na zajta wykazu [[Special:ProtectedPages|zawartych zajtůw]].",
-       "protectedarticle": "zawar [[$1]]",
-       "modifiedarticleprotection": "pomjyńiu poźům zawarćo [[$1]]",
+       "protectedarticle": "zawar „[[$1]]”",
+       "modifiedarticleprotection": "zmiyniōł(yła) poziōm zawarciŏ „[[$1]]”",
        "unprotectedarticle": "uodymknyu [[$1]]",
        "movedarticleprotection": "przekludzůno sztalowańa zabezpjeczyńo s „[[$2]]” ku „[[$1]]”",
        "protect-title": "Pomjyńeńe poźomu zawarćo „$1”",
        "protect-locked-dblock": "Ńy idźe půmjyńić poźůmu zawarća s kuli tygo co baza danych tyž je zawarto. Uobecne štalowańa dla zajty '''$1''' to:",
        "protect-locked-access": "Ńy moš uprowńyń coby pomjyńyć poziům zawarcia zajty. Uobecne ustawjyńo dlo zajty '''$1''' to:",
        "protect-cascadeon": "Ta zajta je zawarto od pomjyńań, po takjymu, co jei užywo {{PLURAL:$1|ta zajta, kero je zawarto|nastympůjůnce zajty, kere zostauy zawarte}} a opcyjo dźedźičyńo je zaůončono. Možeš pomjyńyć poziům zawarcia tyi zajty, ale dlo dźedźičyńo zawarcia to ńy mo wpuywu.",
-       "protect-default": "Dozwolōne do wszyjskich używaczy.",
+       "protect-default": "Przizwolōne wszyjskim",
        "protect-fallback": "Wymago pozwolynjo \"$1\"",
        "protect-level-autoconfirmed": "Blokuj nowe a ńyregistrowane używocze",
        "protect-level-sysop": "Ino admini",
        "undelete-show-file-submit": "Ja",
        "namespace": "Przestrzyń mian:",
        "invert": "Ôdwrōć zaznaczynie",
-       "tooltip-invert": "Ôznŏcz tyn kastlik, coby skryć pōmiany na zajtach we ôbranych przestrzyniach mian (i swiōnzanych ze nimi inkszymi przestrzyniami mian, eli ôznŏczōno)",
-       "namespace_association": "powiōnzanŏ przestrzyń mian",
-       "tooltip-namespace_association": "Ôznŏcz tyn kastlik, coby zawrzić zajty dyskusyje i tyjmy swiōnzane ze ôbranymi przestrzyniami mian",
+       "tooltip-invert": "Ôznŏcz te pole, coby skryć zmiany na strōnach we ôbranyj przestrzyni mian (i swiōnzanōm z niōm inkszōm przestrzyniōm mian, jeźli je ôznaczōnŏ)",
+       "namespace_association": "Swiōnzanŏ przestrzyń mian",
+       "tooltip-namespace_association": "Ôznŏcz te pole, coby przidać strōnã dyskusyje i tymat swiōnzane ze ôbranōm przestrzyniōm mian",
        "blanknamespace": "(przodńo)",
-       "contributions": "Ajnzac {{GENDER:$1|używocza|używoczki}}",
-       "contributions-title": "Wkłod użytkowńika $1",
+       "contributions": "Wkłŏd ôd {{GENDER:$1|używŏcza|używŏczki}}",
+       "contributions-title": "Wkłŏd {{GENDER:$1|używŏcza|używŏczki}} $1",
        "mycontris": "Edycyje",
        "anoncontribs": "Edycyje",
-       "contribsub2": "Lo {{GENDER:$3|używocza|używoczki}} $1 ($2)",
-       "nocontribs": "Brak pomjyńań uodpowjadajůncych tym kryterjům.",
-       "uctop": "teroźńo",
+       "contribsub2": "{{GENDER:$3|używŏcza|używŏczki}} $1 ($2)",
+       "nocontribs": "Brak zmian, co ôdpowiadajōm tym kryteriōm.",
+       "uctop": "terŏźnŏ",
        "month": "Do miesiōnca:",
        "year": "Do roku:",
-       "sp-contributions-newbies": "Pokoż ajnzac ino uod nowych użytkowńikůw",
+       "sp-contributions-newbies": "Pokŏż ino wkłŏd ôd nowych kōnt",
        "sp-contributions-newbies-sub": "Dlo nowych užytkowńikůw",
        "sp-contributions-newbies-title": "Wkłod nowych użytkowńików",
-       "sp-contributions-blocklog": "zawarća",
-       "sp-contributions-deleted": "Wyćepane sprowjyńa użytkowńika",
-       "sp-contributions-uploads": "wćepane uobrozki",
-       "sp-contributions-logs": "rejer dźołońo",
-       "sp-contributions-talk": "↓ dyskusyjo",
-       "sp-contributions-userrights": "Zařůndzańy prowami užytkowńikůw",
+       "sp-contributions-blocklog": "zawarcia",
+       "sp-contributions-deleted": "skasowany wkłŏd ôd {{GENDER:$1|używŏcza|używŏczki}}",
+       "sp-contributions-uploads": "zaladowane zbiory",
+       "sp-contributions-logs": "regest",
+       "sp-contributions-talk": "dyskusyjŏ",
+       "sp-contributions-userrights": "zarzōndzanie prawami ôd {{GENDER:$1|używŏcza|używŏczki}}",
        "sp-contributions-search": "Szukej wkładu",
-       "sp-contributions-username": "Adres IP abo mjano użytkowńika",
-       "sp-contributions-toponly": "Ukoż jyno ůostanie wersyje",
+       "sp-contributions-username": "Adresa IP abo miano używŏcza",
+       "sp-contributions-toponly": "Pokŏż ino edycyje, co sōm ôstatnimi wersyjami",
        "sp-contributions-newonly": "Pokŏż ino edycyje, co stworziły strōny",
        "sp-contributions-submit": "Szukej",
        "whatlinkshere": "Co sam linkuje",
-       "whatlinkshere-title": "Zajty, kere linkujům na \"$1\"",
-       "whatlinkshere-page": "Zajta:",
-       "linkshere": "Nastympůjůnce zajty sóm adrésůwane do '''$1''':",
-       "nolinkshere": "Żodno zajta ńy je adrésowana do '''$2'''.",
+       "whatlinkshere-title": "Strōny, co linkujōm do „$1”",
+       "whatlinkshere-page": "Strōna:",
+       "linkshere": "Te strōny linkujōm do <strong>$2</strong>:",
+       "nolinkshere": "Żŏdnŏ strōna niy linkuje do <strong>$2</strong>.",
        "nolinkshere-ns": "Žodno zajta ńy je adresowano do '''$2''' we wybrany přestřyni mjan.",
        "isredirect": "strōna przekerowaniŏ",
-       "istemplate": "doÅ\82ůnczony muster",
-       "isimage": "Link do plika",
+       "istemplate": "doÅ\82Å\8dnczynie",
+       "isimage": "link do zbioru",
        "whatlinkshere-prev": "{{PLURAL:$1|poprzednie|poprzednie $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|nastympne|nastympne $1}}",
-       "whatlinkshere-links": "← do adrésata",
+       "whatlinkshere-links": "← linki",
        "whatlinkshere-hideredirs": "$1 pōnkniyńcia",
        "whatlinkshere-hidetrans": "$1 dołōnczynia",
        "whatlinkshere-hidelinks": "$1 linki",
        "whatlinkshere-hideimages": "$1 linki zbiorōw",
-       "whatlinkshere-filters": "Filtery",
+       "whatlinkshere-filters": "Filtry",
        "blockip": "Zawrzij sprowjorza",
        "blockiptext": "Tyn formularz służy do zawjerańo sprowjyń spod uokreślůnygo adresu IP abo kůnkretnymu użytkowńikowi.\nZawjerać noleży jydyńy po to, by zapobjec wandalizmům, zgodńy ze [[{{MediaWiki:Policy-url}}|przijyntymi reglůma]].\nPodej powůd (np. umjeszczajůnc mjana zajtůw, na kerych dopuszczůno śe wandalizmu).",
        "ipaddressorusername": "Adres IP abo mjano użytkowńika",
        "ipbenableautoblock": "Zawřij uostatńi adres IP tygo užytkowńika i autůmatyčńy wšyjstke kolejne, s kerych bydźe průbowou sprowjać zajty",
        "ipbsubmit": "Zawřij uod sprowjyń tygo užytkowńika",
        "ipbother": "Ikszy czas",
-       "ipboptions": "2 godźiny:2 hours,1 dźyń:1 day,3 dńi:3 days,1 tydźyń:1 week,2 tydńe:2 weeks,1 mjeśůnc:1 month,3 mjeśůnce:3 months,6 mjeśůncůw:6 months,1 rok:1 year,nawdy:infinite",
+       "ipboptions": "2 godziny:2 hours,1 dziyń:1 day,3 dni:3 days,1 tydziyń:1 week,2 tydnie:2 weeks,1 miesiōnc:1 month,3 miesiōnce:3 months,6 miesiyncy:6 months,1 rok:1 year,na dycki:infinite",
        "ipbhidename": "Schrůń mjano użytkowńika/adres IP w rejerze zawarć, na liśće aktywnych zawarć i liśće użytkowńikůw",
        "ipbwatchuser": "Dowej pozůr na zajta uosobisto i zajta godki tygo užytkowńika",
        "ipb-change-block": "Zmjyń sztalowańa zawarća uod sprowjyń",
        "ipblocklist-empty": "Lista zawarć je pusto.",
        "ipblocklist-no-results": "Podany adres IP abo užytkowńik ńy je zawarty uod sprowjyń.",
        "blocklink": "blokuj",
-       "unblocklink": "uodymknij",
-       "change-blocklink": "půmjyń zawarće uod sprowjyń",
+       "unblocklink": "ôdblokuj",
+       "change-blocklink": "zmiyń blokadã",
        "contribslink": "wkłŏd",
        "autoblocker": "Zawarto Ci sprowjyńo autůmatyczńy, bez tůż co używosz tygo samygo adresu IP, co używocz „[[User:$1|$1]]”.\nPowůd zawarća $1 to: „$2”",
-       "blocklogpage": "Gyszichta zawjyrańo",
-       "blocklogentry": "zawarto [[$1]], bydźe uodymkńynty: $2 $3",
-       "reblock-logentry": "{{GENDER:$2|pōmiynił|pōmiyniła}} nasztalowania zawarciŏ dlŏ [[$1]], czas zawarciŏ: $2 $3",
+       "blocklogpage": "Regest blokad",
+       "blocklogentry": "zawartŏ [[$1]], bydzie ôtwartŏ: $2 $3",
+       "reblock-logentry": "{{GENDER:$2|zmiynił|zmiyniyła}} sztelōnki zawarciŏ dlŏ [[$1]], kōniec zawarciŏ: $2 $3",
        "blocklogtext": "Půńižej znojdowo śe lista zawarć zouožůnych i zdjyntych s poščygůlnych adresůw IP.\nNa li'śće ńy mo adresůw IP, kere zawarto w sposůb autůmatyčny.\nCoby přejřeć lista uobecńy aktywnych zawarć, přyńdź na zajta [[Special:BlockList|zawartych adresůw i užytkowńikůw]].",
        "unblocklogentry": "uodymknyu $1",
        "block-log-flags-anononly": "ino anůnimowi",
-       "block-log-flags-nocreate": "tworzińy kůnta je zawarte",
+       "block-log-flags-nocreate": "tworzynie kōnta je zastawiōne",
        "block-log-flags-noautoblock": "autůmatyczne zawjerańy uod sprawjyń wyłůnczůne",
        "block-log-flags-noemail": "e-brif zawarty",
        "block-log-flags-nousertalk": "ńy może sprowjać włosnyj zajty godki",
        "ipb_cant_unblock": "Feler: Zawarće uo ID $1 ńy zostouo znejdźone. Moguo uone zostać oudymkńynte wčeśnij.",
        "ipb_blocked_as_range": "Feler: Adres IP $1 ńy zostou zawarty bezpośredńo i ńy može zostać uodymkńjynty.\nNoležy uůn do zawartygo zakresu adresůw $2. Uodymknůńć možno ino couki zakres.",
        "ip_range_invalid": "Ńypoprowny zakres adresów IP.",
-       "proxyblocker": "Zawjyrańe proxy",
+       "proxyblocker": "Blokowanie proxy",
        "proxyblockreason": "Twůj adres IP zostou zawarty, bo je to adres uotwartygo proxy.\nSprawa noležy wyjaśńić s dostawcům Internetu abo půmocům techńičnům informujůnc uo tym powažnym problymje s bezpječyństwym.",
        "sorbsreason": "Twůj adres IP znojdowo śe na liśće serwerůw open proxy w DNSBL, užywanej bez {{GRAMMAR:B.lp|{{SITENAME}}}}.",
        "sorbs_create_account_reason": "Twůj adres IP znojdowo śe na liśće serwerůw open proxy w DNSBL, užywanej bez {{GRAMMAR:B.lp|{{SITENAME}}}}.\nŃy možeš utwořić kůnta",
        "movepage-page-moved": "Zajta $1 uostoła przekludzůno ku $2.",
        "movepage-page-unmoved": "Mjana zajty $1 ńy idźe půmjyńić na $2.",
        "movepage-max-pages": "Przekludzůnych uostało $1 {{PLURAL:$1|zajta|zajty|zajtůw}}. Wjynkszyj liczby ńy idźe przekludźić automatyczńy.",
-       "movelogpage": "Przećepńynte",
+       "movelogpage": "Regest przeniysiōnych",
        "movelogpagetext": "Uoto lista zajtůw, kere uostatńo zostouy přećepane.",
        "movereason": "Czymu:",
        "revertmove": "cofej",
        "imageinvalidfilename": "Mjano plika docelowygo je felerne",
        "fix-double-redirects": "Poprow przekerowańa kere adresujům ku uoryginalnymu titlowi zajty",
        "move-leave-redirect": "Uostow przekerowańy pode dotychczasowym titlem",
-       "export": "Eksport zajtůw",
+       "export": "Eksport strōn",
        "exporttext": "Možeš wyeksportować treść i historja sprowjyń jednyj zajty abo zestawu zajtůw we formaće XML.\nWyeksportowane informacyje možna půźńij zaimportować do inkšej wiki, dźouajůncyj na uoprůgramowańu MediaWiki, kořistajůnc ze [[Special:Import|zajty importu]].\n\nWyeksportowańy wjelu zajtůw wymogo wpisańo půńižej titli zajtůw, po jednym titlu we wjeršu a uokreślyńo čy mo zostać wyeksportowano bježůnco čy wšyjstke wersyje zajty s uopisůma sprawjyń abo tyž ino bježůnca wersyjo s uopisym uostatńygo sprawjyńo.\n\nMožeš tyž užyć linku, np.[[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] do zajty „[[{{MediaWiki:Mainpage}}]]”.",
        "exportcuronly": "Ino bježůnco wersyjo, bes historji",
        "exportnohistory": "----\n'''Pozůr:''' Wůuůnčůno možliwość eksportowańo peunej historii zajtůw s užyćym tygo nařyńdźa s kuli kuopotůw s wydajnośćůn",
        "import-upload": "Wćepej dane XML",
        "import-token-mismatch": "Straćiły śe dane ze sesyje. Prosza spróbować zaś.",
        "import-invalid-interwiki": "Ńy idźe importować s podanyj wiki.",
-       "importlogpage": "Rejer importa",
+       "importlogpage": "Regest importōw",
        "importlogpagetext": "Rejer přeprowadzůnych importůw zajtůw s inkšych serwisůw wiki.",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|wersyja|wersyje|wersyji}}",
        "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|wersyja|wersyje|wersyji}} ze $2",
        "tooltip-pt-mytalk": "{{GENDER:|Moja}} strōna dyskusyje",
        "tooltip-pt-anontalk": "Godka użytkowńika do adresu IP spod kerygo sprowjosz",
        "tooltip-pt-preferences": "{{GENDER:|Moje}} preferyncyje",
-       "tooltip-pt-watchlist": "Lista artiklůw, na kere dowosz pozůr",
-       "tooltip-pt-mycontris": "Lista {{GENDER:|moich}} edycyji",
+       "tooltip-pt-watchlist": "Lista strōn, co je ôbserwujesz",
+       "tooltip-pt-mycontris": "Lista {{GENDER:|mojich}} edycyji",
        "tooltip-pt-login": "Rekōmyndujymy wlogowanie, ale ône niyma musowe.",
        "tooltip-pt-logout": "Ôdloguj sie",
        "tooltip-pt-createaccount": "Rekōmyndujymy stworzynie kōnta i wlogowanie sie, ale to niyma musowe.",
        "tooltip-ca-talk": "Dyskusyjŏ ô strōnie",
        "tooltip-ca-edit": "Edytuj tã strōnã",
        "tooltip-ca-addsection": "Przidej nowõ sekcyjõ",
-       "tooltip-ca-viewsource": "Ta zajta je zawrzito. Mogesz uobźyrać zdrzůdłowy tekst.",
+       "tooltip-ca-viewsource": "Ta strōna je zawartŏ. Możesz ôglōndać jeji zdrzōdło.",
        "tooltip-ca-history": "Starsze wersyje tyj strōny",
        "tooltip-ca-protect": "Zawrzij tã strōnã",
-       "tooltip-ca-delete": "Wyćep ta zajta",
+       "tooltip-ca-delete": "Skasuj tã strōnã",
        "tooltip-ca-undelete": "Prziwrůć wersyjo tyj zajty sprzed wyćepańo",
-       "tooltip-ca-move": "Przećep ta zajta kaj indzij.",
+       "tooltip-ca-move": "Przeniyś tã strōnã",
        "tooltip-ca-watch": "Przidej tã strōnã do ôbserwowanych",
        "tooltip-ca-unwatch": "Skasuj tyn artykuł ze ôbserwowanych",
        "tooltip-search": "Szukej we {{SITENAME}}",
        "tooltip-t-print": "Wersyjŏ do durku",
        "tooltip-t-permalink": "Trwały link do tyj wersyje strōny",
        "tooltip-ca-nstab-main": "Pokŏż strōnã treści",
-       "tooltip-ca-nstab-user": "Ukoż perzůnalno zajta używocza",
+       "tooltip-ca-nstab-user": "Wejzdrzij na strōnã ôd użwŏcza",
        "tooltip-ca-nstab-media": "Uobejřij zajta artikla",
        "tooltip-ca-nstab-special": "To je specjalnŏ strōna i niy idzie jij edytować",
        "tooltip-ca-nstab-project": "Pokŏż strōnã projektu",
        "tooltip-ca-nstab-image": "Pokŏż strōnã grafiki",
        "tooltip-ca-nstab-mediawiki": "Pokŏż kōmunikat systymowy",
-       "tooltip-ca-nstab-template": "Uobźyrej muster",
+       "tooltip-ca-nstab-template": "Ôbejzdrzij muster",
        "tooltip-ca-nstab-help": "Pokŏż zajtã pōmocy",
        "tooltip-ca-nstab-category": "Pokŏż strōnã kategoryje",
-       "tooltip-minoredit": "Uoznacz ta zmjana za drobno",
-       "tooltip-save": "Naszkryflej půmjyńańa",
-       "tooltip-preview": "Niźli spamiyntŏsz pōmiany pozdrzij na efekt swojij edycyje.",
-       "tooltip-diff": "Ukozuje twoje půmjyńańa we tekśće",
-       "tooltip-compareselectedversions": "Uobźyrej zmjyny mjyndzy dwůma uobranymi wersyjůma tyj zajty",
-       "tooltip-watch": "Dodej tyn artikel do pozorlisty",
+       "tooltip-minoredit": "Ôznŏcz tã zmianã za małõ edycyjõ",
+       "tooltip-save": "Spamiyntej swoje zmiany",
+       "tooltip-preview": "Podyjzdrzij swoje zmiany; użyj tego przed spamiyntowaniym.",
+       "tooltip-diff": "Pokŏż zmiany zrobiōne we tekście.",
+       "tooltip-compareselectedversions": "Ôbejzdrzij rōżnice miyndzy dwōma ôbranymi wersyjami tyj strōny",
+       "tooltip-watch": "Przidej tyn artykuł do ôbserwowanych",
        "tooltip-recreate": "Wćepej nazod zajta mimo aže bůua wčeśńij wyćepano.",
        "tooltip-upload": "Rozpočyńće wćepywańa",
        "tooltip-rollback": "\"Cŏfej\" jednym klikniyńciym cŏfie wszyjske zmiany ôd ôstatnigo używŏcza.",
-       "tooltip-undo": "\"anuluj pōmianã\" cofŏ tã edycyjõ i ôtwiyrŏ ôkno edycyje we trybie ôbziyraniŏ.\nDozwolŏ na wkludzyniy szticha we popisie pōmian.",
-       "tooltip-summary": "Krůtko popisz",
+       "tooltip-undo": "\"Cŏfnij\" cŏfie tã edycyjõ i ôtwiyrŏ ôkno edycyje we trybie podglōndu.\nDozwolŏ na wkludzynie powodu we ôpisie.",
+       "tooltip-summary": "Wkludź krōtki ôpis",
        "anonymous": "{{PLURAL:$1|Anůńimowy użytkowńik|Anůńimowe użytkowńiki}} {{SITENAME}}",
        "siteuser": "Užytkowńik {{GRAMMAR:D.lp|{{SITENAME}}}} – $1",
        "lastmodifiedatby": "Uostatńy sprowjyńy tej zajty: $2, $1 (autor půmjyńań: $3)",
        "markedaspatrollederror": "Ńy idźe uoznačyć kej „sprawdzůne”",
        "markedaspatrollederrortext": "Muśyš wybrać wersyja coby uoznačyć jům kej „sprawdzůna”.",
        "markedaspatrollederror-noautopatrol": "Ńy moš uprawńyń wymaganych do uoznačańo swojich sprawjyń kej „sprawdzůne”.",
-       "patrol-log-page": "Dźynńik patrolowańo",
+       "patrol-log-page": "Regest patrolowaniŏ",
        "patrol-log-header": "Půniżej je dźeńńik patrolowańo zajtůw.",
        "deletedrevision": "Wyćepano popředńy wersyje $1",
        "filedeleteerror-short": "Feler při wyćepywańu plika $1",
        "mediawarning": "'''Pozůr!''' Tyn plik može zawjerać zuośliwy kod. Jak go uodymkńyš možeš zaraźić swůj systym.",
        "imagemaxsize": "Na zajtach uopisu plikůw uůgrańič rozmjar uobrazkůw do:",
        "thumbsize": "Rozmjar mińjatůrki",
-       "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|zajta|zajty|zajtůw}}",
+       "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|strōna|strōny|strōn}}",
        "file-info": "rozmjor plika: $1, typ MIME: $2",
        "file-info-size": "$1 × $2 pikselōw, srogość zbioru: $3, zorta MIME: $4",
        "file-info-size-pages": "$1 × $2 pikselōw, srogość zbioru: $3, typ MIME: $4, $5 {{PLURAL:$5|strōna|strōny|strōn}}",
-       "file-nohires": "Wjynksze wymjyry ńy sům dostympne",
-       "svg-long-desc": "Plik SVG, nůminalńe $1 × $2 pixelůw, rozmior plika: $3",
+       "file-nohires": "Niy ma dostympnyj srogszyj rodzielczości.",
+       "svg-long-desc": "Zbiōr SVG, nōminalnie $1 × $2 pikselōw, srogość zbioru: $3",
        "show-big-image": "Ôryginalny zbiōr",
        "show-big-image-preview": "Srogość tego podglōndu: $1.",
        "show-big-image-other": "{{PLURAL:$2|Inkszŏ rozdzielczość|Inksze rozdzielczości}}: $1.",
        "sp-newimages-showfrom": "pokož nowe pliki začynajůnc uod $2, $1",
        "bad_image_list": "Dane trza wćepać we formaće:\n\nJyno tajle listy (lińije, kere śe napoczynajům uod *) absztychujemy.\nPjyrszy link we lińiji muśi być linkym do zakozanygo pliku.\nDolsze linki we lińiji sům uwożane za wyjimki  – sům to mjana zajtůw, na kerych idzie użyć plik ze zakozanym mjanym.",
        "metadata": "Metadane",
-       "metadata-help": "Tyn plik mo ekstra informacyje na isto przidane uod fotoaparata abo skanera, kere bůły użite lo powstańo tygo pliku.\nEli plik był modyfikowany, dane mogům w tajli ńy być we zgodźe ze parametrůma modyfikowanego pliku.",
+       "metadata-help": "We tym zbiorze sōm ekstra informacyje pewnikym przidane ôd fotoaparatu abo skanera użytego do zrobiyniŏ abo zdigitalizowaniŏ go.\nJeźli zbiōr bōł modyfikowany, niykere informacyje mogōm niy cołkym ôdpowiadać zmodyfikowanymu zbiorowi.",
        "metadata-expand": "Pokož ščygůuy",
        "metadata-collapse": "Schowej ščygůuy",
        "metadata-fields": "Metadane ôbrazōw wymianowane we tyj wiadōmości bydōm pokazowane na strōnie grafiki po zwiniyńciu tabule metadanych.\nInksze pola bydōm wychodnie skryte.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "confirm-purge-top": "Wyčyśćić pamjyńć podrynčnům do tyi zajty?",
        "confirm-purge-bottom": "Uodśwjyżeńy zajty wyczyśći pamjyńć podrynczno a wymuśi pokozańy jeij aktualnyj wersyji.",
        "imgmultipageprev": "← popředńo zajta",
-       "imgmultipagenext": "nostympno zajta →",
+       "imgmultipagenext": "nastympnŏ strōna →",
        "imgmultigo": "Idź!",
        "imgmultigoto": "Idź do strōny $1",
        "ascending_abbrev": "rosn.",
        "watchlisttools-clear": "Wysnŏż ôbserwowane",
        "watchlisttools-view": "Pokŏż zmiany we ôbserwowanych",
        "watchlisttools-edit": "Pokŏż i edytuj ôbserwowane",
-       "watchlisttools-raw": "Zmjyńoj surowo pozorlista",
-       "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|dyskusyjo]])",
+       "watchlisttools-raw": "tekstowy edytōr listy",
+       "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|dyskusyjŏ]])",
        "duplicate-defaultsort": "Pozůr: Zmjarkowanym kluczym sortowańo bydźe \"$2\" a zastůmpi uůn zawczasu używany klucz \"$1\".",
        "version": "Wersjo",
        "version-extensions": "Zainstalowane rozšeřyńa",
        "version-software-product": "Mjano",
        "version-software-version": "Wersjo",
        "redirect": "Przekerowanie podle zbioru, używŏcza, strōny, wersyje, abo idyntyfikatora regestu.",
-       "redirect-summary": "Ta specjalnŏ strōna przekerowuje do: zbioru (ze podanym mianym), strōny (ze podanym numerym wersyje abo idyntyfiaktorym strōny), strōny używŏcza (ze podanym idyntyfikatorym numerycznym) abo do regestu (ze podanym numerym akcyje). Spusōb użyciŏ:\n[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] abo [[{{#Special:Redirect}}/logid/186]].",
+       "redirect-summary": "Ta specjalnŏ strōna przekerowuje do: zbioru (ze podanym mianym), strōny (ze podanym numerym wersyje abo idyntyfikatorym strōny), strōny używŏcza (ze podanym idyntyfikatorym numerycznym) abo do regestu (ze podanym numerym akcyje). Spusōb użyciŏ:\n[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] abo [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Idź",
        "redirect-lookup": "Znojdź:",
        "redirect-value": "Werta:",
        "tags-active-no": "Niy",
        "tags-hitcount": "$1 {{PLURAL:$1|zmiana|zmiany|zmian}}",
        "diff-form": "Rōżnice",
-       "logentry-delete-delete": "$1 {{GENDER:$2|wyćepoł|wyćepała}} zajta $3",
+       "logentry-delete-delete": "$1 {{GENDER:$2|skasowoł|skasowała}} strōnã $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|prziwrōciōł|prziwrōciyła}} strōnã $3",
        "logentry-delete-revision": "$1 {{GENDER:$2|zmiyniōł|zmiyniyła}} widoczność {{PLURAL:$5|wersyje|$5 wersyji}} strōny $3: $4",
        "revdelete-content-hid": "zawartość skrytŏ",
        "revdelete-restricted": "naštaluj uograničyńo do administratorůw",
        "revdelete-unrestricted": "wycofej uograničyńo do administratorůw",
-       "logentry-move-move": "$1 {{GENDER:$2|przećep|przećepła}} zajta $3 do $4",
+       "logentry-move-move": "$1 {{GENDER:$2|przeniōs|przeniosła}} strōnã $3 do $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|pōnknōł|pōnkła}} strōnã $3 do $4 bez ôstawianiŏ przekerowaniŏ",
        "logentry-move-move_redir": "$1 {{GENDER:$2|pōnknōł|pōnkła}} strōnã $3 do $4 na przekerowanie",
-       "logentry-patrol-patrol-auto": "$1 automatycznie {{GENDER:$2|ôznaczōł|ôznaczyła}} wersyjõ $4 strōny $3 za sprawdzonõ",
+       "logentry-patrol-patrol-auto": "$1 autōmatycznie {{GENDER:$2|ôznaczōł|ôznaczyła}} wersyjõ $4 strōny $3 za sprawdzonõ",
        "logentry-newusers-create": "Kōnto ôd {{GENDER:$2|używŏcza|używŏczki}} $1 ôstało stworzōne",
        "logentry-newusers-autocreate": "Kōnto $1 było stworzōne autōmatycznie",
-       "logentry-upload-upload": "$1 {{GENDER:$2|posłoł|posłała}} $3",
+       "logentry-upload-upload": "$1 {{GENDER:$2|przisłoł|przisłała}} $3",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|zaladowoł|zaladowała}} nowõ wersyjõ $3",
        "rightsnone": "podstawowo",
        "searchsuggest-search": "Szukej we {{SITENAME}}",
index b83bc05..251b62c 100644 (file)
        "changeemail-submit": "Baguhin ang e-liham",
        "changeemail-throttled": "Masyadong madami ang kamakailan lamang mong pagsubok sa pag-login.\nMaghintay po muna ng $1 bago subukan uli.",
        "resettokens": "I-reset ang mga token o susi",
+       "resettokens-token-label": "$1 (kasalukuyang halaga: $2)",
        "bold_sample": "Makapal na panitik",
        "bold_tip": "Makapal na panitik",
        "italic_sample": "Nakahilig na panitik",
        "listfiles_size": "Sukat",
        "listfiles_description": "Paglalarawan",
        "listfiles_count": "Mga bersiyon",
+       "listfiles-latestversion": "Kasalukuyang bersiyon",
        "listfiles-latestversion-yes": "Oo",
        "listfiles-latestversion-no": "Hindi",
        "file-anchor-link": "File",
        "apisandbox-request-time": "Oras ng paghiling: $1",
        "apisandbox-continue": "Ipagpatuloy",
        "apisandbox-continue-clear": "Burado",
+       "apisandbox-multivalue-all-namespaces": "$1 (Lahat ng ngalan-espasyo)",
        "booksources": "Mga mapagkukunang aklat",
        "booksources-search-legend": "Maghanap ng mapagkukunang aklat",
        "booksources-isbn": "ISBN:",
        "protect-default": "Pahintulutan ang lahat ng mga tagagamit",
        "protect-fallback": "Pahintulutan ang mga tagagamit lamang na may pahintulot na \"$1\"",
        "protect-level-autoconfirmed": "Hadlangan ang bago at hindi nagpapatalang mga tagagamit",
-       "protect-level-sysop": "Mga tagapangasiwa (''sysop'') lamang",
+       "protect-level-sysop": "Pahintulutan lamang ang mga tagapangasiwa (''sysop'')",
        "protect-summary-cascade": "baita-baitang",
        "protect-expiring": "mawawalan ng bisa sa $1 (UTC)",
        "protect-expiring-local": "magtatapos sa $1",
        "logentry-newusers-autocreate": "Automatikong {{GENDER:$2|inilikha}} ang account ng tagagamit na $1",
        "logentry-upload-upload": "{{GENDER:$2|Ikinarga}} ni $1 ang $3",
        "rightsnone": "(wala)",
+       "rightslogentry-temporary-group": "$1 (pansamantala, hanggang $2)",
        "feedback-adding": "Idinaragdag ang pakaing-tugon sa pahina...",
        "feedback-back": "Magbalik",
        "feedback-bugcheck": "Mahusay! Suriin lang na hindi pa ito isa sa [$1 nalalamang mga depekto].",
index c9db9bc..4ada642 100644 (file)
                        "Fitoschido",
                        "TmY e12",
                        "Dual",
-                       "ToprakM"
+                       "ToprakM",
+                       "Suvarioglu"
                ]
        },
        "tog-underline": "Bağlantıların altını çizme:",
        "rcfilters-watchlist-preference-label": "JavaScript olmayan bir arayüz kullanın",
        "rcfilters-watchlist-preference-help": "Filtre Listesini arama olmadan veya işlevselliği vurgulayarak İzleme Listesi'ni yükler.",
        "rcfilters-target-page-placeholder": "Bir sayfa (ya da kategori) adı girin",
+       "rcfilters-allcontents-label": "Tüm içerikler",
        "rcnotefrom": "<strong>$3, $4</strong> tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır (<strong>$1</strong> tarhine kadar olanlar gösterilmektedir).",
        "rclistfromreset": "Tarih seçimini sıfırla",
        "rclistfrom": "$3 $2 tarihinden itibaren yeni değişiklikleri göster",
index 8c1df68..f9ea792 100644 (file)
        "rcfilters-filter-showlinkedto-label": "显示链接到该页面的页面上的更改",
        "rcfilters-filter-showlinkedto-option-label": "<strong>链接到</strong>选定页面的页面",
        "rcfilters-target-page-placeholder": "输入页面(或分类)名称",
+       "rcfilters-allcontents-label": "所有内容",
+       "rcfilters-alldiscussions-label": "所有讨论",
        "rcnotefrom": "下面{{PLURAL:$5|是}}<strong>$3 $4</strong>之后的更改(最多显示<strong>$1</strong>个)。",
        "rclistfromreset": "重置时间选择",
        "rclistfrom": "显示$3 $2之后的新更改",
index c5641c9..9d3ef54 100644 (file)
        "rcfilters-filter-showlinkedto-label": "顯示連結到該頁面的頁面上的更改",
        "rcfilters-filter-showlinkedto-option-label": "<strong>連結到</strong>指定頁面的頁面",
        "rcfilters-target-page-placeholder": "輸入頁面名稱(或分類)",
+       "rcfilters-allcontents-label": "所有內容",
+       "rcfilters-alldiscussions-label": "所有討論",
        "rcnotefrom": "以下{{PLURAL:$5|為}}自 <strong>$3 $4</strong> 以來的變更 (最多顯示 <strong>$1</strong> 筆)。",
        "rclistfromreset": "重設日期選擇",
        "rclistfrom": "顯示自 $3 $2 以來的新變更",
index 4213d5f..a239fa0 100644 (file)
  * @ingroup Maintenance
  */
 
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -77,13 +81,25 @@ class RebuildLocalisationCache extends Maintenance {
 
                $conf = $wgLocalisationCacheConf;
                $conf['manualRecache'] = false; // Allow fallbacks to create CDB files
-               if ( $force ) {
-                       $conf['forceRecache'] = true;
-               }
+               $conf['forceRecache'] = $force || !empty( $conf['forceRecache'] );
                if ( $this->hasOption( 'outdir' ) ) {
                        $conf['storeDirectory'] = $this->getOption( 'outdir' );
                }
-               $lc = new LocalisationCacheBulkLoad( $conf );
+               // XXX Copy-pasted from ServiceWiring.php. Do we need a factory for this one caller?
+               $lc = new LocalisationCacheBulkLoad(
+                       new ServiceOptions(
+                               LocalisationCache::$constructorOptions,
+                               $conf,
+                               MediaWikiServices::getInstance()->getMainConfig()
+                       ),
+                       new LCStoreDB( [] ),
+                       LoggerFactory::getInstance( 'localisation' ),
+                       [ function () {
+                               MediaWikiServices::getInstance()->getResourceLoader()
+                                       ->getMessageBlobStore()->clear();
+                       } ],
+                       MediaWikiServices::getInstance()->getLanguageNameUtils()
+               );
 
                $allCodes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
                if ( $this->hasOption( 'lang' ) ) {
index 35af15c..7267b2c 100644 (file)
@@ -367,7 +367,9 @@ class RebuildRecentchanges extends Maintenance {
                # @NOTE: users with 'bot' rights choose when edits are bot edits or not. That information
                # may be lost at this point (aside from joining on the patrol log table entries).
                $botgroups = [ 'bot' ];
-               $autopatrolgroups = $wgUseRCPatrol ? User::getGroupsWithPermission( 'autopatrol' ) : [];
+               $autopatrolgroups = $wgUseRCPatrol ? MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->getGroupsWithPermission( 'autopatrol' ) : [];
 
                # Flag our recent bot edits
                if ( $botgroups ) {
index e71cc88..99b548e 100644 (file)
@@ -61,6 +61,7 @@ $wgAutoloadClasses += [
        'MediaWikiPHPUnitResultPrinter' => "$testDir/phpunit/MediaWikiPHPUnitResultPrinter.php",
        'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
        'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiIntegrationTestCase.php",
+       'MediaWikiTestCaseTrait' => "$testDir/phpunit/MediaWikiTestCaseTrait.php",
        'MediaWikiUnitTestCase' => "$testDir/phpunit/MediaWikiUnitTestCase.php",
        'MediaWikiIntegrationTestCase' => "$testDir/phpunit/MediaWikiIntegrationTestCase.php",
        'MediaWikiTestResult' => "$testDir/phpunit/MediaWikiTestResult.php",
@@ -217,6 +218,12 @@ $wgAutoloadClasses += [
        'MockSearchResultSet' => "$testDir/phpunit/mocks/search/MockSearchResultSet.php",
        'MockSearchResult' => "$testDir/phpunit/mocks/search/MockSearchResult.php",
 
+       # tests/phpunit/unit/includes
+       'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php",
+
+       # tests/phpunit/unit/includes/language
+       'LanguageNameUtilsTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php",
+
        # tests/phpunit/unit/includes/libs/filebackend/fsfile
        'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php",
 
index 496f265..af5d88b 100644 (file)
@@ -23,8 +23,9 @@ use Wikimedia\TestingAccessWrapper;
 abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
        use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
        use MediaWikiGroupValidator;
+       use MediaWikiTestCaseTrait;
+       use PHPUnit4And6Compat;
 
        /**
         * The original service locator. This is overridden during setUp().
@@ -403,6 +404,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
                $wgRequest = new FauxRequest();
                MediaWiki\Session\SessionManager::resetCache();
+               Language::clearCaches();
        }
 
        public function run( PHPUnit_Framework_TestResult $result = null ) {
@@ -2510,20 +2512,6 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                        'comment' => $comment,
                ] );
        }
-
-       /**
-        * Returns a PHPUnit constraint that matches anything other than a fixed set of values. This can
-        * be used to whitelist values, e.g.
-        *   $mock->expects( $this->never() )->method( $this->anythingBut( 'foo', 'bar' ) );
-        * which will throw if any unexpected method is called.
-        *
-        * @param mixed ...$values Values that are not matched
-        */
-       protected function anythingBut( ...$values ) {
-               return $this->logicalNot( $this->logicalOr(
-                       ...array_map( [ $this, 'matches' ], $values )
-               ) );
-       }
 }
 
 class_alias( 'MediaWikiIntegrationTestCase', 'MediaWikiTestCase' );
diff --git a/tests/phpunit/MediaWikiTestCaseTrait.php b/tests/phpunit/MediaWikiTestCaseTrait.php
new file mode 100644 (file)
index 0000000..77d7c04
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * For code common to both MediaWikiUnitTestCase and MediaWikiIntegrationTestCase.
+ */
+trait MediaWikiTestCaseTrait {
+       /**
+        * Returns a PHPUnit constraint that matches anything other than a fixed set of values. This can
+        * be used to whitelist values, e.g.
+        *   $mock->expects( $this->never() )->method( $this->anythingBut( 'foo', 'bar' ) );
+        * which will throw if any unexpected method is called.
+        *
+        * @param mixed ...$values Values that are not matched
+        */
+       protected function anythingBut( ...$values ) {
+               return $this->logicalNot( $this->logicalOr(
+                       ...array_map( [ $this, 'matches' ], $values )
+               ) );
+       }
+}
index 5f7746b..ccf3357 100644 (file)
@@ -26,10 +26,13 @@ use PHPUnit\Framework\TestCase;
  *
  * Extend this class if you are testing classes which use dependency injection and do not access
  * global functions, variables, services or a storage backend.
+ *
+ * @since 1.34
  */
 abstract class MediaWikiUnitTestCase extends TestCase {
        use PHPUnit4And6Compat;
        use MediaWikiCoversValidator;
+       use MediaWikiTestCaseTrait;
 
        private $unitGlobals = [];
 
@@ -38,7 +41,7 @@ abstract class MediaWikiUnitTestCase extends TestCase {
                $reflection = new ReflectionClass( $this );
                $dirSeparator = DIRECTORY_SEPARATOR;
                if ( strpos( $reflection->getFilename(), "${dirSeparator}unit${dirSeparator}" ) === false ) {
-                       $this->fail( 'This unit test needs to be in "tests/phpunit/unit" !' );
+                       $this->fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
                }
                $this->unitGlobals = $GLOBALS;
                unset( $GLOBALS );
@@ -54,4 +57,19 @@ abstract class MediaWikiUnitTestCase extends TestCase {
                $GLOBALS = $this->unitGlobals;
                parent::tearDown();
        }
+
+       /**
+        * Create a temporary hook handler which will be reset by tearDown.
+        * This replaces other handlers for the same hook.
+        * @param string $hookName Hook name
+        * @param mixed $handler Value suitable for a hook handler
+        * @since 1.34
+        */
+       protected function setTemporaryHook( $hookName, $handler ) {
+               // This will be reset by tearDown() when it restores globals. We don't want to use
+               // Hooks::register()/clear() because they won't replace other handlers for the same hook,
+               // which doesn't match behavior of MediaWikiIntegrationTestCase.
+               global $wgHooks;
+               $wgHooks[$hookName] = [ $handler ];
+       }
 }
index 0765ab8..95571f2 100644 (file)
@@ -5,28 +5,55 @@
  * @group Database
  */
 class GlobalWithDBTest extends MediaWikiTestCase {
+       private function setUpBadImageTests( $name ) {
+               if ( in_array( $name, [
+                       'Hook bad.jpg',
+                       'Redirect to bad.jpg',
+                       'Redirect_to_good.jpg',
+                       'Redirect to hook bad.jpg',
+                       'Redirect to hook good.jpg',
+               ] ) ) {
+                       $this->markTestSkipped( "Didn't get RepoGroup working properly yet" );
+               }
+
+               // Don't try to fetch the files from Commons or anything, please
+               $this->setMwGlobals( 'wgForeignFileRepos', [] );
+               // We need to reset services immediately so that editPage() doesn't use the old RepoGroup
+               // and hit the network
+               $this->resetServices();
+
+               // XXX How do we get file redirects to work?
+               $this->editPage( 'File:Redirect to bad.jpg', '#REDIRECT [[Bad.jpg]]' );
+               $this->editPage( 'File:Redirect to good.jpg', '#REDIRECT [[Good.jpg]]' );
+               $this->editPage( 'File:Redirect to hook bad.jpg', '#REDIRECT [[Hook bad.jpg]]' );
+               $this->editPage( 'File:Redirect to hook good.jpg', '#REDIRECT [[Hook good.jpg]]' );
+
+               $this->setTemporaryHook( 'BadImage', 'BadFileLookupTest::badImageHook' );
+       }
+
        /**
-        * @dataProvider provideWfIsBadImageList
+        * @dataProvider BadFileLookupTest::provideIsBadFile
         * @covers ::wfIsBadImage
         */
-       public function testWfIsBadImage( $name, $title, $blacklist, $expected, $desc ) {
-               $this->assertEquals( $expected, wfIsBadImage( $name, $title, $blacklist ), $desc );
+       public function testWfIsBadImage( $name, $title, $expected ) {
+               $this->setUpBadImageTests( $name );
+
+               $this->editPage( 'MediaWiki:Bad image list', BadFileLookupTest::BLACKLIST );
+               $this->resetServices();
+               // Enable messages from MediaWiki namespace
+               MessageCache::singleton()->enable();
+
+               $this->assertEquals( $expected, wfIsBadImage( $name, $title ) );
        }
 
-       public static function provideWfIsBadImageList() {
-               $blacklist = '* [[File:Bad.jpg]] except [[Nasty page]]';
-
-               return [
-                       [ 'Bad.jpg', false, $blacklist, true,
-                               'Called on a bad image' ],
-                       [ 'Bad.jpg', Title::makeTitle( NS_MAIN, 'A page' ), $blacklist, true,
-                               'Called on a bad image' ],
-                       [ 'NotBad.jpg', false, $blacklist, false,
-                               'Called on a non-bad image' ],
-                       [ 'Bad.jpg', Title::makeTitle( NS_MAIN, 'Nasty page' ), $blacklist, false,
-                               'Called on a bad image but is on a whitelisted page' ],
-                       [ 'File:Bad.jpg', false, $blacklist, false,
-                               'Called on a bad image with File:' ],
-               ];
+       /**
+        * @dataProvider BadFileLookupTest::provideIsBadFile
+        * @covers ::wfIsBadImage
+        */
+       public function testWfIsBadImage_blacklistParam( $name, $title, $expected ) {
+               $this->setUpBadImageTests( $name );
+
+               $this->hideDeprecated( 'wfIsBadImage with $blacklist parameter' );
+               $this->assertSame( $expected, wfIsBadImage( $name, $title, BadFileLookupTest::BLACKLIST ) );
        }
 }
index 00b8d18..6520fc5 100644 (file)
@@ -3029,6 +3029,35 @@ class OutputPageTest extends MediaWikiTestCase {
                ];
        }
 
+       /**
+        * @param int $titleLastRevision Last Title revision to set
+        * @param int $outputRevision Revision stored in OutputPage
+        * @param bool $expectedResult Expected result of $output->isRevisionCurrent call
+        * @covers OutputPage::isRevisionCurrent
+        * @dataProvider provideIsRevisionCurrent
+        */
+       public function testIsRevisionCurrent( $titleLastRevision, $outputRevision, $expectedResult ) {
+               $titleMock = $this->getMock( Title::class, [], [], '', false );
+               $titleMock->expects( $this->any() )
+                       ->method( 'getLatestRevID' )
+                       ->willReturn( $titleLastRevision );
+
+               $output = $this->newInstance( [], null, [ 'notitle' => true ] );
+               $output->setTitle( $titleMock );
+               $output->setRevisionId( $outputRevision );
+               $this->assertEquals( $expectedResult, $output->isRevisionCurrent() );
+       }
+
+       public function provideIsRevisionCurrent() {
+               return [
+                       [ 10, null, true ],
+                       [ 42, 42, true ],
+                       [ null, 0, true ],
+                       [ 42, 47, false ],
+                       [ 47, 42, false ]
+               ];
+       }
+
        /**
         * @return OutputPage
         */
index 3c5f43b..88847e2 100644 (file)
@@ -15,6 +15,7 @@ use MediaWiki\MediaWikiServices;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RevisionLookup;
+use MWException;
 use TestAllServiceOptionsUsed;
 use Wikimedia\ScopedCallback;
 use MediaWiki\Session\SessionId;
@@ -738,7 +739,9 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                                        'BlockDisablesLogin' => false,
                                        'GroupPermissions' => [],
                                        'RevokePermissions' => [],
-                                       'AvailableRights' => []
+                                       'AvailableRights' => [],
+                                       'NamespaceProtection' => [],
+                                       'RestrictionLevels' => []
                                ]
                        ),
                        $services->getSpecialPageFactory(),
@@ -1788,4 +1791,75 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                return $revision;
        }
 
+       public function provideGetRestrictionLevels() {
+               return [
+                       'No namespace restriction' => [ [ '', 'autoconfirmed', 'sysop' ], NS_TALK ],
+                       'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
+                       'Restricted to sysop' => [ [ '' ], NS_USER ],
+                       'Restricted to someone in two groups' => [ [ '', 'sysop' ], 101 ],
+                       'No special permissions' => [
+                               [ '' ],
+                               NS_TALK,
+                               []
+                       ],
+                       'autoconfirmed' => [
+                               [ '', 'autoconfirmed' ],
+                               NS_TALK,
+                               [ 'autoconfirmed' ]
+                       ],
+                       'autoconfirmed revoked' => [
+                               [ '' ],
+                               NS_TALK,
+                               [ 'autoconfirmed', 'noeditsemiprotected' ]
+                       ],
+                       'sysop' => [
+                               [ '', 'autoconfirmed', 'sysop' ],
+                               NS_TALK,
+                               [ 'sysop' ]
+                       ],
+                       'sysop with autoconfirmed revoked (a bit silly)' => [
+                               [ '', 'sysop' ],
+                               NS_TALK,
+                               [ 'sysop', 'noeditsemiprotected' ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetRestrictionLevels
+        * @covers       \MediaWiki\Permissions\PermissionManager::getNamespaceRestrictionLevels
+        *
+        * @param array $expected
+        * @param int $ns
+        * @param array|null $userGroups
+        * @throws MWException
+        */
+       public function testGetRestrictionLevels( array $expected, $ns, array $userGroups = null ) {
+               $this->setMwGlobals( [
+                       'wgGroupPermissions' => [
+                               '*' => [ 'edit' => true ],
+                               'autoconfirmed' => [ 'editsemiprotected' => true ],
+                               'sysop' => [
+                                       'editsemiprotected' => true,
+                                       'editprotected' => true,
+                               ],
+                               'privileged' => [ 'privileged' => true ],
+                       ],
+                       'wgRevokePermissions' => [
+                               'noeditsemiprotected' => [ 'editsemiprotected' => true ],
+                       ],
+                       'wgNamespaceProtection' => [
+                               NS_MAIN => 'autoconfirmed',
+                               NS_USER => 'sysop',
+                               101 => [ 'editsemiprotected', 'privileged' ],
+                       ],
+                       'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
+                       'wgAutopromote' => []
+               ] );
+               $this->resetServices();
+               $user = is_null( $userGroups ) ? null : $this->getTestUser( $userGroups )->getUser();
+               $this->assertSame( $expected, MediaWikiServices::getInstance()
+                       ->getPermissionManager()
+                       ->getNamespaceRestrictionLevels( $ns, $user ) );
+       }
 }
index 30ba1c1..bdce70c 100644 (file)
@@ -29,8 +29,6 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                // Set up groups and rights
                $this->mUserMock->expects( $this->any() )
                        ->method( 'getEffectiveGroups' )->will( $this->returnValue( [ '*', 'user' ] ) );
-               $this->mUserMock->expects( $this->any() )
-                       ->method( 'isAllowedAny' )->will( $this->returnValue( true ) );
 
                // Set up callback for User::getOptionKinds
                $this->mUserMock->expects( $this->any() )
@@ -49,6 +47,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) );
                $this->mContext->setUser( $this->mUserMock );
 
+               $this->overrideUserPermissions( $this->mUserMock, [ 'editmyoptions' ] );
                $main = new ApiMain( $this->mContext );
 
                // Empty session
index 282188d..6308b82 100644 (file)
@@ -160,6 +160,7 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
                        'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
                        'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
                ] );
+               $this->resetServices();
 
                MessageCache::singleton()->enable();
 
index 42957b6..39526fb 100644 (file)
@@ -1,4 +1,9 @@
 <?php
+
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
+use Psr\Log\NullLogger;
+
 /**
  * @group Database
  * @group Cache
@@ -19,8 +24,51 @@ class LocalisationCacheTest extends MediaWikiTestCase {
         */
        protected function getMockLocalisationCache() {
                global $IP;
-               $lc = $this->getMockBuilder( \LocalisationCache::class )
-                       ->setConstructorArgs( [ [ 'store' => 'detect' ] ] )
+
+               $mockLangNameUtils = $this->createMock( LanguageNameUtils::class );
+               $mockLangNameUtils->method( 'isValidBuiltInCode' )->will( $this->returnCallback(
+                       function ( $code ) {
+                               // Copy-paste, but it's only one line
+                               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+                       }
+               ) );
+               $mockLangNameUtils->method( 'isSupportedLanguage' )->will( $this->returnCallback(
+                       function ( $code ) {
+                               return in_array( $code, [
+                                       'ar',
+                                       'arz',
+                                       'ba',
+                                       'de',
+                                       'en',
+                                       'ksh',
+                                       'ru',
+                               ] );
+                       }
+               ) );
+               $mockLangNameUtils->method( 'getMessagesFileName' )->will( $this->returnCallback(
+                       function ( $code ) {
+                               global $IP;
+                               $code = str_replace( '-', '_', ucfirst( $code ) );
+                               return "$IP/languages/messages/Messages$code.php";
+                       }
+               ) );
+               $mockLangNameUtils->expects( $this->never() )->method( $this->anythingBut(
+                       'isValidBuiltInCode', 'isSupportedLanguage', 'getMessagesFileName'
+               ) );
+
+               $lc = $this->getMockBuilder( LocalisationCache::class )
+                       ->setConstructorArgs( [
+                               new ServiceOptions( LocalisationCache::$constructorOptions, [
+                                       'forceRecache' => false,
+                                       'manualRecache' => false,
+                                       'ExtensionMessagesFiles' => [],
+                                       'MessagesDirs' => [],
+                               ] ),
+                               new LCStoreDB( [] ),
+                               new NullLogger,
+                               [],
+                               $mockLangNameUtils
+                       ] )
                        ->setMethods( [ 'getMessagesDirs' ] )
                        ->getMock();
                $lc->expects( $this->any() )->method( 'getMessagesDirs' )
@@ -31,7 +79,7 @@ class LocalisationCacheTest extends MediaWikiTestCase {
                return $lc;
        }
 
-       public function testPuralRulesFallback() {
+       public function testPluralRulesFallback() {
                $cache = $this->getMockLocalisationCache();
 
                $this->assertEquals(
index 43e7075..f037a8c 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use Psr\Log\NullLogger;
 use Wikimedia\Rdbms\TransactionProfiler;
 use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Database;
@@ -43,19 +44,31 @@ class DatabaseTestHelper extends Database {
        protected $unionSupportsOrderAndLimit = true;
 
        public function __construct( $testName, array $opts = [] ) {
+               parent::__construct( $opts + [
+                       'host' => null,
+                       'user' => null,
+                       'password' => null,
+                       'dbname' => null,
+                       'schema' => null,
+                       'tablePrefix' => '',
+                       'flags' => 0,
+                       'cliMode' => $opts['cliMode'] ?? true,
+                       'agent' => '',
+                       'srvCache' => new HashBagOStuff(),
+                       'profiler' => null,
+                       'trxProfiler' => new TransactionProfiler(),
+                       'connLogger' => new NullLogger(),
+                       'queryLogger' => new NullLogger(),
+                       'errorLogger' => function ( Exception $e ) {
+                               wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
+                       },
+                       'deprecationLogger' => function ( $msg ) {
+                               wfWarn( $msg );
+                       }
+               ] );
+
                $this->testName = $testName;
 
-               $this->profiler = null;
-               $this->trxProfiler = new TransactionProfiler();
-               $this->cliMode = $opts['cliMode'] ?? true;
-               $this->connLogger = new \Psr\Log\NullLogger();
-               $this->queryLogger = new \Psr\Log\NullLogger();
-               $this->errorLogger = function ( Exception $e ) {
-                       wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
-               };
-               $this->deprecationLogger = function ( $msg ) {
-                       wfWarn( $msg );
-               };
                $this->currentDomain = DatabaseDomain::newUnspecified();
                $this->open( 'localhost', 'testuser', 'password', 'testdb', null, '' );
        }
index b377c63..7e5ff84 100644 (file)
@@ -373,4 +373,27 @@ class DeferredUpdatesTest extends MediaWikiTestCase {
                DeferredUpdates::tryOpportunisticExecute( 'run' );
                $this->assertEquals( [ 'oti', 1, 2 ], $calls );
        }
+
+       /**
+        * @covers DeferredUpdates::attemptUpdate
+        */
+       public function testCallbackUpdateRounds() {
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
+               $fname = __METHOD__;
+               $called = false;
+               DeferredUpdates::attemptUpdate(
+                       new MWCallableUpdate(
+                               function () use ( $lbFactory, $fname, &$called ) {
+                                       $lbFactory->flushReplicaSnapshots( $fname );
+                                       $lbFactory->commitMasterChanges( $fname );
+                                       $called = true;
+                               },
+                               $fname
+                       ),
+                       $lbFactory
+               );
+
+               $this->assertTrue( $called, "Callback ran" );
+       }
 }
diff --git a/tests/phpunit/includes/filebackend/lockmanager/LockManagerGroupIntegrationTest.php b/tests/phpunit/includes/filebackend/lockmanager/LockManagerGroupIntegrationTest.php
new file mode 100644 (file)
index 0000000..0615e95
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\Rdbms\LBFactory;
+
+/**
+ * Most of the file is covered by the unit test and/or FileBackendTest. Here we fill in the missing
+ * bits that don't work with unit tests yet.
+ *
+ * @covers LockManagerGroup
+ */
+class LockManagerGroupIntegrationTest extends MediaWikiIntegrationTestCase {
+       public function testWgLockManagers() {
+               $this->setMwGlobals( 'wgLockManagers',
+                       [ [ 'name' => 'a', 'class' => 'b' ], [ 'name' => 'c', 'class' => 'd' ] ] );
+               LockManagerGroup::destroySingletons();
+
+               $lmg = LockManagerGroup::singleton();
+               $domain = WikiMap::getCurrentWikiDbDomain()->getId();
+
+               $this->assertSame(
+                       [ 'class' => 'b', 'name' => 'a', 'domain' => $domain ],
+                       $lmg->config( 'a' ) );
+               $this->assertSame(
+                       [ 'class' => 'd', 'name' => 'c', 'domain' => $domain ],
+                       $lmg->config( 'c' ) );
+       }
+
+       public function testSingletonFalse() {
+               $this->setMwGlobals( 'wgLockManagers', [ [ 'name' => 'a', 'class' => 'b' ] ] );
+               LockManagerGroup::destroySingletons();
+
+               $this->assertSame(
+                       WikiMap::getCurrentWikiDbDomain()->getId(),
+                       LockManagerGroup::singleton( false )->config( 'a' )['domain']
+               );
+       }
+
+       public function testSingletonNull() {
+               $this->setMwGlobals( 'wgLockManagers', [ [ 'name' => 'a', 'class' => 'b' ] ] );
+               LockManagerGroup::destroySingletons();
+
+               $this->assertSame(
+                       null,
+                       LockManagerGroup::singleton( null )->config( 'a' )['domain']
+               );
+       }
+
+       public function testDestroySingletons() {
+               $instance = LockManagerGroup::singleton();
+               $this->assertSame( $instance, LockManagerGroup::singleton() );
+               LockManagerGroup::destroySingletons();
+               $this->assertNotSame( $instance, LockManagerGroup::singleton() );
+       }
+
+       public function testDestroySingletonsNamedDomain() {
+               $instance = LockManagerGroup::singleton( 'domain' );
+               $this->assertSame( $instance, LockManagerGroup::singleton( 'domain' ) );
+               LockManagerGroup::destroySingletons();
+               $this->assertNotSame( $instance, LockManagerGroup::singleton( 'domain' ) );
+       }
+
+       public function testGetDBLockManager() {
+               $this->markTestSkipped( 'DBLockManager case in LockManagerGroup::get appears to be ' .
+                       'broken, tries to instantiate an abstract class' );
+
+               $mockLB = $this->createMock( ILoadBalancer::class );
+               $mockLB->expects( $this->never() )
+                       ->method( $this->anythingBut( '__destruct', 'getLazyConnectionRef' ) );
+               $mockLB->expects( $this->once() )->method( 'getLazyConnectionRef' )
+                       ->with( DB_MASTER, [], 'domain', $mockLB::CONN_TRX_AUTOCOMMIT )
+                       ->willReturn( 'bogus value' );
+
+               $mockLBFactory = $this->createMock( LBFactory::class );
+               $mockLBFactory->expects( $this->never() )
+                       ->method( $this->anythingBut( '__destruct', 'getMainLB' ) );
+               $mockLBFactory->expects( $this->once() )->method( 'getMainLB' )->with( 'domain' )
+                       ->willReturn( $mockLB );
+
+               $lmg = new LockManagerGroup( 'domain',
+                       [ [ 'name' => 'a', 'class' => DBLockManager::class ] ], $mockLBFactory );
+               $this->assertSame( [], $lmg->get( 'a' ) );
+       }
+}
index 4bb9d5a..839272f 100644 (file)
@@ -39,13 +39,13 @@ class LogFormatterTest extends MediaWikiLangTestCase {
                global $wgExtensionMessagesFiles;
                self::$oldExtMsgFiles = $wgExtensionMessagesFiles;
                $wgExtensionMessagesFiles['LogTests'] = __DIR__ . '/LogTests.i18n.php';
-               Language::getLocalisationCache()->recache( 'en' );
+               Language::clearCaches();
        }
 
        public static function tearDownAfterClass() {
                global $wgExtensionMessagesFiles;
                $wgExtensionMessagesFiles = self::$oldExtMsgFiles;
-               Language::getLocalisationCache()->recache( 'en' );
+               Language::clearCaches();
 
                parent::tearDownAfterClass();
        }
index cbafbe9..30973c8 100644 (file)
@@ -1434,7 +1434,13 @@ more stuff
                                                        . " nonumy eirmod tempor invidunt ut labore et dolore magna "
                                                        . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
                                                        . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
-                                                       . "no sea  takimata sanctus est Lorem ipsum dolor sit amet.'",
+                                                       . "no sea  takimata sanctus est Lorem ipsum dolor sit amet. "
+                                                       . " this here is some more filler content added to try and "
+                                                       . "reach the maximum automatic summary length so that this is"
+                                                       . " truncated ipot sodit colrad ut ad olve amit basul dat"
+                                                       . "Dorbet romt crobit trop bri. DannyS712 put me here lor pe"
+                                                       . " ode quob zot bozro see also T22281 for background pol sup"
+                                                       . "Lorem ipsum dolor sit amet'",
                                                null
                                        ],
                                ],
index 651c871..4175ead 100644 (file)
@@ -234,6 +234,7 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
                $po = new ParserOptions( $frank );
 
                yield 'current' => [ $text, $po, 0, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
+               yield 'current' => [ $text, $po, null, 'user:;id:;time:' ];
                yield 'current with ID' => [ $text, $po, 200, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
 
                $text = '* user:{{REVISIONUSER}};id:{{REVISIONID}};time:{{REVISIONTIMESTAMP}};';
index 90f6ad9..510a2f2 100644 (file)
@@ -24,7 +24,6 @@ class SpecialPreferencesTest extends MediaWikiTestCase {
        public function testT43337() {
                // Set a low limit
                $this->setMwGlobals( 'wgMaxSigChars', 2 );
-
                $user = $this->createMock( User::class );
                $user->expects( $this->any() )
                        ->method( 'isAnon' )
@@ -47,6 +46,10 @@ class SpecialPreferencesTest extends MediaWikiTestCase {
                $user->method( 'getOptions' )
                        ->willReturn( [] );
 
+               // isAnyAllowed used to return null from the mock,
+               // thus revoke it's permissions.
+               $this->overrideUserPermissions( $user, [] );
+
                # Forge a request to call the special page
                $context = new RequestContext();
                $context->setRequest( new FauxRequest() );
index 028c438..7f97a16 100644 (file)
@@ -54,14 +54,12 @@ class NamespaceInfoTest extends MediaWikiTestCase {
                'ExtraNamespaces' => [],
                'ExtraSignatureNamespaces' => [],
                'NamespaceContentModels' => [],
-               'NamespaceProtection' => [],
                'NamespacesWithSubpages' => [
                        NS_TALK => true,
                        NS_USER => true,
                        NS_USER_TALK => true,
                ],
                'NonincludableNamespaces' => [],
-               'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
        ];
 
        private function newObj( array $options = [] ) : NamespaceInfo {
@@ -1245,53 +1243,17 @@ class NamespaceInfoTest extends MediaWikiTestCase {
         */
 
        /**
-        * This mock user can only have isAllowed() called on it.
-        *
-        * @param array $groups Groups for the mock user to have
-        * @return User
-        */
-       private function getMockUser( array $groups = [] ) : User {
-               $groups[] = '*';
-
-               $mock = $this->createMock( User::class );
-               $mock->method( 'isAllowed' )->will( $this->returnCallback(
-                       function ( $action ) use ( $groups ) {
-                               global $wgGroupPermissions, $wgRevokePermissions;
-                               if ( $action == '' ) {
-                                       return true;
-                               }
-                               foreach ( $wgRevokePermissions as $group => $rights ) {
-                                       if ( !in_array( $group, $groups ) ) {
-                                               continue;
-                                       }
-                                       if ( isset( $rights[$action] ) && $rights[$action] ) {
-                                               return false;
-                                       }
-                               }
-                               foreach ( $wgGroupPermissions as $group => $rights ) {
-                                       if ( !in_array( $group, $groups ) ) {
-                                               continue;
-                                       }
-                                       if ( isset( $rights[$action] ) && $rights[$action] ) {
-                                               return true;
-                                       }
-                               }
-                               return false;
-                       }
-               ) );
-               $mock->expects( $this->never() )->method( $this->anythingBut( 'isAllowed' ) );
-               return $mock;
-       }
-
-       /**
+        * TODO: This is superceeded by PermissionManagerTest::testGetNamespaceRestrictionLevels
+        * Remove when deprecated method is removed.
         * @dataProvider provideGetRestrictionLevels
-        * @covers NamespaceInfo::getRestrictionLevels
+        * @covers       NamespaceInfo::getRestrictionLevels
         *
         * @param array $expected
         * @param int $ns
-        * @param User|null $user
+        * @param array|null $groups
+        * @throws MWException
         */
-       public function testGetRestrictionLevels( array $expected, $ns, User $user = null ) {
+       public function testGetRestrictionLevels( array $expected, $ns, array $groups = null ) {
                $this->setMwGlobals( [
                        'wgGroupPermissions' => [
                                '*' => [ 'edit' => true ],
@@ -1305,14 +1267,17 @@ class NamespaceInfoTest extends MediaWikiTestCase {
                        'wgRevokePermissions' => [
                                'noeditsemiprotected' => [ 'editsemiprotected' => true ],
                        ],
-               ] );
-               $obj = $this->newObj( [
-                       'NamespaceProtection' => [
+                       'wgNamespaceProtection' => [
                                NS_MAIN => 'autoconfirmed',
                                NS_USER => 'sysop',
                                101 => [ 'editsemiprotected', 'privileged' ],
                        ],
+                       'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
+                       'wgAutopromote' => []
                ] );
+               $this->resetServices();
+               $obj = $this->newObj();
+               $user = is_null( $groups ) ? null : $this->getTestUser( $groups )->getUser();
                $this->assertSame( $expected, $obj->getRestrictionLevels( $ns, $user ) );
        }
 
@@ -1322,26 +1287,26 @@ class NamespaceInfoTest extends MediaWikiTestCase {
                        'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
                        'Restricted to sysop' => [ [ '' ], NS_USER ],
                        'Restricted to someone in two groups' => [ [ '', 'sysop' ], 101 ],
-                       'No special permissions' => [ [ '' ], NS_TALK, $this->getMockUser() ],
+                       'No special permissions' => [ [ '' ], NS_TALK, [] ],
                        'autoconfirmed' => [
                                [ '', 'autoconfirmed' ],
                                NS_TALK,
-                               $this->getMockUser( [ 'autoconfirmed' ] )
+                               [ 'autoconfirmed' ]
                        ],
                        'autoconfirmed revoked' => [
                                [ '' ],
                                NS_TALK,
-                               $this->getMockUser( [ 'autoconfirmed', 'noeditsemiprotected' ] )
+                               [ 'autoconfirmed', 'noeditsemiprotected' ]
                        ],
                        'sysop' => [
                                [ '', 'autoconfirmed', 'sysop' ],
                                NS_TALK,
-                               $this->getMockUser( [ 'sysop' ] )
+                               [ 'sysop' ]
                        ],
                        'sysop with autoconfirmed revoked (a bit silly)' => [
                                [ '', 'sysop' ],
                                NS_TALK,
-                               $this->getMockUser( [ 'sysop', 'noeditsemiprotected' ] )
+                               [ 'sysop', 'noeditsemiprotected' ]
                        ],
                ];
        }
index 2f6fa39..6f618a2 100644 (file)
@@ -3,6 +3,24 @@
 use Wikimedia\TestingAccessWrapper;
 
 class LanguageTest extends LanguageClassesTestCase {
+       use LanguageNameUtilsTestTrait;
+
+       /** @var array Copy of $wgHooks from before we unset LanguageGetTranslatedLanguageNames */
+       private $origHooks;
+
+       public function setUp() {
+               global $wgHooks;
+
+               parent::setUp();
+
+               // Don't allow installed hooks to run, except if a test restores them via origHooks (needed
+               // for testIsKnownLanguageTag_cldr)
+               $this->origHooks = $wgHooks;
+               $newHooks = $wgHooks;
+               unset( $newHooks['LanguageGetTranslatedLanguageNames'] );
+               $this->setMwGlobals( 'wgHooks', $newHooks );
+       }
+
        /**
         * @covers Language::convertDoubleWidth
         * @covers Language::normalizeForSearch
@@ -510,84 +528,6 @@ class LanguageTest extends LanguageClassesTestCase {
                );
        }
 
-       /**
-        * Test Language::isValidBuiltInCode()
-        * @dataProvider provideLanguageCodes
-        * @covers Language::isValidBuiltInCode
-        */
-       public function testBuiltInCodeValidation( $code, $expected, $message = '' ) {
-               $this->assertEquals( $expected,
-                       (bool)Language::isValidBuiltInCode( $code ),
-                       "validating code $code $message"
-               );
-       }
-
-       public static function provideLanguageCodes() {
-               return [
-                       [ 'fr', true, 'Two letters, minor case' ],
-                       [ 'EN', false, 'Two letters, upper case' ],
-                       [ 'tyv', true, 'Three letters' ],
-                       [ 'be-tarask', true, 'With dash' ],
-                       [ 'be-x-old', true, 'With extension (two dashes)' ],
-                       [ 'be_tarask', false, 'Reject underscores' ],
-               ];
-       }
-
-       /**
-        * Test Language::isKnownLanguageTag()
-        * @dataProvider provideKnownLanguageTags
-        * @covers Language::isKnownLanguageTag
-        */
-       public function testKnownLanguageTag( $code, $message = '' ) {
-               $this->assertTrue(
-                       (bool)Language::isKnownLanguageTag( $code ),
-                       "validating code $code - $message"
-               );
-       }
-
-       public static function provideKnownLanguageTags() {
-               return [
-                       [ 'fr', 'simple code' ],
-                       [ 'bat-smg', 'an MW legacy tag' ],
-                       [ 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ],
-               ];
-       }
-
-       /**
-        * @covers Language::isKnownLanguageTag
-        */
-       public function testKnownCldrLanguageTag() {
-               if ( !class_exists( 'LanguageNames' ) ) {
-                       $this->markTestSkipped( 'The LanguageNames class is not available. '
-                               . 'The CLDR extension is probably not installed.' );
-               }
-
-               $this->assertTrue(
-                       (bool)Language::isKnownLanguageTag( 'pal' ),
-                       'validating code "pal" an ancient language, which probably will '
-                               . 'not appear in Names.php, but appears in CLDR in English'
-               );
-       }
-
-       /**
-        * Negative tests for Language::isKnownLanguageTag()
-        * @dataProvider provideUnKnownLanguageTags
-        * @covers Language::isKnownLanguageTag
-        */
-       public function testUnknownLanguageTag( $code, $message = '' ) {
-               $this->assertFalse(
-                       (bool)Language::isKnownLanguageTag( $code ),
-                       "checking that code $code is invalid - $message"
-               );
-       }
-
-       public static function provideUnknownLanguageTags() {
-               return [
-                       [ 'mw', 'non-existent two-letter code' ],
-                       [ 'foo"<bar', 'very invalid language code' ],
-               ];
-       }
-
        /**
         * Test too short timestamp
         * @expectedException MWException
@@ -1812,12 +1752,6 @@ class LanguageTest extends LanguageClassesTestCase {
        public function testClearCaches() {
                $languageClass = TestingAccessWrapper::newFromClass( Language::class );
 
-               // Populate $dataCache
-               Language::getLocalisationCache()->getItem( 'zh', 'mainpage' );
-               $oldCacheObj = Language::$dataCache;
-               $this->assertNotCount( 0,
-                       TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
-
                // Populate $mLangObjCache
                $lang = Language::factory( 'en' );
                $this->assertNotCount( 0, Language::$mLangObjCache );
@@ -1830,36 +1764,11 @@ class LanguageTest extends LanguageClassesTestCase {
                $lang->getGrammarTransformations();
                $this->assertNotNull( $languageClass->grammarTransformations );
 
-               // Populate $languageNameCache
-               Language::fetchLanguageNames();
-               $this->assertNotNull( $languageClass->languageNameCache );
-
                Language::clearCaches();
 
-               $this->assertNotSame( $oldCacheObj, Language::$dataCache );
-               $this->assertCount( 0,
-                       TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
                $this->assertCount( 0, Language::$mLangObjCache );
                $this->assertCount( 0, $languageClass->fallbackLanguageCache );
                $this->assertNull( $languageClass->grammarTransformations );
-               $this->assertNull( $languageClass->languageNameCache );
-       }
-
-       /**
-        * @dataProvider provideIsSupportedLanguage
-        * @covers Language::isSupportedLanguage
-        */
-       public function testIsSupportedLanguage( $code, $expected, $comment ) {
-               $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment );
-       }
-
-       public static function provideIsSupportedLanguage() {
-               return [
-                       [ 'en', true, 'is supported language' ],
-                       [ 'fi', true, 'is supported language' ],
-                       [ 'bunny', false, 'is not supported language' ],
-                       [ 'FI', false, 'is not supported language, input should be in lower case' ],
-               ];
        }
 
        /**
@@ -1965,4 +1874,82 @@ class LanguageTest extends LanguageClassesTestCase {
                        [ 'èl', 'Ll' , 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
                ];
        }
+
+       // The following methods are for LanguageNameUtilsTestTrait
+
+       private function isSupportedLanguage( $code ) {
+               return Language::isSupportedLanguage( $code );
+       }
+
+       private function isValidCode( $code ) {
+               return Language::isValidCode( $code );
+       }
+
+       private function isValidBuiltInCode( $code ) {
+               return Language::isValidBuiltInCode( $code );
+       }
+
+       private function isKnownLanguageTag( $code ) {
+               return Language::isKnownLanguageTag( $code );
+       }
+
+       /**
+        * Call getLanguageName() and getLanguageNames() using the Language static methods.
+        *
+        * @param array $options To set globals for testing Language
+        * @param string $expected
+        * @param string $code
+        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+        */
+       private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
+               if ( $options ) {
+                       foreach ( $options as $key => $val ) {
+                               $this->setMwGlobals( "wg$key", $val );
+                       }
+                       $this->resetServices();
+               }
+               $this->assertSame( $expected,
+                       Language::fetchLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
+               $this->assertSame( $expected, Language::fetchLanguageName( $code, ...$otherArgs ) );
+       }
+
+       private function getLanguageNames( ...$args ) {
+               return Language::fetchLanguageNames( ...$args );
+       }
+
+       private function getLanguageName( ...$args ) {
+               return Language::fetchLanguageName( ...$args );
+       }
+
+       private static function getFileName( ...$args ) {
+               return Language::getFileName( ...$args );
+       }
+
+       private static function getMessagesFileName( $code ) {
+               return Language::getMessagesFileName( $code );
+       }
+
+       private static function getJsonMessagesFileName( $code ) {
+               return Language::getJsonMessagesFileName( $code );
+       }
+
+       /**
+        * @todo This really belongs in the cldr extension's tests.
+        *
+        * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
+        * @covers Language::isKnownLanguageTag
+        */
+       public function testIsKnownLanguageTag_cldr() {
+               if ( !class_exists( 'LanguageNames' ) ) {
+                       $this->markTestSkipped( 'The LanguageNames class is not available. '
+                               . 'The CLDR extension is probably not installed.' );
+               }
+
+               // We need to restore the extension's hook that we removed.
+               $this->setMwGlobals( 'wgHooks', $this->origHooks );
+
+               // "pal" is an ancient language, which probably will not appear in Names.php, but appears in
+               // CLDR in English
+               $this->assertTrue( Language::isKnownLanguageTag( 'pal' ) );
+       }
 }
diff --git a/tests/phpunit/unit/includes/BadFileLookupTest.php b/tests/phpunit/unit/includes/BadFileLookupTest.php
new file mode 100644 (file)
index 0000000..6ecfe37
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+
+use MediaWiki\BadFileLookup;
+
+/**
+ * @coversDefaultClass MediaWiki\BadFileLookup
+ */
+class BadFileLookupTest extends MediaWikiUnitTestCase {
+       /** Shared with GlobalWithDBTest */
+       const BLACKLIST = <<<WIKITEXT
+Comment line, no effect [[File:Good.jpg]]
+ * Indented list is also a comment [[File:Good.jpg]]
+* [[File:Bad.jpg]] except [[Nasty page]]
+*[[Image:Bad2.jpg]] also works
+* So does [[Bad3.jpg]]
+* [[User:Bad4.jpg]] works although it is silly
+* [[File:Redirect to good.jpg]] doesn't do anything if RepoGroup is working, because we only look at
+  the final name, but will work if RepoGroup returns null
+* List line with no link
+* [[Malformed title<>]] doesn't break anything, the line is ignored [[File:Good.jpg]]
+* [[File:Bad5.jpg]] before [[malformed title<>]] doesn't ignore the line
+WIKITEXT;
+
+       /** Shared with GlobalWithDBTest */
+       public static function badImageHook( $name, &$bad ) {
+               switch ( $name ) {
+               case 'Hook_bad.jpg':
+               case 'Redirect_to_hook_good.jpg':
+                       $bad = true;
+                       return false;
+
+               case 'Hook_good.jpg':
+               case 'Redirect_to_hook_bad.jpg':
+                       $bad = false;
+                       return false;
+               }
+
+               return true;
+       }
+
+       private function getMockRepoGroup() {
+               $mock = $this->createMock( RepoGroup::class );
+               $mock->expects( $this->once() )->method( 'findFile' )
+                       ->will( $this->returnCallback( function ( $name ) {
+                               $mockFile = $this->createMock( File::class );
+                               $mockFile->expects( $this->once() )->method( 'getTitle' )
+                                       ->will( $this->returnCallback( function () use ( $name ) {
+                                               switch ( $name ) {
+                                               case 'Redirect to bad.jpg':
+                                                       return new TitleValue( NS_FILE, 'Bad.jpg' );
+                                               case 'Redirect_to_good.jpg':
+                                                       return new TitleValue( NS_FILE, 'Good.jpg' );
+                                               case 'Redirect to hook bad.jpg':
+                                                       return new TitleValue( NS_FILE, 'Hook_bad.jpg' );
+                                               case 'Redirect to hook good.jpg':
+                                                       return new TitleValue( NS_FILE, 'Hook_good.jpg' );
+                                               default:
+                                                       return new TitleValue( NS_FILE, $name );
+                                               }
+                                       } ) );
+                               $mockFile->expects( $this->never() )->method( $this->anythingBut( 'getTitle' ) );
+                               return $mockFile;
+                       } ) );
+               $mock->expects( $this->never() )->method( $this->anythingBut( 'findFile' ) );
+
+               return $mock;
+       }
+
+       /**
+        * Just returns null for every findFile().
+        */
+       private function getMockRepoGroupNull() {
+               $mock = $this->createMock( RepoGroup::class );
+               $mock->expects( $this->once() )->method( 'findFile' )->willReturn( null );
+               $mock->expects( $this->never() )->method( $this->anythingBut( 'findFile' ) );
+
+               return $mock;
+       }
+
+       private function getMockTitleParser() {
+               $mock = $this->createMock( TitleParser::class );
+               $mock->method( 'parseTitle' )->will( $this->returnCallback( function ( $text ) {
+                       if ( strpos( $text, '<' ) !== false ) {
+                               throw $this->createMock( MalformedTitleException::class );
+                       }
+                       if ( strpos( $text, ':' ) === false ) {
+                               return new TitleValue( NS_MAIN, $text );
+                       }
+                       list( $ns, $text ) = explode( ':', $text );
+                       switch ( $ns ) {
+                       case 'Image':
+                       case 'File':
+                               $ns = NS_FILE;
+                               break;
+
+                       case 'User':
+                               $ns = NS_USER;
+                               break;
+                       }
+                       return new TitleValue( $ns, $text );
+               } ) );
+               $mock->expects( $this->never() )->method( $this->anythingBut( 'parseTitle' ) );
+
+               return $mock;
+       }
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->setTemporaryHook( 'BadImage', __CLASS__ . '::badImageHook' );
+       }
+
+       /**
+        * @dataProvider provideIsBadFile
+        * @covers ::__construct
+        * @covers ::isBadFile
+        */
+       public function testIsBadFile( $name, $title, $expected ) {
+               $bfl = new BadFileLookup(
+                       function () {
+                               return self::BLACKLIST;
+                       },
+                       new EmptyBagOStuff,
+                       $this->getMockRepoGroup(),
+                       $this->getMockTitleParser()
+               );
+
+               $this->assertSame( $expected, $bfl->isBadFile( $name, $title ) );
+       }
+
+       /**
+        * @dataProvider provideIsBadFile
+        * @covers ::__construct
+        * @covers ::isBadFile
+        */
+       public function testIsBadFile_nullRepoGroup( $name, $title, $expected ) {
+               $bfl = new BadFileLookup(
+                       function () {
+                               return self::BLACKLIST;
+                       },
+                       new EmptyBagOStuff,
+                       $this->getMockRepoGroupNull(),
+                       $this->getMockTitleParser()
+               );
+
+               // Hack -- these expectations are reversed if the repo group returns null. In that case 1)
+               // we don't honor redirects, and 2) we don't replace spaces by underscores (which makes the
+               // hook not see 'Hook bad.jpg').
+               if ( in_array( $name, [
+                       'Redirect to bad.jpg',
+                       'Redirect_to_good.jpg',
+                       'Hook bad.jpg',
+                       'Redirect to hook bad.jpg',
+               ] ) ) {
+                       $expected = !$expected;
+               }
+
+               $this->assertSame( $expected, $bfl->isBadFile( $name, $title ) );
+       }
+
+       /** Shared with GlobalWithDBTest */
+       public static function provideIsBadFile() {
+               return [
+                       'No context page' => [ 'Bad.jpg', null, true ],
+                       'Context page not whitelisted' =>
+                               [ 'Bad.jpg', new TitleValue( NS_MAIN, 'A page' ), true ],
+                       'Good image' => [ 'Good.jpg', null, false ],
+                       'Whitelisted context page' =>
+                               [ 'Bad.jpg', new TitleValue( NS_MAIN, 'Nasty page' ), false ],
+                       'Bad image with Image:' => [ 'Image:Bad.jpg', null, false ],
+                       'Bad image with File:' => [ 'File:Bad.jpg', null, false ],
+                       'Bad image with Image: in blacklist' => [ 'Bad2.jpg', null, true ],
+                       'Bad image without prefix in blacklist' => [ 'Bad3.jpg', null, true ],
+                       'Bad image with different namespace in blacklist' => [ 'Bad4.jpg', null, true ],
+                       'Redirect to bad image' => [ 'Redirect to bad.jpg', null, true ],
+                       'Redirect to good image' => [ 'Redirect_to_good.jpg', null, false ],
+                       'Hook says bad (with space)' => [ 'Hook bad.jpg', null, true ],
+                       'Hook says bad (with underscore)' => [ 'Hook_bad.jpg', null, true ],
+                       'Hook says good' => [ 'Hook good.jpg', null, false ],
+                       'Redirect to hook bad image' => [ 'Redirect to hook bad.jpg', null, true ],
+                       'Redirect to hook good image' => [ 'Redirect to hook good.jpg', null, false ],
+                       'Malformed title doesn\'t break the line' => [ 'Bad5.jpg', null, true ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php b/tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php
new file mode 100644 (file)
index 0000000..6fbd4a2
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
+
+class LanguageNameUtilsTest extends MediaWikiUnitTestCase {
+       /**
+        * @param array $optionsArray
+        */
+       private static function newObj( array $optionsArray = [] ) : LanguageNameUtils {
+               return new LanguageNameUtils( new ServiceOptions(
+                       LanguageNameUtils::$constructorOptions,
+                       $optionsArray,
+                       [
+                               'ExtraLanguageNames' => [],
+                               'LanguageCode' => 'en',
+                               'UsePigLatinVariant' => false,
+                       ]
+               ) );
+       }
+
+       use LanguageNameUtilsTestTrait;
+
+       private function isSupportedLanguage( $code ) {
+               return $this->newObj()->isSupportedLanguage( $code );
+       }
+
+       private function isValidCode( $code ) {
+               return $this->newObj()->isValidCode( $code );
+       }
+
+       private function isValidBuiltInCode( $code ) {
+               return $this->newObj()->isValidBuiltInCode( $code );
+       }
+
+       private function isKnownLanguageTag( $code ) {
+               return $this->newObj()->isKnownLanguageTag( $code );
+       }
+
+       private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
+               $this->assertSame( $expected, $this->newObj( $options )
+                       ->getLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
+               $this->assertSame( $expected,
+                       $this->newObj( $options )->getLanguageName( $code, ...$otherArgs ) );
+       }
+
+       private function getLanguageNames( ...$args ) {
+               return $this->newObj()->getLanguageNames( ...$args );
+       }
+
+       private function getLanguageName( ...$args ) {
+               return $this->newObj()->getLanguageName( ...$args );
+       }
+
+       private static function getFileName( ...$args ) {
+               return self::newObj()->getFileName( ...$args );
+       }
+
+       private static function getMessagesFileName( $code ) {
+               return self::newObj()->getMessagesFileName( $code );
+       }
+
+       private static function getJsonMessagesFileName( $code ) {
+               return self::newObj()->getJsonMessagesFileName( $code );
+       }
+}
diff --git a/tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php b/tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php
new file mode 100644 (file)
index 0000000..bd777e9
--- /dev/null
@@ -0,0 +1,555 @@
+<?php
+
+use MediaWiki\Languages\LanguageNameUtils;
+
+const AUTONYMS = LanguageNameUtils::AUTONYMS;
+const ALL = LanguageNameUtils::ALL;
+const DEFINED = LanguageNameUtils::DEFINED;
+const SUPPORTED = LanguageNameUtils::SUPPORTED;
+
+/**
+ * For code shared between LanguageNameUtilsTest and LanguageTest.
+ */
+trait LanguageNameUtilsTestTrait {
+       abstract protected function isSupportedLanguage( $code );
+
+       /**
+        * @dataProvider provideIsSupportedLanguage
+        * @covers MediaWiki\Languages\LanguageNameUtils::__construct
+        * @covers MediaWiki\Languages\LanguageNameUtils::isSupportedLanguage
+        * @covers Language::isSupportedLanguage
+        */
+       public function testIsSupportedLanguage( $code, $expected ) {
+               $this->assertSame( $expected, $this->isSupportedLanguage( $code ) );
+       }
+
+       public static function provideIsSupportedLanguage() {
+               return [
+                       'en' => [ 'en', true ],
+                       'fi' => [ 'fi', true ],
+                       'bunny' => [ 'bunny', false ],
+                       'qqq' => [ 'qqq', false ],
+                       'uppercase is not considered supported' => [ 'FI', false ],
+               ];
+       }
+
+       abstract protected function isValidCode( $code );
+
+       /**
+        * We don't test that the result is cached, because that should only be noticeable if the
+        * configuration changes in between calls, and 1) that should never happen in normal operation,
+        * 2) if you do it you deserve whatever you get, and 3) once the static Language method is
+        * dropped and the invalid title regex is moved to something injected instead of a static call,
+        * the cache will be undetectable.
+        *
+        * @todo Should we test changes to $wgLegalTitleChars here? Does anybody actually change that?
+        * Is it possible to change it usefully without breaking everything?
+        *
+        * @dataProvider provideIsValidCode
+        * @covers MediaWiki\Languages\LanguageNameUtils::isValidCode
+        * @covers Language::isValidCode
+        *
+        * @param string $code
+        * @param bool $expected
+        */
+       public function testIsValidCode( $code, $expected ) {
+               $this->assertSame( $expected, $this->isValidCode( $code ) );
+       }
+
+       public static function provideIsValidCode() {
+               $ret = [
+                       'en' => [ 'en', true ],
+                       'en-GB' => [ 'en-GB', true ],
+                       'Funny chars' => [ "%!$()*,-.;=?@^_`~\x80\xA2\xFF+", true ],
+                       'Percent escape not allowed' => [ 'a%aF', false ],
+                       'Percent with only one following char is okay' => [ '%a', true ],
+                       'Percent with non-hex following chars is okay' => [ '%AG', true ],
+                       'Named char reference "a"' => [ 'a&a', false ],
+                       'Named char reference "A"' => [ 'a&A', false ],
+                       'Named char reference "0"' => [ 'a&0', false ],
+                       'Named char reference non-ASCII' => [ "a&\x92", false ],
+                       'Numeric char reference' => [ "a&#0", false ],
+                       'Hex char reference 0' => [ "a&#x0", false ],
+                       'Hex char reference A' => [ "a&#xA", false ],
+                       'Lone ampersand is valid for title but not lang code' => [ '&', false ],
+                       'Ampersand followed by just # is valid for title but not lang code' => [ '&#', false ],
+                       'Ampersand followed by # and non-x/digit is valid for title but not lang code' =>
+                               [ '&#a', false ],
+               ];
+               $disallowedChars = ":/\\\000&<>'\"";
+               foreach ( str_split( $disallowedChars ) as $char ) {
+                       $ret["Disallowed character $char"] = [ "a{$char}a", false ];
+               }
+               return $ret;
+       }
+
+       abstract protected function isValidBuiltInCode( $code );
+
+       /**
+        * @dataProvider provideIsValidBuiltInCode
+        * @covers MediaWiki\Languages\LanguageNameUtils::isValidBuiltInCode
+        * @covers Language::isValidBuiltInCode
+        *
+        * @param string $code
+        * @param bool $expected
+        */
+       public function testIsValidBuiltInCode( $code, $expected ) {
+               $this->assertSame( $expected, $this->isValidBuiltInCode( $code ) );
+       }
+
+       public static function provideIsValidBuiltInCode() {
+               return [
+                       'Two letters, lowercase' => [ 'fr', true ],
+                       'Two letters, uppercase' => [ 'EN', false ],
+                       'Three letters' => [ 'tyv', true ],
+                       'With dash' => [ 'be-tarask', true ],
+                       'With extension (two dashes)' => [ 'be-x-old', true ],
+                       'Reject underscores' => [ 'be_tarask', false ],
+                       'One letter' => [ 'a', false ],
+                       'Only digits' => [ '00', true ],
+                       'Only dashes' => [ '--', true ],
+                       'Unreasonably long' => [ str_repeat( 'x', 100 ), true ],
+                       'qqq' => [ 'qqq', true ],
+               ];
+       }
+
+       abstract protected function isKnownLanguageTag( $code );
+
+       /**
+        * @dataProvider provideIsKnownLanguageTag
+        * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
+        * @covers Language::isKnownLanguageTag
+        *
+        * @param string $code
+        * @param bool $expected
+        */
+       public function testIsKnownLanguageTag( $code, $expected ) {
+               $this->assertSame( $expected, $this->isKnownLanguageTag( $code ) );
+       }
+
+       public static function provideIsKnownLanguageTag() {
+               $invalidBuiltInCodes = array_filter( static::provideIsValidBuiltInCode(),
+                       function ( $arr ) {
+                               // If isValidBuiltInCode() returns false, we want to also, but if it returns true,
+                               // we could still return false from isKnownLanguageTag(), so skip those.
+                               return !$arr[1];
+                       }
+               );
+               return array_merge( $invalidBuiltInCodes, [
+                       'Simple code' => [ 'fr', true ],
+                       'An MW legacy tag' => [ 'bat-smg', true ],
+                       'An internal standard MW name, for which a legacy tag is used externally' =>
+                               [ 'sgs', true ],
+                       'Non-existent two-letter code' => [ 'mw', false ],
+                       'Very invalid language code' => [ 'foo"<bar', false ],
+               ] );
+       }
+
+       abstract protected function assertGetLanguageNames(
+               array $options, $expected, $code, ...$otherArgs
+       );
+
+       abstract protected function getLanguageNames( ...$args );
+
+       abstract protected function getLanguageName( ...$args );
+
+       /**
+        * @dataProvider provideGetLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        *
+        * @param string $expected
+        * @param string $code
+        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+        */
+       public function testGetLanguageNames( $expected, $code, ...$otherArgs ) {
+               $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
+       }
+
+       public static function provideGetLanguageNames() {
+               // @todo There are probably lots of interesting tests to add here.
+               return [
+                       'Simple code' => [ 'Deutsch', 'de' ],
+                       'Simple code in a different language (doesn\'t work without hook)' =>
+                               [ 'Deutsch', 'de', 'fr' ],
+                       'Invalid code' => [ '', '&' ],
+                       'Pig Latin not enabled' => [ '', 'en-x-piglatin', AUTONYMS, ALL ],
+                       'qqq doesn\'t have a name' => [ '', 'qqq', AUTONYMS, ALL ],
+                       'An MW legacy tag is recognized' => [ 'žemaitėška', 'bat-smg' ],
+                       // @todo Is the next test's result desired?
+                       'An MW legacy tag is not supported' => [ '', 'bat-smg', AUTONYMS, SUPPORTED ],
+                       'An internal standard name, for which a legacy tag is used externally, is supported' =>
+                               [ 'žemaitėška', 'sgs', AUTONYMS, SUPPORTED ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetLanguageNames_withHook
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        *
+        * @param string $expected Expected return value of getLanguageName()
+        * @param string $code
+        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+        */
+       public function testGetLanguageNames_withHook( $expected, $code, ...$otherArgs ) {
+               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+                       function ( &$names, $inLanguage ) {
+                               switch ( $inLanguage ) {
+                               case 'de':
+                                       $names = [
+                                               'de' => 'Deutsch',
+                                               'en' => 'Englisch',
+                                               'fr' => 'Französisch',
+                                       ];
+                                       break;
+
+                               case 'en':
+                                       $names = [
+                                               'de' => 'German',
+                                               'en' => 'English',
+                                               'fr' => 'French',
+                                               'sqsqsqsq' => '!!?!',
+                                               'bat-smg' => 'Samogitian',
+                                       ];
+                                       break;
+
+                               case 'fr':
+                                       $names = [
+                                               'de' => 'allemand',
+                                               'en' => 'anglais',
+                                               // Deliberate mistake (no cedilla)
+                                               'fr' => 'francais',
+                                       ];
+                                       break;
+                               }
+                       }
+               );
+
+               // Really we could dispense with assertGetLanguageNames() and just call
+               // testGetLanguageNames() here, but it looks weird to call a test method from another test
+               // method.
+               $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
+       }
+
+       public static function provideGetLanguageNames_withHook() {
+               return [
+                       'Simple code in a different language' => [ 'allemand', 'de', 'fr' ],
+                       'Invalid inLanguage defaults to English' => [ 'German', 'de', '&' ],
+                       'If inLanguage not provided, default to autonym' => [ 'Deutsch', 'de' ],
+                       'Hooks ignored for explicitly-requested autonym' => [ 'français', 'fr', 'fr' ],
+                       'Hooks don\'t make a language supported' => [ '', 'bat-smg', 'en', SUPPORTED ],
+                       'Hooks don\'t make a language defined' => [ '', 'sqsqsqsq', 'en', DEFINED ],
+                       'Hooks do make a language name returned with ALL' => [ '!!?!', 'sqsqsqsq', 'en', ALL ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetLanguageNames_ExtraLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        *
+        * @param string $expected Expected return value of getLanguageName()
+        * @param string $code
+        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+        */
+       public function testGetLanguageNames_ExtraLanguageNames( $expected, $code, ...$otherArgs ) {
+               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+                       function ( &$names ) {
+                               $names['de'] = 'die deutsche Sprache';
+                       }
+               );
+               $this->assertGetLanguageNames(
+                       [ 'ExtraLanguageNames' => [ 'de' => 'deutsche Sprache', 'sqsqsqsq' => '!!?!' ] ],
+                       $expected, $code, ...$otherArgs
+               );
+       }
+
+       public static function provideGetLanguageNames_ExtraLanguageNames() {
+               return [
+                       'Simple extra language name' => [ '!!?!', 'sqsqsqsq' ],
+                       'Extra language is defined' => [ '!!?!', 'sqsqsqsq', AUTONYMS, DEFINED ],
+                       'Extra language is not supported' => [ '', 'sqsqsqsq', AUTONYMS, SUPPORTED ],
+                       'Extra language overrides default' => [ 'deutsche Sprache', 'de' ],
+                       'Extra language overrides hook for explicitly requested autonym' =>
+                               [ 'deutsche Sprache', 'de', 'de' ],
+                       'Hook overrides extra language for non-autonym' =>
+                               [ 'die deutsche Sprache', 'de', 'fr' ],
+               ];
+       }
+
+       /**
+        * Test that getLanguageNames() defaults to DEFINED, and getLanguageName() defaults to ALL.
+        *
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        */
+       public function testGetLanguageNames_parameterDefault() {
+               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+                       function ( &$names ) {
+                               $names = [ 'sqsqsqsq' => '!!?!' ];
+                       }
+               );
+
+               // We use 'en' here because the hook is not run if we're requesting autonyms, although in
+               // this case (language that isn't defined by MediaWiki itself) that behavior seems wrong.
+               $this->assertArrayNotHasKey( 'sqsqsqsq', $this->getLanguageNames(), 'en' );
+
+               $this->assertSame( '!!?!', $this->getLanguageName( 'sqsqsqsq', 'en' ) );
+       }
+
+       /**
+        * @dataProvider provideGetLanguageNames_sorted
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers Language::fetchLanguageNames
+        *
+        * @param mixed ...$args To pass to method
+        */
+       public function testGetLanguageNames_sorted( ...$args ) {
+               $names = $this->getLanguageNames( ...$args );
+               $sortedNames = $names;
+               ksort( $sortedNames );
+               $this->assertSame( $sortedNames, $names );
+       }
+
+       public static function provideGetLanguageNames_sorted() {
+               return [
+                       [],
+                       [ AUTONYMS ],
+                       [ AUTONYMS, 'mw' ],
+                       [ AUTONYMS, ALL ],
+                       [ AUTONYMS, SUPPORTED ],
+                       [ 'he', 'mw' ],
+                       [ 'he', ALL ],
+                       [ 'he', SUPPORTED ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers Language::fetchLanguageNames
+        */
+       public function testGetLanguageNames_hookNotCalledForAutonyms() {
+               $count = 0;
+               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+                       function () use ( &$count ) {
+                               $count++;
+                       }
+               );
+
+               $this->getLanguageNames();
+               $this->assertSame( 0, $count, 'Hook must not be called for autonyms' );
+
+               // We test elsewhere that the hook works, but the following verifies that our test is
+               // working and $count isn't being incremented above only because we're checking autonyms.
+               $this->getLanguageNames( 'fr' );
+               $this->assertSame( 1, $count, 'Hook must be called for non-autonyms' );
+       }
+
+       /**
+        * @dataProvider provideGetLanguageNames_pigLatin
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        *
+        * @param string $expected
+        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+        */
+       public function testGetLanguageNames_pigLatin( $expected, ...$otherArgs ) {
+               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+                       function ( &$names, $inLanguage ) {
+                               switch ( $inLanguage ) {
+                               case 'fr':
+                                       $names = [ 'en-x-piglatin' => 'latin de cochons' ];
+                                       break;
+
+                               case 'en-x-piglatin':
+                                       // Deliberately lowercase
+                                       $names = [ 'en-x-piglatin' => 'igpay atinlay' ];
+                                       break;
+                               }
+                       }
+               );
+
+               $this->assertGetLanguageNames(
+                       [ 'UsePigLatinVariant' => true ], $expected, 'en-x-piglatin', ...$otherArgs );
+       }
+
+       public static function provideGetLanguageNames_pigLatin() {
+               return [
+                       'Simple test' => [ 'Igpay Atinlay' ],
+                       'Not supported' => [ '', AUTONYMS, SUPPORTED ],
+                       'Foreign language' => [ 'latin de cochons', 'fr' ],
+                       'Hook doesn\'t override explicit autonym' =>
+                               [ 'Igpay Atinlay', 'en-x-piglatin', 'en-x-piglatin' ],
+               ];
+       }
+
+       /**
+        * Just for the sake of completeness, test that ExtraLanguageNames will not override the name
+        * for pig Latin. Nobody actually cares about this and if anything current behavior is probably
+        * wrong, but once we're testing the whole file we may as well be comprehensive.
+        *
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+        * @covers Language::fetchLanguageNames
+        * @covers Language::fetchLanguageName
+        */
+       public function testGetLanguageNames_pigLatinAndExtraLanguageNames() {
+               $this->assertGetLanguageNames(
+                       [
+                               'UsePigLatinVariant' => true,
+                               'ExtraLanguageNames' => [ 'en-x-piglatin' => 'igpay atinlay' ]
+                       ],
+                       'Igpay Atinlay',
+                       'en-x-piglatin'
+               );
+       }
+
+       abstract protected static function getFileName( ...$args );
+
+       /**
+        * @dataProvider provideGetFileName
+        * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
+        * @covers Language::getFileName
+        *
+        * @param string $expected
+        * @param mixed ...$args To pass to method
+        */
+       public function testGetFileName( $expected, ...$args ) {
+               $this->assertSame( $expected, $this->getFileName( ...$args ) );
+       }
+
+       public static function provideGetFileName() {
+               return [
+                       'Simple case' => [ 'MessagesXx.php', 'Messages', 'xx' ],
+                       'With extension' => [ 'MessagesXx.ext', 'Messages', 'xx', '.ext' ],
+                       'Replacing dashes' => [ '!__?', '!', '--', '?' ],
+                       'Empty prefix and extension' => [ 'Xx', '', 'xx', '' ],
+                       'Uppercase only first letter' => [ 'Messages_a.php', 'Messages', '-a' ],
+               ];
+       }
+
+       abstract protected function getMessagesFileName( $code );
+
+       /**
+        * @dataProvider provideGetMessagesFileName
+        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+        * @covers Language::getMessagesFileName
+        *
+        * @param string $code
+        * @param string $expected
+        */
+       public function testGetMessagesFileName( $code, $expected ) {
+               $this->assertSame( $expected, $this->getMessagesFileName( $code ) );
+       }
+
+       public static function provideGetMessagesFileName() {
+               global $IP;
+               return [
+                       'Simple case' => [ 'en', "$IP/languages/messages/MessagesEn.php" ],
+                       'Replacing dashes' => [ '--', "$IP/languages/messages/Messages__.php" ],
+                       'Uppercase only first letter' => [ '-a', "$IP/languages/messages/Messages_a.php" ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+        * @covers Language::getMessagesFileName
+        */
+       public function testGetMessagesFileName_withHook() {
+               $called = 0;
+
+               $this->setTemporaryHook( 'Language::getMessagesFileName',
+                       function ( $code, &$file ) use ( &$called ) {
+                               global $IP;
+
+                               $called++;
+
+                               $this->assertSame( 'ab-cd', $code );
+                               $this->assertSame( "$IP/languages/messages/MessagesAb_cd.php", $file );
+                               $file = 'bye-bye';
+                       }
+               );
+
+               $this->assertSame( 'bye-bye', $this->getMessagesFileName( 'ab-cd' ) );
+               $this->assertSame( 1, $called );
+       }
+
+       abstract protected function getJsonMessagesFileName( $code );
+
+       /**
+        * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
+        * @covers Language::getJsonMessagesFileName
+        */
+       public function testGetJsonMessagesFileName() {
+               global $IP;
+
+               // Not so much to test here, one test seems to be enough
+               $expected = "$IP/languages/i18n/en--123.json";
+               $this->assertSame( $expected, $this->getJsonMessagesFileName( 'en--123' ) );
+       }
+
+       /**
+        * getFileName, getMessagesFileName, and getJsonMessagesFileName all throw if they get an
+        * invalid code. To save boilerplate, test them all in one method.
+        *
+        * @dataProvider provideExceptionFromInvalidCode
+        * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
+        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+        * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
+        * @covers Language::getFileName
+        * @covers Language::getMessagesFileName
+        * @covers Language::getJsonMessagesFileName
+        *
+        * @param callable $callback Will throw when passed $code
+        * @param string $code
+        */
+       public function testExceptionFromInvalidCode( $callback, $code ) {
+               $this->setExpectedException( MWException::class, "Invalid language code \"$code\"" );
+
+               $callback( $code );
+       }
+
+       public static function provideExceptionFromInvalidCode() {
+               $ret = [];
+               foreach ( static::provideIsValidBuiltInCode() as $desc => list( $code, $valid ) ) {
+                       if ( $valid ) {
+                               // Won't get an exception from this one
+                               continue;
+                       }
+
+                       // For getFileName, we define an anonymous function because of the extra first param
+                       $ret["getFileName: $desc"] = [
+                               function ( $code ) {
+                                       return static::getFileName( 'Messages', $code );
+                               },
+                               $code
+                       ];
+
+                       $ret["getMessagesFileName: $desc"] =
+                               [ [ static::class, 'getMessagesFileName' ], $code ];
+
+                       $ret["getJsonMessagesFileName: $desc"] =
+                               [ [ static::class, 'getJsonMessagesFileName' ], $code ];
+               }
+               return $ret;
+       }
+}
index 4e5c213..13dbc0e 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -91,6 +91,7 @@ function wfThumbHandle404() {
  */
 function wfStreamThumb( array $params ) {
        global $wgVaryOnXFP;
+       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
 
        $headers = []; // HTTP headers to send
 
@@ -154,9 +155,8 @@ function wfStreamThumb( array $params ) {
 
        // Check permissions if there are read restrictions
        $varyHeader = [];
-       if ( !in_array( 'read', User::getGroupPermissions( [ '*' ] ), true ) ) {
+       if ( !in_array( 'read', $permissionManager->getGroupPermissions( [ '*' ] ), true ) ) {
                $user = RequestContext::getMain()->getUser();
-               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
                $imgTitle = $img->getTitle();
 
                if ( !$imgTitle || !$permissionManager->userCan( 'read', $user, $imgTitle ) ) {