Merge "Load GlobalFunctions.php to tests/phpunit/bootstrap.php"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 15 Jul 2019 01:56:45 +0000 (01:56 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 15 Jul 2019 01:56:45 +0000 (01:56 +0000)
64 files changed:
RELEASE-NOTES-1.34
includes/DefaultSettings.php
includes/Setup.php
includes/SiteStats.php
includes/Storage/PageEditStash.php
includes/Title.php
includes/WikiMap.php
includes/api/i18n/fr.json
includes/api/i18n/mk.json
includes/api/i18n/zh-hans.json
includes/block/BlockRestrictionStore.php
includes/deferred/SiteStatsUpdate.php
includes/deferred/UserEditCountUpdate.php
includes/installer/i18n/be-tarask.json
includes/installer/i18n/it.json
includes/installer/i18n/sv.json
includes/installer/i18n/uk.json
includes/jobqueue/JobQueue.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobQueueMemory.php
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/filebackend/FileBackend.php
includes/libs/http/MultiHttpClient.php
includes/libs/lockmanager/LockManager.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/redis/RedisConnectionPool.php
includes/page/WikiPage.php
includes/search/SearchOracle.php
includes/search/SearchSqlite.php
includes/session/PHPSessionHandler.php
includes/site/DBSiteStore.php
includes/user/User.php
includes/user/UserGroupMembership.php
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/ckb.json
languages/i18n/co.json
languages/i18n/da.json
languages/i18n/dsb.json
languages/i18n/he.json
languages/i18n/ja.json
languages/i18n/ko.json
languages/i18n/min.json
languages/i18n/nap.json
languages/i18n/nqo.json
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WikiMapTest.php
tests/phpunit/includes/deferred/SiteStatsUpdateTest.php
tests/phpunit/includes/http/HttpTest.php
tests/phpunit/includes/session/SessionTest.php
tests/phpunit/unit/includes/http/HttpUnitTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/LanguageCodeTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/SpecialPageAliasTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionUnitTest.php [new file with mode: 0644]
tests/phpunit/unit/languages/LanguageCodeTest.php [deleted file]
tests/phpunit/unit/languages/SpecialPageAliasTest.php [deleted file]

index be24b50..ac10762 100644 (file)
@@ -58,6 +58,9 @@ For notes on 1.33.x and older releases, see HISTORY.
 * $wgWikiDiff2MovedParagraphDetectionCutoff — If you still want a custom change
   size threshold, please specify in php.ini, using the configuration variable
   wikidiff2.moved_paragraph_detection_cutoff.
+* $wgDebugPrintHttpHeaders - The default of including HTTP headers in the
+  debug log channel is no longer configurable. The debug log itself remains
+  configurable via $wgDebugLogFile.
 
 === New user-facing features in 1.34 ===
 * Special:Mute has been added as a quick way for users to block unwanted emails
@@ -272,6 +275,8 @@ because of Phabricator reports.
   0..(numRows-1).
 * The ChangePasswordForm hook, deprecated in 1.27, has been removed. Use the
   AuthChangeFormFields hook or security levels instead.
+* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed.
+  Use WikiMap::getWikiIdFromDbDomain() instead.
 * …
 
 === Deprecations in 1.34 ===
index 0886f38..5bf6163 100644 (file)
@@ -6330,11 +6330,6 @@ $wgShowDebug = false;
  */
 $wgDebugTimestamps = false;
 
-/**
- * Print HTTP headers for every request in the debug information.
- */
-$wgDebugPrintHttpHeaders = true;
-
 /**
  * Show the contents of $wgHooks in Special:Version
  */
@@ -6523,14 +6518,6 @@ $wgStatsdSamplingRates = [
  */
 $wgPageInfoTransclusionLimit = 50;
 
-/**
- * Set this to an integer to only do synchronous site_stats updates
- * one every *this many* updates. The other requests go into pending
- * delta values in $wgMemc. Make sure that $wgMemc is a global cache.
- * If set to -1, updates *only* go to $wgMemc (useful for daemons).
- */
-$wgSiteStatsAsyncFactor = false;
-
 /**
  * Parser test suite files to be run by parserTests.php when no specific
  * filename is passed to it.
index df53c99..d6f390a 100644 (file)
@@ -820,13 +820,9 @@ if ( $wgCommandLineMode ) {
        }
 } else {
        $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n";
-
-       if ( $wgDebugPrintHttpHeaders ) {
-               $debug .= "HTTP HEADERS:\n";
-
-               foreach ( $wgRequest->getAllHeaders() as $name => $value ) {
-                       $debug .= "$name: $value\n";
-               }
+       $debug .= "HTTP HEADERS:\n";
+       foreach ( $wgRequest->getAllHeaders() as $name => $value ) {
+               $debug .= "$name: $value\n";
        }
        wfDebug( $debug );
 }
index e3cb617..cf3a1eb 100644 (file)
@@ -52,14 +52,14 @@ class SiteStats {
                $config = MediaWikiServices::getInstance()->getMainConfig();
 
                $lb = self::getLB();
-               $dbr = $lb->getConnection( DB_REPLICA );
+               $dbr = $lb->getConnectionRef( DB_REPLICA );
                wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
                $row = self::doLoadFromDB( $dbr );
 
                if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
                        // Might have just been initialized during this request? Underflow?
                        wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
-                       $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+                       $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
                }
 
                if ( !self::isRowSane( $row ) ) {
@@ -76,7 +76,7 @@ class SiteStats {
                                SiteStatsInit::doAllAndCommit( $dbr );
                        }
 
-                       $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+                       $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
                }
 
                if ( !self::isRowSane( $row ) ) {
@@ -155,7 +155,7 @@ class SiteStats {
                        $cache->makeKey( 'SiteStats', 'groupcounts', $group ),
                        $cache::TTL_HOUR,
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) {
-                               $dbr = self::getLB()->getConnection( DB_REPLICA );
+                               $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
                                $setOpts += Database::getCacheSetOptions( $dbr );
 
                                return (int)$dbr->selectField(
@@ -206,7 +206,7 @@ class SiteStats {
                        $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
                        $cache::TTL_HOUR,
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns, $fname ) {
-                               $dbr = self::getLB()->getConnection( DB_REPLICA );
+                               $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
                                $setOpts += Database::getCacheSetOptions( $dbr );
 
                                return (int)$dbr->selectField(
index 2285f4a..6caca29 100644 (file)
@@ -109,7 +109,7 @@ class PageEditStash {
                // the stash request finishes parsing. For the lock acquisition below, there is not much
                // need to duplicate parsing of the same content/user/summary bundle, so try to avoid
                // blocking at all here.
-               $dbw = $this->lb->getConnection( DB_MASTER );
+               $dbw = $this->lb->getConnectionRef( DB_MASTER );
                if ( !$dbw->lock( $key, $fname, 0 ) ) {
                        // De-duplicate requests on the same key
                        return self::ERROR_BUSY;
@@ -357,7 +357,8 @@ class PageEditStash {
         * @return string|null TS_MW timestamp or null
         */
        private function lastEditTime( User $user ) {
-               $db = $this->lb->getConnection( DB_REPLICA );
+               $db = $this->lb->getConnectionRef( DB_REPLICA );
+
                $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
                $time = $db->selectField(
                        [ 'recentchanges' ] + $actorQuery['tables'],
index 28bec0b..95ccd9a 100644 (file)
@@ -1770,6 +1770,7 @@ class Title implements LinkTarget, IDBAccessObject {
                if (
                        !MediaWikiServices::getInstance()->getNamespaceInfo()->
                                hasSubpages( $this->mNamespace )
+                       || strtok( $this->getText(), '/' ) === false
                ) {
                        return $this->getText();
                }
index 23b0e3e..f2641f4 100644 (file)
@@ -286,15 +286,6 @@ class WikiMap {
                        : (string)$domain->getDatabase();
        }
 
-       /**
-        * @param string $domain
-        * @return string
-        * @deprecated Since 1.33; use getWikiIdFromDbDomain()
-        */
-       public static function getWikiIdFromDomain( $domain ) {
-               return self::getWikiIdFromDbDomain( $domain );
-       }
-
        /**
         * @return DatabaseDomain Database domain of the current wiki
         * @since 1.33
@@ -311,7 +302,7 @@ class WikiMap {
         * @since 1.33
         */
        public static function isCurrentWikiDbDomain( $domain ) {
-               return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) );
+               return self::getCurrentWikiDbDomain()->equals( $domain );
        }
 
        /**
index 640ddfa..b04ad1b 100644 (file)
@@ -33,7 +33,8 @@
                        "Kenjiraw",
                        "Framawiki",
                        "Epok",
-                       "Derugon"
+                       "Derugon",
+                       "Lucas Werkmeister (WMDE)"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> L’API MediaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errors and warnings]].\n\n<p class=\"mw-apisandbox-link\"><strong>Test :</strong> Pour faciliter le test des requêtes à l’API, voyez [[Special:ApiSandbox]].</p>",
        "apihelp-query+languageinfo-paramvalue-prop-code": "Le code de langue (ce code est spécifique à MédiaWiki, bien qu’il y ait des recouvrements avec d’autres standards).",
        "apihelp-query+languageinfo-paramvalue-prop-bcp47": "Le code de langue BCP-47.",
        "apihelp-query+languageinfo-paramvalue-prop-dir": "La direction d’écriture de la langue (<code>ltr</code> ou <code>rtl</code>).",
-       "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme d'une langue, c’est-à-dire son nom dans cette langue.",
+       "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme dune langue, c’est-à-dire son nom dans cette langue.",
        "apihelp-query+languageinfo-paramvalue-prop-name": "Le nom de la langue dans la langue spécifiée par le paramètre <var>lilang</var>, avec application des langues de secours si besoin.",
        "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "Les codes de langue des langues de secours configurées pour cette langue. Le secours implicite final en 'en' n’est pas inclus (mais certaines langues peuvent avoir 'en' en secours explicitement).",
        "apihelp-query+languageinfo-paramvalue-prop-variants": "Les codes de langue des variantes supportées par cette langue.",
index 26c7f10..f91cf3d 100644 (file)
@@ -20,7 +20,7 @@
        "apihelp-main-param-uselang": "Јазик за преведување на пораките. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> со <kbd>siprop=languages</kbd> дава список на јазични кодови, или укажете <kbd>user</kbd> за да го користите тековно зададениот јазик корисникот, или пак укажете <kbd>content</kbd> за да го користите јазикот на содржината на ова вики.",
        "apihelp-block-summary": "Блокирај корисник.",
        "apihelp-block-param-user": "Корисничко име, IP-адреса или IP-опсег ако сакате да блокирате. Не може да се користи заедно со <var>$1userid</var>",
-       "apihelp-block-param-expiry": "Време на истек. Може да биде релативно (на пр. <kbd>5 months</kbd> или „2 недели“) или пак апсолутно (на пр. <kbd>2014-09-18T12:34:56Z</kbd>). Ако го зададете <kbd>infinite</kbd>, <kbd>indefinite</kbd> или <kbd>never</kbd>, блокот ќе трае засекогаш.",
+       "apihelp-block-param-expiry": "Време на истек. Може да биде релативно (на пр. <kbd>5 months</kbd> или <kbd>2 weeks</kbd>) или пак апсолутно (на пр. <kbd>2014-09-18T12:34:56Z</kbd>). Ако го зададете <kbd>infinite</kbd>, <kbd>indefinite</kbd> или <kbd>never</kbd>, блокот ќе трае засекогаш.",
        "apihelp-block-param-reason": "Причина за блокирање.",
        "apihelp-block-param-anononly": "Блокирај само анонимни корисници (т.е. оневозможи анонимно уредување од оваа IP-адреса).",
        "apihelp-block-param-nocreate": "Оневозможи создавање кориснички сметки.",
        "apihelp-opensearch-param-search": "Низа за пребарување.",
        "apihelp-opensearch-param-limit": "Највеќе ставки за прикажување.",
        "apihelp-opensearch-param-namespace": "Именски простори за пребарување.",
-       "apihelp-opensearch-param-suggest": "Не прави ништо ако <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> е неточно.",
+       "apihelp-opensearch-param-suggest": "Не прави ништо ако <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> е неточно.",
        "apihelp-opensearch-param-redirects": "Како да се работи со пренасочувања:\n;return: Дај го самото пренасочување.\n;resolve: Дај ја целната страница. Може да даде помалку од $1limit ставки.\nОд историски причини, по основно е „return“ за $1format=json и „resolve“ за други формати.",
        "apihelp-opensearch-param-format": "Формат на изводот.",
        "apihelp-opensearch-example-te": "Најди страници што почнуваат со <kbd>Те</kbd>.",
        "apihelp-options-param-resetkinds": "Сисок на типови можности за повраток кога е зададена можноста <var>$1reset</var>.",
        "apihelp-options-param-change": "Список на промени во форматот name=value (на пр. skin=vector). Вредностите не треба да содржат исправени црти. Ако не зададете вредност (дури ни знак за равенство), на пр., можност|другаможност|..., ќе биде зададена вредноста на можноста по основно.",
        "apihelp-options-param-optionname": "Назив на можноста што треба да ѝ се зададе на вредноста дадена од <var>$1optionvalue</var>.",
-       "apihelp-options-param-optionvalue": "Вредноста на можноста укажана од <var>$1optionname</var>. Може да содржи исправени црти.",
+       "apihelp-options-param-optionvalue": "Вредноста на можноста укажана од <var>$1optionname</var>.",
        "apihelp-options-example-reset": "Врати ги сите поставки по основно",
        "apihelp-options-example-change": "Смени ги поставките <kbd>skin</kbd и <kbd>hideminor</kbd>.",
        "apihelp-options-example-complex": "Врати ги сите нагодувања по основно, а потоа задај ги <kbd>skin</kbd> и <kbd>nickname</kbd>.",
        "apihelp-paraminfo-summary": "Набави информации за извршнички (API) модули.",
-       "apihelp-paraminfo-param-modules": "Список на називи на модули (вредности на параметрите <var>action</var> и <var>format</var>, или пак <kbd>main</kbd>). Може да се укажат подмодули со <kbd>+</kbd>.",
+       "apihelp-paraminfo-param-modules": "Список на називи на модули (вредности на параметрите <var>action</var> и <var>format</var>, или пак <kbd>main</kbd>). Може да се укажат подмодули со <kbd>+</kbd>, или сите подмодули <kbd>+*</kbd>, или сите подмодули рекурзивно со <kbd>+**</kbd>.",
        "apihelp-paraminfo-param-helpformat": "Формат на помошните низи.",
        "apihelp-paraminfo-param-querymodules": "Список на називи на модули за барања (вредност на параметарот <var>prop</var>, <var>meta</var> или <var>list</var>). Користете го <kbd>$1modules=query+foo</kbd> наместо <kbd>$1querymodules=foo</kbd>.",
        "apihelp-paraminfo-param-mainmodule": "Добави информации и за главниот (врховен) модул. Користете го <kbd>$1modules=main</kbd> наместо тоа.",
        "apihelp-paraminfo-param-pagesetmodule": "Дај ги сите информации и за модулот на збирот страници (укажувајќи titles= и сродни).",
        "apihelp-paraminfo-param-formatmodules": "Список на називи на форматни модули (вредностза параметарот <var>format</var>). Наместо тоа, користете го <var>$1modules</var>.",
+       "apihelp-paraminfo-example-1": "Прикажи информации за <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> и <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
+       "apihelp-paraminfo-example-2": "Прикажи информации за сите подмодули на <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
        "apihelp-parse-param-summary": "Опис за расчленување.",
        "apihelp-parse-param-preview": "Расчлени во прегледен режим.",
        "apihelp-parse-param-sectionpreview": "Расчлени во прегледен режим на поднасловот (го овозможува и прегледниот режим).",
        "apihelp-patrol-summary": "Испатролирај страница или преработка.",
        "apihelp-patrol-param-rcid": "Назнака на спорешните промени за патролирање.",
        "apihelp-patrol-param-revid": "Назнака на преработката за патролирање.",
+       "apihelp-patrol-param-tags": "Ознаки за примена врз ставката во дневникот на патролирања.",
        "apihelp-patrol-example-rcid": "Испатролирај скорешна промена",
        "apihelp-patrol-example-revid": "Патролирај праработка",
        "apihelp-protect-summary": "Смени го степенот на заштита на страница.",
        "apihelp-protect-param-title": "Наслов на страница што се (од)заштитува. Не може да се користи заедно со $1pageid.",
        "apihelp-protect-param-pageid": "Назнака на страница што се (од)заштитува. Не може да се користи заедно со $1title.",
        "apihelp-protect-param-reason": "Причиина за (од)заштитување",
+       "apihelp-protect-param-tags": "Ознаки за примена врз ставката во дневникот на заштита.",
        "apihelp-protect-example-protect": "Заштити страница",
        "apihelp-purge-param-forcelinkupdate": "Поднови ги табелите со врски.",
        "apihelp-purge-example-simple": "Превчитај ги <kbd>Main Page</kbd> и <kbd>API</kbd>.",
        "apihelp-query+allcategories-param-from": "Од која категорија да почне набројувањето.",
        "apihelp-query+allcategories-param-to": "На која категорија да запре набројувањето.",
        "apihelp-query+allcategories-param-dir": "Насока на подредувањето.",
+       "apihelp-query+allcategories-param-limit": "Колку категории да се дадат.",
        "apihelp-query+allcategories-param-prop": "Кои својства да се дадат:",
        "apihelp-query+alldeletedrevisions-param-from": "Почни го исписот од овој наслов.",
        "apihelp-query+alldeletedrevisions-param-to": "Запри го исписот на овој наслов.",
index cf80ac0..83e8314 100644 (file)
@@ -29,7 +29,8 @@
                        "WhitePhosphorus",
                        "科劳",
                        "SolidBlock",
-                       "神樂坂秀吉"
+                       "神樂坂秀吉",
+                       "94rain"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>MediaWiki API是一个成熟稳定的,不断受到支持和改进的界面。尽管我们尽力避免,但偶尔也需要作出重大更新;请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n<p class=\"mw-apisandbox-link\"><strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。</p>",
        "apihelp-upload-param-comment": "上传注释。如果没有指定<var>$1text</var>,那么它也被用于新文件的初始页面文本。",
        "apihelp-upload-param-tags": "更改标签以应用于上传日志记录和文件页面修订中。",
        "apihelp-upload-param-text": "用于新文件的初始页面文本。",
-       "apihelp-upload-param-watch": "关注页面。",
+       "apihelp-upload-param-watch": "监视页面。",
        "apihelp-upload-param-watchlist": "无条件地将页面加入至当前用户的监视列表或将其移除,使用设置或不更改监视。",
        "apihelp-upload-param-ignorewarnings": "忽略任何警告。",
        "apihelp-upload-param-file": "文件内容。",
index df09ead..4fa4cfe 100644 (file)
@@ -66,7 +66,7 @@ class BlockRestrictionStore {
                        return [];
                }
 
-               $db = $db ?: $this->loadBalancer->getConnection( DB_REPLICA );
+               $db = $db ?: $this->loadBalancer->getConnectionRef( DB_REPLICA );
 
                $result = $db->select(
                        [ 'ipblocks_restrictions', 'page' ],
@@ -104,7 +104,7 @@ class BlockRestrictionStore {
                        return false;
                }
 
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->insert(
                        'ipblocks_restrictions',
@@ -125,7 +125,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function update( array $restrictions ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
 
@@ -197,7 +197,7 @@ class BlockRestrictionStore {
 
                $parentBlockId = (int)$parentBlockId;
 
-               $db = $this->loadBalancer->getConnection( DB_MASTER );
+               $db = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $db->startAtomic( __METHOD__ );
 
@@ -230,7 +230,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function delete( array $restrictions ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                $result = true;
                foreach ( $restrictions as $restriction ) {
                        if ( !$restriction instanceof Restriction ) {
@@ -260,7 +260,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function deleteByBlockId( $blockId ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                return $dbw->delete(
                        'ipblocks_restrictions',
                        [ 'ir_ipb_id' => $blockId ],
@@ -277,7 +277,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function deleteByParentBlockId( $parentBlockId ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                return $dbw->deleteJoin(
                        'ipblocks_restrictions',
                        'ipblocks',
index 7cb2950..11e9337 100644 (file)
@@ -25,8 +25,6 @@ use Wikimedia\Rdbms\IDatabase;
  * Class for handling updates to the site_stats table
  */
 class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
-       /** @var BagOStuff */
-       protected $stash;
        /** @var int */
        protected $edits = 0;
        /** @var int */
@@ -38,7 +36,14 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
        /** @var int */
        protected $images = 0;
 
-       private static $counters = [ 'edits', 'pages', 'articles', 'users', 'images' ];
+       /** @var string[] Map of (table column => counter type) */
+       private static $counters = [
+               'ss_total_edits'   => 'edits',
+               'ss_total_pages'   => 'pages',
+               'ss_good_articles' => 'articles',
+               'ss_users'         => 'users',
+               'ss_images'        => 'images'
+       ];
 
        // @todo deprecate this constructor
        function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
@@ -46,8 +51,6 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                $this->articles = $good;
                $this->pages = $pages;
                $this->users = $users;
-
-               $this->stash = MediaWikiServices::getInstance()->getMainObjectStash();
        }
 
        public function merge( MergeableUpdate $update ) {
@@ -60,8 +63,9 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
        }
 
        /**
-        * @param array $deltas
+        * @param int[] $deltas Map of (counter type => integer delta)
         * @return SiteStatsUpdate
+        * @throws UnexpectedValueException
         */
        public static function factory( array $deltas ) {
                $update = new self( 0, 0, 0 );
@@ -73,73 +77,46 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                }
 
                foreach ( self::$counters as $field ) {
-                       if ( isset( $deltas[$field] ) && $deltas[$field] ) {
-                               $update->$field = $deltas[$field];
-                       }
+                       $update->$field = $deltas[$field] ?? 0;
                }
 
                return $update;
        }
 
        public function doUpdate() {
-               $this->doUpdateContextStats();
-
-               $rate = MediaWikiServices::getInstance()->getMainConfig()->get( 'SiteStatsAsyncFactor' );
-               // If set to do so, only do actual DB updates 1 every $rate times.
-               // The other times, just update "pending delta" values in memcached.
-               if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
-                       $this->doUpdatePendingDeltas();
-               } else {
-                       // Need a separate transaction because this a global lock
-                       DeferredUpdates::addCallableUpdate( [ $this, 'tryDBUpdateInternal' ] );
-               }
-       }
-
-       /**
-        * Do not call this outside of SiteStatsUpdate
-        */
-       public function tryDBUpdateInternal() {
                $services = MediaWikiServices::getInstance();
-               $config = $services->getMainConfig();
-
-               $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
-               $lockKey = $dbw->getDomainID() . ':site_stats'; // prepend wiki ID
-               $pd = [];
-               if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
-                       // Lock the table so we don't have double DB/memcached updates
-                       if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
-                               $this->doUpdatePendingDeltas();
+               $stats = $services->getStatsdDataFactory();
 
-                               return;
+               $deltaByType = [];
+               foreach ( self::$counters as $type ) {
+                       $delta = $this->$type;
+                       if ( $delta !== 0 ) {
+                               $stats->updateCount( "site.$type", $delta );
                        }
-                       $pd = $this->getPendingDeltas();
-                       // Piggy-back the async deltas onto those of this stats update....
-                       $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] );
-                       $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] );
-                       $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] );
-                       $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] );
-                       $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] );
-               }
-
-               // Build up an SQL query of deltas and apply them...
-               $updates = '';
-               $this->appendUpdate( $updates, 'ss_total_edits', $this->edits );
-               $this->appendUpdate( $updates, 'ss_good_articles', $this->articles );
-               $this->appendUpdate( $updates, 'ss_total_pages', $this->pages );
-               $this->appendUpdate( $updates, 'ss_users', $this->users );
-               $this->appendUpdate( $updates, 'ss_images', $this->images );
-               if ( $updates != '' ) {
-                       $dbw->update( 'site_stats', [ $updates ], [], __METHOD__ );
+                       $deltaByType[$type] = $delta;
                }
 
-               if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
-                       // Decrement the async deltas now that we applied them
-                       $this->removePendingDeltas( $pd );
-                       // Commit the updates and unlock the table
-                       $dbw->unlock( $lockKey, __METHOD__ );
-               }
+               ( new AutoCommitUpdate(
+                       $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ),
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) use ( $deltaByType ) {
+                               $set = [];
+                               foreach ( self::$counters as $column => $type ) {
+                                       $delta = (int)$deltaByType[$type];
+                                       if ( $delta > 0 ) {
+                                               $set[] = "$column=$column+" . abs( $delta );
+                                       } elseif ( $delta < 0 ) {
+                                               $set[] = "$column=$column-" . abs( $delta );
+                                       }
+                               }
+
+                               if ( $set ) {
+                                       $dbw->update( 'site_stats', $set, [ 'ss_row_id' => 1 ], $fname );
+                               }
+                       }
+               ) )->doUpdate();
 
-               // Invalid cache used by parser functions
+               // Invalidate cache used by parser functions
                SiteStats::unload();
        }
 
@@ -151,7 +128,7 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                $services = MediaWikiServices::getInstance();
                $config = $services->getMainConfig();
 
-               $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
+               $dbr = $services->getDBLoadBalancer()->getConnectionRef( DB_REPLICA, 'vslow' );
                # Get non-bot users than did some recent action other than making accounts.
                # If account creation is included, the number gets inflated ~20+ fold on enwiki.
                $rcQuery = RecentChange::getQueryInfo();
@@ -182,105 +159,4 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
 
                return $activeUsers;
        }
-
-       protected function doUpdateContextStats() {
-               $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-               foreach ( [ 'edits', 'articles', 'pages', 'users', 'images' ] as $type ) {
-                       $delta = $this->$type;
-                       if ( $delta !== 0 ) {
-                               $stats->updateCount( "site.$type", $delta );
-                       }
-               }
-       }
-
-       protected function doUpdatePendingDeltas() {
-               $this->adjustPending( 'ss_total_edits', $this->edits );
-               $this->adjustPending( 'ss_good_articles', $this->articles );
-               $this->adjustPending( 'ss_total_pages', $this->pages );
-               $this->adjustPending( 'ss_users', $this->users );
-               $this->adjustPending( 'ss_images', $this->images );
-       }
-
-       /**
-        * @param string &$sql
-        * @param string $field
-        * @param int $delta
-        */
-       protected function appendUpdate( &$sql, $field, $delta ) {
-               if ( $delta ) {
-                       if ( $sql ) {
-                               $sql .= ',';
-                       }
-                       if ( $delta < 0 ) {
-                               $sql .= "$field=$field-" . abs( $delta );
-                       } else {
-                               $sql .= "$field=$field+" . abs( $delta );
-                       }
-               }
-       }
-
-       /**
-        * @param BagOStuff $stash
-        * @param string $type
-        * @param string $sign ('+' or '-')
-        * @return string
-        */
-       private function getTypeCacheKey( BagOStuff $stash, $type, $sign ) {
-               return $stash->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
-       }
-
-       /**
-        * Adjust the pending deltas for a stat type.
-        * Each stat type has two pending counters, one for increments and decrements
-        * @param string $type
-        * @param int $delta Delta (positive or negative)
-        */
-       protected function adjustPending( $type, $delta ) {
-               if ( $delta < 0 ) { // decrement
-                       $key = $this->getTypeCacheKey( $this->stash, $type, '-' );
-               } else { // increment
-                       $key = $this->getTypeCacheKey( $this->stash, $type, '+' );
-               }
-
-               $magnitude = abs( $delta );
-               $this->stash->incrWithInit( $key, 0, $magnitude, $magnitude );
-       }
-
-       /**
-        * Get pending delta counters for each stat type
-        * @return array Positive and negative deltas for each type
-        */
-       protected function getPendingDeltas() {
-               $pending = [];
-               foreach ( [ 'ss_total_edits',
-                       'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ] as $type
-               ) {
-                       // Get pending increments and pending decrements
-                       $flg = BagOStuff::READ_LATEST;
-                       $pending[$type]['+'] = (int)$this->stash->get(
-                               $this->getTypeCacheKey( $this->stash, $type, '+' ),
-                               $flg
-                       );
-                       $pending[$type]['-'] = (int)$this->stash->get(
-                               $this->getTypeCacheKey( $this->stash, $type, '-' ),
-                               $flg
-                       );
-               }
-
-               return $pending;
-       }
-
-       /**
-        * Reduce pending delta counters after updates have been applied
-        * @param array $pd Result of getPendingDeltas(), used for DB update
-        */
-       protected function removePendingDeltas( array $pd ) {
-               foreach ( $pd as $type => $deltas ) {
-                       foreach ( $deltas as $sign => $magnitude ) {
-                               // Lower the pending counter now that we applied these changes
-                               $key = $this->getTypeCacheKey( $this->stash, $type, $sign );
-                               $this->stash->decr( $key, $magnitude );
-                       }
-               }
-       }
 }
index ed7e00c..687dfbe 100644 (file)
@@ -67,7 +67,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate {
         */
        public function doUpdate() {
                $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $dbw = $lb->getConnection( DB_MASTER );
+               $dbw = $lb->getConnectionRef( DB_MASTER );
                $fname = __METHOD__;
 
                ( new AutoCommitUpdate( $dbw, __METHOD__, function () use ( $lb, $dbw, $fname ) {
@@ -85,8 +85,8 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate {
                                        // The user_editcount is probably NULL (e.g. not initialized).
                                        // Since this update runs after the new revisions were committed,
                                        // wait for the replica DB to catch up so they will be counted.
-                                       $dbr = $lb->getConnection( DB_REPLICA );
-                                       // If $dbr is actually the master DB, then clearing the snapshot is
+                                       $dbr = $lb->getConnectionRef( DB_REPLICA );
+                                       // If $dbr is actually the master DB, then clearing the snapshot
                                        // is harmless and waitForMasterPos() will just no-op.
                                        $dbr->flushSnapshot( $fname );
                                        $lb->waitForMasterPos( $dbr );
index 52cab04..136f3a2 100644 (file)
@@ -51,6 +51,8 @@
        "config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]",
        "config-sidebar-readme": "Прачытай мяне",
        "config-sidebar-relnotes": "Заўвагі да выпуску",
+       "config-sidebar-license": "Капіяваньне",
+       "config-sidebar-upgrade": "Абнаўленьне",
        "config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
        "config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
        "config-env-php": "Усталяваны PHP $1.",
index 36e1902..ce38338 100644 (file)
@@ -67,7 +67,7 @@
        "config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/Special:MyLanguage/Help:Contents Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:Contents Guida ai contenuti per admin]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:FAQ FAQ]",
        "config-sidebar-readme": "Leggimi",
        "config-sidebar-relnotes": "Note di versione",
-       "config-sidebar-license": "copiando",
+       "config-sidebar-license": "Licenza",
        "config-sidebar-upgrade": "Aggiornamento",
        "config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
        "config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
index e2cb99e..1db3fca 100644 (file)
        "config-restart": "Ja, starta om",
        "config-welcome": "=== Miljökontroller ===\nGrundläggande kontroller kommer nu att utföras för att se om denna miljö är lämplig för installation av MediaWiki.\nKom ihåg att ta med denna information om du söker stöd för hur du skall slutföra installationen.",
        "config-copyright": "=== Upphovsrätt och Villkor ===\n\n$1\n\nDetta program är fri programvara; du kan vidaredistribuera den och/eller modifiera det enligt villkoren i GNU General Public License som publicerats av Free Software Foundation; antingen genom version 2 av licensen, eller (på ditt initiativ) någon senare version.\n\nDetta program är distribuerat i hopp om att det kommer att vara användbart, men '''utan någon garanti'''; utan att ens ha en underförstådd garanti om '''säljbarhet''' eller '''lämplighet för ett särskilt ändamål'''.\nSe GNU General Public License för mer detaljer.\n\nDu bör ha fått <doclink href=Copying>en kopia av GNU General Public License</doclink> tillsammans med detta program; om inte, skriv till Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eller [https://www.gnu.org/copyleft/gpl.html läs den online].",
-       "config-sidebar": "* [https://www.mediawiki.org MediaWikis webbplats]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Användarguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratörguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Frågor och svar]\n----\n* <doclink href=Readme>Läs mig</doclink>\n* <doclink href=ReleaseNotes>Utgivningsanteckningar</doclink>\n* <doclink href=Copying>Kopiering</doclink>\n* <doclink href=UpgradeDoc>Uppgradering</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org MediaWikis webbplats]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Användarguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratörsguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Frågor och svar]",
+       "config-sidebar-readme": "Läs mig",
+       "config-sidebar-relnotes": "Utgivningsanteckningar",
+       "config-sidebar-license": "Kopierar",
+       "config-sidebar-upgrade": "Uppgradering",
        "config-env-good": "Miljön har kontrollerats.\nDu kan installera MediaWiki.",
        "config-env-bad": "Miljön har kontrollerats.\nDu kan inte installera MediaWiki.",
        "config-env-php": "PHP $1 är installerat.",
index 321caef..bfb0e64 100644 (file)
        "config-restart": "Так, перезапустити установку",
        "config-welcome": "=== Перевірка оточення ===\nБудуть проведені базові перевірки, щоб виявити, чи можлива установка MediaWiki у даній системі.\nНе забудьте включити цю інформацію, якщо ви звернетеся по підтримку, як завершити установку.",
        "config-copyright": "=== Авторське право і умови ===\n\n$1\n\nЦя програма є вільним програмним забезпеченням; Ви можете розповсюджувати та/або змінювати її під ліцензією GNU General Public License, опублікованою Фондом вільного програмного забезпечення; версією 2 цієї ліцензії або будь-якою пізнішою на Ваш вибір.\n\nЦя програма поширюється з надією на те, що вона буде корисною, однак '''без жодних гарантій'''; навіть без неявної гарантії '''комерційної цінності''' або '''придатності для певних цілей'''.\nДив. GNU General Public License для детальної інформації.\n\nВи повинні були отримати <doclink href=Copying>копію GNU General Public License</doclink> разом із цією програмою; якщо ж ні, зверніться до Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. або [https://www.gnu.org/copyleft/gpl.html ознайомтесь з нею онлайн].",
-       "config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Посібник користувача]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Посібник адміністратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Інформація про випуск</doclink>\n* <doclink href=Copying>Ліцензія</doclink>\n* <doclink href=UpgradeDoc>Оновлення</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Посібник користувача]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Посібник адміністратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]",
+       "config-sidebar-readme": "Прочитай мене",
+       "config-sidebar-relnotes": "Інформація про версію",
+       "config-sidebar-license": "Копіювання",
+       "config-sidebar-upgrade": "Оновлення",
        "config-env-good": "Перевірку середовища успішно завершено.\nВи можете встановити MediaWiki.",
        "config-env-bad": "Було проведено перевірку середовища. Ви не можете встановити MediaWiki.",
        "config-env-php": "Встановлено версію PHP: $1.",
        "config-env-hhvm": "HHVM $1  встановлено.",
-       "config-unicode-using-intl": "Ð\92икоÑ\80иÑ\81Ñ\82овÑ\83ваÑ\82и [https://pecl.php.net/intl Ð¼Ñ\96жнаÑ\80одне Ñ\80озÑ\88иÑ\80еннÑ\8f PECL] для нормалізації Юнікоду.",
-       "config-unicode-pure-php-warning": "'''Увага''': [https://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
+       "config-unicode-using-intl": "Ð\97а Ð´Ð¾Ð¿Ð¾Ð¼Ð¾Ð³Ð¾Ñ\8e [https://php.net/manual/en/book.intl.php PHP-Ñ\80озÑ\88иÑ\80еннÑ\8f intl] для нормалізації Юнікоду.",
+       "config-unicode-pure-php-warning": "'''Увага''': [https://php.net/manual/en/book.intl.php PHP-розширення intl] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
        "config-unicode-update-warning": "'''Увага''': Встановлена версія обгортки нормалізації Юнікоду використовує стару версію бібліотеки [http://site.icu-project.org/ проекту ICU].\nВи маєте [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations оновити версію], якщо плануєте повноцінно використовувати Юнікод.",
        "config-no-db": "Не вдалося знайти потрібний драйвер бази даних! Вам необхідно встановити драйвер бази даних для PHP. Підтримуються {{PLURAL:$2|такий тип|такі типи}} баз даних: $1.\n\nЯкщо ви скомпілювали PHP самостійно, переналаштуйте його з увімкненим клієнтом бази даних, наприклад за допомогою <code>./configure --with-mysqli</code>.\n\nЯкщо установлено PHP з пакетів Debian або Ubuntu, тоді ви також повинні встановити, наприклад, пакунок <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Увага:</strong> у Вас встановлена версія SQLite $2, а це нижче, ніж мінімально необхідна версія $1. SQLite буде недоступним.",
        "config-support-info": "MediaWiki підтримує такі системи баз даних:\n\n$1\n\nЯкщо Ви не бачите серед перерахованих систему баз даних, яку використовуєте, виконайте вказівки, вказані вище, щоб увімкнути підтримку.",
        "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] є основною ціллю для MediaWiki і найкраще підтримується.  MediaWiki також працює з [{{int:version-db-mysql-url}} MySQL] та [{{int:version-db-percona-url}} Percona Server], які сумісні з MariaDB.  ([https://www.php.net/manual/en/mysqli.installation.php Як зібрати PHP з підтримкою MySQL])",
        "config-dbsupport-postgres": "*  [{{int:version-db-postgres-url}} PostgreSQL] — популярна відкрита СУБД, альтернатива MySQL. ([https://www.php.net/manual/en/pgsql.installation.php як зібрати PHP з допомогою PostgreSQL]).",
-       "config-dbsupport-sqlite": "*  [{{int:version-db-sqlite-url}} SQLite] — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], що використовує PDO)",
-       "config-dbsupport-oracle": "*  [{{int:version-db-oracle-url}} Oracle] — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])",
+       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], використовує PDO)",
+       "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])",
        "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] — це комерційна база даних для Windows масштабу підприємства. ([https://www.php.net/manual/en/sqlsrv.installation.php Як зібрати PHP з підтримкою SQLSRV])",
        "config-header-mysql": "Налаштування MariaDB/MySQL",
        "config-header-postgres": "Налаштування PostgreSQL",
        "config-license-help": "Чимало загальнодоступних вікі публікують увесь свій вміст під [https://freedomdefined.org/Definition вільною ліцензією]. Це розвиває відчуття спільної власності і заохочує довготривалу участь. У загальному випадку для приватної чи корпоративної вікі у цьому немає необхідності.\n\nЯкщо Ви хочете мати змогу використовувати текст з Вікіпедії і дати Вікіпедії змогу використовувати текст, скопійований з Вашої вікі, вам необхідно обрати <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаніше Вікіпедія використовувала GNU Free Documentation License.\nGFDL — допустима ліцензія, але у ній важко розібратися, а контент під GFDL важко використовувати повторно.",
        "config-email-settings": "Налаштування електронної пошти",
        "config-enable-email": "Увімкнути вихідну електронну пошту",
-       "config-enable-email-help": "Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [Config-dbsupport-oracle/manual/en/mail.configuration.php налаштування пошти у PHP].\nЯкщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відімкнути.",
+       "config-enable-email-help": "Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [https://www.php.net/manual/en/mail.configuration.php налаштування пошти у PHP].\nЯкщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відімкнути.",
        "config-email-user": "Увімкнути електронну пошту користувач-користувачеві",
        "config-email-user-help": "Дозволити усім користувачам надсилати один одному електронну пошту, якщо вони увімкнули цю можливість у своїх налаштуваннях.",
        "config-email-usertalk": "Увімкнути сповіщення про повідомлення на сторінці обговорення користувача",
index f5ed7b9..e52f295 100644 (file)
@@ -44,8 +44,8 @@ abstract class JobQueue {
        /** @var StatsdDataFactoryInterface */
        protected $stats;
 
-       /** @var BagOStuff */
-       protected $dupCache;
+       /** @var WANObjectCache */
+       protected $wanCache;
 
        const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
 
@@ -53,6 +53,14 @@ abstract class JobQueue {
 
        /**
         * @param array $params
+        *       - type           : A job type
+        *   - domain         : A DB domain ID
+        *   - wanCache       : An instance of WANObjectCache to use for caching [default: none]
+        *   - stats          : An instance of StatsdDataFactoryInterface [default: none]
+        *   - claimTTL       : Seconds a job can be claimed for exclusive execution [default: forever]
+        *   - maxTries       : Total times a job can be tried, assuming claims expire [default: 3]
+        *   - order          : Queue order, one of ("fifo", "timestamp", "random") [default: variable]
+        *   - readOnlyReason : Mark the queue as read-only with this reason [default: false]
         * @throws JobQueueError
         */
        protected function __construct( array $params ) {
@@ -70,7 +78,7 @@ abstract class JobQueue {
                }
                $this->readOnlyReason = $params['readOnlyReason'] ?? false;
                $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
-               $this->dupCache = $params['stash'] ?? new EmptyBagOStuff();
+               $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
        }
 
        /**
@@ -459,24 +467,23 @@ abstract class JobQueue {
         * @return bool
         */
        protected function doDeduplicateRootJob( IJobSpecification $job ) {
-               if ( !$job->hasRootJobParams() ) {
+               $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+               if ( !$params ) {
                        throw new JobQueueError( "Cannot register root job; missing parameters." );
                }
-               $params = $job->getRootJobParams();
 
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
-               // Callers should call JobQueueGroup::push() before this method so that if the insert
-               // fails, the de-duplication registration will be aborted. Since the insert is
-               // deferred till "transaction idle", do the same here, so that the ordering is
-               // maintained. Having only the de-duplication registration succeed would cause
-               // jobs to become no-ops without any actual jobs that made them redundant.
-               $timestamp = $this->dupCache->get( $key ); // current last timestamp of this job
-               if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+               // Callers should call JobQueueGroup::push() before this method so that if the
+               // insert fails, the de-duplication registration will be aborted. Having only the
+               // de-duplication registration succeed would cause jobs to become no-ops without
+               // any actual jobs that made them redundant.
+               $timestamp = $this->wanCache->get( $key ); // last known timestamp of such a root job
+               if ( $timestamp !== false && $timestamp >= $params['rootJobTimestamp'] ) {
                        return true; // a newer version of this root job was enqueued
                }
 
                // Update the timestamp of the last root job started at the location...
-               return $this->dupCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+               return $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
        }
 
        /**
@@ -490,9 +497,8 @@ abstract class JobQueue {
                if ( $job->getType() !== $this->type ) {
                        throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
                }
-               $isDuplicate = $this->doIsRootJobOldDuplicate( $job );
 
-               return $isDuplicate;
+               return $this->doIsRootJobOldDuplicate( $job );
        }
 
        /**
@@ -501,14 +507,18 @@ abstract class JobQueue {
         * @return bool
         */
        protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
-               if ( !$job->hasRootJobParams() ) {
+               $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+               if ( !$params ) {
                        return false; // job has no de-deplication info
                }
-               $params = $job->getRootJobParams();
 
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
                // Get the last time this root job was enqueued
-               $timestamp = $this->dupCache->get( $key );
+               $timestamp = $this->wanCache->get( $key );
+               if ( $timestamp === false || $params['rootJobTimestamp'] > $timestamp ) {
+                       // Update the timestamp of the last known root job started at the location...
+                       $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+               }
 
                // Check if a new root job was started at the location after this one's...
                return ( $timestamp && $timestamp > $params['rootJobTimestamp'] );
@@ -519,7 +529,7 @@ abstract class JobQueue {
         * @return string
         */
        protected function getRootJobCacheKey( $signature ) {
-               return $this->dupCache->makeGlobalKey(
+               return $this->wanCache->makeGlobalKey(
                        'jobqueue',
                        $this->domain,
                        $this->type,
index 7c78f40..f7b8ed2 100644 (file)
@@ -24,6 +24,7 @@ use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\DBError;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\ScopedCallback;
 
 /**
@@ -38,9 +39,7 @@ class JobQueueDB extends JobQueue {
        const MAX_JOB_RANDOM = 2147483647; // integer; 2^31 - 1, used for job_random
        const MAX_OFFSET = 255; // integer; maximum number of rows to skip
 
-       /** @var WANObjectCache */
-       protected $cache;
-       /** @var IDatabase|DBError|null */
+       /** @var IMaintainableDatabase|DBError|null */
        protected $conn;
 
        /** @var array|null Server configuration array */
@@ -55,7 +54,6 @@ class JobQueueDB extends JobQueue {
         *               If not specified, the primary DB cluster for the wiki will be used.
         *               This can be overridden with a custom cluster so that DB handles will
         *               be retrieved via LBFactory::getExternalLB() and getConnection().
-        *   - wanCache : An instance of WANObjectCache to use for caching.
         * @param array $params
         */
        protected function __construct( array $params ) {
@@ -66,8 +64,6 @@ class JobQueueDB extends JobQueue {
                } elseif ( isset( $params['cluster'] ) && is_string( $params['cluster'] ) ) {
                        $this->cluster = $params['cluster'];
                }
-
-               $this->cache = $params['wanCache'] ?? WANObjectCache::newEmpty();
        }
 
        protected function supportedOrders() {
@@ -104,7 +100,7 @@ class JobQueueDB extends JobQueue {
        protected function doGetSize() {
                $key = $this->getCacheKey( 'size' );
 
-               $size = $this->cache->get( $key );
+               $size = $this->wanCache->get( $key );
                if ( is_int( $size ) ) {
                        return $size;
                }
@@ -120,7 +116,7 @@ class JobQueueDB extends JobQueue {
                } catch ( DBError $e ) {
                        throw $this->getDBException( $e );
                }
-               $this->cache->set( $key, $size, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $size, self::CACHE_TTL_SHORT );
 
                return $size;
        }
@@ -136,7 +132,7 @@ class JobQueueDB extends JobQueue {
 
                $key = $this->getCacheKey( 'acquiredcount' );
 
-               $count = $this->cache->get( $key );
+               $count = $this->wanCache->get( $key );
                if ( is_int( $count ) ) {
                        return $count;
                }
@@ -152,7 +148,7 @@ class JobQueueDB extends JobQueue {
                } catch ( DBError $e ) {
                        throw $this->getDBException( $e );
                }
-               $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
 
                return $count;
        }
@@ -169,7 +165,7 @@ class JobQueueDB extends JobQueue {
 
                $key = $this->getCacheKey( 'abandonedcount' );
 
-               $count = $this->cache->get( $key );
+               $count = $this->wanCache->get( $key );
                if ( is_int( $count ) ) {
                        return $count;
                }
@@ -190,7 +186,7 @@ class JobQueueDB extends JobQueue {
                        throw $this->getDBException( $e );
                }
 
-               $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
 
                return $count;
        }
@@ -345,7 +341,7 @@ class JobQueueDB extends JobQueue {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = $this->getScopedNoTrxFlag( $dbw );
                // Check cache to see if the queue has <= OFFSET items
-               $tinyQueue = $this->cache->get( $this->getCacheKey( 'small' ) );
+               $tinyQueue = $this->wanCache->get( $this->getCacheKey( 'small' ) );
 
                $invertedDirection = false; // whether one job_random direction was already scanned
                // This uses a replication safe method for acquiring jobs. One could use UPDATE+LIMIT
@@ -385,7 +381,7 @@ class JobQueueDB extends JobQueue {
                                );
                                if ( !$row ) {
                                        $tinyQueue = true; // we know the queue must have <= MAX_OFFSET rows
-                                       $this->cache->set( $this->getCacheKey( 'small' ), 1, 30 );
+                                       $this->wanCache->set( $this->getCacheKey( 'small' ), 1, 30 );
                                        continue; // use job_random
                                }
                        }
@@ -510,32 +506,17 @@ class JobQueueDB extends JobQueue {
         * @return bool
         */
        protected function doDeduplicateRootJob( IJobSpecification $job ) {
-               $params = $job->getParams();
-               if ( !isset( $params['rootJobSignature'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
-               } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
-               }
-               $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
-               // Callers should call JobQueueGroup::push() before this method so that if the insert
-               // fails, the de-duplication registration will be aborted. Since the insert is
-               // deferred till "transaction idle", do the same here, so that the ordering is
+               // Callers should call JobQueueGroup::push() before this method so that if the
+               // insert fails, the de-duplication registration will be aborted. Since the insert
+               // is deferred till "transaction idle", do the same here, so that the ordering is
                // maintained. Having only the de-duplication registration succeed would cause
                // jobs to become no-ops without any actual jobs that made them redundant.
                $dbw = $this->getMasterDB();
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = $this->getScopedNoTrxFlag( $dbw );
-
-               $cache = $this->dupCache;
                $dbw->onTransactionCommitOrIdle(
-                       function () use ( $cache, $params, $key ) {
-                               $timestamp = $cache->get( $key ); // current last timestamp of this job
-                               if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
-                                       return true; // a newer version of this root job was enqueued
-                               }
-
-                               // Update the timestamp of the last root job started at the location...
-                               return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+                       function () use ( $job ) {
+                               parent::doDeduplicateRootJob( $job );
                        },
                        __METHOD__
                );
@@ -581,7 +562,7 @@ class JobQueueDB extends JobQueue {
         */
        protected function doFlushCaches() {
                foreach ( [ 'size', 'acquiredcount' ] as $type ) {
-                       $this->cache->delete( $this->getCacheKey( $type ) );
+                       $this->wanCache->delete( $this->getCacheKey( $type ) );
                }
        }
 
@@ -789,7 +770,7 @@ class JobQueueDB extends JobQueue {
 
        /**
         * @throws JobQueueConnectionError
-        * @return IDatabase
+        * @return IMaintainableDatabase
         */
        protected function getMasterDB() {
                try {
@@ -801,7 +782,7 @@ class JobQueueDB extends JobQueue {
 
        /**
         * @param int $index (DB_REPLICA/DB_MASTER)
-        * @return IDatabase
+        * @return IMaintainableDatabase
         */
        protected function getDB( $index ) {
                if ( $this->server ) {
@@ -825,12 +806,16 @@ class JobQueueDB extends JobQueue {
                                ? $lbFactory->getExternalLB( $this->cluster )
                                : $lbFactory->getMainLB( $this->domain );
 
-                       return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
+                       if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
                                // Keep a separate connection to avoid contention and deadlocks;
                                // However, SQLite has the opposite behavior due to DB-level locking.
-                               ? $lb->getConnectionRef( $index, [], $this->domain, $lb::CONN_TRX_AUTOCOMMIT )
+                               $flags = $lb::CONN_TRX_AUTOCOMMIT;
+                       } else {
                                // Jobs insertion will be defered until the PRESEND stage to reduce contention.
-                               : $lb->getConnectionRef( $index, [], $this->domain );
+                               $flags = 0;
+                       }
+
+                       return $lb->getMaintenanceConnectionRef( $index, [], $this->domain, $flags );
                }
        }
 
@@ -856,7 +841,7 @@ class JobQueueDB extends JobQueue {
        private function getCacheKey( $property ) {
                $cluster = is_string( $this->cluster ) ? $this->cluster : 'main';
 
-               return $this->cache->makeGlobalKey(
+               return $this->wanCache->makeGlobalKey(
                        'jobqueue',
                        $this->domain,
                        $cluster,
index 756724e..06cd04c 100644 (file)
@@ -121,7 +121,6 @@ class JobQueueGroup {
                $services = MediaWikiServices::getInstance();
                $conf['stats'] = $services->getStatsdDataFactory();
                $conf['wanCache'] = $services->getMainWANObjectCache();
-               $conf['stash'] = $services->getMainObjectStash();
 
                return JobQueue::factory( $conf );
        }
index cb20a76..b26129e 100644 (file)
@@ -33,9 +33,9 @@ class JobQueueMemory extends JobQueue {
        protected static $data = [];
 
        public function __construct( array $params ) {
-               parent::__construct( $params );
+               $params['wanCache'] = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
 
-               $this->dupCache = new HashBagOStuff();
+               parent::__construct( $params );
        }
 
        /**
index b8a5ad2..569a5d4 100644 (file)
@@ -451,7 +451,7 @@ LUA;
 
                $conn = $this->getConnection();
                try {
-                       $timestamp = $conn->get( $key ); // current last timestamp of this job
+                       $timestamp = $conn->get( $key ); // last known timestamp of such a root job
                        if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
                                return true; // a newer version of this root job was enqueued
                        }
index 882ae64..cb4b051 100644 (file)
@@ -81,7 +81,7 @@ class CategoryMembershipChangeJob extends Job {
        public function run() {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lb = $lbFactory->getMainLB();
-               $dbw = $lb->getConnection( DB_MASTER );
+               $dbw = $lb->getConnectionRef( DB_MASTER );
 
                $this->ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
 
@@ -92,7 +92,7 @@ class CategoryMembershipChangeJob extends Job {
                }
 
                // Cut down on the time spent in waitForMasterPos() in the critical section
-               $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
+               $dbr = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
                if ( !$lb->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( "Timed out while pre-waiting for replica DB to catch up" );
                        return false;
index 74b90ed..e373605 100644 (file)
@@ -40,8 +40,8 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
                $batchSize = $wgUpdateRowsPerQuery;
 
                $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $dbw = $loadBalancer->getConnection( DB_MASTER );
-               $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] );
+               $dbw = $loadBalancer->getConnectionRef( DB_MASTER );
+               $dbr = $loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
 
                // Wait before lock to try to reduce time waiting in the lock.
                if ( !$loadBalancer->waitForMasterPos( $dbr ) ) {
index f53174a..054718d 100644 (file)
@@ -51,7 +51,7 @@ class ClearWatchlistNotificationsJob extends Job implements GenericParameterJob
                $lbFactory = $services->getDBLoadBalancerFactory();
                $rowsPerQuery = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
 
-               $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+               $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
                $timestamp = $this->params['timestamp'] ?? null;
                if ( $timestamp === null ) {
index 3179a2f..b4046a6 100644 (file)
@@ -156,7 +156,9 @@ class RefreshLinksJob extends Job {
                // Serialize link update job by page ID so they see each others' changes.
                // The page ID and latest revision ID will be queried again after the lock
                // is acquired to bail if they are changed from that of loadPageData() above.
-               $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+               // Serialize links updates by page ID so they see each others' changes
+               $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' );
                if ( $scopedLock === null ) {
                        // Another job is already updating the page, likely for a prior revision (T170596)
index 53a0ca0..4ad48c7 100644 (file)
@@ -30,6 +30,7 @@
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use Wikimedia\ScopedCallback;
+use Psr\Log\NullLogger;
 
 /**
  * @brief Base class for all file backend classes (including multi-write backends).
@@ -190,7 +191,7 @@ abstract class FileBackend implements LoggerAwareInterface {
                if ( !is_callable( $this->profiler ) ) {
                        $this->profiler = null;
                }
-               $this->logger = $config['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $config['logger'] ?? new NullLogger();
                $this->statusWrapper = $config['statusWrapper'] ?? null;
                $this->tmpDirectory = $config['tmpDirectory'] ?? null;
        }
index a6135ae..2e418b9 100644 (file)
@@ -57,7 +57,7 @@ class MultiHttpClient implements LoggerAwareInterface {
        /** @var float */
        protected $connTimeout = 10;
        /** @var float */
-       protected $reqTimeout = 300;
+       protected $reqTimeout = 900;
        /** @var bool */
        protected $usePipelining = false;
        /** @var int */
index d152c65..b8d3ad2 100644 (file)
@@ -4,6 +4,7 @@
  * @ingroup FileBackend
  */
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 use Wikimedia\WaitConditionLoop;
 
 /**
@@ -101,7 +102,7 @@ abstract class LockManager {
                }
                $this->session = md5( implode( '-', $random ) );
 
-               $this->logger = $config['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $config['logger'] ?? new NullLogger();
        }
 
        /**
index 42146f4..2462174 100644 (file)
@@ -21,6 +21,7 @@
  */
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 /**
  * Implements functions related to MIME types such as detection and mapping to file extension
@@ -199,7 +200,7 @@ EOT;
                $this->detectCallback = $params['detectCallback'] ?? null;
                $this->guessCallback = $params['guessCallback'] ?? null;
                $this->extCallback = $params['extCallback'] ?? null;
-               $this->logger = $params['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $params['logger'] ?? new NullLogger();
 
                $this->loadFiles();
        }
index c47f6ee..dce49c4 100644 (file)
@@ -682,25 +682,33 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
 
        /**
         * Get an associative array containing the item for each of the keys that have items.
-        * @param string[] $keys List of keys
+        * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
         * @param int $flags Bitfield; supports READ_LATEST [optional]
-        * @return array Map of (key => value) for existing keys
+        * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
         */
        public function getMulti( array $keys, $flags = 0 ) {
-               $valuesBykey = $this->doGetMulti( $keys, $flags );
-               foreach ( $valuesBykey as $key => $value ) {
+               $foundByKey = $this->doGetMulti( $keys, $flags );
+
+               $res = [];
+               foreach ( $keys as $key ) {
                        // Resolve one blob at a time (avoids too much I/O at once)
-                       $valuesBykey[$key] = $this->resolveSegments( $key, $value );
+                       if ( array_key_exists( $key, $foundByKey ) ) {
+                               // A value should not appear in the key if a segment is missing
+                               $value = $this->resolveSegments( $key, $foundByKey[$key] );
+                               if ( $value !== false ) {
+                                       $res[$key] = $value;
+                               }
+                       }
                }
 
-               return $valuesBykey;
+               return $res;
        }
 
        /**
         * Get an associative array containing the item for each of the keys that have items.
         * @param string[] $keys List of keys
         * @param int $flags Bitfield; supports READ_LATEST [optional]
-        * @return array Map of (key => value) for existing keys
+        * @return mixed[] Map of (key => value) for existing keys
         */
        protected function doGetMulti( array $keys, $flags = 0 ) {
                $res = [];
index dd859ad..a72b3ff 100644 (file)
@@ -116,7 +116,6 @@ class RedisBagOStuff extends BagOStuff {
                        if ( $ttl ) {
                                $result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
                        } else {
-                               // No expiry, that is very different from zero expiry in Redis
                                $result = $conn->set( $key, $this->serialize( $value ) );
                        }
                } catch ( RedisException $e ) {
@@ -215,11 +214,7 @@ class RedisBagOStuff extends BagOStuff {
                                        $this->debug( "setMulti request to $server failed" );
                                        continue;
                                }
-                               foreach ( $batchResult as $value ) {
-                                       if ( $value === false ) {
-                                               $result = false;
-                                       }
-                               }
+                               $result = $result && !in_array( false, $batchResult, true );
                        } catch ( RedisException $e ) {
                                $this->handleException( $conn, $e );
                                $result = false;
@@ -254,11 +249,7 @@ class RedisBagOStuff extends BagOStuff {
                                        $this->debug( "deleteMulti request to $server failed" );
                                        continue;
                                }
-                               foreach ( $batchResult as $value ) {
-                                       if ( $value === false ) {
-                                               $result = false;
-                                       }
-                               }
+                               $result = $result && !in_array( false, $batchResult, true );
                        } catch ( RedisException $e ) {
                                $this->handleException( $conn, $e );
                                $result = false;
@@ -273,55 +264,86 @@ class RedisBagOStuff extends BagOStuff {
                if ( !$conn ) {
                        return false;
                }
-               $expiry = $this->convertToRelative( $expiry );
+
+               $ttl = $this->convertToRelative( $expiry );
                try {
-                       if ( $expiry ) {
-                               $result = $conn->set(
-                                       $key,
-                                       $this->serialize( $value ),
-                                       [ 'nx', 'ex' => $expiry ]
-                               );
-                       } else {
-                               $result = $conn->setnx( $key, $this->serialize( $value ) );
-                       }
+                       $result = $conn->set(
+                               $key,
+                               $this->serialize( $value ),
+                               $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ]
+                       );
                } catch ( RedisException $e ) {
                        $result = false;
                        $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'add', $key, $server, $result );
+
                return $result;
        }
 
-       /**
-        * Non-atomic implementation of incr().
-        *
-        * Probably all callers actually want incr() to atomically initialise
-        * values to zero if they don't exist, as provided by the Redis INCR
-        * command. But we are constrained by the memcached-like interface to
-        * return null in that case. Once the key exists, further increments are
-        * atomic.
-        * @param string $key Key to increase
-        * @param int $value Value to add to $key (Default 1)
-        * @return int|bool New value or false on failure
-        */
        public function incr( $key, $value = 1 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
                }
+
                try {
-                       if ( !$conn->exists( $key ) ) {
-                               return false;
+                       $conn->watch( $key );
+                       if ( $conn->exists( $key ) ) {
+                               $conn->multi( Redis::MULTI );
+                               $conn->incrBy( $key, $value );
+                               $batchResult = $conn->exec();
+                               if ( $batchResult === false ) {
+                                       $result = false;
+                               } else {
+                                       $result = end( $batchResult );
+                               }
+                       } else {
+                               $result = false;
+                               $conn->unwatch();
                        }
-                       // @FIXME: on races, the key may have a 0 TTL
-                       $result = $conn->incrBy( $key, $value );
                } catch ( RedisException $e ) {
+                       try {
+                               $conn->unwatch(); // sanity
+                       } catch ( RedisException $ex ) {
+                               // already errored
+                       }
                        $result = false;
                        $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'incr', $key, $server, $result );
+
+               return $result;
+       }
+
+       public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+
+               $ttl = $this->convertToRelative( $exptime );
+               $preIncrInit = $init - $value;
+               try {
+                       $conn->multi( Redis::MULTI );
+                       $conn->set( $key, $preIncrInit, $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ] );
+                       $conn->incrBy( $key, $value );
+                       $batchResult = $conn->exec();
+                       if ( $batchResult === false ) {
+                               $result = false;
+                               $this->debug( "incrWithInit request to $server failed" );
+                       } else {
+                               $result = end( $batchResult );
+                       }
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'incr', $key, $server, $result );
+
                return $result;
        }
 
index 9a8086f..b477855 100644 (file)
@@ -23,6 +23,7 @@
 
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 /**
  * Helper class to manage Redis connections.
@@ -81,7 +82,7 @@ class RedisConnectionPool implements LoggerAwareInterface {
                                __CLASS__ . ' requires a Redis client library. ' .
                                'See https://www.mediawiki.org/wiki/Redis#Setup' );
                }
-               $this->logger = $options['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $options['logger'] ?? new NullLogger();
                $this->connectTimeout = $options['connectTimeout'];
                $this->readTimeout = $options['readTimeout'];
                $this->persistent = $options['persistent'];
index af1781a..fdba6fb 100644 (file)
@@ -1588,7 +1588,7 @@ class WikiPage implements Page, IDBAccessObject {
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
                        $lb = $this->getDBLoadBalancer();
-                       $dbr = $lb->getConnection( DB_REPLICA );
+                       $dbr = $lb->getConnectionRef( DB_REPLICA );
                        $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
                        // Try the master if this thread may have just added it.
                        // This could be abstracted into a Revision method, but we don't want
@@ -1597,7 +1597,7 @@ class WikiPage implements Page, IDBAccessObject {
                                && $lb->getServerCount() > 1
                                && $lb->hasOrMadeRecentMasterChanges()
                        ) {
-                               $dbw = $lb->getConnection( DB_MASTER );
+                               $dbw = $lb->getConnectionRef( DB_MASTER );
                                $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
                        }
                        if ( $rev ) {
index a5d351b..7240e81 100644 (file)
@@ -240,7 +240,7 @@ class SearchOracle extends SearchDatabase {
         * @param string $text
         */
        function update( $id, $title, $text ) {
-               $dbw = $this->lb->getConnection( DB_MASTER );
+               $dbw = $this->lb->getMaintenanceConnectionRef( DB_MASTER );
                $dbw->replace( 'searchindex',
                        [ 'si_page' ],
                        [
index 3646b27..dedcdff 100644 (file)
@@ -33,11 +33,15 @@ class SearchSqlite extends SearchDatabase {
         * Whether fulltext search is supported by current schema
         * @return bool
         */
-       function fulltextSearchSupported() {
+       private function fulltextSearchSupported() {
+               // Avoid getConnectionRef() in order to get DatabaseSqlite specifically
                /** @var DatabaseSqlite $dbr */
                $dbr = $this->lb->getConnection( DB_REPLICA );
-
-               return $dbr->checkForEnabledSearch();
+               try {
+                       return $dbr->checkForEnabledSearch();
+               } finally {
+                       $this->lb->reuseConnection( $dbr );
+               }
        }
 
        /**
@@ -285,7 +289,7 @@ class SearchSqlite extends SearchDatabase {
         * @param string $title
         * @param string $text
         */
-       function update( $id, $title, $text ) {
+       public function update( $id, $title, $text ) {
                if ( !$this->fulltextSearchSupported() ) {
                        return;
                }
@@ -308,7 +312,7 @@ class SearchSqlite extends SearchDatabase {
         * @param int $id
         * @param string $title
         */
-       function updateTitle( $id, $title ) {
+       public function updateTitle( $id, $title ) {
                if ( !$this->fulltextSearchSupported() ) {
                        return;
                }
index 4d447d3..64c2b84 100644 (file)
@@ -25,6 +25,7 @@ namespace MediaWiki\Session;
 
 use Psr\Log\LoggerInterface;
 use BagOStuff;
+use Psr\Log\NullLogger;
 
 /**
  * Adapter for PHP's session handling
@@ -299,7 +300,7 @@ class PHPSessionHandler implements \SessionHandlerInterface {
                }
                // Anything deleted in $_SESSION and unchanged in Session should be deleted too
                // (but not if $_SESSION can't represent it at all)
-               \Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
+               \Wikimedia\PhpSessionSerializer::setLogger( new NullLogger() );
                foreach ( $cache as $key => $value ) {
                        if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
                                \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
index bb6a6b3..6076aba 100644 (file)
@@ -75,7 +75,7 @@ class DBSiteStore implements SiteStore {
        protected function loadSites() {
                $this->sites = new SiteList();
 
-               $dbr = $this->dbLoadBalancer->getConnection( DB_REPLICA );
+               $dbr = $this->dbLoadBalancer->getConnectionRef( DB_REPLICA );
 
                $res = $dbr->select(
                        'sites',
@@ -178,7 +178,7 @@ class DBSiteStore implements SiteStore {
                        return true;
                }
 
-               $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
 
@@ -269,7 +269,7 @@ class DBSiteStore implements SiteStore {
         * @return bool Success
         */
        public function clear() {
-               $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
                $ok = $dbw->delete( 'sites', '*', __METHOD__ );
index 1f61cb9..7c2f038 100644 (file)
@@ -2584,7 +2584,7 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $mode === 'refresh' ) {
                        $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
                } else {
-                       $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
+                       $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
                                function () use ( $cache, $key ) {
                                        $cache->delete( $key );
                                },
index dff19ff..fdac4a2 100644 (file)
@@ -256,7 +256,7 @@ class UserGroupMembership {
 
                $lbFactory = $services->getDBLoadBalancerFactory();
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
-               $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
+               $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER );
 
                $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
                $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
index 7882aaa..21c661f 100644 (file)
        "actionfailed": "Дзеяньне ня выкананае",
        "deletedtext": "«$1» была выдаленая.\nЗапісы пра выдаленыя старонкі зьмяшчаюцца ў $2.",
        "dellogpage": "Журнал выдаленьняў",
-       "dellogpagetext": "Сьпіс апошніх выдаленьняў.",
+       "dellogpagetext": "Ð\9dÑ\96жÑ\8dй Ð·Ð½Ð°Ñ\85одзÑ\96Ñ\86Ñ\86а Ñ\81ьпіс апошніх выдаленьняў.",
        "deletionlog": "журнал выдаленьняў",
        "log-name-create": "Журнал стварэньня старонак",
        "log-description-create": "Ніжэй знаходзіцца сьпіс апошніх стварэньняў старонак.",
index f1bbf9b..832c1ca 100644 (file)
        "history": "История",
        "history_short": "История",
        "history_small": "история",
-       "updatedmarker": "пÑ\80оменено Ð¾Ñ\82 Ð¿Ð¾Ñ\81ледноÑ\82о Ð¼и посещение",
+       "updatedmarker": "пÑ\80оменено Ð¾Ñ\82 Ð¿Ð¾Ñ\81ледноÑ\82о Ð\92и посещение",
        "printableversion": "Версия за печат",
        "permalink": "Постоянна препратка",
        "print": "Печат",
        "deleteprotected": "Не можете да изтриете страницата, защото е защитена.",
        "deleting-backlinks-warning": "<strong>Внимание:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Други страници]] сочат към или включват като шаблон страницата, която се опитвате да изтриете.",
        "rollback": "Отмяна на промените",
+       "rollback-confirmation-confirm": "Моля потвърдете:",
+       "rollback-confirmation-yes": "Отмяна",
        "rollback-confirmation-no": "Отказ",
        "rollbacklink": "отмяна",
        "rollbacklinkcount": "отмяна на $1 {{PLURAL:$1|редакция|редакции}}",
        "blocklist-userblocks": "Скриване блокирането на потребителски сметки",
        "blocklist-tempblocks": "Скриване на временни блокирания",
        "blocklist-addressblocks": "Скриване на отделни блокирания на IP адреси",
+       "blocklist-type-opt-sitewide": "За всички уикита",
+       "blocklist-type-opt-partial": "Частично",
        "blocklist-rangeblocks": "Скриване на блокиранията по IP диапазон",
        "blocklist-timestamp": "Дата и час",
        "blocklist-target": "Цел",
        "watchlistedit-clear-titles": "Заглавия:",
        "watchlistedit-clear-submit": "Изчистване на списъка за наблюдение (Необратимо!)",
        "watchlistedit-clear-done": "Списъкът за наблюдение беше изчистен.",
+       "watchlistedit-clear-jobqueue": "Вашият списък за наблюдение се изчиства. Това може да отнеме известно време!",
        "watchlistedit-clear-removed": "{{PLURAL:$1|1 заглавие беше премахнато|$1 заглавия бяха премахнати}}:",
        "watchlistedit-too-many": "Има твърде много страници за показване.",
        "watchlisttools-clear": "Изчистване на списъка за наблюдение",
        "redirect-file": "Име на файл",
        "redirect-logid": "Номер на записа",
        "redirect-not-exists": "Стойността не е намерена",
+       "redirect-not-numeric": "Стойността не е числова",
        "fileduplicatesearch": "Търсене на повтарящи се файлове",
        "fileduplicatesearch-summary": "Търсене на повтарящи се файлове на база хеш стойности.",
        "fileduplicatesearch-filename": "Име на файл:",
        "tag-mw-contentmodelchange": "промяна на модела на съдържание",
        "tag-mw-new-redirect": "Ново пренасочване",
        "tag-mw-removed-redirect": "Премахнато пренасочване",
+       "tag-mw-changed-redirect-target": "Промяна целта на пренасочване",
+       "tag-mw-changed-redirect-target-description": "Редакции, променящи целта на пренасочване",
        "tag-mw-blank": "Изтриване на съдържанието",
        "tag-mw-replace": "Заменено",
+       "tag-mw-replace-description": "Редакции, премахващи над 90% от съдържанието на страница",
        "tag-mw-rollback": "Отмяна",
        "tag-mw-undo": "Отмяна",
        "tags-title": "Етикети",
index 72b1763..b721395 100644 (file)
        "powersearch-togglelabel": "تاوتوێ بکە:",
        "powersearch-toggleall": "ھەموویان",
        "powersearch-togglenone": "ھیچیان",
-       "powersearch-remember": "ھەڵبژاردەکانت بۆ گەڕانەکانی تر لە بیر بێت",
+       "powersearch-remember": "بەبیرھێناوەی ھەڵبژاردەکان بۆ گەڕانەکانی داھاتوو",
        "search-external": "گەڕانی دەرەکی",
        "searchdisabled": "گەڕانی {{SITENAME}} ئێستە کار ناکات.\nدەتوانی بۆ ئێستا لە گەڕانی گووگڵ کەڵک وەرگری.\nلەیادت بێت لەوانەیە پێرستەکانیان بۆ گەڕانی ناو {{SITENAME}}، کات‌بەسەرچوو بێت.",
        "preferences": "ھەڵبژاردەکان",
index 8c1b440..d5beb37 100644 (file)
@@ -99,7 +99,9 @@
        "navigation": "Navigazione",
        "and": "&#32;è",
        "actions": "Azzione",
+       "namespaces": "Spazii",
        "variants": "Variante",
+       "navigation-heading": "Navigazione",
        "errorpagetitle": "Errore",
        "returnto": "Vultà à $1.",
        "tagline": "À prupositu di {{SITENAME}}",
        "specialpage": "Pagina speciale",
        "personaltools": "Strumenti persunali",
        "talk": "Discussione",
+       "views": "Viste sfarenti",
        "toolbox": "Stuvigli",
        "mediawikipage": "Vede i missaghji",
        "templatepage": "Vede a pagina di mudellu",
        "categorypage": "Vede a pagina di categuria",
        "viewtalkpage": "Vede a discussione",
        "otherlanguages": "In altre lingue",
+       "redirectedfrom": "(Reindirizzamentu da $1)",
        "redirectpagesub": "Pagina di reindirizzamentu",
        "redirectto": "Reindirizzamentu à:",
-       "lastmodifiedat": "Ultima mudifica di sta pagina u $1 à e $2.",
+       "lastmodifiedat": "Ùltima mudìfica di sta pàgina u $1 à e $2.",
        "protectedpage": "Pagina prutetta",
        "jumpto": "Andà à:",
        "jumptonavigation": "navigazione",
        "loginlanguagelabel": "Lingua: $1",
        "pt-login": "Cunnessione",
        "pt-login-button": "Cunnessione",
+       "pt-createaccount": "Registramentu",
        "pt-userlogout": "Scunnessione",
        "retypenew": "Scrive torna a nova parulla secreta:",
        "resetpass-submit-cancel": "Cancillà",
        "currentrev": "Ultima revisione",
        "currentrev-asof": "Versione attuale di e $1",
        "revisionasof": "Versione di e $1",
-       "revision-info": "Versione di e $4 à e $5 di $2",
+       "revision-info": "Versione di e $4 à e $5 da {{GENDER:$6|$2}}$7",
        "previousrevision": "← Versione menu ricente",
        "nextrevision": "Versione più nova →",
        "currentrevisionlink": "Ultima revisione",
        "searchprofile-everything": "Tuttu",
        "searchprofile-advanced": "Avanzatu",
        "searchprofile-articles-tooltip": "Circà in $1",
+       "searchprofile-images-tooltip": "Circà schedarii",
        "searchprofile-everything-tooltip": "Circà dapertuttu (incluse e pagine di discussione)",
        "search-result-size": "$1 ({{PLURAL:$2|1 parolla|$2 parolle}})",
        "search-redirect": "(Reindirizzamentu da $1)",
        "hide": "piattà",
        "show": "mustrà",
        "minoreditletter": "m",
+       "newpageletter": "N",
        "boteditletter": "b",
        "rc-enhanced-hide": "Nasconde i dittagli",
        "recentchangeslinked": "Mudifiche assuciate",
        "file-anchor-link": "Schedariu",
        "filehist": "Cronolugia di l'imagine",
        "filehist-deleteone": "supprimà",
+       "filehist-current": "attuale",
+       "filehist-datetime": "Data/Óra",
+       "filehist-thumb": "Previsualizzazione",
+       "filehist-thumbtext": "Previsualizzazione di a versione di",
        "filehist-user": "Cuntributore",
        "filehist-dimensions": "Dimensione",
        "filehist-comment": "Cummentu",
        "watchlistfor2": "Per $1 ($2)",
        "watch": "Suvità",
        "unwatch": "Ùn suvità micca",
-       "wlshowlast": "Mustrà l'ultime $1 ore $2 ghjorni",
+       "wlshowlast": "Mustrà l'ùltime $1 ore $2 ghjorni",
        "enotif_reset": "Marcà tutte e pagine visitate",
        "created": "creatu",
        "changed": "cambiatu",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|revisione|revisione}}",
        "tooltip-pt-userpage": "{{GENDER:|A to}} pàgina di cuntributore",
        "tooltip-pt-mytalk": "{{GENDER:|A to}} pàgina di discussione",
-       "tooltip-pt-preferences": "{{GENDER:|E to}}} preferenze",
+       "tooltip-pt-preferences": "{{GENDER:|E to}} preferenze",
        "tooltip-pt-watchlist": "Lista di e pagine ch'è tù suviti",
        "tooltip-pt-mycontris": "Lista di {{GENDER:|e to}} cuntribuzioni",
        "tooltip-pt-login": "U registramentu hè suggeritu, micca ubligatoriu",
        "tooltip-pt-logout": "Esce da a sessione",
        "tooltip-ca-talk": "Vede e discussione relative à sta pagina",
-       "tooltip-ca-edit": "Pò mudificà 'ssa pagina. Per piacè improda l'ozzione di previsualisazzione prima di salvà",
+       "tooltip-ca-edit": "Mudificà 'ssa pagina",
        "tooltip-ca-addsection": "Cumincià una nova sezzione",
        "tooltip-ca-viewsource": "Sta pagina hè prutetta, ma si pò vede u so codice surghjente",
        "tooltip-ca-history": "Versione precedente di sta pagina",
        "tooltip-t-whatlinkshere": "Listinu di tutte e pagine chì sò ligate à quessa",
        "tooltip-t-recentchangeslinked": "Versione di l'ultime mudifiche à e pagine legate à quessa",
        "tooltip-t-contributions": "Listinu di e mudifiche {{GENDER:$1|di 'ssu cuntributore}}",
+       "tooltip-t-upload": "Incaricà un schedariu",
        "tooltip-t-specialpages": "Listinu di tutte e pagine spiciale",
        "tooltip-t-print": "Versione stampevule di 'ssa pagina",
        "tooltip-t-permalink": "Ligame permanente à e revisione di sta pagina",
        "tooltip-ca-nstab-user": "Vede a pagina di cuntributore",
        "tooltip-ca-nstab-special": "Questa hè una pàgina particulare chi ùn si pó micca esse mudificata",
        "tooltip-ca-nstab-project": "Vede a pagina di u prugettu",
+       "tooltip-ca-nstab-image": "Vede pàgina di schedariu",
        "tooltip-ca-nstab-template": "Vede u mudellu",
        "tooltip-ca-nstab-category": "Vede a pagina di categuria",
        "tooltip-minoredit": "Signalà com'è mudifica minore",
        "noimages": "Nulla da vede.",
        "ilsubmit": "Ricerca",
        "bydate": "per data",
+       "namespacesall": "tutti",
        "monthsall": "tutti",
        "confirm_purge_button": "D'accordu",
        "table_pager_next": "Pagina seguente",
        "logentry-move-move": "$1 {{GENDER:$2|hà spustatu}} a pagina $3 à $4",
        "logentry-newusers-create": "U participante $3 hè statu creatu da $1",
        "rightsnone": "(nessunu)",
-       "searchsuggest-search": "Ricerca",
+       "searchsuggest-search": "Circà in {{SITENAME}}",
        "expand_templates_output": "Risultatu"
 }
index e7ae281..0d88510 100644 (file)
@@ -74,7 +74,8 @@
                        "Weblars",
                        "Kranix",
                        "Psl85",
-                       "Dipsacus fullonum"
+                       "Dipsacus fullonum",
+                       "Fugithora"
                ]
        },
        "tog-underline": "Understreg link:",
        "history": "Sidehistorik",
        "history_short": "Historik",
        "history_small": "historik",
-       "updatedmarker": "opdateret siden seneste besøg",
+       "updatedmarker": "opdateret siden dit seneste besøg",
        "printableversion": "Udskriftsvenlig udgave",
        "permalink": "Permanent link",
        "print": "Udskriv",
        "virus-scanfailed": "scan fejlede (fejlkode $1)",
        "virus-unknownscanner": "ukendt antivirus:",
        "logouttext": "<strong>Du er nu logget af.</strong>\n\nBemærk, at nogle sider stadigvæk kan vises som om du var logget på, indtil du tømmer din browsers cache.",
+       "logging-out-notify": "Du bliver logget ud, vent venligst.",
+       "logout-failed": "Kan ikke logge ud nu: $1",
        "cannotlogoutnow-title": "Kan ikke logge af på nuværende tidspunkt",
        "cannotlogoutnow-text": "Det er ikke muligt at logge af når du bruger $1.",
        "welcomeuser": "Velkommen, $1!",
index ccd9218..88bf510 100644 (file)
@@ -21,7 +21,8 @@
                        "Macofe",
                        "Matma Rex",
                        "Fitoschido",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "J budissin"
                ]
        },
        "tog-underline": "Wótkaze pódšmarnuś:",
        "nstab-template": "Pśedłoga",
        "nstab-help": "Pomoc",
        "nstab-category": "Kategorija",
+       "mainpage-nstab": "Głowny bok",
        "nosuchaction": "Toś tu akciju njedajo",
        "nosuchactiontext": "Akcija, kótaruž URL pódawa, jo njepłaśiwa.\nSy se snaź zapisał pśi zapódaśu URL abo sy slědował wopacnemu wótkazoju.\nTo by mógło teke programěrowańska zmólka w {{GRAMMAR:lokatiw|{{SITENAME}}}} byś.",
        "nosuchspecialpage": "Toś ten specialny bok njeeksistěrujo",
index 491c771..5b522e2 100644 (file)
        "currentevents": "אקטואליה",
        "currentevents-url": "Project:אקטואליה",
        "disclaimers": "הבהרות משפטיות",
-       "disclaimerpage": "Project:×\94×\91×\94ר×\94 ×\9eשפ×\98×\99ת",
+       "disclaimerpage": "Project:×\94×\91×\94ר×\95ת ×\9eשפ×\98×\99×\95ת",
        "edithelp": "עזרה בעריכה",
        "helppage-top-gethelp": "עזרה",
        "mainpage": "עמוד ראשי",
        "pt-createaccount": "יצירת חשבון",
        "pt-userlogout": "יציאה מהחשבון",
        "php-mail-error-unknown": "שגיאה לא ידועה בפונקציה mail()‎ של PHP.",
-       "user-mail-no-addy": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93×¢×\94 ×\9c×\9c×\90 ×\9bת×\95×\91ת ×\93×\95×\90×´ל.",
-       "user-mail-no-body": "× ×\99ס×\99×\95×\9f ×\9cש×\9c×\95×\97 דוא\"ל עם תוכן ריק או קצר מאוד.",
+       "user-mail-no-addy": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93עת ×\93×\95×\90\"×\9c ×\9c×\9c×\90 ×\9bת×\95×\91ת ×\93×\95×\90\"ל.",
+       "user-mail-no-body": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93עת דוא\"ל עם תוכן ריק או קצר מאוד.",
        "changepassword": "שינוי סיסמה",
        "resetpass_announce": "כדי לסיים את הכניסה לחשבון, יש להגדיר סיסמה חדשה.",
        "resetpass_text": "<!-- יש להוסיף טקסט כאן -->",
        "autoblockedtext": "כתובת ה־IP שלך נחסמה באופן אוטומטי כיוון שמשתמש אחר, שנחסם על־ידי $1, השתמש בה.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nבאפשרותך ליצור קשר עם $1 או עם כל אחד מ[[{{MediaWiki:Grouppage-sysop}}|מפעילי המערכת]] האחרים כדי לדון בחסימה.\n\nכמו־כן, באפשרותך להשתמש בתכונת \"{{int:emailuser}}\", אלא אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\n\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "systemblockedtext": "שם המשתמש או כתובת ה־IP שלך נחסמו באופן אוטומטי על־ידי תוכנת מדיה־ויקי.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nכתובת ה־IP הנוכחית שלך היא $3.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "blockednoreason": "לא ניתנה סיבה",
-       "blockedtext-composite": "<strong>ש×\9d ×\94×\9eשת×\9eש ×\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9b×\9d × ×\97ס×\9e×\95 ×\9eער×\99×\9b×\94.</strong>\n\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\94×\99×\90:\n\n:<em>$2</em>.\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94 ×\94×\90ר×\95×\9b×\94 ×\91×\99×\95תר: $6\n\n×\9bת×\95×\91ת ×\94Ö¾IP ×\94× ×\95×\9b×\97×\99ת ×©×\9c×\9a ×\94×\99×\90 $3.\n×\99ש ×\9cספק ×\90ת ×\9b×\9c ×\94×\9e×\99×\93×¢ ×\94× \"×\9c ×¢×\91×\95ר ×\9b×\9c ×\94ש×\90×\99×\9cת×\95ת ×©×\90ת×\9d ×\9e×\91צע×\99×\9d.",
-       "blockedtext-composite-reason": "×\99שנ×\9f ×\9eספר ×\97ס×\99×\9e×\95ת ×¢×\9c ×\94×\97ש×\91×\95×\9f ×©×\9c×\9a ×\95\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a",
+       "blockedtext-composite": "<strong>ש×\9d ×\94×\9eשת×\9eש ×\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\95.</strong>\n\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\9b×\9a ×\94×\99×\90:\n\n:<em>$2</em>.\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94 ×\94×\90ר×\95×\9b×\94 ×\91×\99×\95תר: $6\n\n×\9bת×\95×\91ת ×\94Ö¾IP ×\94× ×\95×\9b×\97×\99ת ×©×\9c×\9a ×\94×\99×\90 $3.\n×\99ש ×\9cצ×\99×\99×\9f ×\90ת ×\9b×\9c ×\94פר×\98×\99×\9d ×\94×\9c×\9c×\95 ×\91×\9b×\9c ×¤× ×\99×\99×\94 ×\9c×\91×\99ר×\95ר ×\94×\97ס×\99×\9e×\94.",
+       "blockedtext-composite-reason": "×\94×\95פע×\9c×\95 ×\9eספר ×\97ס×\99×\9e×\95ת ×¢×\9c ×\97ש×\91×\95×\9f ×\94×\9eשת×\9eש ×©×\9c×\9a ×\90×\95 ×¢×\9c ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a (×\90×\95 ×¢×\9c ×©× ×\99×\94×\9d)",
        "whitelistedittext": "נדרשת $1 כדי לערוך דפים.",
        "confirmedittext": "יש לאמת את כתובת הדוא\"ל לפני עריכת דפים.\nנא להגדיר ולאמת את כתובת הדוא\"ל שלך באמצעות [[Special:Preferences|העדפות המשתמש]] שלך.",
        "nosuchsectiontitle": "הפסקה לא נמצאה",
        "gender-male": "הוא עורך דפים בוויקי",
        "gender-female": "היא עורכת דפים בוויקי",
        "prefs-help-gender": "לא חובה למלא העדפה זו.\nהמערכת משתמשת במידע הזה כדי לפנות אליך/אלייך ולציין את שם המשתמש שלך במין הדקדוקי הנכון.\nהמידע יהיה ציבורי.",
-       "email": "דוא״ל",
+       "email": "דוא\"ל",
        "prefs-help-realname": "לא חובה למלא את השם האמיתי.\nאם סופק, הוא עשוי לשמש כדי לייחס לך את עבודתך.",
        "prefs-help-email": "כתובת דואר אלקטרוני היא אופציונלית, אבל היא חיונית לאיפוס הסיסמה במקרה ש{{GENDER:|תשכח|תשכחי}} אותה.",
        "prefs-help-email-others": "באפשרותך גם לאפשר למשתמשים ליצור איתך קשר באמצעות דוא\"ל דרך קישור בדף המשתמש או בדף השיחה שלך.\nכתובת הדוא\"ל שלך לא תיחשף כשמשתמשים יצרו איתך קשר.",
        "mw-widgets-abandonedit-discard": "ביטול העריכות",
        "mw-widgets-abandonedit-keep": "המשך עריכה",
        "mw-widgets-abandonedit-title": "בטוח?",
-       "mw-widgets-copytextlayout-copy": "העתק",
+       "mw-widgets-copytextlayout-copy": "העתקה",
        "mw-widgets-copytextlayout-copy-fail": "ההעתקה ללוח נכשלה.",
        "mw-widgets-copytextlayout-copy-success": "הועתק ללוח.",
        "mw-widgets-dateinput-no-date": "לא נבחר תאריך",
        "edit-error-short": "שגיאה: $1",
        "edit-error-long": "שגיאות:\n\n$1",
        "specialmute": "השתקה",
-       "specialmute-success": "העדפות ההשתקה שלך עודכנו. ר' את כל המשתמשים המושתקים ב[[Special:Preferences|העדפות שלך]].",
+       "specialmute-success": "העדפות ההשתקה שלך עודכנו. רשימת כל המשתמשים המושתקים זמינה ב[[Special:Preferences|העדפות שלך]].",
        "specialmute-submit": "אישור",
-       "specialmute-label-mute-email": "×\9c×\94שת×\99ק דואר אלקטרוני מהמשתמש הזה",
-       "specialmute-header": "× ×\90 ×\9c×\91×\97×\95ר ×\90ת העדפות ההשתקה שלך עבור {{BIDI:[[User:$1]]}}.",
+       "specialmute-label-mute-email": "×\94שתקת ×\94×\95×\93×¢×\95ת דואר אלקטרוני מהמשתמש הזה",
+       "specialmute-header": "×\91×\97×\99ר×\95ת העדפות ההשתקה שלך עבור {{BIDI:[[User:$1]]}}.",
        "specialmute-error-invalid-user": "שם המשתמש המבוקש לא נמצא.",
-       "specialmute-error-email-blacklist-disabled": "השתקת משתמשים משליחת דואר אלקטרוני אליך אינה מופעלת.",
+       "specialmute-error-email-blacklist-disabled": "×\94×\90פשר×\95ת ×\9c×\94שתקת ×\9eשת×\9eש×\99×\9d ×\9eש×\9c×\99×\97ת ×\93×\95×\90ר ×\90×\9cק×\98ר×\95× ×\99 ×\90×\9c×\99×\9a ×\90×\99× ×\94 ×\9e×\95פע×\9cת.",
        "specialmute-error-email-preferences": "יש לאמת את כתובת הדואר האלקטרוני שלך לפני שתהיה לך אפשרות להשתיק משתמש. אפשר לעשות זאת מהדף [[Special:Preferences]].",
-       "specialmute-email-footer": "×\9b×\93×\99 ×\9c× ×\94×\9c ×\90ת ×\94×\94×¢×\93פ×\95ת ×¢×\91×\95ר {{BIDI:$2}} × ×\90 לבקר בדף <$1>.",
-       "specialmute-login-required": "× ×\90 ×\9c×\94×\99×\9bנס ×\9c×\97ש×\91×\95×\9f ×\9b×\93×\99 ×\9cש×\91ות את העדפות ההשתקה שלך.",
+       "specialmute-email-footer": "×\9b×\93×\99 ×\9c× ×\94×\9c ×\90ת ×\94×¢×\93פ×\95ת ×§×\91×\9cת ×\94×\93×\95×\90ר ×\94×\90×\9cק×\98ר×\95× ×\99 ×©× ×©×\9c×\97 ×¢×\9cÖ¾×\99×\93×\99 {{BIDI:$2}}, ×\91×\90פשר×\95ת×\9a לבקר בדף <$1>.",
+       "specialmute-login-required": "× ×\93רשת ×\9b× ×\99ס×\94 ×\9c×\97ש×\91×\95×\9f ×\9b×\93×\99 ×\9cשנות את העדפות ההשתקה שלך.",
        "revid": "גרסה $1",
        "pageid": "מזהה דף $1",
        "interfaceadmin-info": "$1\n\nההרשאות לעריכת קובצי CSS/JS/JSON של האתר כולו הופרדו לאחרונה מההרשאה <code>editinterface</code>. אם לא ברור לך מדוע קיבלת את הודעת השגיאה הזאת, ר' [[mw:MediaWiki_1.32/interface-admin]].",
index 7879fce..7764a21 100644 (file)
        "logentry-partialblock-block-ns": "{{PLURAL:$1|名前空間}} $2",
        "logentry-partialblock-block": "$1 が {{GENDER:$4|$3}} に対して $7 からの編集を $5 {{GENDER:$2||ブロックしました}} $6",
        "logentry-partialblock-reblock": "$1 が {{GENDER:$4|$3}} に対する $7 のブロックの期限を $5 に{{GENDER:$2|変更しました}} $6",
+       "logentry-non-editing-block-block": "$1 が {{GENDER:$4|$3}} に対して編集以外の処理を $5 $6 で{{GENDER:$2||ブロックしました}}",
+       "logentry-non-editing-block-reblock": "$1 が {{GENDER:$4|$3}} に対する特定の編集以外の処理のブロックの期限を $5 $6 に{{GENDER:$2|変更しました}}",
        "logentry-suppress-block": "$1 が {{GENDER:$4|$3}} を$5で{{GENDER:$2|ブロックしました}} $6",
        "logentry-suppress-reblock": "$1 が {{GENDER:$4|$3}} のブロックの期限を$5に{{GENDER:$2|変更しました}} $6",
        "logentry-import-upload": "$1 がファイルをアップロードして $3 を{{GENDER:$2|インポートしました}}",
        "edit-error-short": "エラー: $1",
        "edit-error-long": "エラー:\n\n\n\n$1",
        "specialmute": "ミュート",
+       "specialmute-success": "ミュートの個人設定が更新されました。[[Special:Preferences|ご自分の個人設定ページ]]でミューとした利用者の一覧を確認できます。",
+       "specialmute-submit": "確定",
        "specialmute-label-mute-email": "この利用者からのウィキメールをミュートする",
+       "specialmute-header": "{{BIDI:[[User:$1]]}}さんに対するミュートを個人設定で選択してください。",
        "specialmute-error-invalid-user": "あなたが要求した利用者名は見つかりませんでした。",
+       "specialmute-error-email-blacklist-disabled": "利用者からメールを受け取らないようにするミュートは設定されていません。",
+       "specialmute-error-email-preferences": "発信者をミューとする準備として、ご自分のeメールアドレスの認証が必要です。手続きは[[Special:Preferences|個人設定]]のページで行います。",
+       "specialmute-email-footer": "{{BIDI:$2}}のeメール発信者の個人設定を変更するには<$1>を開いてください。",
+       "specialmute-login-required": "ミュートの個人設定を変更するにはログインしてください。",
        "revid": "版 $1",
        "pageid": "ページID $1",
        "interfaceadmin-info": "$1\n\nサイト全体のCSS/JavaScriptの編集権限は、最近<code>editinterface</code> 権限から分離されました。なぜこのエラーが表示されたのかわからない場合は、[[mw:MediaWiki_1.32/interface-admin]]をご覧ください。",
index da07dd6..3a4ba28 100644 (file)
@@ -76,7 +76,8 @@
                        "Delim",
                        "Comjun04",
                        "Son77391",
-                       "Jango"
+                       "Jango",
+                       "D6283"
                ]
        },
        "tog-underline": "링크에 밑줄 긋기:",
        "logentry-block-block": "$1님이 {{GENDER:$4|$3}}님을 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-block-unblock": "$1님이 {{GENDER:$4|$3}}님의 {{GENDER:$2|차단을 해제했습니다}}",
        "logentry-block-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
-       "logentry-partialblock-block": "$1님이 {{GENDER:$4|$3}}님을 $7 {{PLURAL:$8|문서를|문서들을}} 편집하지 못하도록 $5 {{GENDER:$2|차단}}했습니다. $6",
+       "logentry-partialblock-block": "$1님이 {{GENDER:$4|$3}}님을 $7 편집하지 못하도록 $5 {{GENDER:$2|차단}}했습니다. $6",
        "logentry-suppress-block": "$1님이 {{GENDER:$4|$3}} 사용자를 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-suppress-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
        "logentry-import-upload": "$1님이 $3 문서를 파일 올리기로 {{GENDER:$2|가져왔습니다}}",
        "edit-error-short": "오류: $1",
        "edit-error-long": "오류:\n\n$1",
        "specialmute": "알림 미표시",
-       "specialmute-success": "ì\95\8c림 ë¯¸í\91\9cì\8b\9c í\99\98ê²½ ì\84¤ì \95ì\9d´ ì\84±ê³µì \81ì\9c¼ë¡\9c ì\97\85ë\8d°ì\9d´í\8a¸ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤. [[Special:Preferences]]에서 알림이 표시되지 않는 모든 사용자를 확인하십시오.",
+       "specialmute-success": "ì\95\8c림 ë¯¸í\91\9cì\8b\9c í\99\98ê²½ ì\84¤ì \95ì\9d´ ì\97\85ë\8d°ì\9d´í\8a¸ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤. [[Special:Preferences|í\99\98ê²½ ì\84¤ì \95]]에서 알림이 표시되지 않는 모든 사용자를 확인하십시오.",
        "specialmute-submit": "확인",
        "specialmute-label-mute-email": "이 사용자의 이메일 알림을 표시하지 않습니다",
        "specialmute-header": "{{BIDI:[[User:$1]]}}의 알림 미표시 환경 설정을 선택해 주십시오.",
index de47550..c29f5d6 100644 (file)
        "versionrequired": "Dibutuahan MediaWiki versi $1",
        "versionrequiredtext": "MediaWiki versi $1 dibutuahan untuak manggunoan laman ko. Caliak [[Special:Version|versi laman]]",
        "ok": "OK",
-       "pagetitle": "$1 - {{SITENAME}} bahaso Minang",
+       "pagetitle": "$1 - {{SITENAME}} Minangkabau",
        "pagetitle-view-mainpage": "{{SITENAME}} bahaso Minang",
        "backlinksubtitle": "← $1",
        "retrievedfrom": "Didapek dari \"$1\"",
index 75726e2..0ac7b3d 100644 (file)
        "minoredit": "Chisto è nu cagnamiénto piccerillo",
        "watchthis": "Tiene d'uocchio sta paggena",
        "savearticle": "Sarva 'a paggena",
-       "savechanges": "Sarva 'e cagnamiénte",
+       "savechanges": "Sarva",
        "publishpage": "Pubbreca paggena",
        "publishchanges": "Pubbreca 'e cagnamiente",
        "savearticle-start": "Sarva 'a paggena...",
index b658cd0..a011006 100644 (file)
        "search-filter-title-prefix": "ߞߐߜߍ ߡߍ߲ ߠߎ߬ ߞߎ߲߬ߕߐ߮ ߦߋ߫ ߘߊߡߌ߬ߣߊ߬ ߟߊ߫  \"$1\" ߡߊ߬ ߏ߬ ߟߎ߫ ߟߋ߬ ߘߐߙߐ߲߫ ߢߌߣߌ߲ ߦߴߌ ߘߐ߫.",
        "search-filter-title-prefix-reset": "ߞߐߜߍ ߓߍ߯ ߢߌߣߌ߲߫",
        "searchresults-title": "ߣߌ߲߬ \"$1\" ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ",
+       "titlematches": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߢߐ߲߰ߡߊ߬ߣߍ߲߫",
        "prevn": "ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ {{PLURAL:$1|$1}}",
        "nextn": "ߟߊߕߎ߲߰ߠߊ {{PLURAL:$1|$1}}",
        "prev-page": "ߞߐߜߍ ߢߍߕߊ",
        "right-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "right-viewmywatchlist": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫",
        "right-editmyoptions": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߟߊߝߌߛߦߊߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-import": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߥߞߌ ߕߐ߭ ߟߎ߬ ߘߐ߫",
+       "right-importupload": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬ ߘߐ߫",
+       "right-patrol": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߣߍ߲ ߠߎ߬ ߣߐ߬ߣߐ߬.",
+       "right-autopatrol": "ߒ ߖߍ߬ߘߍ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߣߍ߲ ߠߎ߬ ߞߍ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߣߍ߲ ߘߌ߫ ߞߍ߲ߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬",
+       "right-patrolmarks": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬ ߦߋ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߠߌ߲߫ ߣߐ߬ߣߐ߬ߣߍ߲ ߘߌ߫",
        "right-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫",
        "right-mergehistory": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
        "right-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫",
        "rcfilters-filterlist-feedbacklink": "ߌ ߤߊߞߟߌߣߊ߲ ߝߐ߫ ߊ߲ ߧߋ߫ ߞߊ߬ ߓߍ߲߬ ߛߍ߲ߛߍ߲ߟߊ߲ ߖߐ߯ߙߊ߲ ߠߊ߫ ߞߏ ߡߊ߬.",
        "rcfilters-highlightbutton-title": "ߞߐߝߟߌ߫ ߡߊߦߋߙߋ߲ߣߍ߲ ߠߎ߬",
        "rcfilters-highlightmenu-title": "ߞߐ߬ߟߐ ߘߏ߫ ߓߊߓߌ߬ߟߊ߬",
+       "rcfilters-filterlist-noresults": "ߛߍ߲ߛߍ߲ߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬",
        "rcfilters-filter-editsbyself-label": "ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ߣߍ߲߬ ߌ ߓߟߏ߫",
        "rcfilters-filter-editsbyself-description": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲.",
        "rcfilters-filter-editsbyother-label": "ߘߏ ߟߎ߬ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬",
        "filedelete-reason-dropdown": "* ߖߏ߰ߛߌ߬ߟߌ ߟߎ߬ ߝߊ߲߬ߓߊ ߞߎ߲߭\n** ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߕߌߢߍߟߌ\n** ߞߐߕߐ߯ ߓߊߟߌߣߍ߲ ߠߎ߬",
        "filedelete-edit-reasonlist": "ߖߏ߰ߛߌ߬ߟߌ ߞߎ߲߭ ߡߊߦߟߍ߬ߡߊ߲߫",
        "filedelete-maintenance-title": "ߞߐߕߐ߮ ߕߍ߫ ߛߐ߲߬ ߖߏ߰ߛߌ߬ ߟߊ߫",
+       "mimetype": "MIME ߛߎ߮ߦߊ:",
+       "download": "ߟߊ߬ߖߌ߰ߒ߬ߞߎ߲߬ߠߌ߲",
+       "unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬",
+       "listredirects": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߛߙߍߘߍ ߟߎ߬",
+       "listduplicatedfiles": "ߞߐߕߐ߯ ߓߊߟߌߣߍ߲ ߠߎ߬ ߛߙߍߘߍ",
+       "listduplicatedfiles-entry": "[[:File:$1|$1]] ߓߘߊ߫ [[$3|{{PLURAL:$2|ߓߊߟߌ߫|ߟߎ߬ ߓߊߟߌߣߍ߲߫}}]]",
+       "unusedtemplates": "ߞߙߊߞߏ߫ ߟߊߓߊ߯ߙߊߓߊߟߌ ߟߎ߬",
        "unusedtemplateswlh": "ߛߘߌ߬ߜߋ߲ ߜߘߍ ߟߎ߬",
        "randompage": "ߞߎ߲߬ߝߍ߬ ߞߐߜߍ",
+       "randompage-nopages": "ߞߐߕߐ߯ ߛߌ߫ ߕߍ߫ ߢߌ߲߬ ߠߎ߬ ߘߐ߫ \n{{PLURAL:$2|ߕߐ߯ߛߓߍ ߞߣߍ|ߕߐ߯ߛߓߍ߫ ߞߣߍ ߟߎ߬}}: $1.",
        "randomincategory": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ ߦߌߟߡߊ ߘߐ߫",
+       "randomincategory-invalidcategory": "$1 ߕߍ߫ ߦߌߟߡߊ߫ ߕߐ߯ ߓߍ߲߬ߣߍ߲߬ ߘߌ߫.",
        "randomincategory-nopages": "ߞߐߜߍ߫ ߛߌ߫ ߕߍ߫  [[:Category:$1|$1]] ߘߌ߫ ߦߌߟߡߊ",
        "randomincategory-category": "ߦߌߟߡߊ",
        "randomincategory-legend": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ ߦߌߟߡߊ ߘߐ߫",
        "statistics-pages": "ߞߐߜߍ ߟߎ߬",
        "statistics-pages-desc": "ߞߐߜߍ ߡߍ߲ ߓߍ߯ ߦߋ߫ ߥߞߌ ߞߊ߲߬߸ ߦߏ߫ ߞߎߡߊߢߐ߲߯ߦߊ߫ ߞߐߜߍ߸ ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲߸ ߊ߬ ߣߌ߫.",
        "statistics-files": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬",
+       "statistics-edits-average": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߎ߰ߡߍ ߡߍ߲ ߞߍߣߍ߲߫ ߞߐߜߍ ߡߊ߬",
+       "statistics-users": "ߟߊߓߊ߯ߙߊߓߊ߯ ߛߙߍߘߍߦߊߣߍ߲ ߠߎ߬",
+       "statistics-users-active-desc": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߡߍ߲ ߠߎ߬ ߝߊߘߌ߲ߧߊ߫ ߘߊ߫ ߞߏ߫ ߘߏ߫ ߞߍ {{PLURAL:$1|ߕߟߋ߬|$1 ߕߋ߬ߟߋ}} ߟߎ߬ ߞߘߐ߫.",
        "pageswithprop-submit": "ߕߊ߯",
        "double-redirect-fixer": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߘߐߓߍ߲߬ߟߊ߲",
        "brokenredirects-edit": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "apisandbox-dynamic-parameters-add-label": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊߘߏ߲߬",
        "apisandbox-dynamic-parameters-add-placeholder": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߕߐ߮",
        "apisandbox-dynamic-error-exists": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߕߐ߮  \"$1\" ߦߋ߫ ߦߋ߲߬ ߞߘߐ߬ߡߊ߲߬.",
+       "apisandbox-fetch-token": "ߖߐߟߐ߲ߞߐ ߞߍߒߖߘߍߦߋ߫ ߟߝߊߟߌ",
        "apisandbox-add-multi": "ߟߊ߬ߘߏ߲߬ߠߌ߲",
        "apisandbox-results": "ߞߐߖߋߓߌ ߟߎ߬",
        "apisandbox-sending-request": "API ߡߊ߬ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߗߋߟߌ ߦߴߌ ߘߐ߫...",
        "allpagessubmit": "ߥߊ߫",
        "allpages-hide-redirects": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߢߡߊߘߏ߲߰",
        "categories": "ߦߌߟߡߊ ߟߎ߬",
+       "categoriesfrom": "ߦߌߟߡߊ ߟߎ߬ ߦߌ߬ߘߊ߬ߟߌ ߟߊߝߟߐ߫ ߣߌ߲߬ ߡߊ߬:",
+       "deletedcontributions": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߡߊߜߍ߲ ߠߎ߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬",
+       "deletedcontributions-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߡߊߜߍ߲ ߓߘߊ߫ ߓߊ߲߫ ߖߏ߬ߛߌ߬ ߟߊ߫",
+       "sp-deletedcontributions-contribs": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߠߎ߬",
        "linksearch": "ߞߐߞߊ߲ߠߊ ߛߘߌ߬ߜߋ߲ ߢߌߣߌ߲ߠߌ߲",
+       "linksearch-ns": "ߕߐ߯ߛߓߍ ߞߣߍ:",
+       "linksearch-ok": "ߢߌߣߌ߲ߠߌ߲",
+       "linksearch-line": "$1 ߦߋ߫ ߛߘߌ߬ߜߋ߲ ߠߋ߬ ߘߌ߫ ߞߊ߬ ߓߐ߫ $2",
+       "listusersfrom": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߦߌ߬ߘߊ߬ߟߌ ߟߊߝߟߐ߫ ߣߌ߲߬ ߡߊ߬:",
+       "listusers-submit": "ߦߌ߬ߘߊ߬ߟߌ",
+       "listusers-noresult": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߕߴߦߋ߲߬",
        "activeusers-noresult": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߕߴߦߋ߲߬",
        "listgrouprights-members": "(ߛߌ߲߬ߝߏ߲ ߠߎ߫ ߛߙߍߘߍ)",
        "emailuser": "ߗߋߛߓߍ ߗߋ߫ ߣߌ߲߬ ߕߌ߭ ߡߊ߬",
index 448eec8..00b8d18 100644 (file)
@@ -2537,35 +2537,42 @@ class OutputPageTest extends MediaWikiTestCase {
                $rl = $out->getResourceLoader();
                $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
                $rl->register( [
-                       'test.foo' => new ResourceLoaderTestModule( [
+                       'test.foo' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.foo( { a: true } );',
                                'styles' => '.mw-test-foo { content: "style"; }',
-                       ] ),
-                       'test.bar' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.bar' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.bar( { a: true } );',
                                'styles' => '.mw-test-bar { content: "style"; }',
-                       ] ),
-                       'test.baz' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.baz' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.baz( { a: true } );',
                                'styles' => '.mw-test-baz { content: "style"; }',
-                       ] ),
-                       'test.quux' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.quux' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.baz( { token: 123 } );',
                                'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
                                'group' => 'private',
-                       ] ),
-                       'test.noscript' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.noscript' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'styles' => '.stuff { color: red; }',
                                'group' => 'noscript',
-                       ] ),
-                       'test.group.foo' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.group.foo' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.doStuff( "foo" );',
                                'group' => 'foo',
-                       ] ),
-                       'test.group.bar' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.group.bar' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.doStuff( "bar" );',
                                'group' => 'bar',
-                       ] ),
+                       ],
                ] );
                $links = $method->invokeArgs( $out, $args );
                $actualHtml = strval( $links );
@@ -2648,17 +2655,16 @@ class OutputPageTest extends MediaWikiTestCase {
                        ->setConstructorArgs( [ $ctx ] )
                        ->setMethods( [ 'buildCssLinksArray' ] )
                        ->getMock();
-               $op->expects( $this->any() )
-                       ->method( 'buildCssLinksArray' )
+               $op->method( 'buildCssLinksArray' )
                        ->willReturn( [] );
                $rl = $op->getResourceLoader();
                $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
 
                // Register custom modules
                $rl->register( [
-                       'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
-                       'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
-                       'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
+                       'example.site.a' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
+                       'example.site.b' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
+                       'example.user' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'user' ],
                ] );
 
                $op = TestingAccessWrapper::newFromObject( $op );
index d0a95c2..913f56d 100644 (file)
@@ -554,6 +554,10 @@ class TitleTest extends MediaWikiTestCase {
                        # Title, expected base, optional message
                        [ 'User:John_Doe/subOne/subTwo', 'John Doe' ],
                        [ 'User:Foo / Bar / Baz', 'Foo ' ],
+                       [ 'Talk:////', '////' ],
+                       [ 'Template:////', '////' ],
+                       [ 'Template:Foo////', 'Foo' ],
+                       [ 'Template:Foo////Bar', 'Foo' ],
                ];
        }
 
index 6850a24..6fe9218 100644 (file)
@@ -236,7 +236,7 @@ class WikiMapTest extends MediaWikiLangTestCase {
                $this->assertEquals( $wiki, WikiMap::getWikiFromUrl( $url ) );
        }
 
-       public function provideGetWikiIdFromDomain() {
+       public function provideGetWikiIdFromDbDomain() {
                return [
                        [ 'db-prefix_', 'db-prefix_' ],
                        [ wfWikiID(), wfWikiID() ],
@@ -249,10 +249,10 @@ class WikiMapTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @dataProvider provideGetWikiIdFromDomain
+        * @dataProvider provideGetWikiIdFromDbDomain
         * @covers WikiMap::getWikiIdFromDbDomain()
         */
-       public function testGetWikiIdFromDomain( $domain, $wikiId ) {
+       public function testGetWikiIdFromDbDomain( $domain, $wikiId ) {
                $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDbDomain( $domain ) );
        }
 
index 83e9a47..ccfcc18 100644 (file)
@@ -42,11 +42,13 @@ class SiteStatsUpdateTest extends MediaWikiTestCase {
                $fi = SiteStats::images();
                $ai = SiteStats::articles();
 
+               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
+
                $dbw->begin( __METHOD__ ); // block opportunistic updates
 
-               $update = SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] );
-               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
-               $update->doUpdate();
+               DeferredUpdates::addUpdate(
+                       SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] )
+               );
                $this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() );
 
                // Still the same
index 09bcfc9..ef499a1 100644 (file)
@@ -7,20 +7,6 @@
  */
 class HttpTest extends MediaWikiTestCase {
 
-       /**
-        * Test Http::isValidURI()
-        * T29854 : Http::isValidURI is too lax
-        * @dataProvider provideURI
-        * @covers Http::isValidURI
-        */
-       public function testIsValidUri( $expect, $URI, $message = '' ) {
-               $this->assertEquals(
-                       $expect,
-                       (bool)Http::isValidURI( $URI ),
-                       $message
-               );
-       }
-
        /**
         * @covers Http::getProxy
         */
@@ -41,71 +27,4 @@ class HttpTest extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * Feeds URI to test a long regular expression in Http::isValidURI
-        */
-       public static function provideURI() {
-               /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
-               return [
-                       [ false, '¿non sens before!! http://a', 'Allow anything before URI' ],
-
-                       # (http|https) - only two schemes allowed
-                       [ true, 'http://www.example.org/' ],
-                       [ true, 'https://www.example.org/' ],
-                       [ true, 'http://www.example.org', 'URI without directory' ],
-                       [ true, 'http://a', 'Short name' ],
-                       [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star'
-                       [ false, '\\host\directory', 'CIFS share' ],
-                       [ false, 'gopher://host/dir', 'Reject gopher scheme' ],
-                       [ false, 'telnet://host', 'Reject telnet scheme' ],
-
-                       # :\/\/ - double slashes
-                       [ false, 'http//example.org', 'Reject missing colon in protocol' ],
-                       [ false, 'http:/example.org', 'Reject missing slash in protocol' ],
-                       [ false, 'http:example.org', 'Must have two slashes' ],
-                       # Following fail since hostname can be made of anything
-                       [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ],
-
-                       # (\w+:{0,1}\w*@)? - optional user:pass
-                       [ true, 'http://user@host', 'Username provided' ],
-                       [ true, 'http://user:@host', 'Username provided, no password' ],
-                       [ true, 'http://user:pass@host', 'Username and password provided' ],
-
-                       # (\S+) - host part is made of anything not whitespaces
-                       // commented these out in order to remove @group Broken
-                       // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them
-                       // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ],
-                       // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ],
-
-                       # (:[0-9]+)? - port number
-                       [ true, 'http://example.org:80/' ],
-                       [ true, 'https://example.org:80/' ],
-                       [ true, 'http://example.org:443/' ],
-                       [ true, 'https://example.org:443/' ],
-
-                       # Part after the hostname is / or / with something else
-                       [ true, 'http://example/#' ],
-                       [ true, 'http://example/!' ],
-                       [ true, 'http://example/:' ],
-                       [ true, 'http://example/.' ],
-                       [ true, 'http://example/?' ],
-                       [ true, 'http://example/+' ],
-                       [ true, 'http://example/=' ],
-                       [ true, 'http://example/&' ],
-                       [ true, 'http://example/%' ],
-                       [ true, 'http://example/@' ],
-                       [ true, 'http://example/-' ],
-                       [ true, 'http://example//' ],
-                       [ true, 'http://example/&' ],
-
-                       # Fragment
-                       [ true, 'http://exam#ple.org', ], # This one is valid, really!
-                       [ true, 'http://example.org:80#anchor' ],
-                       [ true, 'http://example.org/?id#anchor' ],
-                       [ true, 'http://example.org/?#anchor' ],
-
-                       [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ],
-               ];
-       }
-
 }
index a74056d..0031cb3 100644 (file)
@@ -13,214 +13,6 @@ use Wikimedia\TestingAccessWrapper;
  */
 class SessionTest extends MediaWikiTestCase {
 
-       public function testConstructor() {
-               $backend = TestUtils::getDummySessionBackend();
-               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
-               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
-
-               $session = new Session( $backend, 42, new \TestLogger );
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $this->assertSame( $backend, $priv->backend );
-               $this->assertSame( 42, $priv->index );
-
-               $request = new \FauxRequest();
-               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
-               $this->assertSame( $backend, $priv2->backend );
-               $this->assertNotSame( $priv->index, $priv2->index );
-               $this->assertSame( $request, $priv2->getRequest() );
-       }
-
-       /**
-        * @dataProvider provideMethods
-        * @param string $m Method to test
-        * @param array $args Arguments to pass to the method
-        * @param bool $index Whether the backend method gets passed the index
-        * @param bool $ret Whether the method returns a value
-        */
-       public function testMethods( $m, $args, $index, $ret ) {
-               $mock = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ $m, 'deregisterSession' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'deregisterSession' )
-                       ->with( $this->identicalTo( 42 ) );
-
-               $tmp = $mock->expects( $this->once() )->method( $m );
-               $expectArgs = [];
-               if ( $index ) {
-                       $expectArgs[] = $this->identicalTo( 42 );
-               }
-               foreach ( $args as $arg ) {
-                       $expectArgs[] = $this->identicalTo( $arg );
-               }
-               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
-
-               $retval = new \stdClass;
-               $tmp->will( $this->returnValue( $retval ) );
-
-               $session = TestUtils::getDummySession( $mock, 42 );
-
-               if ( $ret ) {
-                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
-               } else {
-                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
-               }
-
-               // Trigger Session destructor
-               $session = null;
-       }
-
-       public static function provideMethods() {
-               return [
-                       [ 'getId', [], false, true ],
-                       [ 'getSessionId', [], false, true ],
-                       [ 'resetId', [], false, true ],
-                       [ 'getProvider', [], false, true ],
-                       [ 'isPersistent', [], false, true ],
-                       [ 'persist', [], false, false ],
-                       [ 'unpersist', [], false, false ],
-                       [ 'shouldRememberUser', [], false, true ],
-                       [ 'setRememberUser', [ true ], false, false ],
-                       [ 'getRequest', [], true, true ],
-                       [ 'getUser', [], false, true ],
-                       [ 'getAllowedUserRights', [], false, true ],
-                       [ 'canSetUser', [], false, true ],
-                       [ 'setUser', [ new \stdClass ], false, false ],
-                       [ 'suggestLoginUsername', [], true, true ],
-                       [ 'shouldForceHTTPS', [], false, true ],
-                       [ 'setForceHTTPS', [ true ], false, false ],
-                       [ 'getLoggedOutTimestamp', [], false, true ],
-                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
-                       [ 'getProviderMetadata', [], false, true ],
-                       [ 'save', [], false, false ],
-                       [ 'delaySave', [], false, true ],
-                       [ 'renew', [], false, false ],
-               ];
-       }
-
-       public function testDataAccess() {
-               $session = TestUtils::getDummySession();
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session->get( 'foo' ) );
-               $this->assertEquals( 'zero', $session->get( 0 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( null, $session->get( 'null' ) );
-               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->set( 'foo', 55 );
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertTrue( $session->exists( 'foo' ) );
-               $this->assertTrue( $session->exists( 1 ) );
-               $this->assertFalse( $session->exists( 'null' ) );
-               $this->assertFalse( $session->exists( 100 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->remove( 'foo' );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               $session->remove( 1 );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->remove( 101 );
-               $this->assertFalse( $backend->dirty );
-
-               $backend->data = [ 'a', 'b', '?' => 'c' ];
-               $this->assertSame( 3, $session->count() );
-               $this->assertSame( 3, count( $session ) );
-               $this->assertFalse( $backend->dirty );
-
-               $data = [];
-               foreach ( $session as $key => $value ) {
-                       $data[$key] = $value;
-               }
-               $this->assertEquals( $backend->data, $data );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
-               $this->assertFalse( $backend->dirty );
-       }
-
-       public function testArrayAccess() {
-               $logger = new \TestLogger;
-               $session = TestUtils::getDummySession( null, -1, $logger );
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session['foo'] );
-               $this->assertEquals( 'zero', $session[0] );
-               $this->assertFalse( $backend->dirty );
-
-               $logger->setCollect( true );
-               $this->assertEquals( null, $session['null'] );
-               $logger->setCollect( false );
-               $this->assertFalse( $backend->dirty );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $session['foo'] = 55;
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertFalse( $backend->dirty );
-
-               $session['bar'] = [ 'baz' => [] ];
-               $session['bar']['baz']['quux'] = 2;
-               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
-
-               $logger->setCollect( true );
-               $session['bar2']['baz']['quux'] = 3;
-               $logger->setCollect( false );
-               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $backend->dirty = false;
-               $this->assertTrue( isset( $session['foo'] ) );
-               $this->assertTrue( isset( $session[1] ) );
-               $this->assertFalse( isset( $session['null'] ) );
-               $this->assertFalse( isset( $session['missing'] ) );
-               $this->assertFalse( isset( $session[100] ) );
-               $this->assertFalse( $backend->dirty );
-
-               unset( $session['foo'] );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               unset( $session[1] );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               unset( $session[101] );
-               $this->assertFalse( $backend->dirty );
-       }
-
        public function testClear() {
                $session = TestUtils::getDummySession();
                $priv = TestingAccessWrapper::newFromObject( $session );
@@ -268,66 +60,6 @@ class SessionTest extends MediaWikiTestCase {
                $this->assertTrue( $backend->dirty );
        }
 
-       public function testTokens() {
-               $session = TestUtils::getDummySession();
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $backend = $priv->backend;
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $secret = $backend->data['wsTokenSecrets']['default'];
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( '', $token->salt );
-               $this->assertTrue( $token->wasNew() );
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( 'foo', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
-               $token = TestingAccessWrapper::newFromObject(
-                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
-               );
-               $this->assertSame( 'sekret', $token->secret );
-               $this->assertSame( 'bar|baz', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $session->resetToken( 'secret' );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
-
-               $session->resetAllTokens();
-               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
-       }
-
-       /**
-        * @dataProvider provideSecretsRoundTripping
-        * @param mixed $data
-        */
-       public function testSecretsRoundTripping( $data ) {
-               $session = TestUtils::getDummySession();
-
-               // Simple round-trip
-               $session->setSecret( 'secret', $data );
-               $this->assertNotEquals( $data, $session->get( 'secret' ) );
-               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
-       }
-
-       public static function provideSecretsRoundTripping() {
-               return [
-                       [ 'Foobar' ],
-                       [ 42 ],
-                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ true ],
-                       [ false ],
-                       [ null ],
-               ];
-       }
-
        public function testSecrets() {
                $logger = new \TestLogger;
                $session = TestUtils::getDummySession( null, -1, $logger );
@@ -370,4 +102,29 @@ class SessionTest extends MediaWikiTestCase {
                \Wikimedia\restoreWarnings();
        }
 
+       /**
+        * @dataProvider provideSecretsRoundTripping
+        * @param mixed $data
+        */
+       public function testSecretsRoundTripping( $data ) {
+               $session = TestUtils::getDummySession();
+
+               // Simple round-trip
+               $session->setSecret( 'secret', $data );
+               $this->assertNotEquals( $data, $session->get( 'secret' ) );
+               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
+       }
+
+       public static function provideSecretsRoundTripping() {
+               return [
+                       [ 'Foobar' ],
+                       [ 42 ],
+                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ true ],
+                       [ false ],
+                       [ null ],
+               ];
+       }
+
 }
diff --git a/tests/phpunit/unit/includes/http/HttpUnitTest.php b/tests/phpunit/unit/includes/http/HttpUnitTest.php
new file mode 100644 (file)
index 0000000..af73f34
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @covers Http
+ * @group Http
+ * @group small
+ */
+class HttpUnitTest extends MediaWikiUnitTestCase {
+
+       /**
+        * Test Http::isValidURI()
+        * T29854 : Http::isValidURI is too lax
+        * @dataProvider provideURI
+        * @covers Http::isValidURI
+        */
+       public function testIsValidUri( $expect, $URI, $message = '' ) {
+               $this->assertEquals(
+                       $expect,
+                       (bool)Http::isValidURI( $URI ),
+                       $message
+               );
+       }
+
+       /**
+        * Feeds URI to test a long regular expression in Http::isValidURI
+        */
+       public static function provideURI() {
+               /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
+               return [
+                       [ false, '¿non sens before!! http://a', 'Allow anything before URI' ],
+
+                       # (http|https) - only two schemes allowed
+                       [ true, 'http://www.example.org/' ],
+                       [ true, 'https://www.example.org/' ],
+                       [ true, 'http://www.example.org', 'URI without directory' ],
+                       [ true, 'http://a', 'Short name' ],
+                       [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star'
+                       [ false, '\\host\directory', 'CIFS share' ],
+                       [ false, 'gopher://host/dir', 'Reject gopher scheme' ],
+                       [ false, 'telnet://host', 'Reject telnet scheme' ],
+
+                       # :\/\/ - double slashes
+                       [ false, 'http//example.org', 'Reject missing colon in protocol' ],
+                       [ false, 'http:/example.org', 'Reject missing slash in protocol' ],
+                       [ false, 'http:example.org', 'Must have two slashes' ],
+                       # Following fail since hostname can be made of anything
+                       [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ],
+
+                       # (\w+:{0,1}\w*@)? - optional user:pass
+                       [ true, 'http://user@host', 'Username provided' ],
+                       [ true, 'http://user:@host', 'Username provided, no password' ],
+                       [ true, 'http://user:pass@host', 'Username and password provided' ],
+
+                       # (\S+) - host part is made of anything not whitespaces
+                       // commented these out in order to remove @group Broken
+                       // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them
+                       // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ],
+                       // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ],
+
+                       # (:[0-9]+)? - port number
+                       [ true, 'http://example.org:80/' ],
+                       [ true, 'https://example.org:80/' ],
+                       [ true, 'http://example.org:443/' ],
+                       [ true, 'https://example.org:443/' ],
+
+                       # Part after the hostname is / or / with something else
+                       [ true, 'http://example/#' ],
+                       [ true, 'http://example/!' ],
+                       [ true, 'http://example/:' ],
+                       [ true, 'http://example/.' ],
+                       [ true, 'http://example/?' ],
+                       [ true, 'http://example/+' ],
+                       [ true, 'http://example/=' ],
+                       [ true, 'http://example/&' ],
+                       [ true, 'http://example/%' ],
+                       [ true, 'http://example/@' ],
+                       [ true, 'http://example/-' ],
+                       [ true, 'http://example//' ],
+                       [ true, 'http://example/&' ],
+
+                       # Fragment
+                       [ true, 'http://exam#ple.org', ], # This one is valid, really!
+                       [ true, 'http://example.org:80#anchor' ],
+                       [ true, 'http://example.org/?id#anchor' ],
+                       [ true, 'http://example.org/?#anchor' ],
+
+                       [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/language/LanguageCodeTest.php b/tests/phpunit/unit/includes/language/LanguageCodeTest.php
new file mode 100644 (file)
index 0000000..f3a7ae4
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @covers LanguageCode
+ * @group Language
+ *
+ * @author Thiemo Kreuz
+ */
+class LanguageCodeTest extends MediaWikiUnitTestCase {
+
+       public function testConstructor() {
+               $instance = new LanguageCode();
+
+               $this->assertInstanceOf( LanguageCode::class, $instance );
+       }
+
+       public function testGetDeprecatedCodeMapping() {
+               $map = LanguageCode::getDeprecatedCodeMapping();
+
+               $this->assertInternalType( 'array', $map );
+               $this->assertContainsOnly( 'string', array_keys( $map ) );
+               $this->assertArrayNotHasKey( '', $map );
+               $this->assertContainsOnly( 'string', $map );
+               $this->assertNotContains( '', $map );
+
+               // Codes special to MediaWiki should never appear in a map of "deprecated" codes
+               $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
+               $this->assertNotContains( 'qqq', $map, 'documentation' );
+               $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
+               $this->assertNotContains( 'qqx', $map, 'debug code' );
+
+               // Valid language codes that are currently not "deprecated"
+               $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
+               $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
+               $this->assertArrayNotHasKey( 'simple', $map );
+       }
+
+       public function testReplaceDeprecatedCodes() {
+               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
+               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
+               $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
+       }
+
+       /**
+        * test @see LanguageCode::bcp47().
+        * Please note the BCP 47 explicitly state that language codes are case
+        * insensitive, there are some exceptions to the rule :)
+        * This test is used to verify our formatting against all lower and
+        * all upper cases language code.
+        *
+        * @see https://tools.ietf.org/html/bcp47
+        * @dataProvider provideLanguageCodes()
+        */
+       public function testBcp47( $code, $expected ) {
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to '$code'"
+               );
+
+               $code = strtolower( $code );
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to lower case '$code'"
+               );
+
+               $code = strtoupper( $code );
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to upper case '$code'"
+               );
+       }
+
+       /**
+        * Array format is ($code, $expected)
+        */
+       public static function provideLanguageCodes() {
+               return [
+                       // Extracted from BCP 47 (list not exhaustive)
+                       # 2.1.1
+                       [ 'en-ca-x-ca', 'en-CA-x-ca' ],
+                       [ 'sgn-be-fr', 'sgn-BE-FR' ],
+                       [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
+                       # 2.2
+                       [ 'sr-Latn-RS', 'sr-Latn-RS' ],
+                       [ 'az-arab-ir', 'az-Arab-IR' ],
+
+                       # 2.2.5
+                       [ 'sl-nedis', 'sl-nedis' ],
+                       [ 'de-ch-1996', 'de-CH-1996' ],
+
+                       # 2.2.6
+                       [
+                               'en-latn-gb-boont-r-extended-sequence-x-private',
+                               'en-Latn-GB-boont-r-extended-sequence-x-private'
+                       ],
+
+                       // Examples from BCP 47 Appendix A
+                       # Simple language subtag:
+                       [ 'DE', 'de' ],
+                       [ 'fR', 'fr' ],
+                       [ 'ja', 'ja' ],
+
+                       # Language subtag plus script subtag:
+                       [ 'zh-hans', 'zh-Hans' ],
+                       [ 'sr-cyrl', 'sr-Cyrl' ],
+                       [ 'sr-latn', 'sr-Latn' ],
+
+                       # Extended language subtags and their primary language subtag
+                       # counterparts:
+                       [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
+                       [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
+                       [ 'zh-yue-hk', 'zh-yue-HK' ],
+                       [ 'yue-hk', 'yue-HK' ],
+
+                       # Language-Script-Region:
+                       [ 'zh-hans-cn', 'zh-Hans-CN' ],
+                       [ 'sr-latn-RS', 'sr-Latn-RS' ],
+
+                       # Language-Variant:
+                       [ 'sl-rozaj', 'sl-rozaj' ],
+                       [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
+                       [ 'sl-nedis', 'sl-nedis' ],
+
+                       # Language-Region-Variant:
+                       [ 'de-ch-1901', 'de-CH-1901' ],
+                       [ 'sl-it-nedis', 'sl-IT-nedis' ],
+
+                       # Language-Script-Region-Variant:
+                       [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
+
+                       # Language-Region:
+                       [ 'de-de', 'de-DE' ],
+                       [ 'en-us', 'en-US' ],
+                       [ 'es-419', 'es-419' ],
+
+                       # Private use subtags:
+                       [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
+                       [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
+                       /**
+                        * Previous test does not reflect the BCP 47 which states:
+                        *  az-Arab-x-AZE-derbend
+                        * AZE being private, it should be lower case, hence the test above
+                        * should probably be:
+                        * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
+                        */
+
+                       # Private use registry values:
+                       [ 'x-whatever', 'x-whatever' ],
+                       [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
+                       [ 'de-qaaa', 'de-Qaaa' ],
+                       [ 'sr-latn-qm', 'sr-Latn-QM' ],
+                       [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
+
+                       # Tags that use extensions
+                       [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
+                       [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
+                       [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
+
+                       # Invalid:
+                       // de-419-DE
+                       // a-DE
+                       // ar-a-aaa-b-bbb-a-ccc
+
+                       # Non-standard and deprecated language codes used by MediaWiki
+                       [ 'als', 'gsw' ],
+                       [ 'bat-smg', 'sgs' ],
+                       [ 'be-x-old', 'be-tarask' ],
+                       [ 'fiu-vro', 'vro' ],
+                       [ 'roa-rup', 'rup' ],
+                       [ 'zh-classical', 'lzh' ],
+                       [ 'zh-min-nan', 'nan' ],
+                       [ 'zh-yue', 'yue' ],
+                       [ 'cbk-zam', 'cbk' ],
+                       [ 'de-formal', 'de-x-formal' ],
+                       [ 'eml', 'egl' ],
+                       [ 'en-rtl', 'en-x-rtl' ],
+                       [ 'es-formal', 'es-x-formal' ],
+                       [ 'hu-formal', 'hu-x-formal' ],
+                       [ 'kk-Arab', 'kk-Arab' ],
+                       [ 'kk-Cyrl', 'kk-Cyrl' ],
+                       [ 'kk-Latn', 'kk-Latn' ],
+                       [ 'map-bms', 'jv-x-bms' ],
+                       [ 'mo', 'ro-Cyrl-MD' ],
+                       [ 'nrm', 'nrf' ],
+                       [ 'nl-informal', 'nl-x-informal' ],
+                       [ 'roa-tara', 'nap-x-tara' ],
+                       [ 'simple', 'en-simple' ],
+                       [ 'sr-ec', 'sr-Cyrl' ],
+                       [ 'sr-el', 'sr-Latn' ],
+                       [ 'zh-cn', 'zh-Hans-CN' ],
+                       [ 'zh-sg', 'zh-Hans-SG' ],
+                       [ 'zh-my', 'zh-Hans-MY' ],
+                       [ 'zh-tw', 'zh-Hant-TW' ],
+                       [ 'zh-hk', 'zh-Hant-HK' ],
+                       [ 'zh-mo', 'zh-Hant-MO' ],
+                       [ 'zh-hans', 'zh-Hans' ],
+                       [ 'zh-hant', 'zh-Hant' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/language/SpecialPageAliasTest.php b/tests/phpunit/unit/includes/language/SpecialPageAliasTest.php
new file mode 100644 (file)
index 0000000..cce9d0e
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Verifies that special page aliases are valid, with no slashes.
+ *
+ * @group Language
+ * @group SpecialPageAliases
+ * @group SystemTest
+ * @group medium
+ * @todo This should be a structure test
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @coversNothing
+        * @dataProvider validSpecialPageAliasesProvider
+        */
+       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
+               foreach ( $specialPageAliases as $specialPage => $aliases ) {
+                       foreach ( $aliases as $alias ) {
+                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
+                               $this->assertRegExp( '/^[^\/]*$/', $msg );
+                       }
+               }
+       }
+
+       public function validSpecialPageAliasesProvider() {
+               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
+
+               $data = [];
+
+               foreach ( $codes as $code ) {
+                       $specialPageAliases = $this->getSpecialPageAliases( $code );
+
+                       if ( $specialPageAliases !== [] ) {
+                               $data[] = [ $code, $specialPageAliases ];
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * @param string $code
+        *
+        * @return array
+        */
+       protected function getSpecialPageAliases( $code ) {
+               $file = Language::getMessagesFileName( $code );
+
+               if ( is_readable( $file ) ) {
+                       include $file;
+
+                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
+                               return $specialPageAliases;
+                       }
+               }
+
+               return [];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/SessionUnitTest.php b/tests/phpunit/unit/includes/session/SessionUnitTest.php
new file mode 100644 (file)
index 0000000..b6e1d3a
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiUnitTestCase;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Session
+ */
+class SessionUnitTest extends MediaWikiUnitTestCase {
+
+       public function testConstructor() {
+               $backend = TestUtils::getDummySessionBackend();
+               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
+               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
+
+               $session = new Session( $backend, 42, new \TestLogger );
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $this->assertSame( $backend, $priv->backend );
+               $this->assertSame( 42, $priv->index );
+
+               $request = new \FauxRequest();
+               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
+               $this->assertSame( $backend, $priv2->backend );
+               $this->assertNotSame( $priv->index, $priv2->index );
+               $this->assertSame( $request, $priv2->getRequest() );
+       }
+
+       /**
+        * @dataProvider provideMethods
+        * @param string $m Method to test
+        * @param array $args Arguments to pass to the method
+        * @param bool $index Whether the backend method gets passed the index
+        * @param bool $ret Whether the method returns a value
+        */
+       public function testMethods( $m, $args, $index, $ret ) {
+               $mock = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ $m, 'deregisterSession' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'deregisterSession' )
+                       ->with( $this->identicalTo( 42 ) );
+
+               $tmp = $mock->expects( $this->once() )->method( $m );
+               $expectArgs = [];
+               if ( $index ) {
+                       $expectArgs[] = $this->identicalTo( 42 );
+               }
+               foreach ( $args as $arg ) {
+                       $expectArgs[] = $this->identicalTo( $arg );
+               }
+               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
+
+               $retval = new \stdClass;
+               $tmp->will( $this->returnValue( $retval ) );
+
+               $session = TestUtils::getDummySession( $mock, 42 );
+
+               if ( $ret ) {
+                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
+               } else {
+                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
+               }
+
+               // Trigger Session destructor
+               $session = null;
+       }
+
+       public static function provideMethods() {
+               return [
+                       [ 'getId', [], false, true ],
+                       [ 'getSessionId', [], false, true ],
+                       [ 'resetId', [], false, true ],
+                       [ 'getProvider', [], false, true ],
+                       [ 'isPersistent', [], false, true ],
+                       [ 'persist', [], false, false ],
+                       [ 'unpersist', [], false, false ],
+                       [ 'shouldRememberUser', [], false, true ],
+                       [ 'setRememberUser', [ true ], false, false ],
+                       [ 'getRequest', [], true, true ],
+                       [ 'getUser', [], false, true ],
+                       [ 'getAllowedUserRights', [], false, true ],
+                       [ 'canSetUser', [], false, true ],
+                       [ 'setUser', [ new \stdClass ], false, false ],
+                       [ 'suggestLoginUsername', [], true, true ],
+                       [ 'shouldForceHTTPS', [], false, true ],
+                       [ 'setForceHTTPS', [ true ], false, false ],
+                       [ 'getLoggedOutTimestamp', [], false, true ],
+                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
+                       [ 'getProviderMetadata', [], false, true ],
+                       [ 'save', [], false, false ],
+                       [ 'delaySave', [], false, true ],
+                       [ 'renew', [], false, false ],
+               ];
+       }
+
+       public function testDataAccess() {
+               $session = TestUtils::getDummySession();
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session->get( 'foo' ) );
+               $this->assertEquals( 'zero', $session->get( 0 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( null, $session->get( 'null' ) );
+               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->set( 'foo', 55 );
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertTrue( $session->exists( 'foo' ) );
+               $this->assertTrue( $session->exists( 1 ) );
+               $this->assertFalse( $session->exists( 'null' ) );
+               $this->assertFalse( $session->exists( 100 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->remove( 'foo' );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               $session->remove( 1 );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->remove( 101 );
+               $this->assertFalse( $backend->dirty );
+
+               $backend->data = [ 'a', 'b', '?' => 'c' ];
+               $this->assertSame( 3, $session->count() );
+               $this->assertSame( 3, count( $session ) );
+               $this->assertFalse( $backend->dirty );
+
+               $data = [];
+               foreach ( $session as $key => $value ) {
+                       $data[$key] = $value;
+               }
+               $this->assertEquals( $backend->data, $data );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testArrayAccess() {
+               $logger = new \TestLogger;
+               $session = TestUtils::getDummySession( null, -1, $logger );
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session['foo'] );
+               $this->assertEquals( 'zero', $session[0] );
+               $this->assertFalse( $backend->dirty );
+
+               $logger->setCollect( true );
+               $this->assertEquals( null, $session['null'] );
+               $logger->setCollect( false );
+               $this->assertFalse( $backend->dirty );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $session['foo'] = 55;
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertFalse( $backend->dirty );
+
+               $session['bar'] = [ 'baz' => [] ];
+               $session['bar']['baz']['quux'] = 2;
+               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
+
+               $logger->setCollect( true );
+               $session['bar2']['baz']['quux'] = 3;
+               $logger->setCollect( false );
+               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $backend->dirty = false;
+               $this->assertTrue( isset( $session['foo'] ) );
+               $this->assertTrue( isset( $session[1] ) );
+               $this->assertFalse( isset( $session['null'] ) );
+               $this->assertFalse( isset( $session['missing'] ) );
+               $this->assertFalse( isset( $session[100] ) );
+               $this->assertFalse( $backend->dirty );
+
+               unset( $session['foo'] );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               unset( $session[1] );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               unset( $session[101] );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testTokens() {
+               $session = TestUtils::getDummySession();
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $backend = $priv->backend;
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $secret = $backend->data['wsTokenSecrets']['default'];
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( '', $token->salt );
+               $this->assertTrue( $token->wasNew() );
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( 'foo', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
+               $token = TestingAccessWrapper::newFromObject(
+                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
+               );
+               $this->assertSame( 'sekret', $token->secret );
+               $this->assertSame( 'bar|baz', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $session->resetToken( 'secret' );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
+
+               $session->resetAllTokens();
+               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
+       }
+
+}
diff --git a/tests/phpunit/unit/languages/LanguageCodeTest.php b/tests/phpunit/unit/languages/LanguageCodeTest.php
deleted file mode 100644 (file)
index f3a7ae4..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-<?php
-
-/**
- * @covers LanguageCode
- * @group Language
- *
- * @author Thiemo Kreuz
- */
-class LanguageCodeTest extends MediaWikiUnitTestCase {
-
-       public function testConstructor() {
-               $instance = new LanguageCode();
-
-               $this->assertInstanceOf( LanguageCode::class, $instance );
-       }
-
-       public function testGetDeprecatedCodeMapping() {
-               $map = LanguageCode::getDeprecatedCodeMapping();
-
-               $this->assertInternalType( 'array', $map );
-               $this->assertContainsOnly( 'string', array_keys( $map ) );
-               $this->assertArrayNotHasKey( '', $map );
-               $this->assertContainsOnly( 'string', $map );
-               $this->assertNotContains( '', $map );
-
-               // Codes special to MediaWiki should never appear in a map of "deprecated" codes
-               $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
-               $this->assertNotContains( 'qqq', $map, 'documentation' );
-               $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
-               $this->assertNotContains( 'qqx', $map, 'debug code' );
-
-               // Valid language codes that are currently not "deprecated"
-               $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
-               $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
-               $this->assertArrayNotHasKey( 'simple', $map );
-       }
-
-       public function testReplaceDeprecatedCodes() {
-               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
-               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
-               $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
-       }
-
-       /**
-        * test @see LanguageCode::bcp47().
-        * Please note the BCP 47 explicitly state that language codes are case
-        * insensitive, there are some exceptions to the rule :)
-        * This test is used to verify our formatting against all lower and
-        * all upper cases language code.
-        *
-        * @see https://tools.ietf.org/html/bcp47
-        * @dataProvider provideLanguageCodes()
-        */
-       public function testBcp47( $code, $expected ) {
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to '$code'"
-               );
-
-               $code = strtolower( $code );
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to lower case '$code'"
-               );
-
-               $code = strtoupper( $code );
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to upper case '$code'"
-               );
-       }
-
-       /**
-        * Array format is ($code, $expected)
-        */
-       public static function provideLanguageCodes() {
-               return [
-                       // Extracted from BCP 47 (list not exhaustive)
-                       # 2.1.1
-                       [ 'en-ca-x-ca', 'en-CA-x-ca' ],
-                       [ 'sgn-be-fr', 'sgn-BE-FR' ],
-                       [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
-                       # 2.2
-                       [ 'sr-Latn-RS', 'sr-Latn-RS' ],
-                       [ 'az-arab-ir', 'az-Arab-IR' ],
-
-                       # 2.2.5
-                       [ 'sl-nedis', 'sl-nedis' ],
-                       [ 'de-ch-1996', 'de-CH-1996' ],
-
-                       # 2.2.6
-                       [
-                               'en-latn-gb-boont-r-extended-sequence-x-private',
-                               'en-Latn-GB-boont-r-extended-sequence-x-private'
-                       ],
-
-                       // Examples from BCP 47 Appendix A
-                       # Simple language subtag:
-                       [ 'DE', 'de' ],
-                       [ 'fR', 'fr' ],
-                       [ 'ja', 'ja' ],
-
-                       # Language subtag plus script subtag:
-                       [ 'zh-hans', 'zh-Hans' ],
-                       [ 'sr-cyrl', 'sr-Cyrl' ],
-                       [ 'sr-latn', 'sr-Latn' ],
-
-                       # Extended language subtags and their primary language subtag
-                       # counterparts:
-                       [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
-                       [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
-                       [ 'zh-yue-hk', 'zh-yue-HK' ],
-                       [ 'yue-hk', 'yue-HK' ],
-
-                       # Language-Script-Region:
-                       [ 'zh-hans-cn', 'zh-Hans-CN' ],
-                       [ 'sr-latn-RS', 'sr-Latn-RS' ],
-
-                       # Language-Variant:
-                       [ 'sl-rozaj', 'sl-rozaj' ],
-                       [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
-                       [ 'sl-nedis', 'sl-nedis' ],
-
-                       # Language-Region-Variant:
-                       [ 'de-ch-1901', 'de-CH-1901' ],
-                       [ 'sl-it-nedis', 'sl-IT-nedis' ],
-
-                       # Language-Script-Region-Variant:
-                       [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
-
-                       # Language-Region:
-                       [ 'de-de', 'de-DE' ],
-                       [ 'en-us', 'en-US' ],
-                       [ 'es-419', 'es-419' ],
-
-                       # Private use subtags:
-                       [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
-                       [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
-                       /**
-                        * Previous test does not reflect the BCP 47 which states:
-                        *  az-Arab-x-AZE-derbend
-                        * AZE being private, it should be lower case, hence the test above
-                        * should probably be:
-                        * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
-                        */
-
-                       # Private use registry values:
-                       [ 'x-whatever', 'x-whatever' ],
-                       [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
-                       [ 'de-qaaa', 'de-Qaaa' ],
-                       [ 'sr-latn-qm', 'sr-Latn-QM' ],
-                       [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
-
-                       # Tags that use extensions
-                       [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
-                       [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
-                       [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
-
-                       # Invalid:
-                       // de-419-DE
-                       // a-DE
-                       // ar-a-aaa-b-bbb-a-ccc
-
-                       # Non-standard and deprecated language codes used by MediaWiki
-                       [ 'als', 'gsw' ],
-                       [ 'bat-smg', 'sgs' ],
-                       [ 'be-x-old', 'be-tarask' ],
-                       [ 'fiu-vro', 'vro' ],
-                       [ 'roa-rup', 'rup' ],
-                       [ 'zh-classical', 'lzh' ],
-                       [ 'zh-min-nan', 'nan' ],
-                       [ 'zh-yue', 'yue' ],
-                       [ 'cbk-zam', 'cbk' ],
-                       [ 'de-formal', 'de-x-formal' ],
-                       [ 'eml', 'egl' ],
-                       [ 'en-rtl', 'en-x-rtl' ],
-                       [ 'es-formal', 'es-x-formal' ],
-                       [ 'hu-formal', 'hu-x-formal' ],
-                       [ 'kk-Arab', 'kk-Arab' ],
-                       [ 'kk-Cyrl', 'kk-Cyrl' ],
-                       [ 'kk-Latn', 'kk-Latn' ],
-                       [ 'map-bms', 'jv-x-bms' ],
-                       [ 'mo', 'ro-Cyrl-MD' ],
-                       [ 'nrm', 'nrf' ],
-                       [ 'nl-informal', 'nl-x-informal' ],
-                       [ 'roa-tara', 'nap-x-tara' ],
-                       [ 'simple', 'en-simple' ],
-                       [ 'sr-ec', 'sr-Cyrl' ],
-                       [ 'sr-el', 'sr-Latn' ],
-                       [ 'zh-cn', 'zh-Hans-CN' ],
-                       [ 'zh-sg', 'zh-Hans-SG' ],
-                       [ 'zh-my', 'zh-Hans-MY' ],
-                       [ 'zh-tw', 'zh-Hant-TW' ],
-                       [ 'zh-hk', 'zh-Hant-HK' ],
-                       [ 'zh-mo', 'zh-Hant-MO' ],
-                       [ 'zh-hans', 'zh-Hans' ],
-                       [ 'zh-hant', 'zh-Hant' ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/unit/languages/SpecialPageAliasTest.php b/tests/phpunit/unit/languages/SpecialPageAliasTest.php
deleted file mode 100644 (file)
index cce9d0e..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * Verifies that special page aliases are valid, with no slashes.
- *
- * @group Language
- * @group SpecialPageAliases
- * @group SystemTest
- * @group medium
- * @todo This should be a structure test
- *
- * @author Katie Filbert < aude.wiki@gmail.com >
- */
-class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
-
-       /**
-        * @coversNothing
-        * @dataProvider validSpecialPageAliasesProvider
-        */
-       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
-               foreach ( $specialPageAliases as $specialPage => $aliases ) {
-                       foreach ( $aliases as $alias ) {
-                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
-                               $this->assertRegExp( '/^[^\/]*$/', $msg );
-                       }
-               }
-       }
-
-       public function validSpecialPageAliasesProvider() {
-               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
-
-               $data = [];
-
-               foreach ( $codes as $code ) {
-                       $specialPageAliases = $this->getSpecialPageAliases( $code );
-
-                       if ( $specialPageAliases !== [] ) {
-                               $data[] = [ $code, $specialPageAliases ];
-                       }
-               }
-
-               return $data;
-       }
-
-       /**
-        * @param string $code
-        *
-        * @return array
-        */
-       protected function getSpecialPageAliases( $code ) {
-               $file = Language::getMessagesFileName( $code );
-
-               if ( is_readable( $file ) ) {
-                       include $file;
-
-                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
-                               return $specialPageAliases;
-                       }
-               }
-
-               return [];
-       }
-
-}