Merge "Add $specialPageAliases and $magicWords for Tajik"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 2 Sep 2019 23:17:22 +0000 (23:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 2 Sep 2019 23:17:22 +0000 (23:17 +0000)
213 files changed:
.mailmap
.phan/config.php
CREDITS
INSTALL
RELEASE-NOTES-1.34
composer.json
includes/EditPage.php
includes/FileDeleteForm.php
includes/GlobalFunctions.php
includes/LinkFilter.php
includes/Linker.php
includes/MovePage.php
includes/ProxyLookup.php
includes/Revision/RenderedRevision.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/Setup.php
includes/Title.php
includes/WikiMap.php
includes/api/ApiAuthManagerHelper.php
includes/api/ApiBlock.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiMove.php
includes/api/ApiOpenSearch.php
includes/api/ApiPageSet.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiQueryCategories.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiUnblock.php
includes/api/ApiUndelete.php
includes/api/ApiUpload.php
includes/api/ApiUserrights.php
includes/api/ApiValidatePassword.php
includes/api/SearchApi.php
includes/api/i18n/pt-br.json
includes/auth/AuthenticationRequest.php
includes/auth/Throttler.php
includes/block/AbstractBlock.php
includes/block/CompositeBlock.php
includes/changes/EnhancedChangesList.php
includes/changes/RecentChange.php
includes/changetags/ChangeTags.php
includes/content/ContentHandler.php
includes/content/FileContentHandler.php
includes/diff/DiffEngine.php
includes/exception/MWException.php
includes/exception/MWExceptionRenderer.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/LocalFile.php
includes/historyblob/DiffHistoryBlob.php
includes/htmlform/HTMLForm.php
includes/http/HttpRequestFactory.php
includes/http/MWHttpRequest.php
includes/import/WikiImporter.php
includes/installer/CliInstaller.php
includes/installer/Installer.php
includes/installer/i18n/nap.json
includes/jobqueue/Job.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/ThumbnailRenderJob.php
includes/libs/MappedIterator.php
includes/libs/XhprofData.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendMultiWrite.php
includes/libs/http/MultiHttpClient.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MediumSpecificBagOStuff.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/wancache/WANObjectCache.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/logging/BlockLogFormatter.php
includes/logging/LogPager.php
includes/logging/ManualLogEntry.php
includes/logging/PatrolLog.php
includes/media/IPTC.php
includes/page/Article.php
includes/page/CategoryPage.php
includes/page/ImagePage.php
includes/page/PageArchive.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/preferences/DefaultPreferencesFactory.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/revisiondelete/RevDelList.php
includes/search/SearchEngine.php
includes/session/SessionManager.php
includes/shell/Shell.php
includes/skins/BaseTemplate.php
includes/specialpage/AuthManagerSpecialPage.php
includes/specials/SpecialAllMessages.php
includes/specials/SpecialBlock.php
includes/specials/SpecialContributions.php
includes/specials/SpecialDeletedContributions.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialListFiles.php
includes/specials/SpecialMediaStatistics.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewimages.php
includes/specials/SpecialPageLanguage.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialWhatLinksHere.php
includes/specials/forms/UploadForm.php
includes/specials/helpers/ImportReporter.php
includes/specials/pagers/AllMessagesTablePager.php
includes/specials/pagers/BlockListPager.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/DeletedContribsPager.php
includes/specials/pagers/ImageListPager.php
includes/specials/pagers/NewFilesPager.php
includes/widget/ComplexTitleInputWidget.php
languages/i18n/az.json
languages/i18n/ba.json
languages/i18n/ban.json
languages/i18n/be.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/en.json
languages/i18n/et.json
languages/i18n/exif/lb.json
languages/i18n/exif/zh-hant.json
languages/i18n/fa.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/ia.json
languages/i18n/io.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/luz.json
languages/i18n/min.json
languages/i18n/ml.json
languages/i18n/nap.json
languages/i18n/nds-nl.json
languages/i18n/ne.json
languages/i18n/nqo.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sr-ec.json
languages/i18n/szl.json
maintenance/categoryChangesAsRdf.php
maintenance/checkDependencies.php
maintenance/cleanupPreferences.php
maintenance/convertExtensionToRegistration.php
maintenance/getReplicaServer.php
maintenance/includes/MigrateActors.php
maintenance/includes/TextPassDumper.php
maintenance/migrateArchiveText.php
maintenance/namespaceDupes.php
maintenance/populateArchiveRevId.php
maintenance/populateContentTables.php
maintenance/populateImageSha1.php
maintenance/reassignEdits.php
maintenance/rebuildmessages.php
maintenance/removeUnusedAccounts.php
maintenance/sql.php
maintenance/storage/compressOld.php
maintenance/updateCollation.php
maintenance/updateExtensionJsonSchema.php
tests/common/TestSetup.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiTestCaseTrait.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/WikiReferenceTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiTestCaseUpload.php [deleted file]
tests/phpunit/includes/api/ApiUploadTest.php
tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php
tests/phpunit/includes/block/CompositeBlockTest.php
tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/includes/diff/SlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php [new file with mode: 0644]
tests/phpunit/includes/media/JpegMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/ParserFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteExporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.xml [new file with mode: 0644]
tests/phpunit/includes/specials/ContribsPagerTest.php
tests/phpunit/includes/specials/ImageListPagerTest.php
tests/phpunit/includes/specials/SpecialWatchlistTest.php
tests/phpunit/includes/specials/pagers/BlockListPagerTest.php
tests/phpunit/includes/utils/ZipDirectoryReaderTest.php [new file with mode: 0644]
tests/phpunit/phpunit.php
tests/phpunit/suites/UploadFromUrlTestSuite.php
tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php [deleted file]
tests/phpunit/unit/includes/Revision/SlotRecordTest.php [deleted file]
tests/phpunit/unit/includes/WikiReferenceTest.php [deleted file]
tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [deleted file]
tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php [deleted file]
tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php [deleted file]
tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php [deleted file]
tests/phpunit/unit/includes/parser/ParserFactoryTest.php [deleted file]
tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteExporterTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteImporterTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteImporterTest.xml [deleted file]
tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php [deleted file]

index 0f5413e..82ebc21 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -233,6 +233,7 @@ Joel Sahleen <jsahleen@wikimedia.org>
 John Du Hart <john@compwhizii.net> <johnduhart@users.mediawiki.org>
 John Erling Blad <john.blad@wikimedia.de>
 Jon Harald Søby <jhsoby@gmail.com> <jhsoby@users.mediawiki.org>
+Jon Harald Søby <jhsoby@gmail.com>
 Jon Robson <jrobson@wikimedia.org>
 Jon Robson <jrobson@wikimedia.org> <jdlrobson@gmail.com>
 Juliusz Gonera <jgonera@gmail.com>
@@ -286,6 +287,7 @@ Mark Holmquist <mtraceur@member.fsf.org> <mholmquist@wikimedia.org>
 Marko Obrovac <mobrovac@wikimedia.org>
 Markus Glaser <glaser@hallowelt.biz>
 Markus Glaser <glaser@hallowelt.biz> <mglaser@users.mediawiki.org>
+Martin Urbanec <martin.urbanec@wikimedia.cz>
 Matt Johnston <mattj@emazestudios.com> <mattj@users.mediawiki.org>
 Matthew Britton <hugglegurch@gmail.com> <gurch@users.mediawiki.org>
 Matthew Bowker <matthewrbowker.bugs@gmail.com>
index 29729ae..e02dba7 100644 (file)
@@ -76,26 +76,21 @@ $cfg['exclude_analysis_directory_list'] = [
 ];
 
 $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [
-       // approximate error count: 22
-       "PhanAccessMethodInternal",
        // approximate error count: 19
-       "PhanParamReqAfterOpt",
+       "PhanParamReqAfterOpt", // False positives with nullables, ref phan issue #3159
        // approximate error count: 110
-       "PhanParamTooMany",
-       // approximate error count: 63
-       "PhanTypeArraySuspicious",
-       // approximate error count: 88
-       "PhanTypeInvalidDimOffset",
-       // approximate error count: 60
-       "PhanTypeMismatchArgument",
+       "PhanParamTooMany", // False positives with variargs. Unsuppress after dropping HHVM
+
+       // approximate error count: 22
+       "PhanAccessMethodInternal",
        // approximate error count: 36
        "PhanUndeclaredConstant",
+       // approximate error count: 60
+       "PhanTypeMismatchArgument",
        // approximate error count: 219
        "PhanUndeclaredMethod",
        // approximate error count: 752
        "PhanUndeclaredProperty",
-       // approximate error count: 53
-       "PhanUndeclaredVariableDim",
 ] );
 
 $cfg['ignore_undeclared_variables_in_global_scope'] = true;
@@ -103,6 +98,20 @@ $cfg['globals_type_map'] = array_merge( $cfg['globals_type_map'], [
        'IP' => 'string',
        'wgGalleryOptions' => 'array',
        'wgDummyLanguageCodes' => 'string[]',
+       'wgNamespaceProtection' => 'array<string,string|string[]>',
+       'wgNamespaceAliases' => 'array<string,int>',
+       'wgLockManagers' => 'array[]',
+       'wgForeignFileRepos' => 'array[]',
+       'wgDefaultUserOptions' => 'array',
+       'wgSkipSkins' => 'string[]',
+       'wgLogTypes' => 'string[]',
+       'wgLogNames' => 'array<string,string>',
+       'wgLogHeaders' => 'array<string,string>',
+       'wgLogActionsHandlers' => 'array<string,class-string>',
+       'wgPasswordPolicy' => 'array<string,array<string,string|array>>',
+       'wgVirtualRestConfig' => 'array<string,array>',
+       'wgWANObjectCaches' => 'array[]',
+       'wgLocalInterwikis' => 'string[]',
 ] );
 
 return $cfg;
diff --git a/CREDITS b/CREDITS
index 319b566..140ada2 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -314,7 +314,6 @@ The following list can be found parsed under Special:Version/Credits -->
 * Jaska Zedlik
 * Jason Richey
 * Jayprakash12345
-* jeblad
 * Jeff Hobson
 * Jeff Janes
 * jeff303
@@ -328,7 +327,6 @@ The following list can be found parsed under Special:Version/Credits -->
 * Jerome Jamnicky
 * Jesús Martínez Novo
 * jhobs
-* jhsoby
 * Jiabao
 * Jidanni
 * Jimmy Collins
diff --git a/INSTALL b/INSTALL
index bf64ab7..07dd9c3 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -5,8 +5,16 @@ Installing MediaWiki
 Starting with MediaWiki 1.2.0, it's possible to install and configure the wiki
 "in-place", as long as you have the necessary prerequisites available.
 
-Required software:
-* Web server with PHP 7.0.0 or HHVM 3.18.5 or higher.
+Required software as of MediaWiki 1.34.0:
+
+* Web server with PHP 7.0.13 or higher, plus the following extesnsions:
+** ctype
+** dom
+** fileinfo
+** iconv
+** json
+** mbstring
+** xml
 * A SQL server, the following types are supported
 ** MySQL 5.5.8 or higher
 ** PostgreSQL 9.2 or higher
index e57dacc..275f4c2 100644 (file)
@@ -485,7 +485,15 @@ because of Phabricator reports.
 == Compatibility ==
 MediaWiki 1.34 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
 supported, it is generally advised to use PHP 7.0.13 or later for long term
-support.
+support. It also requires the following PHP extensions:
+
+* ctype
+* dom
+* fileinfo
+* iconv
+* json
+* mbstring
+* xml
 
 MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
 but support for them is somewhat less mature.
index 98e7ebf..dad3c5c 100644 (file)
@@ -20,6 +20,7 @@
                "composer/semver": "1.5.0",
                "cssjanus/cssjanus": "1.3.0",
                "ext-ctype": "*",
+               "ext-dom": "*",
                "ext-fileinfo": "*",
                "ext-iconv": "*",
                "ext-json": "*",
index f066a61..541541a 100644 (file)
@@ -4166,7 +4166,7 @@ ERROR;
         *  - 'legacy-name' (optional): short name for backwards-compatibility
         * @param array $checked Array of checkbox name (matching the 'legacy-name') => bool,
         *   where bool indicates the checked status of the checkbox
-        * @return array
+        * @return array[]
         */
        public function getCheckboxesDefinition( $checked ) {
                $checkboxes = [];
index 8272ccf..75eedcc 100644 (file)
@@ -181,7 +181,7 @@ class FileDeleteForm {
                                $logEntry->setPerformer( $user );
                                $logEntry->setTarget( $title );
                                $logEntry->setComment( $logComment );
-                               $logEntry->setTags( $tags );
+                               $logEntry->addTags( $tags );
                                $logid = $logEntry->insert();
                                $logEntry->publish( $logid );
 
@@ -212,7 +212,7 @@ class FileDeleteForm {
                                                $logEntry->setPerformer( $user );
                                                $logEntry->setTarget( clone $title );
                                                $logEntry->setComment( $reason );
-                                               $logEntry->setTags( $tags );
+                                               $logEntry->addTags( $tags );
                                                $logid = $logEntry->insert();
                                                $dbw->onTransactionPreCommitOrIdle(
                                                        function () use ( $logEntry, $logid ) {
@@ -261,6 +261,7 @@ class FileDeleteForm {
                );
                $options = Xml::listDropDownOptionsOoui( $options );
 
+               $fields = [];
                $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
                        $this->prepareMessage( 'filedelete-intro' ) ) ]
                );
index 4ae9237..2cde173 100644 (file)
@@ -2116,6 +2116,7 @@ function wfEscapeShellArg( ...$args ) {
  *     including errors from limit.sh
  *   - profileMethod: By default this function will profile based on the calling
  *     method. Set this to a string for an alternative method to profile from
+ * @phan-param array{duplicateStderr?:bool,profileMethod?:string} $options
  *
  * @return string Collected stdout as a string
  * @deprecated since 1.30 use class MediaWiki\Shell\Shell
@@ -2134,6 +2135,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = [],
        }
 
        $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr'];
+       // @phan-suppress-next-line PhanTypeInvalidDimOffset
        $profileMethod = $options['profileMethod'] ?? wfGetCaller();
 
        try {
@@ -2190,6 +2192,7 @@ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits =
  * @param array $options Associative array of options:
  *     'php': The path to the php executable
  *     'wrapper': Path to a PHP wrapper to handle the maintenance script
+ * @phan-param array{php?:string,wrapper?:string} $options
  * @return string
  */
 function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) {
@@ -2197,6 +2200,7 @@ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] )
        // Give site config file a chance to run the script in a wrapper.
        // The caller may likely want to call wfBasename() on $script.
        Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
+       // @phan-suppress-next-line PhanTypeInvalidDimOffset
        $cmd = [ $options['php'] ?? $wgPhpCli ];
        if ( isset( $options['wrapper'] ) ) {
                $cmd[] = $options['wrapper'];
index 6ad9b31..c46df94 100644 (file)
@@ -292,7 +292,7 @@ class LinkFilter {
                // The constant prefix is smaller than el_index_60, so we use a LIKE
                // for a prefix search.
                return [
-                       "{$p}_index_60" . $db->buildLike( [ $index, $db->anyString() ] ),
+                       "{$p}_index_60" . $db->buildLike( $index, $db->anyString() ),
                        "{$p}_index" . $db->buildLike( $like ),
                ];
        }
@@ -338,6 +338,7 @@ class LinkFilter {
                        }
                }
 
+               $like = [];
                $like[] = $bits['scheme'] . $bits['delimiter'] . $bits['host'];
 
                if ( $subdomains ) {
index 03d2516..1a5058d 100644 (file)
@@ -688,13 +688,14 @@ class Linker {
                if ( $label == '' ) {
                        $label = $title->getPrefixedText();
                }
+               $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
                $currentExists = $time
-                       && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ) !== false;
+                       && $repoGroup->findFile( $title ) !== false;
 
                if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
                        && !$currentExists
                ) {
-                       if ( RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ) ) {
+                       if ( $repoGroup->getLocalRepo()->checkRedirect( $title ) ) {
                                // We already know it's a redirect, so mark it accordingly
                                return self::link(
                                        $title,
@@ -1040,7 +1041,7 @@ class Linker {
                }
 
                $userTalkPage = new TitleValue( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
-               $moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
+               $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
 
                return self::link( $userTalkPage,
                        wfMessage( 'talkpagelinktext' )->escaped(),
@@ -1062,7 +1063,7 @@ class Linker {
                }
 
                $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
-               $moreLinkAttribs['class'] = 'mw-usertoollinks-block';
+               $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
 
                return self::link( $blockPage,
                        wfMessage( 'blocklink' )->escaped(),
@@ -1083,7 +1084,7 @@ class Linker {
                }
 
                $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
-               $moreLinkAttribs['class'] = 'mw-usertoollinks-mail';
+               $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
                return self::link( $emailPage,
                        wfMessage( 'emaillink' )->escaped(),
                        $moreLinkAttribs
index e6faace..4180e32 100644 (file)
@@ -446,7 +446,7 @@ class MovePage {
                                $status = Status::newFatal( 'movepage-max-pages', $wgMaximumMovedPages );
                                $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
                                $topStatus->merge( $status );
-                               $topStatus->setOk( true );
+                               $topStatus->setOK( true );
                                break;
                        }
 
@@ -479,7 +479,7 @@ class MovePage {
                        }
                        $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
                        $topStatus->merge( $status );
-                       $topStatus->setOk( true );
+                       $topStatus->setOK( true );
                }
 
                $topStatus->value = $perTitleStatus;
@@ -598,7 +598,7 @@ class MovePage {
                                '4::oldtitle' => $this->oldTitle->getPrefixedText(),
                        ] );
                        $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
-                       $logEntry->setTags( $changeTags );
+                       $logEntry->addTags( $changeTags );
                        $logId = $logEntry->insert();
                        $logEntry->publish( $logId );
                }
@@ -895,7 +895,7 @@ class MovePage {
                # Log the move
                $logid = $logEntry->insert();
 
-               $logEntry->setTags( $changeTags );
+               $logEntry->addTags( $changeTags );
                $logEntry->publish( $logid );
 
                return $nullRevision;
index 246ae95..7450bb9 100644 (file)
@@ -58,7 +58,7 @@ class ProxyLookup {
         */
        public function isConfiguredProxy( $ip ) {
                // Quick check of known singular proxy servers
-               if ( in_array( $ip, $this->proxyServers ) ) {
+               if ( in_array( $ip, $this->proxyServers, true ) ) {
                        return true;
                }
 
index 3bc8dda..ba229d1 100644 (file)
@@ -185,6 +185,7 @@ class RenderedRevision implements SlotRenderingProvider {
         * @param array $hints Hints given as an associative array. Known keys:
         *      - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed
         *        to just meta-data). Default is to generate HTML.
+        * @phan-param array{generate-html?:bool} $hints
         *
         * @return ParserOutput
         */
@@ -212,6 +213,7 @@ class RenderedRevision implements SlotRenderingProvider {
         * @param array $hints Hints given as an associative array. Known keys:
         *      - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed
         *        to just meta-data). Default is to generate HTML.
+        * @phan-param array{generate-html?:bool} $hints
         *
         * @throws SuppressedDataException if the content is not accessible for the audience
         *         specified in the constructor.
index 3c3b6a9..ea90255 100644 (file)
@@ -95,6 +95,7 @@ class RevisionRenderer {
         *        matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
         *        for the time until caches have been changed to store RenderedRevision states instead
         *        of ParserOutput objects.
+        * @phan-param array{use-master?:bool,audience?:int,known-revision-output?:ParserOutput} $hints
         *
         * @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
         */
@@ -108,6 +109,7 @@ class RevisionRenderer {
                        throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
                }
 
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $audience = $hints['audience']
                        ?? ( $forUser ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC );
 
@@ -121,6 +123,7 @@ class RevisionRenderer {
                        $options = ParserOptions::newCanonical( $forUser ?: 'canonical' );
                }
 
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $useMaster = $hints['use-master'] ?? false;
 
                $dbIndex = $useMaster
index 9e8dfe7..818a536 100644 (file)
@@ -2324,6 +2324,7 @@ class RevisionStore
         *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
         *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
         *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        * @phan-return array{tables:string[],fields:string[],joins:array}
         */
        public function getQueryInfo( $options = [] ) {
                $ret = [
index 2267800..1e65f52 100644 (file)
@@ -386,6 +386,7 @@ $wgSkipSkins[] = 'apioutput';
 if ( $wgLocalInterwiki ) {
        // Hard deprecated in 1.34.
        wfDeprecated( '$wgLocalInterwiki – use $wgLocalInterwikis instead', '1.23' );
+       // @phan-suppress-next-line PhanUndeclaredVariableDim
        array_unshift( $wgLocalInterwikis, $wgLocalInterwiki );
 }
 
@@ -789,14 +790,6 @@ if ( $wgCommandLineMode ) {
 $wgMemc = ObjectCache::getLocalClusterInstance();
 $messageMemc = wfGetMessageCacheStorage();
 
-wfDebugLog( 'caches',
-       'cluster: ' . get_class( $wgMemc ) .
-       ', WAN: ' . ( $wgMainWANCache === CACHE_NONE ? 'CACHE_NONE' : $wgMainWANCache ) .
-       ', stash: ' . $wgMainStash .
-       ', message: ' . get_class( $messageMemc ) .
-       ', session: ' . get_class( ObjectCache::getInstance( $wgSessionCacheType ) )
-);
-
 // Most of the config is out, some might want to run hooks here.
 Hooks::run( 'SetupAfterCache' );
 
index 3bf87c2..f621e66 100644 (file)
@@ -2955,7 +2955,7 @@ class Title implements LinkTarget, IDBAccessObject {
                }
 
                $dbr = wfGetDB( DB_REPLICA );
-               $conds['page_namespace'] = $this->mNamespace;
+               $conds = [ 'page_namespace' => $this->mNamespace ];
                $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
                $options = [];
                if ( $limit > -1 ) {
@@ -3532,7 +3532,7 @@ class Title implements LinkTarget, IDBAccessObject {
                $method = $auth ? 'moveSubpagesIfAllowed' : 'moveSubpages';
                $result = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
 
-               if ( !$result->isOk() ) {
+               if ( !$result->isOK() ) {
                        return $result->getErrorsArray();
                }
 
index 0363877..dba60f2 100644 (file)
@@ -256,7 +256,7 @@ class WikiMap {
         * Get the wiki ID of a database domain
         *
         * This is like DatabaseDomain::getId() without encoding (for legacy reasons) and
-        * without the schema if it is the generic installer default of "mediawiki"/"dbo"
+        * without the schema if it is the generic installer default of "mediawiki"
         *
         * @see $wgDBmwschema
         * @see PostgresInstaller
@@ -272,7 +272,7 @@ class WikiMap {
                // the installer default then it is probably the case that the schema is the same for
                // all wikis in the farm. Historically, any wiki farm had to make the database/prefix
                // combination unique per wiki. Ommit the schema if it does not seem wiki specific.
-               if ( !in_array( $domain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) {
+               if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) {
                        // This means a site admin may have specifically taylored the schemas.
                        // Domain IDs might use the form <DB>-<project>- or <DB>-<project>-<language>_,
                        // meaning that the schema portion must be accounted for to disambiguate wikis.
index 7a548cc..9a3f75e 100644 (file)
@@ -307,6 +307,9 @@ class ApiAuthManagerHelper {
        /**
         * Clean up a field array for output
         * @param array $fields
+        * @codingStandardsIgnoreStart
+        * @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields
+        * @codingStandardsIgnoreEnd
         * @return array
         */
        private function formatFields( array $fields ) {
index 2c1564e..755f319 100644 (file)
@@ -141,6 +141,7 @@ class ApiBlock extends ApiBase {
                }
 
                list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
+               $res = [];
                $res['user'] = $params['user'];
                $res['userID'] = $target instanceof User ? $target->getId() : 0;
 
index e631e3f..fdf9cf1 100644 (file)
@@ -378,6 +378,7 @@ class ApiEditPage extends ApiBase {
                $status = $ep->attemptSave( $result );
                $wgRequest = $oldRequest;
 
+               $r = [];
                switch ( $status->value ) {
                        case EditPage::AS_HOOK_ERROR:
                        case EditPage::AS_HOOK_ERROR_EXPECTED:
index 641aa9f..574d83b 100644 (file)
@@ -153,6 +153,7 @@ class ApiMain extends ApiBase {
        private $mModule;
 
        private $mCacheMode = 'private';
+       /** @var array */
        private $mCacheControl = [];
        private $mParamsUsed = [];
        private $mParamsSensitive = [];
index d8f48b8..74c6f8f 100644 (file)
@@ -59,9 +59,10 @@ class ApiMove extends ApiBase {
                }
                $toTalk = $toTitle->getTalkPageIfDefined();
 
+               $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
                if ( $toTitle->getNamespace() == NS_FILE
-                       && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle )
-                       && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $toTitle )
+                       && !$repoGroup->getLocalRepo()->findFile( $toTitle )
+                       && $repoGroup->findFile( $toTitle )
                ) {
                        if ( !$params['ignorewarnings'] &&
                                 $this->getPermissionManager()->userHasRight( $user, 'reupload-shared' ) ) {
@@ -207,7 +208,7 @@ class ApiMove extends ApiBase {
                $mp = new MovePage( $fromTitle, $toTitle );
                $result =
                        $mp->moveSubpagesIfAllowed( $this->getUser(), $reason, !$noredirect, $changeTags );
-               if ( !$result->isOk() ) {
+               if ( !$result->isOK() ) {
                        // This means the whole thing failed
                        return [ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $result ) ];
                }
index 6a575ec..0ba4a0e 100644 (file)
@@ -96,6 +96,7 @@ class ApiOpenSearch extends ApiBase {
                        // Trim extracts, if necessary
                        $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' );
                        foreach ( $results as &$r ) {
+                               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
                                        $r['extract'] = self::trimExtract( $r['extract'], $length );
                                }
index c604322..6afb018 100644 (file)
@@ -77,6 +77,7 @@ class ApiPageSet extends ApiBase {
        private $mGeneratorData = []; // [ns][dbkey] => data array
        private $mFakePageId = -1;
        private $mCacheMode = 'public';
+       /** @var array */
        private $mRequestedPageFields = [];
        /** @var int */
        private $mDefaultNamespace = NS_MAIN;
index f82a559..6c1eb0f 100644 (file)
@@ -35,7 +35,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
         */
        private $rootTitle;
 
-       private $params, $cont, $redirect;
+       private $params;
+       /** @var array */
+       private $cont;
+       private $redirect;
        private $bl_ns, $bl_from, $bl_from_ns, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
 
        /**
@@ -306,7 +309,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
                        }
 
                        if ( is_null( $resultPageSet ) ) {
-                               $a['pageid'] = (int)$row->page_id;
+                               $a = [ 'pageid' => (int)$row->page_id ];
                                ApiQueryBase::addTitleInfo( $a, Title::makeTitle( $row->page_namespace, $row->page_title ) );
                                if ( $row->page_is_redirect ) {
                                        $a['redirect'] = true;
@@ -323,6 +326,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
        /**
         * @param ApiPageSet $resultPageSet
         * @return void
+        * @suppress PhanTypeInvalidDimOffset
         */
        private function run( $resultPageSet = null ) {
                $this->params = $this->extractRequestParams( false );
index 547a4e8..79347e6 100644 (file)
@@ -127,6 +127,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
                                'cl_to' . $sort
                        ] );
                }
+               $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
                $res = $this->select( __METHOD__ );
 
index 1af4d95..eb787d1 100644 (file)
@@ -368,7 +368,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
                                $pageID = $newPageID++;
                                $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
-                               $a['revisions'] = [ $rev ];
+                               $a = [ 'revisions' => [ $rev ] ];
                                ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
                                $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
                                ApiQueryBase::addTitleInfo( $a, $title );
index ac7e5cc..98474c7 100644 (file)
@@ -118,6 +118,7 @@ class ApiQueryInfo extends ApiQueryBase {
                return $this->tokenFunctions;
        }
 
+       /** @var string[] */
        protected static $cachedTokens = [];
 
        /**
index ab8d93a..12d7435 100644 (file)
@@ -34,7 +34,9 @@ class ApiQueryUserInfo extends ApiQueryBase {
 
        const WL_UNREAD_LIMIT = 1000;
 
+       /** @var array */
        private $params = [];
+       /** @var array */
        private $prop = [];
 
        public function __construct( ApiQuery $query, $moduleName ) {
index 0718ac8..15c2564 100644 (file)
@@ -86,11 +86,13 @@ class ApiUnblock extends ApiBase {
                        $this->dieStatus( $this->errorArrayToStatus( $retval ) );
                }
 
-               $res['id'] = $block->getId();
                $target = $block->getType() == DatabaseBlock::TYPE_AUTO ? '' : $block->getTarget();
-               $res['user'] = $target instanceof User ? $target->getName() : $target;
-               $res['userid'] = $target instanceof User ? $target->getId() : 0;
-               $res['reason'] = $params['reason'];
+               $res = [
+                       'id' => $block->getId(),
+                       'user' => $target instanceof User ? $target->getName() : $target,
+                       'userid' => $target instanceof User ? $target->getId() : 0,
+                       'reason' => $params['reason']
+               ];
                $this->getResult()->addValue( null, $this->getModuleName(), $res );
        }
 
index ba9be81..9ef17e6 100644 (file)
@@ -84,10 +84,12 @@ class ApiUndelete extends ApiBase {
 
                $this->setWatch( $params['watchlist'], $titleObj );
 
-               $info['title'] = $titleObj->getPrefixedText();
-               $info['revisions'] = (int)$retval[0];
-               $info['fileversions'] = (int)$retval[1];
-               $info['reason'] = $retval[2];
+               $info = [
+                       'title' => $titleObj->getPrefixedText(),
+                       'revisions' => (int)$retval[0],
+                       'fileversions' => (int)$retval[1],
+                       'reason' => $retval[2]
+               ];
                $this->getResult()->addValue( null, $this->getModuleName(), $info );
        }
 
index b15b998..3a54772 100644 (file)
@@ -793,6 +793,7 @@ class ApiUpload extends ApiBase {
                        }
                }
 
+               $result = [];
                // No errors, no warnings: do the upload
                if ( $this->mParams['async'] ) {
                        $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
index 89ec6cb..3aaae70 100644 (file)
@@ -112,6 +112,7 @@ class ApiUserrights extends ApiBase {
 
                $form = $this->getUserRightsPage();
                $form->setContext( $this->getContext() );
+               $r = [];
                $r['user'] = $user->getName();
                $r['userid'] = $user->getId();
                list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups(
index 943149d..c36759a 100644 (file)
@@ -33,6 +33,7 @@ class ApiValidatePassword extends ApiBase {
                        $user = $this->getUser();
                }
 
+               $r = [];
                $validity = $user->checkPasswordValidity( $params['password'] );
                $r['validity'] = $validity->isGood() ? 'Good' : ( $validity->isOK() ? 'Change' : 'Invalid' );
                $messages = array_merge(
index 02abb1e..6f46c56 100644 (file)
@@ -99,6 +99,7 @@ trait SearchApi {
         *
         * @return array array containing available additional api param definitions.
         *  Empty if profiles are not supported by the searchEngine implementation.
+        * @suppress PhanTypeMismatchDimFetch
         */
        private function buildProfileApiParam() {
                $configs = $this->getSearchProfileParams();
@@ -119,6 +120,7 @@ trait SearchApi {
                                if ( isset( $profile['desc-message'] ) ) {
                                        $helpMessages[$profile['name']] = $profile['desc-message'];
                                }
+
                                if ( !empty( $profile['default'] ) ) {
                                        $defaultProfile = $profile['name'];
                                }
index 0ad7687..6e2c1f1 100644 (file)
        "apiwarn-deprecation-missingparam": "Foi usado um formato antigo para a saída, porque <var>$1</var> não foi especificado. Este formato foi descontinuado e de futuro será sempre usado o formato novo.",
        "apiwarn-deprecation-parameter": "O parâmetro <var>$1</var> é obsoleto.",
        "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> está depreciado desde o MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> quando for criar novos documentos HTML, ou <kbd>prop=modules|jsconfigvars</kbd> quando for atualizar um documento no lado do cliente.",
+       "apiwarn-deprecation-post-without-content-type": "Um pedido POST foi feito sem um cabeçalho <code>Content-Type</code>. Isto não funciona de forma fiável.",
        "apiwarn-deprecation-purge-get": "O uso de <kbd>action=purge</kbd> via GET está obsoleto. Use o POST em vez disso.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> está obsoleto. Por favor, use <kbd>$2</kbd> em vez.",
        "apiwarn-difftohidden": "Não foi possível diferenciar r$1: o conteúdo está oculto.",
index f59760a..1a2442c 100644 (file)
@@ -122,6 +122,7 @@ abstract class AuthenticationRequest {
         * a 'password' field).
         *
         * @return array As above
+        * @phan-return array<string,array{type:string,options?:array,value?:string,label:Message,help:Message,optional?:bool,sensitive?:bool,skippable?:bool}>
         */
        abstract public function getFieldInfo();
 
@@ -297,6 +298,7 @@ abstract class AuthenticationRequest {
         * @param AuthenticationRequest[] $reqs
         * @return array
         * @throws \UnexpectedValueException If fields cannot be merged
+        * @suppress PhanTypeInvalidDimOffset
         */
        public static function mergeFieldInfo( array $reqs ) {
                $merged = [];
@@ -337,7 +339,6 @@ abstract class AuthenticationRequest {
                                }
 
                                $options['sensitive'] = !empty( $options['sensitive'] );
-                               // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                                $type = $options['type'];
 
                                if ( !array_key_exists( $name, $merged ) ) {
index 9d0175a..7128fe2 100644 (file)
@@ -40,7 +40,7 @@ class Throttler implements LoggerAwareInterface {
        /**
         * See documentation of $wgPasswordAttemptThrottle for format. Old (pre-1.27) format is not
         * allowed here.
-        * @var array
+        * @var array[]
         * @see https://www.mediawiki.org/wiki/Manual:$wgPasswordAttemptThrottle
         */
        protected $conditions;
@@ -179,7 +179,7 @@ class Throttler implements LoggerAwareInterface {
        /**
         * Handles B/C for $wgPasswordAttemptThrottle.
         * @param array $throttleConditions
-        * @return array
+        * @return array[]
         * @see $wgPasswordAttemptThrottle for structure
         */
        protected static function normalizeThrottleConditions( $throttleConditions ) {
index 4d4bb07..fa91909 100644 (file)
@@ -250,8 +250,9 @@ abstract class AbstractBlock {
         * may be overridden according to global configs.
         *
         * @since 1.33
-        * @param string $right Right to check
-        * @return bool|null null if unrecognized right or unset property
+        * @param string $right
+        * @return bool|null The block applies to the right, or null if
+        *  unsure (e.g. unrecognized right or unset property)
         */
        public function appliesToRight( $right ) {
                $config = RequestContext::getMain()->getConfig();
index 3f3e2d3..6f49f17 100644 (file)
@@ -164,9 +164,28 @@ class CompositeBlock extends AbstractBlock {
 
        /**
         * @inheritDoc
+        *
+        * Determines whether the CompositeBlock applies to a right by checking
+        * whether the original blocks apply to that right. Each block can report
+        * true (applies), false (does not apply) or null (unsure). Then:
+        * - If any original blocks apply, this block applies
+        * - If no original blocks apply but any are unsure, this block is unsure
+        * - If all blocks do not apply, this block does not apply
         */
        public function appliesToRight( $right ) {
-               return $this->methodReturnsValue( __FUNCTION__, true, $right );
+               $isUnsure = false;
+
+               foreach ( $this->originalBlocks as $block ) {
+                       $appliesToRight = $block->appliesToRight( $right );
+
+                       if ( $appliesToRight ) {
+                               return true;
+                       } elseif ( $appliesToRight === null ) {
+                               $isUnsure = true;
+                       }
+               }
+
+               return $isUnsure ? null : false;
        }
 
        /**
index 62cf39e..e461762 100644 (file)
@@ -265,7 +265,7 @@ class EnhancedChangesList extends ChangesList {
                                $block[0], $block[0]->unpatrolled, $block[0]->watched );
                }
 
-               $queryParams['curid'] = $curId;
+               $queryParams = [ 'curid' => $curId ];
 
                # Sub-entries
                $lines = [];
@@ -632,7 +632,7 @@ class EnhancedChangesList extends ChangesList {
        protected function recentChangesBlockLine( $rcObj ) {
                $data = [];
 
-               $query['curid'] = $rcObj->mAttribs['rc_cur_id'];
+               $query = [ 'curid' => $rcObj->mAttribs['rc_cur_id'] ];
 
                $type = $rcObj->mAttribs['rc_type'];
                $logType = $rcObj->mAttribs['rc_log_type'];
index edaa963..1d590d9 100644 (file)
@@ -90,6 +90,7 @@ class RecentChange implements Taggable {
         */
        const SEND_FEED = false;
 
+       /** @var array */
        public $mAttribs = [];
        public $mExtra = [];
 
index 30c2f7a..9ee000d 100644 (file)
@@ -1001,7 +1001,7 @@ class ChangeTags {
                }
                $logEntry->setParameters( $params );
                $logEntry->setRelations( [ 'Tag' => $tag ] );
-               $logEntry->setTags( $logEntryTags );
+               $logEntry->addTags( $logEntryTags );
 
                $logId = $logEntry->insert( $dbw );
                $logEntry->publish( $logId );
index 100fa83..f1df087 100644 (file)
@@ -1260,6 +1260,7 @@ abstract class ContentHandler {
         * @since 1.28
         */
        public function getFieldsForSearchIndex( SearchEngine $engine ) {
+               $fields = [];
                $fields['category'] = $engine->makeSearchFieldMapping(
                        'category',
                        SearchIndexField::INDEX_TYPE_TEXT
index 6a1cc62..f3f9a97 100644 (file)
@@ -11,6 +11,7 @@ use MediaWiki\MediaWikiServices;
 class FileContentHandler extends WikitextContentHandler {
 
        public function getFieldsForSearchIndex( SearchEngine $engine ) {
+               $fields = [];
                $fields['file_media_type'] =
                        $engine->makeSearchFieldMapping( 'file_media_type', SearchIndexField::INDEX_TYPE_KEYWORD );
                $fields['file_media_type']->setFlag( SearchIndexField::FLAG_CASEFOLD );
index ce507d7..6ebec1c 100644 (file)
@@ -47,7 +47,9 @@ use MediaWiki\Diff\ComplexityException;
 class DiffEngine {
 
        // Input variables
+       /** @var string[] */
        private $from;
+       /** @var string[] */
        private $to;
        private $m;
        private $n;
@@ -361,6 +363,7 @@ class DiffEngine {
                         */
                        $max = min( $this->m, $this->n );
                        for ( $forwardBound = 0; $forwardBound < $max
+                               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                && $this->from[$forwardBound] === $this->to[$forwardBound];
                                ++$forwardBound
                        ) {
index c16d9f7..29227c8 100644 (file)
@@ -79,7 +79,7 @@ class MWException extends Exception {
                $res = false;
                if ( $this->useMessageCache() ) {
                        try {
-                               $res = wfMessage( $key, $params )->text();
+                               $res = wfMessage( $key, ...$params )->text();
                        } catch ( Exception $e ) {
                        }
                }
index c52a867..5515ef0 100644 (file)
@@ -199,7 +199,7 @@ class MWExceptionRenderer {
 
                // FIXME: Keep logic in sync with MWException::msg.
                try {
-                       $res = wfMessage( $key, $params )->text();
+                       $res = wfMessage( $key, ...$params )->text();
                } catch ( Exception $e ) {
                        $res = wfMsgReplaceArgs( $fallback, $params );
                        // If an exception happens inside message rendering,
index e474ad3..96df29f 100644 (file)
@@ -115,6 +115,8 @@ class RepoGroup {
         *                   user is allowed to view them. Otherwise, such files will not
         *                   be found.
         *   latest:         If true, load from the latest available data into File objects
+        * @phan-param array{time?:mixed,ignoreRedirect?:bool,private?:bool,latest?:bool} $options
+        * @suppress PhanTypeInvalidDimOffset
         * @return File|bool False if title is not found
         */
        function findFile( $title, $options = [] ) {
index f3116e2..0ef6034 100644 (file)
@@ -344,6 +344,7 @@ class LocalFile extends File {
                                $this->loadFromDB( self::READ_NORMAL );
 
                                $fields = $this->getCacheFields( '' );
+                               $cacheVal = [];
                                $cacheVal['fileExists'] = $this->fileExists;
                                if ( $this->fileExists ) {
                                        foreach ( $fields as $field ) {
@@ -1079,6 +1080,7 @@ class LocalFile extends File {
        /**
         * Delete cached transformed files for the current version only.
         * @param array $options
+        * @phan-param array{forThumbRefresh?:bool} $options
         */
        public function purgeThumbnails( $options = [] ) {
                $files = $this->getThumbnails();
@@ -1090,6 +1092,7 @@ class LocalFile extends File {
                array_shift( $urls ); // don't purge directory
 
                // Give media handler a chance to filter the file purge list
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                if ( !empty( $options['forThumbRefresh'] ) ) {
                        $handler = $this->getHandler();
                        if ( $handler ) {
@@ -1758,7 +1761,7 @@ class LocalFile extends File {
 
                                        # Add change tags, if any
                                        if ( $tags ) {
-                                               $logEntry->setTags( $tags );
+                                               $logEntry->addTags( $tags );
                                        }
 
                                        # Uploads can be patrolled
index fdb3dc4..5173916 100644 (file)
@@ -155,14 +155,13 @@ class DiffHistoryBlob implements HistoryBlob {
                                        $seqName = 'main';
                                }
                        }
-                       $seq =& $sequences[$seqName];
-                       $tail = $seq['tail'];
+
+                       $tail = $sequences[$seqName]['tail'];
                        $diff = $this->diff( $tail, $text );
-                       $seq['diffs'][] = $diff;
-                       $seq['map'][] = $i;
-                       $seq['tail'] = $text;
+                       $sequences[$seqName]['diffs'][] = $diff;
+                       $sequences[$seqName]['map'][] = $i;
+                       $sequences[$seqName]['tail'] = $text;
                }
-               unset( $seq ); // unlink dangerous alias
 
                // Knit the sequences together
                $tail = '';
index f4dad39..04be6c4 100644 (file)
@@ -242,6 +242,10 @@ class HTMLForm extends ContextSource {
 
        protected $mUseMultipart = false;
        protected $mHiddenFields = [];
+       /**
+        * @var array[]
+        * @phan-var array<array{name:string,value:string,label-message?:string,label?:string,label-raw?:string,id?:string,attribs?:array,flags?:string|string[],framed?:bool}>
+        */
        protected $mButtons = [];
 
        protected $mWrapperLegend = false;
@@ -983,6 +987,9 @@ class HTMLForm extends ContextSource {
         *  - attribs: (array, optional) Additional HTML attributes.
         *  - flags: (string|string[], optional) OOUI flags.
         *  - framed: (boolean=true, optional) OOUI framed attribute.
+        * @codingStandardsIgnoreStart
+        * @phan-param array{name:string,value:string,label-message?:string,label?:string,label-raw?:string,id?:string,attribs?:array,flags?:string|string[],framed?:bool} $data
+        * @codingStandardsIgnoreEnd
         * @return HTMLForm $this for chaining calls (since 1.20)
         */
        public function addButton( $data ) {
index 8e5567b..8433df6 100644 (file)
@@ -58,10 +58,14 @@ class HttpRequestFactory {
         *    - password            Password for HTTP Basic Authentication
         *    - originalRequest     Information about the original request (as a WebRequest object or
         *                          an associative array with 'ip' and 'userAgent').
+        * @codingStandardsIgnoreStart
+        * @phan-param array{timeout?:int,connectTimeout?:int,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,logger?:\Psr\Logger\LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string}} $options
+        * @codingStandardsIgnoreEnd
         * @param string $caller The method making this request, for profiling
         * @throws RuntimeException
         * @return MWHttpRequest
         * @see MWHttpRequest::__construct
+        * @suppress PhanUndeclaredTypeParameter
         */
        public function create( $url, array $options = [], $caller = __METHOD__ ) {
                if ( !Http::$httpEngine ) {
index 41ea1dc..3a2f982 100644 (file)
@@ -46,6 +46,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
        protected $sslVerifyCert = true;
        protected $caInfo = null;
        protected $method = "GET";
+       /** @var array */
        protected $reqHeaders = [];
        protected $url;
        protected $parsedUrl;
@@ -63,6 +64,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
        protected $headerList = [];
        protected $respVersion = "0.9";
        protected $respStatus = "200 Ok";
+       /** @var string[][] */
        protected $respHeaders = [];
 
        /** @var StatusValue */
@@ -86,6 +88,9 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
        /**
         * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
         * @param array $options (optional) extra params to pass (see HttpRequestFactory::create())
+        * @codingStandardsIgnoreStart
+        * @phan-param array{timeout?:int,connectTimeout?:int,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,logger?:LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string},method?:string} $options
+        * @codingStandardsIgnoreEnd
         * @param string $caller The method making this request, for profiling
         * @param Profiler|null $profiler An instance of the profiler for profiling, or null
         * @throws Exception
@@ -98,6 +103,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                $this->url = wfExpandUrl( $url, PROTO_HTTP );
                $this->parsedUrl = wfParseUrl( $this->url );
 
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $this->logger = $options['logger'] ?? new NullLogger();
 
                if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
@@ -139,6 +145,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                                // ensure that MWHttpRequest::method is always
                                // uppercased. T38137
                                if ( $o == 'method' ) {
+                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        $options[$o] = strtoupper( $options[$o] );
                                }
                                $this->$o = $options[$o];
index 68f5b9b..0d1cc68 100644 (file)
@@ -738,6 +738,9 @@ class WikiImporter {
                return $this->logItemCallback( $revision );
        }
 
+       /**
+        * @suppress PhanTypeInvalidDimOffset Phan not reading the reference inside the hook
+        */
        private function handlePage() {
                // Handle page data.
                $this->debug( "Enter page handler." );
index 99d594d..424c9d7 100644 (file)
@@ -190,7 +190,7 @@ class CliInstaller extends Installer {
                // PerformInstallation bails on a fatal, so make sure the last item
                // completed before giving 'next.' Likewise, only provide back on failure
                $lastStepStatus = end( $result );
-               if ( $lastStepStatus->isOk() ) {
+               if ( $lastStepStatus->isOK() ) {
                        return Status::newGood();
                } else {
                        return $lastStepStatus;
index de15456..c719c76 100644 (file)
@@ -1270,7 +1270,7 @@ abstract class Installer {
         *
         * @param string $directory Directory to search in, relative to $IP, must be either "extensions"
         *     or "skins"
-        * @return array [ $extName => [ 'screenshots' => [ '...' ] ]
+        * @return array[][] [ $extName => [ 'screenshots' => [ '...' ] ]
         */
        public function findExtensions( $directory = 'extensions' ) {
                switch ( $directory ) {
@@ -1607,11 +1607,11 @@ abstract class Installer {
 
                        // If we've hit some sort of fatal, we need to bail.
                        // Callback already had a chance to do output above.
-                       if ( !$status->isOk() ) {
+                       if ( !$status->isOK() ) {
                                break;
                        }
                }
-               if ( $status->isOk() ) {
+               if ( $status->isOK() ) {
                        $this->showMessage(
                                'config-install-db-success'
                        );
index 03e0e99..c2d349a 100644 (file)
@@ -4,7 +4,8 @@
                        "C.R.",
                        "Chelin",
                        "Macofe",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Ruthven"
                ]
        },
        "config-desc": "'O prugramma d'istallazione 'e MediaWiki",
        "config-db-web-account": "Cunto d' 'o database pe' ne fà acciesso web",
        "config-db-web-help": "Scigliete 'o nomme utente e passwrod ca 'o web server ausarrà pe' se cullegà 'o server database, pe' tramente ca se fa' operazione normale d' 'o wiki.",
        "config-db-web-account-same": "Aúsa 'o stisso cunto comme quanno s'è fatta 'a installazione",
-       "config-db-web-create": "Crìa 'o cunto si nun esiste ancora",
+       "config-db-web-create": "Crìa 'o cunto si nun esiste perzi",
        "config-db-web-no-create-privs": "'O cunto ausato pe' ne fà l'installazione nun tene diritte necessarie pe' ne putè crià n'atu cunto.\n'O cunto zegnàto ccà adda esistere già.",
        "config-mysql-engine": "Mutore d'astipo:",
        "config-mysql-innodb": "InnoDB (fosse 'o cunzigliato)",
index c87dedc..29086e6 100644 (file)
@@ -135,7 +135,7 @@ abstract class Job implements RunnableJob {
                        // When constructing this class for submitting to the queue,
                        // normalise the $title arg of old job classes as part of $params.
                        $params['namespace'] = $title->getNamespace();
-                       $params['title'] = $title->getDBKey();
+                       $params['title'] = $title->getDBkey();
                }
 
                $this->command = $command;
index 06cd04c..7bc97d8 100644 (file)
@@ -397,7 +397,8 @@ class JobQueueGroup {
        }
 
        /**
-        * @return JobQueue[]
+        * @return array[]
+        * @phan-return array<string,array{queue:JobQueue,types:array<string,class-string>}>
         */
        protected function getCoalescedQueues() {
                global $wgJobTypeConf;
index adb4221..709a67b 100644 (file)
@@ -309,7 +309,7 @@ class JobRunner implements LoggerAwareInterface {
                }
                // Always attempt to call teardown() even if Job throws exception.
                try {
-                       $job->teardown( $status );
+                       $job->tearDown( $status );
                } catch ( Exception $e ) {
                        MWExceptionHandler::logException( $e );
                }
index 85e3af9..28e6433 100644 (file)
@@ -108,7 +108,8 @@ class ThumbnailRenderJob extends Job {
 
                // T203135 We don't wait for the request to complete, as this is mostly fire & forget.
                // Looking at the HTTP status of requests that take less than 1s is a sanity check.
-               $request = MWHttpRequest::factory( $thumbUrl,
+               $request = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
+                       $thumbUrl,
                        [ 'method' => 'HEAD', 'followRedirects' => true, 'timeout' => 1 ],
                        __METHOD__
                );
index 4a62e72..9d53a86 100644 (file)
@@ -45,9 +45,10 @@ class MappedIterator extends FilterIterator {
         * the base iterator (post-callback) and will return true if that value should be
         * included in iteration of the MappedIterator (otherwise it will be filtered out).
         *
-        * @param Iterator|Array $iter
+        * @param Iterator|array $iter
         * @param callable $vCallback Value transformation callback
         * @param array $options Options map (includes "accept") (since 1.22)
+        * @phan-param array{accept?:callable} $options
         * @throws UnexpectedValueException
         */
        public function __construct( $iter, $vCallback, array $options = [] ) {
@@ -60,6 +61,7 @@ class MappedIterator extends FilterIterator {
                }
                parent::__construct( $baseIterator );
                $this->vCallback = $vCallback;
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $this->aCallback = $options['accept'] ?? null;
        }
 
index 90e52f0..56e6b19 100644 (file)
@@ -43,13 +43,13 @@ class XhprofData {
 
        /**
         * Per-function inclusive data.
-        * @var array $inclusive
+        * @var array[] $inclusive
         */
        protected $inclusive;
 
        /**
         * Per-function inclusive and exclusive data.
-        * @var array $complete
+        * @var array[] $complete
         */
        protected $complete;
 
@@ -153,7 +153,7 @@ class XhprofData {
         * - max: Maximum value
         * - variance: Variance (spread) of the values
         *
-        * @return array
+        * @return array[]
         * @see getRawData()
         * @see getCompleteMetrics()
         */
@@ -239,7 +239,7 @@ class XhprofData {
         * metrics have an additional 'exclusive' measurement which is the total
         * minus the totals of all child function calls.
         *
-        * @return array
+        * @return array[]
         * @see getRawData()
         * @see getInclusiveMetrics()
         */
index 905e925..428fec6 100644 (file)
@@ -428,7 +428,11 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - b) predicted operation errors occurred and 'force' was not set
         *
         * @param array $ops List of operations to execute in order
+        * @codingStandardsIgnoreStart
+        * @phan-param array{ignoreMissingSource?:bool,overwrite?:bool,overwriteSame?:bool,headers?:bool} $ops
         * @param array $opts Batch operation options
+        * @phan-param array{force?:bool,nonLocking?:bool,nonJournaled?:bool,parallelize?:bool,bypassReadOnly?:bool,preserveCache?:bool} $opts
+        * @codingStandardsIgnoreEnd
         * @return StatusValue
         */
        final public function doOperations( array $ops, array $opts = [] ) {
@@ -666,7 +670,9 @@ abstract class FileBackend implements LoggerAwareInterface {
         * considered "OK" as long as no fatal errors occurred.
         *
         * @param array $ops Set of operations to execute
+        * @phan-param array{ignoreMissingSource?:bool,headers?:bool} $ops
         * @param array $opts Batch operation options
+        * @phan-param array{bypassReadOnly?:bool} $opts
         * @return StatusValue
         * @since 1.20
         */
index 27ad870..9bfb5c5 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
 /**
  * @brief Proxy backend that mirrors writes to several internal backends.
  *
@@ -57,9 +59,11 @@ class FileBackendMultiWrite extends FileBackend {
        /** @var bool */
        protected $asyncWrites = false;
 
-       /* Possible internal backend consistency checks */
+       /** @var int Compare file sizes among backends */
        const CHECK_SIZE = 1;
+       /** @var int Compare file mtimes among backends */
        const CHECK_TIME = 2;
+       /** @var int Compare file hashes among backends */
        const CHECK_SHA1 = 4;
 
        /**
@@ -139,10 +143,8 @@ class FileBackendMultiWrite extends FileBackend {
 
                $mbe = $this->backends[$this->masterIndex]; // convenience
 
-               // Try to lock those files for the scope of this function...
-               $scopeLock = null;
+               // Acquire any locks as needed
                if ( empty( $opts['nonLocking'] ) ) {
-                       // Try to lock those files for the scope of this function...
                        /** @noinspection PhpUnusedLocalVariableInspection */
                        $scopeLock = $this->getScopedLocksForOps( $ops, $status );
                        if ( !$status->isOK() ) {
@@ -152,28 +154,30 @@ class FileBackendMultiWrite extends FileBackend {
                // Clear any cache entries (after locks acquired)
                $this->clearCache();
                $opts['preserveCache'] = true; // only locked files are cached
-               // Get the list of paths to read/write...
+               // Get the list of paths to read/write
                $relevantPaths = $this->fileStoragePathsForOps( $ops );
-               // Check if the paths are valid and accessible on all backends...
+               // Check if the paths are valid and accessible on all backends
                $status->merge( $this->accessibilityCheck( $relevantPaths ) );
                if ( !$status->isOK() ) {
                        return $status; // abort
                }
-               // Do a consistency check to see if the backends are consistent...
+               // Do a consistency check to see if the backends are consistent
                $syncStatus = $this->consistencyCheck( $relevantPaths );
                if ( !$syncStatus->isOK() ) {
-                       wfDebugLog( 'FileOperation', static::class .
-                               " failed sync check: " . FormatJson::encode( $relevantPaths ) );
-                       // Try to resync the clone backends to the master on the spot...
-                       if ( $this->autoResync === false
-                               || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
+                       $this->logger->error(
+                               __METHOD__ . ": failed sync check: " . FormatJson::encode( $relevantPaths )
+                       );
+                       // Try to resync the clone backends to the master on the spot
+                       if (
+                               $this->autoResync === false ||
+                               !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
                        ) {
                                $status->merge( $syncStatus );
 
                                return $status; // abort
                        }
                }
-               // Actually attempt the operation batch on the master backend...
+               // Actually attempt the operation batch on the master backend
                $realOps = $this->substOpBatchPaths( $ops, $mbe );
                $masterStatus = $mbe->doOperations( $realOps, $opts );
                $status->merge( $masterStatus );
@@ -191,16 +195,18 @@ class FileBackendMultiWrite extends FileBackend {
                                        // Bind $scopeLock to the callback to preserve locks
                                        DeferredUpdates::addCallableUpdate(
                                                function () use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) {
-                                                       wfDebugLog( 'FileOperationReplication',
+                                                       $this->logger->error(
                                                                "'{$backend->getName()}' async replication; paths: " .
-                                                               FormatJson::encode( $relevantPaths ) );
+                                                               FormatJson::encode( $relevantPaths )
+                                                       );
                                                        $backend->doOperations( $realOps, $opts );
                                                }
                                        );
                                } else {
-                                       wfDebugLog( 'FileOperationReplication',
+                                       $this->logger->error(
                                                "'{$backend->getName()}' sync replication; paths: " .
-                                               FormatJson::encode( $relevantPaths ) );
+                                               FormatJson::encode( $relevantPaths )
+                                       );
                                        $status->merge( $backend->doOperations( $realOps, $opts ) );
                                }
                        }
@@ -218,6 +224,9 @@ class FileBackendMultiWrite extends FileBackend {
        /**
         * Check that a set of files are consistent across all internal backends
         *
+        * This method should only be called if the files are locked or the backend
+        * is in read-only mode
+        *
         * @param array $paths List of storage paths
         * @return StatusValue
         */
@@ -227,58 +236,75 @@ class FileBackendMultiWrite extends FileBackend {
                        return $status; // skip checks
                }
 
-               // Preload all of the stat info in as few round trips as possible...
+               // Preload all of the stat info in as few round trips as possible
                foreach ( $this->backends as $backend ) {
                        $realPaths = $this->substPaths( $paths, $backend );
                        $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
                }
 
-               $mBackend = $this->backends[$this->masterIndex];
                foreach ( $paths as $path ) {
                        $params = [ 'src' => $path, 'latest' => true ];
-                       $mParams = $this->substOpPaths( $params, $mBackend );
-                       // Stat the file on the 'master' backend
-                       $mStat = $mBackend->getFileStat( $mParams );
+                       // Get the state of the file on the master backend
+                       $masterBackend = $this->backends[$this->masterIndex];
+                       $masterParams = $this->substOpPaths( $params, $masterBackend );
+                       $masterStat = $masterBackend->getFileStat( $masterParams );
+                       if ( $masterStat === self::UNKNOWN ) {
+                               $status->fatal( 'backend-fail-stat', $path );
+                               continue;
+                       }
                        if ( $this->syncChecks & self::CHECK_SHA1 ) {
-                               $mSha1 = $mBackend->getFileSha1Base36( $mParams );
+                               $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
+                               if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
+                                       $status->fatal( 'backend-fail-hash', $path );
+                                       continue;
+                               }
                        } else {
-                               $mSha1 = false;
+                               $masterSha1 = null; // unused
                        }
+
                        // Check if all clone backends agree with the master...
-                       foreach ( $this->backends as $index => $cBackend ) {
+                       foreach ( $this->backends as $index => $cloneBackend ) {
                                if ( $index === $this->masterIndex ) {
                                        continue; // master
                                }
-                               $cParams = $this->substOpPaths( $params, $cBackend );
-                               $cStat = $cBackend->getFileStat( $cParams );
-                               if ( $mStat ) { // file is in master
-                                       if ( !$cStat ) { // file should exist
+
+                               // Get the state of the file on the clone backend
+                               $cloneParams = $this->substOpPaths( $params, $cloneBackend );
+                               $cloneStat = $cloneBackend->getFileStat( $cloneParams );
+
+                               if ( $masterStat ) {
+                                       // File exists in the master backend
+                                       if ( !$cloneStat ) {
+                                               // File is missing from the clone backend
                                                $status->fatal( 'backend-fail-synced', $path );
-                                               continue;
-                                       }
-                                       if ( ( $this->syncChecks & self::CHECK_SIZE )
-                                               && $cStat['size'] != $mStat['size']
-                                       ) { // wrong size
+                                       } elseif (
+                                               ( $this->syncChecks & self::CHECK_SIZE ) &&
+                                               $cloneStat['size'] !== $masterStat['size']
+                                       ) {
+                                               // File in the clone backend is different
                                                $status->fatal( 'backend-fail-synced', $path );
-                                               continue;
-                                       }
-                                       if ( $this->syncChecks & self::CHECK_TIME ) {
-                                               $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
-                                               $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
-                                               if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
-                                                       $status->fatal( 'backend-fail-synced', $path );
-                                                       continue;
-                                               }
-                                       }
-                                       if (
+                                       } elseif (
+                                               ( $this->syncChecks & self::CHECK_TIME ) &&
+                                               abs(
+                                                       ConvertibleTimestamp::convert( TS_UNIX, $masterStat['mtime'] ) -
+                                                       ConvertibleTimestamp::convert( TS_UNIX, $cloneStat['mtime'] )
+                                               ) > 30
+                                       ) {
+                                               // File in the clone backend is significantly newer or older
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                       } elseif (
                                                ( $this->syncChecks & self::CHECK_SHA1 ) &&
-                                               $cBackend->getFileSha1Base36( $cParams ) !== $mSha1
-                                       ) { // wrong SHA1
+                                               $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1
+                                       ) {
+                                               // File in the clone backend is different
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                       }
+                               } else {
+                                       // File does not exist in the master backend
+                                       if ( $cloneStat ) {
+                                               // Stray file exists in the clone backend
                                                $status->fatal( 'backend-fail-synced', $path );
-                                               continue;
                                        }
-                               } elseif ( $cStat ) { // file is not in master; file should not exist
-                                       $status->fatal( 'backend-fail-synced', $path );
                                }
                        }
                }
@@ -314,6 +340,8 @@ class FileBackendMultiWrite extends FileBackend {
         * Check that a set of files are consistent across all internal backends
         * and re-synchronize those files against the "multi master" if needed.
         *
+        * This method should only be called if the files are locked
+        *
         * @param array $paths List of storage paths
         * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
         * @return StatusValue
@@ -321,58 +349,83 @@ class FileBackendMultiWrite extends FileBackend {
        public function resyncFiles( array $paths, $resyncMode = true ) {
                $status = $this->newStatus();
 
-               $mBackend = $this->backends[$this->masterIndex];
+               $fname = __METHOD__;
                foreach ( $paths as $path ) {
-                       $mPath = $this->substPaths( $path, $mBackend );
-                       $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] );
-                       $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] );
-                       if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
-                               $status->fatal( 'backend-fail-internal', $this->name );
-                               wfDebugLog( 'FileOperation', __METHOD__
-                                       . ': File is not available on the master backend' );
-                               continue; // file is not available on the master backend...
+                       $params = [ 'src' => $path, 'latest' => true ];
+                       // Get the state of the file on the master backend
+                       $masterBackend = $this->backends[$this->masterIndex];
+                       $masterParams = $this->substOpPaths( $params, $masterBackend );
+                       $masterPath = $masterParams['src'];
+                       $masterStat = $masterBackend->getFileStat( $masterParams );
+                       if ( $masterStat === self::UNKNOWN ) {
+                               $status->fatal( 'backend-fail-stat', $path );
+                               $this->logger->error( "$fname: file '$masterPath' is not available" );
+                               continue;
+                       }
+                       $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
+                       if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
+                               $status->fatal( 'backend-fail-hash', $path );
+                               $this->logger->error( "$fname: file '$masterPath' hash does not match stat" );
+                               continue;
                        }
+
                        // Check of all clone backends agree with the master...
-                       foreach ( $this->backends as $index => $cBackend ) {
+                       foreach ( $this->backends as $index => $cloneBackend ) {
                                if ( $index === $this->masterIndex ) {
                                        continue; // master
                                }
-                               $cPath = $this->substPaths( $path, $cBackend );
-                               $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] );
-                               $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] );
-                               if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
-                                       $status->fatal( 'backend-fail-internal', $cBackend->getName() );
-                                       wfDebugLog( 'FileOperation', __METHOD__ .
-                                               ': File is not available on the clone backend' );
-                                       continue; // file is not available on the clone backend...
+
+                               // Get the state of the file on the clone backend
+                               $cloneParams = $this->substOpPaths( $params, $cloneBackend );
+                               $clonePath = $cloneParams['src'];
+                               $cloneStat = $cloneBackend->getFileStat( $cloneParams );
+                               if ( $cloneStat === self::UNKNOWN ) {
+                                       $status->fatal( 'backend-fail-stat', $path );
+                                       $this->logger->error( "$fname: file '$clonePath' is not available" );
+                                       continue;
                                }
-                               if ( $mSha1 === $cSha1 ) {
-                                       // already synced; nothing to do
-                               } elseif ( $mSha1 !== false ) { // file is in master
-                                       if ( $resyncMode === 'conservative'
-                                               && $cStat && $cStat['mtime'] > $mStat['mtime']
+                               $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams );
+                               if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) {
+                                       $status->fatal( 'backend-fail-hash', $path );
+                                       $this->logger->error( "$fname: file '$clonePath' hash does not match stat" );
+                                       continue;
+                               }
+
+                               if ( $masterSha1 === $cloneSha1 ) {
+                                       // File is either the same in both backends or absent from both backends
+                                       $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" );
+                               } elseif ( $masterSha1 !== false ) {
+                                       // File is either missing from or different in the clone backend
+                                       if (
+                                               $resyncMode === 'conservative' &&
+                                               $cloneStat &&
+                                               $cloneStat['mtime'] > $masterStat['mtime']
                                        ) {
+                                               // Do not replace files with older ones; reduces the risk of data loss
                                                $status->fatal( 'backend-fail-synced', $path );
-                                               continue; // don't rollback data
+                                       } else {
+                                               // Copy the master backend file to the clone backend in overwrite mode
+                                               $fsFile = $masterBackend->getLocalReference( $masterParams );
+                                               $status->merge( $cloneBackend->quickStore( [
+                                                       'src' => $fsFile,
+                                                       'dst' => $clonePath
+                                               ] ) );
                                        }
-                                       $fsFile = $mBackend->getLocalReference(
-                                               [ 'src' => $mPath, 'latest' => true ] );
-                                       $status->merge( $cBackend->quickStore(
-                                               [ 'src' => $fsFile->getPath(), 'dst' => $cPath ]
-                                       ) );
-                               } elseif ( $mStat === false ) { // file is not in master
+                               } elseif ( $masterStat === false ) {
+                                       // Stray file exists in the clone backend
                                        if ( $resyncMode === 'conservative' ) {
+                                               // Do not delete stray files; reduces the risk of data loss
                                                $status->fatal( 'backend-fail-synced', $path );
-                                               continue; // don't delete data
+                                       } else {
+                                               // Delete the stay file from the clone backend
+                                               $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) );
                                        }
-                                       $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) );
                                }
                        }
                }
 
                if ( !$status->isOK() ) {
-                       wfDebugLog( 'FileOperation', static::class .
-                               " failed to resync: " . FormatJson::encode( $paths ) );
+                       $this->logger->error( "$fname: failed to resync: " . FormatJson::encode( $paths ) );
                }
 
                return $status;
@@ -488,7 +541,7 @@ class FileBackendMultiWrite extends FileBackend {
 
        protected function doQuickOperationsInternal( array $ops ) {
                $status = $this->newStatus();
-               // Do the operations on the master backend; setting StatusValue fields...
+               // Do the operations on the master backend; setting StatusValue fields
                $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
                $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
                $status->merge( $masterStatus );
index ed81a79..6b1ef89 100644 (file)
@@ -188,8 +188,12 @@ class MultiHttpClient implements LoggerAwareInterface {
         *   - reqTimeout      : post-connection timeout per request (seconds)
         *   - usePipelining   : whether to use HTTP pipelining if possible
         *   - maxConnsPerHost : maximum number of concurrent connections (per host)
+        * @codingStandardsIgnoreStart
+        * @phan-param array{connTimeout?:int,reqTimeout?:int,usePipelining?:bool,maxConnsPerHost?:int} $opts
+        * @codingStandardsIgnoreEnd
         * @return array $reqs With response array populated for each
         * @throws Exception
+        * @suppress PhanTypeInvalidDimOffset
         */
        private function runMultiCurl( array $reqs, array $opts = [] ) {
                $chm = $this->getCurlMulti();
@@ -400,6 +404,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                                $name = strtolower( $name );
                                $value = trim( $value );
                                if ( isset( $req['response']['headers'][$name] ) ) {
+                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        $req['response']['headers'][$name] .= ', ' . $value;
                                } else {
                                        $req['response']['headers'][$name] = $value;
@@ -499,7 +504,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                                'error' => '',
                        ];
 
-                       if ( !$sv->isOk() ) {
+                       if ( !$sv->isOK() ) {
                                $svErrors = $sv->getErrors();
                                if ( isset( $svErrors[0] ) ) {
                                        $req['response']['error'] = $svErrors[0]['message'];
@@ -508,6 +513,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                                        if ( isset( $svErrors[0]['params'][0] ) ) {
                                                if ( is_numeric( $svErrors[0]['params'][0] ) ) {
                                                        if ( isset( $svErrors[0]['params'][1] ) ) {
+                                                               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                                                $req['response']['reason'] = $svErrors[0]['params'][1];
                                                        }
                                                } else {
index 42da5f0..ad3f681 100644 (file)
@@ -91,6 +91,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         *   - asyncHandler: Callable to use for scheduling tasks after the web request ends.
         *      In CLI mode, it should run the task immediately.
         * @param array $params
+        * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params
         */
        public function __construct( array $params = [] ) {
                $this->setLogger( $params['logger'] ?? new NullLogger() );
index 6d0940b..0f7011d 100644 (file)
@@ -47,6 +47,10 @@ class HashBagOStuff extends MediumSpecificBagOStuff {
        /**
         * @param array $params Additional parameters include:
         *   - maxKeys : only allow this many keys (using oldest-first eviction)
+        * @codingStandardsIgnoreStart
+        * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int,maxKeys?:int} $params
+        * @codingStandardsIgnoreEnd
+        * @suppress PhanTypeInvalidDimOffset
         */
        function __construct( $params = [] ) {
                $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
index 9d36187..252c089 100644 (file)
@@ -73,6 +73,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         *      This should be configured to a reasonable size give the site traffic and the
         *      amount of I/O between application and cache servers that the network can handle.
         * @param array $params
+        * @codingStandardsIgnoreStart
+        * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int} $params
+        * @codingStandardsIgnoreEnd
         */
        public function __construct( array $params = [] ) {
                parent::__construct( $params );
index dc40931..40f2836 100644 (file)
@@ -49,29 +49,25 @@ abstract class MemcachedBagOStuff extends MediumSpecificBagOStuff {
                // custom prefixes used by thing like WANObjectCache, limit to 205.
                $charsLeft = 205 - strlen( $keyspace ) - count( $args );
 
-               $args = array_map(
-                       function ( $arg ) use ( &$charsLeft ) {
-                               $arg = strtr( $arg, ' ', '_' );
+               foreach ( $args as &$arg ) {
+                       $arg = strtr( $arg, ' ', '_' );
 
-                               // Make sure %, #, and non-ASCII chars are escaped
-                               $arg = preg_replace_callback(
-                                       '/[^\x21-\x22\x24\x26-\x39\x3b-\x7e]+/',
-                                       function ( $m ) {
-                                               return rawurlencode( $m[0] );
-                                       },
-                                       $arg
-                               );
+                       // Make sure %, #, and non-ASCII chars are escaped
+                       $arg = preg_replace_callback(
+                               '/[^\x21-\x22\x24\x26-\x39\x3b-\x7e]+/',
+                               function ( $m ) {
+                                       return rawurlencode( $m[0] );
+                               },
+                               $arg
+                       );
 
-                               // 33 = 32 characters for the MD5 + 1 for the '#' prefix.
-                               if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) {
-                                       $arg = '#' . md5( $arg );
-                               }
+                       // 33 = 32 characters for the MD5 + 1 for the '#' prefix.
+                       if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) {
+                               $arg = '#' . md5( $arg );
+                       }
 
-                               $charsLeft -= strlen( $arg );
-                               return $arg;
-                       },
-                       $args
-               );
+                       $charsLeft -= strlen( $arg );
+               }
 
                if ( $charsLeft < 0 ) {
                        return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
index d0aa380..51f7316 100644 (file)
@@ -61,6 +61,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         *      invalidation uses logical TTLs, invalidation uses etag/timestamp
         *      validation against the DB, or merge() is used to handle races.
         * @param array $params
+        * @phan-param array{caches:array<int,array|BagOStuff>,replication:string} $params
         * @throws InvalidArgumentException
         */
        public function __construct( $params ) {
index 0b5ac46..ff87829 100644 (file)
@@ -33,16 +33,26 @@ use Wikimedia\ObjectFactory;
  */
 class ReplicatedBagOStuff extends BagOStuff {
        /** @var BagOStuff */
-       protected $writeStore;
+       private $writeStore;
        /** @var BagOStuff */
-       protected $readStore;
+       private $readStore;
+
+       /** @var int Seconds to read from the master source for a key after writing to it */
+       private $consistencyWindow;
+       /** @var float[] Map of (key => UNIX timestamp) */
+       private $lastKeyWrites = [];
+
+       /** @var int Max expected delay (in seconds) for writes to reach replicas */
+       const MAX_WRITE_DELAY = 5;
 
        /**
         * Constructor. Parameters are:
-        *   - writeFactory : ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
-        *                    This object will be used for writes (e.g. the master DB).
-        *   - readFactory  : ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
-        *                    This object will be used for reads (e.g. a replica DB).
+        *   - writeFactory: ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
+        *      This object will be used for writes (e.g. the master DB).
+        *   - readFactory: ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
+        *      This object will be used for reads (e.g. a replica DB).
+        *   - sessionConsistencyWindow: Seconds to read from the master source for a key
+        *      after writing to it. [Default: ReplicatedBagOStuff::MAX_WRITE_DELAY]
         *
         * @param array $params
         * @throws InvalidArgumentException
@@ -53,19 +63,18 @@ class ReplicatedBagOStuff extends BagOStuff {
                if ( !isset( $params['writeFactory'] ) ) {
                        throw new InvalidArgumentException(
                                __METHOD__ . ': the "writeFactory" parameter is required' );
-               }
-               if ( !isset( $params['readFactory'] ) ) {
+               } elseif ( !isset( $params['readFactory'] ) ) {
                        throw new InvalidArgumentException(
                                __METHOD__ . ': the "readFactory" parameter is required' );
                }
 
-               $opts = [ 'reportDupes' => false ]; // redundant
+               $this->consistencyWindow = $params['sessionConsistencyWindow'] ?? self::MAX_WRITE_DELAY;
                $this->writeStore = ( $params['writeFactory'] instanceof BagOStuff )
                        ? $params['writeFactory']
-                       : ObjectFactory::getObjectFromSpec( $opts + $params['writeFactory'] );
+                       : ObjectFactory::getObjectFromSpec( $params['writeFactory'] );
                $this->readStore = ( $params['readFactory'] instanceof BagOStuff )
                        ? $params['readFactory']
-                       : ObjectFactory::getObjectFromSpec( $opts + $params['readFactory'] );
+                       : ObjectFactory::getObjectFromSpec( $params['readFactory'] );
                $this->attrMap = $this->mergeFlagMaps( [ $this->readStore, $this->writeStore ] );
        }
 
@@ -76,28 +85,41 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               return $this->fieldHasFlags( $flags, self::READ_LATEST )
+               return (
+                       $this->hadRecentSessionWrite( [ $key ] ) ||
+                       $this->fieldHasFlags( $flags, self::READ_LATEST )
+               )
                        ? $this->writeStore->get( $key, $flags )
                        : $this->readStore->get( $key, $flags );
        }
 
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->set( $key, $value, $exptime, $flags );
        }
 
        public function delete( $key, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->delete( $key, $flags );
        }
 
        public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->add( $key, $value, $exptime, $flags );
        }
 
        public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->merge( $key, $callback, $exptime, $attempts, $flags );
        }
 
        public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->changeTTL( $key, $exptime, $flags );
        }
 
@@ -118,37 +140,52 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
-               return $this->fieldHasFlags( $flags, self::READ_LATEST )
+               return (
+                       $this->hadRecentSessionWrite( $keys ) ||
+                       $this->fieldHasFlags( $flags, self::READ_LATEST )
+               )
                        ? $this->writeStore->getMulti( $keys, $flags )
                        : $this->readStore->getMulti( $keys, $flags );
        }
 
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( array_keys( $data ) );
+
                return $this->writeStore->setMulti( $data, $exptime, $flags );
        }
 
        public function deleteMulti( array $keys, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( $keys );
+
                return $this->writeStore->deleteMulti( $keys, $flags );
        }
 
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( $keys );
+
                return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
        }
 
        public function incr( $key, $value = 1, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->incr( $key, $value, $flags );
        }
 
        public function decr( $key, $value = 1, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->decr( $key, $value, $flags );
        }
 
        public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
+               $this->remarkRecentSessionWrite( [ $key ] );
+
                return $this->writeStore->incrWithInit( $key, $exptime, $value, $init, $flags );
        }
 
        public function getLastError() {
-               return ( $this->writeStore->getLastError() != self::ERR_NONE )
+               return ( $this->writeStore->getLastError() !== self::ERR_NONE )
                        ? $this->writeStore->getLastError()
                        : $this->readStore->getLastError();
        }
@@ -179,4 +216,40 @@ class ReplicatedBagOStuff extends BagOStuff {
                $this->writeStore->setMockTime( $time );
                $this->readStore->setMockTime( $time );
        }
+
+       /**
+        * @param string[] $keys
+        * @return bool
+        */
+       private function hadRecentSessionWrite( array $keys ) {
+               $now = $this->getCurrentTime();
+               foreach ( $keys as $key ) {
+                       $ts = $this->lastKeyWrites[$key] ?? 0;
+                       if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * @param string[] $keys
+        */
+       private function remarkRecentSessionWrite( array $keys ) {
+               $now = $this->getCurrentTime();
+               foreach ( $keys as $key ) {
+                       unset( $this->lastKeyWrites[$key] ); // move to the end
+                       $this->lastKeyWrites[$key] = $now;
+               }
+               // Prune out the map if the first key is obsolete
+               if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) {
+                       $this->lastKeyWrites = array_filter(
+                               $this->lastKeyWrites,
+                               function ( $timestamp ) use ( $now ) {
+                                       return ( ( $now - $timestamp ) <= $this->consistencyWindow );
+                               }
+                       );
+               }
+       }
 }
index b88b496..2ce216d 100644 (file)
@@ -578,10 +578,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *   - version: Integer version number signifiying the format of the value.
         *      Default: null
         *   - walltime: How long the value took to generate in seconds. Default: 0.0
+        * @codingStandardsIgnoreStart
+        * @phan-param array{lag?:int,since?:int,pending?:bool,lockTSE?:int,staleTTL?:int,creating?:bool,version?:?string,walltime?:int|float} $opts
+        * @codingStandardsIgnoreEnd
         * @note Options added in 1.28: staleTTL
         * @note Options added in 1.33: creating
         * @note Options added in 1.34: version, walltime
         * @return bool Success
+        * @suppress PhanTypeInvalidDimOffset
         */
        final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
                $now = $this->getCurrentTime();
@@ -1246,11 +1250,15 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *      most sense for values that are moderately to highly expensive to regenerate and easy
         *      to query for dependency timestamps. The use of "pcTTL" reduces timestamp queries.
         *      Default: null.
+        * @codingStandardsIgnoreStart
+        * @phan-param array{checkKeys?:string[],graceTTL?:int,lockTSE?:int,busyValue?:mixed,pcTTL?:int,pcGroup?:string,version?:int,minAsOf?:int,hotTTR?:int,lowTTL?:int,ageNew?:int,staleTTL?:int,touchedCallback?:callable} $opts
+        * @codingStandardsIgnoreEnd
         * @return mixed Value found or written to the key
         * @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf
         * @note Options added in 1.31: staleTTL, graceTTL
         * @note Options added in 1.33: touchedCallback
         * @note Callable type hints are not used to avoid class-autoloading
+        * @suppress PhanTypeInvalidDimOffset
         */
        final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
                $version = $opts['version'] ?? null;
@@ -1305,6 +1313,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *   - Cached or regenerated value version number or null if not versioned
         *   - Timestamp of the current cached value at the key or null if there is no value
         * @note Callable type hints are not used to avoid class-autoloading
+        * @suppress PhanTypeArraySuspicious
         */
        private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
                $checkKeys = $opts['checkKeys'] ?? [];
@@ -1441,6 +1450,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                                $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
                        } else {
                                $finalSetOpts = [
+                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        'since' => $setOpts['since'] ?? $preCallbackTime,
                                        'version' => $version,
                                        'staleTTL' => $staleTTL,
@@ -2421,6 +2431,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *   - curTTL: remaining time-to-live (negative if tombstoned) or null if there is no value
         *   - version: value version number or null if the if there is no value
         *   - tombAsOf: UNIX timestamp of the tombstone or null if there is no tombstone
+        * @phan-return array{0:mixed,1:array{asOf:?mixed,curTTL:?int|float,version:?mixed,tombAsOf:?mixed}}
         */
        private function unwrap( $wrapped, $now ) {
                $value = false;
index b5ec652..6aa3f6f 100644 (file)
@@ -1487,6 +1487,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->trxAtomicCounter = 0;
                $this->trxIdleCallbacks = []; // T67263; transaction already lost
                $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
+               // Clear additional subclass fields
+               $this->doHandleSessionLossPreconnect();
                // @note: leave trxRecurringCallbacks in place
                if ( $this->trxDoneWrites ) {
                        $this->trxProfiler->transactionWritingOut(
@@ -1499,6 +1501,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
+       /**
+        * Reset any additional subclass trx* and session* fields
+        */
+       protected function doHandleSessionLossPreconnect() {
+               // no-op
+       }
+
        /**
         * Clean things up after session (and thus transaction) loss after reconnect
         */
index 2977291..657d308 100644 (file)
@@ -55,7 +55,7 @@ class DatabaseSqlite extends Database {
        protected $lockMgr;
 
        /** @var array List of shared database already attached to this connection */
-       private $alreadyAttached = [];
+       private $sessionAttachedDbs = [];
 
        /** @var string[] See https://www.sqlite.org/lang_transaction.html */
        private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
@@ -182,6 +182,7 @@ class DatabaseSqlite extends Database {
                        if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
                                $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
                        }
+                       $this->attachDatabasesFromTableAliases();
                } catch ( Exception $e ) {
                        throw $this->newExceptionAfterConnectError( $e->getMessage() );
                }
@@ -1124,12 +1125,23 @@ class DatabaseSqlite extends Database {
 
        public function setTableAliases( array $aliases ) {
                parent::setTableAliases( $aliases );
+               if ( $this->isOpen() ) {
+                       $this->attachDatabasesFromTableAliases();
+               }
+       }
+
+       /**
+        * Issue ATTATCH statements for all unattached foreign DBs in table aliases
+        */
+       private function attachDatabasesFromTableAliases() {
                foreach ( $this->tableAliases as $params ) {
-                       if ( isset( $this->alreadyAttached[$params['dbname']] ) ) {
-                               continue;
+                       if (
+                               $params['dbname'] !== $this->getDBname() &&
+                               !isset( $this->sessionAttachedDbs[$params['dbname']] )
+                       ) {
+                               $this->attachDatabase( $params['dbname'] );
+                               $this->sessionAttachedDbs[$params['dbname']] = true;
                        }
-                       $this->attachDatabase( $params['dbname'] );
-                       $this->alreadyAttached[$params['dbname']] = true;
                }
        }
 
@@ -1147,6 +1159,10 @@ class DatabaseSqlite extends Database {
                return true;
        }
 
+       protected function doHandleSessionLossPreconnect() {
+               $this->sessionAttachedDbs = [];
+       }
+
        /**
         * @return PDO
         */
index 771700c..f60e8db 100644 (file)
@@ -1303,23 +1303,7 @@ class LoadBalancer implements ILoadBalancer {
                $server['flags'] = $server['flags'] ?? IDatabase::DBO_DEFAULT;
 
                // Create a live connection object
-               try {
-                       $conn = Database::factory( $server['type'], $server );
-                       // Log when many connection are made on requests
-                       ++$this->connectionCounter;
-                       $currentConnCount = $this->getCurrentConnectionCount();
-                       if ( $currentConnCount >= self::CONN_HELD_WARN_THRESHOLD ) {
-                               $this->perfLogger->warning(
-                                       __METHOD__ . ": {connections}+ connections made (master={masterdb})",
-                                       [ 'connections' => $currentConnCount, 'masterdb' => $masterName ]
-                               );
-                       }
-               } catch ( DBConnectionError $e ) {
-                       // FIXME: This is probably the ugliest thing I have ever done to
-                       // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
-                       $conn = $e->db;
-               }
-
+               $conn = Database::factory( $server['type'], $server, Database::NEW_UNCONNECTED );
                $conn->setLBInfo( $server );
                $conn->setLazyMasterHandle(
                        $this->getLazyConnectionRef( self::DB_MASTER, [], $conn->getDomainID() )
@@ -1327,6 +1311,13 @@ class LoadBalancer implements ILoadBalancer {
                $conn->setTableAliases( $this->tableAliases );
                $conn->setIndexAliases( $this->indexAliases );
 
+               try {
+                       $conn->initConnection();
+                       ++$this->connectionCounter;
+               } catch ( DBConnectionError $e ) {
+                       // ignore; let the DB handle the logging
+               }
+
                if ( $server['serverIndex'] === $this->getWriterIndex() ) {
                        if ( $this->trxRoundId !== false ) {
                                $this->applyTransactionRoundFlags( $conn );
@@ -1338,6 +1329,19 @@ class LoadBalancer implements ILoadBalancer {
 
                $this->lazyLoadReplicationPositions(); // session consistency
 
+               // Log when many connection are made on requests
+               $count = $this->getCurrentConnectionCount();
+               if ( $count >= self::CONN_HELD_WARN_THRESHOLD ) {
+                       $this->perfLogger->warning(
+                               __METHOD__ . ": {connections}+ connections made (master={masterdb})",
+                               [
+                                       'connections' => $count,
+                                       'dbserver' => $conn->getServer(),
+                                       'masterdb' => $conn->getLBInfo( 'clusterMasterHost' )
+                               ]
+                       );
+               }
+
                return $conn;
        }
 
index ead290f..d27643c 100644 (file)
@@ -262,6 +262,10 @@ class BlockLogFormatter extends LogFormatter {
                return $params;
        }
 
+       /**
+        * @inheritDoc
+        * @suppress PhanTypeInvalidDimOffset
+        */
        public function formatParametersForApi() {
                $ret = parent::formatParametersForApi();
                if ( isset( $ret['flags'] ) ) {
index 15b149e..3871047 100644 (file)
@@ -258,7 +258,7 @@ class LogPager extends ReverseChronologicalPager {
                                $params[] = $db->anyString();
                        }
                        array_pop( $params ); // Get rid of the last % we added.
-                       $this->mConds[] = 'log_title' . $db->buildLike( $params );
+                       $this->mConds[] = 'log_title' . $db->buildLike( ...$params );
                } elseif ( $pattern && !$wgMiserMode ) {
                        $this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() );
                        $this->pattern = $pattern;
index 1d0bbfd..5326705 100644 (file)
@@ -191,18 +191,20 @@ class ManualLogEntry extends LogEntryBase implements Taggable {
                        wfDebug( 'Overwriting existing ManualLogEntry tags' );
                }
                $this->tags = [];
-               if ( $tags !== null ) {
-                       $this->addTags( $tags );
-               }
+               $this->addTags( $tags );
        }
 
        /**
         * Add change tags for the log entry
         *
         * @since 1.33
-        * @param string|string[] $tags Tags to apply
+        * @param string|string[]|null $tags Tags to apply
         */
        public function addTags( $tags ) {
+               if ( $tags === null ) {
+                       return;
+               }
+
                if ( is_string( $tags ) ) {
                        $tags = [ $tags ];
                }
index d737a4b..9392220 100644 (file)
@@ -63,7 +63,7 @@ class PatrolLog {
                $entry->setTarget( $rc->getTitle() );
                $entry->setParameters( self::buildParams( $rc, $auto ) );
                $entry->setPerformer( $user );
-               $entry->setTags( $tags );
+               $entry->addTags( $tags );
                $logid = $entry->insert();
                if ( !$auto ) {
                        $entry->publish( $logid, 'udp' );
index 683ded1..c32db28 100644 (file)
@@ -36,6 +36,7 @@ class IPTC {
         *
         * @param string $rawData The app13 block from jpeg containing iptc/iim data
         * @return array IPTC metadata array
+        * @suppress PhanTypeArraySuspicious
         */
        static function parse( $rawData ) {
                $parsed = iptcparse( $rawData );
index d8cd1c5..3628c7b 100644 (file)
@@ -1935,6 +1935,7 @@ class Article implements Page {
                );
                $options = Xml::listDropDownOptionsOoui( $options );
 
+               $fields = [];
                $fields[] = new OOUI\FieldLayout(
                        new OOUI\DropdownInputWidget( [
                                'name' => 'wpDeleteReasonList',
index 491726b..dda13d3 100644 (file)
 /**
  * Special handling for category description pages, showing pages,
  * subcategories and file that belong to the category
+ *
+ * @property WikiCategoryPage $mPage Set by overwritten newPage() in this class
  */
 class CategoryPage extends Article {
        # Subclasses can change this to override the viewer class.
        protected $mCategoryViewerClass = CategoryViewer::class;
 
-       /**
-        * @var WikiCategoryPage
-        */
-       protected $mPage;
-
        /**
         * @param Title $title
         * @return WikiCategoryPage
index 2e43e8c..d3f0638 100644 (file)
@@ -27,6 +27,8 @@ use Wikimedia\Rdbms\ResultWrapper;
  * Class for viewing MediaWiki file description pages
  *
  * @ingroup Media
+ *
+ * @property WikiFilePage $mPage Set by overwritten newPage() in this class
  */
 class ImagePage extends Article {
        /** @var File|false */
@@ -41,11 +43,6 @@ class ImagePage extends Article {
        /** @var bool */
        protected $mExtraDescription = false;
 
-       /**
-        * @var WikiFilePage
-        */
-       protected $mPage;
-
        /**
         * @param Title $title
         * @return WikiFilePage
index d69a433..40c63d2 100644 (file)
@@ -461,7 +461,7 @@ class PageArchive {
                $logEntry->setPerformer( $user );
                $logEntry->setTarget( $this->title );
                $logEntry->setComment( $comment );
-               $logEntry->setTags( $tags );
+               $logEntry->addTags( $tags );
                $logEntry->setParameters( [
                        ':assoc:count' => [
                                'revisions' => $textRestored,
index b4b5927..4f0f2e2 100644 (file)
@@ -2392,7 +2392,7 @@ class WikiPage implements Page, IDBAccessObject {
                if ( !is_null( $nullRevision ) ) {
                        $logEntry->setAssociatedRevId( $nullRevision->getId() );
                }
-               $logEntry->setTags( $tags );
+               $logEntry->addTags( $tags );
                if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
                        $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
                }
@@ -2793,7 +2793,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $logEntry->setPerformer( $deleter );
                        $logEntry->setTarget( $logTitle );
                        $logEntry->setComment( $reason );
-                       $logEntry->setTags( $tags );
+                       $logEntry->addTags( $tags );
                        $logid = $logEntry->insert();
 
                        $dbw->onTransactionPreCommitOrIdle(
index 267402f..962313e 100644 (file)
@@ -4271,6 +4271,7 @@ class Parser {
         * @param bool $isMain
         * @return mixed|string
         * @private
+        * @suppress PhanTypeInvalidDimOffset
         */
        public function formatHeadings( $text, $origText, $isMain = true ) {
                # Inhibit editsection links if requested in the page
@@ -5577,6 +5578,7 @@ class Parser {
                Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
 
                # Linker does the rest
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $time = $options['time'] ?? false;
                $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
                        $time, $descQuery, $this->mOptions->getThumbSize() );
index 70e38ee..8c44a5e 100644 (file)
@@ -540,6 +540,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
 
                if ( $this->options->get( 'EnableEmail' ) ) {
                        if ( $canViewPrivateInfo ) {
+                               $helpMessages = [];
                                $helpMessages[] = $this->options->get( 'EmailConfirmToEdit' )
                                                ? 'prefs-help-email-required'
                                                : 'prefs-help-email';
index 3e65f6c..07fe318 100644 (file)
@@ -306,16 +306,6 @@ class ExtensionRegistry {
                                $autoloadNamespaces
                        );
 
-                       if ( isset( $info['AutoloadClasses'] ) ) {
-                               $autoload = $this->processAutoLoader( $dir, $info['AutoloadClasses'] );
-                               $GLOBALS['wgAutoloadClasses'] += $autoload;
-                               $autoloadClasses += $autoload;
-                       }
-                       if ( isset( $info['AutoloadNamespaces'] ) ) {
-                               $autoloadNamespaces += $this->processAutoLoader( $dir, $info['AutoloadNamespaces'] );
-                               AutoLoader::$psr4Namespaces += $autoloadNamespaces;
-                       }
-
                        // get all requirements/dependencies for this extension
                        $requires = $processor->getRequirements( $info, $this->checkDev );
 
index e17b393..e5a0d61 100644 (file)
@@ -424,6 +424,7 @@ JAVASCRIPT;
                                $idx = -1;
                                foreach ( $grpModules as $name => $module ) {
                                        $shouldEmbed = $module->shouldEmbedModule( $context );
+                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
                                                $moduleSets[++$idx] = [ $shouldEmbed, [] ];
                                        }
index 680ae8e..dc43aed 100644 (file)
@@ -394,7 +394,7 @@ abstract class RevDelList extends RevisionListBase {
                }
                $logEntry->setRelations( $relations );
                // Apply change tags to the log entry
-               $logEntry->setTags( $params['tags'] );
+               $logEntry->addTags( $params['tags'] );
                $logId = $logEntry->insert();
                $logEntry->publish( $logId );
        }
index 87a7861..66e59e5 100644 (file)
@@ -727,6 +727,7 @@ abstract class SearchEngine {
         * @param string $profileType the type of profiles
         * @param User|null $user the user requesting the list of profiles
         * @return array|null the list of profiles or null if none available
+        * @phan-return null|array{name:string,desc-message:string,default?:bool}
         */
        public function getProfiles( $profileType, User $user = null ) {
                return null;
index 85f4569..fc117a8 100644 (file)
@@ -287,6 +287,7 @@ final class SessionManager implements SessionManagerInterface {
                                        "$provider returned empty session info with id flagged unsafe"
                                );
                        }
+                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                        $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
                        if ( $compare > 0 ) {
                                continue;
index 19fa1da..74d77a8 100644 (file)
@@ -230,6 +230,7 @@ class Shell {
         * @param array $options Associative array of options:
         *     'php': The path to the php executable
         *     'wrapper': Path to a PHP wrapper to handle the maintenance script
+        * @phan-param array{php?:string,wrapper?:string} $options
         * @return Command
         */
        public static function makeScriptCommand( $script, $parameters, $options = [] ): Command {
@@ -237,6 +238,7 @@ class Shell {
                // Give site config file a chance to run the script in a wrapper.
                // The caller may likely want to call wfBasename() on $script.
                Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $cmd = [ $options['php'] ?? $wgPhpCli ];
                if ( isset( $options['wrapper'] ) ) {
                        $cmd[] = $options['wrapper'];
index 09e439b..bee4aff 100644 (file)
@@ -444,8 +444,10 @@ abstract class BaseTemplate extends QuickTemplate {
         * @param array $item Array of list item data containing some of a specific set of keys.
         * The "id", "class" and "itemtitle" keys will be used as attributes for the list item,
         * if "active" contains a value of true a "active" class will also be appended to class.
+        * @phan-param array{id?:string,class?:string,itemtitle?:string,active?:bool} $item
         *
         * @param array $options
+        * @phan-param array{tag?:string} $options
         *
         * If you want something other than a "<li>" you can pass a tag name such as
         * "tag" => "span" in the $options array to change the tag used.
@@ -514,6 +516,7 @@ abstract class BaseTemplate extends QuickTemplate {
                if ( isset( $item['itemtitle'] ) ) {
                        $attrs['title'] = $item['itemtitle'];
                }
+               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
        }
 
index e1f0588..9934150 100644 (file)
@@ -509,7 +509,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
         * Generates a HTMLForm descriptor array from a set of authentication requests.
         * @param AuthenticationRequest[] $requests
         * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants)
-        * @return array
+        * @return array[]
         */
        protected function getAuthFormDescriptor( $requests, $action ) {
                $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
@@ -600,7 +600,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
        /**
         * Adds a sequential tabindex starting from 1 to all form elements. This way the user can
         * use the tab key to traverse the form without having to step through all links and such.
-        * @param array &$formDescriptor
+        * @param array[] &$formDescriptor
         */
        protected function addTabIndex( &$formDescriptor ) {
                $i = 1;
index f6b8b90..4c9c428 100644 (file)
@@ -64,7 +64,7 @@ class SpecialAllMessages extends SpecialPage {
                $opts->fetchValuesFromRequest( $this->getRequest() );
                $opts->validateIntBounds( 'limit', 0, 5000 );
 
-               $pager = new AllMessagesTablePager( $this->getContext(), $opts );
+               $pager = new AllMessagesTablePager( $this->getContext(), $opts, $this->getLinkRenderer() );
 
                $formDescriptor = [
                        'prefix' => [
index 59d2806..3a266f2 100644 (file)
@@ -1031,7 +1031,7 @@ class SpecialBlock extends FormSpecialPage {
                $logId = $logEntry->insert();
 
                if ( !empty( $data['Tags'] ) ) {
-                       $logEntry->setTags( $data['Tags'] );
+                       $logEntry->addTags( $data['Tags'] );
                }
 
                $logEntry->publish( $logId );
index 9d5f430..4599b22 100644 (file)
@@ -213,7 +213,7 @@ class SpecialContributions extends IncludableSpecialPage {
                                'hideMinor' => $this->opts['hideMinor'],
                                'nsInvert' => $this->opts['nsInvert'],
                                'associated' => $this->opts['associated'],
-                       ] );
+                       ], $this->getLinkRenderer() );
 
                        if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
                                // Valid range, but outside CIDR limit.
@@ -364,6 +364,7 @@ class SpecialContributions extends IncludableSpecialPage {
 
                $linkRenderer = $sp->getLinkRenderer();
 
+               $tools = [];
                # No talk pages for IP ranges.
                if ( !$isRange ) {
                        $tools['user-talk'] = $linkRenderer->makeLink(
index 902bfd7..e9bf6a2 100644 (file)
@@ -93,7 +93,8 @@ class DeletedContributionsPage extends SpecialPage {
 
                $this->getForm();
 
-               $pager = new DeletedContribsPager( $this->getContext(), $target, $opts->getValue( 'namespace' ) );
+               $pager = new DeletedContribsPager( $this->getContext(), $target, $opts->getValue( 'namespace' ),
+                       $this->getLinkRenderer() );
                if ( !$pager->getNumRows() ) {
                        $out->addWikiMsg( 'nocontribs' );
 
index 480e81a..717edc3 100644 (file)
@@ -639,6 +639,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                $linkRenderer = $this->getLinkRenderer();
                $link = $linkRenderer->makeLink( $title );
 
+               $tools = [];
                $tools['talk'] = $linkRenderer->makeLink(
                        $title->getTalkPage(),
                        $this->msg( 'talkpagelinktext' )->text()
index 94f4753..02a468b 100644 (file)
@@ -46,7 +46,8 @@ class SpecialListFiles extends IncludableSpecialPage {
                        $userName,
                        $search,
                        $this->including(),
-                       $showAll
+                       $showAll,
+                       $this->getLinkRenderer()
                );
 
                $out = $this->getOutput();
index a56a745..45bd524 100644 (file)
@@ -76,9 +76,9 @@ class MediaStatisticsPage extends QueryPage {
                        $dbr->addQuotes( '/' ),
                        'img_minor_mime',
                        $dbr->addQuotes( ';' ),
-                       'COUNT(*)',
+                       $dbr->buildStringCast( 'COUNT(*)' ),
                        $dbr->addQuotes( ';' ),
-                       'SUM( img_size )'
+                       $dbr->buildStringCast( 'SUM( img_size )' )
                ] );
                return [
                        'tables' => [ 'image' ],
index 85f65bb..6da362d 100644 (file)
@@ -285,11 +285,9 @@ class MovePageForm extends UnlistedSpecialPage {
                        # Is the title semi-protected?
                        if ( $this->oldTitle->isSemiProtected( 'move' ) ) {
                                $noticeMsg = 'semiprotectedpagemovewarning';
-                               $classes[] = 'mw-textarea-sprotected';
                        } else {
                                # Then it must be protected based on static groups (regular)
                                $noticeMsg = 'protectedpagemovewarning';
-                               $classes[] = 'mw-textarea-protected';
                        }
                        $out->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
                        $out->addWikiMsg( $noticeMsg );
@@ -307,8 +305,9 @@ class MovePageForm extends UnlistedSpecialPage {
                // mediawiki.special.movePage module
 
                $immovableNamespaces = [];
+               $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
-                       if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->isMovable( $nsId ) ) {
+                       if ( !$namespaceInfo->isMovable( $nsId ) ) {
                                $immovableNamespaces[] = $nsId;
                        }
                }
@@ -536,11 +535,14 @@ class MovePageForm extends UnlistedSpecialPage {
                        return;
                }
 
+               $services = MediaWikiServices::getInstance();
+
                # Show a warning if the target file exists on a shared repo
+               $repoGroup = $services->getRepoGroup();
                if ( $nt->getNamespace() == NS_FILE
                        && !( $this->moveOverShared && $user->isAllowed( 'reupload-shared' ) )
-                       && !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
-                       && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $nt )
+                       && !$repoGroup->getLocalRepo()->findFile( $nt )
+                       && $repoGroup->findFile( $nt )
                ) {
                        $this->showForm( [ [ 'file-exists-sharedrepo' ] ] );
 
@@ -570,8 +572,7 @@ class MovePageForm extends UnlistedSpecialPage {
 
                        // Delete an associated image if there is
                        if ( $nt->getNamespace() == NS_FILE ) {
-                               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
-                                       ->newFile( $nt );
+                               $file = $repoGroup->getLocalRepo()->newFile( $nt );
                                $file->load( File::READ_LATEST );
                                if ( $file->exists() ) {
                                        $file->delete( $reason, false, $user );
@@ -606,7 +607,7 @@ class MovePageForm extends UnlistedSpecialPage {
                        $this->moveTalk = false;
                }
                if ( $this->moveSubpages ) {
-                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+                       $permissionManager = $services->getPermissionManager();
                        $this->moveSubpages = $permissionManager->userCan( 'move-subpages', $user, $ot );
                }
 
@@ -672,7 +673,7 @@ class MovePageForm extends UnlistedSpecialPage {
                 */
 
                // @todo FIXME: Use Title::moveSubpages() here
-               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $nsInfo = $services->getNamespaceInfo();
                $dbr = wfGetDB( DB_MASTER );
                if ( $this->moveSubpages && (
                        $nsInfo->hasSubpages( $nt->getNamespace() ) || (
@@ -749,7 +750,7 @@ class MovePageForm extends UnlistedSpecialPage {
 
                        $mp = new MovePage( $oldSubpage, $newSubpage );
                        # This was copy-pasted from Renameuser, bleh.
-                       if ( $newSubpage->exists() && !$mp->isValidMove()->isOk() ) {
+                       if ( $newSubpage->exists() && !$mp->isValidMove()->isOK() ) {
                                $link = $linkRenderer->makeKnownLink( $newSubpage );
                                $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
                        } else {
index ecbbfd5..29e7789 100644 (file)
@@ -102,7 +102,7 @@ class SpecialNewFiles extends IncludableSpecialPage {
                        $this->buildForm( $context );
                }
 
-               $pager = new NewFilesPager( $context, $opts );
+               $pager = new NewFilesPager( $context, $opts, $this->getLinkRenderer() );
 
                $out->addHTML( $pager->getBody() );
                if ( !$this->including() ) {
index c0f004f..1f222f8 100644 (file)
@@ -253,7 +253,7 @@ class SpecialPageLanguage extends FormSpecialPage {
                $entry->setTarget( $title );
                $entry->setParameters( $logParams );
                $entry->setComment( $reason );
-               $entry->setTags( $tags );
+               $entry->addTags( $tags );
 
                $logid = $entry->insert();
                $entry->publish( $logid );
index 31c277a..9b8022b 100644 (file)
@@ -249,7 +249,7 @@ class SpecialUnblock extends SpecialPage {
                $logEntry->setComment( $data['Reason'] );
                $logEntry->setPerformer( $performer );
                if ( isset( $data['Tags'] ) ) {
-                       $logEntry->setTags( $data['Tags'] );
+                       $logEntry->addTags( $data['Tags'] );
                }
                $logEntry->setRelations( [ 'ipb_id' => $block->getId() ] );
                $logId = $logEntry->insert();
index 9a16a72..32be932 100644 (file)
@@ -247,6 +247,7 @@ class SpecialUndelete extends SpecialPage {
 
                $out->enableOOUI();
 
+               $fields = [];
                $fields[] = new OOUI\ActionFieldLayout(
                        new OOUI\TextInputWidget( [
                                'name' => 'prefix',
@@ -768,6 +769,7 @@ class SpecialUndelete extends SpecialPage {
                }
 
                if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+                       $fields = [];
                        $fields[] = new OOUI\Layout( [
                                'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
                        ] );
index 87534eb..a45ccca 100644 (file)
@@ -464,7 +464,7 @@ class UserrightsPage extends SpecialPage {
                ] );
                $logid = $logEntry->insert();
                if ( count( $tags ) ) {
-                       $logEntry->setTags( $tags );
+                       $logEntry->addTags( $tags );
                }
                $logEntry->publish( $logid );
        }
index 18c10bf..2840086 100644 (file)
@@ -117,6 +117,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage {
                $fetchlinks = ( !$hidelinks || !$hideredirs );
 
                // Build query conds in concert for all three tables...
+               $conds = [];
                $conds['pagelinks'] = [
                        'pl_namespace' => $target->getNamespace(),
                        'pl_title' => $target->getDBkey(),
@@ -229,6 +230,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage {
                // Read the rows into an array and remove duplicates
                // templatelinks comes second so that the templatelinks row overwrites the
                // pagelinks row, so we get (inclusion) rather than nothing
+               $rows = [];
                if ( $fetchlinks ) {
                        foreach ( $plRes as $row ) {
                                $row->is_template = 0;
index be28417..1e5f816 100644 (file)
@@ -40,6 +40,7 @@ class UploadForm extends HTMLForm {
 
        protected $mMaxFileSize = [];
 
+       /** @var array */
        protected $mMaxUploadSize = [];
 
        public function __construct( array $options = [], IContextSource $context = null,
index 0ae708a..938b159 100644 (file)
@@ -163,7 +163,7 @@ class ImportReporter extends ContextSource {
                        // Make sure the null revision will be tagged as well
                        $logEntry->setAssociatedRevId( $nullRevId );
                        if ( count( $this->logTags ) ) {
-                               $logEntry->setTags( $this->logTags );
+                               $logEntry->addTags( $this->logTags );
                        }
                        $logid = $logEntry->insert();
                        $logEntry->publish( $logid );
index bd27919..c804b09 100644 (file)
@@ -20,6 +20,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Linker\LinkRenderer;
 use Wikimedia\Rdbms\FakeResultWrapper;
 
 /**
@@ -58,9 +59,12 @@ class AllMessagesTablePager extends TablePager {
        /**
         * @param IContextSource|null $context
         * @param FormOptions $opts
+        * @param LinkRenderer $linkRenderer
         */
-       public function __construct( IContextSource $context = null, FormOptions $opts ) {
-               parent::__construct( $context );
+       public function __construct( IContextSource $context = null, FormOptions $opts,
+               LinkRenderer $linkRenderer
+       ) {
+               parent::__construct( $context, $linkRenderer );
 
                $this->mIndexField = 'am_title';
                // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering?
index d61a1be..6faf22b 100644 (file)
@@ -74,7 +74,7 @@ class BlockListPager extends TablePager {
         * @param string $name
         * @param string $value
         * @return string
-        * @suppress PhanTypeArraySuspiciousNullable
+        * @suppress PhanTypeArraySuspiciousNullable,PhanTypeArraySuspicious
         */
        function formatValue( $name, $value ) {
                static $msg = null;
@@ -138,6 +138,7 @@ class BlockListPager extends TablePager {
                                        /* User preference timezone */true
                                ) );
                                if ( $this->getUser()->isAllowed( 'block' ) ) {
+                                       $links = [];
                                        if ( $row->ipb_auto ) {
                                                $links[] = $linkRenderer->makeKnownLink(
                                                        SpecialPage::getTitleFor( 'Unblock' ),
index 152f56b..d76dfb8 100644 (file)
@@ -24,6 +24,7 @@
  * @ingroup Pager
  */
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Storage\RevisionRecord;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
@@ -97,7 +98,9 @@ class ContribsPager extends RangeChronologicalPager {
         */
        private $templateParser;
 
-       public function __construct( IContextSource $context, array $options ) {
+       public function __construct( IContextSource $context, array $options,
+               LinkRenderer $linkRenderer = null
+       ) {
                // Set ->target before calling parent::__construct() so
                // parent can call $this->getIndexField() and get the right result. Set
                // the rest too just to keep things simple.
@@ -112,7 +115,7 @@ class ContribsPager extends RangeChronologicalPager {
                $this->newOnly = !empty( $options['newOnly'] );
                $this->hideMinor = !empty( $options['hideMinor'] );
 
-               parent::__construct( $context );
+               parent::__construct( $context, $linkRenderer );
 
                $msgs = [
                        'diff',
index 7dbfae8..cd6294d 100644 (file)
@@ -22,6 +22,7 @@
 /**
  * @ingroup Pager
  */
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Storage\RevisionRecord;
 use Wikimedia\Rdbms\IDatabase;
@@ -60,8 +61,10 @@ class DeletedContribsPager extends IndexPager {
         */
        protected $mNavigationBar;
 
-       public function __construct( IContextSource $context, $target, $namespace = false ) {
-               parent::__construct( $context );
+       public function __construct( IContextSource $context, $target, $namespace = false,
+               LinkRenderer $linkRenderer
+       ) {
+               parent::__construct( $context, $linkRenderer );
                $msgs = [ 'deletionlog', 'undeleteviewlink', 'diff' ];
                foreach ( $msgs as $msg ) {
                        $this->messages[$msg] = $this->msg( $msg )->text();
index 81b7808..5de3ecb 100644 (file)
@@ -22,6 +22,7 @@
 /**
  * @ingroup Pager
  */
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
@@ -51,7 +52,7 @@ class ImageListPager extends TablePager {
        protected $mTableName = 'image';
 
        public function __construct( IContextSource $context, $userName = null, $search = '',
-               $including = false, $showAll = false
+               $including = false, $showAll = false, LinkRenderer $linkRenderer
        ) {
                $this->setContext( $context );
 
@@ -96,7 +97,7 @@ class ImageListPager extends TablePager {
                        $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
                }
 
-               parent::__construct();
+               parent::__construct( $context, $linkRenderer );
        }
 
        /**
index 2cb2b4a..f1b0b9a 100644 (file)
@@ -22,6 +22,7 @@
 /**
  * @ingroup Pager
  */
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\MediaWikiServices;
 
 class NewFilesPager extends RangeChronologicalPager {
@@ -39,9 +40,12 @@ class NewFilesPager extends RangeChronologicalPager {
        /**
         * @param IContextSource $context
         * @param FormOptions $opts
+        * @param LinkRenderer $linkRenderer
         */
-       public function __construct( IContextSource $context, FormOptions $opts ) {
-               parent::__construct( $context );
+       public function __construct( IContextSource $context, FormOptions $opts,
+               LinkRenderer $linkRenderer
+       ) {
+               parent::__construct( $context, $linkRenderer );
 
                $this->opts = $opts;
                $this->setLimit( $opts->getValue( 'limit' ) );
index 392e46d..7737067 100644 (file)
@@ -20,6 +20,7 @@ class ComplexTitleInputWidget extends \OOUI\Widget {
         *   - array $config['namespace'] Configuration for the NamespaceInputWidget dropdown
         *     with list of namespaces
         *   - array $config['title'] Configuration for the TitleInputWidget text field
+        * @phan-param array{namespace?:array,title?:array} $config
         */
        public function __construct( array $config = [] ) {
                // Configuration initialization
index 75b3b3f..7e41483 100644 (file)
        "viewyourtext": "Bu səhifəyə <strong>etdiyiniz dəyişikliklərin</strong> mənbəyinə baxa və köçürə bilərsiniz.",
        "protectedinterface": "Bu səhifədə proqram təminatı üçün sistem məlumatları var və sui-istifadənin qarşısını almaq üçün mühafizə olunmalıdır.",
        "editinginterface": "<strong>Diqqət:</strong> Siz proqram təminatı üçün interfeys mətni olan səhifəni redaktə edirsiniz.\nOnun dəyişdirilməsi digər istifadəçilərin interfeysinin xarici görünüşünə təsir göstərəcək.",
-       "translateinterface": "Bütün vikilər üçün tərcümələri əlavə etmək və ya dəyişmək üçün, xahiş edirik MediaWiki lokallaşdırma layihəsi [https://translatewiki.net/ translatewiki.net]-i istifadə edin.",
+       "translateinterface": "Bütün vikilərə tərcümələr əlavə etmək və ya onları dəyişmək üçün xahiş edirik, MediaWiki lokallaşdırma layihəsi olan [https://translatewiki.net/ translatewiki.net] saytından istifadə edin.",
        "cascadeprotected": "Bu səhifə mühafizə olunub, çünki o, kaskad mühafizə olunan {{PLURAL:$1|aşağıdakı səhifədə|aşağıdakı səhifələrdə}} istifadə edilib:\n$2",
        "namespaceprotected": "Sizin adlarında $1 olan məqalələrdə redaktə etməyə icazəniz yoxdur.",
        "customcssprotected": "Bu səhifəni redaktə etmə izniniz yoxdur, çünki bu səhifə başqa bir istifadəçinin fərdi parametrlərinə sahibdir.",
index 87b566d..47496dd 100644 (file)
        "category-empty": "<em>Был категория әлегә буш.</em>",
        "hidden-categories": "{{PLURAL:$1|Йәшерен категория|Йәшерен категориялар}}",
        "hidden-category-category": "Йәшерен категориялар",
-       "category-subcat-count": "{{PLURAL:$2|Был категорияла тик киләһе эске категория ғына бар.|Барлығы $2 категориянан, был категорияла киләһе {{PLURAL:$1|эске категория|$1 эске категория}} күрһәтелә.}}",
+       "category-subcat-count": "{{PLURAL:$2|1=Был категорияла бер генә эске категория бар.|Был категориялағы барыһы $2 эске категорияның {{PLURAL:$1|$1 эске категорияһы}} күрһәтелгән.}}",
        "category-subcat-count-limited": "Был категорияға киләһе {{PLURAL:$1|эске категория|$1 эске категория}} ингән.",
-       "category-article-count": "{{PLURAL:$2|1=Ð\91Ñ\8bл ÐºÐ°Ñ\82егоÑ\80иÑ\8fла Ð±ÐµÑ\80 Ð³ÐµÐ½Ó\99 Ð±Ð¸Ñ\82 Ð±Ð°Ñ\80.|Ð\9aаÑ\82егоÑ\80иÑ\8fлаÒ\93Ñ\8b $2 Ð±Ð¸Ñ\82Ñ\82ең $1 Ð±Ð¸Ñ\82е күрһәтелгән.}}",
+       "category-article-count": "{{PLURAL:$2|1=Ð\91Ñ\8bл ÐºÐ°Ñ\82егоÑ\80иÑ\8fла Ð±ÐµÑ\80 Ð³ÐµÐ½Ó\99 Ð±Ð¸Ñ\82 Ð±Ð°Ñ\80.|Ð\91Ñ\8bл ÐºÐ°Ñ\82егоÑ\80иÑ\8fла Ð±Ñ\83лÒ\93ан $2 Ð±Ð¸Ñ\82Ñ\82ең {{PLURAL:$1|$1 Ð±Ð¸Ñ\82е}} күрһәтелгән.}}",
        "category-article-count-limited": "Был категорияла {{PLURAL:$1|$1 бит}} бар.",
        "category-file-count": "{{PLURAL:$2|Был категорияла бер генә файл бар.|Категориялағы $2 файлдың {{PLURAL:$1|$1 файлы күрһәтелгән}}.}}",
        "category-file-count-limited": "Был категорияла {{PLURAL:$1|$1 файл}} бар.",
index ec54840..6aaeac8 100644 (file)
        "about": "Indik",
        "article": "Kaca daging",
        "newwindow": "(bukak ring jendela anyar)",
-       "cancel": "Buwung",
+       "cancel": "Wangdé",
        "moredotdotdot": "Lianan...",
        "mypage": "Kaca",
        "mytalk": "Pabligbagan",
        "ok": "OK",
        "retrievedfrom": "Kapolihang saking \"$1\"",
        "youhavenewmessages": "{{PLURAL:$3|Jero madué}} $1 ($2)",
-       "youhavenewmessagesfromusers": "{{PLURAL:$4|You have}} $1 ring {{PLURAL:$3|another user|$3 users}} ($2).",
+       "youhavenewmessagesfromusers": "{{PLURAL:$4|Ida dané madué}} $1 saking {{PLURAL:$3|$3 sang anganggé lianan}} ($2).",
        "youhavenewmessagesmanyusers": "Jero madué $1 saking akéh sang anganggé ($2).",
        "newmessageslinkplural": "{{PLURAL:$1|séwalapatra anyar abesik|999=séwalapatra anyar}}",
        "youhavenewmessagesmulti": "Ida dané madué séwalapatra anyar ring $1",
        "pt-createaccount": "Ngaryanin akun",
        "pt-userlogout": "Medal log",
        "botpasswords-label-create": "Ngae",
-       "botpasswords-label-cancel": "Buungan",
+       "botpasswords-label-cancel": "Wangdé",
        "botpasswords-label-delete": "Usap",
        "botpasswords-label-resetpassword": "Nyumu kruna sandi",
+       "resetpass-submit-cancel": "Wangdé",
        "passwordreset": "Nyumu kruna sandi",
        "bold_sample": "teks puniki mesurat tebel",
        "bold_tip": "teks puniki mesurat tebel",
        "prefs-help-email-others": "ida dane prasida milih anggen ngalugrain anak lianan ngubungin ida dane majalaran lembar penganggen utawi pangraos nenten ja perlu ngagah indik padewekan ida dane",
        "prefs-editor": "Sang anguah",
        "group-bot": "Bot",
+       "group-sysop": "Prajuru",
        "grouppage-bot": "{{ns:project}}:Bot",
        "right-edit": "Uah kaca",
        "right-writeapi": "nganggén API sasuratan",
        "action-browsearchive": "rereh kaca sané kausapin",
        "action-editprotected": "uah kaca sané kasaibin \"{{int:protect-level-sysop}}\"",
        "action-editsemiprotected": "uah kaca sané kasaibin \"{{int:protect-level-autoconfirmed}}\"",
-       "nchanges": "$1{{PLURAL:$1|panguwahan|uwah-uwahan}}",
+       "nchanges": "$1 {{PLURAL:$1|uahan}}",
        "enhancedrc-history": "babad",
        "recentchanges": "Uahan sané mangkin",
        "recentchanges-legend": "Opsi uahan sané mangkin",
        "recentchanges-label-minor": "Punika uahan alit",
        "recentchanges-label-bot": "Uahan puniki kalaksanayang antuk bot",
        "recentchanges-label-unpatrolled": "Uahan puniki durung kapatroli",
-       "recentchanges-label-plusminus": "Pagentos akeh kaca manut ring bita",
+       "recentchanges-label-plusminus": "Agengnyané kacané kauahin antuk akéhnyané bita puniki",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (taler cingak [[Special:NewPages|bacakan kaca anyar]])",
        "recentchanges-submit": "Sinahang",
        "rcfilters-activefilters-show": "Sinahang",
        "rcfilters-savedqueries-remove": "Usap",
+       "rcfilters-savedqueries-cancel-label": "Wangdé",
        "rcfilters-filter-minor-label": "Uahan alit",
        "rcfilters-filter-major-label": "Uahan tan alit",
        "rcfilters-filter-pageedits-label": "Uahan kaca",
        "uploadlogpage": "Log unggahan",
        "filedesc": "Ringkesan",
        "savefile": "Raksa berkas",
+       "upload-dialog-button-cancel": "Wangdé",
        "upload-dialog-button-save": "Raksa",
        "backend-fail-delete": "Tan prasida ngusapin berkas \"$1\".",
        "license": "kepahan lugra",
        "imagelinks": "Panganggén depukan",
        "linkstoimage": "{{PLURAL:$1|Kaca|$1 kaca}} ring sor puniki nganggén depukan puniki:",
        "nolinkstoimage": "Nénten wénten kaca sané nganggén berkas puniki.",
+       "linkstoimage-redirect": "$1 (gingsiran berkas) $2",
        "sharedupload-desc-here": "Depukan puniki mawit saking $1 lan minab kaanggén olih proyék-proyék sané lianan. Déskripsinnyané ring [$2 kaca déskripsi depukannyané] kaarahin ring ungkur puniki.",
        "filepage-nofile": "Nentén wénten berkas sané mamurda sakadi punika",
        "shared-repo-name-wikimediacommons": "Wikimedia Commons",
        "actioncomplete": "pelaksanan sampun wusan",
        "actionfailed": "pelaksana luput",
        "dellogpage": "log pangapus",
+       "rollback-confirmation-no": "Wangdé",
        "rollbacklink": "mabalik",
        "rollbacklinkcount": "balikang $1 {{PLURAL:$1|suratan}}",
        "changecontentmodel-title-label": "Murda kaca",
        "tooltip-t-upload": "Unggahang depukan",
        "tooltip-t-specialpages": "Bacakan makasami kaca kusus",
        "tooltip-t-print": "Vérsi cétak kaca puniki",
-       "tooltip-t-permalink": "Pranala ajeg anggén révisi puniki antuk kacané",
+       "tooltip-t-permalink": "Pranala ajeg anggén révisinnyané kacané puniki",
        "tooltip-ca-nstab-main": "Cingak kaca daging",
        "tooltip-ca-nstab-user": "Cingak kaca sang anganggé",
        "tooltip-ca-nstab-special": "Puniki kaca kusus tur nénten prasida kauwah",
        "previousdiff": "← Uahan sadurungnyané",
        "nextdiff": "Uahan sané pinih anyar →",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|kaca}}",
-       "file-info-size": "$1x$2 piksel, ukuran depukan: $3, tipe MIME:$4",
+       "file-info-size": "$1x$2 piksel, agengnyané depukan: $3, soroh MIME:$4",
+       "file-info-size-pages": "$1 × $2 piksel, agengnyané berkas: $3, soroh MIME: $4, $5 {{PLURAL:$5|kaca}}",
        "file-nohires": "tan kasayagaang ukuran sane lewih ageng",
-       "svg-long-desc": "pupulan SVG, nominal $1 × $2 piksel, geden pupulan: $3",
+       "svg-long-desc": "Berkas SVG, jimbarnyané $1 × $2 piksel, agengnyané berkas: $3",
        "show-big-image": "Depukan sujati",
-       "show-big-image-preview": "agengnyané pratuduh:$1",
+       "show-big-image-preview": "Agengnyané pratuduh puniki: $1.",
        "show-big-image-other": "{{PLURAL:$2|Resolusi}} iianan: $1.",
        "show-big-image-size": "$1 × $2 piksel",
        "sunday-at": "Redite jam $1",
        "metadata-fields": "Widang métadata gambar sané kacantumang ring séwalapatra puniki jagi kalebuang ring tampilan kaca gambar ri tatkala tabél métadata kacenikang.\nSané lianan jagi kasenetang.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "namespacesall": "samian",
        "monthsall": "samian",
+       "confirmemail_invalidated": "Konfirmasi alamat email kawangdéang",
        "imgmultipagenext": "kaca salanturnyané →",
        "imgmultigo": "Ngrereh",
        "imgmultigoto": "Nuju kaca $1",
        "logentry-protect-protect": "$1 {{GENDER:$2|nyaibin}} $3 $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|ngunggahang}} $3",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|ngunggahang}} vèrsi anyar saking $3",
+       "feedback-cancel": "Wangdé",
        "feedback-message": "Séwalapatra:",
        "searchsuggest-search": "Rereh ring {{SITENAME}}",
        "duration-days": "$1 {{PLURAL:$1|rahina}}",
index d10adaa..c356041 100644 (file)
        "botpasswords-existing": "Наяўныя паролі робатаў",
        "botpasswords-createnew": "Стварыць новы пароль робата",
        "botpasswords-editexisting": "Рэдагаваць наяўны пароль робата",
+       "botpasswords-label-needsreset": "(пароль патрабуе скідвання)",
        "botpasswords-label-appid": "Назва робата:",
        "botpasswords-label-create": "Стварыць",
        "botpasswords-label-update": "Абнавіць",
        "botpasswords-restriction-failed": "Уваход не выкананы з-за абмежаванняў на пароль робата.",
        "botpasswords-invalid-name": "Паказанае імя ўдзельніка не ўтрымлівае падзяляльнік паролю робата (\"$1\").",
        "botpasswords-not-exist": "Удзельнік \"$1\" не мае паролю для робата з назвай \"$2\".",
+       "botpasswords-needs-reset": "Пароль для робата \"$1\", які належыць {{GENDER:$2|удзельніку|удзельніцы}} \"$2\", мусіць быць скінуты.",
        "resetpass_forbidden": "Не дазволена мяняць паролі",
        "resetpass_forbidden-reason": "Не дазволена мяняць паролі: $1",
        "resetpass-no-info": "Трэба ўвайсці ў сістэму, каб звяртацца да гэтай старонкі наўпрост.",
        "resetpass-expired": "Ваш пароль пратэрмінаваны. Калі ласка, устанавіце новы пароль для ўваходу ў сістэму.",
        "resetpass-expired-soft": "Ваш пароль пратэрмінаваны і яго трэба замяніць. Калі ласка, выберыце новы пароль зараз, ці націсніце \"{{int:authprovider-resetpass-skip-label}}\", каб змяніць яго пазней.",
        "resetpass-validity": "Ваш пароль няверны: $1 \n\nКалі ласка, устанавіце новы пароль для ўваходу ў сістэму.",
-       "resetpass-validity-soft": "Ваш пароль недапушчальны: $1\n\nКалі ласка, выберыце новы пароль зараз, або націсніце \"{{int:authprovider-resetpass-skip-label}}\", каб скінуць яго пазней.",
+       "resetpass-validity-soft": "Ваш пароль недапушчальны: $1\n\nКалі ласка, выберыце новы пароль зараз, або націсніце \"{{int:authprovider-resetpass-skip-label}}\", каб змяніць яго пазней.",
        "passwordreset": "Выслаць мне новы пароль",
        "passwordreset-text-one": "Запоўніце гэту форму, каб атрымаць часовы пароль па эл.пошце.",
        "passwordreset-text-many": "{{PLURAL:$1|Запоўніце адно з палёў, каб атрымаць тымчасовы пароль па электроннай пошце.}}",
        "autoblockedtext": "Ваш адрас IP быў аўтаматычна заблакаваны, таму што ім карыстаўся ўдзельнік, заблакаваны адміністратарам $1.\nПададзеная прычына блоку:\n\n:''$2''\n\n* Блок пастаўлены: $8\n* Блок канчаецца: $6\n* Атрымальнік блоку: $7\n\nВы можаце звярнуцца да $1 або да аднаго з іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб паразмаўляць пра гэты блок.\n\nВы не зможаце дзеля гэтага карыстацца функцыяй ''{{:{{ns:mediawiki}}:emailuser/be}}'', калі гэта вам забаронена, або калі вы не наставілі правільнага пацверджанага адрасу эл.пошты ў сваіх [[Special:Preferences|настаўленнях]].\n\nВаш адрас IP: $3. Ваш нумар блоку: $5. Падавайце ўсе гэтыя звесткі ў кожным сваім звароце адносна гэтага блоку.",
        "systemblockedtext": "Вашае імя ўдзельніка ці IP-адрас былі аўтаматычна заблакаваныя MediaWiki.\nЗ наступнай прычыны:\n\n:<em>$2</em>\n\n* Пачатак блакіроўкі: $8\n* Заканчэнне блакіроўкі: $6\n* Мэта блакіравання: $7\n\nВаш цяперашні IP-адрас — $3.\nКалі ласка, уключайце ўсе пададзеныя вышэй дэталі ва ўсе запыты, што вы робіце.",
        "blockednoreason": "прычына не вызначана",
+       "blockedtext-composite-no-ids": "Ваш IP-адрас наяўны ў некалькіх чорных спісах",
+       "blockedtext-composite-reason": "Маецца некалькі блакіровак вашага рахунку і/ці IP-адрасу",
        "whitelistedittext": "Належыць $1 каб правіць старонкі.",
        "confirmedittext": "Вам трэба пацвердзіць свой адрас эл.пошты перад тым, як правіць старонкі.\nВызначце і пацвердзіце адрас ў сваіх [[Special:Preferences|настáўленнях]].",
        "nosuchsectiontitle": "Няма такога падраздзелу",
        "nocreate-loggedin": "Вам не дазволена ствараць новыя старонкі.",
        "sectioneditnotsupported-title": "Праўка раздзелу не падтрымліваецца",
        "sectioneditnotsupported-text": "Праўка раздзелу не падтрымліваецца на гэтай старонцы.",
+       "modeleditnotsupported-title": "Рэдагаванне не падтрымліваецца",
+       "modeleditnotsupported-text": "Рэдагаванне не падтрымліваецца для мадэлі змесціва $1.",
        "permissionserrors": "Памылка доступу",
        "permissionserrorstext": "Вам не дазволена гэтага рабіць, з наступн{{PLURAL:$1|ай прычыны|ых прычын}}:",
        "permissionserrorstext-withaction": "Вам не дазволена $2, з-за наступ{{PLURAL:$1|най прычыны|ных прычын}}:",
        "editpage-invalidcontentmodel-text": "Мадэль змесціва \"$1\" не падтрымліваецца.",
        "editpage-notsupportedcontentformat-title": "Фармат змесціва не падтрымліваецца",
        "editpage-notsupportedcontentformat-text": "Фармат змесціва $1 не падтрымліваецца мадэллю змесціва $2.",
+       "slot-name-main": "Галоўная",
        "content-model-wikitext": "вікі-тэкст",
        "content-model-text": "звычайны тэкст",
        "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
        "content-json-empty-object": "Пусты аб’ект",
        "content-json-empty-array": "Пусты масіў",
+       "unsupported-content-model": "<strong>Увага:</strong> Мадэль змесціва $1 не падтрымліваецца на гэтай вікі.",
+       "unsupported-content-diff": "Адрозненні не падтрымліваюцца для мадэлі змесціва $1.",
+       "unsupported-content-diff2": "Адрозненні між мадэлямі змесціва $1 і $2 не падтрымліваюцца на гэтай вікі.",
        "deprecated-self-close-category": "Старонкі з недапушчальнымі самазакрытымі HTML-тэгамі",
        "deprecated-self-close-category-desc": "Старонка ўтрымлівае недапушчальныя самазакрытыя HTML-тэгі, такія як <code>&lt;b/></code> ці <code>&lt;span/></code>. Іх паводзіны ў хуткім часе будуць зменены ў адпаведнасці з спецыфікацыяй HTML5, таму іх ужыванне ў вікітэксце лічыцца састарэлым.",
        "duplicate-args-warning": "<strong>Увага:</strong> [[:$1]] выклікае [[:$2]] з больш чым адным значэннем для параметра \"$3\". Толькі апошняе з пададзеных значэнняў будзе ўжытае.",
        "rcfilters-clear-all-filters": "Ачысціць усе фільтры",
        "rcfilters-show-new-changes": "Паказаць навейшыя змяненні з $1",
        "rcfilters-search-placeholder": "Змяненні фільтра (выкарыстоўвайце меню ці шукайце па назве фільтра)",
+       "rcfilters-search-placeholder-mobile": "Фільтры",
        "rcfilters-invalid-filter": "Недапушчальны фільтр",
        "rcfilters-empty-filter": "Няма актыўных фільтраў. Паказваюцца ўсе праўкі.",
        "rcfilters-filterlist-title": "Фільтры",
        "rcfilters-preference-help": "Адкатвае рэдызайн інтэрфейсу 2017 года і ўсе інструменты, дададзеныя з тых часоў.",
        "rcfilters-watchlist-preference-label": "Выкарыстоўваць інтэрфейс без JavaScript",
        "rcfilters-watchlist-preference-help": "Адкатвае рэдызайн інтэрфейсу 2017 года і ўсе інструменты, дададзеныя з тых часоў.",
+       "rcfilters-filter-showlinkedfrom-label": "Паказаць змены на старонках, на якія спасылаецца",
+       "rcfilters-filter-showlinkedto-label": "Паказаць змены старонак, якія спасылаюцца на",
+       "rcfilters-target-page-placeholder": "Увядзіце назву старонкі (ці катэгорыі)",
+       "rcfilters-allcontents-label": "Увесь змест",
+       "rcfilters-alldiscussions-label": "Усе абмеркаванні",
        "rcnotefrom": "Ніжэй {{PLURAL:$5|паказана змяненне|паказаны змены}} з <strong>$3, $4</strong> (не больш за <strong>$1</strong>).",
        "rclistfrom": "Паказаць змены з $3 $2",
        "rcshowhideminor": "$1 дробныя праўкі",
        "sessionfailure": "Магчыма, ёсць праблемы з вашым сеансам працы ў сістэме. Таму вам было адмоўлена ў выкананні дзеяння, каб засцерагчыся ад захопу сеанса.\n\nВярніцеся на папярэднюю старонку, перазагрузіце яе і тады паспрабуйце зноў.",
        "changecontentmodel": "Змяніць мадэль змесціва старонкі",
        "changecontentmodel-legend": "Змяніць мадэль змесціва",
-       "changecontentmodel-title-label": "Назва старонкі",
-       "changecontentmodel-model-label": "Новая мадэль змесціва",
+       "changecontentmodel-title-label": "Назва старонкі:",
+       "changecontentmodel-current-label": "Бягучая мадэль змесціва:",
+       "changecontentmodel-model-label": "Новая мадэль змесціва:",
        "changecontentmodel-reason-label": "Прычына:",
        "changecontentmodel-submit": "Змяніць",
        "changecontentmodel-success-title": "Мадэль змесціва была зменена",
        "contribsub2": "Для $1 ($2)",
        "contributions-subtitle": "Для {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "Уліковы запіс удзельніка \"$1\" не зарэгістраваны.",
+       "negative-namespace-not-supported": "Прасторы назваў з адмоўнымі значэннямі не падтрымліваюцца.",
        "nocontribs": "Не знойдзена змен, адпаведных зададзеным параметрам.",
        "uctop": "апошн.",
        "month": "Ад месяца (і раней):",
        "blocklink": "заблакаваць",
        "unblocklink": "адблакаваць",
        "change-blocklink": "змяніць блок",
+       "empty-username": "(імя ўдзельніка недаступна)",
        "contribslink": "уклад",
        "emaillink": "адправіць ліст",
        "autoblocker": "Аўтаматычны блок, таму што вашым адрасам IP нядаўна карыстаўся \"[[User:$1|$1]]\".\nПрычына блакіроўкі ўдзельніка $1: \"$2\"",
        "fix-double-redirects": "Абнавіць усе перасылкі, якія вядуць да пачатковай назвы",
        "move-leave-redirect": "Пакінуць перасылку са старой назвы",
        "protectedpagemovewarning": "<strong>Папярэджанне:</strong> Гэта старонка была змешчана пад ахову; пераназваць яе могуць толькі ўдзельнікі з паўнамоцтвамі адміністратараў.\nНіжэй для даведкі прыведзена апошні запіс журнала:",
-       "semiprotectedpagemovewarning": "<strong>Ð\97аÑ\9eвага:</strong> Ð\93Ñ\8dÑ\82а Ñ\81Ñ\82аÑ\80онка Ð±Ñ\8bла Ð·Ð¼ÐµÑ\88Ñ\87ана Ð¿Ð°Ð´ Ð°Ñ\85овÑ\83; Ð¿ÐµÑ\80аноÑ\81Ñ\96Ñ\86Ñ\8c Ñ\8fе Ð¿Ð°Ð´ Ñ\96нÑ\88Ñ\83Ñ\8e Ð½Ð°Ð·Ð²Ñ\83 Ð¼Ð¾Ð³Ñ\83Ñ\86Ñ\8c Ñ\82олÑ\8cкÑ\96 Ð·Ð°Ñ\80Ñ\8dгÑ\96Ñ\81Ñ\82Ñ\80аваныя ўдзельнікі.\nНіжэй для даведкі прыведзена апошні запіс журнала:",
+       "semiprotectedpagemovewarning": "<strong>Ð\97аÑ\9eвага:</strong> Ð\93Ñ\8dÑ\82а Ñ\81Ñ\82аÑ\80онка Ð±Ñ\8bла Ð·Ð¼ÐµÑ\88Ñ\87ана Ð¿Ð°Ð´ Ð°Ñ\85овÑ\83; Ð¿ÐµÑ\80аноÑ\81Ñ\96Ñ\86Ñ\8c Ñ\8fе Ð¿Ð°Ð´ Ñ\96нÑ\88Ñ\83Ñ\8e Ð½Ð°Ð·Ð²Ñ\83 Ð¼Ð¾Ð³Ñ\83Ñ\86Ñ\8c Ñ\82олÑ\8cкÑ\96 Ð°Ñ\9eÑ\82апаÑ\86веÑ\80джаныя ўдзельнікі.\nНіжэй для даведкі прыведзена апошні запіс журнала:",
        "move-over-sharedrepo": "Файл з назвай [[:$1]] ёсць у агульным сховішчы. Файл, перанесены пад такую назву, будзе перамагаць файл з агульнага сховішча.",
        "file-exists-sharedrepo": "Такая назва файла ўжо выкарыстана ў агульным сховішчы.\nВыберыце іншую назву.",
        "export": "Экспартаваць старонкі",
        "tooltip-summary": "Дайце кароткае апісанне",
        "common.css": "/** CSS, упісаны сюды, будзе дзейнічаць на карыстальнікаў усіх світаў */",
        "group-autoconfirmed.css": "/* Размешчаны тут CSS будзе прымяняцца для аўтапацверджаных удзельнікаў */",
+       "common.json": "/* JSON-код, упісаны сюды, будзе выконвацца для кожнага чытача, на кожным счытванні старонкі. */",
        "common.js": "/* Яваскрыпт, упісаны сюды, будзе выконвацца для кожнага чытача, на кожным счытванні старонкі. */",
        "group-autoconfirmed.js": "/* Размешчаны тут код JavaScript будзе прымяняцца для толькі аўтапацверджаных удзельнікаў */",
        "anonymous": "Ананімны{{PLURAL:$1| ўдзельнік|я ўдзельнікі}} на пляцоўцы {{SITENAME}}",
        "pageinfo-category-subcats": "Колькасць падкатэгорый",
        "pageinfo-category-files": "Колькасць файлаў",
        "pageinfo-user-id": "Ідэнтыфікатар удзельніка",
+       "pageinfo-file-hash": "Хэш-значэнне",
        "markaspatrolleddiff": "Пазначыць як ухваленае",
        "markaspatrolledtext": "Пазначыць старонку як ухваленую",
        "markaspatrolledtext-file": "Пазначыць версію файла як ухваленую",
        "redirect-file": "Назва файла",
        "redirect-logid": "ID журнала",
        "redirect-not-exists": "Значэнне не знойдзена",
+       "redirect-not-numeric": "Значэнне не лікавае",
        "fileduplicatesearch": "Пошук дублікатных файлаў",
        "fileduplicatesearch-summary": "Пошук дублікатных файлаў на падставе іх хэшаў.",
        "fileduplicatesearch-filename": "Назва файла:",
        "tags-edit-chosen-placeholder": "Выберыце біркі",
        "tags-edit-chosen-no-results": "Не знойдзена бірак, якія б адпавядалі запыту",
        "tags-edit-reason": "Прычына:",
+       "tags-edit-success": "Змены былі дастасаваныя.",
        "tags-edit-nooldid-title": "Недапушчальная мэтавая версія",
        "tags-edit-nooldid-text": "Вы або не пазначылі мэтавую версію для выканання гэтай функцыі, або пазначаная версія не існуе.",
        "tags-edit-none-selected": "Калі ласка, выберыце прынамсі адну бірку для дадання ці выдалення.",
        "permanentlink": "Пастаянная спасылка",
        "permanentlink-revid": "ідэнтыфікатар праўкі",
        "permanentlink-submit": "Перайсці да версіі",
+       "newsection-page": "Мэтавая старонка",
+       "newsection-submit": "Перайсці на старонку",
        "dberr-problems": "Прабачце, на пляцоўцы здарыліся тэхнічныя цяжкасці.",
        "dberr-again": "Паспрабуйце перачытаць праз некалькі хвілін.",
        "dberr-info": "(Немагчыма звязацца з базай даных: $1)",
        "htmlform-time-placeholder": "ЧЧ:ММ:СС",
        "htmlform-datetime-placeholder": "ГГГГ-ММ-ДД ЧЧ:ММ:СС",
        "htmlform-date-invalid": "Указанае вамі значэнне не похоже на дату. Паспрабуйце выкарыстоўваць фармат ГГГГ-ММ-ДД.",
+       "htmlform-time-invalid": "Указанае вамі значэнне не похоже на час. Паспрабуйце выкарыстоўваць фармат ГГ:ХХ:СС.",
        "htmlform-datetime-invalid": "Вамі выбрана значэнне не падобна на дату і час. Паспрабуйце выкарыстоўваць фармат ГГГГ-ММ-ДД ГГ-ММ-СС.",
        "htmlform-title-badnamespace": "[[:$1]] не ў прасторы назваў \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" - немагчымы загаловак для старонкі",
        "logentry-delete-restore": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3 ($4)",
        "logentry-delete-restore-nocount": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3",
        "restore-count-revisions": "{{PLURAL:$1|1 версія|$1 версіі|$1 версій}}",
+       "restore-count-files": "{{PLURAL:$1|1 файл|$1 файлы|$1 файлаў}}",
        "logentry-delete-event": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|запісу журнала|$5 запісаў журнала}} $3: $4",
        "logentry-delete-revision": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|версіі|$5 версій|$5 версій}} старонкі $3: $4",
        "logentry-delete-event-legacy": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць запісаў журнала $3",
        "expandtemplates": "Разгортванне шаблонаў",
        "expand_templates_intro": "Гэта адмысловая старонка бярэ тэкст і разгортвае ў ім усе шаблоны рэкурсіўна.\nТаксама разгортвае падтрыманыя функцыі парсера кшталту\n<code><nowiki>{{</nowiki>#language:…}}</code> і зменныя віду\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nФактычна, яна разгортвае ў пэўнай ступені ўсё ў двайных фігурных дужках.",
        "expand_templates_title": "Загаловак старонкі, для {{FULLPAGENAME}} і г.д.:",
-       "expand_templates_input": "Уваходны тэкст:",
+       "expand_templates_input": "Уваходны вікітэкст:",
        "expand_templates_output": "Вынік",
        "expand_templates_xml_output": "Выніковы XML",
        "expand_templates_html_output": "Выніковы зыходны код HTML",
        "expand_templates_generate_xml": "Паказаць дрэва сінтаксічнага аналізу XML",
        "expand_templates_generate_rawhtml": "Паказаць зыходны код HTML",
        "expand_templates_preview": "Перадпаказ",
-       "expand_templates_input_missing": "Трэба ўвесці хоць які-небудзь тэкст.",
+       "expand_templates_input_missing": "Трэба ўвесці хоць які-небудзь вікітэкст.",
        "pagelanguage": "Змяніць мову старонкі",
        "pagelang-name": "Старонка",
        "pagelang-language": "Мова",
        "mediastatistics-header-executable": "Выкананыя",
        "mediastatistics-header-archive": "Сціснутыя фарматы",
        "mediastatistics-header-total": "Усе файлы",
+       "json-error-unknown": "Узнікла праблема з JSON. Памылка: $1",
        "json-error-state-mismatch": "Недапушчальны або некарэктны JSON",
        "json-error-syntax": "Памылка сінтаксісу",
        "headline-anchor-title": "Спасылка на гэты раздзел",
        "log-action-filter-contentmodel-change": "Змяненне мадэлі змесціва",
        "log-action-filter-contentmodel-new": "Стварэнне старонкі з нестандартнай мадэллю змесціва",
        "log-action-filter-delete-delete": "Выдаленне старонкі",
+       "log-action-filter-delete-delete_redir": "Перазапіс перасылкі",
        "log-action-filter-delete-restore": "Узнаўленне старонкі",
        "log-action-filter-delete-event": "Выдаленне лога",
        "log-action-filter-delete-revision": "Выдаленне перагляду",
        "log-action-filter-suppress-reblock": "Скрыванне ўдзельніка праз паўторнае блакіраванне",
        "log-action-filter-upload-upload": "Новая загрузка",
        "log-action-filter-upload-overwrite": "Паўторная загрузка",
+       "log-action-filter-upload-revert": "Адкаціць",
        "authmanager-authn-not-in-progress": "Праверка сапраўднасці не выконваецца або сесія перадачы дадзеных была страчана. Калі ласка, пачніце зноў з самага пачатку.",
        "authmanager-authn-no-primary": "Прадастаўленыя ўліковыя дадзеныя не могуць быць завераны.",
        "authmanager-authn-no-local-user": "Пададзеныя ўліковыя дадзеныя не звязаныя з ніводным удзельнікам на гэтай Вікі.",
        "authmanager-authn-autocreate-failed": "Аўтаматычнае стварэнне лакальнага ўліковага запісу не ўдалося: $1",
        "authmanager-change-not-supported": "Прадастаўленыя ўліковыя дадзеныя не могуць быць зменены, як нішто не будзе іх выкарыстоўваць.",
        "authmanager-create-disabled": "стварэнне рахунка не дазволена",
-       "authmanager-create-from-login": "Каб стварыць уліковы запіс, калі ласка, запоўніце палі ніжэй.",
+       "authmanager-create-from-login": "Каб стварыць уліковы запіс, калі ласка, запоўніце палі.",
        "authmanager-create-not-in-progress": "Праверка сапраўднасці не выконваецца або сесія перадачы дадзеных была страчана. Калі ласка, пачніце зноў з самага пачатку.",
        "authmanager-create-no-primary": "Прадастаўленыя ўліковыя дадзеныя не могуць быць выкарыстаны для стварэння ўліковага запісу.",
        "authmanager-link-no-primary": "Прадастаўленыя ўліковыя дадзеныя не могуць быць выкарыстаны для прывязкі рахунку.",
        "revid": "версія $1",
        "pageid": "ID старонкі $1",
        "pagedata-title": "Дадзеныя старонкі",
+       "passwordpolicies-group": "Група",
+       "passwordpolicies-policies": "Палітыкі",
        "passwordpolicies-policyflag-forcechange": "мусіць быць зменены пры ўваходзе",
        "passwordpolicies-policyflag-suggestchangeonlogin": "прапанаваць змяненне пры ўваходзе"
 }
index 30f8142..15f5ee1 100644 (file)
@@ -44,7 +44,8 @@
                        "Jan Růžička",
                        "Jaroslav Cerny",
                        "Slepi",
-                       "Tchoř"
+                       "Tchoř",
+                       "SimonV"
                ]
        },
        "tog-underline": "Podtrhávat odkazy:",
        "sessionfailure": "Nastal problém s vaším přihlášením;\nvámi požadovaná činnost byla zrušena jako prevence před neoprávněným přístupem.\nStiskněte tlačítko „zpět“, obnovte stránku, ze které jste přišli, a zkuste činnost znovu.",
        "changecontentmodel": "Změnit model obsahu stránky",
        "changecontentmodel-legend": "Změnit model obsahu",
-       "changecontentmodel-title-label": "Název stránky",
+       "changecontentmodel-title-label": "Název stránky:",
        "changecontentmodel-current-label": "Současný model obsahu:",
-       "changecontentmodel-model-label": "Nový model obsahu",
+       "changecontentmodel-model-label": "Nový model obsahu:",
        "changecontentmodel-reason-label": "Důvod:",
        "changecontentmodel-submit": "Změnit",
        "changecontentmodel-success-title": "Model obsahu byl změněn",
index b750865..b1a1cce 100644 (file)
        "sessionfailure-title": "Sessionsfejl",
        "sessionfailure": "Der lader til at være et problem med din loginsession; denne handling blev annulleret som en sikkerhedsforanstaltning mod kapring af sessionen. Genindsend venligst formularen.",
        "changecontentmodel-legend": "Ændr indholdsmodel",
-       "changecontentmodel-title-label": "Sidetitel",
-       "changecontentmodel-model-label": "Ny indholdsmodel",
+       "changecontentmodel-title-label": "Sidetitel:",
+       "changecontentmodel-model-label": "Ny indholdsmodel:",
        "changecontentmodel-reason-label": "Begrundelse:",
        "changecontentmodel-submit": "Ændr",
        "changecontentmodel-success-title": "Indholdsmodellen blev ændret",
index 79a94b4..816839c 100644 (file)
        "backend-fail-contenttype": "Could not determine the content type of the file to store at \"$1\".",
        "backend-fail-batchsize": "The storage backend was given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.",
        "backend-fail-usable": "Could not read or write file \"$1\" due to insufficient permissions or missing directories/containers.",
+       "backend-fail-stat": "Could not read the status of file \"$1\".",
+       "backend-fail-hash": "Could not determine the cryptographic hash of file \"$1\".",
        "filejournal-fail-dbconnect": "Could not connect to the journal database for storage backend \"$1\".",
        "filejournal-fail-dbquery": "Could not update the journal database for storage backend \"$1\".",
        "lockmanager-notlocked": "Could not unlock \"$1\"; it is not locked.",
index ab0d137..93bf174 100644 (file)
        "nocreate-loggedin": "Sul ei ole luba luua uusi lehekülgi.",
        "sectioneditnotsupported-title": "Alaosa redigeerimine pole lubatud.",
        "sectioneditnotsupported-text": "Sellel leheküljel pole alaosa redigeerimine lubatud.",
+       "modeleditnotsupported-title": "Redigeerimise toeta",
+       "modeleditnotsupported-text": "Sisumudeli $1 redigeerimise tugi puudub.",
        "permissionserrors": "Loatõrge",
        "permissionserrorstext": "Sul pole õigust seda teha {{PLURAL:$1|järgmisel põhjusel|järgmistel põhjustel}}:",
        "permissionserrorstext-withaction": "Sul pole lubatud {{lcfirst:$2}} {{PLURAL:$1|järgmisel põhjusel|järgmistel põhjustel}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Tühi objekt",
        "content-json-empty-array": "Tühi massiiv",
+       "unsupported-content-model": "<strong>Hoiatus:</strong> Selles vikis puudub sisumudeli $1 tugi.",
+       "unsupported-content-diff": "Erinevuste vaates puudub sisumudeli $1 tugi.",
+       "unsupported-content-diff2": "Sisumudelite $1 ja $2 vaheliste erinevuste vaate tugi puudub selles vikis.",
        "deprecated-self-close-category": "Vigaste endassesuletud HTML-siltidega leheküljed",
        "deprecated-self-close-category-desc": "Leheküljel on endassesuletud HTML-silte nagu <code>&lt;b/></code> või <code>&lt;span/></code>. Nende kuvamisviis viiakse peagi vastavusse HTML5 spetsifikatsiooniga. Seetõttu selliseid silte vikitekstis enam kasutama ei peaks.",
        "duplicate-args-warning": "<strong>Hoiatus:</strong> [[:$1]] kutsub malli [[:$2]] nii, et parameetrile \"$3\" vastab rohkem kui üks väärtus. Väärtustest kasutatakse ainult viimast.",
        "rcfilters-filter-showlinkedto-label": "Näita muudatusi lehekülgedel, millel viidatakse leheküljele",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Leheküljed, mis viitavad</strong> valitud leheküljele",
        "rcfilters-target-page-placeholder": "Sisesta lehekülje pealkiri (või kategooria)",
+       "rcfilters-allcontents-label": "Kõik sisu",
+       "rcfilters-alldiscussions-label": "Kõik arutelud",
        "rcnotefrom": "Allpool on toodud {{PLURAL:$5|muudatus|muudatused}} alates: <strong>$3, kell $4</strong> (näidatakse kuni <strong>$1</strong> muudatust)",
        "rclistfromreset": "Lähtesta kuupäeva valik",
        "rclistfrom": "Näita muudatusi alates: $3, kell $2",
        "sessionfailure": "Näib, et sinu sisselogimisseanss on probleemne.\nSeansiärandamise vastase ettevaatusabinõuna on see toiming tühistatud.\nPalun saada vorm uuesti.",
        "changecontentmodel": "Lehekülje sisumudeli muutmine",
        "changecontentmodel-legend": "Sisumudeli muutmine",
-       "changecontentmodel-title-label": "Lehekülje pealkiri",
-       "changecontentmodel-model-label": "Uus sisumudel",
+       "changecontentmodel-title-label": "Lehekülje pealkiri:",
+       "changecontentmodel-current-label": "Praegune sisumudel:",
+       "changecontentmodel-model-label": "Uus sisumudel:",
        "changecontentmodel-reason-label": "Põhjus:",
        "changecontentmodel-submit": "Muuda",
        "changecontentmodel-success-title": "Sisumudel on muudetud",
        "block-log-flags-angry-autoblock": "Täiustatud automaatblokeerija sisse lülitatud",
        "block-log-flags-hiddenname": "kasutajanimi peidetud",
        "range_block_disabled": "Administraatori õigus blokeerida IP-aadresside vahemik on ära võetud.",
+       "ipb-prevent-user-talk-edit": "Kui osaline blokeering ei piira eraldi nimeruumi \"Kasutaja arutelu\" kasutust, siis peab see lubama enda arutelulehekülje redigeerimist.",
        "ipb_expiry_invalid": "Vigane aegumise tähtaeg.",
        "ipb_expiry_old": "Aegumistähtaeg on minevikus.",
        "ipb_expiry_temp": "Peidetud kasutajanime blokeeringud peavad olema alalised.",
        "lockedbyandtime": "(lukustas $1; $2, kell $3)",
        "move-page": "Lehekülje \"$1\" teisaldamine",
        "move-page-legend": "Lehekülje teisaldamine",
-       "movepagetext": "Allolevat vormi kasutades saad lehekülje ümber nimetada. Lehekülje ajalugu tõstetakse uue pealkirja alla automaatselt.\nPraeguse pealkirjaga leheküljest saab ümbersuunamislehekülg uuele leheküljele.\nSaad senisele pealkirjale viitavad ümbersuunamised automaatselt parandada.\nKui sa seda ei tee, kontrolli, et teisaldamise tõttu ei jää maha [[Special:DoubleRedirects|kahekordseid]] ega [[Special:BrokenRedirects|katkiseid ümbersuunamisi]].\nSinu kohus on hoolitseda selle eest, et kõik jääks toimima, nagu ette nähtud.\n\nPane tähele, et lehekülge <strong>ei teisaldata</strong> juhul, kui uue pealkirjaga lehekülg on juba olemas. Erandiks on juhud, kui viimane on redigeerimisajaloota ümbersuunamislehekülg.\nSee tähendab, et kogemata ei saa üle kirjutada juba olemasolevat lehekülge, kuid saab ebaõnnestunud ümbernimetamise tagasi pöörata.\n\n<strong>Märkus:</strong>\nTegu võib olla väga loetava lehekülje jaoks tõsise ja ootamatu muudatusega;\nenne jätkamist teadvusta palun tagajärgi.",
+       "movepagetext": "Allolevat vormi kasutades saad lehekülje ümber nimetada, nii et selle ajalugu tõstetakse uue pealkirja alla.\nVana pealkirjaga leheküljest saab ümbersuunamine uue pealkirjaga leheküljele.\nSaad senisele pealkirjale viitavad ümbersuunamised automaatselt parandada.\nKui sa seda ei tee, siis kontrolli, et teisaldamise tõttu ei jää maha [[Special:DoubleRedirects|kahekordseid]] ega [[Special:BrokenRedirects|katkiseid ümbersuunamisi]].\nSinu kohus on hoolitseda selle eest, et kõik jääks toimima, nagu ette nähtud.\n\nPane tähele, et lehekülge <strong>ei teisaldata</strong> juhul, kui uue pealkirjaga lehekülg on juba olemas. Erandiks on juhud, kui viimane on redigeerimisajaloota ümbersuunamislehekülg.\nSee tähendab, et kogemata ei saa üle kirjutada juba olemasolevat lehekülge, kuid saab ebaõnnestunud ümbernimetamise tagasi pöörata.\n\n<strong>Märkus:</strong>\nTegu võib olla väga loetava lehekülje jaoks tõsise ja ootamatu muudatusega;\nenne jätkamist teadvusta palun tagajärgi.",
        "movepagetext-noredirectfixer": "Allolevat vormi kasutades saad lehekülje ümber nimetada. Lehekülje ajalugu tõstetakse uue pealkirja alla automaatselt.\nPraeguse pealkirjaga leheküljest saab ümbersuunamislehekülg uuele leheküljele.\nKontrolli, et teisaldamise tõttu ei jää maha [[Special:DoubleRedirects|kahekordseid]] ega [[Special:BrokenRedirects|katkiseid ümbersuunamisi]].\nSinu kohus on hoolitseda selle eest, et kõik jääks toimima, nagu ette nähtud.\n\nPane tähele, et lehekülge <strong>ei teisaldata</strong> juhul, kui uue pealkirjaga lehekülg on juba olemas. Erandiks on juhud, kui olemasolev lehekülg on tühi või redigeerimisajaloota ümbersuunamislehekülg.\nSee tähendab, et kogemata ei saa üle kirjutada juba olemasolevat lehekülge, kuid saab ebaõnnestunud ümbernimetamise tagasi pöörata.\n\n<strong>Hoiatus!</strong>\nTegu võib olla väga loetava lehekülje jaoks tõsise ja ootamatu muudatusega;\nenne jätkamist teadvusta palun tagajärgi.",
+       "movepagetext-noredirectsupport": "Allolevat vormi kasutades saad lehekülje ümber nimetada, nii et selle ajalugu tõstetakse uue pealkirja alla.\nSinu kohus on hoolitseda selle eest, et lingid viitaks jätkuvalt sinna, kuhu vaja.\n\nPane tähele, et lehekülge <strong>ei teisaldata</strong> juhul, kui uue pealkirjaga lehekülg on juba olemas.\nSee tähendab, et kogemata ei saa üle kirjutada juba olemasolevat lehekülge, kuid saab ebaõnnestunud ümbernimetamise tagasi pöörata.\n\n<strong>Märkus:</strong>\nTegu võib olla väga loetava lehekülje jaoks tõsise ja ootamatu muudatusega;\nenne jätkamist teadvusta palun tagajärgi.",
        "movepagetalktext": "Kui märgid selle ruudu, teisaldatakse arutelulehekülg automaatselt uue pealkirja alla. Seda välja arvatud juhul, kui uue pealkirja all on juba arutelulehekülg, mis pole tühi.\n\nSel juhul saad lehekülje soovi korral käsitsi teisaldada või liita.",
        "moveuserpage-warning": "'''Hoiatus:''' Oled teisaldamas kasutajalehekülge. Pane tähele, et teisaldatakse ainult lehekülg ja kasutajat '''ei''' nimetata ümber.",
        "movecategorypage-warning": "<strong>Hoiatus:</strong> Oled teisaldamas kategoorialehekülge. Pane palun tähele, et teisaldatakse vaid see lehekülg ja ühtegi vanas kategoorias sisalduvat lehekülge <em>ei</em> kategoriseerita ümber uude kategooriasse.",
        "move-subpages": "Teisalda alamleheküljed (kuni $1)",
        "move-talk-subpages": "Teisalda arutelulehekülje alamleheküljed (kuni $1)",
        "movepage-page-exists": "Lehekülg $1 on juba olemas ja seda ei saa automaatselt üle kirjutada.",
+       "movepage-source-doesnt-exist": "Lehekülge \"$1\" pole olemas ja seda ei saa teisaldada.",
        "movepage-page-moved": "Lehekülg $1 on teisaldatud pealkirja $2 alla.",
        "movepage-page-unmoved": "Lehekülge $1 ei saanud teisaldada pealkirja $2 alla.",
        "movepage-max-pages": "Teisaldatud on $1 {{PLURAL:$1|lehekülg|lehekülge}}, mis on teisaldatavate lehekülgede ülemmäär. Rohkem lehekülgi automaatselt ei teisaldata.",
        "delete_and_move_reason": "Kustutatud, et tõsta asemele lehekülg \"[[$1]]\"",
        "selfmove": "Sama pealkiri;\nlehekülge ei saa teisaldada iseenda asemele.",
        "immobile-source-namespace": "Lehekülgi ei saa teisaldada nimeruumis $1",
+       "immobile-source-namespace-iw": "Selle viki kaudu ei saa teisaldada lehekülgi, mis asuvad teises vikis.",
        "immobile-target-namespace": "Lehekülgi ei saa teisaldada nimeruumi \"$1\"",
        "immobile-target-namespace-iw": "Keelelink ei ole sobiv koht lehekülje teisaldamiseks.",
        "immobile-source-page": "Lehekülg ei ole teisaldatav.",
        "immobile-target-page": "Soovitud pealkirja alla ei saa teisaldada.",
+       "movepage-invalid-target-title": "Päritud pealkiri on vigane.",
        "bad-target-model": "Soovitud sihtlehekülje sisumudel on erinev. {{ucfirst:$1}}i ei saa teisendada $2iks.",
        "imagenocrossnamespace": "Faili ei saa teisaldada mõnda muusse nimeruumi.",
        "nonfile-cannot-move-to-file": "Failinimeruumi saab ainult faile teisaldada.",
        "permanentlink": "Püsilink",
        "permanentlink-revid": "Redaktsiooni identifikaator",
        "permanentlink-submit": "Mine redaktsiooni juurde",
+       "newsection": "Uus alaosa",
+       "newsection-page": "Sihtlehekülg",
+       "newsection-submit": "Mine leheküljele",
        "dberr-problems": "Kahjuks on sellel saidil tehnilisi probleeme",
        "dberr-again": "Oota mõni hetk ja laadi lehekülg uuesti.",
        "dberr-info": "(Juurdepääs andmebaasile puudub: $1)",
index 2b8491a..b4eeb1f 100644 (file)
        "exif-scenetype-1": "D'Bild gouf fotograféiert",
        "exif-customrendered-0": "Standard",
        "exif-customrendered-1": "Benotzerdefinéiert",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-8": "Portrait",
        "exif-exposuremode-0": "Automatesch Beliichtung",
        "exif-exposuremode-1": "Manuell Beliichtung",
        "exif-exposuremode-2": "Beliichtungsserie",
index 8ebf8b1..2ccc002 100644 (file)
        "exif-scenetype-1": "直接照像圖片",
        "exif-customrendered-0": "一般程序",
        "exif-customrendered-1": "自訂程序",
+       "exif-customrendered-2": "HDR(原始未儲存)",
+       "exif-customrendered-3": "HDR(原始已儲存)",
+       "exif-customrendered-4": "原始(用於 HDR)",
+       "exif-customrendered-6": "全景",
+       "exif-customrendered-7": "人像 HDR",
+       "exif-customrendered-8": "人像",
        "exif-exposuremode-0": "自動曝光",
        "exif-exposuremode-1": "手動曝光",
        "exif-exposuremode-2": "自動包圍曝光",
index 3096f77..88a292c 100644 (file)
        "exbeforeblank": "محتوای صفحه قبل از خالی‌کردن این بود: «$1»",
        "delete-confirm": "حذف «$1»",
        "delete-legend": "حذف",
-       "historywarning": "<strong>هشدار:</strong> صفحه‌ای که در حال پاک کردن آن هستید دارای یک تاریخچه همراه با $1 {{PLURAL:$1|بازبینی|بازبینی}} است:",
+       "historywarning": "<strong>هشدار:</strong> صفحه‌ای که در حال حذف کردن آن هستید دارای تاریخچه‌ای شامل $1 {{PLURAL:$1|نسخه}} است:",
        "historyaction-submit": "نمایش نسخه‌ها",
        "confirmdeletetext": "شما در حال حذف کردن یک صفحه یا تصویر از پایگاه‌های داده همراه با تمام تاریخچهٔ آن هستید.\nلطفاً این عمل را تأیید کنید و اطمینان حاصل کنید که عواقب این کار را می‌دانید و این عمل را مطابق با [[{{MediaWiki:Policy-url}}|سیاست‌ها]] انجام می‌دهید.",
        "actioncomplete": "عمل انجام شد",
index ebed1b0..a4310e9 100644 (file)
        "sessionfailure": "Votre session de connexion semble avoir des problèmes ;\ncette action a été annulée en prévention d'un piratage de session.\nVeuillez soumettre le formulaire de nouveau.",
        "changecontentmodel": "Modifier le modèle de contenu d’une page",
        "changecontentmodel-legend": "Modifier le modèle de contenu",
-       "changecontentmodel-title-label": "Titre de la page",
+       "changecontentmodel-title-label": "Titre de la page :",
        "changecontentmodel-current-label": "Modèle de contenu actuel :",
-       "changecontentmodel-model-label": "Nouveau modèle de contenu",
+       "changecontentmodel-model-label": "Nouveau modèle de contenu :",
        "changecontentmodel-reason-label": "Motif :",
        "changecontentmodel-submit": "Modifier",
        "changecontentmodel-success-title": "Le modèle de contenu a été modifié",
index f4e6f6f..ed268de 100644 (file)
        "sessionfailure": "נראה שיש בעיה בחיבור שלך לאתר;\nפעולה זו בוטלה כאמצעי זהירות נגד התחזות לתקשורת ממחשבך.\nנא לשלוח מחדש את הטופס.",
        "changecontentmodel": "שינוי מודל התוכן של דף",
        "changecontentmodel-legend": "שינוי מודל התוכן",
-       "changecontentmodel-title-label": "שם הדף",
+       "changecontentmodel-title-label": "שם הדף:",
        "changecontentmodel-current-label": "מודל התוכן הנוכחי:",
-       "changecontentmodel-model-label": "מודל התוכן החדש",
+       "changecontentmodel-model-label": "מודל התוכן החדש:",
        "changecontentmodel-reason-label": "סיבה:",
        "changecontentmodel-submit": "שינוי",
        "changecontentmodel-success-title": "מודל התוכן שוּנה",
index 3a67537..42f3c58 100644 (file)
        "content-json-empty-object": "Objecto vacue",
        "content-json-empty-array": "Array vacue",
        "unsupported-content-model": "<strong>Attention:</strong> Le modello de contento $1 non es supportate sur iste wiki.",
+       "unsupported-content-diff": "Non es possibile monstrar differentias pro contento del modello $1.",
+       "unsupported-content-diff2": "Non es possibile monstrar differentias inter contento del modellos $1 e $2 sur iste wiki.",
        "deprecated-self-close-category": "Paginas que usa etiquettas HTML auto-claudite non valide",
        "deprecated-self-close-category-desc": "Le pagina contine etiquettas HTML auto-claudite non valide, como <code>&lt;b/></code> o <code>&lt;span/></code>. Le comportamento de istes cambiara proximemente pro esser in accordo con le specification HTML5, dunque lor uso in wikitexto es obsolete.",
        "duplicate-args-warning": "<strong>Attention:</strong> [[:$1]] appella [[:$2]] con plure valores pro le parametro \"$3\". Solmente le ultime valor fornite essera usate.",
        "right-editmyusercss": "Modificar le proprie files CSS de usator",
        "right-editmyuserjson": "Modificar le files JSON del proprie usator",
        "right-editmyuserjs": "Modificar le files JavaScript del proprie usator",
+       "right-editmyuserjsredirect": "Modificar le proprie paginas JavaScript de usator que es redirectiones",
        "right-viewmywatchlist": "Vider le proprie observatorio",
        "right-editmywatchlist": "Modificar le proprie observatorio. Remarca que alcun actiones totevia adde paginas mesmo sin iste derecto.",
        "right-viewmyprivateinfo": "Vider le proprie datos private (p.ex. adresse de e-mail, nomine real)",
        "action-editmyusercss": "modificar le files CSS del proprie usator",
        "action-editmyuserjson": "modificar le files JSON del proprie usator",
        "action-editmyuserjs": "modificar le files JavaScript del proprie usator",
+       "action-editmyuserjsredirect": "modificar le proprie paginas JavaScript de usator que es redirectiones",
        "action-viewsuppressed": "vider versiones celate pro tote le usatores",
        "action-hideuser": "blocar un nomine de usator, celante lo del publico",
        "action-ipblock-exempt": "contornar le blocadas de adresses IP, blocadas automatic e blocadas de intervallos IP",
        "rcfilters-clear-all-filters": "Rader tote le filtros",
        "rcfilters-show-new-changes": "Vider le modificationes apportate desde $1",
        "rcfilters-search-placeholder": "Filtrar le modificationes (usa le menu o cerca le nomine del filtro)",
+       "rcfilters-search-placeholder-mobile": "Filtros",
        "rcfilters-invalid-filter": "Filtro non valide",
        "rcfilters-empty-filter": "Nulle filtro active. Tote le contributiones es monstrate.",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-filter-showlinkedto-label": "Monstrar modificationes sur paginas que liga a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Paginas que liga verso</strong> le pagina seligite",
        "rcfilters-target-page-placeholder": "Entra le nomine de un pagina (o categoria)",
+       "rcfilters-allcontents-label": "Tote le contento",
+       "rcfilters-alldiscussions-label": "Tote le discussiones",
        "rcnotefrom": "Ecce le {{PLURAL:$5|modification|modificationes}} a partir del <strong>$3 a $4</strong> (usque a <strong>$1</strong> entratas monstrate).",
        "rclistfromreset": "Reinitialisar selection de data",
        "rclistfrom": "Monstrar nove modificationes a partir del $3 a $2",
        "sessionfailure": "Il pare haber un problema con tu session;\niste action ha essite cancellate como precaution contra le robamento de sessiones.\nPer favor, resubmitte le formulario.",
        "changecontentmodel": "Cambiar le modello de contento de un pagina",
        "changecontentmodel-legend": "Cambiar modello de contento",
-       "changecontentmodel-title-label": "Titulo del pagina",
-       "changecontentmodel-model-label": "Nove modello de contento",
+       "changecontentmodel-title-label": "Titulo del pagina:",
+       "changecontentmodel-current-label": "Modello de contento actual:",
+       "changecontentmodel-model-label": "Nove modello de contento:",
        "changecontentmodel-reason-label": "Motivo:",
        "changecontentmodel-submit": "Cambiar",
        "changecontentmodel-success-title": "Le modello de contento ha essite cambiate",
index 9cb6ee1..4d8c538 100644 (file)
        "listredirects": "Listo di ridirektili",
        "listduplicatedfiles": "Listo pri arkivi kun duplikati",
        "unusedtemplates": "Neuzata shabloni",
+       "unusedtemplatestext": "Ca pagino montras omna pagini di {{ns:template}} qui ne uzesas en altra pagini.\nVoluntez serchar altra ligili a la shabloni montrata adinfre, ante efacar li.",
        "unusedtemplateswlh": "altra ligili",
        "randompage": "Hazarda pagino",
        "randomincategory-submit": "Irez",
index f22ae2b..9b74f8e 100644 (file)
        "sessionfailure": "로그인 세션에 문제가 발생한 것 같습니다.\n세션 하이재킹을 막기 위해 동작이 취소되었습니다.\n양식을 다시 제출해 주십시오.",
        "changecontentmodel": "문서의 콘텐츠 모델을 변경",
        "changecontentmodel-legend": "콘텐츠 모델 변경",
-       "changecontentmodel-title-label": "문서 제목",
+       "changecontentmodel-title-label": "문서 제목:",
        "changecontentmodel-current-label": "현재의 콘텐츠 모델:",
-       "changecontentmodel-model-label": "새 콘텐츠 모델",
+       "changecontentmodel-model-label": "새 콘텐츠 모델:",
        "changecontentmodel-reason-label": "이유:",
        "changecontentmodel-submit": "바꾸기",
        "changecontentmodel-success-title": "콘텐츠 모델이 변경되었습니다",
index 304d5b4..33ffaa8 100644 (file)
        "nocreate-loggedin": "Dir hutt keng Berechtigung fir nei Säiten unzeleeën.",
        "sectioneditnotsupported-title": "Ännere vum Abschnitt gëtt net ënnerstëtzt",
        "sectioneditnotsupported-text": "D'Ännere vun Abschnitte gëtt op dëser Ännerungssäit net ënnerstëtzt.",
+       "modeleditnotsupported-title": "Ännere gëtt net ënnerstëtzt",
        "permissionserrors": "Net genuch Rechter",
        "permissionserrorstext": "Dir hutt net genuch Rechter fir déi Aktioun auszeféieren. {{PLURAL:$1|Grond|Grënn}}:",
        "permissionserrorstext-withaction": "Dir sidd, aus {{PLURAL:$1|folgendem Grond|folgende Grënn}}, net berechtegt $2 :",
        "sessionfailure": "Et schéngt e Problem mat Ärer Sessioun ze ginn;\nDës Aktioun gouf aus Sécherheetsgrënn ofgebrach, fir ze verhënneren datt Är Sessioun piratéiert ka ginn.\nSchéckt de Formulaire w.e.g. nach eng Kéier.",
        "changecontentmodel": "De Modell vum Inhalt vun enger Säit änneren",
        "changecontentmodel-legend": "Modell vun enger Säit mat Inhalt änneren",
-       "changecontentmodel-title-label": "Titel vun der Säit",
-       "changecontentmodel-model-label": "Neie Modell vun enger Säit mat Inhalt",
+       "changecontentmodel-title-label": "Titel vun der Säit:",
+       "changecontentmodel-model-label": "Neie Modell vun enger Säit mat Inhalt:",
        "changecontentmodel-reason-label": "Grond:",
        "changecontentmodel-submit": "Änneren",
        "changecontentmodel-success-title": "De Modell vum Inhalt gouf geännert",
        "immobile-target-namespace-iw": "En Interwiki-Link ass kee gëltegt Zil beim Réckele vun enger Säit.",
        "immobile-source-page": "Dës Säit kann net geréckelt ginn.",
        "immobile-target-page": "Kann net op de Bestëmmungs-titel geréckelt ginn.",
+       "movepage-invalid-target-title": "De gefroten Numm ass net valabel.",
        "bad-target-model": "Déi gewënschten Zilsäit benotzt en anere Modell fir den Inhalt. Et kann net vun $1 op $2 ëmgewandelt ginn.",
        "imagenocrossnamespace": "Fichiere kënnen net an aner Nummraim geréckelt ginn",
        "nonfile-cannot-move-to-file": "\"Keng Fichiere\" kënnen net an den {{ns:file}}-Nummraum geréckelt ginn",
index 8a37844..373ba26 100644 (file)
@@ -6,7 +6,8 @@
                        "Mjbmr",
                        "Hosseinblue",
                        "MtDu",
-                       "Shahriar dehghani"
+                       "Shahriar dehghani",
+                       "Shahriar.dehghani24"
                ]
        },
        "tog-underline": "لینکیا خط وه دومن",
        "filehist-dimensions": "ابعاد",
        "filehist-comment": "توٙضیح",
        "imagelinks": "ئیستفادھ د فایل",
-       "linkstoimage": "دوٙمین الذکر {{PLURAL:$1|لینکل بألگە|$1 لینک بألگل}} بە ئی فایل:",
+       "linkstoimage": "{{PLURAL:$1|صفحهٔ|صفحَلِ}} زِر و ای عکس پیوند دارہ :",
        "nolinkstoimage": "بألگە یلی کە ڤە ئی فایل لینک دائنە نی.",
        "sharedupload-desc-here": "ئی فایل ز $1 ئوٙمائە ڤ شاید د پۉرۉجە یل دیە مورد ئیستفادھ ڤابین.\nتوٙضیحتل ری [$2 بألگە تۉضیح فایل] دوٙمین نیشۉ ڤابیە .",
        "upload-disallowed-here": "ئیشا نیتأریت ئی فایلنە بینڤیسیت",
index 12e7d8d..12f69f0 100644 (file)
        "minoreditletter": "k",
        "newpageletter": "B",
        "boteditletter": "b",
-       "rc-change-size-new": "$1 {{PLURAL:$1|byte|bita}} salapeh parubahan",
+       "rc-change-size-new": "$1 {{PLURAL:$1|bita}} salapeh parubahan",
        "rc-enhanced-expand": "Caliak rincian",
        "rc-enhanced-hide": "Suruakkan rincian",
        "rc-old-title": "awalnyo dibuek jo judul \"$1\"",
        "tooltip-pt-anonuserpage": "Laman pangguno IP Sanak",
        "tooltip-pt-mytalk": "Laman rundiang {{GENDER:|Sanak}}",
        "tooltip-pt-anontalk": "Parundiangan tantang suntiangan dari IP ko",
-       "tooltip-pt-preferences": "Piliahan {{GENDER:|Sanak}}",
+       "tooltip-pt-preferences": "Pangaturan {{GENDER:|Sanak}}",
        "tooltip-pt-watchlist": "Daftar laman nan dipantau.",
        "tooltip-pt-mycontris": "Daftar jariah {{GENDER:|Sanak}}",
        "tooltip-pt-login": "Sanak disaranan untuak masuak log; walaupun indak wajib",
index ca975c2..30edb01 100644 (file)
        "log-action-filter-upload-upload": "പുതിയ അപ്‌ലോഡ്",
        "log-action-filter-upload-overwrite": "പുനർ അപ്‌ലോഡ്",
        "log-action-filter-upload-revert": "തിരിച്ചാക്കൽ",
+       "authmanager-authn-autocreate-failed": "പ്രാദേശിക അംഗത്വം യാന്ത്രികമായി സൃഷ്ടിക്കൽ പരാജയപ്പെട്ടു: $1",
        "authmanager-create-disabled": "അംഗത്വസൃഷ്ടി പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു.",
        "authmanager-create-from-login": "താങ്കളുടെ അംഗത്വം സൃഷ്ടിക്കാൻ, ദയവായി കളങ്ങൾ പൂരിപ്പിക്കുക.",
        "authmanager-create-not-in-progress": "സെഷൻ ഡേറ്റ നഷ്ടപ്പെട്ടതിനാൽ അംഗത്വസൃഷ്ടിയുടെ പുരോഗതി നഷ്ടമായിരിക്കുന്നു. ദയവായി  ആദ്യം മുതൽ വീണ്ടും തുടങ്ങുക.",
        "authmanager-create-no-primary": "അംഗത്വസൃഷ്ടിക്ക് നൽകിയിരിക്കുന്ന വിവരങ്ങൾ ഉപയോഗിക്കാനാവില്ല.",
        "authmanager-link-no-primary": "അംഗത്വം ബന്ധിപ്പിക്കാൻ നൽകിയിരിക്കുന്ന വിവരങ്ങൾ ഉപയോഗിക്കാനാവില്ല.",
        "authmanager-link-not-in-progress": "സെഷൻ ഡേറ്റ നഷ്ടപ്പെട്ടതിനാൽ അംഗത്വം ബന്ധിപ്പിക്കലിന്റെ പുരോഗതി നഷ്ടമായിരിക്കുന്നു. ദയവായി  ആദ്യം മുതൽ വീണ്ടും തുടങ്ങുക.",
+       "authmanager-autocreate-noperm": "യാന്ത്രികമായ അംഗത്വസൃഷ്ടി അനുവദിച്ചിട്ടില്ല.",
+       "authmanager-autocreate-exception": "മുമ്പുണ്ടായ പിഴവുകളെത്തുടർന്ന് യാന്ത്രികമായ അംഗത്വസൃഷ്ടി താത്കാലികമായി പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു.",
        "authmanager-userdoesnotexist": "\"$1\" എന്ന ഉപയോക്തൃ അം‌ഗത്വം നിലവിലില്ല.",
        "authmanager-userlogin-remembermypassword-help": "രഹസ്യവാക്ക് സെഷൻ കാലയളവിലധികം ഓർത്തുവെക്കണോ.",
        "authmanager-username-help": "രഹസ്യവാക്ക് ഉപയോഗിച്ചുള്ള സാധൂകരണം.",
        "specialmute": "നിശബ്ദമാക്കുക",
        "specialmute-submit": "സ്ഥിരീകരിക്കുക",
        "specialmute-label-mute-email": "ഈ ഉപയോക്താവിൽ നിന്നുമുള്ള ഇമെയിലുകൾ നിശബ്ദമാക്കുക",
+       "specialmute-error-invalid-user": "ആവശ്യപ്പെട്ട ഉപയോക്തൃനാമം കണ്ടെത്താനായില്ല.",
        "specialmute-login-required": "താങ്കളുടെ നിശബ്ദമാക്കൽ ഐച്ഛികങ്ങൾ മാറ്റുന്നതിനായി ദയവായി പ്രവേശിക്കുക.",
        "mute-preferences": "നിശബ്ദമാക്കൽ ഐച്ഛികങ്ങൾ",
        "revid": "നാൾപ്പതിപ്പ് $1",
index 9d5515e..dd6aa2e 100644 (file)
        "viewsource-title": "Vere surgente 'e $1",
        "actionthrottled": "Azione ritardata",
        "actionthrottledtext": "Comme mesùra anti-abuse, site lemmetato 'a ffà st'azione troppe vote dint'a nu curto spazio 'e tiempo, e mo stu lèmmeto l'avite superato.\nPe piacere pruvate n'ata vota dint'a quacche minuto.",
-       "protectedpagetext": "Sta paggena s'è prutetta pe' ne bloccà 'a mudifeca o n'ata azione.",
+       "protectedpagetext": "Sta paggena s'è prutetta pe ne ntuppà 'o càgno o quacche ata azione.",
        "viewsourcetext": "Putite vedé e copià 'o codece surgiva 'e sta paggena.",
        "viewyourtext": "Putite vedé e copià 'o codice surgiva d' 'e <strong>cagnamiénte vuoste</strong> a sta paggena.",
        "protectedinterface": "Sta paggena nce appruviggióna 'e n'interfaccia testo p' 'o software dint'a sta wiki, e s'è prutetta pe' nce scanzà 'e cocch'abbuso.\nSi se buò azzeccà o cagnà traduzzione ncopp'a tutte 'e wiki, pe piacere ausate [https://translatewiki.net/ translatewiki.net], 'o pruggetto Mediawiki p'a localizzaziona dint'a l'ate llengue",
        "logout": "Jèsce",
        "userlogout": "Jèsce",
        "notloggedin": "Acciesso nun affettuato",
-       "userlogin-noaccount": "Nun tenite ancora n'acciesso?",
+       "userlogin-noaccount": "Nun tenite perzine n'acciesso?",
        "userlogin-joinproject": "Facite 'o riggistro ncopp'a {{SITENAME}}",
        "createaccount": "Crèa nu cunto nuovo",
        "userlogin-resetpassword-link": "Te sì scurdat' 'a password?",
        "usercssyoucanpreview": "'''Cunziglio:''' spremme 'o buttone 'Vide anteprimma' pe' pruvà 'o CSS nuovo apprimma d' 'o sarvà.",
        "userjsonyoucanpreview": "<strong>Cunziglio:</strong> premme 'o buttone \"{{int:showpreview}}\" pe' pruvà 'o JSON nuovo apprimma d' 'o sarvà.",
        "userjsyoucanpreview": "'''Cunziglio:''' spremme 'o buttone 'Vide anteprimma' pe' pruvà 'o JavaScript nuovo apprimma d' 'o sarvà.",
-       "usercsspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o CSS perzunale. 'E cagnamiente nun so' state ancora sarvate!'''",
+       "usercsspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o CSS perzunale. 'E cagnamiente nun so' state sarvate perzì!'''",
        "userjsonpreview": "<strong>Arricuordate ca chest'è sulamente n'anteprimma p' 'o JSON perzunale. 'E cagnamiente nun so' state ancora sarvate!</strong>",
-       "userjspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o JavaScript perzunale. 'E cagnamiente nun so' state ancora sarvate!'''",
-       "sitecsspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o CSS. 'E cagnamiente nun so' state ancora sarvate!'''",
+       "userjspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o JavaScript perzunale. 'E cagnamiente nun so' state sarvate perzì!'''",
+       "sitecsspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o CSS. 'E cagnamiente nun so' state sarvate perzì!'''",
        "sitejsonpreview": "<strong>Arricuordate ca chest'è sulamente n'anteprimma d' 'a configurazzione d' 'o JSON. 'E cagnamiente nun so' state ancora sarvate!</strong>",
-       "sitejspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o codece JavaScript. 'E cagnamiente nun so' state ancora sarvate!'''",
+       "sitejspreview": "'''Arricuordate ca chest'è sulamente n'anteprimma p' 'o codece JavaScript. 'E cagnamiente nun so' state sarvate perzì!'''",
        "userinvalidconfigtitle": "<strong>Attenziò:</strong> Nun esiste nisciuna skin c' 'o nomme \"$1\". Vide ch' 'e paggene .css e .js personalezzate teneno nu titolo ca minuscola, p'esempio {{ns:user}}:Esempio/vector.css (e no {{ns:user}}:Esempio/Vector.css).",
        "updated": "(Agghiurnato)",
        "note": "'''Nota:'''",
-       "previewnote": "'''Chesta è sola n'anteprimma; 'e cagnamiénte â paggena nun songo ancora sarvate!'''",
+       "previewnote": "'''Chesta è sola n'anteprimma; 'e cagnamiénte â paggena nun so' state sarvate perzì!'''",
        "continue-editing": "Trasite int'a l'area 'e modifica",
        "previewconflict": "L'anteprimma currisponne a 'o testo presente dint'a cascia 'e modifica ccà ncoppa e rappresentasse 'a paggena comme cumpare si sciglite 'e Sarvà ind'a stu mumento.",
-       "session_fail_preview": "Scusate! nun è possibbile prucessà 'o cagnamiento pecché se so' sperdut' 'e date d' 'a sessione.\n\nPuò darse ca d' 'a parta vosta nun eravate trasute.<strong>Pe' piacere cuntrullate ca site ancora dinto e tentate n'ata vota</strong>.\nSi chesto nun funziunasse ancora, tentate a ve [[Special:UserLogout|n'ascì]] e a trasì n'ata vota, e cuntrullate si 'o navigatóre vuosto tenisse 'e cookies appicciàte.",
+       "session_fail_preview": "Scusate! nun è possibbile prucessà 'o cagnamiento pecché se so' sperdut' 'e date d' 'a sessione.\n\nPuò essere ca d' 'a parta vosta nun stavate trasute.<strong>Pe piacere cuntrullate ca state ancora dinto e pruate n'ata vota</strong>.\nSi cchesto nun funziunasse porzì, pruate a ve [[Special:UserLogout|n'ascì]] e a trasì n'ata vota, e cuntrullate si 'o navigatóre vuosto tenisse 'e cookies appicciàte.",
        "session_fail_preview_html": "Scusate! Nun è possibbile prucessà 'o cagnamiento pecché se so' sperdut' 'e date d' 'a sessione.\nProva n'ata vota.\n\n<em>Siccome dint' 'o {{SITENAME}} è abilitato l'uso 'e l'HTML cruro, 'o buttone d'anteprimma nun è abbiàto comme misura 'e sicurezza annanza cocch'attacco JavaScript</em>\n\n<strong>Si chest'era nu tentativo legittimo 'e cagnamiento, tentate n'ata vota.</strong>\nSi nun funziunass'ancora, putite pruvà a ve [[Special:UserLogout|n'ascì]] e a trasì n'ata vota, e cuntrullate si 'o navigatóre vuosto premmettesse 'e cookies ca veneno 'a stu sito.",
        "token_suffix_mismatch": "'''Stu cagnamiento nun è stato sarvato pecché 'o client ave mmustato nu sbaglio dint'o scrivere d' 'e carattere d' 'a punteggiatura token. Pe luvà na possibbile corruzione d' 'o testo dint'a paggena, s'è rifiutat' 'a modifeca.\n\nSta situazione se può truvà, quanno staje ausanno nu servizio 'e proxy anonime via web cu d' 'e bug.'''",
        "edit_form_incomplete": "'''Cocche parte d' 'o modulo 'e cagnamiento nun ha arrivato a 'o server; cuntrolla ch' 'e cagnamiente songo intatte e prova n'ata vota.'''",
        "last": "prec",
        "page_first": "primma",
        "page_last": "úrdema",
-       "histlegend": "Confronto nfra verziune: sciglite 'e casciulelle c'attoccassero a 'e verziune che vulite cunfruntà e spremmite Invio o pure 'o buttóne ccà abbascio.\n\nLiggenda: '''({{int:cur}})''' = differenze c' 'a verzione 'e mmò, '''({{int:last}})''' = differenze c' 'a verzione 'e primma, '''{{int:minoreditletter}}''' = cagnamiento minore",
-       "history-fieldset-title": "Circa pe' verziune",
+       "histlegend": "Confronto nfra verziune: sciglite 'e casciulelle c'attoccassero a 'e verziune che vulite cunfruntà e spremmite Invio o pure 'o buttóne ccà abbascio.\n\nLiggenda: '''({{int:cur}})''' = differenze c’'a verzione 'e mmò, '''({{int:last}})''' = differenze c’'a verzione 'e primma, '''{{int:minoreditletter}}''' = cagnamiénto piccerillo",
+       "history-fieldset-title": "Truova pe verzione",
        "history-show-deleted": "Sulo 'e verziune scancellate",
        "histfirst": "primma",
        "histlast": "urdema",
        "revdelete-text-text": "'E verziune scancellate cumpareno ancora dint' 'a cronologgia d' 'a paggena, ma na parte d' 'o cuntenuto lloro nun sarrà a disposizione a 'o pubbreco.",
        "revdelete-text-file": "'E verziune 'e file scancellate cumpareno ancora dint' 'a cronologgia d' 'o file, ma parte d' 'o cuntenuto lloro nun sarrà a disposizione a 'o pubbreco.",
        "logdelete-text": "'E fatte 'e riggistro scancellate cumpareno ancora dint' 'a cronologgia 'e riggistro, ma na parte d' 'o cuntenuto lloro nun sarrà a disposizione a 'o pubbreco.",
-       "revdelete-text-others": "Ll'at'ammenistrature puterranno ancora trasì e arrepiglià 'e cuntenute annascunnute, si nun so' state mpustate cchiù restrizziune.",
+       "revdelete-text-others": "Ll'at'ammenistrature putarranno trasì perzì e arrepiglià 'e cuntenute annascunnute, si nun so' state mpustate ate restrizziune.",
        "revdelete-confirm": "Pe ppiacere cunfermate ca overo vulite ffà chisto, ca cunuscite 'e cunseguenze, e ca state facenno chisto rispettanno 'e [[{{MediaWiki:Policy-url}}|linee guida]].",
        "revdelete-suppress-text": "Sti luvamiente hana essere fatte '''unicamente''' dint' 'e situaziune ccà abbascio:\n* nfurmaziune potenzialmente diffamatorie\n* date perzunale inopportune\n*: ''indirizze, nummeri 'e telefono, codece fiscale, ecc.''",
        "revdelete-legend": "Miette 'e limmete 'e visibilità",
        "filerevert-identical": "'A verziona 'e mo d' 'o file è già eguale eguale a chella scigliuta.",
        "filedelete": "Scancella $1",
        "filedelete-legend": "Scancella 'o file",
-       "filedelete-intro": "State pe' scancellà 'o file '''[[Media:$1|$1]]''' cu tutta 'a cronologgia 'e chisto.",
+       "filedelete-intro": "State pe scancellà 'o file '''[[Media:$1|$1]]''' cu tutta 'a cronologgia soia.",
        "filedelete-intro-old": "State a scancellà 'a verziona 'e '''[[Media:$1|$1]]''' d' 'o [$4 $3, $2].",
        "filedelete-comment": "Mutivo:",
        "filedelete-submit": "Scancèlla",
        "unusedtemplates": "Template ca nun se song'ausate",
        "unusedtemplatestext": "Sta paggena alenca tutt' 'e paggene int'a 'o namespace {{ns:template}} ca nun se songo nzertàte dint'a n'ata paggena.\nArricuòrdete 'e cuntrullà l'ati cullegamiente a 'e template apprimm' 'e scancellà.",
        "unusedtemplateswlh": "ati cullegamiente",
-       "randompage": "Na paggena qualsiase",
+       "randompage": "Na paggena qualonca",
        "randompage-nopages": "Nun gè song paggene {{PLURAL:$2|dint'ô seguente namespace|dint'ê seguenti namespace}}: $1.",
        "randomincategory": "Paggena a uocchio dint' 'a categurìa",
        "randomincategory-invalidcategory": "\"$1\" nun è nu nomme 'e categurìa bbuono.",
        "statistics-pages-desc": "Tutt' 'e paggene dint'a wiki, mettenno 'e chiacchieriate, redirezionamiente, ecc.",
        "statistics-files": "File carrecate",
        "statistics-edits": "Cagnamiente d' 'e paggene 'a che {{SITENAME}} s'è accumminciata",
-       "statistics-edits-average": "Cagnamiente medie pe' paggena",
+       "statistics-edits-average": "Cagni medie pe paggena",
        "statistics-users": "Utente riggistrate",
        "statistics-users-active": "Utente attive",
        "statistics-users-active-desc": "Utente c'hanno fatto coccosa dint' 'a {{PLURAL:$1|l'urdemo juorno|l'urdeme $1 juorne}}",
        "move": "Mòve",
        "movethispage": "Mòve sta paggena",
        "unusedimagestext": "'E file ccà abbascio esisteno, ma nun songo appennute dint' 'a nisciuna paggena.\nPe' piacere vedite ca n'ati site ncopp' 'a ll'Internet putessero cullegà cu nu file direttamente cu l'URL, picciò vedite ca putessero stà dint'a sta lista ancora tenenno nu cullegamiento diretto.",
-       "unusedcategoriestext": "'E categurìe ccà abbascio esisteno, ancora ch' 'e categurìe o l'ati paggene nun l'aùsano.",
+       "unusedcategoriestext": "'E categurìe ccà abbascio esisteno, simbè ca nun nce stanne categurìe o ati paggene ca l'aùsano.",
        "notargettitle": "Nisciuna destinazione",
        "notargettext": "Nun avete specificato na paggena o n'utente 'e destinazione pe' putè fa sta operazione.",
        "nopagetitle": "Nisciuna paggena 'e destinazione",
        "exbeforeblank": "'O cuntenuto apprimm' 'a ll'arrevacamento era: '$1'",
        "delete-confirm": "Scancella \"$1\"",
        "delete-legend": "Scancella",
-       "historywarning": "'''Attenzione:''' 'A paggena ca state pe' scancellà tene na cronologgia cu $1 {{PLURAL:$1|verzione|verziune}}:",
+       "historywarning": "'''Attenzione:''' 'A paggena ca state pe scancellà tene na cronologgia cu $1 {{PLURAL:$1|verzione|verziune}}:",
        "historyaction-submit": "Faje vedé",
-       "confirmdeletetext": "Vedite bbuono, vedite ca state a scancellà na paggena nziem' 'a tutt' 'a cronologgia.\nPe' piacere cunfermate si overo vulite fà cchesto, ca ve site fatto/a capace 'e l'effette 'e st'azione e ca chest'azione rispetta 'e [[{{MediaWiki:Policy-url}}|reole 'e scancellamiento]].",
+       "confirmdeletetext": "Vedite bbuono, vedite ca state a scancellà na paggena nziem' 'a tutt' 'a cronologgia soia.\nPe piacere cunfermate si overo vulite fà cchesto, ca ve site fatto/a capace 'e l'effette 'e st'azione e ca chest'azione rispetta 'e [[{{MediaWiki:Policy-url}}|reole 'e scancellamiento]].",
        "actioncomplete": "Azzione fernuta",
        "actionfailed": "Aziona sfalluta",
        "deletedtext": "Qauccheruno ha scancellata 'a paggena \"$1\".  Addumannà 'o $2 pe na lista d\"e ppaggene scancellate urdemamente.",
        "delete-toobig": "Sta paggena tene na storia 'e cagnamiente troppo longa, ncopp'a $1 {{PLURAL:$1|verzione|verziune}}.\n'O scancellamiento 'e chiste paggene è stato ristretto pe nce 'e putè astipà si ce sta cocche probblema dint' 'o database 'e {{SITENAME}}.",
        "delete-warning-toobig": "Sta paggena tene na cronologgia troppo longa, ncopp'a $1 {{PLURAL:$1|verzione|verziune}}.\nScancellannole se putesse crià troppo burdello ncopp' 'e operaziune 'e database dint'a {{SITENAME}};\niate cuoncio cuoncio.",
        "deleteprotected": "Nun putite scancellà sta paggena pecché è stata prutetta.",
-       "deleting-backlinks-warning": "<strong>Attenzione:</strong>\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|ati paggene]] cunteneno cullegamiente o paggene appennute â n'ata paggena ca state pe' scancellà.",
+       "deleting-backlinks-warning": "<strong>Attenzione:</strong>\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|ati paggene]] cunteneno cullegamiente o paggene appennute â n'ata paggena ca state pe scancellà.",
        "deleting-subpages-warning": "<strong>Accuorto:</strong> 'A paggena ca staie pe scancellà tene  [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|na sottopaggena|$1 sottopaggene|51=cchiù 'e 50 sottopaggene}}]].",
        "rollback": "Ausa na revizione 'e primma",
        "rollback-confirmation-yes": "Sfàjere",
        "revertpage-nouser": "Annullate 'e cagnamiente 'e n'utente annascunnuto, è stata ripigliata ll'urdema verzione 'e {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Cagnamiente annullate 'a {{GENDER:$3|$1}};\ns'è turnato arreto a l'urdema verzione 'e {{GENDER:$4|$2}}.",
        "sessionfailure-title": "Sessione fallita",
-       "sessionfailure": "Pare ca stanno probbleme cu 'a sessiona toja;\nst'azione è stata fermata pe' precauzione annanz' 'e cavall' 'e troia;\nPe' piacere turnate arreto, carrecate n'ata vota 'a paggena pe pruvate n'ata vota.",
+       "sessionfailure": "Pare ca stanno probbleme cu 'a sessiona toja;\nst'azione è stata fremmata pe precauzione annanz' 'e cavall' 'e troia;\nPe piacere mannate n'ata vota 'o modulo.",
        "changecontentmodel": "Cagna 'o mudello 'e cuntenute 'e na paggena",
        "changecontentmodel-legend": "Cagna 'o mudello 'e cuntenute",
        "changecontentmodel-title-label": "Titulo d\"a paggena",
        "tags-create-warnings-above": "{{PLURAL:$2|Chist'avviso s'è truvato|Chist'avvise se so' truvate}} pe' tramente ca se steva a crià 'o tag \"$1\":",
        "tags-create-warnings-below": "Vulite cuntinuà a crià 'o tag?",
        "tags-delete-title": "Scancella tag",
-       "tags-delete-explanation-initial": "State pe' scancellà 'o tag \"$1\" d' 'o database.",
+       "tags-delete-explanation-initial": "State pe scancellà 'o tag \"$1\" d' 'o database.",
        "tags-delete-explanation-in-use": "Sarrà luvato d' 'o {{PLURAL:$2|$2 verziona o d' 'o riggistro|tutt' 'e verziune $2 e/o 'e nutarelle int' 'o riggistro}} addò stesse azzeccato.",
        "tags-delete-explanation-warning": "St'aziona è <strong>irreversibbele</strong> e <strong>nun se pò turnà arreto</strong>, pure 'a ll'ammenistrature d' 'o database. Faciteve capace ca stu tag è chillu ca vulite scancellà.",
        "tags-delete-explanation-active": "<strong>'O tag \"$1\" è ancora attivo, e sarrà apprecato int' 'o futuro.</strong> Pe' fernì cu st'attività, jate, a lloco addò 'o tag s'è apprecato, e stutate llànno.",
        "tags-delete-not-found": "'O tag $1 nun esiste.",
        "tags-delete-too-many-uses": "'O tag \"$1\" è apprecato a cchiù 'e $2 {{PLURAL:$2|verziona|verziune}}, cosa ca vulesse dicere ca nun se ò scancellà.",
        "tags-delete-warnings-after-delete": "'O tag \"$1\" s'è scancellato, ma {{PLURAL:$2|s'è ncuntrato ll'avviso|se songhe ncuntrate ll'avise}} ccà:",
-       "tags-delete-no-permission": "Nun tenite 'o permesso pe' scancellà 'e tag 'e cagnamiente.",
+       "tags-delete-no-permission": "Nun tenite 'o permesso 'e scancellà 'e ttag 'e cagnamiento.",
        "tags-activate-title": "Appiccia 'o tag",
        "tags-activate-question": "Vuje state p'appiccià 'o tag \"$1\".",
        "tags-activate-reason": "Mutivo:",
index 4c125e5..1019520 100644 (file)
@@ -72,7 +72,7 @@
        "monday": "måndag",
        "tuesday": "dinsdag",
        "wednesday": "woonsdag",
-       "thursday": "dunderdag",
+       "thursday": "dunnerdag",
        "friday": "vrydag",
        "saturday": "såterdag",
        "sun": "sün",
@@ -95,7 +95,7 @@
        "november": "november",
        "december": "december",
        "january-gen": "jannewaori",
-       "february-gen": "febrewaori",
+       "february-gen": "febrri",
        "march-gen": "meert",
        "april-gen": "april",
        "may-gen": "mei",
        "subcategories": "Subkategorieën",
        "category-media-header": "Media in kategorie \"$1\"",
        "category-empty": "''In disse kategoria staon op t moment nog gien artikels of media.''",
-       "hidden-categories": "Verbörgen {{PLURAL:$1|kategorie|kategorieën}}",
+       "hidden-categories": "Verbörgen {{PLURAL:$1|kategory|kategoryen}}",
        "hidden-category-category": "Verbörgen kategorieën",
        "category-subcat-count": "{{PLURAL:$2|Disse kategorie hef de volgende subkategorie.|Disse kategorie hef de volgende {{PLURAL:$1|subkategorie|$1 subkategorieën}}, van in totaal $2.}}",
        "category-subcat-count-limited": "Disse kategorie hef de volgende {{PLURAL:$1|subkategorie|$1 subkategorieën}}.",
        "and": "&#32;en",
        "faq": "Vragen die vake esteld wörden",
        "actions": "Haandeling",
-       "namespaces": "Naamrüümdes",
+       "namespaces": "Naamruumdes",
        "variants": "Varianten",
-       "navigation-heading": "Navigasiemenu",
+       "navigation-heading": "Navigatymenu",
        "errorpagetitle": "Foutmelding",
        "returnto": "Weerumme naor $1.",
        "tagline": "Van {{SITENAME}}",
        "go": "Artikel",
        "searcharticle": "Artikel",
        "history": "Geschiedenisse",
-       "history_short": "Geschiedenisse",
+       "history_short": "Geskydenisse",
        "updatedmarker": "bie-ewörken sinds mien leste bezeuk",
-       "printableversion": "Afdrukbåre versy",
+       "printableversion": "Afdrükbåre versy",
        "permalink": "Vaste verwysing",
        "print": "Aofdrokken",
        "view": "Leasen",
        "specialpage": "Speciale syde",
        "personaltools": "Persoonlike instellingen",
        "talk": "Oaverleg",
-       "views": "Weergaven",
+       "views": "Weadergåven",
        "toolbox": "Hülpmiddels",
        "tool-link-userrights": "{{GENDER:$1|Gebrukersgruppen}} wysigen",
        "tool-link-emailuser": "Disse {{GENDER:$1|gebruker}} een bericht stüren",
        "categorypage": "Kategoriezied bekieken",
        "viewtalkpage": "Bekiek overlegzied",
        "otherlanguages": "Andere språken",
-       "redirectedfrom": "(deurestuurd vanaof \"$1\")",
+       "redirectedfrom": "(döärstüürd vanaf \"$1\")",
        "redirectpagesub": "Deurverwieszied",
        "redirectto": "Deurverwiezen naor:",
-       "lastmodifiedat": "Disse syde is et lätst ewysigd up $1 üm $2.",
+       "lastmodifiedat": "Disse syde is et lätst wysigd up $1 üm $2.",
        "viewcount": "Disse zied is $1 {{PLURAL:$1|keer|keer}} bekeken.",
        "protectedpage": "Beveiligden zied",
-       "jumpto": "Gå når:",
+       "jumpto": "Gå nå:",
        "jumptonavigation": "navigaty",
-       "jumptosearch": "zeuk",
+       "jumptosearch": "söök",
        "view-pool-error": "De servers bin op heden overbelast.\nTe veule gebrukers proberen disse zied te bekieken.\nWacht effen veurda'j opniej toegang proberen te kriegen tot disse zied.\n\n$1",
        "generic-pool-error": "De servers bin op heden overbelast.\nTe veule gebrukers proberen disse zied te bekieken.\nWacht effen veurda'j opniej toegang proberen te kriegen tot disse zied.",
        "pool-timeout": "De maximumwachttied veur databankvergrendeling is verleupen.",
        "editold": "bewark",
        "viewsourceold": "brontekste bekyken",
        "editlink": "bewark",
-       "viewsourcelink": "brontekste bekyken",
+       "viewsourcelink": "brontekst bekyken",
        "editsectionhint": "Bewarkingsveld: $1",
        "toc": "Inhold",
        "showtoc": "Bekieken",
        "nohistory": "Der bin gien eerdere versies van disse zied.",
        "currentrev": "Leste versie",
        "currentrev-asof": "Leste versie van $1",
-       "revisionasof": "Versie op $1",
+       "revisionasof": "Versy up $1",
        "revision-info": "Versie op $1 van {{GENDER:$6|$2}}$7",
-       "previousrevision": "&larr; eerdere versie",
+       "previousrevision": "&larr; eyrere versy",
        "nextrevision": "niejere versie &rarr;",
        "currentrevisionlink": "versie zo as t noen is",
        "cur": "noen",
        "lineno": "Regel $1:",
        "compareselectedversions": "Vergeliek de ekeuzen versies",
        "showhideselectedversions": "Ekeuzen versies bekieken/verbargen",
-       "editundo": "weerummedreien",
+       "editundo": "weaderümmedraien",
        "diff-empty": "(Gien verschil)",
        "diff-multi-sameuser": "({{PLURAL:$1|n Tussenliggende versie|$1 tussenliggende versies}} deur de zelfde gebruker is verbörgen)",
        "diff-multi-manyusers": "($1 tussenliggende {{PLURAL:$1|versie|versies}} deur meer as $2 {{PLURAL:$2|gebruker|gebrukers}} niet weeregeven)",
        "difference-missing-revision": "{{PLURAL:$2|Eén versie|$2 versies}} van disse verschillen ($1) {{PLURAL:$2|is|bin}} niet evunnen.\n\nDit kömp meestentieds deur t volgen van n verouwerde verwiezing naor n zied die vortedaon is.\nWaorschienlik ku'j der meer gegevens over vienen in t [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} vortdologboek].",
-       "searchresults": "Zeukresultaoten",
-       "searchresults-title": "Zeukresultaoten veur \"$1\"",
+       "searchresults": "Söökresultaten",
+       "searchresults-title": "Söökresultaten vöär \"$1\"",
        "titlematches": "Overeenkomst mit t onderwarp",
        "textmatches": "Overeenkomst mit teksten",
        "notextmatches": "Gien overeenstemming",
        "nextn": "volgende {{PLURAL:$1|$1}}",
        "prevn-title": "{{PLURAL:$1|Veurig resultaot|Veurige $1 resultaoten}}",
        "nextn-title": "{{PLURAL:$1|Volgend resultaot|Volgende $1 resultaoten}}",
-       "shown-title": "Laot $1 {{PLURAL:$1|resultaot|resultaoten}} per zied zien",
+       "shown-title": "Låt $1 {{PLURAL:$1|resultaat|resultaten}} per syde seen",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''Der is n zied mit de naam \"[[:$1]]\" op disse wiki.'''",
        "searchmenu-new": "<strong>De zied \"[[:$1]]\" op disse wiki anmaken!</strong> \n{{PLURAL:$2|0=|Zie oek de zied mit joew zeukresultaoten.|Zie oek de lieste mit evunnen zeukresultaoten.}}",
        "searchprofile-articles": "Artikels",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Alles",
-       "searchprofile-advanced": "Uutgebreid",
-       "searchprofile-articles-tooltip": "Zeuken in $1",
-       "searchprofile-images-tooltip": "Zeuken naor bestaanden",
-       "searchprofile-everything-tooltip": "Alle inhoud deurzeuken (oek overlegziejen)",
-       "searchprofile-advanced-tooltip": "Zeuken in de an-egeven naamruumtes",
+       "searchprofile-advanced": "Uutwyded",
+       "searchprofile-articles-tooltip": "ken in $1",
+       "searchprofile-images-tooltip": "Söken nå bestanden",
+       "searchprofile-everything-tooltip": "Alle inhold döärsöken (ouk oaverlegsyden)",
+       "searchprofile-advanced-tooltip": "Söken in de angeaven naamruumden",
        "search-result-size": "$1 ({{PLURAL:$2|1 woord|$2 woorden}})",
        "search-result-category-size": "{{PLURAL:$1|1 kategorielid|$1 kategorielejen}} ({{PLURAL:$2|1 onderkategorie|$2 onderkategorieën}}, {{PLURAL:$3|1 bestaand|$3 bestaanden}})",
        "search-redirect": "(deurverwiezing vanaof $1)",
        "right-siteadmin": "De databanke blokkeren en weer vriegeven",
        "right-override-export-depth": "Ziejen exporteren, oek de ziejen waor naor verwezen wördt, tot n diepte van 5",
        "right-sendemail": "Bericht versturen naor aandere gebrukers",
-       "newuserlogpage": "Logboek mit anwas",
+       "newuserlogpage": "Logbook van nye brukers",
        "newuserlogpagetext": "Hieronder staon de niej in-eschreven gebrukers",
        "rightslog": "Gebrukersrechtenlogboek",
        "rightslogtext": "Dit is n logboek mit veraanderingen van gebrukersrechten",
        "recentchanges-summary": "Up disse syde kün jy de lätste wysigingen van disse wiki bekyken.",
        "recentchanges-noresult": "Der waren in disse periode gien wiezigingen die an de kriteria voldoon.",
        "recentchanges-feed-description": "Zeuk naor de alderleste wiezingen op disse wiki in disse voer.",
-       "recentchanges-label-newpage": "Mid disse bewarking is een nye syde emaked",
+       "recentchanges-label-newpage": "Mid disse bewarking is een nye syde maked",
        "recentchanges-label-minor": "Dit is een kleine wysiging",
-       "recentchanges-label-bot": "Disse bewarking is üütevoord döär een bot",
-       "recentchanges-label-unpatrolled": "Disse bewarking is noch neet nå-ekeaken",
-       "recentchanges-label-plusminus": "Disse sydgroutte is mid dit antal bytes ewysigd",
+       "recentchanges-label-bot": "Disse bewarking is uutvoord döär een bot",
+       "recentchanges-label-unpatrolled": "Disse bewarking is noch neet nåkeaken",
+       "recentchanges-label-plusminus": "Disse sydgroutte is mid dit antal bytes wysigd",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}}<br />(see ouk de [[Special:NewPages|lyste mid nye syden]])",
        "recentchanges-submit": "Bekiek",
        "rcfilters-filter-user-experience-level-experienced-label": "Ervåren gebrukers",
        "rcfilters-filter-user-experience-level-experienced-description": "An-emelde bewarkers mid meyr as 500 bewarkingen en 30 dagen van aktiviteit.",
        "rcfilters-filter-bots-label": "Bot",
-       "rcfilters-filter-humans-label": "Meanskelik (geen bot)",
+       "rcfilters-filter-humans-label": "Menskelik (geen bot)",
        "rcfilters-filter-humans-description": "Bewarkingen döär meanskelike bewarkers.",
        "rcfilters-filtergroup-reviewstatus": "Beoordelingsstaotus",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Niet nao-ekeken",
        "recentchanges-page-removed-from-category": "[[:$1]] is vortedaon uut kategorie",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] vortedaon uut kategorie, [[Special:WhatLinksHere/$1|disse zied zit in aandere ziejen in-esleuten]]",
        "autochange-username": "Automatiese wieziging van MediaWiki",
-       "upload": "Holder upladen",
+       "upload": "Bestand upladen",
        "uploadbtn": "Holder upladen",
        "reuploaddesc": "Weerumme naor de opstuurzied",
        "upload-tryagain": "Bestaandsbeschrieving biewarken",
        "listfiles-latestversion": "Aktuele versie",
        "listfiles-latestversion-yes": "Ja",
        "listfiles-latestversion-no": "Nee",
-       "file-anchor-link": "Bestaand",
-       "filehist": "Bestaandsgeschiedenisse",
-       "filehist-help": "Klik op n daotum/tied um t bestaand te zien zo as t to was.",
+       "file-anchor-link": "Bestand",
+       "filehist": "Bestandsgeskydenisse",
+       "filehist-help": "Klik up een dåtum/tyd üm et bestand te seen so as et destyds was.",
        "filehist-deleteall": "alles vortdoon",
        "filehist-deleteone": "disse vortdoon",
        "filehist-revert": "weerummedreien",
-       "filehist-current": "zo as t noen is",
-       "filehist-datetime": "Daotum/tied",
-       "filehist-thumb": "Miniatuuraofbeelding",
-       "filehist-thumbtext": "Miniatuuraofbeelding veur versie van $1",
+       "filehist-current": "aktueel",
+       "filehist-datetime": "Dåtum/tyd",
+       "filehist-thumb": "Miniatuurafbealding",
+       "filehist-thumbtext": "Miniatuurafbealding vöär versy van $1",
        "filehist-nothumb": "Gien miniatuuraofbeelding",
-       "filehist-user": "Gebruker",
-       "filehist-dimensions": "Grootte",
+       "filehist-user": "Bruker",
+       "filehist-dimensions": "Groutde",
        "filehist-filesize": "Bestaandsgrootte",
-       "filehist-comment": "Opmarkingen",
-       "imagelinks": "Bestaandsgebruuk",
+       "filehist-comment": "Kommentaar",
+       "imagelinks": "Bestandsbruuk",
        "linkstoimage": "Disse holder wördt up de volgende {{PLURAL:$1|syde|$1 syden}} gebrüked:",
        "linkstoimage-more": "Der {{PLURAL:$2|is|bin}} meer as $1 {{PLURAL:$1|verwiezing|verwiezingen}} naor dit bestaand.\nDe volgende lieste gif allinnig de eerste {{PLURAL:$1|verwiezing|$1 verwiezingen}} naor dit bestaand weer.\nDe [[Special:WhatLinksHere/$2|hele lieste]] is oek beschikbaor.",
        "nolinkstoimage": "Geen enkelde syde gebrüükt disse holder.",
        "duplicatesoffile": "{{PLURAL:$1|t Volgende bestaand is|De volgende $1 bestaanden bin}} gelieke an dit bestaand ([[Special:FileDuplicateSearch/$2|meer informasie]]):",
        "sharedupload": "Dit is n edeeld bestaand op $1 en ku'j oek gebruken veur aandere projekten.",
        "sharedupload-desc-there": "Dit is n edeeld bestaand op $1 en ku'j oek gebruken veur aandere projekten. Bekiek de [$2 beschrieving van t bestaand] veur meer informasie.",
-       "sharedupload-desc-here": "Dit is n edeeld bestaand op $1 en ku'j oek gebruken veur aandere projekten. De [$2 beschrieving van t bestaand] dergindse, steet hieronder.",
+       "sharedupload-desc-here": "Dit bestand kümt van $1 en kan ouk in andere projekten bruked weasen. De [$2 syde mid de beskryving van et bestand] steyt hyrunder.",
        "sharedupload-desc-edit": "Dit besatand kömp van $1 en kan oek in aandere projekten gebruukt wörden.\nJe kunnen de [$2 zied mit de bestaandsbeschrieving] daor bewarken.",
        "sharedupload-desc-create": "Dit besatand kömp van $1 en kan oek in aandere projekten gebruukt wörden.\nJe kunnen de [$2 zied mit de bestaandsbeschrieving] daor bewarken.",
        "filepage-nofile": "Der besteet gien bestaand mit disse naam.",
        "allpagesto": "Laot ziejen zien tot:",
        "allarticles": "Alle artikels",
        "allinnamespace": "Alle ziejen (naamruumte $1)",
-       "allpagessubmit": "Zeuk",
+       "allpagessubmit": "Söken",
        "allpagesprefix": "Ziejen bekieken die beginnen mit:",
        "allpagesbadtitle": "De op-egeven ziednaam is ongeldig of der steet n interwikiveurvoegsel in. Meugelikerwieze staon der karakters in de naam die niet gebruukt maggen wörden in ziednamen.",
        "allpages-bad-ns": "{{SITENAME}} hef gien \"$1\"-naamruumte.",
        "delete-warning-toobig": "Disse zied hef n lange bewarkingsgeschiedenisse, meer as $1 {{PLURAL:$1|versie|versies}}.\nWoart je: t vortdoon van disse zied kan de warking van de databanke van {{SITENAME}} versteuren.\nWees veurzichtig",
        "deleting-backlinks-warning": "<strong>Waorschuwing:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|aandere ziejen]] gebruken of verwiezen naor de zied die'j vortdoon willen.",
        "rollback": "Wiezigingen herstellen",
-       "rollbacklink": "weerummedreien",
+       "rollbacklink": "weaderümmedraien",
        "rollbacklinkcount": "{{PLURAL:$1|één bewarking|$1 bewarkingen}} weerummedreien",
        "rollbacklinkcount-morethan": "Meer as {{PLURAL:$1|één bewarking|$1 bewarkingen}} weerummedreien",
        "rollbackfailed": "Wieziging herstellen is mislokt",
        "undelete-error-long": "Fouten bie t herstellen van t bestaand:\n\n$1",
        "undelete-show-file-confirm": "Bi'j der wisse van da'j n vortedaone versie van t bestaand \"<nowiki>$1</nowiki>\" van $2 um $3 bekieken willen?",
        "undelete-show-file-submit": "Ja",
-       "namespace": "Naamrüümde:",
-       "invert": "Seleksie ummekeren",
+       "namespace": "Naamruumde:",
+       "invert": "Selekty ümmekeyren",
        "tooltip-invert": "Vink dit vakjen an um wiezigingen an ziejen binnen de ekeuzen naamruumte te verbargen (en de biebeheurende naamruumte as dat an-evinkt is)",
        "namespace_association": "Naamruumte die hieran ekoppeld is",
        "tooltip-namespace_association": "Vink dit vakjen an um oek de overlegnaamruumte, of in t ummekeren geval de naamruumte zelf, derbie te doon die bie disse naamruumte heurt.",
-       "blanknamespace": "(Höyvdnaamrüümde)",
+       "blanknamespace": "(Höyvdnaamruumde)",
        "contributions": "{{GENDER:$1|Gebrukersbydragen}}",
        "contributions-title": "Biedragen van $1",
        "mycontris": "Myn bydragen",
        "allmessages-prefix": "Filtreer op veurvoegsel:",
        "allmessages-language": "Taal:",
        "allmessages-filter-submit": "zeuk",
-       "thumbnail-more": "vergroten",
+       "thumbnail-more": "vergrouten",
        "filemissing": "Bestaand ontbrik",
        "thumbnail_error": "Fout bie t laojen van de miniatuuraofbeelding: $1",
        "thumbnail_error_remote": "Foutmelding van $1:\n$2",
        "tooltip-pt-preferences": "{{GENDER:|Miene}} vuurkeuren",
        "tooltip-pt-watchlist": "Lieste van zieden die op miene volglieste stoan",
        "tooltip-pt-mycontris": "Oaverzicht van {{GENDER:|oew}} biejdreagen",
-       "tooltip-pt-login": "Y wördt van harte uutnöygd üm u an te melden as gebruker, mär et is nich verplicht",
+       "tooltip-pt-login": "Jy wördt van harte uutnöygd üm ju an te melden as bruker, mar et is neet verplicht",
        "tooltip-pt-logout": "Ofmaelden",
        "tooltip-pt-createaccount": "Skryv juw eigen vöäral in en meld juw eigen an. Dit is lykewels neet verplicht.",
        "tooltip-ca-talk": "Låt een oaverlegtekst oaver disse syde seen",
-       "tooltip-ca-edit": "Beweark disse syde",
+       "tooltip-ca-edit": "Bewark disse syde",
        "tooltip-ca-addsection": "Niej oonderwaerp tovogen",
        "tooltip-ca-viewsource": "Disse ziede is beveiligd taegen veraanderen. Iej könt wal kieken noar de ziede",
        "tooltip-ca-history": "Oldere versys van disse syde",
        "tooltip-ca-delete": "Smiet disse ziede vort",
        "tooltip-ca-undelete": "Haal n inhoald van disse ziede oet n emmer",
        "tooltip-ca-move": "Gef disse ziede nen aanderen titel",
-       "tooltip-ca-watch": "Voog disse ziede to an oewe volglieste",
+       "tooltip-ca-watch": "Voog disse syde to an juw volglyste",
        "tooltip-ca-unwatch": "Smiet disse ziede van oewe voalglieste",
        "tooltip-search": "{{SITENAME}} döärsöken",
-       "tooltip-search-go": "Når een syde mid disse name gån as et besteyt",
-       "tooltip-search-fulltext": "Söök når syden wår disse tekst in steyt",
-       "tooltip-p-logo": "Gå når et vöärblad",
-       "tooltip-n-mainpage": "Goa noar t vuurblad",
-       "tooltip-n-mainpage-description": "Gå når et vöärblad",
-       "tooltip-n-portal": "Informaty oaver et projekt: wel, wat, ho en wårümme",
-       "tooltip-n-currentevents": "Achtergrundinformaty oaver dinge in et nys",
+       "tooltip-search-go": "Gå nå een syde mid eksakt disse name as et besteyt",
+       "tooltip-search-fulltext": "Söök nå syden wår disse tekst in steyt",
+       "tooltip-p-logo": "Gå nå et vöärblad",
+       "tooltip-n-mainpage": "Gå nå et vöärblad",
+       "tooltip-n-mainpage-description": "Gå nå et vöärblad",
+       "tooltip-n-portal": "Informaty oaver et projekt: wee, wat, ho en wårümme",
+       "tooltip-n-currentevents": "Achtergrundinformaty oaver dingen in et nys",
        "tooltip-n-recentchanges": "Lyste van pas verrichte veranderingen",
        "tooltip-n-randompage": "Låt ne willeköärige syde seen",
        "tooltip-n-help": "Hülpinformaty oaver {{SITENAME}}",
        "tooltip-t-whatlinkshere": "Lyste van alle syden dee når disse syde verwysen",
-       "tooltip-t-recentchangeslinked": "Pas verrichte veranderingen dee når disse syde verwysen",
+       "tooltip-t-recentchangeslinked": "Pas verrichte veranderingen dee nå disse syde verwyset",
        "tooltip-feed-rss": "RSS-voer vuur disse ziede",
        "tooltip-feed-atom": "Atom-voer vuur disse ziede",
        "tooltip-t-contributions": "Lieste met biejdreagen van {{GENDER:$1|disse gebroeker}}",
        "tooltip-t-emailuser": "Stüür disse {{GENDER:$1|gebruker}} een netpostbericht",
        "tooltip-t-info": "Meer informasie over disse zied",
        "tooltip-t-upload": "Laad afbealdingen en/of gelüüdsmateriaal",
-       "tooltip-t-specialpages": "Lieste van alle biejzeundere zieden",
+       "tooltip-t-specialpages": "Lyste van alle bysündere syden",
        "tooltip-t-print": "De afdrükbåre versy van disse syde",
-       "tooltip-t-permalink": "Permanente verwysing når disse versy van de syde",
+       "tooltip-t-permalink": "Permanente verwysing nå disse versy van de syde",
        "tooltip-ca-nstab-main": "Låt een tekst van et artikel seen",
        "tooltip-ca-nstab-user": "Loat de gebroekersbladziede zeen",
        "tooltip-ca-nstab-media": "Loat n mediatekst zeen",
-       "tooltip-ca-nstab-special": "Dit is ne biejzeundere ziede die'j nich könt veraanderen",
+       "tooltip-ca-nstab-special": "Dit is een bysündere syde dee jy neet veranderen künt",
        "tooltip-ca-nstab-project": "Loat de projektbladziede zeen",
-       "tooltip-ca-nstab-image": "Loat de bestaandsbladziede zeen",
+       "tooltip-ca-nstab-image": "Låt de bestandssyde seen",
        "tooltip-ca-nstab-mediawiki": "Loat de systeemtekstbladziede zeen",
        "tooltip-ca-nstab-template": "Loat de malbladziede zeen",
        "tooltip-ca-nstab-help": "Loat de hölpbladziede zeen",
        "tooltip-watchlistedit-raw-submit": "Volglieste biewarken",
        "tooltip-recreate": "Disse ziede opniej anmaken, ondanks t feit dat t vortdoan is.",
        "tooltip-upload": "Bestaanden opsturen",
-       "tooltip-rollback": "Mit \"weerummedreien\" kö'j mit één klik de bewaerking(en) van n leste gebroeker dee disse ziede bewaerkt hef terugdraeien.",
+       "tooltip-rollback": "\"Weaderümmedraien\" drait mid eyn klik de bewarking(en) van de lätste bruker up disse syde terügge.",
        "tooltip-undo": "A'j op \"weerummedreien\" klikken geet t bewaerkingsvaenster lös en kö'j ne vurige versie terugzetten.\nIej könt in de bewaerkingssamenvatting n reden opgeven.",
        "tooltip-preferences-save": "Vuurkeuren opsloan",
        "tooltip-summary": "Voer ne korte samenvatting in",
        "show-big-image": "Oorspronkelik bestaand",
        "show-big-image-preview": "Grootte van disse weergave: $1.",
        "show-big-image-other": "Aandere {{PLURAL:$2|resolusie|resolusies}}: $1.",
-       "show-big-image-size": "$1 × $2 beeldpunten",
+       "show-big-image-size": "$1 × $2 bealdpunten",
        "file-info-gif-looped": "herhaolend",
        "file-info-gif-frames": "$1 {{PLURAL:$1|beeld|beelden}}",
        "file-info-png-looped": "herhaolend",
        "metadata-help": "In dit bestaand zit metadata mit EXIF-informasie, die deur n fotokamera, inleesapparaot of fotobewarkingsprogramma op-estuurd kan ween.",
        "metadata-expand": "Bekiek uutebreiden gegevens",
        "metadata-collapse": "Verbarg uutebreiden gegevens",
-       "metadata-fields": "De aofbeeldingsmetadatavelden in dit bericht staon oek op n aofbeeldingszied as de metadatatabel in-eklapt is.\nAandere velden wörden verbörgen.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
+       "metadata-fields": "De afbealdingsmetadatavelden in dit bericht ståt ouk up een afbealdingssyde as de metadatatabel inklapped is.\nAndere velden wördet verbörgen.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "namespacesall": "alles",
        "monthsall": "alles",
        "confirmemail": "Bevestig netpostadres",
        "logentry-patrol-patrol": "$1 hef versie $4 van de zied $3 op {{GENDER:$2|nao-ekeken}} ezet",
        "logentry-patrol-patrol-auto": "$1 hef versie $4 van de zied $3 automaties op {{GENDER:$2|nao-ekeken}} ezet",
        "logentry-newusers-newusers": "Gebruker $1 is {{GENDER:$2|an-emaakt}}",
-       "logentry-newusers-create": "Gebruker $1 is {{GENDER:$2|an-emaakt}}",
+       "logentry-newusers-create": "Brukerskonto $1 is {{GENDER:$2|anmaked}}",
        "logentry-newusers-create2": "Gebruker $3 is {{GENDER:$2|an-emaakt}} an-emaakt deur $1",
        "logentry-newusers-byemail": "Gebruker $3 {{GENDER:$2|is}} an-emaakt deur $1 en t wachtwoord is per netpost verstuurd",
        "logentry-newusers-autocreate": "De gebruker $1 is automaties {{GENDER:$2|an-emaakt}}",
index 023f5f7..a2e6203 100644 (file)
        "invalidtitle": "अमान्य शीर्षक",
        "invalidtitle-knownnamespace": "नेमस्पेस \"$2\" तथा अक्षर \"$3\" सहितको अवैश शिर्षक",
        "invalidtitle-unknownnamespace": "अज्ञात नेमस्पेस अंक $1 तथा अक्षर \"$2\" भएको अवैध शिर्षक",
-       "exception-nologin": "प्रवेश (लग ईन) नगरिएको",
+       "exception-nologin": "प्रवेश नगरिएको",
        "exception-nologin-text": "यस पृष्ठमा जान वा कुनै कार्य गर्नको लागि कृपया प्रवेश (लग इन) गर्नुहोस् ।",
        "exception-nologin-text-manual": "यस पृष्ठमा प्रवेश गर्न वा कुनै कार्य गर्नको लागि कृपया $1 गर्नु होस् ।",
        "virus-badscanner": "खराव मिलान: अज्ञात भाइरस स्क्यानर :''$1''",
        "nav-login-createaccount": "प्रवेश गर्ने/नयाँ खाता बनाउने",
        "logout": "निर्गमन",
        "userlogout": "निर्गमन (लग आउट)",
-       "notloggedin": "प्रवेश (लग ईन) नगरिएको",
+       "notloggedin": "प्रवेश नगरिएको",
        "userlogin-noaccount": "के खाता छैन ?",
        "userlogin-joinproject": "{{SITENAME}} मा खाता खोल्नुहोस् ।",
        "createaccount": "खाता खोल्नुहोस्",
        "loginlanguagelabel": "भाषा: $1",
        "suspicious-userlogout": "तपाईंको निर्गमन अनुरोध अस्विकार गरिन्छ किन कि यो खराब ब्राउजर वा क्यासिङ प्रोक्सिले पठाएको जस्तो देखिन्छ।",
        "createacct-another-realname-tip": "वास्तविक नाम ऐच्छिक हो ।\nतपाईंले यो खुलाउनु भएको खण्डमा तपाईंको काममा प्रयोगकर्ता श्रेय दिनको लागि यसको प्रयोग गरिने छ ।",
-       "pt-login": "प्रवेश (लग ईन)",
+       "pt-login": "प्रवेश",
        "pt-login-button": "प्रवेश",
        "pt-login-continue-button": "प्रवेस जारी राख्नुहोस् ।",
        "pt-createaccount": "खाता खोल्नुहोस्",
        "nosuchsectiontitle": "सेक्सन फेला परेन",
        "nosuchsectiontext": "तपाईँले त्यस्तो खण्डको सम्पादन गर्ने प्रयास गर्नुभयो जुन छैन।\nजब तपाईं यस पृष्ठलाई हेर्नुहुँदैथियो, यो सारिएको अथवा मेटाइएको हुनुपर्छ।",
        "loginreqtitle": "प्रवेशगर्नु जरुरी छ।",
-       "loginreqlink": "प्रवेश (लग ईन)",
+       "loginreqlink": "प्रवेश",
        "loginreqpagetext": "अरु पृष्ठ हेर्न तपाईंले $1 गर्नुपर्छ ।",
        "accmailtitle": "पासवर्ड पठाइयो",
        "accmailtext": "जथाभावीरूपमा सृजना गरिएको प्रवेशशब्द प्रयोगकर्ता [[User talk:$1|$1]] को  $2 मा पठाइएको छ।\n\nयो नयाँ खाताको प्रवेशशब्द  ''[[Special:ChangePassword|change password]]'' मा प्रवेश गरेर परिवर्तन गर्न सकिन्छ ।",
        "prefs-pageswatchlist": "हेरिएका पृष्ठहरू",
        "prefs-tokenwatchlist": "टोकन",
        "prefs-diffs": "diffs(भिन्नता)",
-       "prefs-help-prefershttps": "यà¥\8b à¤\85भिरà¥\82à¤\9aà¥\80 à¤¤à¤ªà¤¾à¤\88à¤\82à¤\95à¥\8b à¤\85रà¥\8dà¤\95à¥\8b à¤ªà¥\8dरवà¥\87श (लà¤\97 à¤\87न) à¤¬à¤¾à¤\9f à¤²à¤¾à¤\97à¥\81 à¤¹à¥\81नà¥\87à¤\9b ।",
+       "prefs-help-prefershttps": "यà¥\8b à¤\85भिरà¥\82à¤\9aà¥\80 à¤¤à¤ªà¤¾à¤\88à¤\95à¥\8b à¤\85रà¥\8dà¤\95à¥\8b à¤ªà¥\8dरवà¥\87शबाà¤\9f à¤²à¤¾à¤\97à¥\82 à¤¹à¥\81नà¥\87à¤\9b।",
        "prefswarning-warning": "तपाईंले आफ्नो अभिरूचीमा गर्नुभएको परिवर्तन अहिले सम्म सङ्ग्रह गरिएको छैन। यदि तपाईं \"$1\" मा क्लिक नगरी यस पृष्ठबाट बाहिर जानुभयो भने तपाईंको अभिरूची अपडेट गर्न सकिदैन।",
        "prefs-tabs-navigation-hint": "सुझाव: तपाईं ट्याबसहरूमा ट्याबसको बीच आवागमन गर्नका लागि देब्रे वा दाहिने तीर साँचोको प्रयोग गर्न सक्नुहुन्छ।",
        "userrights": "प्रयोगकर्ता अधिकारहरू",
        "uploadbtn": "फाइलहरू उर्ध्वभरण गर्ने",
        "reuploaddesc": "उर्ध्वभरण रद्द गर्ने र उर्ध्वभरण फारमतिर जाने",
        "upload-tryagain": "संशोधित फाइल विवरण बुझाउने",
-       "uploadnologin": "प्रवेश (लग ईन) नगरिएको",
+       "uploadnologin": "प्रवेश नगरिएको",
        "uploadnologintext": "फाइल उर्ध्वभरण गर्न तपाईंले $1 गर्नुपर्छ।",
        "upload_directory_missing": "उर्ध्वभरण डाइरेक्टरी ($1) हराइरहेको छ र वेवसर्भरले नयाँ डाइरेक्टरी निर्माणगर्न असमर्थ भयो ।",
        "upload_directory_read_only": "उर्ध्व भरण डाइरेक्टरी ($1) वेवसर्भर द्वारा लेख्य छैन ।",
        "watchlistfor2": "$1को $2",
        "nowatchlist": "तपाईंको अवलोकन सूचीमा कुनै पनि सामाग्री छैन।",
        "watchlistanontext": "कृपया तपाईंको निगरानी सूची हेर्न या सम्पादन गर्न लगइन गर्नुहोस्।",
-       "watchnologin": "प्रवेश (लग ईन) नगरिएको",
+       "watchnologin": "प्रवेश नगरिएको",
        "addwatch": "निगरानी सुचीमा थप्ने",
        "addedwatchtext": "\"[[:$1]]\" पृष्ठ [[Special:Watchlist|अवलोकनसूची]]मा थपियो\nयो पृष्ठ र यससित सम्बद्ध वार्तालाप पृष्ठमा भविष्यमा हुने परिवर्तन सूचिबद्ध गरिनेछ।",
        "addedwatchtext-short": "\"$1\" पृष्ठ तपाईंको अवलोकन सूचीमा थप भएको छ ।",
index 4578b9c..bc13353 100644 (file)
        "powersearch-togglelabel": "ߝߛߍ߬ߝߛߍ߬ߟߌ",
        "powersearch-toggleall": "ߊ߬ ߓߍ߯",
        "powersearch-togglenone": "ߝߏߦߌ߬",
+       "powersearch-remember": "ߢߌߣߌ߲ߠߌ߲ ߣߊ߬ߕߐ ߓߊߕߐߡߐ߲ߠߌ߲ ߠߎ߬ ߟߊߓߊ߬ߕߏ߬.",
        "search-external": "ߞߐߞߊ߲߫ ߢߌߣߌ߲ߠߌ߲",
+       "searchdisabled": "{{SITENAME}} ߢߌߣߌ߲ߠߌ߲ ߓߘߊ߫ ߓߴߊ߬ ߟߊ߫. \nߌ ߘߌ߫ ߛߋ߫ ߢߌߣߌ߲ߠߌ߲ ߞߍ߫ ߟߊ߫ ߜ߭ߎߜ߭ߑߟߎ ߞߊ߲߬ ߥߛߎ߬ߣߍ߲߬ ߞߘߐ߫.\nߕߎ߬ߡߊ߬ߘߐ߫ ߊ߬ߟߎ߬ ߟߊ߫ ߛߌߝߊߟߌ ߞߊ߬ ߟߐ߬ {{SITENAME}} ߡߊ߬߸ ߏ߬ ߞߣߐߘߐ ߟߋ߬ ߕߍ߫ ߕߎ߬ߡߊ߬ߘߊ ߟߊ߫.",
        "search-error": "ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߓߘߴߊ߬ ߞߎ߲߬ߓߐ߫ ߞߵߌ ߕߏ߫ $1 ߢߌߣߌ߲ ߠߊ߫",
        "search-warning": "ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ ߘߏ߫ ߓߘߴߊ߬ ߞߎ߲߬ߓߐ߫ ߞߵߌ ߕߏ߫ $1 ߢߌߣߌ߲ ߠߊ߫",
        "preferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
        "prefs-email": "ߢߎߡߍߙߋ߲ ߞߏ߲ߘߏ ߛߎߥߊ߲ߘߟߌ",
        "prefs-rendering": "ߟߊ߲ߞߣߍߡߊ",
        "saveprefs": "ߊ߬ ߟߊߞߎ߲߬ߘߎ߬",
+       "restoreprefs": "ߘߊ߲ߛߎ߲ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߓߍ߯ ߟߊߞߎߣߎ߲߫ (ߕߍߕߎ߲߮ ߓߍ߯ ߘߐ߫)",
        "prefs-editing": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߴߌ ߘߐ߫",
        "searchresultshead": "ߢߌߣߌ߲ߠߌ߲",
        "stub-threshold-sample-link": "ߣߐ߰ߡߊ߲",
index d765808..bf58c17 100644 (file)
        "sessionfailure": "Parece haver um problema com sua sessão de login;\nEsta ação foi cancelada como uma precaução contra o seqüestro de sessão.\nPor favor, reenvie o formulário.",
        "changecontentmodel": "Alterar o modelo de conteúdo de uma página",
        "changecontentmodel-legend": "Alterar o modelo de conteúdo",
-       "changecontentmodel-title-label": "Título da página",
+       "changecontentmodel-title-label": "Título da página:",
        "changecontentmodel-current-label": "Modelo de conteúdo atual:",
-       "changecontentmodel-model-label": "Modelo de conteúdo novo",
+       "changecontentmodel-model-label": "Modelo de conteúdo novo:",
        "changecontentmodel-reason-label": "Motivo:",
        "changecontentmodel-submit": "Mudar",
        "changecontentmodel-success-title": "O modelo de conteúdo foi alterado",
index 03c1da4..50b0658 100644 (file)
        "backend-fail-contenttype": "Used as fatal error message. Parameters:\n* $1 - a storage (file) path\n{{Related|Backend-fail}}",
        "backend-fail-batchsize": "Error message when the limit of operations to be done at once in the file backend was reached.\nParameters:\n* $1 - the number of operations attempted at once in this case\n* $2 - the maximum number of operations that can be attempted at once\nBoth parameters are PLURAL supported\n\nA \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.\n{{Related|Backend-fail}}",
        "backend-fail-usable": "Parameters:\n* $1 - the file name, including the path, formatted for the storage backend used\n{{Related|Backend-fail}}",
+       "backend-fail-stat": "Parameters:\n* $1 - the file name, including the path, formatted for the storage backend used\n{{Related|Backend-fail}}",
+       "backend-fail-hash": "Parameters:\n* $1 - the file name, including the path, formatted for the storage backend used\n{{Related|Backend-fail}}",
        "filejournal-fail-dbconnect": "Parameters:\n* $1 is the name of the \"[[:wikipedia:Front and back ends|backend]]\" that the file journal logs changes for.",
        "filejournal-fail-dbquery": "Parameters:\n* $1 is the name of the \"[[:wikipedia:Front and back ends|backend]]\" that the file journal logs changes for.",
        "lockmanager-notlocked": "Parameters:\n* $1 is a resource path (e.g. \"mwstore://media-public/a/ab/file.jpg\").",
index e7653e1..9b297b8 100644 (file)
        "tog-hideminor": "Скрывать малые изменения из списка свежих правок",
        "tog-hidepatrolled": "Скрывать патрулированные правки в списке свежих правок",
        "tog-newpageshidepatrolled": "Скрывать отпатрулированные страницы в списке новых страниц",
-       "tog-hidecategorization": "СкÑ\80Ñ\8bваÑ\82Ñ\8c ÐºÐ°Ñ\82егоÑ\80изаÑ\86иÑ\8e Ñ\81Ñ\82Ñ\80аниÑ\86",
+       "tog-hidecategorization": "СкÑ\80Ñ\8bваÑ\82Ñ\8c Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ðµ Ñ\81оÑ\81Ñ\82ава Ð¾Ñ\82Ñ\81леживаемÑ\8bÑ\85 ÐºÐ°Ñ\82егоÑ\80ий",
        "tog-extendwatchlist": "Расширить список наблюдения, включая все изменения, а не только последние",
        "tog-usenewrc": "Группировать изменения в свежих правках и списке наблюдения",
        "tog-numberheadings": "Автоматически нумеровать заголовки",
        "tog-watchlistunwatchlinks": "Добавить прямые маркеры для включения/исключения из списка наблюдения ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) для наблюдаемых страниц с изменениями (для переключения функций требуется JavaScript)",
        "tog-watchlisthideanons": "Скрывать правки анонимных участников из списка наблюдения",
        "tog-watchlisthidepatrolled": "Скрывать отпатрулированные правки из списка наблюдения",
-       "tog-watchlisthidecategorization": "СкÑ\80Ñ\8bваÑ\82Ñ\8c ÐºÐ°Ñ\82егоÑ\80изаÑ\86иÑ\8e Ñ\81Ñ\82Ñ\80аниÑ\86",
+       "tog-watchlisthidecategorization": "СкÑ\80Ñ\8bваÑ\82Ñ\8c Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ðµ Ñ\81оÑ\81Ñ\82ава Ð¾Ñ\82Ñ\81леживаемÑ\8bÑ\85 ÐºÐ°Ñ\82егоÑ\80ий",
        "tog-ccmeonemails": "Отправлять мне копии писем, которые я посылаю другим участникам",
        "tog-diffonly": "Не показывать содержание страницы под сравнением двух версий",
        "tog-showhiddencats": "Показывать скрытые категории",
index ca4c1d8..4c95a58 100644 (file)
        "nocreate-loggedin": "Немате дозволу да правите нове странице.",
        "sectioneditnotsupported-title": "Уређивање одељка није подржано",
        "sectioneditnotsupported-text": "Уређивање одељка није подржано на овој страници.",
+       "modeleditnotsupported-title": "Уређивање није подржано",
+       "modeleditnotsupported-text": "Уређивање није подржано за модел садржаја $1.",
        "permissionserrors": "Грешка у дозволи",
        "permissionserrorstext": "Немате дозволу за ову радњу из {{PLURAL:$1|следећег|следећих}} разлога:",
        "permissionserrorstext-withaction": "Немате дозволу да $2 из {{PLURAL:$1|следећег|следећих}} разлога:",
        "content-model-json": "ЈСОН-а",
        "content-json-empty-object": "Празан објекат",
        "content-json-empty-array": "Празан низ",
+       "unsupported-content-model": "<strong>Упозорење:</strong> Модел садржаја $1 није подржан на овом викију.",
        "deprecated-self-close-category": "Странице које користе невалидне самозатварајуће HTML тагове",
        "duplicate-args-warning": "<strong>Упозорење:</strong> [[:$1]] позива [[:$2]] са више од једне вредности за параметар „$3“. Само последња наведена вредност ће бити коришћена.",
        "duplicate-args-category": "Странице с дуплираним аргументима код позива шаблона",
        "rcfilters-clear-all-filters": "Обришите све филтере",
        "rcfilters-show-new-changes": "Нове промене од $1",
        "rcfilters-search-placeholder": "Филтрирајте промене (користите мени или претражите име филтера)",
+       "rcfilters-search-placeholder-mobile": "Филтери",
        "rcfilters-invalid-filter": "Неважећи филтер",
        "rcfilters-empty-filter": "Нема активних филтера. Сви доприноси су приказани.",
        "rcfilters-filterlist-title": "Филтери",
        "rcfilters-filter-showlinkedto-label": "Прикажи промене на страницама ка којима воде везе",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Странице ка којима воде везе са</strong> изабране странице",
        "rcfilters-target-page-placeholder": "Унесите име странице (или категорије)",
+       "rcfilters-allcontents-label": "Сви садржаји",
+       "rcfilters-alldiscussions-label": "Све дискусије",
        "rcnotefrom": "Испод {{PLURAL:$5|је промена|су промене}} од <strong>$3, $4</strong> (до <strong>$1</strong> приказано).",
        "rclistfromreset": "Ресетуј избор датума",
        "rclistfrom": "Прикажи нове промене почев од $2, $3",
        "sessionfailure": "Изгледа да постоји проблем с вашом сесијом;\nова радња је отказана да би се избегла злоупотреба.\nМолимо, поново пошаљите образац.",
        "changecontentmodel": "Промена модела садржаја странице",
        "changecontentmodel-legend": "Промени модел садржаја",
-       "changecontentmodel-title-label": "Наслов странице",
-       "changecontentmodel-model-label": "Нови модел садржаја",
+       "changecontentmodel-title-label": "Наслов странице:",
+       "changecontentmodel-current-label": "Тренутни модел садржаја:",
+       "changecontentmodel-model-label": "Нови модел садржаја:",
        "changecontentmodel-reason-label": "Разлог:",
        "changecontentmodel-submit": "Промени",
        "changecontentmodel-success-title": "Модел садржаја је промењен",
        "move-subpages": "Премести и подстранице (до $1)",
        "move-talk-subpages": "Премести подстранице странице за разговор (до $1)",
        "movepage-page-exists": "Страница $1 већ постоји и не може се заменити.",
+       "movepage-source-doesnt-exist": "Страница $1 не постоји и не може бити премештена.",
        "movepage-page-moved": "Страница $1 је премештена на $2.",
        "movepage-page-unmoved": "Страница $1 не може да се премести на $2.",
        "movepage-max-pages": "Највише $1 {{PLURAL:$1|страница је премештена|странице су премештене|страница је премештено}} и више не може да буде аутоматски премештено.",
        "delete_and_move_reason": "Избрисано да се ослободи место за премештање из „[[$1]]“",
        "selfmove": "Наслов је истоветан;\nне можете преместити страницу преко саме себе.",
        "immobile-source-namespace": "Не могу преместити странице у именски простор „$1“.",
+       "immobile-source-namespace-iw": "Странице на осталим викијима не могу бити премештене са овог викија.",
        "immobile-target-namespace": "Не могу преместити странице у именски простор „$1“.",
        "immobile-target-namespace-iw": "Међувики веза није важеће одредиште за премештање странице.",
        "immobile-source-page": "Ова страница се не може преместити.",
        "immobile-target-page": "Премештање није могуће на одредишни наслов.",
+       "movepage-invalid-target-title": "Тражено име није ваљано.",
        "bad-target-model": "Жељено одредиште користи други модел садржаја. Није могуће конвертовати из $1 у садржај $2.",
        "imagenocrossnamespace": "Датотека се не може преместити у именски простор који не припада датотекама.",
        "nonfile-cannot-move-to-file": "Не-датотеке не можете преместити у именски простор за датотеке",
        "permanentlink": "Трајна веза",
        "permanentlink-revid": "ID измене",
        "permanentlink-submit": "Пређи на измену",
+       "newsection": "Нови одељак",
+       "newsection-page": "Одредишна страница",
+       "newsection-submit": "Иди на страницу",
        "dberr-problems": "Дошло је до техничких проблема.",
        "dberr-again": "Сачекајте неколико минута и поново учитајте страницу.",
        "dberr-info": "(Не могу приступити бази података: $1)",
index 65274bf..19dea12 100644 (file)
@@ -53,7 +53,7 @@
        "tog-watchlisthideown": "Schow moje pomjyńańa we artiklach, na kere dowom pozůr",
        "tog-watchlisthidebots": "Schow pomjyńańa sprowjone bez boty we artiklach, na kere dowom pozůr",
        "tog-watchlisthideminor": "Schow ńywjelge pomjyńańa w artiklach, na kere dowom pozůr",
-       "tog-watchlisthideliu": "Schow sprowjyńo zalůgowanych sprowjaczy na pozorliśće",
+       "tog-watchlisthideliu": "Skryj edycyje ôd zalogowanych używŏczōw we ôbserwowanych",
        "tog-watchlisthideanons": "Schow sprowjyńa anůńimowych sprowjoczy na liśće artikli, na kere dowom pozůr",
        "tog-watchlisthidepatrolled": "Schowej sprowdzůne sprowjyńa na pozorliśće",
        "tog-ccmeonemails": "Przesyłej mi kopje e-brifůw co żech je posłoł inkszym sprowjaczom",
        "portal-url": "Project:Portal społeczności",
        "privacy": "Prawidła chrōniyniŏ prywatności",
        "privacypage": "Project:Prawidła chrōniyniŏ prywatności",
-       "badaccess": "Felerne uprawńyńo",
+       "badaccess": "Felerne uprawniynia",
        "badaccess-group0": "Ńy mosz uprawńyń coby wykůnać ta uoperacyjo.",
        "badaccess-groups": "Ta uoperacyjo mogům wykůnać ino użytkownicy ze keryjś z {{PLURAL:$2|grupy|grup}}: $1.",
        "versionrequired": "Wymagano MediaWiki we wersyji $1",
        "toc": "Wykŏz treści",
        "showtoc": "uobejrzij",
        "hidetoc": "schrůń",
-       "collapsible-collapse": "Zwjyń",
-       "collapsible-expand": "Rozwjyń",
+       "collapsible-collapse": "Skryj",
+       "collapsible-expand": "Pokŏż",
        "thisisdeleted": "Pokoż/wćepej nazod $1",
        "viewdeleted": "Uobejrzij $1",
        "restorelink": "{{PLURAL:$1|jedna wyćepano wersyjo|$1 wyćepane wersyje|$1 wyćepanych wersyjůw}}",
        "parser-template-recursion-depth-warning": "Przekroczůno limit głymbokośći rekurencyji mustru ($1)",
        "undo-success": "Sprowjyńy zostoło wycofane. Prosza pomjarkować ukozane půniżyj dyferencyje mjyndzy wersyjůma, coby zweryfikować jejich poprawność, potym zaś naszkryflać pomjyńańo coby zakończyć uoperacyjo.",
        "undo-failure": "Ta edycyjŏ niy może być cŏfniyntŏ skuli kōnfliktu ze wersyjami postrzednimi.",
-       "undo-norev": "Sprowjyńo ńy idźe cofnůńć skuli tego, co ńy istńije abo uostoło wyćepane.",
+       "undo-norev": "Edycyje niy idzie cŏfnōńć, bo ôna niy istniyje abo była wyciepniyntŏ.",
        "undo-summary": "Wycůfańy wersyji $1 naszkryflanej bez [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]])",
        "cantcreateaccount-text": "Tworzyńy kůnta s tygo adresu IP ('''$1''') uostoło zawarte bez użytkowńika [[User:$3|$3]].\n\nSkuli: ''$2''",
        "viewpagelogs": "Ôbejzdrz regesty dlŏ tyj strōny",
        "mergehistory-box": "Skupluj gyszichta sprowjyń dwůch zajtůw:",
        "mergehistory-from": "Zdrzůdłowo zajta:",
        "mergehistory-into": "Zajta docelowo:",
-       "mergehistory-list": "Gyszichta půmjyńań do śe skuplować",
+       "mergehistory-list": "Historyjõ edycyji idzie scalić",
        "mergehistory-merge": "Nastympujůnce půmjyńyńo zajty [[:$1]] idźe scalić s [[:$2]]. Uoznocz we kolůmńy kropkům kero zmjana, wroz ze wcześńijszymi, mo być scalůno. Użyće linkůw uod nawigacyji kasuje wybůr we kolůmńy.",
        "mergehistory-go": "Pokoż půmjyńańo kere idźe scalić",
        "mergehistory-submit": "Scal historyjo půmjyńań",
        "mergehistory-empty": "Ńy mo historyje zmjan do scalyńo.",
        "mergehistory-done": "$3 {{PLURAL:$3|pomjyńańe|pomjyńańa|pomjyńań}} we $1 ze sukcesym uostało scalonych ze [[:$2]].",
-       "mergehistory-fail": "Ńy idźe scalić historyje půmjyńań. Zmjyń sztalowańo parametrůw tyj uoperacyji.",
+       "mergehistory-fail": "Niy idzie scalić historyji. Wejzdrzij na parametry strōny i czasu.",
        "mergehistory-no-source": "Ńy ma sam zajty zdrzůdłowyj $1.",
        "mergehistory-no-destination": "Ńy ma sam zajty docelowyj $1.",
        "mergehistory-invalid-source": "Zajta zdrzůdłowo muśi mjeć poprawne mjano.",
        "mergehistory-reason": "Kůmyntorz:",
        "mergelog": "Regest scalyń",
        "revertmerge": "Uodkupluj",
-       "mergelogpagetext": "Půńiżyj je lista uostatńich kuplowań historyji půmjyńań zajtůw.",
+       "mergelogpagetext": "Niżyj je wykŏz ôstatnich scalyń jednyj historyje strōny ze inkszōm.",
        "history-title": "Historyjŏ wersyji strōny „$1”",
        "difference-title": "$1: Porōwnanie wersyji",
        "difference-multipage": "(Porůwnańy zajt)",
        "right-noratelimit": "Ńy mo uograńičyń přepustowośći",
        "right-import": "Import zajtůw s inkšych Wiki",
        "right-importupload": "Import zajtůw ze wćepanygo plika",
-       "right-patrol": "Uoznocz sprowjyńo kej przezdrzane",
+       "right-patrol": "Ôznŏcz edycyje za przejzdrzane",
        "right-autopatrol": "Naštaluj na autůmatyčne uoznačańy sprowjyń kej přezdřane",
        "right-patrolmarks": "Podglůnd značnikůw patrolowańo pomjeńanych na uostatku – uoznačańo kej „sprawdzůne”",
        "right-unwatchedpages": "Pokož lista zajtůw na kere žodyn ńy dowo pozoru",
        "right-mergehistory": "Pouůnč historyjo sprowjyń do zajtůw",
-       "right-userrights": "Sprowjej wšyjske uprawńyńo užytkowńikůw",
-       "right-userrights-interwiki": "Sprowjej uprawńyńo užytkowńikůw na zajtach inkšych Wiki",
+       "right-userrights": "Edytuj uprawniynia wszyjskich używŏczōw",
+       "right-userrights-interwiki": "Edytuj uprawniynia używŏczōw na inkszych wiki",
        "right-siteadmin": "Zawjerańy i uodmykańy bazy danych",
        "newuserlogpage": "Ksiōnżka nowych używŏczōw",
        "newuserlogpagetext": "To je rejer uostatńo utworzůnych kůnt użytkowńikůw",
        "action-createpage": "tworzyńo zajtůw",
        "action-createtalk": "tworzyńo zajtůw godki",
        "action-createaccount": "stworzynie tego kōnta używŏcza",
-       "action-minoredit": "do uoznačyńo tygo sprowjyńo kej drobne půmjyńańe",
+       "action-minoredit": "ôznaczynie tyj edycyje za małõ",
        "action-move": "přećepańe tyj zajty",
        "action-move-subpages": "přećepańo tyj zajty uoroz s jeij podzajtůma",
        "action-move-rootuserpages": "Překludzańy zajtůw uod užytkowńikůw (nale bes jeich podzajtůw)",
        "action-suppressrevision": "podglůndu a wćepańo nazod tyj wersyje schrůńůnyj",
        "action-suppressionlog": "podglůndu rejera schrůńańo",
        "action-block": "zawarća uod sprowjyń tygo spowjořa",
-       "action-protect": "půmjyńań poźůmu zawarćo tyj zajty",
+       "action-protect": "zmiany poziōmōw zabezpieczyń na tyj strōnie",
        "action-import": "importu tyj zajty s inkšyj wiki",
        "action-importupload": "importu tyj zajty bez wćepańe plika",
-       "action-patrol": "označyńo sprowjyńo kej „sprowdzůne”",
-       "action-autopatrol": "uoznačyńo wuasnygo sprowjyńo kej „sprawdzonygo”",
+       "action-patrol": "ôznaczynie edycyje za sprawdzōnõ",
+       "action-autopatrol": "ôznaczynie włŏsnyj edycyje za przejzdrzanõ",
        "action-unwatchedpages": "podglůndu listy zajtůw na kere ńikt ńy dowo pozoru",
        "action-mergehistory": "skuplowańo historyje sprowjyń tyj zajty",
        "action-userrights": "sprowjańo uprowńyń wszyjstkich sprowjorzy",
        "recentchanges-legend": "Ôpcyje ôstatnich zmian",
        "recentchanges-summary": "Na tyj strōnie idzie śledzić ôstatnie zmiany na wiki.",
        "recentchanges-noresult": "Żŏdne zmiany we podanym ôkresie niy pasujōm tym kryteriōm.",
-       "recentchanges-feed-description": "Dowej pozůr na půmjyÅ\84ane na uostatku na tyj wiki.",
+       "recentchanges-feed-description": "Dowej pozÅ\8dr na Ã´statnie zmiany na tyj wiki.",
        "recentchanges-label-newpage": "Ta edycyjŏ stworziła nowõ strōnã",
        "recentchanges-label-minor": "To je małŏ zmiana",
        "recentchanges-label-bot": "To je zmiana zrobiōnŏ ôd bota",
        "recentchanges-label-plusminus": "Strōna zmiyniyła srogość ô tela bajtōw",
        "recentchanges-legend-heading": "<strong>Legynda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ôbejzdrzij tyż [[Special:NewPages|listã nowych strōn]])",
+       "rcfilters-other-review-tools": "Inksze nŏrzyńdzia kōntrole",
+       "rcfilters-filter-humans-label": "Czowiek (niy bot)",
+       "rcfilters-liveupdates-button": "Aktualizacyje na żywo",
+       "rcfilters-liveupdates-button-title-on": "Zastŏw aktualizacyje na żywo",
        "rcnotefrom": "Niżyj {{PLURAL:$5|je zmiana|sōm zmiany}} ôd <strong>$3, $4</strong> ({{PLURAL:$5|je pokŏzanŏ|sōm pokŏzane}} nojwyżyj <strong>$1</strong>).",
        "rclistfrom": "Pokŏż zmiany ôd $3 $2",
        "rcshowhideminor": "$1 małe zmiany",
        "listusers-submit": "Uobejrzij",
        "listusers-noresult": "Ńy znejdźůno žodnygo užytkowńika.",
        "activeusers-noresult": "Niy szło znŏjść żŏdnych używŏczōw",
-       "listgrouprights": "Uprawńyńo grup użytkowńikůw",
-       "listgrouprights-summary": "Půńiży znojdowo śe spis grup użytkowńikůw zdefińjowanych na tyj wiki, s wyszczygůlńyńym przidźelůnych im prow dostympu.\nSprowdź zajta [[{{MediaWiki:Listgrouprights-helppage}}|s dodatkowymi informacjami]] uo uprowńyńach użytkowńikůw.",
+       "listgrouprights": "Uprawniynia grup używŏczōw",
+       "listgrouprights-summary": "Niżyj widać wykŏz grup używŏczōw zdefiniowanych na tyj wiki, społym ze jejich prawami dostympu.\n[[{{MediaWiki:Listgrouprights-helppage}}|Ekstra informacyje]] ô uprawniyniach.",
        "listgrouprights-key": "* <span class=\"listgrouprights-granted\">Dane uprawńyńy</span>\n* <span class=\"listgrouprights-revoked\">Uodebrane uprawńyńy</span>",
        "listgrouprights-group": "Grupa",
-       "listgrouprights-rights": "Uprawńyńo",
+       "listgrouprights-rights": "Uprawniynia",
        "listgrouprights-helppage": "Help:Uprawńyńo grup użytkowńikůw",
        "listgrouprights-members": "(lista czōnkōw grupy)",
        "listgrouprights-addgroup": "Idźe dodać do {{PLURAL:$2|grupy|grup}}: $1",
        "rollback": "Wycofej sprowjyńe",
        "rollbacklink": "cŏfej",
        "rollbacklinkcount": "cŏfnij $1 {{PLURAL:$1|edycyjõ|edycyje|edycyji}}",
-       "rollbackfailed": "Ńy idźe wycofać sprowjyńo",
+       "rollbackfailed": "Niy szło wycŏfać zmiany",
        "cantrollback": "Ńy idże cofnůńć pomjyńyńo, sam je ino jedna wersyja tyi zajty.",
        "alreadyrolled": "Ńy idźe lů zajty [[:$1|$1]] cofnůńć uostatńygo pomjyńeńa, kere wykonoł [[User:$2|$2]] ([[User talk:$2|godka]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]).\nKto inkszy zdůnżůł już to zrobić abo wprowadźił własne poprowki do treśći zajty.\n\nAutorym ostatńygo pomjyńyńo je terozki [[User:$3|$3]] ([[User talk:$3|godka]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Sprowjyńe uopisano: <em>$1</em>.",
        "ipb-blocklist": "Zoboč istńijůnce zawarća",
        "ipb-blocklist-contribs": "Wkłod $1",
        "block-expiry": "Wygaso:",
-       "unblockip": "Uodymkńij sprowjyńo užytkowńikowi",
+       "unblockip": "Ôdblokuj używŏcza",
        "unblockiptext": "Ůžyj formulořa půńižej coby přiwrůćić možliwość sprowjańo s wčeśńij zawartygo adresu IP abo užytkowńikowi.",
        "ipusubmit": "Uodymkńij sprowjyńo užytkowńikowi",
        "unblocked": "[[User:$1|$1]] zostou uodymkńynty.",
        "creditspage": "Autořy",
        "nocredits": "Brak informacyji uo autorach tyi zajty.",
        "spamprotectiontitle": "Filter antyspamowy",
-       "spamprotectiontext": "Zajta, kero żeś průbowou naszkryflać, uostoła zawarta bez filter antyspamowy.\nNojprawdopodobńij zostoło to spowodowane bez link do zewnyntrznyj zajty internecowyj kero je na czornyj liśće.",
+       "spamprotectiontext": "Strōna, co jōm {{GENDER:prōbowołś|prōbowałaś|prōbujesz}} spamiyntać, ôstała zawartŏ ôd filtra antyspamowego.\nNojpewnij stało sie to skuli linka do zewnyntrznyj strōny, co je na czŏrnyj liście.",
        "spamprotectionmatch": "Filtr antyspamowy śe zouůnčůu s kuli tygo co znod tekst: $1",
        "spambot_username": "MediaWiki – wyćepywańe spamu",
        "spam_reverting": "Přiwracańy uostatńij wersyji we kerej ńy bůuo linkůw do $1",
index c4f175f..95a59d2 100644 (file)
@@ -115,19 +115,18 @@ SPARQLDI;
        }
 
        public function execute() {
-               global $wgRCMaxAge;
-
                $this->initialize();
                $startTS = new MWTimestamp( $this->getOption( "start" ) );
 
                $endTS = new MWTimestamp( $this->getOption( "end" ) );
                $now = new MWTimestamp();
+               $rcMaxAge = $this->getConfig()->get( 'RCMaxAge' );
 
-               if ( $now->getTimestamp() - $startTS->getTimestamp() > $wgRCMaxAge ) {
-                       $this->error( "Start timestamp too old, maximum RC age is $wgRCMaxAge!" );
+               if ( $now->getTimestamp() - $startTS->getTimestamp() > $rcMaxAge ) {
+                       $this->error( "Start timestamp too old, maximum RC age is $rcMaxAge!" );
                }
-               if ( $now->getTimestamp() - $endTS->getTimestamp() > $wgRCMaxAge ) {
-                       $this->error( "End timestamp too old, maximum RC age is $wgRCMaxAge!" );
+               if ( $now->getTimestamp() - $endTS->getTimestamp() > $rcMaxAge ) {
+                       $this->error( "End timestamp too old, maximum RC age is $rcMaxAge!" );
                }
 
                $this->startTS = $startTS->getTimestamp();
@@ -595,6 +594,8 @@ SPARQL;
                         * TODO: For now, we do full update even though some data hasn't changed,
                         * e.g. parents for parent cat and counts for child cat.
                         */
+                       $childPages = [];
+                       $parentCats = [];
                        foreach ( $batch as $row ) {
                                $childPages[$row->rc_cur_id] = true;
                                $parentCats[$row->rc_title] = true;
@@ -614,7 +615,7 @@ SPARQL;
                        $pages = [];
                        $deleteUrls = [];
 
-                       if ( !empty( $childPages ) ) {
+                       if ( $childPages ) {
                                // Load child rows by ID
                                $childRows = $dbr->select(
                                        [ 'page', 'page_props', 'category' ],
@@ -642,7 +643,7 @@ SPARQL;
                                }
                        }
 
-                       if ( !empty( $parentCats ) ) {
+                       if ( $parentCats ) {
                                // Load parent rows by title
                                $joinConditions = [
                                        'page' => [
index 3e8b754..1d588ec 100644 (file)
@@ -79,11 +79,12 @@ class CheckDependencies extends Maintenance {
        }
 
        private function loadThing( &$dependencies, $name, $extensions, $skins ) {
-               global $wgExtensionDirectory, $wgStyleDirectory;
+               $extDir = $this->getConfig()->get( 'ExtensionDirectory' );
+               $styleDir = $this->getConfig()->get( 'StyleDirectory' );
                $queue = [];
                $missing = false;
                foreach ( $extensions as $extension ) {
-                       $path = "$wgExtensionDirectory/$extension/extension.json";
+                       $path = "$extDir/$extension/extension.json";
                        if ( file_exists( $path ) ) {
                                // 1 is ignored
                                $queue[$path] = 1;
@@ -95,7 +96,7 @@ class CheckDependencies extends Maintenance {
                }
 
                foreach ( $skins as $skin ) {
-                       $path = "$wgStyleDirectory/$skin/skin.json";
+                       $path = "$styleDir/$skin/skin.json";
                        if ( file_exists( $path ) ) {
                                $queue[$path] = 1;
                                $this->addToDependencies( $dependencies, [], [ $skin ], $name );
index bed3956..720e3fd 100644 (file)
@@ -59,8 +59,6 @@ class CleanupPreferences extends Maintenance {
         *      all values are in that range. Drop ones that aren't.
         */
        public function execute() {
-               global $wgHiddenPrefs, $wgDefaultUserOptions;
-
                $dbw = $this->getDB( DB_MASTER );
                $hidden = $this->hasOption( 'hidden' );
                $unknown = $this->hasOption( 'unknown' );
@@ -73,10 +71,11 @@ class CleanupPreferences extends Maintenance {
 
                // Remove hidden prefs. Iterate over them to avoid the IN on a large table
                if ( $hidden ) {
-                       if ( !$wgHiddenPrefs ) {
+                       $hiddenPrefs = $this->getConfig()->get( 'HiddenPrefs' );
+                       if ( !$hiddenPrefs ) {
                                $this->output( "No hidden preferences, skipping\n" );
                        }
-                       foreach ( $wgHiddenPrefs as $hiddenPref ) {
+                       foreach ( $hiddenPrefs as $hiddenPref ) {
                                $this->deleteByWhere(
                                        $dbw,
                                        'Dropping hidden preferences',
@@ -87,9 +86,10 @@ class CleanupPreferences extends Maintenance {
 
                // Remove unknown preferences. Special-case 'userjs-' as we can't control those names.
                if ( $unknown ) {
+                       $defaultUserOptions = $this->getConfig()->get( 'DefaultUserOptions' );
                        $where = [
                                'up_property NOT' . $dbw->buildLike( 'userjs-', $dbw->anyString() ),
-                               'up_property NOT IN (' . $dbw->makeList( array_keys( $wgDefaultUserOptions ) ) . ')',
+                               'up_property NOT IN (' . $dbw->makeList( array_keys( $defaultUserOptions ) ) . ')',
                        ];
                        // Allow extensions to add to the where clause to prevent deletion of their own prefs.
                        Hooks::run( 'DeleteUnknownPreferences', [ &$where, $dbw ] );
index b0ac638..737e65f 100644 (file)
@@ -276,6 +276,11 @@ class ConvertExtensionToRegistration extends Maintenance {
                $this->json[$realName] = $value;
        }
 
+       /**
+        * @param string $realName
+        * @param array[] $value
+        * @suppress PhanTypeInvalidDimOffset
+        */
        protected function handleResourceModules( $realName, $value ) {
                $defaults = [];
                $remote = $this->hasOption( 'skin' ) ? 'remoteSkinPath' : 'remoteExtPath';
index 554e373..d861348 100644 (file)
@@ -38,8 +38,7 @@ class GetReplicaServer extends Maintenance {
        }
 
        public function execute() {
-               global $wgAllDBsAreLocalhost;
-               if ( $wgAllDBsAreLocalhost ) {
+               if ( $this->getConfig()->get( 'AllDBsAreLocalhost' ) ) {
                        $host = 'localhost';
                } elseif ( $this->hasOption( 'group' ) ) {
                        $db = $this->getDB( DB_REPLICA, $this->getOption( 'group' ) );
index aff6758..1b35a20 100644 (file)
@@ -51,9 +51,9 @@ class MigrateActors extends LoggedUpdateMaintenance {
        }
 
        protected function doDBUpdates() {
-               global $wgActorTableSchemaMigrationStage;
+               $actorTableSchemaMigrationStage = $this->getConfig()->get( 'ActorTableSchemaMigrationStage' );
 
-               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
+               if ( !( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
                        $this->output(
                                "...cannot update while \$wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_WRITE_NEW\n"
                        );
index bcf84aa..00d7c90 100644 (file)
@@ -97,6 +97,7 @@ class TextPassDumper extends BackupDumper {
        protected $firstPageWritten = false;
        protected $lastPageWritten = false;
        protected $checkpointJustWritten = false;
+       /** @var string[] */
        protected $checkpointFiles = [];
 
        /**
@@ -303,6 +304,7 @@ TEXT
                        $param = $split[1];
                }
                $fileURIs = explode( ';', $param );
+               $newFileURIs = [];
                foreach ( $fileURIs as $URI ) {
                        switch ( $val ) {
                                case "file":
index 71fff56..2271c39 100644 (file)
@@ -55,9 +55,8 @@ class MigrateArchiveText extends LoggedUpdateMaintenance {
        }
 
        protected function doDBUpdates() {
-               global $wgDefaultExternalStore;
-
                $replaceMissing = $this->hasOption( 'replace-missing' );
+               $defaultExternalStore = $this->getConfig()->get( 'DefaultExternalStore' );
                $batchSize = $this->getBatchSize();
 
                $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
@@ -96,7 +95,7 @@ class MigrateArchiveText extends LoggedUpdateMaintenance {
                                        if ( $data !== false ) {
                                                $flags = Revision::compressRevisionText( $data );
 
-                                               if ( $wgDefaultExternalStore ) {
+                                               if ( $defaultExternalStore ) {
                                                        $data = ExternalStore::insertToDefault( $data );
                                                        if ( $flags ) {
                                                                $flags .= ',';
index ea12e42..f9868a1 100644 (file)
@@ -102,8 +102,6 @@ class NamespaceDupes extends Maintenance {
         * @return bool
         */
        private function checkAll( $options ) {
-               global $wgNamespaceAliases, $wgCapitalLinks;
-
                $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                $spaces = [];
 
@@ -129,7 +127,7 @@ class NamespaceDupes extends Maintenance {
                                $spaces[$name] = $ns;
                        }
                }
-               foreach ( $wgNamespaceAliases as $name => $ns ) {
+               foreach ( $this->getConfig()->get( 'NamespaceAliases' ) as $name => $ns ) {
                        $spaces[$name] = $ns;
                }
                foreach ( $contLang->getNamespaceAliases() as $name => $ns ) {
@@ -138,6 +136,7 @@ class NamespaceDupes extends Maintenance {
 
                // We'll need to check for lowercase keys as well,
                // since we're doing case-sensitive searches in the db.
+               $capitalLinks = $this->getConfig()->get( 'CapitalLinks' );
                foreach ( $spaces as $name => $ns ) {
                        $moreNames = [];
                        $moreNames[] = $contLang->uc( $name );
@@ -146,7 +145,7 @@ class NamespaceDupes extends Maintenance {
                        $moreNames[] = $contLang->ucwords( $contLang->lc( $name ) );
                        $moreNames[] = $contLang->ucwordbreaks( $name );
                        $moreNames[] = $contLang->ucwordbreaks( $contLang->lc( $name ) );
-                       if ( !$wgCapitalLinks ) {
+                       if ( !$capitalLinks ) {
                                foreach ( $moreNames as $altName ) {
                                        $moreNames[] = $contLang->lcfirst( $altName );
                                }
index c85e194..84b962a 100644 (file)
@@ -122,7 +122,7 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                                $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
                                        $dbw->insert( 'revision', self::$dummyRev, $fname );
                                        $id = $dbw->insertId();
-                                       $toDelete[] = $id;
+                                       $toDelete = [ $id ];
 
                                        $maxId = max(
                                                (int)$dbw->selectField( 'archive', 'MAX(ar_rev_id)', [], $fname ),
index c84f3de..3325b05 100644 (file)
@@ -77,11 +77,12 @@ class PopulateContentTables extends Maintenance {
        }
 
        public function execute() {
-               global $wgMultiContentRevisionSchemaMigrationStage;
+               $multiContentRevisionSchemaMigrationStage =
+                       $this->getConfig()->get( 'MultiContentRevisionSchemaMigrationStage' );
 
                $t0 = microtime( true );
 
-               if ( ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) === 0 ) {
+               if ( ( $multiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) === 0 ) {
                        $this->writeln(
                                '...cannot update while \$wgMultiContentRevisionSchemaMigrationStage '
                                . 'does not have the SCHEMA_COMPAT_WRITE_NEW bit set.'
index 0de9d67..9f77fda 100644 (file)
@@ -109,10 +109,10 @@ class PopulateImageSha1 extends LoggedUpdateMaintenance {
                        // with the database write operation, because the writes are queued
                        // in the pipe buffer. This can improve performance by up to a
                        // factor of 2.
-                       global $wgDBuser, $wgDBserver, $wgDBpassword, $wgDBname;
-                       $cmd = 'mysql -u' . Shell::escape( $wgDBuser ) .
-                               ' -h' . Shell::escape( $wgDBserver ) .
-                               ' -p' . Shell::escape( $wgDBpassword, $wgDBname );
+                       $config = $this->getConfig();
+                       $cmd = 'mysql -u' . Shell::escape( $config->get( 'DBuser' ) ) .
+                               ' -h' . Shell::escape( $config->get( 'DBserver' ) ) .
+                               ' -p' . Shell::escape( $config->get( 'DBpassword' ), $config->get( 'DBname' ) );
                        $this->output( "Using pipe method\n" );
                        $pipe = popen( $cmd, 'w' );
                }
index 98025d1..54f1862 100644 (file)
@@ -76,7 +76,7 @@ class ReassignEdits extends Maintenance {
         * @return int Number of entries changed, or that would be changed
         */
        private function doReassignEdits( &$from, &$to, $rc = false, $report = false ) {
-               global $wgActorTableSchemaMigrationStage;
+               $actorTableSchemaMigrationStage = $this->getConfig()->get( 'ActorTableSchemaMigrationStage' );
 
                $dbw = $this->getDB( DB_MASTER );
                $this->beginTransaction( $dbw, __METHOD__ );
@@ -136,7 +136,7 @@ class ReassignEdits extends Maintenance {
                        if ( $total ) {
                                # Reassign edits
                                $this->output( "\nReassigning current edits..." );
-                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+                               if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                        $dbw->update(
                                                'revision',
                                                [
@@ -148,7 +148,7 @@ class ReassignEdits extends Maintenance {
                                                __METHOD__
                                        );
                                }
-                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                               if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                        $dbw->update(
                                                'revision_actor_temp',
                                                [ 'revactor_actor' => $to->getActorId( $dbw ) ],
@@ -189,16 +189,16 @@ class ReassignEdits extends Maintenance {
         * @return array
         */
        private function userSpecification( IDatabase $dbw, &$user, $idfield, $utfield, $acfield ) {
-               global $wgActorTableSchemaMigrationStage;
+               $actorTableSchemaMigrationStage = $this->getConfig()->get( 'ActorTableSchemaMigrationStage' );
 
                $ret = [];
-               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+               if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $ret += [
                                $idfield => $user->getId(),
                                $utfield => $user->getName(),
                        ];
                }
-               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+               if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $ret += [ $acfield => $user->getActorId( $dbw ) ];
                }
                return $ret;
index 88eaf67..2f8dcc4 100644 (file)
@@ -1,7 +1,5 @@
 <?php
 /**
- * Purge all languages from the message cache.
- *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
- * Maintenance script that purges all languages from the message cache.
+ * Maintenance script that purges cache used by MessageCache.
  *
  * @ingroup Maintenance
  */
 class RebuildMessages extends Maintenance {
        public function __construct() {
                parent::__construct();
-               $this->addDescription( 'Purge all language messages from the cache' );
+               $this->addDescription( 'Purge the MessageCache for all interface languages.' );
        }
 
        public function execute() {
-               global $wgLocalDatabases, $wgDBname, $wgEnableSidebarCache, $messageMemc;
-               if ( $wgLocalDatabases ) {
-                       $databases = $wgLocalDatabases;
-               } else {
-                       $databases = [ $wgDBname ];
-               }
-
-               foreach ( $databases as $db ) {
-                       $this->output( "Deleting message cache for {$db}... " );
-                       $messageMemc->delete( "{$db}:messages" );
-                       if ( $wgEnableSidebarCache ) {
-                               $messageMemc->delete( "{$db}:sidebar" );
-                       }
-                       $this->output( "Deleted\n" );
-               }
+               $this->output( "Purging message cache for all languages on this wiki... " );
+               $messageCache = MediaWikiServices::getInstance()->getMessageCache();
+               $messageCache->clear();
+               $this->output( "Done\n" );
        }
 }
 
index 6b2f488..16a7346 100644 (file)
@@ -39,7 +39,7 @@ class RemoveUnusedAccounts extends Maintenance {
        }
 
        public function execute() {
-               global $wgActorTableSchemaMigrationStage;
+               $actorTableSchemaMigrationStage = $this->getConfig()->get( 'ActorTableSchemaMigrationStage' );
 
                $this->output( "Remove unused accounts\n\n" );
 
@@ -48,7 +48,7 @@ class RemoveUnusedAccounts extends Maintenance {
                $delUser = [];
                $delActor = [];
                $dbr = $this->getDB( DB_REPLICA );
-               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+               if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $res = $dbr->select(
                                [ 'user', 'actor' ],
                                [ 'user_id', 'user_name', 'user_touched', 'actor_id' ],
@@ -94,7 +94,7 @@ class RemoveUnusedAccounts extends Maintenance {
                        $this->output( "\nDeleting unused accounts..." );
                        $dbw = $this->getDB( DB_MASTER );
                        $dbw->delete( 'user', [ 'user_id' => $delUser ], __METHOD__ );
-                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                       if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                # Keep actor rows referenced from ipblocks
                                $keep = $dbw->selectFieldValues(
                                        'ipblocks', 'ipb_by_actor', [ 'ipb_by_actor' => $delActor ], __METHOD__
@@ -110,11 +110,11 @@ class RemoveUnusedAccounts extends Maintenance {
                        $dbw->delete( 'user_groups', [ 'ug_user' => $delUser ], __METHOD__ );
                        $dbw->delete( 'user_former_groups', [ 'ufg_user' => $delUser ], __METHOD__ );
                        $dbw->delete( 'user_properties', [ 'up_user' => $delUser ], __METHOD__ );
-                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                       if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $dbw->delete( 'logging', [ 'log_actor' => $delActor ], __METHOD__ );
                                $dbw->delete( 'recentchanges', [ 'rc_actor' => $delActor ], __METHOD__ );
                        }
-                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+                       if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $dbw->delete( 'logging', [ 'log_user' => $delUser ], __METHOD__ );
                                $dbw->delete( 'recentchanges', [ 'rc_user' => $delUser ], __METHOD__ );
                        }
index 612c092..e7988fe 100644 (file)
@@ -175,9 +175,9 @@ class MwSql extends Maintenance {
                        return $this->sqlPrintResult( $res, $db );
                } catch ( DBQueryError $e ) {
                        if ( $dieOnError ) {
-                               $this->fatalError( $e );
+                               $this->fatalError( (string)$e );
                        } else {
-                               $this->error( $e );
+                               $this->error( (string)$e );
                        }
                }
                return null;
index beb1975..b6aa626 100644 (file)
@@ -223,6 +223,7 @@ class CompressOld extends Maintenance {
         * @param string $extdb
         * @param bool|int $maxPageId
         * @return bool
+        * @suppress PhanTypeInvalidDimOffset
         */
        private function compressWithConcat( $startId, $maxChunkSize, $beginDate,
                $endDate, $extdb = "", $maxPageId = false
index ebace75..9bcba7e 100644 (file)
@@ -44,10 +44,10 @@ class UpdateCollation extends Maintenance {
        public function __construct() {
                parent::__construct();
 
-               global $wgCategoryCollation;
+               $categoryCollation = $this->getConfig()->get( 'CategoryCollation' );
                $this->addDescription( <<<TEXT
 This script will find all rows in the categorylinks table whose collation is
-out-of-date (cl_collation != '$wgCategoryCollation') and repopulate cl_sortkey
+out-of-date (cl_collation != '$categoryCollation') and repopulate cl_sortkey
 using the page title and cl_sortkey_prefix.  If all collations are
 up-to-date, it will do nothing.
 TEXT
@@ -70,8 +70,6 @@ TEXT
        }
 
        public function execute() {
-               global $wgCategoryCollation;
-
                $dbw = $this->getDB( DB_MASTER );
                $dbr = $this->getDB( DB_REPLICA );
                $force = $this->getOption( 'force' );
@@ -81,7 +79,7 @@ TEXT
                        $collationName = $this->getOption( 'target-collation' );
                        $collation = Collation::factory( $collationName );
                } else {
-                       $collationName = $wgCategoryCollation;
+                       $collationName = $this->getConfig()->get( 'CategoryCollation' );
                        $collation = Collation::singleton();
                }
 
@@ -104,9 +102,8 @@ TEXT
                        'STRAIGHT_JOIN' // per T58041
                ];
 
-               if ( $force ) {
-                       $collationConds = [];
-               } else {
+               $collationConds = [];
+               if ( !$force ) {
                        if ( $this->hasOption( 'previous-collation' ) ) {
                                $collationConds['cl_collation'] = $this->getOption( 'previous-collation' );
                        } else {
index a27c8a5..c9fb780 100644 (file)
@@ -18,7 +18,7 @@ class UpdateExtensionJsonSchema extends Maintenance {
                }
 
                $json = FormatJson::decode( file_get_contents( $filename ), true );
-               if ( $json === null ) {
+               if ( !is_array( $json ) ) {
                        $this->fatalError( "Error: Invalid JSON" );
                }
 
index d4df8ae..141e307 100644 (file)
@@ -4,6 +4,23 @@
  * Common code for test environment initialisation and teardown
  */
 class TestSetup {
+       public static $bootstrapGlobals;
+
+       /**
+        * For use in MediaWikiUnitTestCase.
+        *
+        * This should be called before DefaultSettings.php or Setup.php loads.
+        */
+       public static function snapshotGlobals() {
+               self::$bootstrapGlobals = [];
+               foreach ( $GLOBALS as $key => $_ ) {
+                       // Support: HHVM (avoid self-ref)
+                       if ( $key !== 'GLOBALS' ) {
+                               self::$bootstrapGlobals[ $key ] =& $GLOBALS[$key];
+                       }
+               }
+       }
+
        /**
         * This should be called before Setup.php, e.g. from the finalSetup() method
         * of a Maintenance subclass
index 6cd7811..00ff2c9 100644 (file)
@@ -87,7 +87,6 @@ $wgAutoloadClasses += [
        'ApiQueryTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryTestBase.php",
        'ApiQueryContinueTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryContinueTestBase.php",
        'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php",
-       'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php",
        'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php",
        'ApiUploadTestCase' => "$testDir/phpunit/includes/api/ApiUploadTestCase.php",
        'MockApi' => "$testDir/phpunit/includes/api/MockApi.php",
index b1eb9ef..82d359f 100644 (file)
@@ -182,8 +182,10 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                global $IP;
                parent::setUpBeforeClass();
                if ( !file_exists( "$IP/LocalSettings.php" ) ) {
-                       echo 'A working MediaWiki installation with a configured LocalSettings.php file is'
-                       . ' required for tests that extend ' . self::class;
+                               echo "File \"$IP/LocalSettings.php\" could not be found. "
+                               . "Test case " . static::class . " extends " . self::class . " "
+                               . "which requires a working MediaWiki installation.\n"
+                               . ( new RuntimeException() )->getTraceAsString();
                        die();
                }
                self::initializeForStandardPhpunitEntrypointIfNeeded();
@@ -584,6 +586,17 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
        }
 
+       private static function formatErrorLevel( $errorLevel ) {
+               switch ( gettype( $errorLevel ) ) {
+               case 'integer':
+                       return '0x' . strtoupper( dechex( $errorLevel ) );
+               case 'NULL':
+                       return 'null';
+               default:
+                       throw new MWException( 'Unexpected error level type ' . gettype( $errorLevel ) );
+               }
+       }
+
        protected function tearDown() {
                global $wgRequest, $wgSQLMode;
 
@@ -649,10 +662,10 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                if ( $phpErrorLevel !== $this->phpErrorLevel ) {
                        ini_set( 'error_reporting', $this->phpErrorLevel );
 
-                       $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
-                       $newHex = strtoupper( dechex( $phpErrorLevel ) );
+                       $oldVal = self::formatErrorLevel( $this->phpErrorLevel );
+                       $newVal = self::formatErrorLevel( $phpErrorLevel );
                        $message = "PHP error_reporting setting was left dirty: "
-                               . "was 0x$oldHex before test, 0x$newHex after test!";
+                               . "was $oldVal before test, $newVal after test!";
 
                        $this->fail( $message );
                }
index f047d82..4ccfe39 100644 (file)
@@ -26,7 +26,7 @@ trait MediaWikiTestCaseTrait {
         */
        protected function createNoOpMock( $type ) {
                $mock = $this->createMock( $type );
-               $mock->expects( $this->never() )->method( $this->anything() );
+               $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) );
                return $mock;
        }
 }
index edd8195..3f876ae 100644 (file)
@@ -34,30 +34,89 @@ abstract class MediaWikiUnitTestCase extends TestCase {
        use MediaWikiCoversValidator;
        use MediaWikiTestCaseTrait;
 
-       private $unitGlobals = [];
+       private static $originalGlobals;
+       private static $unitGlobals;
 
-       protected function setUp() {
-               parent::setUp();
-               $reflection = new ReflectionClass( $this );
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               $reflection = new ReflectionClass( static::class );
                $dirSeparator = DIRECTORY_SEPARATOR;
-               if ( strpos( $reflection->getFilename(), "${dirSeparator}unit${dirSeparator}" ) === false ) {
-                       $this->fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
+               if ( stripos( $reflection->getFilename(), "${dirSeparator}unit${dirSeparator}" ) === false ) {
+                       self::fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
+               }
+
+               if ( defined( 'HHVM_VERSION' ) ) {
+                       // There are a number of issues we encountered in trying to make this
+                       // work on HHVM. Specifically, once an MediaWikiIntegrationTestCase executes
+                       // before us, the original globals go missing. This might have to do with
+                       // one of the non-unit tests passing GLOBALS somewhere and causing HHVM
+                       // to get confused somehow.
+                       return;
+               }
+
+               self::$unitGlobals =& TestSetup::$bootstrapGlobals;
+               // The autoloader may change between bootstrap and the first test,
+               // so (lazily) capture these here instead.
+               self::$unitGlobals['wgAutoloadClasses'] =& $GLOBALS['wgAutoloadClasses'];
+               self::$unitGlobals['wgAutoloadLocalClasses'] =& $GLOBALS['wgAutoloadLocalClasses'];
+               // This value should always be true.
+               self::$unitGlobals['wgAutoloadAttemptLowercase'] = true;
+
+               // Would be nice if we coud simply replace $GLOBALS as a whole,
+               // but unsetting or re-assigning that breaks the reference of this magic
+               // variable. Thus we have to modify it in place.
+               self::$originalGlobals = [];
+               foreach ( $GLOBALS as $key => $_ ) {
+                       // Stash current values
+                       self::$originalGlobals[$key] =& $GLOBALS[$key];
+
+                       // Remove globals not part of the snapshot (see bootstrap.php, phpunit.php).
+                       // Support: HHVM (avoid self-ref)
+                       if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
+                               unset( $GLOBALS[$key] );
+                       }
                }
-               $this->unitGlobals = $GLOBALS;
-               unset( $GLOBALS );
-               $GLOBALS = [];
-               // Add back the minimal set of globals needed for unit tests to run for core +
-               // extensions/skins.
-               foreach ( $this->unitGlobals['wgPhpUnitBootstrapGlobals'] ?? [] as $key => $value ) {
-                       $GLOBALS[ $key ] = $this->unitGlobals[ $key ];
+               // Restore values from the early snapshot
+               // Not by ref because tests must not be able to modify the snapshot.
+               foreach ( self::$unitGlobals as $key => $value ) {
+                       $GLOBALS[ $key ] = $value;
                }
        }
 
        protected function tearDown() {
-               $GLOBALS = $this->unitGlobals;
+               if ( !defined( 'HHVM_VERSION' ) ) {
+                       // Quick reset between tests
+                       foreach ( $GLOBALS as $key => $_ ) {
+                               if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
+                                       unset( $GLOBALS[$key] );
+                               }
+                       }
+                       foreach ( self::$unitGlobals as $key => $value ) {
+                               $GLOBALS[ $key ] = $value;
+                       }
+               }
+
                parent::tearDown();
        }
 
+       public static function tearDownAfterClass() {
+               if ( !defined( 'HHVM_VERSION' ) ) {
+                       // Remove globals created by the test
+                       foreach ( $GLOBALS as $key => $_ ) {
+                               if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$originalGlobals ) ) {
+                                       unset( $GLOBALS[$key] );
+                               }
+                       }
+                       // Restore values (including reference!)
+                       foreach ( self::$originalGlobals as $key => &$value ) {
+                               $GLOBALS[ $key ] =& $value;
+                       }
+               }
+
+               parent::tearDownAfterClass();
+       }
+
        /**
         * Create a temporary hook handler which will be reset by tearDown.
         * This replaces other handlers for the same hook.
index f227ae1..477dbd2 100644 (file)
@@ -55,35 +55,34 @@ define( 'MW_CONFIG_FILE', "$IP/LocalSettings.php" );
 
 // these variables must be defined before setup runs
 $GLOBALS['IP'] = $IP;
-// Set bootstrap globals to reuse in MediaWikiUnitTestCase
-$bootstrapGlobals = [];
-foreach ( $GLOBALS as $key => $value ) {
-       $bootstrapGlobals[ $key ] = $value;
-}
-$GLOBALS['wgPhpUnitBootstrapGlobals'] = $bootstrapGlobals;
-// Faking for Setup.php
+
+require_once "$IP/tests/common/TestSetup.php";
+TestSetup::snapshotGlobals();
+
+// Faking in lieu of Setup.php
 $GLOBALS['wgScopeTest'] = 'MediaWiki Setup.php scope test';
 $GLOBALS['wgCommandLineMode'] = true;
 $GLOBALS['wgAutoloadClasses'] = [];
 
-require_once "$IP/tests/common/TestSetup.php";
-
 wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/tests/common/TestsAutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/DefaultSettings.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/GlobalFunctions.php" );
 
-// Load extensions/skins present in filesystem so that classes can be discovered.
+// Populate classes and namespaces from extensions and skins present in filesystem.
 $directoryToJsonMap = [
-       'extensions' => [ 'extension.json', 'extension-wip.json' ],
-       'skins' => [ 'skin.json', 'skin-wip.json' ]
+       $GLOBALS['wgExtensionDirectory'] => [ 'extension.json', 'extension-wip.json' ],
+       $GLOBALS['wgStyleDirectory'] => [ 'skin.json', 'skin-wip.json' ]
 ];
 foreach ( $directoryToJsonMap as $directory => $jsonFile ) {
-       foreach ( new DirectoryIterator( __DIR__ . '/../../' . $directory ) as $iterator ) {
+       foreach ( new DirectoryIterator( $directory ) as $iterator ) {
                foreach ( $jsonFile as $file ) {
+
                        $jsonPath = $iterator->getPathname() . '/' . $file;
                        if ( file_exists( $jsonPath ) ) {
+                               // ExtensionRegistry->readFromQueue is not used as it checks extension/skin
+                               // dependencies, which we don't need or want for unit tests.
                                $json = file_get_contents( $jsonPath );
                                $info = json_decode( $json, true );
                                $dir = dirname( $jsonPath );
diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..60d456d
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\MainSlotRoleHandler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\MainSlotRoleHandler
+ */
+class MainSlotRoleHandlerTest extends \MediaWikiIntegrationTestCase {
+
+       private function makeTitleObject( $ns ) {
+               /** @var Title|MockObject $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $title->method( 'getNamespace' )
+                       ->willReturn( $ns );
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new MainSlotRoleHandler( [] );
+               $this->assertSame( 'main', $handler->getRole() );
+               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
+        */
+       public function testFetDefaultModel() {
+               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
+
+               // For the main handler, the namespace determins the default model
+               $titleMain = $this->makeTitleObject( NS_MAIN );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
+
+               $title100 = $this->makeTitleObject( 100 );
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               // For the main handler, (nearly) all models are allowed
+               $title = $this->makeTitleObject( NS_MAIN );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               $this->assertTrue( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php
new file mode 100644 (file)
index 0000000..7ffe004
--- /dev/null
@@ -0,0 +1,415 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends \MediaWikiIntegrationTestCase {
+
+       private function makeRow( $data = [] ) {
+               $data = $data + [
+                       'slot_id' => 1234,
+                       'slot_content_id' => 33,
+                       'content_size' => '5',
+                       'content_sha1' => 'someHash',
+                       'content_address' => 'tt:456',
+                       'model_name' => CONTENT_MODEL_WIKITEXT,
+                       'format_name' => CONTENT_FORMAT_WIKITEXT,
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '1',
+                       'role_name' => 'myRole',
+               ];
+               return (object)$data;
+       }
+
+       public function testCompleteConstruction() {
+               $row = $this->makeRow();
+               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertTrue( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 5, $record->getSize() );
+               $this->assertSame( 'someHash', $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 1, $record->getOrigin() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( 33, $record->getContentId() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testConstructionDeferred() {
+               $row = $this->makeRow( [
+                       'content_size' => null, // to be computed
+                       'content_sha1' => null, // to be computed
+                       'format_name' => function () {
+                               return CONTENT_FORMAT_WIKITEXT;
+                       },
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
+               ] );
+
+               $content = function () {
+                       return new WikitextContent( 'A' );
+               };
+
+               $record = new SlotRecord( $row, $content );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testNewUnsaved() {
+               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+               $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->hasRevision() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function provideInvalidConstruction() {
+               yield 'both null' => [ null, null ];
+               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+               yield 'null content' => [ (object)[], null ];
+       }
+
+       /**
+        * @dataProvider provideInvalidConstruction
+        */
+       public function testInvalidConstruction( $row, $content ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new SlotRecord( $row, $content );
+       }
+
+       public function testGetContentId_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getContentId();
+       }
+
+       public function testGetAddress_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getAddress();
+       }
+
+       public function provideIncomplete() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               yield 'unsaved' => [ $unsaved ];
+
+               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $inherited = SlotRecord::newInherited( $parent );
+               yield 'inherited' => [ $inherited ];
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetRevision_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getRevision();
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetOrigin_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getOrigin();
+       }
+
+       public function provideHashStability() {
+               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+       }
+
+       /**
+        * @dataProvider provideHashStability
+        */
+       public function testHashStability( $text, $hash ) {
+               // Changing the output of the hash function will break things horribly!
+
+               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+               $this->assertSame( $hash, $record->getSha1() );
+       }
+
+       public function testHashComputed() {
+               $row = $this->makeRow();
+               $row->content_sha1 = '';
+
+               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
+               $this->assertNotEmpty( $rec->getSha1() );
+       }
+
+       public function testNewWithSuppressedContent() {
+               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $output = SlotRecord::newWithSuppressedContent( $input );
+
+               $this->setExpectedException( SuppressedDataException::class );
+               $output->getContent();
+       }
+
+       public function testNewInherited() {
+               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, before saving revision meta-data.
+               $inherited = SlotRecord::newInherited( $parent );
+
+               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+               $this->assertSame( $parent->getContent(), $inherited->getContent() );
+               $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
+               $this->assertFalse( $inherited->hasRevision() );
+
+               // make sure we didn't mess with the internal state of $parent
+               $this->assertFalse( $parent->isInherited() );
+               $this->assertSame( 7, $parent->getRevision() );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved(
+                       10,
+                       $inherited->getContentId(),
+                       $inherited->getAddress(),
+                       $inherited
+               );
+               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+               $this->assertSame( $parent->getContent(), $saved->getContent() );
+               $this->assertTrue( $saved->isInherited() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertSame( 10, $saved->getRevision() );
+
+               // make sure we didn't mess with the internal state of $parent or $inherited
+               $this->assertSame( 7, $parent->getRevision() );
+               $this->assertFalse( $inherited->hasRevision() );
+       }
+
+       public function testNewSaved() {
+               // This would happen while doing an edit, before saving revision meta-data.
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+               $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
+               $this->assertSame( 'theNewAddress', $saved->getAddress() );
+               $this->assertSame( 20, $saved->getContentId() );
+               $this->assertSame( 'A', $saved->getContent()->getText() );
+               $this->assertSame( 10, $saved->getRevision() );
+               $this->assertSame( 10, $saved->getOrigin() );
+
+               // make sure we didn't mess with the internal state of $unsaved
+               $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
+               $this->assertFalse( $unsaved->hasRevision() );
+       }
+
+       public function provideNewSaved_LogicException() {
+               $freshRow = $this->makeRow( [
+                       'content_id' => 10,
+                       'content_address' => 'address:1',
+                       'slot_origin' => 1,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+               $inheritedRow = $this->makeRow( [
+                       'content_id' => null,
+                       'content_address' => null,
+                       'slot_origin' => 0,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_LogicException
+        */
+       public function testNewSaved_LogicException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( LogicException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideNewSaved_InvalidArgumentException() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_InvalidArgumentException
+        */
+       public function testNewSaved_InvalidArgumentException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/WikiReferenceTest.php b/tests/phpunit/includes/WikiReferenceTest.php
new file mode 100644 (file)
index 0000000..702d3d7
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * @covers WikiReference
+ */
+class WikiReferenceTest extends MediaWikiIntegrationTestCase {
+
+       public function provideGetDisplayName() {
+               return [
+                       'http' => [ 'foo.bar', 'http://foo.bar' ],
+                       'https' => [ 'foo.bar', 'http://foo.bar' ],
+
+                       // apparently, this is the expected behavior
+                       'invalid' => [ 'purple kittens', 'purple kittens' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetDisplayName
+        */
+       public function testGetDisplayName( $expected, $canonicalServer ) {
+               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
+               $this->assertEquals( $expected, $reference->getDisplayName() );
+       }
+
+       public function testGetCanonicalServer() {
+               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
+               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
+       }
+
+       public function provideGetCanonicalUrl() {
+               return [
+                       'no fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               'https://acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               'https://acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               'https://acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               'https://acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        */
+       public function testGetCanonicalUrl(
+               $expected, $canonicalServer, $server, $path, $page, $fragmentId
+       ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        * @note getUrl is an alias for getCanonicalUrl
+        */
+       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
+       }
+
+       public function provideGetFullUrl() {
+               return [
+                       'no fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               '//acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               '//acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               '//acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               '//acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFullUrl
+        */
+       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
+       }
+
+}
index 6a44ff3..3adf1b6 100644 (file)
@@ -1443,7 +1443,7 @@ class ApiBaseTest extends ApiTestCase {
                }
 
                $status = StatusValue::newGood();
-               $status->setOk( false );
+               $status->setOK( false );
                try {
                        $mock->dieStatus( $status );
                        $this->fail( 'Expected exception not thrown' );
diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php
deleted file mode 100644 (file)
index a4ff1f0..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-/**
- * For backward compatibility since 1.31
- */
-abstract class ApiTestCaseUpload extends ApiUploadTestCase {
-}
index 41c9aed..3860b76 100644 (file)
 <?php
+
 /**
- * n.b. Ensure that you can write to the images/ directory as the
- * user that will run tests.
- *
- * Note for reviewers: this intentionally duplicates functionality already in
- * "ApiSetup" and so on. This framework works better IMO and has less
- * strangeness (such as test cases inheriting from "ApiSetup"...) (and in the
- * case of the other Upload tests, this flat out just actually works... )
- *
- * @todo Port the other Upload tests, and other API tests to this framework
- *
- * @todo Broken test, reports false errors from time to time.
- * See https://phabricator.wikimedia.org/T28169
- *
- * @todo This is pretty sucky... needs to be prettified.
- *
  * @group API
  * @group Database
  * @group medium
- * @group Broken
  *
  * @covers ApiUpload
  */
 class ApiUploadTest extends ApiUploadTestCase {
-       /**
-        * Testing login
-        * XXX this is a funny way of getting session context
-        */
-       public function testLogin() {
-               $user = self::$users['uploader'];
-               $userName = $user->getUser()->getName();
-               $password = $user->getPassword();
-
-               $params = [
-                       'action' => 'login',
-                       'lgname' => $userName,
-                       'lgpassword' => $password
-               ];
-               list( $result, , $session ) = $this->doApiRequest( $params );
-               $this->assertArrayHasKey( "login", $result );
-               $this->assertArrayHasKey( "result", $result['login'] );
-               $this->assertEquals( "NeedToken", $result['login']['result'] );
-               $token = $result['login']['token'];
-
-               $params = [
-                       'action' => 'login',
-                       'lgtoken' => $token,
-                       'lgname' => $userName,
-                       'lgpassword' => $password
-               ];
-               list( $result, , $session ) = $this->doApiRequest( $params, $session );
-               $this->assertArrayHasKey( "login", $result );
-               $this->assertArrayHasKey( "result", $result['login'] );
-               $this->assertEquals( "Success", $result['login']['result'] );
-
-               $this->assertNotEmpty( $session, 'API Login must return a session' );
-
-               return $session;
+       private function filePath( $fileName ) {
+               return __DIR__ . '/../../data/media/' . $fileName;
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadRequiresToken( $session ) {
-               $exception = false;
-               try {
-                       $this->doApiRequest( [
-                               'action' => 'upload'
-                       ] );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-                       $this->assertContains( 'The "token" parameter must be set', $e->getMessage() );
-               }
-               $this->assertTrue( $exception, "Got exception" );
+       public function setUp() {
+               parent::setUp();
+               $this->tablesUsed[] = 'watchlist'; // This test might interfere with watchlists test.
+               $this->tablesUsed = array_merge( $this->tablesUsed, LocalFile::getQueryInfo()['tables'] );
+               $this->setService( 'RepoGroup', new RepoGroup(
+                       [
+                               'class' => LocalRepo::class,
+                               'name' => 'temp',
+                               'backend' => new FSFileBackend( [
+                                       'name' => 'temp-backend',
+                                       'wikiId' => wfWikiID(),
+                                       'basePath' => $this->getNewTempDirectory()
+                               ] )
+                       ],
+                       [],
+                       null
+               ) );
+               $this->resetServices();
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadMissingParams( $session ) {
-               $exception = false;
-               try {
-                       $this->doApiRequestWithToken( [
-                               'action' => 'upload',
-                       ], $session, self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-                       $this->assertEquals(
-                               'One of the parameters "filekey", "file" and "url" is required.',
-                               $e->getMessage()
-                       );
-               }
-               $this->assertTrue( $exception, "Got exception" );
+       public function testUploadRequiresToken() {
+               $this->setExpectedException(
+                       ApiUsageException::class,
+                       'The "token" parameter must be set'
+               );
+               $this->doApiRequest( [
+                       'action' => 'upload'
+               ] );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUpload( $session ) {
-               $extension = 'png';
-               $mimeType = 'image/png';
-
-               try {
-                       $randomImageGenerator = new RandomImageGenerator();
-                       $filePaths = $randomImageGenerator->writeImages( 1, $extension, $this->getNewTempDirectory() );
-               } catch ( Exception $e ) {
-                       $this->markTestIncomplete( $e->getMessage() );
-               }
-
-               /** @var array $filePaths */
-               $filePath = $filePaths[0];
-               $fileSize = filesize( $filePath );
-               $fileName = basename( $filePath );
-
-               $this->deleteFileByFileName( $fileName );
-               $this->deleteFileByContent( $filePath );
+       public function testUploadMissingParams() {
+               $this->setExpectedException(
+                       ApiUsageException::class,
+                       'One of the parameters "filekey", "file" and "url" is required'
+               );
+               $this->doApiRequestWithToken( [
+                       'action' => 'upload',
+               ], null, self::$users['uploader']->getUser() );
+       }
 
-               if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
+       public function testUpload() {
+               $fileName = 'TestUpload.jpg';
+               $mimeType = 'image/jpeg';
+               $filePath = $this->filePath( 'yuv420.jpg' );
 
-               $params = [
+               $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );
+               list( $result ) = $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'filename' => $fileName,
                        'file' => 'dummy content',
                        'comment' => 'dummy comment',
                        'text' => "This is the page text for $fileName",
-               ];
+               ], null, self::$users['uploader']->getUser() );
 
-               $exception = false;
-               try {
-                       list( $result, , ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertEquals( $fileSize, (int)$result['upload']['imageinfo']['size'] );
+               $this->assertSame( filesize( $filePath ), (int)$result['upload']['imageinfo']['size'] );
                $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
-               $this->assertFalse( $exception );
-
-               // clean up
-               $this->deleteFileByFileName( $fileName );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadZeroLength( $session ) {
-               $mimeType = 'image/png';
-
+       public function testUploadZeroLength() {
                $filePath = $this->getNewTempFile();
-               $fileName = "apiTestUploadZeroLength.png";
-
-               $this->deleteFileByFileName( $fileName );
+               $mimeType = 'image/jpeg';
+               $fileName = "ApiTestUploadZeroLength.jpg";
 
-               if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
+               $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );
 
-               $params = [
+               $this->setExpectedException(
+                       ApiUsageException::class,
+                       'The file you submitted was empty'
+               );
+               $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'filename' => $fileName,
                        'file' => 'dummy content',
                        'comment' => 'dummy comment',
                        'text' => "This is the page text for $fileName",
-               ];
-
-               $exception = false;
-               try {
-                       $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $this->assertContains( 'The file you submitted was empty', $e->getMessage() );
-                       $exception = true;
-               }
-               $this->assertTrue( $exception );
-
-               // clean up
-               $this->deleteFileByFileName( $fileName );
+               ], null, self::$users['uploader']->getUser() );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadSameFileName( $session ) {
-               $extension = 'png';
-               $mimeType = 'image/png';
-
-               try {
-                       $randomImageGenerator = new RandomImageGenerator();
-                       $filePaths = $randomImageGenerator->writeImages( 2, $extension, $this->getNewTempDirectory() );
-               } catch ( Exception $e ) {
-                       $this->markTestIncomplete( $e->getMessage() );
-               }
-
-               // we'll reuse this filename
-               /** @var array $filePaths */
-               $fileName = basename( $filePaths[0] );
-
-               // clear any other files with the same name
-               $this->deleteFileByFileName( $fileName );
+       public function testUploadSameFileName() {
+               $fileName = 'TestUploadSameFileName.jpg';
+               $mimeType = 'image/jpeg';
+               $filePaths = [
+                       $this->filePath( 'yuv420.jpg' ),
+                       $this->filePath( 'yuv444.jpg' )
+               ];
 
                // we reuse these params
                $params = [
@@ -213,176 +111,78 @@ class ApiUploadTest extends ApiUploadTestCase {
 
                // first upload .... should succeed
 
-               if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
-
-               $exception = false;
-               try {
-                       list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] );
+               list( $result ) = $this->doApiRequestWithToken( $params, null,
+                       self::$users['uploader']->getUser() );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertFalse( $exception );
 
                // second upload with the same name (but different content)
 
-               if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
-
-               $exception = false;
-               try {
-                       list( $result, , ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] );
+               list( $result ) = $this->doApiRequestWithToken( $params, null,
+                       self::$users['uploader']->getUser() );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Warning', $result['upload']['result'] );
-               $this->assertTrue( isset( $result['upload']['warnings'] ) );
-               $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) );
-               $this->assertFalse( $exception );
-
-               // clean up
-               $this->deleteFileByFileName( $fileName );
+               $this->assertArrayHasKey( 'warnings', $result['upload'] );
+               $this->assertArrayHasKey( 'exists', $result['upload']['warnings'] );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadSameContent( $session ) {
-               $extension = 'png';
-               $mimeType = 'image/png';
-
-               try {
-                       $randomImageGenerator = new RandomImageGenerator();
-                       $filePaths = $randomImageGenerator->writeImages( 1, $extension, $this->getNewTempDirectory() );
-               } catch ( Exception $e ) {
-                       $this->markTestIncomplete( $e->getMessage() );
-               }
-
-               /** @var array $filePaths */
-               $fileNames[0] = basename( $filePaths[0] );
-               $fileNames[1] = "SameContentAs" . $fileNames[0];
-
-               // clear any other files with the same name or content
-               $this->deleteFileByContent( $filePaths[0] );
-               $this->deleteFileByFileName( $fileNames[0] );
-               $this->deleteFileByFileName( $fileNames[1] );
+       public function testUploadSameContent() {
+               $fileNames = [ 'TestUploadSameContent_1.jpg', 'TestUploadSameContent_2.jpg' ];
+               $mimeType = 'image/jpeg';
+               $filePath = $this->filePath( 'yuv420.jpg' );
 
                // first upload .... should succeed
-
-               $params = [
+               $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePath );
+               list( $result ) = $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'filename' => $fileNames[0],
                        'file' => 'dummy content',
                        'comment' => 'dummy comment',
-                       'text' => "This is the page text for " . $fileNames[0],
-               ];
-
-               if ( !$this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
-
-               $exception = false;
-               try {
-                       list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+                       'text' => "This is the page text for {$fileNames[0]}",
+               ], null, self::$users['uploader']->getUser() );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertFalse( $exception );
 
                // second upload with the same content (but different name)
+               $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePath );
+               list( $result ) = $this->doApiRequestWithToken( [
+                               'action' => 'upload',
+                               'filename' => $fileNames[1],
+                               'file' => 'dummy content',
+                               'comment' => 'dummy comment',
+                               'text' => "This is the page text for {$fileNames[1]}",
+                       ], null, self::$users['uploader']->getUser() );
 
-               if ( !$this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
-
-               $params = [
-                       'action' => 'upload',
-                       'filename' => $fileNames[1],
-                       'file' => 'dummy content',
-                       'comment' => 'dummy comment',
-                       'text' => "This is the page text for " . $fileNames[1],
-               ];
-
-               $exception = false;
-               try {
-                       list( $result ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Warning', $result['upload']['result'] );
-               $this->assertTrue( isset( $result['upload']['warnings'] ) );
-               $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) );
-               $this->assertFalse( $exception );
-
-               // clean up
-               $this->deleteFileByFileName( $fileNames[0] );
-               $this->deleteFileByFileName( $fileNames[1] );
+               $this->assertArrayHasKey( 'warnings', $result['upload'] );
+               $this->assertArrayHasKey( 'duplicate', $result['upload']['warnings'] );
+               $this->assertArrayEquals( [ $fileNames[0] ], $result['upload']['warnings']['duplicate'] );
+               $this->assertArrayNotHasKey( 'exists', $result['upload']['warnings'] );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadStash( $session ) {
-               $this->setMwGlobals( [
-                       'wgUser' => self::$users['uploader']->getUser(), // @todo FIXME: still used somewhere
-               ] );
-
-               $extension = 'png';
-               $mimeType = 'image/png';
-
-               try {
-                       $randomImageGenerator = new RandomImageGenerator();
-                       $filePaths = $randomImageGenerator->writeImages( 1, $extension, $this->getNewTempDirectory() );
-               } catch ( Exception $e ) {
-                       $this->markTestIncomplete( $e->getMessage() );
-               }
-
-               /** @var array $filePaths */
-               $filePath = $filePaths[0];
-               $fileSize = filesize( $filePath );
-               $fileName = basename( $filePath );
-
-               $this->deleteFileByFileName( $fileName );
-               $this->deleteFileByContent( $filePath );
-
-               if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
-                       $this->markTestIncomplete( "Couldn't upload file!\n" );
-               }
+       public function testUploadStash() {
+               $fileName = 'TestUploadStash.jpg';
+               $mimeType = 'image/jpeg';
+               $filePath = $this->filePath( 'yuv420.jpg' );
 
-               $params = [
+               $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );
+               list( $result ) = $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'stash' => 1,
                        'filename' => $fileName,
                        'file' => 'dummy content',
                        'comment' => 'dummy comment',
                        'text' => "This is the page text for $fileName",
-               ];
+               ], null, self::$users['uploader']->getUser() );
 
-               $exception = false;
-               try {
-                       list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertFalse( $exception );
-               $this->assertTrue( isset( $result['upload'] ) );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertEquals( $fileSize, (int)$result['upload']['imageinfo']['size'] );
+               $this->assertSame( filesize( $filePath ), (int)$result['upload']['imageinfo']['size'] );
                $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
-               $this->assertTrue( isset( $result['upload']['filekey'] ) );
+               $this->assertArrayHasKey( 'filekey', $result['upload'] );
                $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] );
                $filekey = $result['upload']['filekey'];
 
@@ -390,58 +190,28 @@ class ApiUploadTest extends ApiUploadTestCase {
                // XXX ...but how to test this, with a fake WebRequest with the session?
 
                // now we should try to release the file from stash
-               $params = [
+               $this->clearFakeUploads();
+               list( $result ) = $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'filekey' => $filekey,
                        'filename' => $fileName,
                        'comment' => 'dummy comment',
                        'text' => "This is the page text for $fileName, altered",
-               ];
-
-               $this->clearFakeUploads();
-               $exception = false;
-               try {
-                       list( $result ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               ], null, self::$users['uploader']->getUser() );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertFalse( $exception, "No ApiUsageException exception." );
-
-               // clean up
-               $this->deleteFileByFileName( $fileName );
        }
 
-       /**
-        * @depends testLogin
-        */
-       public function testUploadChunks( $session ) {
-               $this->setMwGlobals( [
-                       // @todo FIXME: still used somewhere
-                       'wgUser' => self::$users['uploader']->getUser(),
-               ] );
-
-               $chunkSize = 1048576;
-               // Download a large image file
-               // (using RandomImageGenerator for large files is not stable)
-               // @todo Don't download files from wikimedia.org
+       public function testUploadChunks() {
+               $fileName = 'TestUploadChunks.jpg';
                $mimeType = 'image/jpeg';
-               $url = 'http://upload.wikimedia.org/wikipedia/commons/'
-                       . 'e/ed/Oberaargletscher_from_Oberaar%2C_2010_07.JPG';
-               $filePath = $this->getNewTempDirectory() . '/Oberaargletscher_from_Oberaar.jpg';
-               try {
-                       copy( $url, $filePath );
-               } catch ( Exception $e ) {
-                       $this->markTestIncomplete( $e->getMessage() );
-               }
-
+               $filePath = $this->filePath( 'yuv420.jpg' );
                $fileSize = filesize( $filePath );
-               $fileName = basename( $filePath );
+               $chunkSize = 20 * 1024; // The file is ~60kB, use 20kB chunks
 
-               $this->deleteFileByFileName( $fileName );
-               $this->deleteFileByContent( $filePath );
+               $this->setMwGlobals( [
+                       'wgMinUploadChunkSize' => $chunkSize
+               ] );
 
                // Base upload params:
                $params = [
@@ -453,108 +223,68 @@ class ApiUploadTest extends ApiUploadTestCase {
                ];
 
                // Upload chunks
-               $chunkSessionKey = false;
-               $resultOffset = 0;
-               // Open the file:
-               Wikimedia\suppressWarnings();
                $handle = fopen( $filePath, "r" );
-               Wikimedia\restoreWarnings();
-
-               if ( $handle === false ) {
-                       $this->markTestIncomplete( "could not open file: $filePath" );
-               }
-
+               $resultOffset = 0;
+               $filekey = false;
                while ( !feof( $handle ) ) {
-                       // Get the current chunk
-                       Wikimedia\suppressWarnings();
                        $chunkData = fread( $handle, $chunkSize );
-                       Wikimedia\restoreWarnings();
 
                        // Upload the current chunk into the $_FILE object:
                        $this->fakeUploadChunk( 'chunk', 'blob', $mimeType, $chunkData );
-
-                       // Check for chunkSessionKey
-                       if ( !$chunkSessionKey ) {
-                               // Upload fist chunk ( and get the session key )
-                               try {
-                                       list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
-                                               self::$users['uploader']->getUser() );
-                               } catch ( ApiUsageException $e ) {
-                                       $this->markTestIncomplete( $e->getMessage() );
-                               }
+                       if ( !$filekey ) {
+                               list( $result ) = $this->doApiRequestWithToken( $params, null,
+                                       self::$users['uploader']->getUser() );
                                // Make sure we got a valid chunk continue:
-                               $this->assertTrue( isset( $result['upload'] ) );
-                               $this->assertTrue( isset( $result['upload']['filekey'] ) );
-                               // If we don't get a session key mark test incomplete.
-                               if ( !isset( $result['upload']['filekey'] ) ) {
-                                       $this->markTestIncomplete( "no filekey provided" );
-                               }
-                               $chunkSessionKey = $result['upload']['filekey'];
+                               $this->assertArrayHasKey( 'upload', $result );
+                               $this->assertArrayHasKey( 'filekey', $result['upload'] );
                                $this->assertEquals( 'Continue', $result['upload']['result'] );
-                               // First chunk should have chunkSize == offset
                                $this->assertEquals( $chunkSize, $result['upload']['offset'] );
+
+                               $filekey = $result['upload']['filekey'];
                                $resultOffset = $result['upload']['offset'];
-                               continue;
-                       }
-                       // Filekey set to chunk session
-                       $params['filekey'] = $chunkSessionKey;
-                       // Update the offset ( always add chunkSize for subquent chunks
-                       // should be in-sync with $result['upload']['offset'] )
-                       $params['offset'] += $chunkSize;
-                       // Make sure param offset is insync with resultOffset:
-                       $this->assertEquals( $resultOffset, $params['offset'] );
-                       // Upload current chunk
-                       try {
-                               list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
-                                       self::$users['uploader']->getUser() );
-                       } catch ( ApiUsageException $e ) {
-                               $this->markTestIncomplete( $e->getMessage() );
-                       }
-                       // Make sure we got a valid chunk continue:
-                       $this->assertTrue( isset( $result['upload'] ) );
-                       $this->assertTrue( isset( $result['upload']['filekey'] ) );
-
-                       // Check if we were on the last chunk:
-                       if ( $params['offset'] + $chunkSize >= $fileSize ) {
-                               $this->assertEquals( 'Success', $result['upload']['result'] );
-                               break;
                        } else {
-                               $this->assertEquals( 'Continue', $result['upload']['result'] );
-                               // update $resultOffset
-                               $resultOffset = $result['upload']['offset'];
+                               // Filekey set to chunk session
+                               $params['filekey'] = $filekey;
+                               // Update the offset ( always add chunkSize for subquent chunks
+                               // should be in-sync with $result['upload']['offset'] )
+                               $params['offset'] += $chunkSize;
+                               // Make sure param offset is insync with resultOffset:
+                               $this->assertEquals( $resultOffset, $params['offset'] );
+                               // Upload current chunk
+                               list( $result ) = $this->doApiRequestWithToken( $params, null,
+                                       self::$users['uploader']->getUser() );
+                               // Make sure we got a valid chunk continue:
+                               $this->assertArrayHasKey( 'upload', $result );
+                               $this->assertArrayHasKey( 'filekey', $result['upload'] );
+
+                               // Check if we were on the last chunk:
+                               if ( $params['offset'] + $chunkSize >= $fileSize ) {
+                                       $this->assertEquals( 'Success', $result['upload']['result'] );
+                                       break;
+                               } else {
+                                       $this->assertEquals( 'Continue', $result['upload']['result'] );
+                                       $resultOffset = $result['upload']['offset'];
+                               }
                        }
                }
                fclose( $handle );
 
                // Check that we got a valid file result:
-               wfDebug( __METHOD__
-                       . " hohoh filesize {$fileSize} info {$result['upload']['imageinfo']['size']}\n\n" );
                $this->assertEquals( $fileSize, $result['upload']['imageinfo']['size'] );
                $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
-               $this->assertTrue( isset( $result['upload']['filekey'] ) );
+               $this->assertArrayHasKey( 'filekey', $result['upload'] );
                $filekey = $result['upload']['filekey'];
 
                // Now we should try to release the file from stash
-               $params = [
+               $this->clearFakeUploads();
+               list( $result ) = $this->doApiRequestWithToken( [
                        'action' => 'upload',
                        'filekey' => $filekey,
                        'filename' => $fileName,
                        'comment' => 'dummy comment',
                        'text' => "This is the page text for $fileName, altered",
-               ];
-               $this->clearFakeUploads();
-               $exception = false;
-               try {
-                       list( $result ) = $this->doApiRequestWithToken( $params, $session,
-                               self::$users['uploader']->getUser() );
-               } catch ( ApiUsageException $e ) {
-                       $exception = true;
-               }
-               $this->assertTrue( isset( $result['upload'] ) );
+               ], null, self::$users['uploader']->getUser() );
+               $this->assertArrayHasKey( 'upload', $result );
                $this->assertEquals( 'Success', $result['upload']['result'] );
-               $this->assertFalse( $exception );
-
-               // clean up
-               $this->deleteFileByFileName( $fileName );
        }
 }
index fc1930a..8c9a88f 100644 (file)
@@ -32,7 +32,7 @@ class UserDataAuthenticationRequestTest extends AuthenticationRequestTestCase {
                $req->email = $email;
                $req->realname = $realname;
                $this->assertEquals( $expect, $req->populateUser( $user ) );
-               if ( $expect->isOk() ) {
+               if ( $expect->isOK() ) {
                        $this->assertSame( $email ?: 'default@example.com', $user->getEmail() );
                        $this->assertSame( $realname ?: 'Fake Name', $user->getRealName() );
                }
index 8409f56..428440f 100644 (file)
@@ -207,40 +207,52 @@ class CompositeBlockTest extends MediaWikiLangTestCase {
         * @covers ::appliesToRight
         * @dataProvider provideTestBlockAppliesToRight
         */
-       public function testBlockAppliesToRight( $blocks, $right, $expected ) {
+       public function testBlockAppliesToRight( $applies, $expected ) {
                $this->setMwGlobals( [
                        'wgBlockDisablesLogin' => false,
                ] );
 
                $block = new CompositeBlock( [
-                       'originalBlocks' => $blocks,
+                       'originalBlocks' => [
+                               $this->getMockBlockForTestAppliesToRight( $applies[ 0 ] ),
+                               $this->getMockBlockForTestAppliesToRight( $applies[ 1 ] ),
+                       ],
                ] );
 
-               $this->assertSame( $block->appliesToRight( $right ), $expected );
+               $this->assertSame( $block->appliesToRight( 'right' ), $expected );
+       }
+
+       private function getMockBlockForTestAppliesToRight( $applies ) {
+               $mockBlock = $this->getMockBuilder( DatabaseBlock::class )
+                       ->setMethods( [ 'appliesToRight' ] )
+                       ->getMock();
+               $mockBlock->method( 'appliesToRight' )
+                       ->willReturn( $applies );
+               return $mockBlock;
        }
 
-       public static function provideTestBlockAppliesToRight() {
+       public function provideTestBlockAppliesToRight() {
                return [
-                       'Read is not blocked' => [
-                               [
-                                       new DatabaseBlock(),
-                                       new DatabaseBlock(),
-                               ],
-                               'read',
+                       'Block does not apply if no original blocks apply' => [
+                               [ false, false ],
                                false,
                        ],
-                       'Email is blocked if blocked by any blocks' => [
-                               [
-                                       new DatabaseBlock( [
-                                               'blockEmail' => true,
-                                       ] ),
-                                       new DatabaseBlock( [
-                                               'blockEmail' => false,
-                                       ] ),
-                               ],
-                               'sendemail',
+                       'Block applies if any original block applies (second block doesn\'t apply)' => [
+                               [ true, false ],
+                               true,
+                       ],
+                       'Block applies if any original block applies (second block unsure)' => [
+                               [ true, null ],
                                true,
                        ],
+                       'Block is unsure if all original blocks are unsure' => [
+                               [ null, null ],
+                               null,
+                       ],
+                       'Block is unsure if any original block is unsure, and no others apply' => [
+                               [ null, false ],
+                               null,
+                       ],
                ];
        }
 
diff --git a/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..e5c23d1
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers DifferenceEngineSlotDiffRenderer
+ */
+class DifferenceEngineSlotDiffRendererTest extends MediaWikiIntegrationTestCase {
+
+       public function testGetDiff() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
+               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
+               $this->assertEquals( 'xxx|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( null, $newContent );
+               $this->assertEquals( '|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
+               $this->assertEquals( 'xxx|', $diff );
+       }
+
+       public function testAddModules() {
+               $output = $this->getMockBuilder( OutputPage::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'addModules' ] )
+                       ->getMock();
+               $output->expects( $this->once() )
+                       ->method( 'addModules' )
+                       ->with( 'foo' );
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $slotDiffRenderer->addModules( $output );
+       }
+
+       public function testGetExtraCacheKeys() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
+               $this->assertSame( [ 'foo' ], $extraCacheKeys );
+       }
+
+}
diff --git a/tests/phpunit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/includes/diff/SlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..9f15517
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+use Wikimedia\Assert\ParameterTypeException;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers SlotDiffRenderer
+ */
+class SlotDiffRendererTest extends \MediaWikiIntegrationTestCase {
+
+       /**
+        * @dataProvider provideNormalizeContents
+        */
+       public function testNormalizeContents(
+               $oldContent, $newContent, $allowedClasses,
+               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
+       ) {
+               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
+                       ->getMock();
+               try {
+                       // __call needs help deciding which parameter to take by reference
+                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
+                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
+                       $this->assertEquals( $expectedOldContent, $oldContent );
+                       $this->assertEquals( $expectedNewContent, $newContent );
+               } catch ( Exception $e ) {
+                       if ( !$expectedExceptionClass ) {
+                               throw $e;
+                       }
+                       $this->assertInstanceOf( $expectedExceptionClass, $e );
+               }
+       }
+
+       public function provideNormalizeContents() {
+               return [
+                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
+                       'left null' => [
+                               null, new WikitextContent( 'abc' ), null,
+                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
+                       ],
+                       'right null' => [
+                               new WikitextContent( 'def' ), null, null,
+                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (subclass)' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (null)' => [
+                               new WikitextContent( 'abc' ), null, TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter failure (left)' => [
+                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter failure (right)' => [
+                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter (array syntax)' => [
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
+                       ],
+                       'type filter failure (array syntax)' => [
+                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               null, null, ParameterTypeException::class,
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php
new file mode 100644 (file)
index 0000000..69fc367
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class FileBackendDBRepoWrapperTest extends MediaWikiIntegrationTestCase {
+       protected $backendName = 'foo-backend';
+       protected $repoName = 'pureTestRepo';
+
+       /**
+        * @dataProvider getBackendPathsProvider
+        * @covers FileBackendDBRepoWrapper::getBackendPaths
+        */
+       public function testGetBackendPaths(
+               $mocks,
+               $latest,
+               $dbReadsExpected,
+               $dbReturnValue,
+               $originalPath,
+               $expectedBackendPath,
+               $message ) {
+               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
+
+               $dbMock->expects( $dbReadsExpected )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( $dbReturnValue ) );
+
+               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
+
+               $this->assertEquals(
+                       $expectedBackendPath,
+                       $newPaths[0],
+                       $message );
+       }
+
+       public function getBackendPathsProvider() {
+               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
+               $mocksForCaching = $this->getMocks();
+
+               return [
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Public path translated correctly',
+                       ],
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'LRU cache leveraged',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Latest obtained',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-deleted/f/o/foobar.jpg',
+                               $prefix . '-original/f/o/o/foobar',
+                               'Deleted path translated correctly',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               null,
+                               $prefix . '-public/b/a/baz.jpg',
+                               $prefix . '-public/b/a/baz.jpg',
+                               'Path left untouched if no sha1 can be found',
+                       ],
+               ];
+       }
+
+       /**
+        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
+        */
+       public function testGetFileContentsMulti() {
+               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
+
+               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
+               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-public/f/o/foobar.jpg';
+
+               $dbMock->expects( $this->once() )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
+
+               $backendMock->expects( $this->once() )
+                       ->method( 'getFileContentsMulti' )
+                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
+
+               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
+
+               $this->assertEquals(
+                       [ $filenamePath => 'foo' ],
+                       $result,
+                       'File contents paths translated properly'
+               );
+       }
+
+       protected function getMocks() {
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
+                       ->disableOriginalClone()
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $backendMock = $this->getMockBuilder( FSFileBackend::class )
+                       ->setConstructorArgs( [ [
+                                       'name' => $this->backendName,
+                                       'wikiId' => wfWikiID()
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
+                       ->setMethods( [ 'getDB' ] )
+                       ->setConstructorArgs( [ [
+                                       'backend' => $backendMock,
+                                       'repoName' => $this->repoName,
+                                       'dbHandleFactory' => null
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
+
+               return [ $dbMock, $backendMock, $wrapperMock ];
+       }
+}
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..6c56510
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ *
+ * @group Media
+ * @covers JpegMetadataExtractor
+ */
+class JpegMetadataExtractorTest extends MediaWikiIntegrationTestCase {
+
+       protected $filePath;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->filePath = __DIR__ . '/../../data/media/';
+       }
+
+       /**
+        * We also use this test to test padding bytes don't
+        * screw stuff up
+        *
+        * @param string $file Filename
+        *
+        * @dataProvider provideUtf8Comment
+        */
+       public function testUtf8Comment( $file ) {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
+       }
+
+       public static function provideUtf8Comment() {
+               return [
+                       [ 'jpeg-comment-utf.jpg' ],
+                       [ 'jpeg-padding-even.jpg' ],
+                       [ 'jpeg-padding-odd.jpg' ],
+               ];
+       }
+
+       /** The file is iso-8859-1, but it should get auto converted */
+       public function testIso88591Comment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
+       }
+
+       /** Comment values that are non-textual (random binary junk) should not be shown.
+        * The example test file has a comment with a 0x5 byte in it which is a control character
+        * and considered binary junk for our purposes.
+        */
+       public function testBinaryCommentStripped() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+               $this->assertEmpty( $res['COM'] );
+       }
+
+       /* Very rarely a file can have multiple comments.
+        *   Order of comments is based on order inside the file.
+        */
+       public function testMultipleComment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
+       }
+
+       public function testXMPExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testPSIRExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = '50686f746f73686f7020332e30003842494d04040000000'
+                       . '000181c02190004746573741c02190003666f6f1c020000020004';
+               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+       }
+
+       public function testXMPExtractionAltAppId() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testIPTCHashComparisionNoHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-no-hash', $res );
+       }
+
+       public function testIPTCHashComparisionBadHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-bad-hash', $res );
+       }
+
+       public function testIPTCHashComparisionGoodHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-good-hash', $res );
+       }
+
+       public function testExifByteOrder() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+               $expected = 'BE';
+               $this->assertEquals( $expected, $res['byteOrder'] );
+       }
+
+       public function testInfiniteRead() {
+               // test file truncated right after a segment, which previously
+               // caused an infinite loop looking for the next segment byte.
+               // Should get past infinite loop and throw in wfUnpack()
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
+       }
+
+       public function testInfiniteRead2() {
+               // test file truncated after a segment's marker and size, which
+               // would cause a seek past end of file. Seek past end of file
+               // doesn't actually fail, but prevents further reading and was
+               // devolving into the previous case (testInfiniteRead).
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
+       }
+}
diff --git a/tests/phpunit/includes/parser/ParserFactoryTest.php b/tests/phpunit/includes/parser/ParserFactoryTest.php
new file mode 100644 (file)
index 0000000..e6e9db4
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @covers ParserFactory
+ */
+class ParserFactoryTest extends MediaWikiIntegrationTestCase {
+       use FactoryArgTestTrait;
+
+       protected static function getFactoryClass() {
+               return ParserFactory::class;
+       }
+
+       protected static function getInstanceClass() {
+               return Parser::class;
+       }
+
+       protected static function getFactoryMethodName() {
+               return 'create';
+       }
+
+       protected static function getExtraClassArgCount() {
+               // The parser factory itself is passed to the parser
+               return 1;
+       }
+
+       protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
+               if ( $param->getPosition() === 0 ) {
+                       return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ];
+               }
+               return [];
+       }
+}
diff --git a/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php
new file mode 100644 (file)
index 0000000..7a6647b
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+use MediaWiki\Site\MediaWikiPageNameNormalizer;
+
+/**
+ * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.27
+ *
+ * @group Site
+ * @group medium
+ *
+ * @author Marius Hoch
+ */
+class MediaWikiPageNameNormalizerTest extends MediaWikiIntegrationTestCase {
+
+       /**
+        * @dataProvider normalizePageTitleProvider
+        */
+       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
+               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
+
+               $normalizer = new MediaWikiPageNameNormalizer(
+                       new MediaWikiPageNameNormalizerTestMockHttp()
+               );
+
+               $this->assertSame(
+                       $expected,
+                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
+               );
+       }
+
+       public function normalizePageTitleProvider() {
+               // Response are taken from wikidata and kkwiki using the following API request
+               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
+               return [
+                       'universe (Q1)' => [
+                               'Q1',
+                               'Q1',
+                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
+                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
+                       ],
+                       'Q404 redirects to Q395' => [
+                               'Q395',
+                               'Q404',
+                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
+                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
+                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
+                       ],
+                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
+                               'Д',
+                               'D',
+                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
+                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
+                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
+                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
+                               . '"lastrevid":2373618,"length":3501}}}}'
+                       ],
+                       'there is no Q0' => [
+                               false,
+                               'Q0',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
+                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
+                       ],
+                       'invalid title' => [
+                               false,
+                               '{{',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
+                               . '"invalidreason":"The requested page title contains invalid '
+                               . 'characters: \"{\".","invalid":""}}}}'
+                       ],
+                       'error on get' => [ false, 'ABC', false ]
+               ];
+       }
+
+}
+
+/**
+ * @private
+ * @see Http
+ */
+class MediaWikiPageNameNormalizerTestMockHttp extends Http {
+
+       /**
+        * @var mixed
+        */
+       public static $response;
+
+       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
+
+               return self::$response;
+       }
+}
diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php
new file mode 100644 (file)
index 0000000..158be69
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteExporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteExporterTest extends MediaWikiIntegrationTestCase {
+
+       public function testConstructor_InvalidArgument() {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new SiteExporter( 'Foo' );
+       }
+
+       public function testExportSites() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( [ $foo, $acme ] );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $this->assertContains( '<sites ', $xml );
+               $this->assertContains( '<site>', $xml );
+               $this->assertContains( '<globalid>Foo</globalid>', $xml );
+               $this->assertContains( '</site>', $xml );
+               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
+               $this->assertContains( '<group>Test</group>', $xml );
+               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
+               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
+               $this->assertContains( '</sites>', $xml );
+
+               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
+               $xsdFile = __DIR__ . '/../../../../docs/sitelist-1.0.xsd';
+               $xsdData = file_get_contents( $xsdFile );
+
+               $document = new DOMDocument();
+               $document->loadXML( $xml, LIBXML_NONET );
+               $document->schemaValidateSource( $xsdData );
+       }
+
+       private function newSiteStore( SiteList $sites ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
+                               foreach ( $moreSites as $site ) {
+                                       $sites->setSite( $site );
+                               }
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               return $store;
+       }
+
+       public function provideRoundTrip() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               new SiteList()
+                       ],
+
+                       'some' => [
+                               new SiteList( [ $foo, $acme, $dewiki ] ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideRoundTrip()
+        */
+       public function testRoundTrip( SiteList $sites ) {
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( $sites );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $actualSites = new SiteList();
+               $store = $this->newSiteStore( $actualSites );
+
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( $xml );
+
+               $this->assertEquals( $sites, $actualSites );
+       }
+
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.php b/tests/phpunit/includes/site/SiteImporterTest.php
new file mode 100644 (file)
index 0000000..c614dd4
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteImporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteImporterTest extends MediaWikiIntegrationTestCase {
+
+       private function newSiteImporter( array $expectedSites, $errorCount ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
+                               $this->assertSitesEqual( $expectedSites, $sites );
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
+               $errorHandler->expects( $this->exactly( $errorCount ) )
+                       ->method( 'error' );
+
+               $importer = new SiteImporter( $store );
+               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
+
+               return $importer;
+       }
+
+       public function assertSitesEqual( $expected, $actual, $message = '' ) {
+               $this->assertEquals(
+                       $this->getSerializedSiteList( $expected ),
+                       $this->getSerializedSiteList( $actual ),
+                       $message
+               );
+       }
+
+       public function provideImportFromXML() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               '<sites></sites>',
+                               [],
+                       ],
+                       'no sites' => [
+                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
+                               [],
+                       ],
+                       'minimal' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                               '</sites>',
+                               [ $foo ],
+                       ],
+                       'full' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                                       '<site type="mediawiki">' .
+                                               '<source>meta.wikimedia.org</source>' .
+                                               '<globalid>dewiki</globalid>' .
+                                               '<localid type="interwiki">wikipedia</localid>' .
+                                               '<localid type="equivalent">de</localid>' .
+                                               '<group>wikipedia</group>' .
+                                               '<forward/>' .
+                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
+                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme, $dewiki ],
+                       ],
+                       'skip' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site><barf>Foo</barf></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<silly>boop!</silly>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme ],
+                               1
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideImportFromXML
+        */
+       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
+               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
+               $importer->importFromXML( $xml );
+       }
+
+       public function testImportFromXML_malformed() {
+               $this->setExpectedException( Exception::class );
+
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( 'THIS IS NOT XML' );
+       }
+
+       public function testImportFromFile() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
+
+               $file = __DIR__ . '/SiteImporterTest.xml';
+               $importer->importFromFile( $file );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @return array[]
+        */
+       private function getSerializedSiteList( $sites ) {
+               $serialized = [];
+
+               foreach ( $sites as $site ) {
+                       $key = $site->getGlobalId();
+                       $data = unserialize( $site->serialize() );
+
+                       $serialized[$key] = $data;
+               }
+
+               return $serialized;
+       }
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.xml b/tests/phpunit/includes/site/SiteImporterTest.xml
new file mode 100644 (file)
index 0000000..720b1fa
--- /dev/null
@@ -0,0 +1,19 @@
+<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
+       <site><globalid>Foo</globalid></site>
+       <site>
+               <globalid>acme.com</globalid>
+               <localid type="interwiki">acme</localid>
+               <group>Test</group>
+               <path type="link">http://acme.com/</path>
+       </site>
+       <site type="mediawiki">
+               <source>meta.wikimedia.org</source>
+               <globalid>dewiki</globalid>
+               <localid type="interwiki">wikipedia</localid>
+               <localid type="equivalent">de</localid>
+               <group>wikipedia</group>
+               <forward/>
+               <path type="link">http://de.wikipedia.org/w/</path>
+               <path type="page_path">http://de.wikipedia.org/wiki/</path>
+       </site>
+</sites>
index e881611..338a86e 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -9,12 +10,16 @@ class ContribsPagerTest extends MediaWikiTestCase {
        /** @var ContribsPager */
        private $pager;
 
+       /** @var LinkRenderer */
+       private $linkRenderer;
+
        function setUp() {
+               $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                $context = new RequestContext();
                $this->pager = new ContribsPager( $context, [
                        'start' => '2017-01-01',
                        'end' => '2017-02-02',
-               ] );
+               ], $this->linkRenderer );
 
                parent::setUp();
        }
@@ -127,7 +132,7 @@ class ContribsPagerTest extends MediaWikiTestCase {
                $pager = new ContribsPager( new RequestContext(), [
                        'start' => '',
                        'end' => '',
-               ] );
+               ], $this->linkRenderer );
 
                /** @var ContribsPager $pager */
                $pager = TestingAccessWrapper::newFromObject( $pager );
@@ -150,7 +155,7 @@ class ContribsPagerTest extends MediaWikiTestCase {
                        'target' => '116.17.184.5/32',
                        'start' => '',
                        'end' => '',
-               ] );
+               ], $this->linkRenderer );
 
                /** @var ContribsPager $pager */
                $pager = TestingAccessWrapper::newFromObject( $pager );
index 10c6d04..2f7b40d 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
+
 /**
  * Test class for ImageListPagerTest class.
  *
@@ -15,7 +18,8 @@ class ImageListPagerTest extends MediaWikiTestCase {
         * @covers ImageListPager::formatValue
         */
        public function testFormatValuesThrowException() {
-               $page = new ImageListPager( RequestContext::getMain() );
+               $page = new ImageListPager( RequestContext::getMain(), null, '', false, false,
+                       MediaWikiServices::getInstance()->getLinkRenderer() );
                $page->formatValue( 'invalid_field', 'invalid_value' );
        }
 }
index 642ae3e..28e7699 100644 (file)
@@ -12,7 +12,7 @@ use Wikimedia\TestingAccessWrapper;
 class SpecialWatchlistTest extends SpecialPageTestBase {
        public function setUp() {
                parent::setUp();
-
+               $this->tablesUsed = [ 'watchlist' ];
                $this->setTemporaryHook(
                        'ChangesListSpecialPageQuery',
                        null
index c0eadac..a029150 100644 (file)
@@ -12,6 +12,17 @@ use Wikimedia\TestingAccessWrapper;
  */
 class BlockListPagerTest extends MediaWikiTestCase {
 
+       /**
+        * @var LinkRenderer
+        */
+       private $linkRenderer;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+       }
+
        /**
         * @covers ::formatValue
         * @dataProvider formatValueEmptyProvider
@@ -30,7 +41,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
                $expected = $expected ?? MWTimestamp::getInstance()->format( 'H:i, j F Y' );
 
                $row = $row ?: new stdClass;
-               $pager = new BlockListPager( new SpecialPage(),  [] );
+               $pager = new BlockListPager( new SpecialPage(),  [], $this->linkRenderer );
                $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
                $wrappedPager->mCurrentRow = $row;
 
@@ -118,7 +129,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
                        'wgScript' => '/w/index.php',
                ] );
 
-               $pager = new BlockListPager( new SpecialPage(),  [] );
+               $pager = new BlockListPager( new SpecialPage(),  [], $this->linkRenderer );
 
                $row = (object)[
                        'ipb_id' => 0,
@@ -198,7 +209,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
                        'ipb_sitewide' => 1,
                        'ipb_timestamp' => $this->db->timestamp( wfTimestamp( TS_MW ) ),
                ];
-               $pager = new BlockListPager( new SpecialPage(),  [] );
+               $pager = new BlockListPager( new SpecialPage(),  [], $this->linkRenderer );
                $pager->preprocessResults( [ $row ] );
 
                foreach ( $links as $link ) {
@@ -211,7 +222,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
                        'by_user_name' => 'Admin',
                        'ipb_sitewide' => 1,
                ];
-               $pager = new BlockListPager( new SpecialPage(),  [] );
+               $pager = new BlockListPager( new SpecialPage(),  [], $this->linkRenderer );
                $pager->preprocessResults( [ $row ] );
 
                $this->assertObjectNotHasAttribute( 'ipb_restrictions', $row );
@@ -237,7 +248,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
 
                $result = $this->db->select( 'ipblocks', [ '*' ], [ 'ipb_id' => $block->getId() ] );
 
-               $pager = new BlockListPager( new SpecialPage(),  [] );
+               $pager = new BlockListPager( new SpecialPage(),  [], $this->linkRenderer );
                $pager->preprocessResults( $result );
 
                $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
@@ -248,7 +259,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
                $restriction = $restrictions[0];
                $this->assertEquals( $page->getId(), $restriction->getValue() );
                $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleID() );
-               $this->assertEquals( $title->getDBKey(), $restriction->getTitle()->getDBKey() );
+               $this->assertEquals( $title->getDBkey(), $restriction->getTitle()->getDBkey() );
                $this->assertEquals( $title->getNamespace(), $restriction->getTitle()->getNamespace() );
 
                // Delete the block and the restrictions.
diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
new file mode 100644 (file)
index 0000000..492b250
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ */
+class ZipDirectoryReaderTest extends MediaWikiIntegrationTestCase {
+
+       protected $zipDir;
+       protected $entries;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->zipDir = __DIR__ . '/../../data/zip';
+       }
+
+       function zipCallback( $entry ) {
+               $this->entries[] = $entry;
+       }
+
+       function readZipAssertError( $file, $error, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+       }
+
+       function readZipAssertSuccess( $file, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->isOK(), $assertMessage );
+       }
+
+       public function testEmpty() {
+               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+       }
+
+       public function testMultiDisk0() {
+               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+                       'Split zip error' );
+       }
+
+       public function testNoSignature() {
+               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+                       'No signature should give "wrong format" error' );
+       }
+
+       public function testSimple() {
+               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+               $this->assertEquals( $this->entries, [ [
+                       'name' => 'Class.class',
+                       'mtime' => '20010115000000',
+                       'size' => 1,
+               ] ] );
+       }
+
+       public function testBadCentralEntrySignature() {
+               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+                       'Bad central entry error' );
+       }
+
+       public function testTrailingBytes() {
+               // Due to T40432 this is now zip-wrong-format instead of zip-bad
+               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
+                       'Trailing bytes error' );
+       }
+
+       public function testWrongCDStart() {
+               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+                       'Wrong CD start disk error' );
+       }
+
+       public function testCentralDirectoryGap() {
+               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+                       'CD gap error' );
+       }
+
+       public function testCentralDirectoryTruncated() {
+               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+                       'CD truncated error (should hit unpack() overrun)' );
+       }
+
+       public function testLooksLikeZip64() {
+               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+                       'A file which looks like ZIP64 but isn\'t, should give error' );
+       }
+}
index 6544544..6e236cd 100755 (executable)
@@ -34,6 +34,13 @@ class PHPUnitMaintClass extends Maintenance {
                );
        }
 
+       public function setup() {
+               parent::setup();
+
+               require_once __DIR__ . '/../common/TestSetup.php';
+               TestSetup::snapshotGlobals();
+       }
+
        public function finalSetup() {
                parent::finalSetup();
 
index d340221..2784abd 100644 (file)
@@ -18,7 +18,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
        }
 
        protected function setUp() {
-               global $IP, $messageMemc, $wgMemc, $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory,
+               global $IP, $wgMemc, $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory,
                        $wgParserCacheType, $wgNamespaceAliases, $wgNamespaceProtection;
 
                $tmpDir = $this->getNewTempDirectory();
@@ -60,7 +60,6 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
                $wgParserCacheType = CACHE_NONE;
                DeferredUpdates::clearPendingUpdates();
                $wgMemc = ObjectCache::getLocalClusterInstance();
-               $messageMemc = wfGetMessageCacheStorage();
 
                RequestContext::resetMain();
                $context = RequestContext::getMain();
diff --git a/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 9dff2cc..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\MainSlotRoleHandler;
-use MediaWikiUnitTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\MainSlotRoleHandler
- */
-class MainSlotRoleHandlerTest extends MediaWikiUnitTestCase {
-
-       private function makeTitleObject( $ns ) {
-               /** @var Title|MockObject $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $title->method( 'getNamespace' )
-                       ->willReturn( $ns );
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new MainSlotRoleHandler( [] );
-               $this->assertSame( 'main', $handler->getRole() );
-               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
-        */
-       public function testFetDefaultModel() {
-               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
-
-               // For the main handler, the namespace determins the default model
-               $titleMain = $this->makeTitleObject( NS_MAIN );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
-
-               $title100 = $this->makeTitleObject( 100 );
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               // For the main handler, (nearly) all models are allowed
-               $title = $this->makeTitleObject( NS_MAIN );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               $this->assertTrue( $handler->supportsArticleCount() );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRecordTest.php b/tests/phpunit/unit/includes/Revision/SlotRecordTest.php
deleted file mode 100644 (file)
index aab430a..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Revision\IncompleteRevisionException;
-use MediaWiki\Revision\SlotRecord;
-use MediaWiki\Revision\SuppressedDataException;
-use MediaWikiUnitTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Revision\SlotRecord
- */
-class SlotRecordTest extends MediaWikiUnitTestCase {
-
-       private function makeRow( $data = [] ) {
-               $data = $data + [
-                       'slot_id' => 1234,
-                       'slot_content_id' => 33,
-                       'content_size' => '5',
-                       'content_sha1' => 'someHash',
-                       'content_address' => 'tt:456',
-                       'model_name' => CONTENT_MODEL_WIKITEXT,
-                       'format_name' => CONTENT_FORMAT_WIKITEXT,
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '1',
-                       'role_name' => 'myRole',
-               ];
-               return (object)$data;
-       }
-
-       public function testCompleteConstruction() {
-               $row = $this->makeRow();
-               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasContentId() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertTrue( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 5, $record->getSize() );
-               $this->assertSame( 'someHash', $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 1, $record->getOrigin() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testConstructionDeferred() {
-               $row = $this->makeRow( [
-                       'content_size' => null, // to be computed
-                       'content_sha1' => null, // to be computed
-                       'format_name' => function () {
-                               return CONTENT_FORMAT_WIKITEXT;
-                       },
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '2',
-                       'slot_content_id' => function () {
-                               return null;
-                       },
-               ] );
-
-               $content = function () {
-                       return new WikitextContent( 'A' );
-               };
-
-               $record = new SlotRecord( $row, $content );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testNewUnsaved() {
-               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
-               $this->assertFalse( $record->hasAddress() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->hasRevision() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertFalse( $record->hasOrigin() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function provideInvalidConstruction() {
-               yield 'both null' => [ null, null ];
-               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
-               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
-               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
-               yield 'null content' => [ (object)[], null ];
-       }
-
-       /**
-        * @dataProvider provideInvalidConstruction
-        */
-       public function testInvalidConstruction( $row, $content ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new SlotRecord( $row, $content );
-       }
-
-       public function testGetContentId_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getContentId();
-       }
-
-       public function testGetAddress_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getAddress();
-       }
-
-       public function provideIncomplete() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               yield 'unsaved' => [ $unsaved ];
-
-               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $inherited = SlotRecord::newInherited( $parent );
-               yield 'inherited' => [ $inherited ];
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetRevision_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getRevision();
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetOrigin_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getOrigin();
-       }
-
-       public function provideHashStability() {
-               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
-               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
-       }
-
-       /**
-        * @dataProvider provideHashStability
-        */
-       public function testHashStability( $text, $hash ) {
-               // Changing the output of the hash function will break things horribly!
-
-               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
-               $this->assertSame( $hash, $record->getSha1() );
-       }
-
-       public function testHashComputed() {
-               $row = $this->makeRow();
-               $row->content_sha1 = '';
-
-               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
-               $this->assertNotEmpty( $rec->getSha1() );
-       }
-
-       public function testNewWithSuppressedContent() {
-               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $output = SlotRecord::newWithSuppressedContent( $input );
-
-               $this->setExpectedException( SuppressedDataException::class );
-               $output->getContent();
-       }
-
-       public function testNewInherited() {
-               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
-               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, before saving revision meta-data.
-               $inherited = SlotRecord::newInherited( $parent );
-
-               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
-               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
-               $this->assertSame( $parent->getContent(), $inherited->getContent() );
-               $this->assertTrue( $inherited->isInherited() );
-               $this->assertTrue( $inherited->hasOrigin() );
-               $this->assertFalse( $inherited->hasRevision() );
-
-               // make sure we didn't mess with the internal state of $parent
-               $this->assertFalse( $parent->isInherited() );
-               $this->assertSame( 7, $parent->getRevision() );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved(
-                       10,
-                       $inherited->getContentId(),
-                       $inherited->getAddress(),
-                       $inherited
-               );
-               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
-               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
-               $this->assertSame( $parent->getContent(), $saved->getContent() );
-               $this->assertTrue( $saved->isInherited() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertSame( 10, $saved->getRevision() );
-
-               // make sure we didn't mess with the internal state of $parent or $inherited
-               $this->assertSame( 7, $parent->getRevision() );
-               $this->assertFalse( $inherited->hasRevision() );
-       }
-
-       public function testNewSaved() {
-               // This would happen while doing an edit, before saving revision meta-data.
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
-               $this->assertFalse( $saved->isInherited() );
-               $this->assertTrue( $saved->hasOrigin() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertTrue( $saved->hasAddress() );
-               $this->assertTrue( $saved->hasContentId() );
-               $this->assertSame( 'theNewAddress', $saved->getAddress() );
-               $this->assertSame( 20, $saved->getContentId() );
-               $this->assertSame( 'A', $saved->getContent()->getText() );
-               $this->assertSame( 10, $saved->getRevision() );
-               $this->assertSame( 10, $saved->getOrigin() );
-
-               // make sure we didn't mess with the internal state of $unsaved
-               $this->assertFalse( $unsaved->hasAddress() );
-               $this->assertFalse( $unsaved->hasContentId() );
-               $this->assertFalse( $unsaved->hasRevision() );
-       }
-
-       public function provideNewSaved_LogicException() {
-               $freshRow = $this->makeRow( [
-                       'content_id' => 10,
-                       'content_address' => 'address:1',
-                       'slot_origin' => 1,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
-               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
-               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
-               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
-               $inheritedRow = $this->makeRow( [
-                       'content_id' => null,
-                       'content_address' => null,
-                       'slot_origin' => 0,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
-               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_LogicException
-        */
-       public function testNewSaved_LogicException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( LogicException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideNewSaved_InvalidArgumentException() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
-               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
-               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_InvalidArgumentException
-        */
-       public function testNewSaved_InvalidArgumentException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideHasSameContent() {
-               $fail = function () {
-                       self::fail( 'There should be no need to actually load the content.' );
-               };
-
-               $a100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a1b = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100null = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => null,
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a2 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $b100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'B',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a200a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 200,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100x1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-x',
-                                       'content_address' => 'xxx:x1',
-                               ]
-                       ),
-                       $fail
-               );
-
-               yield 'same instance' => [ $a100a1, $a100a1, true ];
-               yield 'no address' => [ $a100a1, $a100null, true ];
-               yield 'same address' => [ $a100a1, $a100a1b, true ];
-               yield 'different address' => [ $a100a1, $a100a2, true ];
-               yield 'different model' => [ $a100a1, $b100a1, false ];
-               yield 'different size' => [ $a100a1, $a200a1, false ];
-               yield 'different hash' => [ $a100a1, $a100x1, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        */
-       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
-               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
-               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/WikiReferenceTest.php b/tests/phpunit/unit/includes/WikiReferenceTest.php
deleted file mode 100644 (file)
index a4aae86..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-/**
- * @covers WikiReference
- */
-class WikiReferenceTest extends MediaWikiUnitTestCase {
-
-       public function provideGetDisplayName() {
-               return [
-                       'http' => [ 'foo.bar', 'http://foo.bar' ],
-                       'https' => [ 'foo.bar', 'http://foo.bar' ],
-
-                       // apparently, this is the expected behavior
-                       'invalid' => [ 'purple kittens', 'purple kittens' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetDisplayName
-        */
-       public function testGetDisplayName( $expected, $canonicalServer ) {
-               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
-               $this->assertEquals( $expected, $reference->getDisplayName() );
-       }
-
-       public function testGetCanonicalServer() {
-               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
-               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
-       }
-
-       public function provideGetCanonicalUrl() {
-               return [
-                       'no fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               'https://acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               'https://acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               'https://acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               'https://acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        */
-       public function testGetCanonicalUrl(
-               $expected, $canonicalServer, $server, $path, $page, $fragmentId
-       ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        * @note getUrl is an alias for getCanonicalUrl
-        */
-       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
-       }
-
-       public function provideGetFullUrl() {
-               return [
-                       'no fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               '//acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               '//acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               '//acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               '//acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFullUrl
-        */
-       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
deleted file mode 100644 (file)
index 1a8b585..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @covers DifferenceEngineSlotDiffRenderer
- */
-class DifferenceEngineSlotDiffRendererTest extends \MediaWikiUnitTestCase {
-
-       public function testGetDiff() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
-               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
-               $this->assertEquals( 'xxx|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( null, $newContent );
-               $this->assertEquals( '|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
-               $this->assertEquals( 'xxx|', $diff );
-       }
-
-       public function testAddModules() {
-               $output = $this->getMockBuilder( OutputPage::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'addModules' ] )
-                       ->getMock();
-               $output->expects( $this->once() )
-                       ->method( 'addModules' )
-                       ->with( 'foo' );
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $slotDiffRenderer->addModules( $output );
-       }
-
-       public function testGetExtraCacheKeys() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
-               $this->assertSame( [ 'foo' ], $extraCacheKeys );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php
deleted file mode 100644 (file)
index f778115..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-use Wikimedia\Assert\ParameterTypeException;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers SlotDiffRenderer
- */
-class SlotDiffRendererTest extends \MediaWikiUnitTestCase {
-
-       /**
-        * @dataProvider provideNormalizeContents
-        */
-       public function testNormalizeContents(
-               $oldContent, $newContent, $allowedClasses,
-               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
-       ) {
-               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
-                       ->getMock();
-               try {
-                       // __call needs help deciding which parameter to take by reference
-                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
-                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
-                       $this->assertEquals( $expectedOldContent, $oldContent );
-                       $this->assertEquals( $expectedNewContent, $newContent );
-               } catch ( Exception $e ) {
-                       if ( !$expectedExceptionClass ) {
-                               throw $e;
-                       }
-                       $this->assertInstanceOf( $expectedExceptionClass, $e );
-               }
-       }
-
-       public function provideNormalizeContents() {
-               return [
-                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
-                       'left null' => [
-                               null, new WikitextContent( 'abc' ), null,
-                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
-                       ],
-                       'right null' => [
-                               new WikitextContent( 'def' ), null, null,
-                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (subclass)' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (null)' => [
-                               new WikitextContent( 'abc' ), null, TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter failure (left)' => [
-                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter failure (right)' => [
-                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter (array syntax)' => [
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
-                       ],
-                       'type filter failure (array syntax)' => [
-                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               null, null, ParameterTypeException::class,
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php
deleted file mode 100644 (file)
index 6084601..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-class FileBackendDBRepoWrapperTest extends MediaWikiUnitTestCase {
-       protected $backendName = 'foo-backend';
-       protected $repoName = 'pureTestRepo';
-
-       /**
-        * @dataProvider getBackendPathsProvider
-        * @covers FileBackendDBRepoWrapper::getBackendPaths
-        */
-       public function testGetBackendPaths(
-               $mocks,
-               $latest,
-               $dbReadsExpected,
-               $dbReturnValue,
-               $originalPath,
-               $expectedBackendPath,
-               $message ) {
-               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
-
-               $dbMock->expects( $dbReadsExpected )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( $dbReturnValue ) );
-
-               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
-
-               $this->assertEquals(
-                       $expectedBackendPath,
-                       $newPaths[0],
-                       $message );
-       }
-
-       public function getBackendPathsProvider() {
-               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
-               $mocksForCaching = $this->getMocks();
-
-               return [
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Public path translated correctly',
-                       ],
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'LRU cache leveraged',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Latest obtained',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-deleted/f/o/foobar.jpg',
-                               $prefix . '-original/f/o/o/foobar',
-                               'Deleted path translated correctly',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               null,
-                               $prefix . '-public/b/a/baz.jpg',
-                               $prefix . '-public/b/a/baz.jpg',
-                               'Path left untouched if no sha1 can be found',
-                       ],
-               ];
-       }
-
-       /**
-        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
-        */
-       public function testGetFileContentsMulti() {
-               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
-
-               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
-               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-public/f/o/foobar.jpg';
-
-               $dbMock->expects( $this->once() )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
-
-               $backendMock->expects( $this->once() )
-                       ->method( 'getFileContentsMulti' )
-                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
-
-               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
-
-               $this->assertEquals(
-                       [ $filenamePath => 'foo' ],
-                       $result,
-                       'File contents paths translated properly'
-               );
-       }
-
-       protected function getMocks() {
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
-                       ->disableOriginalClone()
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $backendMock = $this->getMockBuilder( FSFileBackend::class )
-                       ->setConstructorArgs( [ [
-                                       'name' => $this->backendName,
-                                       'wikiId' => wfWikiID()
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
-                       ->setMethods( [ 'getDB' ] )
-                       ->setConstructorArgs( [ [
-                                       'backend' => $backendMock,
-                                       'repoName' => $this->repoName,
-                                       'dbHandleFactory' => null
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
-
-               return [ $dbMock, $backendMock, $wrapperMock ];
-       }
-}
diff --git a/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php
deleted file mode 100644 (file)
index 365c140..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-/**
- * @todo Could use a test of extended XMP segments. Hard to find programs that
- * create example files, and creating my own in vim propbably wouldn't
- * serve as a very good "test". (Adobe photoshop probably creates such files
- * but it costs money). The implementation of it currently in MediaWiki is based
- * solely on reading the standard, without any real world test files.
- *
- * @group Media
- * @covers JpegMetadataExtractor
- */
-class JpegMetadataExtractorTest extends MediaWikiUnitTestCase {
-
-       protected $filePath;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->filePath = __DIR__ . '/../../../data/media/';
-       }
-
-       /**
-        * We also use this test to test padding bytes don't
-        * screw stuff up
-        *
-        * @param string $file Filename
-        *
-        * @dataProvider provideUtf8Comment
-        */
-       public function testUtf8Comment( $file ) {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
-               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
-       }
-
-       public static function provideUtf8Comment() {
-               return [
-                       [ 'jpeg-comment-utf.jpg' ],
-                       [ 'jpeg-padding-even.jpg' ],
-                       [ 'jpeg-padding-odd.jpg' ],
-               ];
-       }
-
-       /** The file is iso-8859-1, but it should get auto converted */
-       public function testIso88591Comment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
-               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
-       }
-
-       /** Comment values that are non-textual (random binary junk) should not be shown.
-        * The example test file has a comment with a 0x5 byte in it which is a control character
-        * and considered binary junk for our purposes.
-        */
-       public function testBinaryCommentStripped() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
-               $this->assertEmpty( $res['COM'] );
-       }
-
-       /* Very rarely a file can have multiple comments.
-        *   Order of comments is based on order inside the file.
-        */
-       public function testMultipleComment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
-               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
-       }
-
-       public function testXMPExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testPSIRExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = '50686f746f73686f7020332e30003842494d04040000000'
-                       . '000181c02190004746573741c02190003666f6f1c020000020004';
-               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
-       }
-
-       public function testXMPExtractionAltAppId() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testIPTCHashComparisionNoHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-no-hash', $res );
-       }
-
-       public function testIPTCHashComparisionBadHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-bad-hash', $res );
-       }
-
-       public function testIPTCHashComparisionGoodHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-good-hash', $res );
-       }
-
-       public function testExifByteOrder() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
-               $expected = 'BE';
-               $this->assertEquals( $expected, $res['byteOrder'] );
-       }
-
-       public function testInfiniteRead() {
-               // test file truncated right after a segment, which previously
-               // caused an infinite loop looking for the next segment byte.
-               // Should get past infinite loop and throw in wfUnpack()
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
-       }
-
-       public function testInfiniteRead2() {
-               // test file truncated after a segment's marker and size, which
-               // would cause a seek past end of file. Seek past end of file
-               // doesn't actually fail, but prevents further reading and was
-               // devolving into the previous case (testInfiniteRead).
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
-       }
-}
diff --git a/tests/phpunit/unit/includes/parser/ParserFactoryTest.php b/tests/phpunit/unit/includes/parser/ParserFactoryTest.php
deleted file mode 100644 (file)
index f1e48c7..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @covers ParserFactory
- */
-class ParserFactoryTest extends MediaWikiUnitTestCase {
-       use FactoryArgTestTrait;
-
-       protected static function getFactoryClass() {
-               return ParserFactory::class;
-       }
-
-       protected static function getInstanceClass() {
-               return Parser::class;
-       }
-
-       protected static function getFactoryMethodName() {
-               return 'create';
-       }
-
-       protected static function getExtraClassArgCount() {
-               // The parser factory itself is passed to the parser
-               return 1;
-       }
-
-       protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
-               if ( $param->getPosition() === 0 ) {
-                       return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ];
-               }
-               return [];
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php
deleted file mode 100644 (file)
index d426306..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-use MediaWiki\Site\MediaWikiPageNameNormalizer;
-
-/**
- * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @since 1.27
- *
- * @group Site
- * @group medium
- *
- * @author Marius Hoch
- */
-class MediaWikiPageNameNormalizerTest extends MediaWikiUnitTestCase {
-
-       /**
-        * @dataProvider normalizePageTitleProvider
-        */
-       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
-               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
-
-               $normalizer = new MediaWikiPageNameNormalizer(
-                       new MediaWikiPageNameNormalizerTestMockHttp()
-               );
-
-               $this->assertSame(
-                       $expected,
-                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
-               );
-       }
-
-       public function normalizePageTitleProvider() {
-               // Response are taken from wikidata and kkwiki using the following API request
-               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
-               return [
-                       'universe (Q1)' => [
-                               'Q1',
-                               'Q1',
-                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
-                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
-                       ],
-                       'Q404 redirects to Q395' => [
-                               'Q395',
-                               'Q404',
-                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
-                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
-                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
-                       ],
-                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
-                               'Д',
-                               'D',
-                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
-                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
-                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
-                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
-                               . '"lastrevid":2373618,"length":3501}}}}'
-                       ],
-                       'there is no Q0' => [
-                               false,
-                               'Q0',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
-                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
-                       ],
-                       'invalid title' => [
-                               false,
-                               '{{',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
-                               . '"invalidreason":"The requested page title contains invalid '
-                               . 'characters: \"{\".","invalid":""}}}}'
-                       ],
-                       'error on get' => [ false, 'ABC', false ]
-               ];
-       }
-
-}
-
-/**
- * @private
- * @see Http
- */
-class MediaWikiPageNameNormalizerTestMockHttp extends Http {
-
-       /**
-        * @var mixed
-        */
-       public static $response;
-
-       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
-
-               return self::$response;
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/SiteExporterTest.php b/tests/phpunit/unit/includes/site/SiteExporterTest.php
deleted file mode 100644 (file)
index dcf51ac..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteExporter
- *
- * @author Daniel Kinzler
- */
-class SiteExporterTest extends MediaWikiUnitTestCase {
-
-       public function testConstructor_InvalidArgument() {
-               $this->setExpectedException( InvalidArgumentException::class );
-
-               new SiteExporter( 'Foo' );
-       }
-
-       public function testExportSites() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( [ $foo, $acme ] );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $this->assertContains( '<sites ', $xml );
-               $this->assertContains( '<site>', $xml );
-               $this->assertContains( '<globalid>Foo</globalid>', $xml );
-               $this->assertContains( '</site>', $xml );
-               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
-               $this->assertContains( '<group>Test</group>', $xml );
-               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
-               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
-               $this->assertContains( '</sites>', $xml );
-
-               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
-               $xsdFile = __DIR__ . '/../../../../../docs/sitelist-1.0.xsd';
-               $xsdData = file_get_contents( $xsdFile );
-
-               $document = new DOMDocument();
-               $document->loadXML( $xml, LIBXML_NONET );
-               $document->schemaValidateSource( $xsdData );
-       }
-
-       private function newSiteStore( SiteList $sites ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
-                               foreach ( $moreSites as $site ) {
-                                       $sites->setSite( $site );
-                               }
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               return $store;
-       }
-
-       public function provideRoundTrip() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               new SiteList()
-                       ],
-
-                       'some' => [
-                               new SiteList( [ $foo, $acme, $dewiki ] ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideRoundTrip()
-        */
-       public function testRoundTrip( SiteList $sites ) {
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( $sites );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $actualSites = new SiteList();
-               $store = $this->newSiteStore( $actualSites );
-
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( $xml );
-
-               $this->assertEquals( $sites, $actualSites );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.php b/tests/phpunit/unit/includes/site/SiteImporterTest.php
deleted file mode 100644 (file)
index d4e4103..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteImporter
- *
- * @author Daniel Kinzler
- */
-class SiteImporterTest extends MediaWikiUnitTestCase {
-
-       private function newSiteImporter( array $expectedSites, $errorCount ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
-                               $this->assertSitesEqual( $expectedSites, $sites );
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
-               $errorHandler->expects( $this->exactly( $errorCount ) )
-                       ->method( 'error' );
-
-               $importer = new SiteImporter( $store );
-               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
-
-               return $importer;
-       }
-
-       public function assertSitesEqual( $expected, $actual, $message = '' ) {
-               $this->assertEquals(
-                       $this->getSerializedSiteList( $expected ),
-                       $this->getSerializedSiteList( $actual ),
-                       $message
-               );
-       }
-
-       public function provideImportFromXML() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               '<sites></sites>',
-                               [],
-                       ],
-                       'no sites' => [
-                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
-                               [],
-                       ],
-                       'minimal' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                               '</sites>',
-                               [ $foo ],
-                       ],
-                       'full' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                                       '<site type="mediawiki">' .
-                                               '<source>meta.wikimedia.org</source>' .
-                                               '<globalid>dewiki</globalid>' .
-                                               '<localid type="interwiki">wikipedia</localid>' .
-                                               '<localid type="equivalent">de</localid>' .
-                                               '<group>wikipedia</group>' .
-                                               '<forward/>' .
-                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
-                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme, $dewiki ],
-                       ],
-                       'skip' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site><barf>Foo</barf></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<silly>boop!</silly>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme ],
-                               1
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideImportFromXML
-        */
-       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
-               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
-               $importer->importFromXML( $xml );
-       }
-
-       public function testImportFromXML_malformed() {
-               $this->setExpectedException( Exception::class );
-
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( 'THIS IS NOT XML' );
-       }
-
-       public function testImportFromFile() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
-
-               $file = __DIR__ . '/SiteImporterTest.xml';
-               $importer->importFromFile( $file );
-       }
-
-       /**
-        * @param Site[] $sites
-        *
-        * @return array[]
-        */
-       private function getSerializedSiteList( $sites ) {
-               $serialized = [];
-
-               foreach ( $sites as $site ) {
-                       $key = $site->getGlobalId();
-                       $data = unserialize( $site->serialize() );
-
-                       $serialized[$key] = $data;
-               }
-
-               return $serialized;
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.xml b/tests/phpunit/unit/includes/site/SiteImporterTest.xml
deleted file mode 100644 (file)
index 720b1fa..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
-       <site><globalid>Foo</globalid></site>
-       <site>
-               <globalid>acme.com</globalid>
-               <localid type="interwiki">acme</localid>
-               <group>Test</group>
-               <path type="link">http://acme.com/</path>
-       </site>
-       <site type="mediawiki">
-               <source>meta.wikimedia.org</source>
-               <globalid>dewiki</globalid>
-               <localid type="interwiki">wikipedia</localid>
-               <localid type="equivalent">de</localid>
-               <group>wikipedia</group>
-               <forward/>
-               <path type="link">http://de.wikipedia.org/w/</path>
-               <path type="page_path">http://de.wikipedia.org/wiki/</path>
-       </site>
-</sites>
diff --git a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php
deleted file mode 100644 (file)
index be7e224..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * @covers ZipDirectoryReader
- * NOTE: this test is more like an integration test than a unit test
- */
-class ZipDirectoryReaderTest extends MediaWikiUnitTestCase {
-
-       protected $zipDir;
-       protected $entries;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->zipDir = __DIR__ . '/../../../data/zip';
-       }
-
-       function zipCallback( $entry ) {
-               $this->entries[] = $entry;
-       }
-
-       function readZipAssertError( $file, $error, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
-       }
-
-       function readZipAssertSuccess( $file, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->isOK(), $assertMessage );
-       }
-
-       public function testEmpty() {
-               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
-       }
-
-       public function testMultiDisk0() {
-               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
-                       'Split zip error' );
-       }
-
-       public function testNoSignature() {
-               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
-                       'No signature should give "wrong format" error' );
-       }
-
-       public function testSimple() {
-               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
-               $this->assertEquals( $this->entries, [ [
-                       'name' => 'Class.class',
-                       'mtime' => '20010115000000',
-                       'size' => 1,
-               ] ] );
-       }
-
-       public function testBadCentralEntrySignature() {
-               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
-                       'Bad central entry error' );
-       }
-
-       public function testTrailingBytes() {
-               // Due to T40432 this is now zip-wrong-format instead of zip-bad
-               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
-                       'Trailing bytes error' );
-       }
-
-       public function testWrongCDStart() {
-               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
-                       'Wrong CD start disk error' );
-       }
-
-       public function testCentralDirectoryGap() {
-               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
-                       'CD gap error' );
-       }
-
-       public function testCentralDirectoryTruncated() {
-               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
-                       'CD truncated error (should hit unpack() overrun)' );
-       }
-
-       public function testLooksLikeZip64() {
-               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
-                       'A file which looks like ZIP64 but isn\'t, should give error' );
-       }
-}