Merge "resourceloader: Drop support for low Suhosin 'max_value_length' values"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 4 Sep 2019 21:53:44 +0000 (21:53 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 4 Sep 2019 21:53:44 +0000 (21:53 +0000)
472 files changed:
.mailmap
.phan/config.php
.phan/internal_stubs/dom.phan_php [new file with mode: 0644]
.phan/internal_stubs/pgsql.phan_php [new file with mode: 0644]
.phan/stubs/excimer.php
.phan/stubs/mail.php
CREDITS
INSTALL
RELEASE-NOTES-1.34
composer.json
includes/DefaultSettings.php
includes/EditPage.php
includes/FauxRequest.php
includes/FileDeleteForm.php
includes/GlobalFunctions.php
includes/LinkFilter.php
includes/Linker.php
includes/MediaWiki.php
includes/MovePage.php
includes/Navigation/PrevNextNavigationRenderer.php
includes/OutputPage.php
includes/Permissions/PermissionManager.php
includes/ProxyLookup.php
includes/Rest/EntryPoint.php
includes/Rest/HeaderContainer.php
includes/Rest/Router.php
includes/Rest/SimpleHandler.php
includes/Revision.php
includes/Revision/MutableRevisionRecord.php
includes/Revision/RenderedRevision.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/Revision/RevisionStoreFactory.php
includes/ServiceWiring.php
includes/Setup.php
includes/Storage/PageUpdater.php
includes/Title.php
includes/TitleArray.php
includes/WebRequest.php
includes/WikiMap.php
includes/actions/HistoryAction.php
includes/actions/RevertAction.php
includes/api/ApiAuthManagerHelper.php
includes/api/ApiBase.php
includes/api/ApiBlock.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiErrorFormatter.php
includes/api/ApiExpandTemplates.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiImageRotate.php
includes/api/ApiImportReporter.php
includes/api/ApiMain.php
includes/api/ApiMessageTrait.php
includes/api/ApiMove.php
includes/api/ApiOpenSearch.php
includes/api/ApiPageSet.php
includes/api/ApiParse.php
includes/api/ApiQuery.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiQueryBlocks.php
includes/api/ApiQueryCategories.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryRevisionsBase.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiQueryUsers.php
includes/api/ApiStashEdit.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/es.json
includes/api/i18n/pt-br.json
includes/auth/AuthenticationRequest.php
includes/auth/RememberMeAuthenticationRequest.php
includes/auth/Throttler.php
includes/block/AbstractBlock.php
includes/block/BlockManager.php
includes/block/CompositeBlock.php
includes/block/Restriction/AbstractRestriction.php
includes/block/Restriction/PageRestriction.php
includes/block/Restriction/Restriction.php
includes/cache/CacheHelper.php
includes/cache/MessageCache.php
includes/cache/localisation/LCStoreCDB.php
includes/cache/localisation/LCStoreStaticArray.php
includes/cache/localisation/LocalisationCache.php
includes/changes/ChangesList.php
includes/changes/ChangesListBooleanFilterGroup.php
includes/changes/ChangesListFilterGroup.php
includes/changes/ChangesListStringOptionsFilterGroup.php
includes/changes/EnhancedChangesList.php
includes/changes/RecentChange.php
includes/changetags/ChangeTags.php
includes/changetags/ChangeTagsList.php
includes/config/EtcdConfig.php
includes/content/ContentHandler.php
includes/content/FileContentHandler.php
includes/content/TextContent.php
includes/content/TextContentHandler.php
includes/content/UnknownContentHandler.php
includes/content/WikitextContent.php
includes/context/ContextSource.php
includes/context/DerivativeContext.php
includes/context/RequestContext.php
includes/deferred/DeferredUpdates.php
includes/diff/ArrayDiffFormatter.php
includes/diff/DiffEngine.php
includes/diff/DiffOp.php
includes/diff/TextSlotDiffRenderer.php
includes/exception/MWException.php
includes/exception/MWExceptionRenderer.php
includes/export/DumpNamespaceFilter.php
includes/export/DumpPipeOutput.php
includes/export/ExportProgressFilter.php
includes/export/WikiExporter.php
includes/export/XmlDumpWriter.php
includes/filebackend/filejournal/DBFileJournal.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/LocalRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignAPIFile.php
includes/filerepo/file/LocalFile.php
includes/gallery/ImageGalleryBase.php
includes/historyblob/ConcatenatedGzipHistoryBlob.php
includes/historyblob/DiffHistoryBlob.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormElement.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLAutoCompleteSelectField.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/http/GuzzleHttpRequest.php
includes/http/HttpRequestFactory.php
includes/http/MWHttpRequest.php
includes/import/ImportableOldRevision.php
includes/import/ImportableOldRevisionImporter.php
includes/import/WikiImporter.php
includes/import/WikiRevision.php
includes/installer/CliInstaller.php
includes/installer/DatabaseInstaller.php
includes/installer/DatabaseUpdater.php
includes/installer/Installer.php
includes/installer/MysqlInstaller.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresInstaller.php
includes/installer/WebInstaller.php
includes/installer/WebInstallerOutput.php
includes/installer/i18n/es.json
includes/installer/i18n/nap.json
includes/installer/i18n/nl.json
includes/jobqueue/Job.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/ThumbnailRenderJob.php
includes/language/Message.php
includes/language/MessageLocalizer.php
includes/libs/HashRing.php
includes/libs/MappedIterator.php
includes/libs/XhprofData.php
includes/libs/filebackend/FSFileBackend.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendMultiWrite.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/MemoryFileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fileiteration/SwiftFileBackendList.php
includes/libs/filebackend/filejournal/FileJournal.php
includes/libs/filebackend/fileop/CopyFileOp.php
includes/libs/filebackend/fileop/CreateFileOp.php
includes/libs/filebackend/fileop/DeleteFileOp.php
includes/libs/filebackend/fileop/DescribeFileOp.php
includes/libs/filebackend/fileop/FileOp.php
includes/libs/filebackend/fileop/MoveFileOp.php
includes/libs/filebackend/fileop/StoreFileOp.php
includes/libs/http/MultiHttpClient.php
includes/libs/lockmanager/PostgreSqlLockManager.php
includes/libs/lockmanager/QuorumLockManager.php
includes/libs/mime/XmlTypeCheck.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/RedisBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/wancache/WANObjectCache.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/database/position/MySQLMasterPos.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/logging/BlockLogFormatter.php
includes/logging/LogEntryBase.php
includes/logging/LogPager.php
includes/logging/ManualLogEntry.php
includes/logging/PatrolLog.php
includes/media/ExifBitmapHandler.php
includes/media/FormatMetadata.php
includes/media/IPTC.php
includes/media/TiffHandler.php
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/page/CategoryPage.php
includes/page/ImageHistoryList.php
includes/page/ImagePage.php
includes/page/MovePageFactory.php
includes/page/Page.php
includes/page/PageArchive.php
includes/page/WikiPage.php
includes/parser/PPDPart_Hash.php
includes/parser/PPDStack.php
includes/parser/PPDStackElement_Hash.php
includes/parser/PPFrame.php
includes/parser/PPFrame_DOM.php
includes/parser/PPNode_DOM.php
includes/parser/Parser.php
includes/parser/ParserDiffTest.php
includes/parser/Preprocessor.php
includes/parser/Preprocessor_Hash.php
includes/password/LayeredParameterizedPassword.php
includes/poolcounter/PoolCounterRedis.php
includes/preferences/DefaultPreferencesFactory.php
includes/profiler/ProfilerExcimer.php
includes/profiler/output/ProfilerOutputDump.php
includes/rcfeed/FormattedRCFeed.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/DerivativeResourceLoaderContext.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderOOUIImageModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/revisiondelete/RevDelArchivedFileItem.php
includes/revisiondelete/RevDelList.php
includes/revisiondelete/RevDelRevisionItem.php
includes/search/SearchEngine.php
includes/search/SearchResultSetTrait.php
includes/session/PHPSessionHandler.php
includes/session/SessionInfo.php
includes/session/SessionManager.php
includes/shell/Command.php
includes/shell/Shell.php
includes/site/Site.php
includes/skins/BaseTemplate.php
includes/skins/SkinTemplate.php
includes/specialpage/AuthManagerSpecialPage.php
includes/specialpage/FormSpecialPage.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialAllMessages.php
includes/specials/SpecialBlock.php
includes/specials/SpecialBotPasswords.php
includes/specials/SpecialContributions.php
includes/specials/SpecialDeletedContributions.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialExpandTemplates.php
includes/specials/SpecialListFiles.php
includes/specials/SpecialListGroupRights.php
includes/specials/SpecialMediaStatistics.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewimages.php
includes/specials/SpecialPageLanguage.php
includes/specials/SpecialRecentChanges.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUncategorizedcategories.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialWatchlist.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/title/NamespaceInfo.php
includes/upload/UploadBase.php
includes/user/BotPassword.php
includes/user/PasswordReset.php
includes/user/User.php
includes/user/UserNamePrefixSearch.php
includes/utils/AvroValidator.php
includes/utils/ClassCollector.php
includes/widget/ComplexTitleInputWidget.php
includes/widget/search/SearchFormWidget.php
languages/Language.php
languages/LanguageConverter.php
languages/i18n/ar.json
languages/i18n/az.json
languages/i18n/ba.json
languages/i18n/ban.json
languages/i18n/bcl.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/ca.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/en.json
languages/i18n/es-formal.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/exif/lb.json
languages/i18n/exif/ru.json
languages/i18n/exif/sh.json
languages/i18n/exif/sv.json
languages/i18n/exif/uk.json
languages/i18n/exif/zh-hant.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fit.json
languages/i18n/fr.json
languages/i18n/gom-deva.json
languages/i18n/gom-latn.json
languages/i18n/he.json
languages/i18n/ia.json
languages/i18n/io.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/luz.json
languages/i18n/lv.json
languages/i18n/min.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mni.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nqo.json
languages/i18n/pl.json
languages/i18n/pms.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/sh.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/szl.json
languages/i18n/uk.json
languages/i18n/vi.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/i18n/zh-hk.json
languages/messages/MessagesTg_cyrl.php
maintenance/Maintenance.php
maintenance/addSite.php
maintenance/archives/upgradeLogging.php
maintenance/benchmarks/benchmarkTidy.php
maintenance/categoryChangesAsRdf.php
maintenance/checkDependencies.php
maintenance/checkLess.php
maintenance/cleanupPreferences.php
maintenance/cleanupUploadStash.php
maintenance/compareParsers.php
maintenance/convertExtensionToRegistration.php
maintenance/convertLinks.php
maintenance/copyFileBackend.php
maintenance/deleteArchivedFiles.php
maintenance/eraseArchivedFile.php
maintenance/generateSitemap.php
maintenance/getReplicaServer.php
maintenance/importDump.php
maintenance/importImages.php
maintenance/includes/MigrateActors.php
maintenance/includes/TextPassDumper.php
maintenance/migrateArchiveText.php
maintenance/namespaceDupes.php
maintenance/nukeNS.php
maintenance/populateArchiveRevId.php
maintenance/populateContentTables.php
maintenance/populateImageSha1.php
maintenance/populateRevisionSha1.php
maintenance/preprocessDump.php
maintenance/preprocessorFuzzTest.php
maintenance/reassignEdits.php
maintenance/rebuildImages.php
maintenance/rebuildmessages.php
maintenance/removeUnusedAccounts.php
maintenance/sql.php
maintenance/storage/checkStorage.php
maintenance/storage/compressOld.php
maintenance/storage/orphanStats.php
maintenance/storage/recompressTracked.php
maintenance/update.php
maintenance/updateCollation.php
maintenance/updateExtensionJsonSchema.php
maintenance/userDupes.inc
maintenance/wrapOldPasswords.php
resources/src/mediawiki.base/mediawiki.base.js
resources/src/mediawiki.rcfilters/dm/ItemModel.js
resources/src/mediawiki.rcfilters/ui/ChangesListWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/ItemMenuOptionWidget.js
resources/src/startup/mediawiki.js
tests/common/TestSetup.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiTestCaseTrait.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
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/filebackend/FileBackendTest.php
tests/phpunit/includes/filebackend/filejournal/DBFileJournalIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/LocalRepoTest.php
tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/HashRingTest.php
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/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
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/libs/filebackend/filejournal/FileJournalTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/filebackend/filejournal/NullFileJournalTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/filebackend/filejournal/TestFileJournal.php [new file with mode: 0644]
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]
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js

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 893eebb..fc775fe 100644 (file)
@@ -32,21 +32,36 @@ $cfg['file_list'] = array_merge(
        class_exists( PHPUnit_TextUI_Command::class ) ? [] : [ '.phan/stubs/phpunit4.php' ],
        class_exists( ProfilerExcimer::class ) ? [] : [ '.phan/stubs/excimer.php' ],
        [
-               'maintenance/cleanupTable.inc',
-               'maintenance/CodeCleanerGlobalsPass.inc',
-               'maintenance/commandLine.inc',
-               'maintenance/sqlite.inc',
-               'maintenance/userDupes.inc',
-               'maintenance/language/checkLanguage.inc',
-               'maintenance/language/languages.inc',
+               // This makes constants and globals known to Phan before processing all other files.
+               // You can check the parser order with --dump-parsed-file-list
+               'includes/Defines.php',
+               // @todo This isn't working yet, see globals_type_map below
+               // 'includes/DefaultSettings.php',
+               // 'includes/Setup.php',
        ]
 );
 
+$cfg['exclude_file_list'] = array_merge(
+       $cfg['exclude_file_list'],
+       [
+               // This file has invalid PHP syntax
+               'vendor/squizlabs/php_codesniffer/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc',
+       ]
+);
+
+$cfg['analyzed_file_extensions'] = array_merge(
+       $cfg['analyzed_file_extensions'] ?? [ 'php' ],
+       [ 'inc' ]
+);
+
 $cfg['autoload_internal_extension_signatures'] = [
+       'dom' => '.phan/internal_stubs/dom.phan_php',
        'imagick' => '.phan/internal_stubs/imagick.phan_php',
+       'intl' => '.phan/internal_stubs/intl.phan_php',
        'memcached' => '.phan/internal_stubs/memcached.phan_php',
        'oci8' => '.phan/internal_stubs/oci8.phan_php',
        'pcntl' => '.phan/internal_stubs/pcntl.phan_php',
+       'pgsql' => '.phan/internal_stubs/pgsql.phan_php',
        'redis' => '.phan/internal_stubs/redis.phan_php',
        'sockets' => '.phan/internal_stubs/sockets.phan_php',
        'sqlsrv' => '.phan/internal_stubs/sqlsrv.phan_php',
@@ -65,7 +80,7 @@ $cfg['directory_list'] = [
 
 $cfg['exclude_analysis_directory_list'] = [
        'vendor/',
-       '.phan/stubs/',
+       '.phan/',
        // The referenced classes are not available in vendor, only when
        // included from composer.
        'includes/composer/',
@@ -73,48 +88,56 @@ $cfg['exclude_analysis_directory_list'] = [
        'maintenance/language/',
        // External class
        'includes/libs/jsminplus.php',
+       // External class
+       'includes/libs/objectcache/utils/MemcachedClient.php',
 ];
 
+// NOTE: If you're facing an issue which you cannot easily fix, DO NOT add it here. Suppress it
+// either in-line with @phan-suppress-next-line and similar, at block-level (via @suppress), or at
+// file-level (with @phan-file-suppress), so that it stays enabled for the rest of the codebase.
 $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [
-       // approximate error count: 22
-       "PhanAccessMethodInternal",
-       // approximate error count: 22
-       "PhanCommentParamWithoutRealParam",
        // approximate error count: 19
-       "PhanParamReqAfterOpt",
-       // approximate error count: 20
-       "PhanParamSignatureMismatch",
+       "PhanParamReqAfterOpt", // False positives with nullables (phan issue #3159). Use real nullables
+       //after dropping HHVM
        // approximate error count: 110
-       "PhanParamTooMany",
-       // approximate error count: 63
-       "PhanTypeArraySuspicious",
-       // approximate error count: 28
-       "PhanTypeArraySuspiciousNullable",
-       // approximate error count: 22
-       "PhanTypeComparisonFromArray",
-       // approximate error count: 88
-       "PhanTypeInvalidDimOffset",
-       // approximate error count: 60
+       "PhanParamTooMany", // False positives with variargs. Unsuppress after dropping HHVM
+
+       // approximate error count: 45
        "PhanTypeMismatchArgument",
-       // approximate error count: 20
-       "PhanTypeMismatchArgumentInternal",
-       // approximate error count: 40
-       "PhanTypeMismatchProperty",
-       // approximate error count: 36
-       "PhanUndeclaredConstant",
-       // approximate error count: 219
-       "PhanUndeclaredMethod",
-       // approximate error count: 752
+       // approximate error count: 693
        "PhanUndeclaredProperty",
-       // approximate error count: 53
-       "PhanUndeclaredVariableDim",
 ] );
 
+// This helps a lot in discovering bad code, but unfortunately it will always fail for
+// hooks + pass by reference, see phan issue #2943.
+// @todo Enable when the issue above is resolved and we update our config!
+$cfg['redundant_condition_detection'] = false;
+
 $cfg['ignore_undeclared_variables_in_global_scope'] = true;
+// @todo It'd be great if we could just make phan read these from DefaultSettings, to avoid
+// duplicating the types.
 $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[]',
+       'wgDebugLogGroups' => 'string|false|array{destination:string,sample?:int,level:int}',
+       'wgCookiePrefix' => 'string|false',
+       'wgOut' => 'OutputPage',
+       'wgExtraNamespaces' => 'string[]',
 ] );
 
 return $cfg;
diff --git a/.phan/internal_stubs/dom.phan_php b/.phan/internal_stubs/dom.phan_php
new file mode 100644 (file)
index 0000000..608e3a1
--- /dev/null
@@ -0,0 +1,420 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension dom@20031129
+
+namespace {
+class DOMAttr extends \DOMNode {
+
+    // properties
+    public $name;
+    public $ownerElement;
+    public $schemaTypeInfo;
+    public $specified;
+    public $value;
+
+    // methods
+    public function isId() {}
+    public function __construct($name, $value = null) {}
+}
+
+class DOMCdataSection extends \DOMText {
+
+    // methods
+    public function __construct($value) {}
+}
+
+class DOMCharacterData extends \DOMNode {
+
+    // properties
+    public $data;
+    public $length;
+
+    // methods
+    public function substringData($offset, $count) {}
+    public function appendData($arg) {}
+    public function insertData($offset, $arg) {}
+    public function deleteData($offset, $count) {}
+    public function replaceData($offset, $count, $arg) {}
+}
+
+class DOMComment extends \DOMCharacterData {
+
+    // methods
+    public function __construct($value = null) {}
+}
+
+class DOMConfiguration {
+
+    // methods
+    public function setParameter($name, $value) {}
+    public function getParameter($name = null) {}
+    public function canSetParameter($name = null, $value = null) {}
+}
+
+class DOMDocument extends \DOMNode {
+
+    // properties
+    public $actualEncoding;
+    public $config;
+    public $doctype;
+    public $documentElement;
+    public $documentURI;
+    public $encoding;
+    public $formatOutput;
+    public $implementation;
+    public $preserveWhiteSpace;
+    public $recover;
+    public $resolveExternals;
+    public $standalone;
+    public $strictErrorChecking;
+    public $substituteEntities;
+    public $validateOnParse;
+    public $version;
+    public $xmlEncoding;
+    public $xmlStandalone;
+    public $xmlVersion;
+
+    // methods
+    public function createElement($tagName, $value = null) {}
+    public function createDocumentFragment() {}
+    public function createTextNode($data) {}
+    public function createComment($data) {}
+    public function createCDATASection($data) {}
+    public function createProcessingInstruction($target, $data) {}
+    public function createAttribute($name) {}
+    public function createEntityReference($name) {}
+    public function getElementsByTagName($tagName) {}
+    public function importNode(\DOMNode $importedNode, $deep) {}
+    public function createElementNS($namespaceURI, $qualifiedName, $value = null) {}
+    public function createAttributeNS($namespaceURI, $qualifiedName) {}
+    public function getElementsByTagNameNS($namespaceURI, $localName) {}
+    public function getElementById($elementId) {}
+    public function adoptNode(\DOMNode $source) {}
+    public function normalizeDocument() {}
+    public function renameNode(\DOMNode $node, $namespaceURI, $qualifiedName) {}
+    public function load($source, $options = null) {}
+    public function save($file) {}
+    public function loadXML($source, $options = null) {}
+    public function saveXML(\DOMNode $node = null, $options = null) {}
+    public function __construct($version = null, $encoding = null) {}
+    public function validate() {}
+    public function xinclude($options = null) {}
+    public function loadHTML($source, $options = null) {}
+    public function loadHTMLFile($source, $options = null) {}
+    public function saveHTML() {}
+    public function saveHTMLFile($file) {}
+    public function schemaValidate($filename) {}
+    public function schemaValidateSource($source) {}
+    public function relaxNGValidate($filename) {}
+    public function relaxNGValidateSource($source) {}
+    public function registerNodeClass($baseClass, $extendedClass) {}
+}
+
+class DOMDocumentFragment extends \DOMNode {
+
+    // properties
+    public $name;
+
+    // methods
+    public function __construct() {}
+    public function appendXML($data) {}
+}
+
+class DOMDocumentType extends \DOMNode {
+
+    // properties
+    public $entities;
+    public $internalSubset;
+    public $name;
+    public $notations;
+    public $publicId;
+    public $systemId;
+}
+
+class DOMDomError {
+}
+
+class DOMElement extends \DOMNode {
+
+    // properties
+    public $schemaTypeInfo;
+    public $tagName;
+
+    // methods
+    public function getAttribute($name) {}
+    public function setAttribute($name, $value) {}
+    public function removeAttribute($name) {}
+    public function getAttributeNode($name) {}
+    public function setAttributeNode(\DOMAttr $newAttr) {}
+    public function removeAttributeNode(\DOMAttr $oldAttr) {}
+    public function getElementsByTagName($name) {}
+    public function getAttributeNS($namespaceURI, $localName) {}
+    public function setAttributeNS($namespaceURI, $qualifiedName, $value) {}
+    public function removeAttributeNS($namespaceURI, $localName) {}
+    public function getAttributeNodeNS($namespaceURI, $localName) {}
+    public function setAttributeNodeNS(\DOMAttr $newAttr) {}
+    public function getElementsByTagNameNS($namespaceURI, $localName) {}
+    public function hasAttribute($name) {}
+    public function hasAttributeNS($namespaceURI, $localName) {}
+    public function setIdAttribute($name, $isId) {}
+    public function setIdAttributeNS($namespaceURI, $localName, $isId) {}
+    public function setIdAttributeNode(\DOMAttr $attr, $isId) {}
+    public function __construct($name, $value = null, $uri = null) {}
+}
+
+class DOMEntity extends \DOMNode {
+
+    // properties
+    public $actualEncoding;
+    public $encoding;
+    public $notationName;
+    public $publicId;
+    public $systemId;
+    public $version;
+}
+
+class DOMEntityReference extends \DOMNode {
+
+    // properties
+    public $name;
+
+    // methods
+    public function __construct($name) {}
+}
+
+class DOMErrorHandler {
+
+    // methods
+    public function handleError(\DOMDomError $error) {}
+}
+
+final class DOMException extends \Exception {
+
+    // properties
+    public $code;
+    protected $message;
+    protected $file;
+    protected $line;
+}
+
+class DOMImplementation {
+
+    // properties
+    public $name;
+
+    // methods
+    public function getFeature($feature, $version) {}
+    public function hasFeature() {}
+    public function createDocumentType($qualifiedName, $publicId, $systemId) {}
+    public function createDocument($namespaceURI, $qualifiedName, \DOMDocumentType $docType) {}
+}
+
+class DOMImplementationList {
+
+    // methods
+    public function item($index) {}
+}
+
+class DOMImplementationSource {
+
+    // methods
+    public function getDomimplementation($features) {}
+    public function getDomimplementations($features) {}
+}
+
+class DOMLocator {
+}
+
+class DOMNameList {
+
+    // methods
+    public function getName($index) {}
+    public function getNamespaceURI($index) {}
+}
+
+class DOMNameSpaceNode {
+}
+
+class DOMNamedNodeMap implements \Traversable, \Countable {
+
+    // properties
+    public $length;
+
+    // methods
+    public function getNamedItem($name) {}
+    public function setNamedItem(\DOMNode $arg) {}
+    public function removeNamedItem($name = null) {}
+    public function item($index = null) {}
+    public function getNamedItemNS($namespaceURI = null, $localName = null) {}
+    public function setNamedItemNS(\DOMNode $arg = null) {}
+    public function removeNamedItemNS($namespaceURI = null, $localName = null) {}
+    public function count() {}
+}
+
+class DOMNode {
+
+    // properties
+    public $attributes;
+    public $baseURI;
+    public $childNodes;
+    public $firstChild;
+    public $lastChild;
+    public $localName;
+    public $namespaceURI;
+    public $nextSibling;
+    public $nodeName;
+    public $nodeType;
+    public $nodeValue;
+    public $ownerDocument;
+    public $parentNode;
+    public $prefix;
+    public $previousSibling;
+    public $textContent;
+
+    // methods
+    public function insertBefore(\DOMNode $newChild, \DOMNode $refChild = null) {}
+    public function replaceChild(\DOMNode $newChild, \DOMNode $oldChild) {}
+    public function removeChild(\DOMNode $oldChild) {}
+    public function appendChild(\DOMNode $newChild) {}
+    public function hasChildNodes() {}
+    public function cloneNode($deep = null) {}
+    public function normalize() {}
+    public function isSupported($feature, $version) {}
+    public function hasAttributes() {}
+    public function compareDocumentPosition(\DOMNode $other) {}
+    public function isSameNode(\DOMNode $other) {}
+    public function lookupPrefix($namespaceURI) {}
+    public function isDefaultNamespace($namespaceURI) {}
+    public function lookupNamespaceUri($prefix) {}
+    public function isEqualNode(\DOMNode $arg) {}
+    public function getFeature($feature, $version) {}
+    public function setUserData($key, $data, $handler) {}
+    public function getUserData($key) {}
+    public function getNodePath() {}
+    public function getLineNo() {}
+    public function C14N($exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null) {}
+    public function C14NFile($uri, $exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null) {}
+}
+
+class DOMNodeList implements \Traversable, \Countable {
+
+    // properties
+    public $length;
+
+    // methods
+    public function item($index) {}
+    public function count() {}
+}
+
+class DOMNotation extends \DOMNode {
+
+    // properties
+    public $publicId;
+    public $systemId;
+}
+
+class DOMProcessingInstruction extends \DOMNode {
+
+    // properties
+    public $data;
+    public $target;
+
+    // methods
+    public function __construct($name, $value = null) {}
+}
+
+class DOMStringExtend {
+
+    // methods
+    public function findOffset16($offset32) {}
+    public function findOffset32($offset16) {}
+}
+
+class DOMStringList {
+
+    // methods
+    public function item($index) {}
+}
+
+class DOMText extends \DOMCharacterData {
+
+    // properties
+    public $wholeText;
+
+    // methods
+    public function splitText($offset) {}
+    public function isWhitespaceInElementContent() {}
+    public function isElementContentWhitespace() {}
+    public function replaceWholeText($content) {}
+    public function __construct($value = null) {}
+}
+
+class DOMTypeinfo {
+}
+
+class DOMUserDataHandler {
+
+    // methods
+    public function handle() {}
+}
+
+class DOMXPath {
+
+    // properties
+    public $document;
+
+    // methods
+    public function __construct(\DOMDocument $doc) {}
+    public function registerNamespace($prefix, $uri) {}
+    public function query($expr, \DOMNode $context = null, $registerNodeNS = null) {}
+    public function evaluate($expr, \DOMNode $context = null, $registerNodeNS = null) {}
+    public function registerPhpFunctions() {}
+}
+
+function dom_import_simplexml($node) {}
+const DOMSTRING_SIZE_ERR = 2;
+const DOM_HIERARCHY_REQUEST_ERR = 3;
+const DOM_INDEX_SIZE_ERR = 1;
+const DOM_INUSE_ATTRIBUTE_ERR = 10;
+const DOM_INVALID_ACCESS_ERR = 15;
+const DOM_INVALID_CHARACTER_ERR = 5;
+const DOM_INVALID_MODIFICATION_ERR = 13;
+const DOM_INVALID_STATE_ERR = 11;
+const DOM_NAMESPACE_ERR = 14;
+const DOM_NOT_FOUND_ERR = 8;
+const DOM_NOT_SUPPORTED_ERR = 9;
+const DOM_NO_DATA_ALLOWED_ERR = 6;
+const DOM_NO_MODIFICATION_ALLOWED_ERR = 7;
+const DOM_PHP_ERR = 0;
+const DOM_SYNTAX_ERR = 12;
+const DOM_VALIDATION_ERR = 16;
+const DOM_WRONG_DOCUMENT_ERR = 4;
+const XML_ATTRIBUTE_CDATA = 1;
+const XML_ATTRIBUTE_DECL_NODE = 16;
+const XML_ATTRIBUTE_ENTITY = 6;
+const XML_ATTRIBUTE_ENUMERATION = 9;
+const XML_ATTRIBUTE_ID = 2;
+const XML_ATTRIBUTE_IDREF = 3;
+const XML_ATTRIBUTE_IDREFS = 4;
+const XML_ATTRIBUTE_NMTOKEN = 7;
+const XML_ATTRIBUTE_NMTOKENS = 8;
+const XML_ATTRIBUTE_NODE = 2;
+const XML_ATTRIBUTE_NOTATION = 10;
+const XML_CDATA_SECTION_NODE = 4;
+const XML_COMMENT_NODE = 8;
+const XML_DOCUMENT_FRAG_NODE = 11;
+const XML_DOCUMENT_NODE = 9;
+const XML_DOCUMENT_TYPE_NODE = 10;
+const XML_DTD_NODE = 14;
+const XML_ELEMENT_DECL_NODE = 15;
+const XML_ELEMENT_NODE = 1;
+const XML_ENTITY_DECL_NODE = 17;
+const XML_ENTITY_NODE = 6;
+const XML_ENTITY_REF_NODE = 5;
+const XML_HTML_DOCUMENT_NODE = 13;
+const XML_LOCAL_NAMESPACE = 18;
+const XML_NAMESPACE_DECL_NODE = 18;
+const XML_NOTATION_NODE = 12;
+const XML_PI_NODE = 7;
+const XML_TEXT_NODE = 3;
+}
diff --git a/.phan/internal_stubs/pgsql.phan_php b/.phan/internal_stubs/pgsql.phan_php
new file mode 100644 (file)
index 0000000..c8a2b25
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension pgsql@7.3.4
+
+namespace {
+function pg_affected_rows($result) {}
+function pg_cancel_query($connection) {}
+function pg_client_encoding($connection = null) {}
+function pg_clientencoding($connection = null) {}
+function pg_close($connection = null) {}
+function pg_cmdtuples($result) {}
+function pg_connect($connection_string, $connect_type = null, $host = null, $port = null, $options = null, $tty = null, $database = null) {}
+function pg_connect_poll($connection = null) {}
+function pg_connection_busy($connection) {}
+function pg_connection_reset($connection) {}
+function pg_connection_status($connection) {}
+function pg_consume_input($connection) {}
+function pg_convert($db, $table, $values, $options = null) {}
+function pg_copy_from($connection, $table_name, $rows, $delimiter = null, $null_as = null) {}
+function pg_copy_to($connection, $table_name, $delimiter = null, $null_as = null) {}
+function pg_dbname($connection = null) {}
+function pg_delete($db, $table, $ids, $options = null) {}
+function pg_end_copy($connection = null) {}
+function pg_errormessage($connection = null) {}
+function pg_escape_bytea($connection = null, $data = null) {}
+function pg_escape_identifier($connection = null, $data = null) {}
+function pg_escape_literal($connection = null, $data = null) {}
+function pg_escape_string($connection = null, $data = null) {}
+function pg_exec($connection = null, $query = null) {}
+function pg_execute($connection = null, $stmtname = null, $params = null) {}
+function pg_fetch_all($result, $result_type = null) {}
+function pg_fetch_all_columns($result, $column_number = null) {}
+function pg_fetch_array($result, $row = null, $result_type = null) {}
+function pg_fetch_assoc($result, $row = null) {}
+function pg_fetch_object($result, $row = null, $class_name = null, $l = null, $ctor_params = null) {}
+function pg_fetch_result($result, $row_number = null, $field_name = null) {}
+function pg_fetch_row($result, $row = null, $result_type = null) {}
+function pg_field_is_null($result, $row = null, $field_name_or_number = null) {}
+function pg_field_name($result, $field_number) {}
+function pg_field_num($result, $field_name) {}
+function pg_field_prtlen($result, $row = null, $field_name_or_number = null) {}
+function pg_field_size($result, $field_number) {}
+function pg_field_table($result, $field_number, $oid_only = null) {}
+function pg_field_type($result, $field_number) {}
+function pg_field_type_oid($result, $field_number) {}
+function pg_fieldisnull($result, $row = null, $field_name_or_number = null) {}
+function pg_fieldname($result, $field_number) {}
+function pg_fieldnum($result, $field_name) {}
+function pg_fieldprtlen($result, $row = null, $field_name_or_number = null) {}
+function pg_fieldsize($result, $field_number) {}
+function pg_fieldtype($result, $field_number) {}
+function pg_flush($connection) {}
+function pg_free_result($result) {}
+function pg_freeresult($result) {}
+function pg_get_notify($connection = null, $e = null) {}
+function pg_get_pid($connection = null) {}
+function pg_get_result($connection) {}
+function pg_getlastoid($result) {}
+function pg_host($connection = null) {}
+function pg_insert($db, $table, $values, $options = null) {}
+function pg_last_error($connection = null) {}
+function pg_last_notice($connection, $option = null) {}
+function pg_last_oid($result) {}
+function pg_lo_close($large_object) {}
+function pg_lo_create($connection = null, $large_object_id = null) {}
+function pg_lo_export($connection = null, $objoid = null, $filename = null) {}
+function pg_lo_import($connection = null, $filename = null, $large_object_oid = null) {}
+function pg_lo_open($connection = null, $large_object_oid = null, $mode = null) {}
+function pg_lo_read($large_object, $len = null) {}
+function pg_lo_read_all($large_object) {}
+function pg_lo_seek($large_object, $offset, $whence = null) {}
+function pg_lo_tell($large_object) {}
+function pg_lo_truncate($large_object, $size = null) {}
+function pg_lo_unlink($connection = null, $large_object_oid = null) {}
+function pg_lo_write($large_object, $buf, $len = null) {}
+function pg_loclose($large_object) {}
+function pg_locreate($connection = null, $large_object_id = null) {}
+function pg_loexport($connection = null, $objoid = null, $filename = null) {}
+function pg_loimport($connection = null, $filename = null, $large_object_oid = null) {}
+function pg_loopen($connection = null, $large_object_oid = null, $mode = null) {}
+function pg_loread($large_object, $len = null) {}
+function pg_loreadall($large_object) {}
+function pg_lounlink($connection = null, $large_object_oid = null) {}
+function pg_lowrite($large_object, $buf, $len = null) {}
+function pg_meta_data($db, $table) {}
+function pg_num_fields($result) {}
+function pg_num_rows($result) {}
+function pg_numfields($result) {}
+function pg_numrows($result) {}
+function pg_options($connection = null) {}
+function pg_parameter_status($connection, $param_name = null) {}
+function pg_pconnect($connection_string, $host = null, $port = null, $options = null, $tty = null, $database = null) {}
+function pg_ping($connection = null) {}
+function pg_port($connection = null) {}
+function pg_prepare($connection = null, $stmtname = null, $query = null) {}
+function pg_put_line($connection = null, $query = null) {}
+function pg_query($connection = null, $query = null) {}
+function pg_query_params($connection = null, $query = null, $params = null) {}
+function pg_result($connection) {}
+function pg_result_error($result) {}
+function pg_result_error_field($result, $fieldcode) {}
+function pg_result_seek($result, $offset) {}
+function pg_result_status($result, $result_type = null) {}
+function pg_select($db, $table, $ids, $options = null, $result_type = null) {}
+function pg_send_execute($connection, $stmtname, $params) {}
+function pg_send_prepare($connection, $stmtname, $query) {}
+function pg_send_query($connection, $query) {}
+function pg_send_query_params($connection, $query, $params) {}
+function pg_set_client_encoding($connection = null, $encoding = null) {}
+function pg_set_error_verbosity($connection = null, $verbosity = null) {}
+function pg_setclientencoding($connection = null, $encoding = null) {}
+function pg_socket($connection) {}
+function pg_trace($filename, $mode = null, $connection = null) {}
+function pg_transaction_status($connection) {}
+function pg_tty($connection = null) {}
+function pg_unescape_bytea($data) {}
+function pg_untrace($connection = null) {}
+function pg_update($db, $table, $fields, $ids, $options = null) {}
+function pg_version($connection = null) {}
+const PGSQL_ASSOC = 1;
+const PGSQL_BAD_RESPONSE = 5;
+const PGSQL_BOTH = 3;
+const PGSQL_COMMAND_OK = 1;
+const PGSQL_CONNECTION_AUTH_OK = 5;
+const PGSQL_CONNECTION_AWAITING_RESPONSE = 4;
+const PGSQL_CONNECTION_BAD = 1;
+const PGSQL_CONNECTION_MADE = 3;
+const PGSQL_CONNECTION_OK = 0;
+const PGSQL_CONNECTION_SETENV = 6;
+const PGSQL_CONNECTION_STARTED = 2;
+const PGSQL_CONNECT_ASYNC = 4;
+const PGSQL_CONNECT_FORCE_NEW = 2;
+const PGSQL_CONV_FORCE_NULL = 4;
+const PGSQL_CONV_IGNORE_DEFAULT = 2;
+const PGSQL_CONV_IGNORE_NOT_NULL = 8;
+const PGSQL_COPY_IN = 4;
+const PGSQL_COPY_OUT = 3;
+const PGSQL_DIAG_COLUMN_NAME = 99;
+const PGSQL_DIAG_CONSTRAINT_NAME = 110;
+const PGSQL_DIAG_CONTEXT = 87;
+const PGSQL_DIAG_DATATYPE_NAME = 100;
+const PGSQL_DIAG_INTERNAL_POSITION = 112;
+const PGSQL_DIAG_INTERNAL_QUERY = 113;
+const PGSQL_DIAG_MESSAGE_DETAIL = 68;
+const PGSQL_DIAG_MESSAGE_HINT = 72;
+const PGSQL_DIAG_MESSAGE_PRIMARY = 77;
+const PGSQL_DIAG_SCHEMA_NAME = 115;
+const PGSQL_DIAG_SEVERITY = 83;
+const PGSQL_DIAG_SEVERITY_NONLOCALIZED = 86;
+const PGSQL_DIAG_SOURCE_FILE = 70;
+const PGSQL_DIAG_SOURCE_FUNCTION = 82;
+const PGSQL_DIAG_SOURCE_LINE = 76;
+const PGSQL_DIAG_SQLSTATE = 67;
+const PGSQL_DIAG_STATEMENT_POSITION = 80;
+const PGSQL_DIAG_TABLE_NAME = 116;
+const PGSQL_DML_ASYNC = 1024;
+const PGSQL_DML_ESCAPE = 4096;
+const PGSQL_DML_EXEC = 512;
+const PGSQL_DML_NO_CONV = 256;
+const PGSQL_DML_STRING = 2048;
+const PGSQL_EMPTY_QUERY = 0;
+const PGSQL_ERRORS_DEFAULT = 1;
+const PGSQL_ERRORS_TERSE = 0;
+const PGSQL_ERRORS_VERBOSE = 2;
+const PGSQL_FATAL_ERROR = 7;
+const PGSQL_LIBPQ_VERSION = '9.6.9';
+const PGSQL_LIBPQ_VERSION_STR = 'PostgreSQL 9.6.9 (win32)';
+const PGSQL_NONFATAL_ERROR = 6;
+const PGSQL_NOTICE_ALL = 2;
+const PGSQL_NOTICE_CLEAR = 3;
+const PGSQL_NOTICE_LAST = 1;
+const PGSQL_NUM = 2;
+const PGSQL_POLLING_ACTIVE = 4;
+const PGSQL_POLLING_FAILED = 0;
+const PGSQL_POLLING_OK = 3;
+const PGSQL_POLLING_READING = 1;
+const PGSQL_POLLING_WRITING = 2;
+const PGSQL_SEEK_CUR = 1;
+const PGSQL_SEEK_END = 2;
+const PGSQL_SEEK_SET = 0;
+const PGSQL_STATUS_LONG = 1;
+const PGSQL_STATUS_STRING = 2;
+const PGSQL_TRANSACTION_ACTIVE = 1;
+const PGSQL_TRANSACTION_IDLE = 0;
+const PGSQL_TRANSACTION_INERROR = 3;
+const PGSQL_TRANSACTION_INTRANS = 2;
+const PGSQL_TRANSACTION_UNKNOWN = 4;
+const PGSQL_TUPLES_OK = 2;
+}
index e87d4cd..d663a44 100644 (file)
@@ -22,7 +22,7 @@ class ExcimerProfiler {
        }
        public function stop() {
        }
-       public function getLog() {
+       public function getLog() : ExcimerLog {
        }
        public function flush() {
        }
@@ -33,8 +33,14 @@ class ExcimerLog {
        }
        function formatCollapsed() {
        }
+       /**
+        * @return array[]
+        */
        function aggregateByFunction() {
        }
+       /**
+        * @return int
+        */
        function getEventCount() {
        }
        function current() {
index ba1efb9..662a0e0 100644 (file)
@@ -40,6 +40,11 @@ class Mail {
         */
        public function send( $recipients, array $headers, $body ) {
        }
+       /**
+        * @return string
+        */
+       public function getMessage() {
+       }
 }
 
 class Mail_smtp extends Mail {
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..77dfc62 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": "*",
@@ -41,7 +42,7 @@
                "wikimedia/cldr-plural-rule-parser": "1.0.0",
                "wikimedia/composer-merge-plugin": "1.4.1",
                "wikimedia/html-formatter": "1.0.2",
-               "wikimedia/ip-set": "2.0.1",
+               "wikimedia/ip-set": "2.1.0",
                "wikimedia/less.php": "1.8.0",
                "wikimedia/object-factory": "2.1.0",
                "wikimedia/password-blacklist": "0.1.4",
@@ -76,7 +77,7 @@
                "wikimedia/avro": "1.8.0",
                "wikimedia/testing-access-wrapper": "~1.0",
                "wmde/hamcrest-html-matchers": "^0.1.0",
-               "mediawiki/mediawiki-phan-config": "0.6.1",
+               "mediawiki/mediawiki-phan-config": "0.7.1",
                "symfony/yaml": "3.4.28",
                "johnkary/phpunit-speedtrap": "^1.0 | ^2.0"
        },
index fdf1b1b..81de1a0 100644 (file)
@@ -1278,7 +1278,7 @@ $wgMaxAnimatedGifArea = 1.25e7;
  *  $wgTiffThumbnailType = [ 'jpg', 'image/jpeg' ];
  * @endcode
  */
-$wgTiffThumbnailType = false;
+$wgTiffThumbnailType = [];
 
 /**
  * If rendered thumbnail files are older than this timestamp, they
index f066a61..6ae4371 100644 (file)
@@ -1799,8 +1799,11 @@ class EditPage {
                } elseif ( !$status->isOK() ) {
                        # ...or the hook could be expecting us to produce an error
                        // FIXME this sucks, we should just use the Status object throughout
+                       if ( !$status->getErrors() ) {
+                               // Provide a fallback error message if none was set
+                               $status->fatal( 'hookaborted' );
+                       }
                        $this->hookError = $this->formatStatusErrors( $status );
-                       $status->fatal( 'hookaborted' );
                        $status->value = self::AS_HOOK_ERROR_EXPECTED;
                        return false;
                }
@@ -4166,7 +4169,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 ecbc6e3..78f6ca9 100644 (file)
@@ -88,6 +88,7 @@ class FauxRequest extends WebRequest {
 
        /**
         * @return array
+        * @suppress PhanParamSignatureMismatch
         */
        public function getValues() {
                return $this->data;
index 8272ccf..1241e1c 100644 (file)
@@ -147,7 +147,7 @@ class FileDeleteForm {
         * Really delete the file
         *
         * @param Title &$title
-        * @param File &$file
+        * @param LocalFile &$file
         * @param string &$oldimage Archive name
         * @param string $reason Reason of the deletion
         * @param bool $suppress Whether to mark all deleted versions as restricted
@@ -167,7 +167,7 @@ class FileDeleteForm {
                if ( $oldimage ) {
                        $page = null;
                        $status = $file->deleteOld( $oldimage, $reason, $suppress, $user );
-                       if ( $status->ok ) {
+                       if ( $status->isOK() ) {
                                // Need to do a log item
                                $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
                                if ( trim( $reason ) != '' ) {
@@ -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 ) {
@@ -255,16 +255,18 @@ class FileDeleteForm {
 
                $wgOut->enableOOUI();
 
+               $fields = [];
+
+               $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
+                       $this->prepareMessage( 'filedelete-intro' ) ) ]
+               );
+
                $options = Xml::listDropDownOptions(
                        $wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text(),
                        [ 'other' => $wgOut->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
                );
                $options = Xml::listDropDownOptionsOoui( $options );
 
-               $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
-                       $this->prepareMessage( 'filedelete-intro' ) ) ]
-               );
-
                $fields[] = new OOUI\FieldLayout(
                        new OOUI\DropdownInputWidget( [
                                'name' => 'wpDeleteReasonList',
index cc998c7..38daab5 100644 (file)
@@ -1127,6 +1127,7 @@ function wfLogProfilingData() {
        if ( isset( $ctx['forwarded_for'] ) ||
                isset( $ctx['client_ip'] ) ||
                isset( $ctx['from'] ) ) {
+               // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                $ctx['proxy'] = $_SERVER['REMOTE_ADDR'];
        }
 
@@ -2115,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
@@ -2189,6 +2191,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 = [] ) {
index 6ad9b31..e4a5f96 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 ),
                ];
        }
@@ -311,6 +311,7 @@ class LinkFilter {
         */
        public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
                $db = wfGetDB( DB_REPLICA );
+               $like = [];
 
                $target = $protocol . $filterEntry;
                $bits = wfParseUrl( $target );
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 7a6987e..f91477a 100644 (file)
@@ -745,7 +745,7 @@ class MediaWiki {
                        Profiler::instance()->logDataPageOutputOnly();
                } catch ( Exception $e ) {
                        // An error may already have been shown in run(), so just log it to be safe
-                       MWExceptionHandler::rollbackMasterChangesAndLog( $e );
+                       MWExceptionHandler::logException( $e );
                }
 
                // Disable WebResponse setters for post-send processing (T191537).
index e6faace..564c8f4 100644 (file)
@@ -25,7 +25,7 @@ use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\SlotRecord;
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\ILoadBalancer;
 
 /**
  * Handles the backend logic of moving a page from one title
@@ -51,7 +51,7 @@ class MovePage {
        protected $options;
 
        /**
-        * @var LoadBalancer
+        * @var ILoadBalancer
         */
        protected $loadBalancer;
 
@@ -61,7 +61,7 @@ class MovePage {
        protected $nsInfo;
 
        /**
-        * @var WatchedItemStore
+        * @var WatchedItemStoreInterface
         */
        protected $watchedItems;
 
@@ -81,7 +81,7 @@ class MovePage {
         * @param Title $oldTitle
         * @param Title $newTitle
         * @param ServiceOptions|null $options
-        * @param LoadBalancer|null $loadBalancer
+        * @param ILoadBalancer|null $loadBalancer
         * @param NamespaceInfo|null $nsInfo
         * @param WatchedItemStore|null $watchedItems
         * @param PermissionManager|null $permMgr
@@ -90,9 +90,9 @@ class MovePage {
                Title $oldTitle,
                Title $newTitle,
                ServiceOptions $options = null,
-               LoadBalancer $loadBalancer = null,
+               ILoadBalancer $loadBalancer = null,
                NamespaceInfo $nsInfo = null,
-               WatchedItemStore $watchedItems = null,
+               WatchedItemStoreInterface $watchedItems = null,
                PermissionManager $permMgr = null,
                RepoGroup $repoGroup = null
        ) {
@@ -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 c60b8c6..539758f 100644 (file)
 
 namespace MediaWiki\Navigation;
 
-use MediaWiki\Linker\LinkTarget;
-use MessageLocalizer;
 use Html;
+use MessageLocalizer;
+use Title;
 
 /**
  * Helper class for generating prev/next links for paging.
+ * @todo Use LinkTarget instead of Title
  *
  * @since 1.34
  */
@@ -36,6 +37,9 @@ class PrevNextNavigationRenderer {
         */
        private $messageLocalizer;
 
+       /**
+        * @param MessageLocalizer $messageLocalizer
+        */
        public function __construct( MessageLocalizer $messageLocalizer ) {
                $this->messageLocalizer = $messageLocalizer;
        }
@@ -43,15 +47,19 @@ class PrevNextNavigationRenderer {
        /**
         * Generate (prev x| next x) (20|50|100...) type links for paging
         *
-        * @param LinkTarget $title LinkTarget object to link
+        * @param Title $title Title object to link
         * @param int $offset
         * @param int $limit
         * @param array $query Optional URL query parameter string
         * @param bool $atend Optional param for specified if this is the last page
         * @return string
         */
-       public function buildPrevNextNavigation( LinkTarget $title, $offset, $limit,
-                                                                                        array $query = [], $atend = false
+       public function buildPrevNextNavigation(
+               Title $title,
+               $offset,
+               $limit,
+               array $query = [],
+               $atend = false
        ) {
                # Make 'previous' link
                $prev = $this->messageLocalizer->msg( 'prevn' )->title( $title )
@@ -76,6 +84,8 @@ class PrevNextNavigationRenderer {
 
                # Make links to set number of items per page
                $numLinks = [];
+               // @phan-suppress-next-next-line PhanUndeclaredMethod
+               // @fixme MessageLocalizer doesn't have a getLanguage() method!
                $lang = $this->messageLocalizer->getLanguage();
                foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
                        $numLinks[] = $this->numLink( $title, $offset, $num, $query,
@@ -89,7 +99,7 @@ class PrevNextNavigationRenderer {
        /**
         * Helper function for buildPrevNextNavigation() that generates links
         *
-        * @param LinkTarget $title LinkTarget object to link
+        * @param Title $title Title object to link
         * @param int $offset
         * @param int $limit
         * @param array $query Extra query parameters
@@ -98,7 +108,7 @@ class PrevNextNavigationRenderer {
         * @param string $class Value of the "class" attribute of the link
         * @return string HTML fragment
         */
-       private function numLink( LinkTarget $title, $offset, $limit, array $query, $link,
+       private function numLink( Title $title, $offset, $limit, array $query, $link,
                                                          $tooltipMsg, $class
        ) {
                $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
index 9af16d3..15a759b 100644 (file)
@@ -44,7 +44,7 @@ use Wikimedia\WrappedStringList;
  * @todo document
  */
 class OutputPage extends ContextSource {
-       /** @var array Should be private. Used with addMeta() which adds "<meta>" */
+       /** @var string[][] Should be private. Used with addMeta() which adds "<meta>" */
        protected $mMetatags = [];
 
        /** @var array */
@@ -995,6 +995,8 @@ class OutputPage extends ContextSource {
         * @param Title $t
         */
        public function setTitle( Title $t ) {
+               // @phan-suppress-next-next-line PhanUndeclaredMethod
+               // @fixme Not all implementations of IContextSource have this method!
                $this->getContext()->setTitle( $t );
        }
 
@@ -1820,14 +1822,10 @@ class OutputPage extends ContextSource {
         * @param string $text Wikitext
         * @param Title $title
         * @param bool $linestart Is this the start of a line?
-        * @param bool $tidy Whether to use tidy.
-        *             Setting this to false (or omitting it) is deprecated
-        *             since 1.32; all wikitext should be tidied.
         * @param bool $interface Whether it is an interface message
         *   (for example disables conversion)
         * @param string $wrapperClass if not empty, wraps the output in
         *   a `<div class="$wrapperClass">`
-        * @private
         */
        private function addWikiTextTitleInternal(
                $text, Title $title, $linestart, $interface, $wrapperClass = null
@@ -3027,10 +3025,11 @@ class OutputPage extends ContextSource {
                $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
 
                $pieces = [];
-               $pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes(
+               $htmlAttribs = Sanitizer::mergeAttributes(
                        $this->getRlClient()->getDocumentAttributes(),
                        $sk->getHtmlElementAttributes()
-               ) );
+               );
+               $pieces[] = Html::htmlHeader( $htmlAttribs );
                $pieces[] = Html::openElement( 'head' );
 
                if ( $this->getHTMLTitle() == '' ) {
@@ -3050,7 +3049,7 @@ class OutputPage extends ContextSource {
                }
 
                $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
-               $pieces[] = $this->getRlClient()->getHeadHtml();
+               $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null );
                $pieces[] = $this->buildExemptModules();
                $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
                $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
index 37791d0..0a8e515 100644 (file)
@@ -85,8 +85,8 @@ class PermissionManager {
        /** @var NamespaceInfo */
        private $nsInfo;
 
-       /** @var string[] Cached results of getAllRights() */
-       private $allRights = false;
+       /** @var string[]|null Cached results of getAllRights() */
+       private $allRights;
 
        /** @var string[][] Cached user rights */
        private $usersRights = null;
@@ -1220,7 +1220,8 @@ class PermissionManager {
         * Check if user is allowed to make any action
         *
         * @param UserIdentity $user
-        * // TODO: HHVM can't create mocks with variable params @param string ...$actions
+        * // TODO: HHVM bug T228695#5450847 @param string ...$actions
+        * @suppress PhanCommentParamWithoutRealParam
         * @return bool True if user is allowed to perform *any* of the given actions
         * @since 1.34
         */
@@ -1238,7 +1239,8 @@ class PermissionManager {
         * Check if user is allowed to make all actions
         *
         * @param UserIdentity $user
-        * // TODO: HHVM can't create mocks with variable params @param string ...$actions
+        * // TODO: HHVM bug T228695#5450847 @param string ...$actions
+        * @suppress PhanCommentParamWithoutRealParam
         * @return bool True if user is allowed to perform *all* of the given actions
         * @since 1.34
         */
@@ -1469,7 +1471,7 @@ class PermissionManager {
         * @return string[] Array of permission names
         */
        public function getAllPermissions() {
-               if ( $this->allRights === false ) {
+               if ( $this->allRights === null ) {
                        if ( count( $this->options->get( 'AvailableRights' ) ) ) {
                                $this->allRights = array_unique( array_merge(
                                        $this->coreRights,
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 a4959d1..f28b4ea 100644 (file)
@@ -47,6 +47,7 @@ class EntryPoint {
                        'cookiePrefix' => $conf->get( 'CookiePrefix' )
                ] );
 
+               // @phan-suppress-next-line PhanAccessMethodInternal
                $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
 
index a71f6a6..528bac1 100644 (file)
@@ -51,7 +51,6 @@ class HeaderContainer {
         * better served by an HTTP header parsing library which provides the full
         * parse tree.
         *
-        * @param string $name The header name
         * @param string|string[] $value The input header value
         * @return array
         */
index 14b4c9c..961da01 100644 (file)
@@ -263,6 +263,7 @@ class Router {
         * @return ResponseInterface
         */
        private function executeHandler( $handler ): ResponseInterface {
+               // @phan-suppress-next-line PhanAccessMethodInternal
                $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
                if ( $authResult ) {
                        return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
index 85749c6..3718d66 100644 (file)
@@ -8,12 +8,14 @@ namespace MediaWiki\Rest;
  *
  * run() must be declared in the subclass. It cannot be declared as abstract
  * here because it has a variable parameter list.
+ * @todo Declare it as abstract after dropping HHVM
  *
  * @package MediaWiki\Rest
  */
 class SimpleHandler extends Handler {
        public function execute() {
                $params = array_values( $this->getRequest()->getPathParams() );
+               // @phan-suppress-next-line PhanUndeclaredMethod
                return $this->run( ...$params );
        }
 }
index de3c299..c6e727e 100644 (file)
@@ -89,6 +89,7 @@ class Revision implements IDBAccessObject {
         * @return SqlBlobStore
         */
        protected static function getBlobStore( $wiki = false ) {
+               // @phan-suppress-next-line PhanAccessMethodInternal
                $store = MediaWikiServices::getInstance()
                        ->getBlobStoreFactory()
                        ->newSqlBlobStore( $wiki );
index e9136cb..8bb2c89 100644 (file)
@@ -37,6 +37,7 @@ use Wikimedia\Assert\Assert;
  *
  * @since 1.31
  * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord
+ * @property MutableRevisionSlots $mSlots
  */
 class MutableRevisionRecord extends RevisionRecord {
 
@@ -78,8 +79,6 @@ class MutableRevisionRecord extends RevisionRecord {
                $slots = new MutableRevisionSlots();
 
                parent::__construct( $title, $slots, $dbDomain );
-
-               $this->mSlots = $slots; // redundant, but nice for static analysis
        }
 
        /**
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..5d09e01 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.
         */
index 9e8dfe7..73f622a 100644 (file)
@@ -54,8 +54,10 @@ use Psr\Log\NullLogger;
 use RecentChange;
 use Revision;
 use RuntimeException;
+use StatusValue;
 use stdClass;
 use Title;
+use Traversable;
 use User;
 use WANObjectCache;
 use Wikimedia\Assert\Assert;
@@ -1876,6 +1878,125 @@ class RevisionStore
                return $rev;
        }
 
+       /**
+        * Construct a RevisionRecord instance for each row in $rows,
+        * and return them as an associative array indexed by revision ID.
+        * @param Traversable|array $rows the rows to construct revision records from
+        * @param array $options Supports the following options:
+        *               'slots' - whether metadata about revision slots should be
+        *               loaded immediately. Supports falsy or truthy value as well
+        *               as an explicit list of slot role names.
+        *               'content'- whether the actual content of the slots should be
+        *               preloaded. TODO: no supported yet.
+        * @param int $queryFlags
+        * @param Title|null $title
+        * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
+        *                                         and an array of errors for the revisions failed to fetch.
+        */
+       public function newRevisionsFromBatch(
+               $rows,
+               array $options = [],
+               $queryFlags = 0,
+               Title $title = null
+       ) {
+               $result = new StatusValue();
+
+               $rowsByRevId = [];
+               $pageIds = [];
+               $titlesByPageId = [];
+               foreach ( $rows as $row ) {
+                       if ( isset( $rowsByRevId[$row->rev_id] ) ) {
+                               throw new InvalidArgumentException( "Duplicate rows in newRevisionsFromBatch {$row->rev_id}" );
+                       }
+                       if ( $title && $row->rev_page != $title->getArticleID() ) {
+                               throw new InvalidArgumentException(
+                                       "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
+                               );
+                       }
+                       $pageIds[] = $row->rev_page;
+                       $rowsByRevId[$row->rev_id] = $row;
+               }
+
+               if ( empty( $rowsByRevId ) ) {
+                       $result->setResult( true, [] );
+                       return $result;
+               }
+
+               // If the title is not supplied, batch-fetch Title objects.
+               if ( $title ) {
+                       $titlesByPageId[$title->getArticleID()] = $title;
+               } else {
+                       $pageIds = array_unique( $pageIds );
+                       foreach ( Title::newFromIDs( $pageIds ) as $t ) {
+                               $titlesByPageId[$t->getArticleID()] = $t;
+                       }
+               }
+
+               if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $result->setResult( true,
+                               array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
+                                       try {
+                                               return $this->newRevisionFromRow(
+                                                       $row,
+                                                       $queryFlags,
+                                                       $titlesByPageId[$row->rev_page]
+                                               );
+                                       } catch ( MWException $e ) {
+                                               $result->warning( 'internalerror', $e->getMessage() );
+                                               return null;
+                                       }
+                               }, $rowsByRevId )
+                       );
+                       return $result;
+               }
+
+               $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ];
+               if ( is_array( $options['slots'] ) ) {
+                       $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) {
+                               return $this->slotRoleStore->getId( $slot_name );
+                       }, $options['slots'] );
+               }
+
+               // TODO: Support optional fetching of the content
+               $queryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+               $slotRows = $db->select(
+                       $queryInfo['tables'],
+                       $queryInfo['fields'],
+                       $slotQueryConds,
+                       __METHOD__,
+                       [],
+                       $queryInfo['joins']
+               );
+
+               $slotRowsByRevId = [];
+               foreach ( $slotRows as $slotRow ) {
+                       $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow;
+               }
+               $result->setResult( true, array_map( function ( $row ) use
+                       ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
+                               if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
+                                       $result->warning(
+                                               'internalerror',
+                                               "Couldn't find slots for rev {$row->rev_id}"
+                                       );
+                                       return null;
+                               }
+                               try {
+                                       return $this->newRevisionFromRowAndSlots(
+                                               $row,
+                                               $slotRowsByRevId[$row->rev_id],
+                                               $queryFlags,
+                                               $titlesByPageId[$row->rev_page]
+                                       );
+                               } catch ( MWException $e ) {
+                                       $result->warning( 'internalerror', $e->getMessage() );
+                                       return null;
+                               }
+               }, $rowsByRevId ) );
+               return $result;
+       }
+
        /**
         * Constructs a new MutableRevisionRecord based on the given associative array following
         * the MW1.29 convention for the Revision constructor.
@@ -2324,6 +2445,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 0475557..acecee1 100644 (file)
@@ -125,6 +125,7 @@ class RevisionStoreFactory {
 
                $store = new RevisionStore(
                        $this->dbLoadBalancerFactory->getMainLB( $dbDomain ),
+                       // @phan-suppress-next-line PhanAccessMethodInternal
                        $this->blobStoreFactory->newSqlBlobStore( $dbDomain ),
                        $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
                        $this->commentStore,
index d081629..740377c 100644 (file)
@@ -287,9 +287,9 @@ return [
 
        'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff {
                $config = $services->getMainConfig();
-               $cacheId = \ObjectCache::detectLocalServerCache();
+               $cacheId = ObjectCache::detectLocalServerCache();
 
-               return \ObjectCache::newFromParams( $config->get( 'ObjectCaches' )[$cacheId] );
+               return ObjectCache::newFromParams( $config->get( 'ObjectCaches' )[$cacheId] );
        },
 
        'LockManagerGroupFactory' => function ( MediaWikiServices $services ) : LockManagerGroupFactory {
@@ -318,7 +318,7 @@ return [
                                "Cache type \"$id\" is not present in \$wgObjectCaches." );
                }
 
-               return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
+               return ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
        },
 
        'MainWANObjectCache' => function ( MediaWikiServices $services ) : WANObjectCache {
@@ -338,7 +338,7 @@ return [
                }
                $params['store'] = $mainConfig->get( 'ObjectCaches' )[$objectCacheId];
 
-               return \ObjectCache::newWANCacheFromParams( $params );
+               return ObjectCache::newWANCacheFromParams( $params );
        },
 
        'MediaHandlerFactory' => function ( MediaWikiServices $services ) : MediaHandlerFactory {
@@ -362,6 +362,7 @@ return [
 
        'MessageFormatterFactory' =>
        function ( MediaWikiServices $services ) : IMessageFormatterFactory {
+               // @phan-suppress-next-line PhanAccessMethodInternal
                return new MessageFormatterFactory();
        },
 
@@ -493,8 +494,7 @@ return [
                        // 'class' and 'preprocessorClass'
                        $services->getMainConfig()->get( 'ParserConf' ),
                        // Make sure to have defaults in case someone overrode ParserConf with something silly
-                       [ 'class' => Parser::class,
-                               'preprocessorClass' => Parser::getDefaultPreprocessorClass() ],
+                       [ 'class' => Parser::class, 'preprocessorClass' => Preprocessor_Hash::class ],
                        // Plus a buch of actual config options
                        $services->getMainConfig()
                );
@@ -830,6 +830,7 @@ return [
        },
 
        '_SqlBlobStore' => function ( MediaWikiServices $services ) : SqlBlobStore {
+               // @phan-suppress-next-line PhanAccessMethodInternal
                return $services->getBlobStoreFactory()->newSqlBlobStore();
        },
 
index 4c673c2..d629021 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 );
 }
 
@@ -606,6 +607,7 @@ if ( $wgPHPSessionHandling !== 'enable' &&
 if ( defined( 'MW_NO_SESSION' ) ) {
        // If the entry point wants no session, force 'disable' here unless they
        // specifically set it to the (undocumented) 'warn'.
+       // @phan-suppress-next-line PhanUndeclaredConstant
        $wgPHPSessionHandling = MW_NO_SESSION === 'warn' ? 'warn' : 'disable';
 }
 
@@ -778,14 +780,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 7246238..fd555f6 100644 (file)
@@ -882,6 +882,7 @@ class PageUpdater {
                // TODO: introduce something like an UnsavedRevisionFactory service instead!
                /** @var MutableRevisionRecord $rev */
                $rev = $this->derivedDataUpdater->getRevision();
+               '@phan-var MutableRevisionRecord $rev';
 
                $rev->setPageId( $title->getArticleID() );
 
index 8c5bbdc..547b28c 100644 (file)
@@ -178,8 +178,8 @@ class Title implements LinkTarget, IDBAccessObject {
        /** @var bool Whether a page has any subpages */
        private $mHasSubpages;
 
-       /** @var bool The (string) language code of the page's language and content code. */
-       private $mPageLanguage = false;
+       /** @var array|null The (string) language code of the page's language and content code. */
+       private $mPageLanguage;
 
        /** @var string|bool|null The page language code from the database, null if not saved in
         * the database or false if not loaded, yet.
@@ -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 ) {
@@ -3163,7 +3163,7 @@ class Title implements LinkTarget, IDBAccessObject {
                $this->mLatestID = false;
                $this->mContentModel = false;
                $this->mEstimateRevisions = null;
-               $this->mPageLanguage = false;
+               $this->mPageLanguage = null;
                $this->mDbPageLanguage = false;
                $this->mIsBigDeletion = null;
        }
@@ -3212,6 +3212,7 @@ class Title implements LinkTarget, IDBAccessObject {
                //        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
                /** @var MediaWikiTitleCodec $titleCodec */
                $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
+               '@phan-var MediaWikiTitleCodec $titleCodec';
                // MalformedTitleException can be thrown here
                $parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
 
@@ -3532,7 +3533,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 f696985..895b5a7 100644 (file)
@@ -29,6 +29,8 @@ use Wikimedia\Rdbms\IResultWrapper;
 /**
  * The TitleArray class only exists to provide the newFromResult method at pre-
  * sent.
+ *
+ * @method int count()
  */
 abstract class TitleArray implements Iterator {
        /**
index defe07e..9b8f5a6 100644 (file)
@@ -27,6 +27,7 @@ use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\Session;
 use MediaWiki\Session\SessionId;
 use MediaWiki\Session\SessionManager;
+use Wikimedia\AtEase\AtEase;
 
 // The point of this class is to be a wrapper around super globals
 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
@@ -39,7 +40,10 @@ use MediaWiki\Session\SessionManager;
  * @ingroup HTTP
  */
 class WebRequest {
-       protected $data, $headers = [];
+       /** @var array */
+       protected $data;
+       /** @var array */
+       protected $headers = [];
 
        /**
         * Flag to make WebRequest::getHeader return an array of values.
@@ -114,77 +118,79 @@ class WebRequest {
         * @return array Any query arguments found in path matches.
         */
        public static function getPathInfo( $want = 'all' ) {
-               global $wgUsePathInfo;
                // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
                // And also by Apache 2.x, double slashes are converted to single slashes.
                // So we will use REQUEST_URI if possible.
-               $matches = [];
-               if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
+               if ( isset( $_SERVER['REQUEST_URI'] ) ) {
                        // Slurp out the path portion to examine...
                        $url = $_SERVER['REQUEST_URI'];
                        if ( !preg_match( '!^https?://!', $url ) ) {
                                $url = 'http://unused' . $url;
                        }
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $a = parse_url( $url );
-                       Wikimedia\restoreWarnings();
-                       if ( $a ) {
-                               $path = $a['path'] ?? '';
-
-                               global $wgScript;
-                               if ( $path == $wgScript && $want !== 'all' ) {
-                                       // Script inside a rewrite path?
-                                       // Abort to keep from breaking...
-                                       return $matches;
-                               }
+                       AtEase::restoreWarnings();
+                       if ( !$a ) {
+                               return [];
+                       }
+                       $path = $a['path'] ?? '';
 
-                               $router = new PathRouter;
+                       global $wgScript;
+                       if ( $path == $wgScript && $want !== 'all' ) {
+                               // Script inside a rewrite path?
+                               // Abort to keep from breaking...
+                               return [];
+                       }
 
-                               // Raw PATH_INFO style
-                               $router->add( "$wgScript/$1" );
+                       $router = new PathRouter;
 
-                               if ( isset( $_SERVER['SCRIPT_NAME'] )
-                                       && preg_match( '/\.php/', $_SERVER['SCRIPT_NAME'] )
-                               ) {
-                                       # Check for SCRIPT_NAME, we handle index.php explicitly
-                                       # But we do have some other .php files such as img_auth.php
-                                       # Don't let root article paths clober the parsing for them
-                                       $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
-                               }
-
-                               global $wgArticlePath;
-                               if ( $wgArticlePath ) {
-                                       $router->add( $wgArticlePath );
-                               }
+                       // Raw PATH_INFO style
+                       $router->add( "$wgScript/$1" );
 
-                               global $wgActionPaths;
-                               if ( $wgActionPaths ) {
-                                       $router->add( $wgActionPaths, [ 'action' => '$key' ] );
-                               }
+                       if ( isset( $_SERVER['SCRIPT_NAME'] )
+                               && strpos( $_SERVER['SCRIPT_NAME'], '.php' ) !== false
+                       ) {
+                               // Check for SCRIPT_NAME, we handle index.php explicitly
+                               // But we do have some other .php files such as img_auth.php
+                               // Don't let root article paths clober the parsing for them
+                               $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
+                       }
 
-                               global $wgVariantArticlePath;
-                               if ( $wgVariantArticlePath ) {
-                                       $router->add( $wgVariantArticlePath,
-                                               [ 'variant' => '$2' ],
-                                               [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()->
-                                               getVariants() ]
-                                       );
-                               }
+                       global $wgArticlePath;
+                       if ( $wgArticlePath ) {
+                               $router->add( $wgArticlePath );
+                       }
 
-                               Hooks::run( 'WebRequestPathInfoRouter', [ $router ] );
+                       global $wgActionPaths;
+                       if ( $wgActionPaths ) {
+                               $router->add( $wgActionPaths, [ 'action' => '$key' ] );
+                       }
 
-                               $matches = $router->parse( $path );
+                       global $wgVariantArticlePath;
+                       if ( $wgVariantArticlePath ) {
+                               $router->add( $wgVariantArticlePath,
+                                       [ 'variant' => '$2' ],
+                                       [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()->
+                                       getVariants() ]
+                               );
                        }
-               } elseif ( $wgUsePathInfo ) {
-                       if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
-                               // Mangled PATH_INFO
-                               // https://bugs.php.net/bug.php?id=31892
-                               // Also reported when ini_get('cgi.fix_pathinfo')==false
-                               $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
-
-                       } elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
-                               // Regular old PATH_INFO yay
-                               $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+
+                       Hooks::run( 'WebRequestPathInfoRouter', [ $router ] );
+
+                       $matches = $router->parse( $path );
+               } else {
+                       global $wgUsePathInfo;
+                       $matches = [];
+                       if ( $wgUsePathInfo ) {
+                               if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
+                                       // Mangled PATH_INFO
+                                       // https://bugs.php.net/bug.php?id=31892
+                                       // Also reported when ini_get('cgi.fix_pathinfo')==false
+                                       $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
+                               } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
+                                       // Regular old PATH_INFO yay
+                                       $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+                               }
                        }
                }
 
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 385ccc9..db874f2 100644 (file)
@@ -95,6 +95,7 @@ class HistoryAction extends FormlessAction {
        private function preCacheMessages() {
                // Precache various messages
                if ( !isset( $this->message ) ) {
+                       $this->message = [];
                        $msgs = [ 'cur', 'last', 'pipe-separator' ];
                        foreach ( $msgs as $msg ) {
                                $this->message[$msg] = $this->msg( $msg )->escaped();
index 8a5d7c9..254f7a8 100644 (file)
@@ -118,7 +118,9 @@ class RevertAction extends FormAction {
                $this->useTransactionalTimeLimit();
 
                $old = $this->getRequest()->getText( 'oldimage' );
+               /** @var LocalFile $localFile */
                $localFile = $this->page->getFile();
+               '@phan-var LocalFile $localFile';
                $oldFile = OldLocalFile::newFromArchiveName( $this->getTitle(), $localFile->getRepo(), $old );
 
                $source = $localFile->getArchiveVirtualUrl( $old );
index 2f66277..9a3f75e 100644 (file)
@@ -306,9 +306,10 @@ class ApiAuthManagerHelper {
 
        /**
         * Clean up a field array for output
-        * @param ApiBase $module For context and parameters 'mergerequestfields'
-        *  and 'messageformat'
         * @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 8b6a3e5..0cd9806 100644 (file)
@@ -274,7 +274,7 @@ abstract class ApiBase extends ContextSource {
        /** @var array Maps extension paths to info arrays */
        private static $extensionInfo = null;
 
-       /** @var int[][][] Cache for self::filterIDs() */
+       /** @var stdClass[][] Cache for self::filterIDs() */
        private static $filterIDsCache = [];
 
        /** $var array Map of web UI block messages to corresponding API messages and codes */
@@ -1308,8 +1308,15 @@ abstract class ApiBase extends ContextSource {
                                                }
                                                break;
                                        case 'limit':
+                                               // Must be a number or 'max'
+                                               if ( $value !== 'max' ) {
+                                                       $value = (int)$value;
+                                               }
+                                               if ( $multi ) {
+                                                       self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
+                                               }
                                                if ( !$parseLimit ) {
-                                                       // Don't do any validation whatsoever
+                                                       // Don't do min/max validation and don't parse 'max'
                                                        break;
                                                }
                                                if ( !isset( $paramSettings[self::PARAM_MAX] )
@@ -1320,21 +1327,16 @@ abstract class ApiBase extends ContextSource {
                                                                "MAX1 or MAX2 are not defined for the limit $encParamName"
                                                        );
                                                }
-                                               if ( $multi ) {
-                                                       self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
-                                               }
-                                               $min = $paramSettings[self::PARAM_MIN] ?? 0;
-                                               if ( $value == 'max' ) {
+                                               if ( $value === 'max' ) {
                                                        $value = $this->getMain()->canApiHighLimits()
                                                                ? $paramSettings[self::PARAM_MAX2]
                                                                : $paramSettings[self::PARAM_MAX];
                                                        $this->getResult()->addParsedLimit( $this->getModuleName(), $value );
                                                } else {
-                                                       $value = (int)$value;
                                                        $this->validateLimit(
                                                                $paramName,
                                                                $value,
-                                                               $min,
+                                                               $paramSettings[self::PARAM_MIN] ?? 0,
                                                                $paramSettings[self::PARAM_MAX],
                                                                $paramSettings[self::PARAM_MAX2]
                                                        );
index 2c1564e..30a9242 100644 (file)
@@ -140,8 +140,10 @@ class ApiBlock extends ApiBase {
                        $this->dieStatus( $this->errorArrayToStatus( $retval ) );
                }
 
-               list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
+               $res = [];
+
                $res['user'] = $params['user'];
+               list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
                $res['userID'] = $target instanceof User ? $target->getId() : 0;
 
                $block = DatabaseBlock::newFromTarget( $target, null, true );
index 0e13d70..ad171c6 100644 (file)
@@ -42,6 +42,7 @@ class ApiDelete extends ApiBase {
                $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
                $titleObj = $pageObj->getTitle();
                if ( !$pageObj->exists() &&
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        !( $titleObj->getNamespace() == NS_FILE && self::canDeleteFile( $pageObj->getFile() ) )
                ) {
                        $this->dieWithError( 'apierror-missingtitle' );
@@ -156,6 +157,7 @@ class ApiDelete extends ApiBase {
        ) {
                $title = $page->getTitle();
 
+               // @phan-suppress-next-line PhanUndeclaredMethod There's no right typehint for it
                $file = $page->getFile();
                if ( !self::canDeleteFile( $file ) ) {
                        return self::delete( $page, $user, $reason, $tags );
index 3f63a00..fdf9cf1 100644 (file)
@@ -62,9 +62,7 @@ class ApiEditPage extends ApiBase {
 
                                /** @var Title $newTitle */
                                foreach ( $titles as $id => $newTitle ) {
-                                       if ( !isset( $titles[$id - 1] ) ) {
-                                               $titles[$id - 1] = $oldTitle;
-                                       }
+                                       $titles[ $id - 1 ] = $titles[ $id - 1 ] ?? $oldTitle;
 
                                        $redirValues[] = [
                                                'from' => $titles[$id - 1]->getPrefixedText(),
@@ -380,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 8049cd8..81ee9b9 100644 (file)
@@ -26,6 +26,7 @@
  * ApiResult.
  * @since 1.25
  * @ingroup API
+ * @phan-file-suppress PhanUndeclaredMethod Undeclared methods in IApiMessage
  */
 class ApiErrorFormatter {
        /** @var Title Dummy title to silence warnings from MessageCache::parse() */
index 851373d..a5e7437 100644 (file)
@@ -103,9 +103,12 @@ class ApiExpandTemplates extends ApiBase {
                if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
                        $parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
                        $dom = $parser->preprocessToDom( $params['text'] );
+                       // @phan-suppress-next-line PhanUndeclaredMethodInCallable
                        if ( is_callable( [ $dom, 'saveXML' ] ) ) {
+                               // @phan-suppress-next-line PhanUndeclaredMethod
                                $xml = $dom->saveXML();
                        } else {
+                               // @phan-suppress-next-line PhanUndeclaredMethod
                                $xml = $dom->__toString();
                        }
                        if ( isset( $prop['parsetree'] ) ) {
index c4977f4..953c4d8 100644 (file)
@@ -150,6 +150,7 @@ class ApiFeedWatchlist extends ApiBase {
 
                        if ( $e instanceof ApiUsageException ) {
                                foreach ( $e->getStatusValue()->getErrors() as $error ) {
+                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                        $msg = ApiMessage::create( $error )
                                                ->inLanguage( $this->getLanguage() );
                                        $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() );
index ccb26a8..1f8b012 100644 (file)
@@ -104,6 +104,7 @@ class ApiImageRotate extends ApiBase {
                        $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
                                ->newTempFSFile( 'rotate_', $ext );
                        $dstPath = $tmpFile->getPath();
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $err = $handler->rotate( $file, [
                                'srcPath' => $srcPath,
                                'dstPath' => $dstPath,
@@ -113,6 +114,7 @@ class ApiImageRotate extends ApiBase {
                                $comment = wfMessage(
                                        'rotate-comment'
                                )->numParams( $rotation )->inContentLanguage()->text();
+                               // @phan-suppress-next-line PhanUndeclaredMethod
                                $status = $file->upload(
                                        $dstPath,
                                        $comment,
index be53c67..c4a432c 100644 (file)
@@ -34,6 +34,7 @@ class ApiImportReporter extends ImportReporter {
         * @param int $successCount
         * @param array $pageInfo
         * @return void
+        * @suppress PhanParamSignatureMismatch
         */
        public function reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ) {
                // Add a result entry
index 641aa9f..a9fe258 100644 (file)
@@ -153,6 +153,7 @@ class ApiMain extends ApiBase {
        private $mModule;
 
        private $mCacheMode = 'private';
+       /** @var array */
        private $mCacheControl = [];
        private $mParamsUsed = [];
        private $mParamsSensitive = [];
@@ -166,6 +167,7 @@ class ApiMain extends ApiBase {
         * @param IContextSource|WebRequest|null $context If this is an instance of
         *    FauxRequest, errors are thrown and no printing occurs
         * @param bool $enableWrite Should be set to true if the api may modify data
+        * @suppress PhanUndeclaredMethod
         */
        public function __construct( $context = null, $enableWrite = false ) {
                if ( $context === null ) {
@@ -1706,7 +1708,7 @@ class ApiMain extends ApiBase {
         * @return string
         */
        protected function encodeRequestLogValue( $s ) {
-               static $table;
+               static $table = [];
                if ( !$table ) {
                        $chars = ';@$!*(),/:';
                        $numChars = strlen( $chars );
@@ -1912,6 +1914,10 @@ class ApiMain extends ApiBase {
                ];
        }
 
+       /**
+        * @inheritDoc
+        * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options
+        */
        public function modifyHelp( array &$help, array $options, array &$tocData ) {
                // Wish PHP had an "array_insert_before". Instead, we have to manually
                // reindex the array to get 'permissions' in the right place.
index 147b3bd..528a8b5 100644 (file)
@@ -23,6 +23,7 @@
  * @since 1.27
  * @ingroup API
  * @phan-file-suppress PhanTraitParentReference
+ * @phan-file-suppress PhanUndeclaredMethod
  */
 trait ApiMessageTrait {
 
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 8e2837b..7fcb818 100644 (file)
@@ -71,6 +71,7 @@ class ApiOpenSearch extends ApiBase {
 
                        case 'xml':
                                $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
+                               '@phan-var ApiFormatXML $printer';
                                $printer->setRootElement( 'SearchSuggestion' );
                                return $printer;
 
@@ -96,6 +97,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 );
                                }
@@ -111,6 +113,8 @@ class ApiOpenSearch extends ApiBase {
         * @param string $search the search query
         * @param array $params api request params
         * @return array search results. Keys are integers.
+        * @phan-return array<array{title:Title,redirect_from:?Title,extract:false,extract_trimmed:false,image:false,url:string}>
+        *  Note that phan annotations don't support keys containing a space.
         */
        private function search( $search, array $params ) {
                $searchEngine = $this->buildSearchEngine( $params );
@@ -247,6 +251,7 @@ class ApiOpenSearch extends ApiBase {
                                        if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
                                                $item['Description'] = $r['extract'];
                                        }
+                                       // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                                        if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
                                                $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
                                        }
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 a7390e6..40edafa 100644 (file)
@@ -491,6 +491,7 @@ class ApiParse extends ApiBase {
 
                        $parser = MediaWikiServices::getInstance()->getParser();
                        $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
                        $result_array['parsetree'] = $xml;
                        $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
index bdb0dc2..c78e445 100644 (file)
@@ -223,7 +223,9 @@ class ApiQuery extends ApiBase {
                // Filter modules based on continue parameter
                $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
                $this->setContinuationManager( $continuationManager );
+               /** @var ApiQueryBase[] $modules */
                $modules = $continuationManager->getRunModules();
+               '@phan-var ApiQueryBase[] $modules';
 
                if ( !$continuationManager->isGeneratorDone() ) {
                        // Query modules may optimize data requests through the $this->getPageSet()
@@ -242,7 +244,6 @@ class ApiQuery extends ApiBase {
                $cacheMode = $this->mPageSet->getCacheMode();
 
                // Execute all unfinished modules
-               /** @var ApiQueryBase $module */
                foreach ( $modules as $module ) {
                        $params = $module->extractRequestParams();
                        $cacheMode = $this->mergeCacheMode(
index f82a559..d21f111 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;
index c5a8d08..f9da9a3 100644 (file)
@@ -305,6 +305,8 @@ class ApiQueryBlocks extends ApiQueryBase {
                        $id = $restriction->getBlockId();
                        switch ( $restriction->getType() ) {
                                case 'page':
+                                       /** @var \MediaWiki\Block\Restriction\PageRestriction $restriction */
+                                       '@phan-var \MediaWiki\Block\Restriction\PageRestriction $restriction';
                                        $value = [ 'id' => $restriction->getValue() ];
                                        if ( $restriction->getTitle() ) {
                                                self::addTitleInfo( $value, $restriction->getTitle() );
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 5e737c3..97a9b0a 100644 (file)
@@ -523,6 +523,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                                        $vals['thumbmime'] = $mime;
                                                }
                                        } elseif ( $mto && $mto->isError() ) {
+                                               /** @var MediaTransformError $mto */
+                                               '@phan-var MediaTransformError $mto';
                                                $vals['thumberror'] = $mto->toText();
                                        }
                                }
@@ -562,6 +564,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        // Thus there should be no issue with format=xml.
                        $format = new FormatMetadata;
                        $format->setSingleLanguage( !$opts['multilang'] );
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $format->getContext()->setLanguage( $opts['language'] );
                        $extmetaArray = $format->fetchExtendedMetadata( $file );
                        if ( $opts['extmetadatafilter'] ) {
@@ -581,6 +584,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                }
 
                if ( $archive && $file->isOld() ) {
+                       /** @var OldLocalFile $file */
+                       '@phan-var OldLocalFile $file';
                        $vals['archivename'] = $file->getArchiveName();
                }
 
index ac7e5cc..98474c7 100644 (file)
@@ -118,6 +118,7 @@ class ApiQueryInfo extends ApiQueryBase {
                return $this->tokenFunctions;
        }
 
+       /** @var string[] */
        protected static $cachedTokens = [];
 
        /**
index 0d284c0..90e5480 100644 (file)
@@ -501,6 +501,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
 
                if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
                        if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                               /** @var WikitextContent $content */
+                               '@phan-var WikitextContent $content';
                                $t = $content->getText(); # note: don't set $text
 
                                $parser = MediaWikiServices::getInstance()->getParser();
@@ -510,9 +512,12 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                        Parser::OT_PREPROCESS
                                );
                                $dom = $parser->preprocessToDom( $t );
+                               // @phan-suppress-next-line PhanUndeclaredMethodInCallable
                                if ( is_callable( [ $dom, 'saveXML' ] ) ) {
+                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                        $xml = $dom->saveXML();
                                } else {
+                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                        $xml = $dom->__toString();
                                }
                                $vals['parsetree'] = $xml;
@@ -534,6 +539,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
 
                        if ( $this->expandTemplates && !$this->parseContent ) {
                                if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                                       /** @var WikitextContent $content */
+                                       '@phan-var WikitextContent $content';
                                        $text = $content->getText();
 
                                        $text = MediaWikiServices::getInstance()->getParser()->preprocess(
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 8e26d37..ce51a67 100644 (file)
@@ -332,8 +332,8 @@ class ApiQueryUsers extends ApiQueryBase {
                                }
                        }
 
-                       $fit = $result->addValue( [ 'query', $this->getModuleName() ],
-                               null, $data[$u] );
+                       // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
+                       $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data[$u] );
                        if ( !$fit ) {
                                if ( $useNames ) {
                                        $this->setContinueEnumParameter( 'users',
index c3cf5f1..478b0bc 100644 (file)
@@ -131,9 +131,6 @@ class ApiStashEdit extends ApiBase {
                        return;
                }
 
-               // The user will abort the AJAX request by pressing "save", so ignore that
-               ignore_user_abort( true );
-
                if ( $user->pingLimiter( 'stashedit' ) ) {
                        $status = 'ratelimited';
                } else {
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..373ec11 100644 (file)
@@ -794,6 +794,7 @@ class ApiUpload extends ApiBase {
                }
 
                // No errors, no warnings: do the upload
+               $result = [];
                if ( $this->mParams['async'] ) {
                        $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
                        if ( $progress && $progress['result'] === 'Poll' ) {
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 e9fd0a7..55fc061 100644 (file)
@@ -40,8 +40,8 @@
        "apihelp-main-param-action": "Qué acción se realizará.",
        "apihelp-main-param-format": "El formato de la salida.",
        "apihelp-main-param-maxlag": "Se puede usar el retardo máximo cuando se instala MediaWiki en un clúster replicado de base de datos. Para evitar acciones que causen más retardo en la replicación del sitio, este parámetro puede hacer que el cliente espere hasta que el retardo en la replicación sea menor que el valor especificado. En caso de retardo excesivo, se devuelve el código de error <samp>maxlag</samp> con un mensaje como <samp>Esperando a $host: $lag segundos de retardo</samp>.<br />Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.",
-       "apihelp-main-param-smaxage": "Establece la cabecera HTTP <code>s-maxage</code> de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en caché.",
-       "apihelp-main-param-maxage": "Establece la cabecera HTTP <code>max-age</code> de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en la caché.",
+       "apihelp-main-param-smaxage": "Establece la cabecera HTTP <code>s-maxage</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
+       "apihelp-main-param-maxage": "Establece la cabecera HTTP <code>max-age</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
        "apihelp-main-param-assert": "Comprobar que el usuario haya iniciado sesión si el valor es <kbd>user</kbd> o si tiene el permiso de bot si es <kbd>bot</kbd>.",
        "apihelp-main-param-assertuser": "Verificar el usuario actual es el usuario nombrado.",
        "apihelp-main-param-requestid": "Cualquier valor dado aquí se incluirá en la respuesta. Se puede utilizar para distinguir solicitudes.",
        "apihelp-edit-param-text": "Contenido de la página.",
        "apihelp-edit-param-summary": "Editar resumen. Además de la sección del título cuando $1section=new y $1sectiontitle no están establecidos.",
        "apihelp-edit-param-tags": "Cambia las etiquetas para aplicarlas a la revisión.",
-       "apihelp-edit-param-minor": "Edición menor.",
+       "apihelp-edit-param-minor": "Marcar esta edición como menor.",
        "apihelp-edit-param-notminor": "Edición no menor.",
        "apihelp-edit-param-bot": "Marcar esta como una edición de bot.",
        "apihelp-edit-param-basetimestamp": "Marca de tiempo de la revisión base, usada para detectar conflictos de edición. Se puede obtener mediante [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]",
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 4200341..e7527d1 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,12 +339,13 @@ abstract class AuthenticationRequest {
                                }
 
                                $options['sensitive'] = !empty( $options['sensitive'] );
+                               $type = $options['type'];
 
                                if ( !array_key_exists( $name, $merged ) ) {
                                        $merged[$name] = $options;
-                               } elseif ( $merged[$name]['type'] !== $options['type'] ) {
+                               } elseif ( $merged[$name]['type'] !== $type ) {
                                        throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
-                                               "\"{$merged[$name]['type']}\" vs \"{$options['type']}\""
+                                               "\"{$merged[$name]['type']}\" vs \"$type\""
                                        );
                                } else {
                                        if ( isset( $options['options'] ) ) {
@@ -373,7 +376,7 @@ abstract class AuthenticationRequest {
         * @return AuthenticationRequest
         */
        public static function __set_state( $data ) {
-               // @phan-suppress-next-line PhanTypeInstantiateAbstract
+               // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
                $ret = new static();
                foreach ( $data as $k => $v ) {
                        $ret->$k = $v;
index 06060b1..39bcbf3 100644 (file)
@@ -43,6 +43,7 @@ class RememberMeAuthenticationRequest extends AuthenticationRequest {
        public function __construct() {
                /** @var SessionProvider $provider */
                $provider = SessionManager::getGlobalSession()->getProvider();
+               '@phan-var SessionProvider $provider';
                $this->expiration = $provider->getRememberUserDuration();
        }
 
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 83b59c7..e27ebac 100644 (file)
@@ -223,6 +223,8 @@ class BlockManager {
                        if ( $block instanceof SystemBlock ) {
                                $systemBlocks[] = $block;
                        } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
+                               /** @var DatabaseBlock $block */
+                               '@phan-var DatabaseBlock $block';
                                if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
                                        $databaseBlocks[$block->getParentBlockId()] = $block;
                                }
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 a010e83..e08b877 100644 (file)
@@ -99,7 +99,7 @@ abstract class AbstractRestriction implements Restriction {
         * @inheritDoc
         */
        public static function newFromRow( \stdClass $row ) {
-               // @phan-suppress-next-line PhanTypeInstantiateAbstract
+               // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
                return new static( $row->ir_ipb_id, $row->ir_value );
        }
 
index 45aab46..78d6722 100644 (file)
@@ -87,7 +87,9 @@ class PageRestriction extends AbstractRestriction {
         * @inheritDoc
         */
        public static function newFromRow( \stdClass $row ) {
+               /** @var self $restriction */
                $restriction = parent::newFromRow( $row );
+               '@phan-var self $restriction';
 
                // If the page_namespace and the page_title were provided, add the title to
                // the restriction.
index d717fe7..5dddd78 100644 (file)
@@ -70,7 +70,7 @@ interface Restriction {
         *
         * @since 1.33
         * @param \stdClass $row
-        * @return self
+        * @return static
         */
        public static function newFromRow( \stdClass $row );
 
index d798ddb..d1261a8 100644 (file)
@@ -82,9 +82,9 @@ class CacheHelper implements ICacheHelper {
         * Function that gets called when initialization is done.
         *
         * @since 1.20
-        * @var callable
+        * @var callable|null
         */
-       protected $onInitHandler = false;
+       protected $onInitHandler;
 
        /**
         * Elements to build a cache key with.
@@ -183,7 +183,7 @@ class CacheHelper implements ICacheHelper {
                        $this->hasCached = is_array( $cachedChunks );
                        $this->cachedChunks = $this->hasCached ? $cachedChunks : [];
 
-                       if ( $this->onInitHandler !== false ) {
+                       if ( $this->onInitHandler !== null ) {
                                call_user_func( $this->onInitHandler, $this->hasCached );
                        }
                }
index 3a6d892..848d9c9 100644 (file)
@@ -1191,6 +1191,7 @@ class MessageCache {
                        $class = $wgParserConf['class'];
                        if ( $class == ParserDiffTest::class ) {
                                # Uncloneable
+                               // @phan-suppress-next-line PhanTypeMismatchProperty
                                $this->mParser = new $class( $wgParserConf );
                        } else {
                                $this->mParser = clone $parser;
index aad9439..fd9af39 100644 (file)
@@ -33,7 +33,7 @@ use Cdb\Writer;
  */
 class LCStoreCDB implements LCStore {
 
-       /** @var Reader[] */
+       /** @var Reader[]|false[] */
        private $readers;
 
        /** @var Writer */
index 5911656..53893bd 100644 (file)
@@ -121,6 +121,8 @@ class LCStoreStaticArray implements LCStore {
                        'Generated by LCStoreStaticArray.php -- do not edit!'
                );
                file_put_contents( $this->fname, $out );
+               // Release the data to manage the memory in rebuildLocalisationCache
+               unset( $this->data[$this->currentLang] );
                $this->currentLang = null;
                $this->fname = null;
        }
index ffc7cd0..2646845 100644 (file)
@@ -731,6 +731,7 @@ class LocalisationCache {
                                if ( in_array( $key, self::$mergeableMapKeys ) ) {
                                        $value = $value + $fallbackValue;
                                } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
+                                       // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                                        $value = array_unique( array_merge( $fallbackValue, $value ) );
                                } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
                                        $value = array_merge_recursive( $value, $fallbackValue );
@@ -826,7 +827,7 @@ class LocalisationCache {
                if ( !$code ) {
                        throw new MWException( "Invalid language code requested" );
                }
-               $this->recachedLangs[$code] = true;
+               $this->recachedLangs[ $code ] = true;
 
                # Initial values
                $initialData = array_fill_keys( self::$allKeys, null );
@@ -835,16 +836,11 @@ class LocalisationCache {
 
                # Load the primary localisation from the source file
                $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
-               if ( $data === false ) {
-                       $this->logger->debug( __METHOD__ . ": no localisation file for $code, using fallback to en" );
-                       $coreData['fallback'] = 'en';
-               } else {
-                       $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
+               $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
 
-                       # Merge primary localisation
-                       foreach ( $data as $key => $value ) {
-                               $this->mergeItem( $key, $coreData[$key], $value );
-                       }
+               # Merge primary localisation
+               foreach ( $data as $key => $value ) {
+                       $this->mergeItem( $key, $coreData[ $key ], $value );
                }
 
                # Fill in the fallback if it's not there already
@@ -932,16 +928,14 @@ class LocalisationCache {
                                # Load the secondary localisation from the source file to
                                # avoid infinite cycles on cyclic fallbacks
                                $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
-                               if ( $fbData !== false ) {
-                                       # Only merge the keys that make sense to merge
-                                       foreach ( self::$allKeys as $key ) {
-                                               if ( !isset( $fbData[$key] ) ) {
-                                                       continue;
-                                               }
-
-                                               if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
-                                                       $this->mergeItem( $key, $csData[$key], $fbData[$key] );
-                                               }
+                               # Only merge the keys that make sense to merge
+                               foreach ( self::$allKeys as $key ) {
+                                       if ( !isset( $fbData[ $key ] ) ) {
+                                               continue;
+                                       }
+
+                                       if ( is_null( $coreData[ $key ] ) || $this->isMergeableKey( $key ) ) {
+                                               $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
                                        }
                                }
                        }
index 0382d73..a48e191 100644 (file)
@@ -161,6 +161,7 @@ class ChangesList extends ContextSource {
         */
        private function preCacheMessages() {
                if ( !isset( $this->message ) ) {
+                       $this->message = [];
                        foreach ( [
                                'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
                                'semicolon-separator', 'pipe-separator' ] as $msg
index 4401378..59f59d1 100644 (file)
@@ -8,6 +8,7 @@ use Wikimedia\Rdbms\IDatabase;
  * but 'Bot' is unchecked, hidebots=1 will be sent.
  *
  * @since 1.29
+ * @method ChangesListBooleanFilter[] getFilters()
  */
 class ChangesListBooleanFilterGroup extends ChangesListFilterGroup {
        /**
@@ -55,6 +56,7 @@ class ChangesListBooleanFilterGroup extends ChangesListFilterGroup {
         * Registers a filter in this group
         *
         * @param ChangesListBooleanFilter $filter
+        * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch
         */
        public function registerFilter( ChangesListBooleanFilter $filter ) {
                $this->filters[$filter->getName()] = $filter;
index ec86307..5f0cd22 100644 (file)
@@ -32,6 +32,7 @@ use Wikimedia\Rdbms\IDatabase;
  * Represents a filter group (used on ChangesListSpecialPage and descendants)
  *
  * @since 1.29
+ * @method registerFilter($filter)
  */
 abstract class ChangesListFilterGroup {
        /**
index e06f081..b18ae61 100644 (file)
@@ -155,6 +155,7 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
         * Registers a filter in this group
         *
         * @param ChangesListStringOptionsFilter $filter
+        * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch
         */
        public function registerFilter( ChangesListStringOptionsFilter $filter ) {
                $this->filters[$filter->getName()] = $filter;
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 0c6a3d1..1d590d9 100644 (file)
@@ -90,16 +90,17 @@ class RecentChange implements Taggable {
         */
        const SEND_FEED = false;
 
+       /** @var array */
        public $mAttribs = [];
        public $mExtra = [];
 
        /**
-        * @var Title
+        * @var Title|false
         */
        public $mTitle = false;
 
        /**
-        * @var User
+        * @var User|false
         */
        private $mPerformer = false;
 
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 89f8f76..fc53d13 100644 (file)
 
 /**
  * Generic list for change tagging.
+ *
+ * @property ChangeTagsLogItem $current
+ * @method ChangeTagsLogItem next()
+ * @method ChangeTagsLogItem reset()
+ * @method ChangeTagsLogItem current()
+ * @phan-file-suppress PhanParamSignatureMismatch
  */
 abstract class ChangeTagsList extends RevisionListBase {
        function __construct( IContextSource $context, Title $title, array $ids ) {
index 09d0189..f3d3849 100644 (file)
@@ -161,6 +161,7 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                                                if ( is_array( $etcdResponse['config'] ) ) {
                                                        // Avoid having all servers expire cache keys at the same time
                                                        $expiry = microtime( true ) + $this->baseCacheTTL;
+                                                       // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                                                        $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
                                                        $data = [
                                                                'config' => $etcdResponse['config'],
index 100fa83..9fbb72c 100644 (file)
@@ -1102,7 +1102,7 @@ abstract class ContentHandler {
         * @param Revision|Content $undoafter Must be from an earlier revision than $undo
         * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32)
         *
-        * @return mixed Content on success, false on failure
+        * @return Content|false Content on success, false on failure
         */
        public function getUndoContent( $current, $undo, $undoafter, $undoIsLatest = false ) {
                Assert::parameterType( Revision::class . '|' . Content::class, $current, '$current' );
@@ -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 71dd35c..54a57a5 100644 (file)
@@ -155,7 +155,9 @@ class TextContent extends AbstractContent {
         * @return string|bool The raw text, or false if the conversion failed.
         */
        public function getWikitextForTransclusion() {
+               /** @var WikitextContent $wikitext */
                $wikitext = $this->convert( CONTENT_MODEL_WIKITEXT, 'lossy' );
+               '@phan-var WikitextContent $wikitext';
 
                if ( $wikitext ) {
                        return $wikitext->getText();
@@ -214,7 +216,8 @@ class TextContent extends AbstractContent {
         */
        public function diff( Content $that, Language $lang = null ) {
                $this->checkModelID( $that->getModel() );
-
+               /** @var self $that */
+               '@phan-var self $that';
                // @todo could implement this in DifferenceEngine and just delegate here?
 
                if ( !$lang ) {
index e3dc187..e48dd51 100644 (file)
@@ -45,6 +45,7 @@ class TextContentHandler extends ContentHandler {
        public function serializeContent( Content $content, $format = null ) {
                $this->checkFormat( $format );
 
+               // @phan-suppress-next-line PhanUndeclaredMethod
                return $content->getText();
        }
 
index 1427e2b..a5be21c 100644 (file)
@@ -68,6 +68,7 @@ class UnknownContentHandler extends ContentHandler {
         */
        public function serializeContent( Content $content, $format = null ) {
                /** @var UnknownContent $content */
+               '@phan-var UnknownContent $content';
                return $content->getData();
        }
 
index 70b638b..a760a1b 100644 (file)
@@ -89,6 +89,8 @@ class WikitextContent extends TextContent {
                                "document uses $myModelId but " .
                                "section uses $sectionModelId." );
                }
+               /** @var self $with $oldtext */
+               '@phan-var self $with';
 
                $oldtext = $this->getText();
                $text = $with->getText();
index 6182538..a21f404 100644 (file)
@@ -163,6 +163,7 @@ abstract class ContextSource implements IContextSource {
         * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
         *   or a MessageSpecifier.
         * @param mixed $args,...
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function msg( $key /* $args */ ) {
index d32617e..e4340ce 100644 (file)
@@ -257,6 +257,7 @@ class DerivativeContext extends ContextSource implements MutableContext {
         * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
         *   or a MessageSpecifier.
         * @param mixed $args,... Arguments to wfMessage
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function msg( $key ) {
index 6eeac1c..e6a856c 100644 (file)
@@ -411,6 +411,7 @@ class RequestContext implements IContextSource, MutableContext {
         * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
         *   or a MessageSpecifier.
         * @param mixed $args,...
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function msg( $key ) {
index 3380364..3716971 100644 (file)
@@ -161,6 +161,7 @@ class DeferredUpdates {
                        if ( isset( $queue[$class] ) ) {
                                /** @var MergeableUpdate $existingUpdate */
                                $existingUpdate = $queue[$class];
+                               '@phan-var MergeableUpdate $existingUpdate';
                                $existingUpdate->merge( $update );
                                // Move the update to the end to handle things like mergeable purge
                                // updates that might depend on the prior updates in the queue running
index 70a963b..188135f 100644 (file)
@@ -34,6 +34,7 @@ class ArrayDiffFormatter extends DiffFormatter {
         * @param Diff $diff A Diff object.
         *
         * @return array[] List of associative arrays, each describing a difference.
+        * @suppress PhanParamSignatureMismatch
         */
        public function format( $diff ) {
                $oldline = 1;
index ce507d7..6fa40ea 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;
index 2a1f3e1..df2792f 100644 (file)
@@ -42,12 +42,12 @@ abstract class DiffOp {
        public $type;
 
        /**
-        * @var string[]
+        * @var string[]|false
         */
        public $orig;
 
        /**
-        * @var string[]
+        * @var string[]|false
         */
        public $closing;
 
index 935172a..ef8058c 100644 (file)
@@ -67,6 +67,7 @@ class TextSlotDiffRenderer extends SlotDiffRenderer {
                /** @var TextSlotDiffRenderer $slotDiffRenderer */
                $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
                        ->getSlotDiffRenderer( RequestContext::getMain() );
+               '@phan-var TextSlotDiffRenderer $slotDiffRenderer';
                return $slotDiffRenderer->getTextDiff( $oldText, $newText );
        }
 
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 0b8afa2..f99746e 100644 (file)
@@ -35,7 +35,7 @@ class DumpNamespaceFilter extends DumpFilter {
 
        /**
         * @param DumpOutput &$sink
-        * @param array $param
+        * @param string $param
         * @throws MWException
         */
        function __construct( &$sink, $param ) {
@@ -61,7 +61,7 @@ class DumpNamespaceFilter extends DumpFilter {
                        "NS_CATEGORY"       => NS_CATEGORY,
                        "NS_CATEGORY_TALK"  => NS_CATEGORY_TALK ];
 
-               if ( $param { 0 } == '!' ) {
+               if ( $param[0] == '!' ) {
                        $this->invert = true;
                        $param = substr( $param, 1 );
                }
index a353c44..0521c5a 100644 (file)
@@ -32,6 +32,7 @@ use MediaWiki\Shell\Shell;
  */
 class DumpPipeOutput extends DumpFileOutput {
        protected $command, $filename;
+       /** @var resource|bool */
        protected $procOpenResource = false;
 
        /**
index 9b1571f..5dd3e4c 100644 (file)
@@ -30,6 +30,10 @@ class ExportProgressFilter extends DumpFilter {
         */
        private $progress;
 
+       /**
+        * @param DumpOutput &$sink
+        * @param BackupDumper &$progress
+        */
        function __construct( &$sink, &$progress ) {
                parent::__construct( $sink );
                $this->progress = $progress;
index 3ab88e2..ec0b344 100644 (file)
@@ -30,7 +30,6 @@
 use MediaWiki\MediaWikiServices as MediaWikiServicesAlias;
 use MediaWiki\Storage\RevisionRecord;
 use Wikimedia\Rdbms\IResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
 
 /**
  * @ingroup SpecialPage Dump
@@ -68,7 +67,7 @@ class WikiExporter {
        /** @var XmlDumpWriter */
        private $writer;
 
-       /** @var IDatabase */
+       /** @var Database */
        protected $db;
 
        /** @var array|int */
@@ -87,7 +86,7 @@ class WikiExporter {
        }
 
        /**
-        * @param IDatabase $db
+        * @param Database $db
         * @param int|array $history One of WikiExporter::FULL, WikiExporter::CURRENT,
         *   WikiExporter::RANGE or WikiExporter::STABLE, or an associative array:
         *   - offset: non-inclusive offset at which to start the query
index 0003506..e697ef2 100644 (file)
@@ -658,6 +658,8 @@ class XmlDumpWriter {
         */
        function writeUpload( $file, $dumpContents = false ) {
                if ( $file->isOld() ) {
+                       /** @var OldLocalFile $file */
+                       '@phan-var OldLocalFile $file';
                        $archiveName = "      " .
                                Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
                } else {
index 17cf8f0..7ce2edd 100644 (file)
@@ -24,6 +24,7 @@
 use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBError;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
  * Version of FileJournal that logs to a DB table
@@ -36,12 +37,12 @@ class DBFileJournal extends FileJournal {
        protected $domain;
 
        /**
-        * Construct a new instance from configuration.
+        * Construct a new instance from configuration. Do not call directly, use FileJournal::factory.
         *
         * @param array $config Includes:
         *   domain: database domain ID of the wiki
         */
-       protected function __construct( array $config ) {
+       public function __construct( array $config ) {
                parent::__construct( $config );
 
                $this->domain = $config['domain'] ?? $config['wiki']; // b/c
@@ -64,7 +65,7 @@ class DBFileJournal extends FileJournal {
                        return $status;
                }
 
-               $now = wfTimestamp( TS_UNIX );
+               $now = ConvertibleTimestamp::time();
 
                $data = [];
                foreach ( $entries as $entry ) {
@@ -80,8 +81,11 @@ class DBFileJournal extends FileJournal {
 
                try {
                        $dbw->insert( 'filejournal', $data, __METHOD__ );
+                       // XXX Should we do this deterministically so it's testable? Maybe look at the last two
+                       // digits of a hash of a bunch of the data?
                        if ( mt_rand( 0, 99 ) == 0 ) {
-                               $this->purgeOldLogs(); // occasionally delete old logs
+                               // occasionally delete old logs
+                               $this->purgeOldLogs(); // @codeCoverageIgnore
                        }
                } catch ( DBError $e ) {
                        $status->fatal( 'filejournal-fail-dbquery', $this->backend );
@@ -164,7 +168,7 @@ class DBFileJournal extends FileJournal {
                }
 
                $dbw = $this->getMasterDB();
-               $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays );
+               $dbCutoff = $dbw->timestamp( ConvertibleTimestamp::time() - 86400 * $this->ttlDays );
 
                $dbw->delete( 'filejournal',
                        [ 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ],
index 314c4c3..655fd0d 100644 (file)
@@ -176,10 +176,10 @@ class ForeignAPIRepo extends FileRepo {
 
        /**
         * @param string $virtualUrl
-        * @return false
+        * @return array
         */
        function getFileProps( $virtualUrl ) {
-               return false;
+               return [];
        }
 
        /**
index 5ed937f..84c0a61 100644 (file)
@@ -32,6 +32,7 @@ use Wikimedia\Rdbms\IDatabase;
  * in the wiki's own database. This is the most commonly used repository class.
  *
  * @ingroup FileRepo
+ * @method LocalFile|null newFile( $title, $time = false )
  */
 class LocalRepo extends FileRepo {
        /** @var callable */
@@ -180,7 +181,11 @@ class LocalRepo extends FileRepo {
         * @return string
         */
        public static function getHashFromKey( $key ) {
-               return strtok( $key, '.' );
+               $sha1 = strtok( $key, '.' );
+               if ( is_string( $sha1 ) && strlen( $sha1 ) === 32 && $sha1[0] === '0' ) {
+                       $sha1 = substr( $sha1, 1 );
+               }
+               return $sha1;
        }
 
        /**
index e474ad3..f61ca3b 100644 (file)
@@ -115,6 +115,7 @@ 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
         * @return File|bool False if title is not found
         */
        function findFile( $title, $options = [] ) {
index d14e0de..0d5776b 100644 (file)
@@ -1172,6 +1172,7 @@ abstract class File implements IDBAccessObject {
                        $thumb = false;
                } elseif ( $thumb->isError() ) { // transform error
                        /** @var MediaTransformError $thumb */
+                       '@phan-var MediaTransformError $thumb';
                        $this->lastError = $thumb->toText();
                        // Ignore errors if requested
                        if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
index ab8ef2f..99ead16 100644 (file)
@@ -75,6 +75,7 @@ class ForeignAPIFile extends File {
                                ? count( $data['query']['redirects'] ) - 1
                                : -1;
                        if ( $lastRedirect >= 0 ) {
+                               // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                                $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] );
                                $img = new self( $newtitle, $repo, $info, true );
                                $img->redirectedFrom( $title->getDBkey() );
index f3116e2..3090632 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();
@@ -1758,7 +1760,7 @@ class LocalFile extends File {
 
                                        # Add change tags, if any
                                        if ( $tags ) {
-                                               $logEntry->setTags( $tags );
+                                               $logEntry->addTags( $tags );
                                        }
 
                                        # Uploads can be patrolled
@@ -1865,6 +1867,7 @@ class LocalFile extends File {
                                : FSFile::getSha1Base36FromPath( $srcPath );
                        /** @var FileBackendDBRepoWrapper $wrapperBackend */
                        $wrapperBackend = $repo->getBackend();
+                       '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
                        $dst = $wrapperBackend->getPathForSHA1( $sha1 );
                        $status = $repo->quickImport( $src, $dst );
                        if ( $flags & File::DELETE_SOURCE ) {
@@ -1934,6 +1937,7 @@ class LocalFile extends File {
                                        $oldTitleFile->purgeEverything();
                                        foreach ( $archiveNames as $archiveName ) {
                                                /** @var OldLocalFile $oldTitleFile */
+                                               '@phan-var OldLocalFile $oldTitleFile';
                                                $oldTitleFile->purgeOldThumbnails( $archiveName );
                                        }
                                        $newTitleFile->purgeEverything();
index 991ef79..c6d8ddf 100644 (file)
@@ -75,7 +75,7 @@ abstract class ImageGalleryBase extends ContextSource {
        protected $mHideBadImages;
 
        /**
-        * @var Parser Registered parser object for output callbacks
+        * @var Parser|false Registered parser object for output callbacks
         */
        public $mParser;
 
@@ -88,8 +88,8 @@ abstract class ImageGalleryBase extends ContextSource {
        /** @var array */
        protected $mAttribs = [];
 
-       /** @var bool */
-       private static $modeMapping = false;
+       /** @var array */
+       private static $modeMapping;
 
        /**
         * Get a new image gallery. This is the method other callers
@@ -121,7 +121,7 @@ abstract class ImageGalleryBase extends ContextSource {
        }
 
        private static function loadModes() {
-               if ( self::$modeMapping === false ) {
+               if ( self::$modeMapping === null ) {
                        self::$modeMapping = [
                                'traditional' => TraditionalImageGallery::class,
                                'nolines' => NolinesImageGallery::class,
index 7824872..6e760fa 100644 (file)
  * Improves compression ratio by concatenating like objects before gzipping
  */
 class ConcatenatedGzipHistoryBlob implements HistoryBlob {
-       public $mVersion = 0, $mCompressed = false, $mItems = [], $mDefaultHash = '';
+       public $mVersion = 0;
+       public $mCompressed = false;
+       /**
+        * @var array|string
+        * @fixme Why are some methods treating it as an array, and others as a string, unconditionally?
+        */
+       public $mItems = [];
+       public $mDefaultHash = '';
        public $mSize = 0;
        public $mMaxSize = 10000000;
        public $mMaxCount = 100;
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 ed151e6..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;
@@ -294,6 +298,7 @@ class HTMLForm extends ContextSource {
         *
         * @param string $displayFormat
         * @param mixed $arguments,... Additional arguments to pass to the constructor.
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return HTMLForm
         */
        public static function factory( $displayFormat/*, $arguments...*/ ) {
@@ -982,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 b77c17e..1e4460a 100644 (file)
@@ -5,6 +5,7 @@
  * (defined in htmlform.Element.js) picks up the extra config when constructed using OO.ui.infuse().
  *
  * Currently only supports passing 'hide-if' data.
+ * @phan-file-suppress PhanUndeclaredMethod
  */
 trait HTMLFormElement {
 
index 590b9e7..91c6e6a 100644 (file)
@@ -5,6 +5,7 @@
  * be a subclass of this.
  */
 abstract class HTMLFormField {
+       /** @var array|array[] */
        public $mParams;
 
        protected $mValidationCallback;
index 4ae52a9..63e77ce 100644 (file)
@@ -166,7 +166,6 @@ class HTMLAutoCompleteSelectField extends HTMLTextField {
 
                        $ret = $select->getHTML() . "<br />\n";
 
-                       // @phan-suppress-next-line PhanTypeMismatchDimEmpty
                        $this->mClass[] = 'mw-htmlform-hide-if';
                }
 
@@ -179,7 +178,6 @@ class HTMLAutoCompleteSelectField extends HTMLTextField {
                        }
                }
 
-               // @phan-suppress-next-line PhanTypeMismatchDimEmpty
                $this->mClass[] = 'mw-htmlform-autocomplete';
                $ret .= parent::getInputHTML( $valInSelect ? '' : $value );
                $this->mClass = $oldClass;
index 8e51858..595b71e 100644 (file)
@@ -77,6 +77,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
         * mParams['columns'] is an array with column labels as keys and column tags as values.
         *
         * @param array $value Array of the options that should be checked
+        * @suppress PhanParamSignatureMismatch
         *
         * @return string
         */
index 1c4a785..c373f45 100644 (file)
@@ -137,6 +137,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
         * @since 1.28
         * @param string[] $value
         * @return string|OOUI\CheckboxMultiselectInputWidget
+        * @suppress PhanParamSignatureMismatch
         */
        public function getInputOOUI( $value ) {
                $this->mParent->getOutput()->addModules( 'oojs-ui-widgets' );
index 3af7f56..fa6dad7 100644 (file)
@@ -41,6 +41,7 @@ class GuzzleHttpRequest extends MWHttpRequest {
 
        protected $handler = null;
        protected $sink = null;
+       /** @var array */
        protected $guzzleOptions = [ 'http_errors' => false ];
 
        /**
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..d1c14ae 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
@@ -139,6 +144,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];
@@ -366,7 +372,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
        /**
         * Take care of whatever is necessary to perform the URI request.
         *
-        * @return StatusValue
+        * @return Status
         * @note currently returns Status for B/C
         */
        public function execute() {
index 6d1e242..8cfb605 100644 (file)
@@ -65,4 +65,10 @@ interface ImportableOldRevision {
         */
        public function getSha1Base36();
 
+       /**
+        * @since 1.34
+        * @return string[]
+        */
+       public function getTags();
+
 }
index ad62e16..821d6f6 100644 (file)
@@ -129,6 +129,11 @@ class ImportableOldRevisionImporter implements OldRevisionImporter {
                $revision->insertOn( $dbw );
                $changed = $page->updateIfNewerOn( $dbw, $revision );
 
+               $tags = $importableRevision->getTags();
+               if ( $tags !== [] ) {
+                       ChangeTags::addTags( $tags, null, $revision->getId() );
+               }
+
                if ( $changed !== false && $this->doUpdates ) {
                        $this->logger->debug( __METHOD__ . ": running updates\n" );
                        // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
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 e36d673..c29ba21 100644 (file)
@@ -144,6 +144,12 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
         */
        public $sha1base36 = false;
 
+       /**
+        * @since 1.34
+        * @var string[]
+        */
+       protected $tags = [];
+
        /**
         * @since 1.17
         * @var string
@@ -310,6 +316,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                $this->sha1base36 = $sha1base36;
        }
 
+       /**
+        * @since 1.34
+        * @param string[] $tags
+        */
+       public function setTags( array $tags ) {
+               $this->tags = $tags;
+       }
+
        /**
         * @since 1.12.2
         * @param string $filename
@@ -509,6 +523,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                return false;
        }
 
+       /**
+        * @since 1.34
+        * @return string[]
+        */
+       public function getTags() {
+               return $this->tags;
+       }
+
        /**
         * @since 1.17
         * @return string
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 8b94d97..ac8c9e6 100644 (file)
@@ -177,6 +177,7 @@ abstract class DatabaseInstaller {
         * This will return a cached connection if one is available.
         *
         * @return Status
+        * @suppress PhanUndeclaredMethod
         */
        public function getConnection() {
                if ( $this->db ) {
@@ -341,6 +342,7 @@ abstract class DatabaseInstaller {
        public function setupSchemaVars() {
                $status = $this->getConnection();
                if ( $status->isOK() ) {
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $status->value->setSchemaVars( $this->getSchemaVars() );
                } else {
                        $msg = __METHOD__ . ': unexpected error while establishing'
index 8a9cd05..e1df844 100644 (file)
@@ -1233,6 +1233,7 @@ abstract class DatabaseUpdater {
                $cl = $this->maintenance->runChild(
                        RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
                );
+               '@phan-var RebuildLocalisationCache $cl';
                $this->output( "Rebuilding localisation cache...\n" );
                $cl->setForce();
                $cl->execute();
@@ -1292,6 +1293,7 @@ abstract class DatabaseUpdater {
                        $task = $this->maintenance->runChild(
                                MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php'
                        );
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $task->setForce();
                        $ok = $task->execute();
                        $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
@@ -1329,6 +1331,7 @@ abstract class DatabaseUpdater {
                if ( $this->db->fieldExists( 'archive', 'ar_text', __METHOD__ ) ) {
                        $this->output( "Migrating archive ar_text to modern storage.\n" );
                        $task = $this->maintenance->runChild( MigrateArchiveText::class, 'migrateArchiveText.php' );
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $task->setForce();
                        if ( $task->execute() ) {
                                $this->applyPatch( 'patch-drop-ar_text.sql', false,
index 0fe198e..6d1e211 100644 (file)
@@ -731,6 +731,7 @@ abstract class Installer {
                if ( !$status->isOK() ) {
                        return $status;
                }
+               // @phan-suppress-next-line PhanUndeclaredMethod
                $status->value->insert(
                        'site_stats',
                        [
@@ -1272,7 +1273,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 ) {
@@ -1609,11 +1610,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 69d03bd..383f8d8 100644 (file)
@@ -131,6 +131,7 @@ class MysqlInstaller extends DatabaseInstaller {
                 * @var Database $conn
                 */
                $conn = $status->value;
+               '@phan-var Database $conn';
 
                // Check version
                return static::meetsMinimumRequirement( $conn->getServerVersion() );
index c33d3dd..0d516b4 100644 (file)
@@ -29,6 +29,7 @@ use MediaWiki\MediaWikiServices;
  *
  * @ingroup Deployment
  * @since 1.17
+ * @property DatabaseMysqlBase $db
  */
 class MysqlUpdater extends DatabaseUpdater {
        protected function getCoreUpdateList() {
index d6a5145..9a3d4a3 100644 (file)
@@ -353,6 +353,7 @@ class PostgresInstaller extends DatabaseInstaller {
                        if ( !$status->isOK() ) {
                                return $status;
                        }
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
                }
 
@@ -507,6 +508,7 @@ class PostgresInstaller extends DatabaseInstaller {
                }
                /** @var DatabasePostgres $conn */
                $conn = $status->value;
+               '@phan-var DatabasePostgres $conn';
 
                // Create the schema if necessary
                $schema = $this->getVar( 'wgDBmwschema' );
@@ -542,7 +544,9 @@ class PostgresInstaller extends DatabaseInstaller {
                if ( !$status->isOK() ) {
                        return $status;
                }
+               /** @var DatabasePostgres $conn */
                $conn = $status->value;
+               '@phan-var DatabasePostgres $conn';
 
                $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
                $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
@@ -599,6 +603,7 @@ class PostgresInstaller extends DatabaseInstaller {
 
                /** @var DatabasePostgres $conn */
                $conn = $status->value;
+               '@phan-var DatabasePostgres $conn';
 
                if ( $conn->tableExists( 'archive' ) ) {
                        $status->warning( 'config-install-tables-exist' );
index db26c0b..b6e90a9 100644 (file)
@@ -188,7 +188,9 @@ class WebInstaller extends Installer {
 
                # Special case for Creative Commons partner chooser box.
                if ( $this->request->getVal( 'SubmitCC' ) ) {
+                       /** @var WebInstallerOptions $page */
                        $page = $this->getPageByName( 'Options' );
+                       '@phan-var WebInstallerOptions $page';
                        $this->output->useShortHeader();
                        $this->output->allowFrames();
                        $page->submitCC();
@@ -197,7 +199,9 @@ class WebInstaller extends Installer {
                }
 
                if ( $this->request->getVal( 'ShowCC' ) ) {
+                       /** @var WebInstallerOptions $page */
                        $page = $this->getPageByName( 'Options' );
+                       '@phan-var WebInstallerOptions $page';
                        $this->output->useShortHeader();
                        $this->output->allowFrames();
                        $this->output->addHTML( $page->getCCDoneBox() );
index 991e484..51d4250 100644 (file)
@@ -168,6 +168,7 @@ class WebInstallerOutput {
                foreach ( $moduleNames as $moduleName ) {
                        /** @var ResourceLoaderFileModule $module */
                        $module = $resourceLoader->getModule( $moduleName );
+                       '@phan-var ResourceLoaderFileModule $module';
                        if ( !$module ) {
                                // T98043: Don't fatal, but it won't look as pretty.
                                continue;
index d349ff6..aa0d5ee 100644 (file)
        "config-apc": "[https://www.php.net/apc APC] está instalado",
        "config-apcu": "[https://www.php.net/apcu APCu] está instalado",
        "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] está instalado",
-       "config-no-cache-apcu": "<strong>Atención:</strong> no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en caché de objetos no está activado.",
+       "config-no-cache-apcu": "<strong>Atención:</strong> no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en antememoria de objetos no está activado.",
        "config-mod-security": "<strong>Advertencia:</strong> tu servidor web tiene activado [https://modsecurity.org/ mod_security]/mod_security2. Muchas de sus configuraciones comunes pueden causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrario. De ser posible, deberías desactivarlo. Si no, consulta la [https://modsecurity.org/documentation/ documentación de mod_security] o contacta con el administrador de tu servidor si encuentras errores aleatorios.",
        "config-diff3-bad": "GNU diff3 no se encuentra.",
        "config-git": "Se encontró el software de control de versiones Git: <code>$1</code>.",
        "config-cc-again": "Elegir otra vez...",
        "config-cc-not-chosen": "Elige la licencia Creative Commons que desees y haz clic en \"proceed\".",
        "config-advanced-settings": "Configuración avanzada",
-       "config-cache-options": "Configuración de la caché de objetos:",
+       "config-cache-options": "Configuración de la antememoria de objetos:",
        "config-cache-help": "El almacenamiento en caché de objetos se utiliza para mejorar la velocidad de MediaWiki mediante el almacenamiento en caché los datos usados más frecuentemente.\nA los sitios medianos y grandes se les recomienda que permitirlo. También es beneficioso para los sitios pequeños.",
        "config-cache-none": "Sin almacenamiento en caché (no se pierde ninguna funcionalidad, pero la velocidad puede resentirse en sitios grandes)",
-       "config-cache-accel": "Almacenamiento en caché de objetos PHP (APC, APCu o WinCache)",
+       "config-cache-accel": "Almacenamiento en antememoria de objetos PHP (APC, APCu o WinCache)",
        "config-cache-memcached": "Utilizar Memcached (necesita ser instalado y configurado aparte)",
        "config-memcached-servers": "Servidores Memcached:",
        "config-memcached-help": "Lista de direcciones IP que serán usadas por Memcached.\nDeben especificarse una por cada línea y especificar el puerto a utilizar. Por ejemplo:\n127.0.0.1:11211\n192.168.1.25:1234",
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 0c687f6..7a13bde 100644 (file)
@@ -62,7 +62,7 @@
        "config-page-existingwiki": "Bestaande wiki",
        "config-help-restart": "Wilt u alle opgeslagen gegevens die u hebt ingevoerd wissen en het installatieproces opnieuw starten?",
        "config-restart": "Ja, opnieuw starten",
-       "config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
+       "config-welcome": "=== Omgevingscontrole ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
        "config-welcome-section-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een [$2 exemplaar van de GNU General Public License] ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [https://www.gnu.org/copyleft/gpl.html lees de licentie online].",
        "config-sidebar": "* [https://www.mediawiki.org MediaWiki-thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen]",
        "config-sidebar-readme": "Leesmij",
@@ -83,9 +83,9 @@
        "config-pcre-no-utf8": "<strong>Onherstelbare fout:</strong> de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.",
        "config-memory-raised": "PHP's <code>memory_limit</code> is $1 en is verhoogd tot $2.",
        "config-memory-bad": "'''Waarschuwing:''' PHP's <code>memory_limit</code> is $1.\nDit is waarschijnlijk te laag.\nDe installatie kan mislukken!",
-       "config-apc": "[https://www.php.net/apc APC] is op dit moment geïnstalleerd",
+       "config-apc": "[https://www.php.net/apc APC] is geïnstalleerd",
        "config-apcu": "[https://www.php.net/apcu APCu] is geïnstalleerd",
-       "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is op dit moment geïnstalleerd",
+       "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is geïnstalleerd",
        "config-no-cache-apcu": "<strong>Waarschuwing:</strong> [https://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] of [https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is niet aangetroffen.\nHet cachen van objecten is niet ingeschakeld.",
        "config-mod-security": "<strong>Waarschuwing:</strong> Uw webserver heeft de module [https://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [https://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.",
        "config-diff3-bad": "GNU diff3 niet aangetroffen. U kunt dit voorlopig negeren, maar bewerkingsconflicten kunnen vaker voorkomen.",
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 0c1ef13..35cc348 100644 (file)
@@ -406,6 +406,7 @@ class Message implements MessageSpecifier, Serializable {
         *
         * @param string|string[]|MessageSpecifier $key
         * @param mixed $param,... Parameters as strings.
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         *
         * @return Message
         */
index 9a1796b..fc51439 100644 (file)
@@ -36,6 +36,7 @@ interface MessageLocalizer {
         * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
         *   or a MessageSpecifier.
         * @param mixed $params,... Normal message parameters
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function msg( $key /*...*/ );
index f8ab6a3..94413c2 100644 (file)
@@ -129,6 +129,12 @@ class HashRing implements Serializable {
                        throw new InvalidArgumentException( "Invalid ring source specified." );
                }
 
+               // Short-circuit for the common single-location case. Note that if there was only one
+               // location and it was ejected from the live ring, getLiveRing() would have error out.
+               if ( count( $this->weightByLocation ) == 1 ) {
+                       return ( $limit > 0 ) ? [ $ring[0][self::KEY_LOCATION] ] : [];
+               }
+
                // Locate the node index for this item's position on the hash ring
                $itemIndex = $this->findNodeIndexForPosition( $this->getItemPosition( $item ), $ring );
 
index 4a62e72..0e53038 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 = [] ) {
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 c1a796f..c333a5e 100644 (file)
@@ -137,7 +137,7 @@ class FSFileBackend extends FileBackendStore {
                        }
                }
 
-               return null;
+               return null; // invalid
        }
 
        /**
@@ -576,25 +576,23 @@ class FSFileBackend extends FileBackendStore {
        protected function doGetFileStat( array $params ) {
                $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
-                       return false; // invalid storage path
+                       return self::$RES_ERROR; // invalid storage path
                }
 
                $this->trapWarnings(); // don't trust 'false' if there were errors
                $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
                $hadError = $this->untrapWarnings();
 
-               if ( $stat ) {
+               if ( is_array( $stat ) ) {
                        $ct = new ConvertibleTimestamp( $stat['mtime'] );
 
                        return [
                                'mtime' => $ct->getTimestamp( TS_MW ),
                                'size' => $stat['size']
                        ];
-               } elseif ( !$hadError ) {
-                       return false; // file does not exist
-               } else {
-                       return self::UNKNOWN; // failure
                }
+
+               return $hadError ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doClearCache( array $paths = null ) {
@@ -610,7 +608,7 @@ class FSFileBackend extends FileBackendStore {
                $exists = is_dir( $dir );
                $hadError = $this->untrapWarnings();
 
-               return $hadError ? self::UNKNOWN : $exists;
+               return $hadError ? self::$RES_ERROR : $exists;
        }
 
        /**
@@ -624,18 +622,27 @@ class FSFileBackend extends FileBackendStore {
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
                $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+               $isReadable = $exists ? is_readable( $dir ) : false;
+               $hadError = $this->untrapWarnings();
 
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+               if ( $isReadable ) {
+                       return new FSFileBackendDirList( $dir, $params );
+               } elseif ( $exists ) {
+                       $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'" );
 
-                       return self::UNKNOWN; // bad permissions?
-               }
+                       return self::$RES_ERROR; // bad permissions?
+               } elseif ( $hadError ) {
+                       $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'" );
+
+                       return self::$RES_ERROR;
+               } else {
+                       $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'" );
 
-               return new FSFileBackendDirList( $dir, $params );
+                       return []; // nothing under this dir
+               }
        }
 
        /**
@@ -649,18 +656,27 @@ class FSFileBackend extends FileBackendStore {
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
                $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+               $isReadable = $exists ? is_readable( $dir ) : false;
+               $hadError = $this->untrapWarnings();
 
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+               if ( $exists && $isReadable ) {
+                       return new FSFileBackendFileList( $dir, $params );
+               } elseif ( $exists ) {
+                       $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'\n" );
 
-                       return self::UNKNOWN; // bad permissions?
-               }
+                       return self::$RES_ERROR; // bad permissions?
+               } elseif ( $hadError ) {
+                       $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'\n" );
 
-               return new FSFileBackendFileList( $dir, $params );
+                       return self::$RES_ERROR;
+               } else {
+                       $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'\n" );
+
+                       return []; // nothing under this dir
+               }
        }
 
        protected function doGetLocalReferenceMulti( array $params ) {
@@ -668,10 +684,21 @@ class FSFileBackend extends FileBackendStore {
 
                foreach ( $params['srcs'] as $src ) {
                        $source = $this->resolveToFSPath( $src );
-                       if ( $source === null || !is_file( $source ) ) {
-                               $fsFiles[$src] = null; // invalid path or file does not exist
-                       } else {
+                       if ( $source === null ) {
+                               $fsFiles[$src] = self::$RES_ERROR; // invalid path
+                               continue;
+                       }
+
+                       $this->trapWarnings(); // don't trust 'false' if there were errors
+                       $isFile = is_file( $source ); // regular files only
+                       $hadError = $this->untrapWarnings();
+
+                       if ( $isFile ) {
                                $fsFiles[$src] = new FSFile( $source );
+                       } elseif ( $hadError ) {
+                               $fsFiles[$src] = self::$RES_ERROR;
+                       } else {
+                               $fsFiles[$src] = self::$RES_ABSENT;
                        }
                }
 
@@ -684,26 +711,31 @@ class FSFileBackend extends FileBackendStore {
                foreach ( $params['srcs'] as $src ) {
                        $source = $this->resolveToFSPath( $src );
                        if ( $source === null ) {
-                               $tmpFiles[$src] = null; // invalid path
+                               $tmpFiles[$src] = self::$RES_ERROR; // invalid path
+                               continue;
+                       }
+                       // Create a new temporary file with the same extension...
+                       $ext = FileBackend::extensionFromPath( $src );
+                       $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
+                       if ( !$tmpFile ) {
+                               $tmpFiles[$src] = self::$RES_ERROR;
+                               continue;
+                       }
+
+                       $tmpPath = $tmpFile->getPath();
+                       // Copy the source file over the temp file
+                       $this->trapWarnings();
+                       $isFile = is_file( $source ); // regular files only
+                       $copySuccess = $isFile ? copy( $source, $tmpPath ) : false;
+                       $hadError = $this->untrapWarnings();
+
+                       if ( $copySuccess ) {
+                               $this->chmod( $tmpPath );
+                               $tmpFiles[$src] = $tmpFile;
+                       } elseif ( $hadError ) {
+                               $tmpFiles[$src] = self::$RES_ERROR; // copy failed
                        } else {
-                               // Create a new temporary file with the same extension...
-                               $ext = FileBackend::extensionFromPath( $src );
-                               $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
-                               if ( !$tmpFile ) {
-                                       $tmpFiles[$src] = null;
-                               } else {
-                                       $tmpPath = $tmpFile->getPath();
-                                       // Copy the source file over the temp file
-                                       $this->trapWarnings();
-                                       $ok = copy( $source, $tmpPath );
-                                       $this->untrapWarnings();
-                                       if ( !$ok ) {
-                                               $tmpFiles[$src] = null;
-                                       } else {
-                                               $this->chmod( $tmpPath );
-                                               $tmpFiles[$src] = $tmpFile;
-                                       }
-                               }
+                               $tmpFiles[$src] = self::$RES_ABSENT;
                        }
                }
 
index 905e925..6ab1707 100644 (file)
@@ -131,8 +131,28 @@ abstract class FileBackend implements LoggerAwareInterface {
        const ATTR_METADATA = 2; // files can be stored with metadata key/values
        const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
 
-       /** @var null Idiom for "could not determine due to I/O errors" */
-       const UNKNOWN = null;
+       /** @var false Idiom for "no info; non-existant file" (since 1.34) */
+       const STAT_ABSENT = false;
+
+       /** @var null Idiom for "no info; I/O errors" (since 1.34) */
+       const STAT_ERROR = null;
+       /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */
+       const LIST_ERROR = null;
+       /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */
+       const TEMPURL_ERROR = null;
+       /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */
+       const EXISTENCE_ERROR = null;
+
+       /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */
+       const TIMESTAMP_FAIL = false;
+       /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */
+       const CONTENT_FAIL = false;
+       /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */
+       const XATTRS_FAIL = false;
+       /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */
+       const SIZE_FAIL = false;
+       /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */
+       const SHA1_FAIL = false;
 
        /**
         * Create a new backend instance from configuration.
@@ -157,9 +177,9 @@ abstract class FileBackend implements LoggerAwareInterface {
         *      Allowed values are "implicit", "explicit" and "off".
         *   - concurrency : How many file operations can be done in parallel.
         *   - tmpDirectory : Directory to use for temporary files.
-        *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if tmpDirectory is
-        *      not set. If both are unset or null, then the backend will try to discover a usable
-        *      temporary directory.
+        *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if
+        *      tmpDirectory is not set. If both are unset or null, then the backend will
+        *      try to discover a usable temporary directory.
         *   - obResetFunc : alternative callback to clear the output buffer
         *   - streamMimeFunc : alternative method to determine the content type from the path
         *   - logger : Optional PSR logger object.
@@ -428,7 +448,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 +690,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
         */
@@ -940,20 +966,29 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Check if a file exists at a storage path in the backend.
         * This returns false if only a directory exists at the path.
         *
+        * Callers that only care if a file is readily accessible can use non-strict
+        * comparisons on the result. If "does not exist" and "existence is unknown"
+        * must be distinguished, then strict comparisons to true/null should be used.
+        *
+        * @see FileBackend::EXISTENCE_ERROR
+        * @see FileBackend::directoryExists()
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return bool|null Returns null on failure
+        * @return bool|null Whether the file exists or null (I/O error)
         */
        abstract public function fileExists( array $params );
 
        /**
         * Get the last-modified timestamp of the file at a storage path.
         *
+        * @see FileBackend::TIMESTAMP_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool TS_MW timestamp or false on failure
+        * @return string|false TS_MW timestamp or false (missing file or I/O error)
         */
        abstract public function getFileTimestamp( array $params );
 
@@ -961,22 +996,22 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Get the contents of a file at a storage path in the backend.
         * This should be avoided for potentially large files.
         *
+        * @see FileBackend::CONTENT_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool Returns false on failure
+        * @return string|false Content string or false (missing file or I/O error)
         */
        final public function getFileContents( array $params ) {
-               $contents = $this->getFileContentsMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $contents[$params['src']];
        }
 
        /**
         * Like getFileContents() except it takes an array of storage paths
-        * and returns a map of storage paths to strings (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * and returns an order preserved map of storage paths to their content.
         *
         * @see FileBackend::getFileContents()
         *
@@ -984,7 +1019,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - srcs        : list of source storage paths
         *   - latest      : use the latest available data
         *   - parallelize : try to do operations in parallel when possible
-        * @return array Map of (path name => string or false on failure)
+        * @return string[]|false[] Map of (path name => file content or false on failure)
         * @since 1.20
         */
        abstract public function getFileContentsMulti( array $params );
@@ -1000,11 +1035,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Use FileBackend::hasFeatures() to check how well this is supported.
         *
+        * @see FileBackend::XATTRS_FAIL
+        *
         * @param array $params
         * $params include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array|bool Returns false on failure
+        * @return array|false File metadata array or false (missing file or I/O error)
         * @since 1.23
         */
        abstract public function getFileXAttributes( array $params );
@@ -1012,10 +1049,12 @@ abstract class FileBackend implements LoggerAwareInterface {
        /**
         * Get the size (bytes) of a file at a storage path in the backend.
         *
+        * @see FileBackend::SIZE_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return int|bool Returns false on failure
+        * @return int|false File size in bytes or false (missing file or I/O error)
         */
        abstract public function getFileSize( array $params );
 
@@ -1027,36 +1066,41 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - size   : the file size (bytes)
         * Additional values may be included for internal use only.
         *
+        * @see FileBackend::STAT_ABSENT
+        * @see FileBackend::STAT_ERROR
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array|bool|null Returns null on failure
+        * @return array|false|null Attribute map, false (missing file), or null (I/O error)
         */
        abstract public function getFileStat( array $params );
 
        /**
-        * Get a SHA-1 hash of the file at a storage path in the backend.
+        * Get a SHA-1 hash of the content of the file at a storage path in the backend.
+        *
+        * @see FileBackend::SHA1_FAIL
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool Hash string or false on failure
+        * @return string|false Hash string or false (missing file or I/O error)
         */
        abstract public function getFileSha1Base36( array $params );
 
        /**
-        * Get the properties of the file at a storage path in the backend.
+        * Get the properties of the content of the file at a storage path in the backend.
         * This gives the result of FSFile::getProps() on a local copy of the file.
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array Returns FSFile::placeholderProps() on failure
+        * @return array Properties map; FSFile::placeholderProps() if file missing or on I/O error
         */
        abstract public function getFileProps( array $params );
 
        /**
-        * Stream the file at a storage path in the backend.
+        * Stream the content of the file at a storage path in the backend.
         *
         * If the file does not exists, an HTTP 404 error will be given.
         * Appropriate HTTP headers (Status, Content-Type, Content-Length)
@@ -1077,34 +1121,36 @@ abstract class FileBackend implements LoggerAwareInterface {
        abstract public function streamFile( array $params );
 
        /**
-        * Returns a file system file, identical to the file at a storage path.
+        * Returns a file system file, identical in content to the file at a storage path.
         * The file returned is either:
-        *   - a) A local copy of the file at a storage path in the backend.
+        *   - a) A TempFSFile local copy of the file at a storage path in the backend.
         *        The temporary copy will have the same extension as the source.
-        *   - b) An original of the file at a storage path in the backend.
-        * Temporary files may be purged when the file object falls out of scope.
+        *        Temporary files may be purged when the file object falls out of scope.
+        *   - b) An FSFile pointing to the original file at a storage path in the backend.
+        *        This is applicable for backends layered directly on top of file systems.
         *
-        * Write operations should *never* be done on this file as some backends
-        * may do internal tracking or may be instances of FileBackendMultiWrite.
-        * In that latter case, there are copies of the file that must stay in sync.
-        * Additionally, further calls to this function may return the same file.
+        * Never modify the returned file since it might be the original, it might be shared
+        * among multiple callers of this method, or the backend might internally keep FSFile
+        * references for deferred operations.
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return FSFile|null Returns null on failure
+        * @return FSFile|null Local file copy or null (missing file or I/O error)
         */
        final public function getLocalReference( array $params ) {
-               $fsFiles = $this->getLocalReferenceMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $fsFiles[$params['src']];
        }
 
        /**
-        * Like getLocalReference() except it takes an array of storage paths
-        * and returns a map of storage paths to FSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * Like getLocalReference() except it takes an array of storage paths and
+        * yields an order-preserved map of storage paths to temporary local file copies.
+        *
+        * Never modify the returned files since they might be originals, they might be shared
+        * among multiple callers of this method, or the backend might internally keep FSFile
+        * references for deferred operations.
         *
         * @see FileBackend::getLocalReference()
         *
@@ -1122,22 +1168,24 @@ abstract class FileBackend implements LoggerAwareInterface {
         * The temporary copy will have the same file extension as the source.
         * Temporary files may be purged when the file object falls out of scope.
         *
+        * Multiple calls to this method for the same path will create new copies.
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return TempFSFile|null Returns null on failure
+        * @return TempFSFile|null Temporary local file copy or null (missing file or I/O error)
         */
        final public function getLocalCopy( array $params ) {
-               $tmpFiles = $this->getLocalCopyMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $tmpFiles[$params['src']];
        }
 
        /**
-        * Like getLocalCopy() except it takes an array of storage paths and
-        * returns a map of storage paths to TempFSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * Like getLocalCopy() except it takes an array of storage paths and yields
+        * an order preserved-map of storage paths to temporary local file copies.
+        *
+        * Multiple calls to this method for the same path will create new copies.
         *
         * @see FileBackend::getLocalCopy()
         *
@@ -1160,10 +1208,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Otherwise, one would need to use getLocalReference(), which involves loading
         * the entire file on to local disk.
         *
+        * @see FileBackend::TEMPURL_ERROR
+        *
         * @param array $params Parameters include:
         *   - src : source storage path
         *   - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
-        * @return string|null
+        * @return string|null URL or null (not supported or I/O error)
         * @since 1.21
         */
        abstract public function getFileHttpUrl( array $params );
@@ -1185,11 +1235,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Storage backends with eventual consistency might return stale data.
         *
+        * @see FileBackend::EXISTENCE_ERROR
         * @see FileBackend::clean()
         *
         * @param array $params Parameters include:
         *   - dir : storage directory
-        * @return bool|null Whether a directory exists or null on failure
+        * @return bool|null Whether a directory exists or null (I/O error)
         * @since 1.20
         */
        abstract public function directoryExists( array $params );
@@ -1207,12 +1258,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
         * @see FileBackend::directoryExists()
         *
         * @param array $params Parameters include:
         *   - dir     : storage directory
         *   - topOnly : only return direct child dirs of the directory
-        * @return Traversable|array|null Directory list enumerator null on failure
+        * @return Traversable|array|null Directory list enumerator or null (initial I/O error)
         * @since 1.20
         */
        abstract public function getDirectoryList( array $params );
@@ -1225,11 +1277,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
         * @see FileBackend::directoryExists()
         *
         * @param array $params Parameters include:
         *   - dir : storage directory
-        * @return Traversable|array|null Directory list enumerator or null on failure
+        * @return Traversable|array|null Directory list enumerator or null (initial I/O error)
         * @since 1.20
         */
        final public function getTopDirectoryList( array $params ) {
@@ -1248,11 +1301,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
+        *
         * @param array $params Parameters include:
         *   - dir        : storage directory
         *   - topOnly    : only return direct child files of the directory (since 1.20)
         *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
-        * @return Traversable|array|null File list enumerator or null on failure
+        * @return Traversable|array|null File list enumerator or null (initial I/O error)
         */
        abstract public function getFileList( array $params );
 
@@ -1264,6 +1319,8 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
+        *
         * @param array $params Parameters include:
         *   - dir        : storage directory
         *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
@@ -1354,7 +1411,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * @param int|string $type LockManager::LOCK_* constant or "mixed"
         * @param StatusValue $status StatusValue to update on lock/unlock
         * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
-        * @return ScopedLock|null Returns null on failure
+        * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
         */
        final public function getScopedFileLocks(
                array $paths, $type, StatusValue $status, $timeout = 0
@@ -1383,7 +1440,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * @param array $ops List of file operations to FileBackend::doOperations()
         * @param StatusValue $status StatusValue to update on lock/unlock
-        * @return ScopedLock|null
+        * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
         * @since 1.20
         */
        abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
@@ -1481,7 +1538,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Returns null if the path is not of the format of a valid storage path.
         *
         * @param string $storagePath
-        * @return string|null
+        * @return string|null Normalized storage path or null on failure
         */
        final public static function normalizeStoragePath( $storagePath ) {
                list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
@@ -1503,7 +1560,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * "mwstore://backend/container/...", or null if there is no parent.
         *
         * @param string $storagePath
-        * @return string|null
+        * @return string|null Parent storage path or null on failure
         */
        final public static function parentStoragePath( $storagePath ) {
                $storagePath = dirname( $storagePath );
@@ -1576,7 +1633,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * This uses the same traversal protection as Title::secureAndSplit().
         *
         * @param string $path Storage path relative to a container
-        * @return string|null
+        * @return string|null Normalized container path or null on failure
         */
        final protected static function normalizeContainerPath( $path ) {
                // Normalize directory separators
index 27ad870..cbfd76e 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::STAT_ERROR ) {
+                               $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::STAT_ERROR ) {
+                               $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::STAT_ERROR ) {
+                                       $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;
@@ -448,7 +501,7 @@ class FileBackendMultiWrite extends FileBackend {
         *
         * @param array|string $paths List of paths or single string path
         * @param FileBackendStore $backend
-        * @return array|string
+        * @return string[]|string
         */
        protected function substPaths( $paths, FileBackendStore $backend ) {
                return preg_replace(
@@ -462,12 +515,13 @@ class FileBackendMultiWrite extends FileBackend {
         * Substitute the backend of internal storage paths with the proxy backend's name
         *
         * @param array|string $paths List of paths or single string path
-        * @return array|string
+        * @param FileBackendStore $backend internal storage backend
+        * @return string[]|string
         */
-       protected function unsubstPaths( $paths ) {
+       protected function unsubstPaths( $paths, FileBackendStore $backend ) {
                return preg_replace(
-                       '!^mwstore://([^/]+)!',
-                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
+                       '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!',
+                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ),
                        $paths // string or array
                );
        }
@@ -488,7 +542,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 );
@@ -621,7 +675,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $contents = []; // (path => FSFile) mapping using the proxy backend's name
                foreach ( $contentsM as $path => $data ) {
-                       $contents[$this->unsubstPaths( $path )] = $data;
+                       $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data;
                }
 
                return $contents;
@@ -656,7 +710,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
                foreach ( $fsFilesM as $path => $fsFile ) {
-                       $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
+                       $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile;
                }
 
                return $fsFiles;
@@ -670,7 +724,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
                foreach ( $tempFilesM as $path => $tempFile ) {
-                       $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
+                       $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile;
                }
 
                return $tempFiles;
@@ -731,8 +785,14 @@ class FileBackendMultiWrite extends FileBackend {
                $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
                // Get the paths under the proxy backend's name
                $pbPaths = [
-                       LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
-                       LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
+                       LockManager::LOCK_UW => $this->unsubstPaths(
+                               $paths[LockManager::LOCK_UW],
+                               $this->backends[$this->masterIndex]
+                       ),
+                       LockManager::LOCK_EX => $this->unsubstPaths(
+                               $paths[LockManager::LOCK_EX],
+                               $this->backends[$this->masterIndex]
+                       )
                ];
 
                // Actually acquire the locks
index e637565..f2c07e8 100644 (file)
@@ -59,6 +59,16 @@ abstract class FileBackendStore extends FileBackend {
        const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
        const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
 
+       /** @var false Idiom for "no result due to missing file" (since 1.34) */
+       protected static $RES_ABSENT = false;
+       /** @var null Idiom for "no result due to I/O errors" (since 1.34) */
+       protected static $RES_ERROR = null;
+
+       /** @var string File does not exist according to a normal stat query */
+       protected static $ABSENT_NORMAL = 'FNE-N';
+       /** @var string File does not exist according to a "latest"-mode stat query */
+       protected static $ABSENT_LATEST = 'FNE-L';
+
        /**
         * @see FileBackend::__construct()
         * Additional $config params include:
@@ -91,9 +101,10 @@ abstract class FileBackendStore extends FileBackend {
        }
 
        /**
-        * Check if a file can be created or changed at a given storage path.
-        * FS backends should check if the parent directory exists, files can be
-        * written under it, and that any file already there is writable.
+        * Check if a file can be created or changed at a given storage path in the backend
+        *
+        * FS backends should check that the parent directory exists, files can be written
+        * under it, and that any file already there is both readable and writable.
         * Backends using key/value stores should check if the container exists.
         *
         * @param string $storagePath
@@ -122,6 +133,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function createInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
                        $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
@@ -164,6 +176,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function storeInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
                        $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
@@ -207,6 +220,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function copyInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doCopyInternal( $params );
                $this->clearCache( [ $params['dst'] ] );
                if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
@@ -240,6 +254,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function deleteInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( [ $params['src'] ] );
                $this->deleteFileCache( $params['src'] ); // persistent cache
@@ -275,6 +290,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function moveInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doMoveInternal( $params );
                $this->clearCache( [ $params['src'], $params['dst'] ] );
                $this->deleteFileCache( $params['src'] ); // persistent cache
@@ -322,6 +338,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function describeInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( count( $params['headers'] ) ) {
                        $status = $this->doDescribeInternal( $params );
                        $this->clearCache( [ $params['src'] ] );
@@ -617,54 +634,71 @@ abstract class FileBackendStore extends FileBackend {
        final public function fileExists( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return true;
+               }
 
-               return ( $stat === self::UNKNOWN ) ? self::UNKNOWN : (bool)$stat;
+               return ( $stat === self::$RES_ABSENT ) ? false : self::EXISTENCE_ERROR;
        }
 
        final public function getFileTimestamp( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return $stat['mtime'];
+               }
 
-               return $stat ? $stat['mtime'] : false;
+               return self::TIMESTAMP_FAIL; // all failure cases
        }
 
        final public function getFileSize( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return $stat['size'];
+               }
 
-               return $stat ? $stat['size'] : false;
+               return self::SIZE_FAIL; // all failure cases
        }
 
        final public function getFileStat( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::STAT_ERROR; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
-               $latest = !empty( $params['latest'] ); // use latest data?
-               $requireSHA1 = !empty( $params['requireSHA1'] ); // require SHA-1 if file exists?
+               // Whether to bypass cache except for process cache entries loaded directly from
+               // high consistency backend queries (caller handles any cache flushing and locking)
+               $latest = !empty( $params['latest'] );
+               // Whether to ignore cache entries missing the SHA-1 field for existing files
+               $requireSHA1 = !empty( $params['requireSHA1'] );
 
+               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
+               // Load the persistent stat cache into process cache if needed
                if ( !$latest ) {
-                       $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
-                       // Note that some backends, like SwiftFileBackend, sometimes set file stat process
-                       // cache entries from mass object listings that do not include the SHA-1. In that
-                       // case, loading the persistent stat cache will likely yield the SHA-1.
                        if (
-                               $stat === self::UNKNOWN ||
+                               // File stat is not in process cache
+                               $stat === null ||
+                               // Key/value store backends might opportunistically set file stat process
+                               // cache entries from object listings that do not include the SHA-1. In that
+                               // case, loading the persistent stat cache will likely yield the SHA-1.
                                ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) )
                        ) {
-                               $this->primeFileCache( [ $path ] ); // check persistent cache
+                               $this->primeFileCache( [ $path ] );
+                               // Get any newly process-cached entry
+                               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
                        }
                }
 
-               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
-               // If we want the latest data, check that this cached
-               // value was in fact fetched with the latest available data.
                if ( is_array( $stat ) ) {
                        if (
                                ( !$latest || $stat['latest'] ) &&
@@ -672,42 +706,90 @@ abstract class FileBackendStore extends FileBackend {
                        ) {
                                return $stat;
                        }
-               } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ], true ) ) {
-                       if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
-                               return false;
+               } elseif ( $stat === self::$ABSENT_LATEST ) {
+                       return self::STAT_ABSENT;
+               } elseif ( $stat === self::$ABSENT_NORMAL ) {
+                       if ( !$latest ) {
+                               return self::STAT_ABSENT;
                        }
                }
 
+               // Load the file stat from the backend and update caches
                $stat = $this->doGetFileStat( $params );
+               $this->ingestFreshFileStats( [ $path => $stat ], $latest );
 
-               if ( is_array( $stat ) ) { // file exists
-                       // Strongly consistent backends can automatically set "latest"
-                       $stat['latest'] = $stat['latest'] ?? $latest;
-                       $this->cheapCache->setField( $path, 'stat', $stat );
-                       $this->setFileCache( $path, $stat ); // update persistent cache
-                       if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                               $this->cheapCache->setField( $path, 'sha1',
-                                       [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                       }
-                       if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                               $this->cheapCache->setField( $path, 'xattr',
-                                       [ 'map' => $stat['xattr'], 'latest' => $latest ] );
+               if ( is_array( $stat ) ) {
+                       return $stat;
+               }
+
+               return ( $stat === self::$RES_ERROR ) ? self::STAT_ERROR : self::STAT_ABSENT;
+       }
+
+       /**
+        * Ingest file stat entries that just came from querying the backend (not cache)
+        *
+        * @param array[]|bool[]|null[] $stats Map of (path => doGetFileStat() stype result)
+        * @param bool $latest Whether doGetFileStat()/doGetFileStatMulti() had the 'latest' flag
+        * @return bool Whether all files have non-error stat replies
+        */
+       final protected function ingestFreshFileStats( array $stats, $latest ) {
+               $success = true;
+
+               foreach ( $stats as $path => $stat ) {
+                       if ( is_array( $stat ) ) {
+                               // Strongly consistent backends might automatically set this flag
+                               $stat['latest'] = $stat['latest'] ?? $latest;
+
+                               $this->cheapCache->setField( $path, 'stat', $stat );
+                               if ( isset( $stat['sha1'] ) ) {
+                                       // Some backends store the SHA-1 hash as metadata
+                                       $this->cheapCache->setField(
+                                               $path,
+                                               'sha1',
+                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ]
+                                       );
+                               }
+                               if ( isset( $stat['xattr'] ) ) {
+                                       // Some backends store custom headers/metadata
+                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                                       $this->cheapCache->setField(
+                                               $path,
+                                               'xattr',
+                                               [ 'map' => $stat['xattr'], 'latest' => $latest ]
+                                       );
+                               }
+                               // Update persistent cache (@TODO: set all entries in one batch)
+                               $this->setFileCache( $path, $stat );
+                       } elseif ( $stat === self::$RES_ABSENT ) {
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'stat',
+                                       $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL
+                               );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'xattr',
+                                       [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
+                               );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'sha1',
+                                       [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
+                               );
+                               $this->logger->debug(
+                                       __METHOD__ . ': File {path} does not exist',
+                                       [ 'path' => $path ]
+                               );
+                       } else {
+                               $success = false;
+                               $this->logger->error(
+                                       __METHOD__ . ': Could not stat file {path}',
+                                       [ 'path' => $path ]
+                               );
                        }
-               } elseif ( $stat === false ) { // file does not exist
-                       $this->cheapCache->setField( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                       $this->cheapCache->setField( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
-                       $this->cheapCache->setField( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
-                       $this->logger->debug( __METHOD__ . ': File {path} does not exist', [
-                               'path' => $path,
-                       ] );
-               } else { // an error occurred
-                       $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
-                               'path' => $path,
-                       ] );
                }
 
-               return $stat;
+               return $success;
        }
 
        /**
@@ -722,6 +804,11 @@ abstract class FileBackendStore extends FileBackend {
 
                $params = $this->setConcurrencyFlags( $params );
                $contents = $this->doGetFileContentsMulti( $params );
+               foreach ( $contents as $path => $content ) {
+                       if ( !is_string( $content ) ) {
+                               $contents[$path] = self::CONTENT_FAIL; // used for all failure cases
+                       }
+               }
 
                return $contents;
        }
@@ -729,26 +816,34 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getFileContentsMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => string, false (missing), or null (error))
         */
        protected function doGetFileContentsMulti( array $params ) {
                $contents = [];
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       AtEase::suppressWarnings();
-                       $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
-                       AtEase::restoreWarnings();
+                       if ( $fsFile instanceof FSFile ) {
+                               AtEase::suppressWarnings();
+                               $content = file_get_contents( $fsFile->getPath() );
+                               AtEase::restoreWarnings();
+                               $contents[$path] = is_string( $content ) ? $content : self::$RES_ERROR;
+                       } elseif ( $fsFile === self::$RES_ABSENT ) {
+                               $contents[$path] = self::$RES_ABSENT;
+                       } else {
+                               $contents[$path] = self::$RES_ERROR;
+                       }
                }
 
                return $contents;
        }
 
        final public function getFileXAttributes( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::XATTRS_FAIL; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
                $latest = !empty( $params['latest'] ); // use latest data?
                if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->getField( $path, 'xattr' );
@@ -759,8 +854,22 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
                $fields = $this->doGetFileXAttributes( $params );
-               $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
-               $this->cheapCache->setField( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
+               if ( is_array( $fields ) ) {
+                       $fields = self::normalizeXAttributes( $fields );
+                       $this->cheapCache->setField(
+                               $path,
+                               'xattr',
+                               [ 'map' => $fields, 'latest' => $latest ]
+                       );
+               } elseif ( $fields === self::$RES_ABSENT ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'xattr',
+                               [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
+                       );
+               } else {
+                       $fields = self::XATTRS_FAIL; // used for all failure cases
+               }
 
                return $fields;
        }
@@ -768,19 +877,20 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getFileXAttributes()
         * @param array $params
-        * @return array[][]|false
+        * @return array[][]|false|null Attributes, false (missing file), or null (error)
         */
        protected function doGetFileXAttributes( array $params ) {
                return [ 'headers' => [], 'metadata' => [] ]; // not supported
        }
 
        final public function getFileSha1Base36( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::SHA1_FAIL; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
                $latest = !empty( $params['latest'] ); // use latest data?
                if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->getField( $path, 'sha1' );
@@ -790,33 +900,49 @@ abstract class FileBackendStore extends FileBackend {
                                return $stat['hash'];
                        }
                }
-               $hash = $this->doGetFileSha1Base36( $params );
-               $this->cheapCache->setField( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
+               $sha1 = $this->doGetFileSha1Base36( $params );
+               if ( is_string( $sha1 ) ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'sha1',
+                               [ 'hash' => $sha1, 'latest' => $latest ]
+                       );
+               } elseif ( $sha1 === self::$RES_ABSENT ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'sha1',
+                               [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
+                       );
+               } else {
+                       $sha1 = self::SHA1_FAIL; // used for all failure cases
+               }
 
-               return $hash;
+               return $sha1;
        }
 
        /**
         * @see FileBackendStore::getFileSha1Base36()
         * @param array $params
-        * @return bool|string
+        * @return bool|string|null SHA1, false (missing file), or null (error)
         */
        protected function doGetFileSha1Base36( array $params ) {
                $fsFile = $this->getLocalReference( $params );
-               if ( !$fsFile ) {
-                       return false;
-               } else {
-                       return $fsFile->getSha1Base36();
+               if ( $fsFile instanceof FSFile ) {
+                       $sha1 = $fsFile->getSha1Base36();
+
+                       return is_string( $sha1 ) ? $sha1 : self::$RES_ERROR;
                }
+
+               return ( $fsFile === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        final public function getFileProps( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $fsFile = $this->getLocalReference( $params );
-               $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
 
-               return $props;
+               return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
        }
 
        final public function getLocalReferenceMulti( array $params ) {
@@ -844,10 +970,15 @@ abstract class FileBackendStore extends FileBackend {
                // Fetch local references of any remaning files...
                $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       $fsFiles[$path] = $fsFile;
-                       if ( $fsFile ) { // update the process cache...
-                               $this->expensiveCache->setField( $path, 'localRef',
-                                       [ 'object' => $fsFile, 'latest' => $latest ] );
+                       if ( $fsFile instanceof FSFile ) {
+                               $fsFiles[$path] = $fsFile;
+                               $this->expensiveCache->setField(
+                                       $path,
+                                       'localRef',
+                                       [ 'object' => $fsFile, 'latest' => $latest ]
+                               );
+                       } else {
+                               $fsFiles[$path] = null; // used for all failure cases
                        }
                }
 
@@ -857,7 +988,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getLocalReferenceMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => FSFile, false (missing), or null (error))
         */
        protected function doGetLocalReferenceMulti( array $params ) {
                return $this->doGetLocalCopyMulti( $params );
@@ -869,6 +1000,11 @@ abstract class FileBackendStore extends FileBackend {
 
                $params = $this->setConcurrencyFlags( $params );
                $tmpFiles = $this->doGetLocalCopyMulti( $params );
+               foreach ( $tmpFiles as $path => $tmpFile ) {
+                       if ( !$tmpFile ) {
+                               $tmpFiles[$path] = null; // used for all failure cases
+                       }
+               }
 
                return $tmpFiles;
        }
@@ -876,7 +1012,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getLocalCopyMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => TempFSFile, false (missing), or null (error))
         */
        abstract protected function doGetLocalCopyMulti( array $params );
 
@@ -886,7 +1022,7 @@ abstract class FileBackendStore extends FileBackend {
         * @return string|null
         */
        public function getFileHttpUrl( array $params ) {
-               return null; // not supported
+               return self::TEMPURL_ERROR; // not supported
        }
 
        final public function streamFile( array $params ) {
@@ -947,7 +1083,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function directoryExists( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
-                       return false; // invalid storage path
+                       return self::EXISTENCE_ERROR; // invalid storage path
                }
                if ( $shard !== null ) { // confined to a single container/shard
                        return $this->doDirectoryExists( $fullCont, $dir, $params );
@@ -957,11 +1093,11 @@ abstract class FileBackendStore extends FileBackend {
                        $res = false; // response
                        foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
                                $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
-                               if ( $exists ) {
+                               if ( $exists === true ) {
                                        $res = true;
                                        break; // found one!
-                               } elseif ( $exists === null ) { // error?
-                                       $res = self::UNKNOWN; // if we don't find anything, it is indeterminate
+                               } elseif ( $exists === self::$RES_ERROR ) {
+                                       $res = self::EXISTENCE_ERROR;
                                }
                        }
 
@@ -981,8 +1117,8 @@ abstract class FileBackendStore extends FileBackend {
 
        final public function getDirectoryList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return self::UNKNOWN;
+               if ( $dir === null ) {
+                       return self::EXISTENCE_ERROR; // invalid storage path
                }
                if ( $shard !== null ) {
                        // File listing is confined to a single container/shard
@@ -1005,14 +1141,14 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container Resolved container name
         * @param string $dir Resolved path relative to container
         * @param array $params
-        * @return Traversable|array|null Returns null on failure
+        * @return Traversable|array|null Iterable list or null (error)
         */
        abstract public function getDirectoryListInternal( $container, $dir, array $params );
 
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return self::UNKNOWN;
+               if ( $dir === null ) {
+                       return self::LIST_ERROR; // invalid storage path
                }
                if ( $shard !== null ) {
                        // File listing is confined to a single container/shard
@@ -1035,7 +1171,7 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container Resolved container name
         * @param string $dir Resolved path relative to container
         * @param array $params
-        * @return Traversable|string[]|null Returns null on failure
+        * @return Traversable|string[]|null Iterable list or null (error)
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
@@ -1356,7 +1492,6 @@ abstract class FileBackendStore extends FileBackend {
        final public function preloadFileStat( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
-               $success = true; // no network errors
 
                $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
                $stats = $this->doGetFileStatMulti( $params );
@@ -1364,45 +1499,10 @@ abstract class FileBackendStore extends FileBackend {
                        return true; // not supported
                }
 
-               $latest = !empty( $params['latest'] ); // use latest data?
-               foreach ( $stats as $path => $stat ) {
-                       $path = FileBackend::normalizeStoragePath( $path );
-                       if ( $path === null ) {
-                               continue; // this shouldn't happen
-                       }
-                       if ( is_array( $stat ) ) { // file exists
-                               // Strongly consistent backends can automatically set "latest"
-                               $stat['latest'] = $stat['latest'] ?? $latest;
-                               $this->cheapCache->setField( $path, 'stat', $stat );
-                               $this->setFileCache( $path, $stat ); // update persistent cache
-                               if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->setField( $path, 'sha1',
-                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                               }
-                               if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                                       $this->cheapCache->setField( $path, 'xattr',
-                                               [ 'map' => $stat['xattr'], 'latest' => $latest ] );
-                               }
-                       } elseif ( $stat === false ) { // file does not exist
-                               $this->cheapCache->setField( $path, 'stat',
-                                       $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                               $this->cheapCache->setField( $path, 'xattr',
-                                       [ 'map' => false, 'latest' => $latest ] );
-                               $this->cheapCache->setField( $path, 'sha1',
-                                       [ 'hash' => false, 'latest' => $latest ] );
-                               $this->logger->debug( __METHOD__ . ': File {path} does not exist', [
-                                       'path' => $path,
-                               ] );
-                       } else { // an error occurred
-                               $success = false;
-                               $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
-                                       'path' => $path,
-                               ] );
-                       }
-               }
+               // Whether this queried the backend in high consistency mode
+               $latest = !empty( $params['latest'] );
 
-               return $success;
+               return $this->ingestFreshFileStats( $stats, $latest );
        }
 
        /**
@@ -1810,7 +1910,7 @@ abstract class FileBackendStore extends FileBackend {
                                $paths[] = FileBackend::normalizeStoragePath( $item );
                        }
                }
-               // Get rid of any paths that failed normalization...
+               // Get rid of any paths that failed normalization
                $paths = array_filter( $paths, 'strlen' ); // remove nulls
                // Get all the corresponding cache keys for paths...
                foreach ( $paths as $path ) {
@@ -1819,22 +1919,33 @@ abstract class FileBackendStore extends FileBackend {
                                $pathNames[$this->fileCacheKey( $path )] = $path;
                        }
                }
-               // Get all cache entries for these file cache keys...
+               // Get all cache entries for these file cache keys.
+               // Note that negatives are not cached by getFileStat()/preloadFileStat().
                $values = $this->memCache->getMulti( array_keys( $pathNames ) );
-               foreach ( $values as $cacheKey => $val ) {
+               // Load all of the results into process cache...
+               foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) {
                        $path = $pathNames[$cacheKey];
-                       if ( is_array( $val ) ) {
-                               $val['latest'] = false; // never completely trust cache
-                               $this->cheapCache->setField( $path, 'stat', $val );
-                               if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->setField( $path, 'sha1',
-                                               [ 'hash' => $val['sha1'], 'latest' => false ] );
-                               }
-                               if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
-                                       $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
-                                       $this->cheapCache->setField( $path, 'xattr',
-                                               [ 'map' => $val['xattr'], 'latest' => false ] );
-                               }
+                       // Sanity; this flag only applies to stat info loaded directly
+                       // from a high consistency backend query to the process cache
+                       unset( $stat['latest'] );
+
+                       $this->cheapCache->setField( $path, 'stat', $stat );
+                       if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) {
+                               // Some backends store SHA-1 as metadata
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'sha1',
+                                       [ 'hash' => $stat['sha1'], 'latest' => false ]
+                               );
+                       }
+                       if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) {
+                               // Some backends store custom headers/metadata
+                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'xattr',
+                                       [ 'map' => $stat['xattr'], 'latest' => false ]
+                               );
                        }
                }
        }
index 88b281e..f3bbecb 100644 (file)
@@ -41,7 +41,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        public function isPathUsableInternal( $storagePath ) {
-               return true;
+               return ( $this->resolveHashKey( $storagePath ) !== null );
        }
 
        protected function doCreateInternal( array $params ) {
@@ -148,7 +148,7 @@ class MemoryFileBackend extends FileBackendStore {
        protected function doGetFileStat( array $params ) {
                $src = $this->resolveHashKey( $params['src'] );
                if ( $src === null ) {
-                       return false; // invalid path
+                       return self::$RES_ERROR; // invalid path
                }
 
                if ( isset( $this->files[$src] ) ) {
@@ -158,15 +158,17 @@ class MemoryFileBackend extends FileBackendStore {
                        ];
                }
 
-               return false;
+               return self::$RES_ABSENT;
        }
 
        protected function doGetLocalCopyMulti( array $params ) {
                $tmpFiles = []; // (path => TempFSFile)
                foreach ( $params['srcs'] as $srcPath ) {
                        $src = $this->resolveHashKey( $srcPath );
-                       if ( $src === null || !isset( $this->files[$src] ) ) {
-                               $fsFile = null;
+                       if ( $src === null ) {
+                               $fsFile = self::$RES_ERROR;
+                       } elseif ( !isset( $this->files[$src] ) ) {
+                               $fsFile = self::$RES_ABSENT;
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
@@ -174,7 +176,7 @@ class MemoryFileBackend extends FileBackendStore {
                                if ( $fsFile ) {
                                        $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
                                        if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
-                                               $fsFile = null;
+                                               $fsFile = self::$RES_ERROR;
                                        }
                                }
                        }
index 1e9c7c5..56a2177 100644 (file)
@@ -145,8 +145,11 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        public function getFeatures() {
-               return ( FileBackend::ATTR_UNICODE_PATHS |
-                       FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
+               return (
+                       FileBackend::ATTR_UNICODE_PATHS |
+                       FileBackend::ATTR_HEADERS |
+                       FileBackend::ATTR_METADATA
+               );
        }
 
        protected function resolveContainerPath( $container, $relStoragePath ) {
@@ -593,7 +596,7 @@ class SwiftFileBackend extends FileBackendStore {
                $stat = $this->getContainerStat( $fullCont );
                if ( is_array( $stat ) ) {
                        return $status; // already there
-               } elseif ( $stat === self::UNKNOWN ) {
+               } elseif ( $stat === self::$RES_ERROR ) {
                        $status->fatal( 'backend-fail-internal', $this->name );
                        $this->logger->error( __METHOD__ . ': cannot get container stat' );
 
@@ -777,8 +780,6 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doGetFileContentsMulti( array $params ) {
-               $contents = [];
-
                $auth = $this->getAuthentication();
 
                $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
@@ -786,11 +787,12 @@ class SwiftFileBackend extends FileBackendStore {
                // if the file does not exist. Do not waste time doing file stats here.
                $reqs = []; // (path => op)
 
+               // Initial dummy values to preserve path order
+               $contents = array_fill_keys( $params['srcs'], self::$RES_ERROR );
                foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
                        if ( $srcRel === null || !$auth ) {
-                               $contents[$path] = false;
-                               continue;
+                               continue; // invalid storage path or auth error
                        }
                        // Create a new temporary memory file...
                        $handle = fopen( 'php://temp', 'wb' );
@@ -803,7 +805,6 @@ class SwiftFileBackend extends FileBackendStore {
                                        'stream'  => $handle,
                                ];
                        }
-                       $contents[$path] = false;
                }
 
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
@@ -812,10 +813,21 @@ class SwiftFileBackend extends FileBackendStore {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        if ( $rcode >= 200 && $rcode <= 299 ) {
                                rewind( $op['stream'] ); // start from the beginning
-                               $contents[$path] = stream_get_contents( $op['stream'] );
+                               $content = (string)stream_get_contents( $op['stream'] );
+                               $size = strlen( $content );
+                               // Make sure that stream finished
+                               if ( $size === (int)$rhdrs['content-length'] ) {
+                                       $contents[$path] = $content;
+                               } else {
+                                       $contents[$path] = self::$RES_ERROR;
+                                       $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
+                                       $this->onError( null, __METHOD__,
+                                               [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
+                               }
                        } elseif ( $rcode === 404 ) {
-                               $contents[$path] = false;
+                               $contents[$path] = self::$RES_ABSENT;
                        } else {
+                               $contents[$path] = self::$RES_ERROR;
                                $this->onError( null, __METHOD__,
                                        [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                        }
@@ -832,7 +844,7 @@ class SwiftFileBackend extends FileBackendStore {
                        return ( count( $status->value ) ) > 0;
                }
 
-               return self::UNKNOWN; // error
+               return self::$RES_ERROR;
        }
 
        /**
@@ -874,6 +886,7 @@ class SwiftFileBackend extends FileBackendStore {
                        return $dirs; // nothing more
                }
 
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                $prefix = ( $dir == '' ) ? null : "{$dir}/";
@@ -956,10 +969,11 @@ class SwiftFileBackend extends FileBackendStore {
                        return $files; // nothing more
                }
 
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                $prefix = ( $dir == '' ) ? null : "{$dir}/";
-               // $objects will contain a list of unfiltered names or CF_Object items
+               // $objects will contain a list of unfiltered names or stdClass items
                // Non-recursive: only list files right under $dir
                if ( !empty( $params['topOnly'] ) ) {
                        if ( !empty( $params['adviseStat'] ) ) {
@@ -982,7 +996,7 @@ class SwiftFileBackend extends FileBackendStore {
                }
 
                $objects = $status->value;
-               $files = $this->buildFileObjectListing( $params, $dir, $objects );
+               $files = $this->buildFileObjectListing( $objects );
 
                // Page on the unfiltered object listing (what is returned may be filtered)
                if ( count( $objects ) < $limit ) {
@@ -997,14 +1011,12 @@ class SwiftFileBackend extends FileBackendStore {
 
        /**
         * Build a list of file objects, filtering out any directories
-        * and extracting any stat info if provided in $objects (for CF_Objects)
+        * and extracting any stat info if provided in $objects
         *
-        * @param array $params Parameters for getDirectoryList()
-        * @param string $dir Resolved container directory path
-        * @param array $objects List of CF_Object items or object names
+        * @param stdClass[]|string[] $objects List of stdClass items or object names
         * @return array List of (names,stat array or null) entries
         */
-       private function buildFileObjectListing( array $params, $dir, array $objects ) {
+       private function buildFileObjectListing( array $objects ) {
                $names = [];
                foreach ( $objects as $object ) {
                        if ( is_object( $object ) ) {
@@ -1042,17 +1054,17 @@ class SwiftFileBackend extends FileBackendStore {
 
        protected function doGetFileXAttributes( array $params ) {
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       if ( !isset( $stat['xattr'] ) ) {
-                               // Stat entries filled by file listings don't include metadata/headers
-                               $this->clearCache( [ $params['src'] ] );
-                               $stat = $this->getFileStat( $params );
-                       }
+               // Stat entries filled by file listings don't include metadata/headers
+               if ( is_array( $stat ) && !isset( $stat['xattr'] ) ) {
+                       $this->clearCache( [ $params['src'] ] );
+                       $stat = $this->getFileStat( $params );
+               }
 
+               if ( is_array( $stat ) ) {
                        return $stat['xattr'];
-               } else {
-                       return false;
                }
+
+               return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doGetFileSha1base36( array $params ) {
@@ -1061,11 +1073,11 @@ class SwiftFileBackend extends FileBackendStore {
                $params['requireSHA1'] = true;
 
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
+               if ( is_array( $stat ) ) {
                        return $stat['sha1'];
-               } else {
-                       return false;
                }
+
+               return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doStreamFile( array $params ) {
@@ -1135,9 +1147,6 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doGetLocalCopyMulti( array $params ) {
-               /** @var TempFSFile[] $tmpFiles */
-               $tmpFiles = [];
-
                $auth = $this->getAuthentication();
 
                $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
@@ -1145,56 +1154,62 @@ class SwiftFileBackend extends FileBackendStore {
                // if the file does not exist. Do not waste time doing file stats here.
                $reqs = []; // (path => op)
 
+               // Initial dummy values to preserve path order
+               $tmpFiles = array_fill_keys( $params['srcs'], self::$RES_ERROR );
                foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
                        if ( $srcRel === null || !$auth ) {
-                               $tmpFiles[$path] = null;
-                               continue;
+                               continue; // invalid storage path or auth error
                        }
                        // Get source file extension
                        $ext = FileBackend::extensionFromPath( $path );
                        // Create a new temporary file...
                        $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
-                       if ( $tmpFile ) {
-                               $handle = fopen( $tmpFile->getPath(), 'wb' );
-                               if ( $handle ) {
-                                       $reqs[$path] = [
-                                               'method'  => 'GET',
-                                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                               'headers' => $this->authTokenHeaders( $auth )
-                                                       + $this->headersFromParams( $params ),
-                                               'stream'  => $handle,
-                                       ];
-                               } else {
-                                       $tmpFile = null;
-                               }
+                       $handle = $tmpFile ? fopen( $tmpFile->getPath(), 'wb' ) : false;
+                       if ( $handle ) {
+                               $reqs[$path] = [
+                                       'method'  => 'GET',
+                                       'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                       'headers' => $this->authTokenHeaders( $auth )
+                                               + $this->headersFromParams( $params ),
+                                       'stream'  => $handle,
+                               ];
+                               $tmpFiles[$path] = $tmpFile;
                        }
-                       $tmpFiles[$path] = $tmpFile;
                }
 
-               $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
+               // Ceph RADOS Gateway is in use (strong consistency) or X-Newest will be used
+               $latest = ( $this->isRGW || !empty( $params['latest'] ) );
+
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
                $reqs = $this->http->runMulti( $reqs, $opts );
                foreach ( $reqs as $path => $op ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        fclose( $op['stream'] ); // close open handle
                        if ( $rcode >= 200 && $rcode <= 299 ) {
-                               $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
-                               // Double check that the disk is not full/broken
-                               if ( $size != $rhdrs['content-length'] ) {
-                                       $tmpFiles[$path] = null;
+                               /** @var TempFSFile $tmpFile */
+                               $tmpFile = $tmpFiles[$path];
+                               // Make sure that the stream finished and fully wrote to disk
+                               $size = $tmpFile->getSize();
+                               if ( $size !== (int)$rhdrs['content-length'] ) {
+                                       $tmpFiles[$path] = self::$RES_ERROR;
                                        $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
                                        $this->onError( null, __METHOD__,
                                                [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                                }
                                // Set the file stat process cache in passing
                                $stat = $this->getStatFromHeaders( $rhdrs );
-                               $stat['latest'] = $isLatest;
+                               $stat['latest'] = $latest;
                                $this->cheapCache->setField( $path, 'stat', $stat );
                        } elseif ( $rcode === 404 ) {
-                               $tmpFiles[$path] = false;
+                               $tmpFiles[$path] = self::$RES_ABSENT;
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'stat',
+                                       $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL
+                               );
                        } else {
-                               $tmpFiles[$path] = null;
+                               $tmpFiles[$path] = self::$RES_ERROR;
                                $this->onError( null, __METHOD__,
                                        [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                        }
@@ -1209,12 +1224,12 @@ class SwiftFileBackend extends FileBackendStore {
                ) {
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                        if ( $srcRel === null ) {
-                               return null; // invalid path
+                               return self::TEMPURL_ERROR; // invalid path
                        }
 
                        $auth = $this->getAuthentication();
                        if ( !$auth ) {
-                               return null;
+                               return self::TEMPURL_ERROR;
                        }
 
                        $ttl = $params['ttl'] ?? 86400;
@@ -1254,7 +1269,7 @@ class SwiftFileBackend extends FileBackendStore {
                        }
                }
 
-               return null;
+               return self::TEMPURL_ERROR;
        }
 
        protected function directoriesAreVirtual() {
@@ -1393,6 +1408,7 @@ class SwiftFileBackend extends FileBackendStore {
         * @return array|bool|null False on 404, null on failure
         */
        protected function getContainerStat( $container, $bypassCache = false ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                if ( $bypassCache ) { // purge cache
@@ -1403,7 +1419,7 @@ class SwiftFileBackend extends FileBackendStore {
                if ( !$this->containerStatCache->hasField( $container, 'stat' ) ) {
                        $auth = $this->getAuthentication();
                        if ( !$auth ) {
-                               return self::UNKNOWN;
+                               return self::$RES_ERROR;
                        }
 
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
@@ -1424,12 +1440,12 @@ class SwiftFileBackend extends FileBackendStore {
                                        $this->setContainerCache( $container, $stat ); // update persistent cache
                                }
                        } elseif ( $rcode === 404 ) {
-                               return false;
+                               return self::$RES_ABSENT;
                        } else {
                                $this->onError( null, __METHOD__,
                                        [ 'cont' => $container ], $rerr, $rcode, $rdesc );
 
-                               return self::UNKNOWN;
+                               return self::$RES_ERROR;
                        }
                }
 
@@ -1594,24 +1610,21 @@ class SwiftFileBackend extends FileBackendStore {
 
                $auth = $this->getAuthentication();
 
-               $reqs = [];
+               $reqs = []; // (path => op)
+               // (a) Check the containers of the paths...
                foreach ( $params['srcs'] as $path ) {
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                       if ( $srcRel === null ) {
-                               $stats[$path] = false;
-                               continue; // invalid storage path
-                       } elseif ( !$auth ) {
-                               $stats[$path] = self::UNKNOWN;
-                               continue;
+                       if ( $srcRel === null || !$auth ) {
+                               $stats[$path] = self::$RES_ERROR;
+                               continue; // invalid storage path or auth error
                        }
 
-                       // (a) Check the container
                        $cstat = $this->getContainerStat( $srcCont );
-                       if ( $cstat === false ) {
-                               $stats[$path] = false;
+                       if ( $cstat === self::$RES_ABSENT ) {
+                               $stats[$path] = self::$RES_ABSENT;
                                continue; // ok, nothing to do
                        } elseif ( !is_array( $cstat ) ) {
-                               $stats[$path] = self::UNKNOWN;
+                               $stats[$path] = self::$RES_ERROR;
                                continue;
                        }
 
@@ -1622,15 +1635,11 @@ class SwiftFileBackend extends FileBackendStore {
                        ];
                }
 
+               // (b) Check the files themselves...
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
                $reqs = $this->http->runMulti( $reqs, $opts );
-
-               foreach ( $params['srcs'] as $path ) {
-                       if ( array_key_exists( $path, $stats ) ) {
-                               continue; // some sort of failure above
-                       }
-                       // (b) Check the file
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        if ( $rcode === 200 || $rcode === 204 ) {
                                // Update the object if it is missing some headers
                                if ( !empty( $params['requireSHA1'] ) ) {
@@ -1642,9 +1651,9 @@ class SwiftFileBackend extends FileBackendStore {
                                        $stat['latest'] = true; // strong consistency
                                }
                        } elseif ( $rcode === 404 ) {
-                               $stat = false;
+                               $stat = self::$RES_ABSENT;
                        } else {
-                               $stat = self::UNKNOWN;
+                               $stat = self::$RES_ERROR;
                                $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
                        }
                        $stats[$path] = $stat;
index bcde8d9..92105c3 100644 (file)
@@ -33,7 +33,7 @@ abstract class SwiftFileBackendList implements Iterator {
        /** @var array List of path or (path,stat array) entries */
        protected $bufferIter = [];
 
-       /** @var string List items *after* this path */
+       /** @var string|null List items *after* this path */
        protected $bufferAfter = null;
 
        /** @var int */
@@ -108,6 +108,7 @@ abstract class SwiftFileBackendList implements Iterator {
                $this->pos = 0;
                $this->bufferAfter = null;
                $this->bufferIter = $this->pageFromList(
+                       // @phan-suppress-next-line PhanTypeMismatchArgumentPropertyReferenceReal
                        $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
                ); // updates $this->bufferAfter
        }
index e512423..c256d72 100644 (file)
@@ -40,7 +40,7 @@ use Wikimedia\Timestamp\ConvertibleTimestamp;
 abstract class FileJournal {
        /** @var string */
        protected $backend;
-       /** @var int */
+       /** @var int|false */
        protected $ttlDays;
 
        /**
@@ -153,7 +153,7 @@ abstract class FileJournal {
         * A starting change ID and/or limit can be specified.
         *
         * @param int|null $start Starting change ID or null
-        * @param int $limit Maximum number of items to return
+        * @param int $limit Maximum number of items to return (0 = unlimited)
         * @param string|null &$next Updated to the ID of the next entry.
         * @return array List of associative arrays, each having:
         *     id         : unique, monotonic, ID for this change
index 527de6a..59af944 100644 (file)
@@ -36,8 +36,10 @@ class CopyFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -50,10 +52,8 @@ class CopyFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
index f45b055..b68b98f 100644 (file)
@@ -34,21 +34,15 @@ class CreateFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source data is too big
-               if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
 
-                       return $status;
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
+               // Check if the source data is too big
+               $maxBytes = $this->backend->maxFileSizeInternal();
+               if ( strlen( $this->getParam( 'content' ) ) > $maxBytes ) {
+                       $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
@@ -61,12 +55,14 @@ class CreateFileOp extends FileOp {
        }
 
        protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
+               if ( $this->overwriteSameCase ) {
+                       $status = StatusValue::newGood(); // nothing to do
+               } else {
                        // Create the file at the destination
-                       return $this->backend->createInternal( $this->setFlags( $this->params ) );
+                       $status = $this->backend->createInternal( $this->setFlags( $this->params ) );
                }
 
-               return StatusValue::newGood();
+               return $status;
        }
 
        protected function getSourceSha1Base36() {
index 1047a98..3b48881 100644 (file)
@@ -32,8 +32,10 @@ class DeleteFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -46,10 +48,8 @@ class DeleteFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-delete', $this->params['src'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
index 0d1e553..3604b26 100644 (file)
@@ -32,21 +32,20 @@ class DescribeFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        $status->fatal( 'backend-fail-notexists', $this->params['src'] );
 
                        return $status;
-                       // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-describe', $this->params['src'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
                // Update file existence predicates
-               $predicates['exists'][$this->params['src']] =
-                       $this->fileExists( $this->params['src'], $predicates );
+               $predicates['exists'][$this->params['src']] = $srcExists;
                $predicates['sha1'][$this->params['src']] =
                        $this->fileSha1( $this->params['src'], $predicates );
 
index 961fdb9..a046588 100644 (file)
@@ -255,6 +255,18 @@ abstract class FileOp {
                        return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
                }
                $this->state = self::STATE_CHECKED;
+
+               $status = StatusValue::newGood();
+               $storagePaths = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
+               foreach ( array_unique( $storagePaths ) as $storagePath ) {
+                       if ( !$this->backend->isPathUsableInternal( $storagePath ) ) {
+                               $status->fatal( 'backend-fail-usable', $storagePath );
+                       }
+               }
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
                $status = $this->doPrecheck( $predicates );
                if ( !$status->isOK() ) {
                        $this->failed = true;
@@ -391,6 +403,8 @@ abstract class FileOp {
 
                                return $status;
                        }
+               } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['dst'] );
                }
 
                return $status;
@@ -409,9 +423,12 @@ abstract class FileOp {
        /**
         * Check if a file will exist in storage when this operation is attempted
         *
+        * Ideally, the file stat entry should already be preloaded via preloadFileStat().
+        * Otherwise, this will query the backend.
+        *
         * @param string $source Storage path
         * @param array $predicates
-        * @return bool
+        * @return bool|null Whether the file will exist or null on error
         */
        final protected function fileExists( $source, array $predicates ) {
                if ( isset( $predicates['exists'][$source] ) ) {
@@ -424,11 +441,14 @@ abstract class FileOp {
        }
 
        /**
-        * Get the SHA-1 of a file in storage when this operation is attempted
+        * Get the SHA-1 hash a file in storage will have when this operation is attempted
+        *
+        * Ideally, file the stat entry should already be preloaded via preloadFileStat() and
+        * the backend tracks hashes as extended attributes. Otherwise, this will query the backend.
         *
         * @param string $source Storage path
         * @param array $predicates
-        * @return string|bool False on failure
+        * @return string|bool The SHA-1 hash the file will have or false if non-existent or on error
         */
        final protected function fileSha1( $source, array $predicates ) {
                if ( isset( $predicates['sha1'][$source] ) ) {
index 55dca51..0a83370 100644 (file)
@@ -36,8 +36,10 @@ class MoveFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -50,14 +52,12 @@ class MoveFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
index 5783a82..b8cfbf6 100644 (file)
@@ -38,26 +38,21 @@ class StoreFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists on the file system
+
+               // Check if the source file exists in the file system and is not too big
                if ( !is_file( $this->params['src'] ) ) {
                        $status->fatal( 'backend-fail-notexists', $this->params['src'] );
 
                        return $status;
-                       // Check if the source file is too big
-               } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+               }
+               // Check if the source file is too big
+               $maxBytes = $this->backend->maxFileSizeInternal();
+               if ( filesize( $this->params['src'] ) > $maxBytes ) {
+                       $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
@@ -70,12 +65,14 @@ class StoreFileOp extends FileOp {
        }
 
        protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
+               if ( $this->overwriteSameCase ) {
+                       $status = StatusValue::newGood(); // nothing to do
+               } else {
                        // Store the file at the destination
-                       return $this->backend->storeInternal( $this->setFlags( $this->params ) );
+                       $status = $this->backend->storeInternal( $this->setFlags( $this->params ) );
                }
 
-               return StatusValue::newGood();
+               return $status;
        }
 
        protected function getSourceSha1Base36() {
index 2e418b9..2e3aa70 100644 (file)
@@ -150,7 +150,7 @@ class MultiHttpClient implements LoggerAwareInterface {
         * This is true for the request headers and the response headers. Integer-indexed
         * method/URL entries will also be changed to use the corresponding string keys.
         *
-        * @param array $reqs Map of HTTP request arrays
+        * @param array[] $reqs Map of HTTP request arrays
         * @param array $opts
         *   - connTimeout     : connection timeout per request (seconds)
         *   - reqTimeout      : post-connection timeout per request (seconds)
@@ -182,14 +182,18 @@ class MultiHttpClient implements LoggerAwareInterface {
         *
         * @see MultiHttpClient::runMulti()
         *
-        * @param array $reqs Map of HTTP request arrays
+        * @param array[] $reqs Map of HTTP request arrays
         * @param array $opts
         *   - connTimeout     : connection timeout per request (seconds)
         *   - 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();
@@ -293,6 +297,7 @@ class MultiHttpClient implements LoggerAwareInterface {
         *   - reqTimeout     : default request timeout
         * @return resource
         * @throws Exception
+        * @suppress PhanTypeMismatchArgumentInternal
         */
        protected function getCurlHandle( array &$req, array $opts = [] ) {
                $ch = curl_init();
@@ -348,9 +353,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                        }
                        curl_setopt( $ch, CURLOPT_READFUNCTION,
                                function ( $ch, $fd, $length ) {
-                                       $data = fread( $fd, $length );
-                                       $len = strlen( $data );
-                                       return $data;
+                                       return (string)fread( $fd, $length );
                                }
                        );
                } elseif ( $req['method'] === 'POST' ) {
@@ -498,7 +501,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'];
@@ -507,6 +510,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 {
@@ -529,7 +533,7 @@ class MultiHttpClient implements LoggerAwareInterface {
        /**
         * Normalize request information
         *
-        * @param array $reqs the requests to normalize
+        * @param array[] $reqs the requests to normalize
         */
        private function normalizeRequests( array &$reqs ) {
                foreach ( $reqs as &$req ) {
index fd3ffa5..5530eed 100644 (file)
@@ -7,6 +7,8 @@ use Wikimedia\Rdbms\DBError;
  * All locks are non-blocking, which avoids deadlocks.
  *
  * @ingroup LockManager
+ * @phan-file-suppress PhanUndeclaredConstant Phan doesn't read constants in LockManager
+ *   when accessed via self::
  */
 class PostgreSqlLockManager extends DBLockManager {
        /** @var array Mapping of lock types to the type actually used */
index 950b283..6478a61 100644 (file)
@@ -38,7 +38,7 @@ abstract class QuorumLockManager extends LockManager {
        final protected function doLockByType( array $pathsByType ) {
                $status = StatusValue::newGood();
 
-               $pathsToLock = []; // (bucket => type => paths)
+               $pathsByTypeByBucket = []; // (bucket => type => paths)
                // Get locks that need to be acquired (buckets => locks)...
                foreach ( $pathsByType as $type => $paths ) {
                        foreach ( $paths as $path ) {
@@ -46,23 +46,27 @@ abstract class QuorumLockManager extends LockManager {
                                        ++$this->locksHeld[$path][$type];
                                } else {
                                        $bucket = $this->getBucketFromPath( $path );
-                                       $pathsToLock[$bucket][$type][] = $path;
+                                       $pathsByTypeByBucket[$bucket][$type][] = $path;
                                }
                        }
                }
 
+               // Acquire locks in each bucket in bucket order to reduce contention. Any blocking
+               // mutexes during the acquisition step will not involve circular waiting on buckets.
+               ksort( $pathsByTypeByBucket );
+
                $lockedPaths = []; // files locked in this attempt (type => paths)
                // Attempt to acquire these locks...
-               foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
+               foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) {
                        // Try to acquire the locks for this bucket
-                       $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
+                       $status->merge( $this->doLockingRequestBucket( $bucket, $bucketPathsByType ) );
                        if ( !$status->isOK() ) {
                                $status->merge( $this->doUnlockByType( $lockedPaths ) );
 
                                return $status;
                        }
                        // Record these locks as active
-                       foreach ( $pathsToLockByType as $type => $paths ) {
+                       foreach ( $bucketPathsByType as $type => $paths ) {
                                foreach ( $paths as $path ) {
                                        $this->locksHeld[$path][$type] = 1; // locked
                                        // Keep track of what locks were made in this attempt
@@ -77,7 +81,7 @@ abstract class QuorumLockManager extends LockManager {
        protected function doUnlockByType( array $pathsByType ) {
                $status = StatusValue::newGood();
 
-               $pathsToUnlock = []; // (bucket => type => paths)
+               $pathsByTypeByBucket = []; // (bucket => type => paths)
                foreach ( $pathsByType as $type => $paths ) {
                        foreach ( $paths as $path ) {
                                if ( !isset( $this->locksHeld[$path][$type] ) ) {
@@ -88,7 +92,7 @@ abstract class QuorumLockManager extends LockManager {
                                        if ( $this->locksHeld[$path][$type] <= 0 ) {
                                                unset( $this->locksHeld[$path][$type] );
                                                $bucket = $this->getBucketFromPath( $path );
-                                               $pathsToUnlock[$bucket][$type][] = $path;
+                                               $pathsByTypeByBucket[$bucket][$type][] = $path;
                                        }
                                        if ( $this->locksHeld[$path] === [] ) {
                                                unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
@@ -99,8 +103,8 @@ abstract class QuorumLockManager extends LockManager {
 
                // Remove these specific locks if possible, or at least release
                // all locks once this process is currently not holding any locks.
-               foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
-                       $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
+               foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) {
+                       $status->merge( $this->doUnlockingRequestBucket( $bucket, $bucketPathsByType ) );
                }
                if ( $this->locksHeld === [] ) {
                        $status->merge( $this->releaseAllLocks() );
index f25287f..9d66326 100644 (file)
@@ -150,7 +150,8 @@ class XmlTypeCheck {
        }
 
        /**
-        * @param string $fname the filename
+        * @param string $xml
+        * @param bool $isFile
         */
        private function validateFromInput( $xml, $isFile ) {
                $reader = new XMLReader();
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..348f300 100644 (file)
@@ -47,6 +47,9 @@ 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
         */
        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 57a2507..aaed69f 100644 (file)
@@ -28,6 +28,7 @@
  *
  * @ingroup Cache
  * @ingroup Redis
+ * @phan-file-suppress PhanTypeComparisonFromArray It's unclear whether exec() can return false
  */
 class RedisBagOStuff extends MediumSpecificBagOStuff {
        /** @var RedisConnectionPool */
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..a090e16 100644 (file)
@@ -578,6 +578,9 @@ 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
@@ -1246,6 +1249,9 @@ 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
@@ -1305,6 +1311,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'] ?? [];
@@ -2318,6 +2325,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                $chance = ( 1 - $curTTL / $lowTTL );
 
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
        }
 
@@ -2360,6 +2368,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
                $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
 
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
        }
 
@@ -2421,6 +2430,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..be41ee0 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
         */
@@ -1672,12 +1681,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * Returns an optional USE INDEX clause to go after the table, and a
         * string to go at the end of the query.
         *
+        * @see Database::select()
+        *
         * @param array $options Associative array of options to be turned into
         *   an SQL query, valid keys are listed in the function.
         * @return array
-        * @see Database::select()
         */
-       protected function makeSelectOptions( $options ) {
+       protected function makeSelectOptions( array $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = '';
 
index 851a178..7be3b7d 100644 (file)
@@ -64,8 +64,6 @@ abstract class DatabaseMysqlBase extends Database {
        /** @var bool|null */
        protected $defaultBigSelects = null;
 
-       /** @var string|null */
-       private $serverVersion = null;
        /** @var bool|null */
        private $insertSelectIsSafe = null;
        /** @var stdClass|null */
@@ -1102,13 +1100,19 @@ abstract class DatabaseMysqlBase extends Database {
         * @return string
         */
        public function getServerVersion() {
-               // Not using mysql_get_server_info() or similar for consistency: in the handshake,
-               // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
-               // it off (see RPL_VERSION_HACK in include/mysql_com.h).
-               if ( $this->serverVersion === null ) {
-                       $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
-               }
-               return $this->serverVersion;
+               $cache = $this->srvCache;
+               $fname = __METHOD__;
+
+               return $cache->getWithSetCallback(
+                       $cache->makeGlobalKey( 'mysql-server-version', $this->getServer() ),
+                       $cache::TTL_HOUR,
+                       function () use ( $fname ) {
+                               // Not using mysql_get_server_info() or similar for consistency: in the handshake,
+                               // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
+                               // it off (see RPL_VERSION_HACK in include/mysql_com.h).
+                               return $this->selectField( '', 'VERSION()', '', $fname );
+                       }
+               );
        }
 
        /**
index 8931ae2..106772b 100644 (file)
@@ -33,6 +33,7 @@ use stdClass;
  * @ingroup Database
  * @since 1.22
  * @see Database
+ * @phan-file-suppress PhanParamSignatureMismatch resource vs mysqli_result
  */
 class DatabaseMysqli extends DatabaseMysqlBase {
        /**
index a7ebc86..c075a1b 100644 (file)
@@ -1301,7 +1301,7 @@ SQL;
                return "'" . pg_escape_string( $conn, (string)$s ) . "'";
        }
 
-       public function makeSelectOptions( $options ) {
+       protected function makeSelectOptions( array $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = $useIndex = $ignoreIndex = '';
 
index 2977291..0e2dc8f 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() );
                }
@@ -602,15 +603,10 @@ class DatabaseSqlite extends Database {
                return in_array( 'UNIQUE', $options );
        }
 
-       /**
-        * Filter the options used in SELECT statements
-        *
-        * @param array $options
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
+       protected function makeSelectOptions( array $options ) {
+               // Remove problematic options that the base implementation converts to SQL
                foreach ( $options as $k => $v ) {
-                       if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
+                       if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
                                $options[$k] = '';
                        }
                }
@@ -1124,12 +1120,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 +1154,10 @@ class DatabaseSqlite extends Database {
                return true;
        }
 
+       protected function doHandleSessionLossPreconnect() {
+               $this->sessionAttachedDbs = [];
+       }
+
        /**
         * @return PDO
         */
index 68735e9..41f7cb6 100644 (file)
@@ -902,7 +902,7 @@ interface IDatabase {
         *   that field to. The data will be quoted by IDatabase::addQuotes().
         *   Values with integer keys form unquoted SET statements, which can be used for
         *   things like "field = field + 1" or similar computed values.
-        * @param array $conds An array of conditions (WHERE). See
+        * @param array|string $conds An array of conditions (WHERE). See
         *   IDatabase::select() for the details of the format of condition
         *   arrays. Use '*' to update all rows.
         * @param string $fname The function name of the caller (from __METHOD__),
@@ -1287,7 +1287,7 @@ interface IDatabase {
         * @param string $joinTable The other table.
         * @param string $delVar The variable to join on, in the first table.
         * @param string $joinVar The variable to join on, in the second table.
-        * @param array $conds Condition array of field names mapped to variables,
+        * @param array|string $conds Condition array of field names mapped to variables,
         *   ANDed together in the WHERE clause
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
         * @throws DBError If an error occurs, see IDatabase::query()
index 54eca79..fa2c1db 100644 (file)
@@ -17,7 +17,7 @@ use UnexpectedValueException;
  * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
  */
 class MySQLMasterPos implements DBMasterPos {
-       /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
+       /** @var string One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
        private $style;
        /** @var string|null Base name of all Binary Log files */
        private $binLog;
index 4b6afe7..6e9591b 100644 (file)
@@ -38,6 +38,9 @@ interface ILBFactory {
        /** @var int Save DB positions, waiting on all DCs */
        const SHUTDOWN_CHRONPROT_SYNC = 2;
 
+       /** @var string Default main LB cluster name (do not change this) */
+       const CLUSTER_MAIN_DEFAULT = 'DEFAULT';
+
        /**
         * Construct a manager of ILoadBalancer objects
         *
index f675b58..ef1f0a6 100644 (file)
@@ -24,6 +24,7 @@
 namespace Wikimedia\Rdbms;
 
 use InvalidArgumentException;
+use UnexpectedValueException;
 
 /**
  * A multi-database, multi-master factory for Wikimedia and similar installations.
@@ -32,64 +33,45 @@ use InvalidArgumentException;
  * @ingroup Database
  */
 class LBFactoryMulti extends LBFactory {
-       /** @var array A map of database names to section names */
-       private $sectionsByDB;
-       /**
-        * @var array A 2-d map. For each section, gives a map of server names to
-        * load ratios
-        */
-       private $sectionLoads;
-       /**
-        * @var array[] Server info associative array
-        * @note The host, hostName and load entries will be overridden
-        */
-       private $serverTemplate;
+       /** @var LoadBalancer[] */
+       private $mainLBs = [];
+       /** @var LoadBalancer[] */
+       private $externalLBs = [];
 
-       /** @var array A 3-d map giving server load ratios for each section and group */
+       /** @var string[] Map of (hostname => IP address) */
+       private $hostsByName = [];
+       /** @var string[] Map of (database name => section name) */
+       private $sectionsByDB = [];
+       /** @var int[][][] Map of (section => group => host => load ratio) */
        private $groupLoadsBySection = [];
-       /** @var array A 3-d map giving server load ratios by DB name */
+       /** @var int[][][] Map of (database => group => host => load ratio) */
        private $groupLoadsByDB = [];
-       /** @var array A map of hostname to IP address */
-       private $hostsByName = [];
-       /** @var array A map of external storage cluster name to server load map */
+       /** @var int[][] Map of (cluster => host => load ratio) */
        private $externalLoads = [];
-       /**
-        * @var array A set of server info keys overriding serverTemplate for
-        * external storage
-        */
-       private $externalTemplateOverrides;
-       /**
-        * @var array A 2-d map overriding serverTemplate and
-        * externalTemplateOverrides on a server-by-server basis. Applies to both
-        * core and external storage
-        */
-       private $templateOverridesByServer;
-       /** @var array A 2-d map overriding the server info by section */
-       private $templateOverridesBySection;
-       /** @var array A 2-d map overriding the server info by external storage cluster */
-       private $templateOverridesByCluster;
-       /** @var array An override array for all master servers */
-       private $masterTemplateOverrides;
-       /**
-        * @var array|bool A map of section name to read-only message. Missing or
-        * false for read/write
-        */
+       /** @var array Server config map ("host", "hostName", "load", and "groupLoads" are ignored) */
+       private $serverTemplate = [];
+       /** @var array Server config map overriding "serverTemplate" for external storage */
+       private $externalTemplateOverrides = [];
+       /** @var array[] Map of (section => server config map overrides) */
+       private $templateOverridesBySection = [];
+       /** @var array[] Map of (cluster => server config map overrides) for external storage */
+       private $templateOverridesByCluster = [];
+       /** @var array Server config override map for all main and external master servers */
+       private $masterTemplateOverrides = [];
+       /** @var array[] Map of (host => server config map overrides) for main and external servers */
+       private $templateOverridesByServer = [];
+       /**  @var string[]|bool[] A map of section name to read-only message */
        private $readOnlyBySection = [];
 
-       /** @var LoadBalancer[] */
-       private $mainLBs = [];
-       /** @var LoadBalancer[] */
-       private $extLBs = [];
-       /** @var string */
-       private $loadMonitorClass = 'LoadMonitor';
+       /** @var string An ILoadMonitor class */
+       private $loadMonitorClass;
+
        /** @var string */
        private $lastDomain;
        /** @var string */
        private $lastSection;
 
        /**
-        * @see LBFactory::__construct()
-        *
         * Template override precedence (highest => lowest):
         *   - templateOverridesByServer
         *   - masterTemplateOverrides
@@ -98,122 +80,108 @@ class LBFactoryMulti extends LBFactory {
         *   - serverTemplate
         * Overrides only work on top level keys (so nested values will not be merged).
         *
-        * Server configuration maps should be of the format Database::factory() requires.
+        * Server config maps should be of the format Database::factory() requires.
         * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
         * data can be before the load balancer tries to avoid using it. The map can have 'is static'
         * set to disable blocking  replication sync checks (intended for archive servers with
         * unchanging data).
-        *
-        * @param array $conf Parameters of LBFactory::__construct() as well as:
-        *   - sectionsByDB                Map of database names to section names.
-        *   - sectionLoads                2-d map. For each section, gives a map of server names to
-        *                                 load ratios. For example:
+
+        * @see LBFactory::__construct()
+        * @param array $conf Additional parameters include:
+        *   - hostsByName                 Optional (hostname => IP address) map.
+        *   - sectionsByDB                Optional map of (database => section name).
+        *                                 For example:
         *                                 [
-        *                                     'section1' => [
-        *                                         'db1' => 100,
-        *                                         'db2' => 100
-        *                                     ]
+        *                                     'DEFAULT' => 'section1',
+        *                                     'database1' => 'section2'
         *                                 ]
-        *   - serverTemplate              Server configuration map intended for Database::factory().
-        *                                 Note that "host", "hostName" and "load" entries will be
-        *                                 overridden by "sectionLoads" and "hostsByName".
-        *   - groupLoadsBySection         3-d map giving server load ratios for each section/group.
+        *   - sectionLoads                Optional map of (section => host => load ratio); the first
+        *                                 host in each section is the master server for that section.
+        *                                 For example:
+        *                                 [
+        *                                     'dbmaser'    => 0,
+        *                                     'dbreplica1' => 100,
+        *                                     'dbreplica2' => 100
+        *                                 ]
+        *   - groupLoadsBySection         Optional map of (section => group => host => load ratio);
+        *                                 any ILoadBalancer::GROUP_GENERIC group will be ignored.
         *                                 For example:
         *                                 [
         *                                     'section1' => [
         *                                         'group1' => [
-        *                                             'db1' => 100,
-        *                                             'db2' => 100
+        *                                             'dbreplica3  => 100,
+        *                                             'dbreplica4' => 100
         *                                         ]
         *                                     ]
         *                                 ]
-        *   - groupLoadsByDB              3-d map giving server load ratios by DB name.
-        *   - hostsByName                 Map of hostname to IP address.
-        *   - externalLoads               Map of external storage cluster name to server load map.
-        *   - externalTemplateOverrides   Set of server configuration maps overriding
-        *                                 "serverTemplate" for external storage.
-        *   - templateOverridesByServer   2-d map overriding "serverTemplate" and
-        *                                 "externalTemplateOverrides" on a server-by-server basis.
-        *                                 Applies to both core and external storage.
-        *   - templateOverridesBySection  2-d map overriding the server configuration maps by section.
-        *   - templateOverridesByCluster  2-d map overriding the server configuration maps by external
-        *                                 storage cluster.
-        *   - masterTemplateOverrides     Server configuration map overrides for all master servers.
-        *   - loadMonitorClass            Name of the LoadMonitor class to always use.
-        *   - readOnlyBySection           A map of section name to read-only message.
-        *                                 Missing or false for read/write.
+        *   - groupLoadsByDB              Optional (database => group => host => load ratio) map.
+        *   - externalLoads               Optional (cluster => host => load ratio) map.
+        *   - serverTemplate              server config map for Database::factory().
+        *                                 Note that "host", "hostName" and "load" entries will be
+        *                                 overridden by "groupLoadsBySection" and "hostsByName".
+        *   - externalTemplateOverrides   Optional server config map overrides for external
+        *                                 stores; respects the override precedence described above.
+        *   - templateOverridesBySection  Optional (section => server config map overrides) map;
+        *                                 respects the override precedence described above.
+        *   - templateOverridesByCluster  Optional (external cluster => server config map overrides)
+        *                                 map; respects the override precedence described above.
+        *   - masterTemplateOverrides     Optional server config map overrides for masters;
+        *                                 respects the override precedence described above.
+        *   - templateOverridesByServer   Optional (host => server config map overrides) map;
+        *                                 respects the override precedence described above
+        *                                 and applies to both core and external storage.
+        *   - loadMonitorClass            Name of the LoadMonitor class to always use. [optional]
+        *   - readOnlyBySection           Optional map of (section name => message text or false).
+        *                                 String values make sections read only, whereas anything
+        *                                 else does not restrict read/write mode.
         */
        public function __construct( array $conf ) {
                parent::__construct( $conf );
 
-               $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ];
-               $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
-                       'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
-                       'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
-                       'readOnlyBySection', 'loadMonitorClass' ];
-
-               foreach ( $required as $key ) {
-                       if ( !isset( $conf[$key] ) ) {
-                               throw new InvalidArgumentException( __CLASS__ . ": $key is required." );
-                       }
-                       $this->$key = $conf[$key];
-               }
-
-               foreach ( $optional as $key ) {
-                       if ( isset( $conf[$key] ) ) {
-                               $this->$key = $conf[$key];
-                       }
+               $this->hostsByName = $conf['hostsByName'] ?? [];
+               $this->sectionsByDB = $conf['sectionsByDB'];
+               $this->groupLoadsBySection = $conf['groupLoadsBySection'] ?? [];
+               foreach ( ( $conf['sectionLoads'] ?? [] ) as $section => $loadByHost ) {
+                       $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] = $loadByHost;
                }
-       }
-
-       /**
-        * @param bool|string $domain
-        * @return string
-        */
-       private function getSectionForDomain( $domain = false ) {
-               if ( $this->lastDomain === $domain ) {
-                       return $this->lastSection;
-               }
-
-               $database = $this->getDatabaseFromDomain( $domain );
-               $section = $this->sectionsByDB[$database] ?? 'DEFAULT';
-               $this->lastSection = $section;
-               $this->lastDomain = $domain;
-
-               return $section;
+               $this->groupLoadsByDB = $conf['groupLoadsByDB'] ?? [];
+               $this->externalLoads = $conf['externalLoads'] ?? [];
+               $this->serverTemplate = $conf['serverTemplate'] ?? [];
+               $this->externalTemplateOverrides = $conf['externalTemplateOverrides'] ?? [];
+               $this->templateOverridesBySection = $conf['templateOverridesBySection'] ?? [];
+               $this->templateOverridesByCluster = $conf['templateOverridesByCluster'] ?? [];
+               $this->masterTemplateOverrides = $conf['masterTemplateOverrides'] ?? [];
+               $this->templateOverridesByServer = $conf['templateOverridesByServer'] ?? [];
+               $this->readOnlyBySection = $conf['readOnlyBySection'] ?? [];
+
+               $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
        }
 
        public function newMainLB( $domain = false ) {
-               $database = $this->getDatabaseFromDomain( $domain );
                $section = $this->getSectionForDomain( $domain );
-               $groupLoads = $this->groupLoadsByDB[$database] ?? [];
-
-               if ( isset( $this->groupLoadsBySection[$section] ) ) {
-                       $groupLoads = array_merge_recursive(
-                               $groupLoads, $this->groupLoadsBySection[$section] );
+               if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) {
+                       throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
                }
 
-               $readOnlyReason = $this->readOnlyReason;
-               // Use the LB-specific read-only reason if everything isn't already read-only
-               if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) {
-                       $readOnlyReason = $this->readOnlyBySection[$section];
-               }
-
-               $template = $this->serverTemplate;
-               if ( isset( $this->templateOverridesBySection[$section] ) ) {
-                       $template = $this->templateOverridesBySection[$section] + $template;
-               }
+               $dbGroupLoads = $this->groupLoadsByDB[$this->getDomainDatabase( $domain )] ?? [];
+               unset( $dbGroupLoads[ILoadBalancer::GROUP_GENERIC] ); // cannot override
 
                return $this->newLoadBalancer(
-                       $template,
-                       $this->sectionLoads[$section],
-                       $groupLoads,
-                       $readOnlyReason
+                       array_merge(
+                               $this->serverTemplate,
+                               $this->templateOverridesBySection[$section] ?? []
+                       ),
+                       array_merge( $this->groupLoadsBySection[$section], $dbGroupLoads ),
+                       // Use the LB-specific read-only reason if everything isn't already read-only
+                       is_string( $this->readOnlyReason )
+                               ? $this->readOnlyReason
+                               : ( $this->readOnlyBySection[$section] ?? false )
                );
        }
 
        public function getMainLB( $domain = false ) {
                $section = $this->getSectionForDomain( $domain );
+
                if ( !isset( $this->mainLBs[$section] ) ) {
                        $this->mainLBs[$section] = $this->newMainLB( $domain );
                }
@@ -223,30 +191,26 @@ class LBFactoryMulti extends LBFactory {
 
        public function newExternalLB( $cluster ) {
                if ( !isset( $this->externalLoads[$cluster] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
-               }
-               $template = $this->serverTemplate;
-               if ( $this->externalTemplateOverrides ) {
-                       $template = $this->externalTemplateOverrides + $template;
-               }
-               if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
-                       $template = $this->templateOverridesByCluster[$cluster] + $template;
+                       throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
                }
 
                return $this->newLoadBalancer(
-                       $template,
-                       $this->externalLoads[$cluster],
-                       [],
+                       array_merge(
+                               $this->serverTemplate,
+                               $this->externalTemplateOverrides,
+                               $this->templateOverridesByCluster[$cluster] ?? []
+                       ),
+                       [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ],
                        $this->readOnlyReason
                );
        }
 
        public function getExternalLB( $cluster ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster );
+               if ( !isset( $this->externalLBs[$cluster] ) ) {
+                       $this->externalLBs[$cluster] = $this->newExternalLB( $cluster );
                }
 
-               return $this->extLBs[$cluster];
+               return $this->externalLBs[$cluster];
        }
 
        public function getAllMainLBs() {
@@ -269,20 +233,45 @@ class LBFactoryMulti extends LBFactory {
                return $lbs;
        }
 
+       public function forEachLB( $callback, array $params = [] ) {
+               foreach ( $this->mainLBs as $lb ) {
+                       $callback( $lb, ...$params );
+               }
+               foreach ( $this->externalLBs as $lb ) {
+                       $callback( $lb, ...$params );
+               }
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return string
+        */
+       private function getSectionForDomain( $domain = false ) {
+               if ( $this->lastDomain === $domain ) {
+                       return $this->lastSection;
+               }
+
+               $database = $this->getDomainDatabase( $domain );
+               $section = $this->sectionsByDB[$database] ?? self::CLUSTER_MAIN_DEFAULT;
+               $this->lastSection = $section;
+               $this->lastDomain = $domain;
+
+               return $section;
+       }
+
        /**
         * Make a new load balancer object based on template and load array
         *
-        * @param array $template
-        * @param array $loads
-        * @param array $groupLoads
+        * @param array $serverTemplate Server config map
+        * @param int[][] $groupLoads Map of (group => host => load)
         * @param string|bool $readOnlyReason
         * @return LoadBalancer
         */
-       private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
+       private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason ) {
                $lb = new LoadBalancer( array_merge(
                        $this->baseLoadBalancerParams(),
                        [
-                               'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
+                               'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
                                'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
                                'readOnlyReason' => $readOnlyReason
                        ]
@@ -293,45 +282,37 @@ class LBFactoryMulti extends LBFactory {
        }
 
        /**
-        * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+        * Make a server array as expected by LoadBalancer::__construct()
         *
-        * @param array $template
-        * @param array $loads
-        * @param array $groupLoads
-        * @return array
+        * @param array $serverTemplate Server config map
+        * @param int[][] $groupLoads Map of (group => host => load)
+        * @return array[] List of server config maps
         */
-       private function makeServerArray( $template, $loads, $groupLoads ) {
-               $servers = [];
-               $master = true;
-               $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
-               foreach ( $groupLoadsByServer as $server => $stuff ) {
-                       if ( !isset( $loads[$server] ) ) {
-                               $loads[$server] = 0;
-                       }
+       private function makeServerArray( array $serverTemplate, array $groupLoads ) {
+               // The master server is the first host explicitly listed in the generic load group
+               if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) {
+                       throw new UnexpectedValueException( "Empty generic load array; no master defined." );
                }
-               foreach ( $loads as $serverName => $load ) {
-                       $serverInfo = $template;
-                       if ( $master ) {
-                               $serverInfo['master'] = true;
-                               if ( $this->masterTemplateOverrides ) {
-                                       $serverInfo = $this->masterTemplateOverrides + $serverInfo;
-                               }
-                               $master = false;
-                       } else {
-                               $serverInfo['replica'] = true;
-                       }
-                       if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
-                               $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
-                       }
-                       if ( isset( $groupLoadsByServer[$serverName] ) ) {
-                               $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
-                       }
-                       $serverInfo['host'] = $this->hostsByName[$serverName] ?? $serverName;
-                       $serverInfo['hostName'] = $serverName;
-                       $serverInfo['load'] = $load;
-                       $serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ];
 
-                       $servers[] = $serverInfo;
+               $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads );
+               // Get the ordered map of (host => load); the master server is first
+               $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC];
+               // Implictly append any hosts that only appear in custom load groups
+               $genericLoads += array_fill_keys( array_keys( $groupLoadsByHost ), 0 );
+
+               $servers = [];
+               foreach ( $genericLoads as $host => $load ) {
+                       $servers[] = array_merge(
+                               $serverTemplate,
+                               $servers ? [] : $this->masterTemplateOverrides,
+                               $this->templateOverridesByServer[$host] ?? [],
+                               [
+                                       'host' => $this->hostsByName[$host] ?? $host,
+                                       'hostName' => $host,
+                                       'load' => $load,
+                                       'groupLoads' => $groupLoadsByHost[$host] ?? []
+                               ]
+                       );
                }
 
                return $servers;
@@ -339,14 +320,15 @@ class LBFactoryMulti extends LBFactory {
 
        /**
         * Take a group load array indexed by group then server, and reindex it by server then group
-        * @param array $groupLoads
-        * @return array
+        * @param int[][] $groupLoads Map of (group => host => load)
+        * @return int[][] Map of (host => group => load)
         */
-       private function reindexGroupLoads( $groupLoads ) {
+       private function reindexGroupLoads( array $groupLoads ) {
                $reindexed = [];
-               foreach ( $groupLoads as $group => $loads ) {
-                       foreach ( $loads as $server => $load ) {
-                               $reindexed[$server][$group] = $load;
+
+               foreach ( $groupLoads as $group => $loadByHost ) {
+                       foreach ( $loadByHost as $host => $load ) {
+                               $reindexed[$host][$group] = $load;
                        }
                }
 
@@ -357,18 +339,9 @@ class LBFactoryMulti extends LBFactory {
         * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
         * @return string
         */
-       private function getDatabaseFromDomain( $domain = false ) {
+       private function getDomainDatabase( $domain = false ) {
                return ( $domain === false )
                        ? $this->localDomain->getDatabase()
                        : DatabaseDomain::newFromId( $domain )->getDatabase();
        }
-
-       public function forEachLB( $callback, array $params = [] ) {
-               foreach ( $this->mainLBs as $lb ) {
-                       $callback( $lb, ...$params );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       $callback( $lb, ...$params );
-               }
-       }
 }
index fd76d88..7e73e5b 100644 (file)
@@ -32,20 +32,20 @@ class LBFactorySimple extends LBFactory {
        /** @var LoadBalancer */
        private $mainLB;
        /** @var LoadBalancer[] */
-       private $extLBs = [];
+       private $externalLBs = [];
 
-       /** @var array[] Map of (server index => server config) */
-       private $servers = [];
-       /** @var array[] Map of (cluster => (server index => server config)) */
-       private $externalClusters = [];
+       /** @var array[] Map of (server index => server config map) */
+       private $mainServers = [];
+       /** @var array[][] Map of (cluster => server index => server config map) */
+       private $externalServersByCluster = [];
 
        /** @var string */
        private $loadMonitorClass;
 
        /**
         * @see LBFactory::__construct()
-        * @param array $conf Parameters of LBFactory::__construct() as well as:
-        *   - servers : list of server configuration maps to Database::factory().
+        * @param array $conf Additional parameters include:
+        *   - servers : list of server config maps to Database::factory().
         *      Additionally, the server maps should have a 'load' key, which is used to decide
         *      how often clients connect to one server verses the others. A 'max lag' key should
         *      also be set on server maps, indicating how stale the data can be before the load
@@ -57,25 +57,30 @@ class LBFactorySimple extends LBFactory {
        public function __construct( array $conf ) {
                parent::__construct( $conf );
 
-               $this->servers = $conf['servers'] ?? [];
-               foreach ( $this->servers as $i => $server ) {
+               $this->mainServers = $conf['servers'] ?? [];
+               foreach ( $this->mainServers as $i => $server ) {
                        if ( $i == 0 ) {
-                               $this->servers[$i]['master'] = true;
+                               $this->mainServers[$i]['master'] = true;
                        } else {
-                               $this->servers[$i]['replica'] = true;
+                               $this->mainServers[$i]['replica'] = true;
                        }
                }
 
-               $this->externalClusters = $conf['externalClusters'] ?? [];
-               $this->loadMonitorClass = $conf['loadMonitorClass'] ?? 'LoadMonitor';
+               foreach ( ( $conf['externalClusters'] ?? [] ) as $cluster => $servers ) {
+                       foreach ( $servers as $index => $server ) {
+                               $this->externalServersByCluster[$cluster][$index] = $server;
+                       }
+               }
+
+               $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
        }
 
        public function newMainLB( $domain = false ) {
-               return $this->newLoadBalancer( $this->servers );
+               return $this->newLoadBalancer( $this->mainServers );
        }
 
        public function getMainLB( $domain = false ) {
-               if ( !$this->mainLB ) {
+               if ( $this->mainLB === null ) {
                        $this->mainLB = $this->newMainLB( $domain );
                }
 
@@ -83,28 +88,28 @@ class LBFactorySimple extends LBFactory {
        }
 
        public function newExternalLB( $cluster ) {
-               if ( !isset( $this->externalClusters[$cluster] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"." );
+               if ( !isset( $this->externalServersByCluster[$cluster] ) ) {
+                       throw new InvalidArgumentException( "Unknown cluster '$cluster'." );
                }
 
-               return $this->newLoadBalancer( $this->externalClusters[$cluster] );
+               return $this->newLoadBalancer( $this->externalServersByCluster[$cluster] );
        }
 
        public function getExternalLB( $cluster ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster );
+               if ( !isset( $this->externalLBs[$cluster] ) ) {
+                       $this->externalLBs[$cluster] = $this->newExternalLB( $cluster );
                }
 
-               return $this->extLBs[$cluster];
+               return $this->externalLBs[$cluster];
        }
 
        public function getAllMainLBs() {
-               return [ 'DEFAULT' => $this->getMainLB() ];
+               return [ self::CLUSTER_MAIN_DEFAULT => $this->getMainLB() ];
        }
 
        public function getAllExternalLBs() {
                $lbs = [];
-               foreach ( $this->externalClusters as $cluster => $unused ) {
+               foreach ( array_keys( $this->externalServersByCluster ) as $cluster ) {
                        $lbs[$cluster] = $this->getExternalLB( $cluster );
                }
 
@@ -125,10 +130,10 @@ class LBFactorySimple extends LBFactory {
        }
 
        public function forEachLB( $callback, array $params = [] ) {
-               if ( isset( $this->mainLB ) ) {
+               if ( $this->mainLB !== null ) {
                        $callback( $this->mainLB, ...$params );
                }
-               foreach ( $this->extLBs as $lb ) {
+               foreach ( $this->externalLBs as $lb ) {
                        $callback( $lb, ...$params );
                }
        }
index 585a782..f60e8db 100644 (file)
@@ -68,7 +68,9 @@ class LoadBalancer implements ILoadBalancer {
        /** @var DatabaseDomain Local DB domain ID and default for selectDB() calls */
        private $localDomain;
 
-       /** @var Database[][][] Map of (connection category => server index => IDatabase[]) */
+       /**
+        * @var IDatabase[][][]|Database[][][] Map of (connection category => server index => IDatabase[])
+        */
        private $conns;
 
        /** @var array[] Map of (server index => server config array) */
@@ -99,7 +101,7 @@ class LoadBalancer implements ILoadBalancer {
        private $tableAliases = [];
        /** @var string[] Map of (index alias => index) */
        private $indexAliases = [];
-       /** @var array[] Map of (name => callable) */
+       /** @var callable[] Map of (name => callable) */
        private $trxRecurringCallbacks = [];
        /** @var bool[] Map of (domain => whether to use "temp tables only" mode) */
        private $tempTablesOnlyMode = [];
@@ -1301,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() )
@@ -1325,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 );
@@ -1336,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 19550f4..c3f8879 100644 (file)
@@ -107,6 +107,7 @@ class LoadMonitor implements ILoadMonitor {
 
                $key = $this->getCacheKey( $serverIndexes );
                # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
                # Keep keys around longer as fallbacks
                $staleTTL = 60;
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 170fc29..4fff1de 100644 (file)
@@ -64,7 +64,7 @@ abstract class LogEntryBase implements LogEntry {
         *
         * @since 1.26
         * @param string $blob
-        * @return array
+        * @return array|false
         */
        public static function extractParams( $blob ) {
                return unserialize( $blob );
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 fa9e1dc..9058340 100644 (file)
@@ -80,7 +80,7 @@ class ExifBitmapHandler extends BitmapHandler {
 
        /**
         * @param File $image
-        * @param array $metadata
+        * @param string $metadata
         * @return bool|int
         */
        public function isMetadataValid( $image, $metadata ) {
index f328760..3993795 100644 (file)
@@ -98,6 +98,7 @@ class FormatMetadata extends ContextSource {
         *   Exif::getFilteredData() or BitmapMetadataHandler )
         * @return array
         * @since 1.23
+        * @suppress PhanTypeArraySuspiciousNullable
         */
        public function makeFormattedData( $tags ) {
                $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
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 15c4dbf..880d382 100644 (file)
@@ -62,7 +62,7 @@ class TiffHandler extends ExifBitmapHandler {
         * @param string $ext
         * @param string $mime
         * @param array|null $params
-        * @return bool
+        * @return array
         */
        public function getThumbType( $ext, $mime, $params = null ) {
                global $wgTiffThumbnailType;
index e634edc..d713396 100644 (file)
@@ -144,7 +144,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        $this->numServerShards = count( $this->serverInfos );
                } else {
                        // Default to using the main wiki's database servers
-                       $this->serverInfos = false;
+                       $this->serverInfos = [];
                        $this->numServerShards = 1;
                        $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
index d8cd1c5..0149171 100644 (file)
@@ -1929,6 +1929,8 @@ class Article implements Page {
 
                $outputPage->enableOOUI();
 
+               $fields = [];
+
                $options = Xml::listDropDownOptions(
                        $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->text(),
                        [ 'other' => $ctx->msg( 'deletereasonotherlist' )->inContentLanguage()->text() ]
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 dc75541..9edaccc 100644 (file)
@@ -120,6 +120,7 @@ class ImageHistoryList extends ContextSource {
                $lang = $this->getLanguage();
                $pm = MediaWikiServices::getInstance()->getPermissionManager();
                $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
+               // @phan-suppress-next-line PhanUndeclaredMethod
                $img = $iscur ? $file->getName() : $file->getArchiveName();
                $userId = $file->getUser( 'id' );
                $userText = $file->getUser( 'text' );
index 2e43e8c..2f6d4da 100644 (file)
@@ -27,6 +27,9 @@ use Wikimedia\Rdbms\ResultWrapper;
  * Class for viewing MediaWiki file description pages
  *
  * @ingroup Media
+ *
+ * @property WikiFilePage $mPage Set by overwritten newPage() in this class
+ * @method WikiFilePage getPage()
  */
 class ImagePage extends Article {
        /** @var File|false */
@@ -41,11 +44,6 @@ class ImagePage extends Article {
        /** @var bool */
        protected $mExtraDescription = false;
 
-       /**
-        * @var WikiFilePage
-        */
-       protected $mPage;
-
        /**
         * @param Title $title
         * @return WikiFilePage
index 26da151..2f3fac2 100644 (file)
@@ -28,7 +28,8 @@ use NamespaceInfo;
 use RepoGroup;
 use Title;
 use WatchedItemStore;
-use Wikimedia\Rdbms\LoadBalancer;
+use WatchedItemStoreInterface;
+use Wikimedia\Rdbms\ILoadBalancer;
 
 /**
  * @since 1.34
@@ -37,7 +38,7 @@ class MovePageFactory {
        /** @var ServiceOptions */
        private $options;
 
-       /** @var LoadBalancer */
+       /** @var ILoadBalancer */
        private $loadBalancer;
 
        /** @var NamespaceInfo */
@@ -63,9 +64,9 @@ class MovePageFactory {
 
        public function __construct(
                ServiceOptions $options,
-               LoadBalancer $loadBalancer,
+               ILoadBalancer $loadBalancer,
                NamespaceInfo $nsInfo,
-               WatchedItemStore $watchedItems,
+               WatchedItemStoreInterface $watchedItems,
                PermissionManager $permMgr,
                RepoGroup $repoGroup
        ) {
index 2cb1fc0..52b2719 100644 (file)
 
 /**
  * Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
+ *
+ * @method array getActionOverrides()
+ * @method string getUserText($audience=1,User $user=null)
+ * @method string getTimestamp()
+ * @method Title getTitle()
  */
 interface Page {
 }
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 4607535..9c5c4e0 100644 (file)
@@ -41,6 +41,8 @@ use Wikimedia\Rdbms\LoadBalancer;
  *
  * Some fields are public only for backwards-compatibility. Use accessors.
  * In the past, this class was part of Article.php and everything was public.
+ *
+ * @phan-file-suppress PhanAccessMethodInternal Due to the use of DerivedPageDataUpdater
  */
 class WikiPage implements Page, IDBAccessObject {
        // Constants for $mDataLoadedFrom and related
@@ -68,7 +70,9 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public $mLatest = false;
 
-       /** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */
+       /**
+        * @var PreparedEdit|false Map of cache fields (text, parser output, ect) for a proposed/new edit
+        */
        public $mPreparedEdit = false;
 
        /**
@@ -2390,7 +2394,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 ] );
                }
@@ -2791,7 +2795,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 327dd77..d4f66f7 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * @ingroup Parser
+ * @property string[] $out
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
 class PPDPart_Hash extends PPDPart {
index adc0bc0..68f1bb2 100644 (file)
  * @ingroup Parser
  */
 class PPDStack {
-       public $stack, $rootAccum;
+       /** @var PPDStackElement[] */
+       public $stack;
+       public $rootAccum;
 
        /**
-        * @var PPDStack|false
+        * @var PPDStackElement|false
         */
        public $top;
        public $out;
index 5de5f47..750049d 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * @ingroup Parser
+ * @property PPDPart_Hash[] $parts
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
 class PPDStackElement_Hash extends PPDStackElement {
@@ -35,6 +36,7 @@ class PPDStackElement_Hash extends PPDStackElement {
         *
         * @param int|bool $openingCount
         * @return array
+        * @suppress PhanParamSignatureMismatch
         */
        public function breakSyntax( $openingCount = false ) {
                if ( $this->open == "\n" ) {
@@ -59,7 +61,7 @@ class PPDStackElement_Hash extends PPDStackElement {
                                } else {
                                        $accum[++$lastIndex] = '|';
                                }
-                               // @phan-suppress-next-line PhanTypeMismatchForeach
+
                                foreach ( $part->out as $node ) {
                                        if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
                                                $accum[$lastIndex] .= $node;
index 79c7c3b..3f147f0 100644 (file)
@@ -69,6 +69,7 @@ interface PPFrame {
         * @param string $sep
         * @param int $flags
         * @param string|PPNode $args,...
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return string
         */
        public function implodeWithFlags( $sep, $flags /*, ... */ );
@@ -77,6 +78,7 @@ interface PPFrame {
         * Implode with no flags specified
         * @param string $sep
         * @param string|PPNode $args,...
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return string
         */
        public function implode( $sep /*, ... */ );
@@ -85,20 +87,22 @@ interface PPFrame {
         * Makes an object that, when expand()ed, will be the same as one obtained
         * with implode()
         * @param string $sep
-        * @param string|PPNode $args,...
+        * @param string|PPNode ...$args
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return PPNode
         */
-       public function virtualImplode( $sep /*, ... */ );
+       public function virtualImplode( $sep /* ...$args */ );
 
        /**
         * Virtual implode with brackets
         * @param string $start
         * @param string $sep
         * @param string $end
-        * @param string|PPNode $args,...
+        * @param string|PPNode ...$args
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return PPNode
         */
-       public function virtualBracketedImplode( $start, $sep, $end /*, ... */ );
+       public function virtualBracketedImplode( $start, $sep, $end /* ...$args */ );
 
        /**
         * Returns true if there are no arguments in this frame
index e3c12eb..ac3a266 100644 (file)
@@ -23,6 +23,7 @@
  * An expansion frame, used as a context to expand the result of preprocessToObj()
  * @deprecated since 1.34, use PPFrame_Hash
  * @ingroup Parser
+ * @phan-file-suppress PhanUndeclaredMethod
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
 class PPFrame_DOM implements PPFrame {
@@ -458,6 +459,7 @@ class PPFrame_DOM implements PPFrame {
         * @param string $sep
         * @param string|PPNode_DOM|DOMNode ...$args
         * @return array
+        * @suppress PhanParamSignatureMismatch
         */
        public function virtualImplode( $sep, ...$args ) {
                $out = [];
@@ -489,6 +491,7 @@ class PPFrame_DOM implements PPFrame {
         * @param string $end
         * @param string|PPNode_DOM|DOMNode ...$args
         * @return array
+        * @suppress PhanParamSignatureMismatch
         */
        public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
                $out = [ $start ];
index 53b1761..ae7f8a2 100644 (file)
@@ -22,6 +22,7 @@
 /**
  * @deprecated since 1.34, use PPNode_Hash_{Tree,Text,Array,Attr}
  * @ingroup Parser
+ * @phan-file-suppress PhanUndeclaredMethod
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
 class PPNode_DOM implements PPNode {
index 130667e..b27338c 100644 (file)
@@ -2010,6 +2010,7 @@ class Parser {
         */
        public function replaceExternalLinks( $text ) {
                $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+               // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
                if ( $bits === false ) {
                        throw new MWException( "PCRE needs to be compiled with "
                                . "--enable-unicode-properties in order for MediaWiki to function" );
@@ -6189,7 +6190,9 @@ class Parser {
         */
        private static function normalizeSectionName( $text ) {
                # T90902: ensure the same normalization is applied for IDs as to links
+               /** @var MediaWikiTitleCodec $titleParser */
                $titleParser = MediaWikiServices::getInstance()->getTitleParser();
+               '@phan-var MediaWikiTitleCodec $titleParser';
                try {
 
                        $parts = $titleParser->splitTitleString( "#$text" );
index 93f4246..bd610de 100644 (file)
@@ -40,6 +40,7 @@ class ParserDiffTest {
                if ( !is_null( $this->parsers ) ) {
                        return;
                }
+               $this->parsers = [];
 
                if ( isset( $this->conf['shortOutput'] ) ) {
                        $this->shortOutput = $this->conf['shortOutput'];
index b321078..99ca1be 100644 (file)
@@ -76,6 +76,7 @@ abstract class Preprocessor {
 
                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $key = $cache->makeKey(
+                       // @phan-suppress-next-line PhanUndeclaredConstant
                        defined( 'static::CACHE_PREFIX' ) ? static::CACHE_PREFIX : static::class,
                        md5( $text ),
                        $flags
@@ -108,6 +109,7 @@ abstract class Preprocessor {
                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
 
                $key = $cache->makeKey(
+                       // @phan-suppress-next-line PhanUndeclaredConstant
                        defined( 'static::CACHE_PREFIX' ) ? static::CACHE_PREFIX : static::class,
                        md5( $text ),
                        $flags
index f7f37ac..9f4b7c7 100644 (file)
@@ -628,7 +628,9 @@ class Preprocessor_Hash extends Preprocessor {
                                }
                                $i += $count;
                        } elseif ( $found == 'close' ) {
+                               /** @var PPDStackElement_Hash $piece */
                                $piece = $stack->top;
+                               '@phan-var PPDStackElement_Hash $piece';
                                # lets check if there are enough characters for closing brace
                                $maxCount = $piece->count;
                                if ( $piece->close === '}-' && $curChar === '}' ) {
index f3d8d03..389b8c3 100644 (file)
@@ -59,6 +59,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword {
                        // Construct pseudo-hash based on params and arguments
                        /** @var ParameterizedPassword $passObj */
                        $passObj = $this->factory->newFromType( $type );
+                       '@phan-var ParameterizedPassword $passObj';
 
                        $params = '';
                        $args = '';
@@ -72,6 +73,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword {
 
                        // Hash the last hash with the next type in the layer
                        $passObj = $this->factory->newFromCiphertext( $existingHash );
+                       '@phan-var ParameterizedPassword $passObj';
                        $passObj->crypt( $lastHash );
 
                        // Move over the params and args
@@ -114,6 +116,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword {
                        // Construct pseudo-hash based on params and arguments
                        /** @var ParameterizedPassword $passObj */
                        $passObj = $this->factory->newFromType( $type );
+                       '@phan-var ParameterizedPassword $passObj';
 
                        $params = '';
                        $args = '';
@@ -127,6 +130,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword {
 
                        // Hash the last hash with the next type in the layer
                        $passObj = $this->factory->newFromCiphertext( $existingHash );
+                       '@phan-var ParameterizedPassword $passObj';
                        $passObj->crypt( $lastHash );
 
                        // Move over the params and args
index f5fa4c7..c89dc15 100644 (file)
@@ -152,7 +152,9 @@ class PoolCounterRedis extends PoolCounter {
                if ( !$status->isOK() ) {
                        return $status;
                }
+               /** @var RedisConnRef $conn */
                $conn = $status->value;
+               '@phan-var RedisConnRef $conn';
 
                // phpcs:disable Generic.Files.LineLength
                static $script =
@@ -238,7 +240,9 @@ LUA;
                if ( !$status->isOK() ) {
                        return $status;
                }
+               /** @var RedisConnRef $conn */
                $conn = $status->value;
+               '@phan-var RedisConnRef $conn';
 
                $now = microtime( true );
                try {
index 00c2903..8a82add 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';
@@ -1585,12 +1586,11 @@ class DefaultPreferencesFactory implements PreferencesFactory {
         * Handle the form submission if everything validated properly
         *
         * @param array $formData
-        * @param HTMLForm $form
+        * @param PreferencesFormOOUI $form
         * @param array[] $formDescriptor
         * @return bool|Status|string
         */
-       protected function saveFormData( $formData, HTMLForm $form, array $formDescriptor ) {
-               /** @var \User $user */
+       protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
                $user = $form->getModifiedUser();
                $hiddenPrefs = $this->options->get( 'HiddenPrefs' );
                $result = true;
@@ -1688,11 +1688,15 @@ class DefaultPreferencesFactory implements PreferencesFactory {
         * Save the form data and reload the page
         *
         * @param array $formData
-        * @param HTMLForm $form
+        * @param PreferencesFormOOUI $form
         * @param array $formDescriptor
         * @return Status
         */
-       protected function submitForm( array $formData, HTMLForm $form, array $formDescriptor ) {
+       protected function submitForm(
+               array $formData,
+               PreferencesFormOOUI $form,
+               array $formDescriptor
+       ) {
                $res = $this->saveFormData( $formData, $form, $formDescriptor );
 
                if ( $res === true ) {
@@ -1726,6 +1730,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
         */
        protected function getTimeZoneList( Language $language ) {
                $identifiers = DateTimeZone::listIdentifiers();
+               // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3162
                if ( $identifiers === false ) {
                        return [];
                }
index 20f9a78..ab59efe 100644 (file)
@@ -1,7 +1,9 @@
 <?php
 
 class ProfilerExcimer extends Profiler {
+       /** @var ExcimerProfiler */
        private $cpuProf;
+       /** @var ExcimerProfiler */
        private $realProf;
        private $period;
 
index 09f5688..64a504a 100644 (file)
@@ -26,6 +26,7 @@
  * @ingroup Profiler
  *
  * @since 1.25
+ * @property ProfilerXhprof $collector
  */
 class ProfilerOutputDump extends ProfilerOutput {
 
index 9b5b29e..d0b7ae3 100644 (file)
@@ -61,7 +61,6 @@ abstract class FormattedRCFeed extends RCFeed {
                        // @codeCoverageIgnoreStart
                        // T109544 - If a feed formatter returns null, this will otherwise cause an
                        // error in at least RedisPubSubFeedEngine. Not sure best to handle this.
-                       // @phan-suppress-next-line PhanTypeMismatchReturn
                        return;
                        // @codeCoverageIgnoreEnd
                }
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 cf0b3c2..d84a92a 100644 (file)
@@ -35,6 +35,7 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
         */
        private $context;
 
+       /** @var int|array */
        protected $modules = self::INHERIT_VALUE;
        protected $language = self::INHERIT_VALUE;
        protected $direction = self::INHERIT_VALUE;
@@ -54,7 +55,7 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
                if ( $this->modules === self::INHERIT_VALUE ) {
                        return $this->context->getModules();
                }
-               // @phan-suppress-next-line PhanTypeMismatchReturn
+
                return $this->modules;
        }
 
index 0785225..693afcf 100644 (file)
@@ -547,13 +547,81 @@ class ResourceLoader implements LoggerAwareInterface {
        }
 
        /**
+        * @internal For use by ResourceLoaderStartUpModule only.
+        */
+       const HASH_LENGTH = 5;
+
+       /**
+        * Create a hash for module versioning purposes.
+        *
+        * This hash is used in three ways:
+        *
+        * - To differentiate between the current version and a past version
+        *   of a module by the same name.
+        *
+        *   In the cache key of localStorage in the browser (mw.loader.store).
+        *   This store keeps only one version of any given module. As long as the
+        *   next version the client encounters has a different hash from the last
+        *   version it saw, it will correctly discard it in favour of a network fetch.
+        *
+        *   A browser may evict a site's storage container for any reason (e.g. when
+        *   the user hasn't visited a site for some time, and/or when the device is
+        *   low on storage space). Anecdotally it seems devices rarely keep unused
+        *   storage beyond 2 weeks on mobile devices and 4 weeks on desktop.
+        *   But, there is no hard limit or expiration on localStorage.
+        *   ResourceLoader's Client also clears localStorage when the user changes
+        *   their language preference or when they (temporarily) use Debug Mode.
+        *
+        *   The only hard factors that reduce the range of possible versions are
+        *   1) the name and existence of a given module, and
+        *   2) the TTL for mw.loader.store, and
+        *   3) the `$wgResourceLoaderStorageVersion` configuration variable.
+        *
+        * - To identify a batch response of modules from load.php in an HTTP cache.
+        *
+        *   When fetching modules in a batch from load.php, a combined hash
+        *   is created by the JS code, and appended as query parameter.
+        *
+        *   In cache proxies (e.g. Varnish, Nginx) and in the browser's HTTP cache,
+        *   these urls are used to identify other previously cached responses.
+        *   The range of possible versions a given version has to be unique amongst
+        *   is determined by the maximum duration each response is stored for, which
+        *   is controlled by `$wgResourceLoaderMaxage['versioned']`.
+        *
+        * - To detect race conditions between multiple web servers in a MediaWiki
+        *   deployment of which some have the newer version and some still the older
+        *   version.
+        *
+        *   An HTTP request from a browser for the Startup manifest may be responded
+        *   to by a server with the newer version. The browser may then use that to
+        *   request a given module, which may then be responded to by a server with
+        *   the older version. To avoid caching this for too long (which would pollute
+        *   all other users without repairing itself), the combined hash that the JS
+        *   client adds to the url is verified by the server (in ::sendResponseHeaders).
+        *   If they don't match, we instruct cache proxies and clients to not cache
+        *   this response as long as they normally would. This is also the reason
+        *   that the algorithm used here in PHP must match the one used in JS.
+        *
+        * The fnv132 digest creates a 32-bit integer, which goes upto 4 Giga and
+        * needs up to 7 chars in base 36.
+        * Within 7 characters, base 36 can count up to 78,364,164,096 (78 Giga),
+        * (but with fnv132 we'd use very little of this range, mostly padding).
+        * Within 6 characters, base 36 can count up to 2,176,782,336 (2 Giga).
+        * Within 5 characters, base 36 can count up to 60,466,176 (60 Mega).
+        *
         * @since 1.26
         * @param string $value
         * @return string Hash
         */
        public static function makeHash( $value ) {
                $hash = hash( 'fnv132', $value );
-               return Wikimedia\base_convert( $hash, 16, 36, 7 );
+               // The base_convert will pad it (if too short),
+               // then substr() will trim it (if too long).
+               return substr(
+                       Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
+                       0,
+                       self::HASH_LENGTH
+               );
        }
 
        /**
@@ -1255,7 +1323,7 @@ MESSAGE;
         *
         * @internal
         * @since 1.32
-        * @param bool|string|array $data
+        * @param mixed $data
         * @return string JSON
         */
        public static function encodeJsonForScript( $data ) {
index e17b393..151b5fd 100644 (file)
@@ -240,19 +240,22 @@ class ResourceLoaderClientHtml {
         * - Inline scripts can't be asynchronous.
         * - For styles, earlier is better.
         *
+        * @param string|null $nojsClass Class name that caller uses on HTML document element
         * @return string|WrappedStringList HTML
         */
-       public function getHeadHtml() {
+       public function getHeadHtml( $nojsClass = null ) {
                $nonce = $this->options['nonce'];
                $data = $this->getData();
                $chunks = [];
 
                // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
                // This must happen synchronously on every page view to avoid flashes of wrong content.
-               // See also #getDocumentAttributes() and /resources/src/startup.js.
-               $script = <<<'JAVASCRIPT'
-document.documentElement.className = document.documentElement.className
-       .replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );
+               // See also startup/startup.js.
+               $nojsClass = $nojsClass ?? $this->getDocumentAttributes()['class'];
+               $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass );
+               $jsClassJson = ResourceLoader::encodeJsonForScript( $jsClass );
+               $script = <<<JAVASCRIPT
+document.documentElement.className = {$jsClassJson};
 JAVASCRIPT;
 
                // Inline script: Declare mw.config variables for this page.
index c3948cb..95b8ff0 100644 (file)
@@ -55,6 +55,7 @@ class ResourceLoaderContext implements MessageLocalizer {
        protected $direction;
        protected $hash;
        protected $userObj;
+       /** @var ResourceLoaderImage|false */
        protected $imageObj;
 
        /**
@@ -214,6 +215,7 @@ class ResourceLoaderContext implements MessageLocalizer {
         * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
         *   or a MessageSpecifier.
         * @param mixed $args,...
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function msg( $key ) {
index 9c204fc..55e9e53 100644 (file)
@@ -137,7 +137,6 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
                                        $dataPath->getRemoteBasePath()
                                );
                        } else {
-                               // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
                                $path = dirname( $dataPath ) . '/' . $path;
                        }
                };
index 58c9ee5..d4a34f3 100644 (file)
@@ -104,6 +104,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
                        'wgSiteName' => $conf->get( 'Sitename' ),
                        'wgDBname' => $conf->get( 'DBname' ),
+                       'wgWikiID' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ),
                        'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
                        'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
                        // MediaWiki sets cookies to have this prefix by default
@@ -291,7 +292,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                                $states[$name] = 'error';
                        }
 
-                       if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
+                       if ( $versionHash !== '' && strlen( $versionHash ) !== ResourceLoader::HASH_LENGTH ) {
                                $e = new RuntimeException( "Badly formatted module version hash" );
                                $resourceLoader->outputErrorAndLog( $e,
                                                "Module '{module}' produced an invalid version hash: '{version}'.",
index ab9830f..5b03ad0 100644 (file)
@@ -23,10 +23,11 @@ use MediaWiki\Storage\RevisionRecord;
 
 /**
  * Item class for a filearchive table row
+ *
+ * @property ArchivedFile $file
+ * @property RevDelArchivedFileList $list
  */
 class RevDelArchivedFileItem extends RevDelFileItem {
-       /** @var RevDelArchivedFileList $list */
-       /** @var ArchivedFile $file */
        /** @var LocalFile */
        protected $lockFile;
 
index 680ae8e..74dd7bc 100644 (file)
@@ -27,6 +27,12 @@ use MediaWiki\Storage\RevisionRecord;
  * needs to be able to make a query from a set of identifiers to pull
  * relevant rows, to return RevDelItem subclasses wrapping them, and
  * to wrap bulk update operations.
+ *
+ * @property RevDelItem $current
+ * @method RevDelItem next()
+ * @method RevDelItem reset()
+ * @method RevDelItem current()
+ * @phan-file-suppress PhanParamSignatureMismatch
  */
 abstract class RevDelList extends RevisionListBase {
        function __construct( IContextSource $context, Title $title, array $ids ) {
@@ -394,7 +400,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 a5859e5..f61d378 100644 (file)
@@ -23,6 +23,8 @@ use MediaWiki\Storage\RevisionRecord;
 
 /**
  * Item class for a live revision table row
+ *
+ * @property RevDelRevisionList $list
  */
 class RevDelRevisionItem extends RevDelItem {
        /** @var Revision */
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 f36a7b5..7361265 100644 (file)
@@ -7,6 +7,7 @@
  * This trait can be used directly by extensions providing a SearchEngine.
  *
  * @ingroup Search
+ * @phan-file-suppress PhanUndeclaredMethod
  */
 trait SearchResultSetTrait {
        /**
index 64c2b84..a0b024e 100644 (file)
@@ -54,7 +54,7 @@ class PHPSessionHandler implements \SessionHandlerInterface {
        /** @var array Track original session fields for later modification check */
        protected $sessionFieldCache = [];
 
-       protected function __construct( SessionManagerInterface $manager ) {
+       protected function __construct( SessionManager $manager ) {
                $this->setEnableFlags(
                        \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
                );
@@ -106,9 +106,9 @@ class PHPSessionHandler implements \SessionHandlerInterface {
 
        /**
         * Install a session handler for the current web request
-        * @param SessionManagerInterface $manager
+        * @param SessionManager $manager
         */
-       public static function install( SessionManagerInterface $manager ) {
+       public static function install( SessionManager $manager ) {
                if ( self::$instance ) {
                        $manager->setupPHPSessionHandler( self::$instance );
                        return;
index a7bbcce..882eb39 100644 (file)
@@ -156,6 +156,7 @@ class SessionInfo {
                        $this->idIsSafe = $data['idIsSafe'];
                        $this->forceUse = $data['forceUse'] && $this->provider;
                } else {
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $this->id = $this->provider->getManager()->generateSessionId();
                        $this->idIsSafe = true;
                        $this->forceUse = false;
index 85f4569..09cdf72 100644 (file)
@@ -86,8 +86,7 @@ final class SessionManager implements SessionManagerInterface {
 
        /**
         * Get the global SessionManager
-        * @return SessionManagerInterface
-        *  (really a SessionManager, but this is to make IDEs less confused)
+        * @return self
         */
        public static function singleton() {
                if ( self::$instance === null ) {
@@ -321,6 +320,7 @@ final class SessionManager implements SessionManagerInterface {
 
        public function getVaryHeaders() {
                // @codeCoverageIgnoreStart
+               // @phan-suppress-next-line PhanUndeclaredConstant
                if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
                        return [];
                }
@@ -341,6 +341,7 @@ final class SessionManager implements SessionManagerInterface {
 
        public function getVaryCookies() {
                // @codeCoverageIgnoreStart
+               // @phan-suppress-next-line PhanUndeclaredConstant
                if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
                        return [];
                }
@@ -815,6 +816,7 @@ final class SessionManager implements SessionManagerInterface {
        public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
                // @codeCoverageIgnoreStart
                if ( defined( 'MW_NO_SESSION' ) ) {
+                       // @phan-suppress-next-line PhanUndeclaredConstant
                        if ( MW_NO_SESSION === 'warn' ) {
                                // Undocumented safety case for converting existing entry points
                                $this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
index 4ba7868..60eae42 100644 (file)
@@ -429,7 +429,8 @@ class Command {
 
                        // clear get_last_error without actually raising an error
                        // from https://www.php.net/manual/en/function.error-get-last.php#113518
-                       // TODO replace with clear_last_error when requirements are bumped to PHP7
+                       // TODO replace with error_clear_last after dropping HHVM
+                       // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                        set_error_handler( function () {
                        }, 0 );
                        AtEase::suppressWarnings();
index 19fa1da..478a615 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 {
index ec13765..bcf8b32 100644 (file)
@@ -89,7 +89,7 @@ class Site implements Serializable {
         *
         * @since 1.21
         *
-        * @var array[]
+        * @var array[]|false
         */
        protected $localIds = [];
 
index cad69a5..cd63796 100644 (file)
@@ -33,6 +33,7 @@ abstract class BaseTemplate extends QuickTemplate {
         *
         * @param string $name Message name
         * @param mixed $params,... Message params
+        * @suppress PhanCommentParamWithoutRealParam HHVM bug T228695#5450847
         * @return Message
         */
        public function getMsg( $name /* ... */ ) {
@@ -443,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.
index 3e8972c..70df73b 100644 (file)
@@ -376,6 +376,7 @@ class SkinTemplate extends Skin {
                                        /** @var CreditsAction $action */
                                        $action = Action::factory(
                                                'credits', $this->getWikiPage(), $this->getContext() );
+                                       '@phan-var CreditsAction $action';
                                        $tpl->set( 'credits',
                                                $action->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
                                } else {
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 939460f..fb69f63 100644 (file)
@@ -150,10 +150,11 @@ abstract class FormSpecialPage extends SpecialPage {
        /**
         * Process the form on POST submission.
         * @param array $data
-        * @param HTMLForm $form
+        * @param HTMLForm|null $form
+        * @suppress PhanCommentParamWithoutRealParam Many implementations don't have $form
         * @return bool|string|array|Status As documented for HTMLForm::trySubmit.
         */
-       abstract public function onSubmit( array $data /* $form = null */ );
+       abstract public function onSubmit( array $data /* HTMLForm $form = null */ );
 
        /**
         * Do something exciting on successful processing of the form, most likely to show a
index 62818a1..ce80c1a 100644 (file)
@@ -760,6 +760,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                $isLoggedIn = $this->getUser()->isLoggedIn();
                $continuePart = $this->isContinued() ? 'continue-' : '';
                $anotherPart = $isLoggedIn ? 'another-' : '';
+               // @phan-suppress-next-line PhanUndeclaredMethod
                $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
                $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
                $secureLoginLink = '';
index f2c9644..8134c9a 100644 (file)
@@ -568,6 +568,7 @@ class SpecialPageFactory {
                                return $title;
                        }
 
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $context->setTitle( $page->getPageTitle( $par ) );
                } elseif ( !$page->isIncludable() ) {
                        return false;
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..07214af 100644 (file)
@@ -423,6 +423,8 @@ class SpecialBlock extends FormSpecialPage {
                                foreach ( $block->getRestrictions() as $restriction ) {
                                        switch ( $restriction->getType() ) {
                                                case PageRestriction::TYPE:
+                                                       /** @var PageRestriction $restriction */
+                                                       '@phan-var PageRestriction $restriction';
                                                        if ( $restriction->getTitle() ) {
                                                                $pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
                                                        }
@@ -1031,7 +1033,7 @@ class SpecialBlock extends FormSpecialPage {
                $logId = $logEntry->insert();
 
                if ( !empty( $data['Tags'] ) ) {
-                       $logEntry->setTags( $data['Tags'] );
+                       $logEntry->addTags( $data['Tags'] );
                }
 
                $logEntry->publish( $logId );
index 7f075ed..6059cea 100644 (file)
@@ -316,6 +316,8 @@ class SpecialBotPasswords extends FormSpecialPage {
                        'restrictions' => $data['restrictions'],
                        'grants' => array_merge(
                                MWGrants::getHiddenGrants(),
+                               // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
+                               // it's probably failing to infer the type of $data['grants']
                                preg_replace( '/^grant-/', '', $data['grants'] )
                        )
                ] );
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 ceba987..ef1b3d8 100644 (file)
@@ -83,8 +83,10 @@ class SpecialExpandTemplates extends SpecialPage {
                                $dom = $parser->preprocessToDom( $input );
 
                                if ( method_exists( $dom, 'saveXML' ) ) {
+                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                        $xml = $dom->saveXML();
                                } else {
+                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                        $xml = $dom->__toString();
                                }
                        }
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 7f00311..33641cd 100644 (file)
@@ -264,6 +264,7 @@ class SpecialListGroupRights extends SpecialPage {
                ];
 
                foreach ( $changeGroups as $messageKey => $changeGroup ) {
+                       // @phan-suppress-next-line PhanTypeComparisonFromArray
                        if ( $changeGroup === true ) {
                                // For grep: listgrouprights-addgroup-all, listgrouprights-removegroup-all,
                                // listgrouprights-addgroup-self-all, listgrouprights-removegroup-self-all
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 30f4655..0bfe185 100644 (file)
@@ -197,26 +197,36 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                $user = $this->getUser();
 
                $significance = $this->getFilterGroup( 'significance' );
+               /** @var ChangesListBooleanFilter $hideMinor */
                $hideMinor = $significance->getFilter( 'hideminor' );
+               '@phan-var ChangesListBooleanFilter $hideMinor';
                $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
 
                $automated = $this->getFilterGroup( 'automated' );
+               /** @var ChangesListBooleanFilter $hideBots */
                $hideBots = $automated->getFilter( 'hidebots' );
+               '@phan-var ChangesListBooleanFilter $hideBots';
                $hideBots->setDefault( true );
 
+               /** @var ChangesListStringOptionsFilterGroup|null $reviewStatus */
                $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
+               '@phan-var ChangesListStringOptionsFilterGroup|null $reviewStatus';
                if ( $reviewStatus !== null ) {
                        // Conditional on feature being available and rights
                        if ( $user->getBoolOption( 'hidepatrolled' ) ) {
                                $reviewStatus->setDefault( 'unpatrolled' );
                                $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
+                               /** @var ChangesListBooleanFilter $legacyHidePatrolled */
                                $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
+                               '@phan-var ChangesListBooleanFilter $legacyHidePatrolled';
                                $legacyHidePatrolled->setDefault( true );
                        }
                }
 
                $changeType = $this->getFilterGroup( 'changeType' );
+               /** @var ChangesListBooleanFilter $hideCategorization */
                $hideCategorization = $changeType->getFilter( 'hidecategorization' );
+               '@phan-var ChangesListBooleanFilter $hideCategorization';
                if ( $hideCategorization !== null ) {
                        // Conditional on feature being available
                        $hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) );
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 2dcb77f..60cbff1 100644 (file)
@@ -47,6 +47,7 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage {
         */
        private function getExceptionList() {
                if ( $this->exceptionList === null ) {
+                       $this->exceptionList = [];
                        $exList = $this->msg( 'uncategorized-categories-exceptionlist' )
                                ->inContentLanguage()->plain();
                        $proposedTitles = explode( "\n", $exList );
index 9a16a72..075b5df 100644 (file)
@@ -247,6 +247,7 @@ class SpecialUndelete extends SpecialPage {
 
                $out->enableOOUI();
 
+               $fields = [];
                $fields[] = new OOUI\ActionFieldLayout(
                        new OOUI\TextInputWidget( [
                                'name' => 'prefix',
@@ -492,6 +493,7 @@ class SpecialUndelete extends SpecialPage {
                $buttonFields = [];
 
                if ( $isText ) {
+                       '@phan-var TextContent $content';
                        // TODO: MCR: make this work for multiple slots
                        // source view for textual content
                        $sourceView = Xml::element( 'textarea', [
@@ -768,6 +770,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..5747f67 100644 (file)
@@ -86,6 +86,7 @@ class UserrightsPage extends SpecialPage {
         *
         * @param string|null $par String if any subpage provided, else null
         * @throws UserBlockedError|PermissionsError
+        * @suppress PhanUndeclaredMethod
         */
        public function execute( $par ) {
                $user = $this->getUser();
@@ -464,7 +465,7 @@ class UserrightsPage extends SpecialPage {
                ] );
                $logid = $logEntry->insert();
                if ( count( $tags ) ) {
-                       $logEntry->setTags( $tags );
+                       $logEntry->addTags( $tags );
                }
                $logEntry->publish( $logid );
        }
@@ -479,10 +480,12 @@ class UserrightsPage extends SpecialPage {
                        $this->getOutput()->addWikiTextAsInterface( $status->getWikiText() );
 
                        return;
-               } else {
-                       $user = $status->value;
                }
 
+               /** @var User $user */
+               $user = $status->value;
+               '@phan-var User $user';
+
                $groups = $user->getGroups();
                $groupMemberships = $user->getGroupMemberships();
                $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
index f5239b4..3d56330 100644 (file)
@@ -148,6 +148,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
 
        /**
         * @inheritDoc
+        * @suppress PhanUndeclaredMethod
         */
        protected function registerFilters() {
                parent::registerFilters();
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 8063804..938b159 100644 (file)
@@ -30,6 +30,9 @@ class ImportReporter extends ContextSource {
        private $mOriginalLogCallback = null;
        private $mOriginalPageOutCallback = null;
        private $mLogItemCount = 0;
+       private $mPageCount;
+       private $mIsUpload;
+       private $mInterwiki;
 
        /**
         * @param WikiImporter $importer
@@ -160,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 77b7326..4441a33 100644 (file)
@@ -70,6 +70,12 @@ class BlockListPager extends TablePager {
                return $headers;
        }
 
+       /**
+        * @param string $name
+        * @param string $value
+        * @return string
+        * @suppress PhanTypeArraySuspicious
+        */
        function formatValue( $name, $value ) {
                static $msg = null;
                if ( $msg === null ) {
@@ -132,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' ),
@@ -259,6 +266,7 @@ class BlockListPager extends TablePager {
 
                        switch ( $restriction->getType() ) {
                                case PageRestriction::TYPE:
+                                       '@phan-var PageRestriction $restriction';
                                        if ( $restriction->getTitle() ) {
                                                $items[$restriction->getType()][] = Html::rawElement(
                                                        'li',
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 105eeaa..7234be2 100644 (file)
@@ -62,6 +62,7 @@ class NamespaceInfo {
        public static $canonicalNames = [
                NS_MEDIA            => 'Media',
                NS_SPECIAL          => 'Special',
+               NS_MAIN             => '',
                NS_TALK             => 'Talk',
                NS_USER             => 'User',
                NS_USER_TALK        => 'User_talk',
@@ -341,7 +342,7 @@ class NamespaceInfo {
         * Returns array of all defined namespaces with their canonical
         * (English) names.
         *
-        * @return array
+        * @return string[]
         */
        public function getCanonicalNamespaces() {
                if ( $this->canonicalNamespaces === null ) {
@@ -396,6 +397,7 @@ class NamespaceInfo {
         */
        public function getValidNamespaces() {
                if ( is_null( $this->validNamespaces ) ) {
+                       $this->validNamespaces = [];
                        foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
                                if ( $ns >= 0 ) {
                                        $this->validNamespaces[] = $ns;
index fb9dcf5..3368e29 100644 (file)
@@ -1763,7 +1763,6 @@ abstract class UploadBase {
         * Check a block of CSS or CSS fragment for anything that looks like
         * it is bringing in remote code.
         * @param string $value a string of CSS
-        * @param bool $propOnly only check css properties (start regex with :)
         * @return bool true if the CSS contains an illegal string, false if otherwise
         */
        private static function checkCssFragment( $value ) {
index df5edef..c3cbc6d 100644 (file)
@@ -514,6 +514,7 @@ class BotPassword implements IDBAccessObject {
                        $throttle->clear( $user->getName(), $request->getIP() );
                }
                return self::loginHook( $user, $bp,
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
        }
 
index aada319..fd8eb3f 100644 (file)
@@ -70,8 +70,6 @@ class PasswordReset implements LoggerAwareInterface {
        /**
         * Check if a given user has permission to use this functionality.
         * @param User $user
-        * @param bool $displayPassword If set, also check whether the user is allowed to reset the
-        *   password of another user and see the temporary password.
         * @since 1.29 Second argument for displayPassword removed.
         * @return StatusValue
         */
index 23c4cfb..7068879 100644 (file)
@@ -110,12 +110,6 @@ class User implements IDBAccessObject, UserIdentity {
                'mActorId',
        ];
 
-       /**
-        * @var string[]
-        * @var string[] Cached results of getAllRights()
-        */
-       protected static $mAllRights = false;
-
        /** Cache variables */
        // @{
        /** @var int */
@@ -255,7 +249,8 @@ class User implements IDBAccessObject, UserIdentity {
                        return $this->$name;
                } else {
                        wfLogWarning( 'tried to get non-visible property' );
-                       return null;
+                       $null = null;
+                       return $null;
                }
        }
 
@@ -1758,6 +1753,7 @@ class User implements IDBAccessObject, UserIdentity {
                // overwriting mBlockedby, surely?
                $this->load();
 
+               // @phan-suppress-next-line PhanAccessMethodInternal It's the only allowed use
                $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
                        $this,
                        $fromReplica
index b7d5058..c185bab 100644 (file)
@@ -65,6 +65,6 @@ class UserNamePrefixSearch {
                        $joinConds
                );
 
-               return $res === false ? [] : $res;
+               return $res;
        }
 }
index b2d6077..f3a8810 100644 (file)
@@ -34,6 +34,7 @@ class AvroValidator {
         * @return string|string[] An error or list of errors in the
         *  provided $datum. When no errors exist the empty array is
         *  returned.
+        * @suppress PhanUndeclaredMethod
         */
        public static function getErrors( AvroSchema $schema, $datum ) {
                switch ( $schema->type ) {
index 12b8a70..cf62f6d 100644 (file)
@@ -39,7 +39,7 @@ class ClassCollector {
        protected $startToken;
 
        /**
-        * @var array List of tokens that are members of the current expect sequence
+        * @var array[]|string[] List of tokens that are members of the current expect sequence
         */
        protected $tokens;
 
@@ -126,7 +126,7 @@ class ClassCollector {
        /**
         * Accepts the next token in an expect sequence
         *
-        * @param array $token
+        * @param array|string $token
         */
        protected function tryEndExpect( $token ) {
                switch ( $this->startToken[0] ) {
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 62ee9cb..fedac4b 100644 (file)
@@ -148,6 +148,7 @@ class SearchFormWidget {
         * @param string $profile The currently selected profile
         * @param string $term The user provided search terms
         * @return string HTML
+        * @suppress PhanTypeArraySuspiciousNullable
         */
        protected function profileTabsHtml( $profile, $term ) {
                $bareterm = $this->startsWithImage( $term )
index ff66b25..51ff8d5 100644 (file)
@@ -55,13 +55,15 @@ class Language {
        const SUPPORTED = 'mwfile';
 
        /**
-        * @var LanguageConverter
+        * @var LanguageConverter|FakeConverter
         */
        public $mConverter;
 
        public $mVariants, $mCode, $mLoaded = false;
        public $mMagicExtensions = [];
-       private $mHtmlCode = null, $mParentLanguage = false;
+       private $mHtmlCode = null;
+       /** @var Language|false */
+       private $mParentLanguage = false;
 
        public $dateFormatStrings = [];
        public $mExtendedSpecialPageAliases;
@@ -525,7 +527,6 @@ class Language {
                        }
 
                        # Sometimes a language will be localised but not actually exist on this wiki.
-                       // @phan-suppress-next-line PhanTypeMismatchForeach
                        foreach ( $this->namespaceNames as $key => $text ) {
                                if ( !isset( $validNamespaces[$key] ) ) {
                                        unset( $this->namespaceNames[$key] );
index d1a5720..9886425 100644 (file)
@@ -63,8 +63,7 @@ class LanguageConverter {
        public $mTablesLoaded = false;
 
        /**
-        * @var ReplacementArray[]
-        * @phan-var array<string,ReplacementArray>
+        * @var ReplacementArray[]|bool[]
         */
        public $mTables;
 
@@ -958,7 +957,7 @@ class LanguageConverter {
                }
 
                $this->mTablesLoaded = true;
-               $this->mTables = false;
+               $this->mTables = null;
                $cache = ObjectCache::getInstance( $wgLanguageConverterCacheType );
                $cacheKey = $cache->makeKey( 'conversiontables', $this->mMainLanguageCode );
                if ( $fromCache ) {
@@ -1045,6 +1044,7 @@ class LanguageConverter {
                                $revision = Revision::newFromTitle( $title );
                                if ( $revision ) {
                                        if ( $revision->getContentModel() == CONTENT_MODEL_WIKITEXT ) {
+                                               // @phan-suppress-next-line PhanUndeclaredMethod
                                                $txt = $revision->getContent( RevisionRecord::RAW )->getText();
                                        }
 
index 52b77fd..3508483 100644 (file)
        "backend-fail-contenttype": "تعذر تحديد نوع محتوى الملف الذي تريد تخزينه في \"$1\".",
        "backend-fail-batchsize": "أعطت خلفية التخزين دفعة $1 ملف {{PLURAL:$1|عملية|عمليات}}; الحد الأقصى هو $2 {{PLURAL:$2|عملية|عمليات}}.",
        "backend-fail-usable": "تعذر قراءة أو كتابة الملف \"$1\" لنقص في التراخيص أو فقدان الدلائل/الحاويات.",
+       "backend-fail-stat": "لا يمكن قراءة حالة الملف \"$1\".",
+       "backend-fail-hash": "لا يمكن تحديد دالة التشفير لملف \"$1\".",
        "filejournal-fail-dbconnect": "تعذر ربط الإتصال بقاعدة بيانات خلفية التخزين \"$1\".",
        "filejournal-fail-dbquery": "تعذر تحديث قاعدة بيانات خلفية تخزين \"$1\".",
        "lockmanager-notlocked": "تعذر فتح \"$1\"، الملف غير مغلق.",
index 75b3b3f..810c304 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.",
        "deletereason-dropdown": "*Əsas silmə səbəbləri\n** Spam\n** Vandalizm\n** Müəllif hüququ pozuntusu\n** Müəllif istəyi\n** Səhv yönləndirmə",
        "delete-edit-reasonlist": "Silmə səbəblərinin redaktəsi",
        "delete-toobig": "Bu səhifə $1-dən artıq redaktə ilə çox böyük redaktə tarixçəsinə malikdir.\n\"{{SITENAME}}\" saytının fəaliyyətində problemlər yaratmamaq üçün bu cür səhifələrin silinməsi qadağandır.",
+       "deleting-backlinks-warning": "<strong>Xəbərdarlıq:</strong> Silmək istədiyiniz səhifəyə [[Xüsusi:WhatLinksHere/{{FULLPAGENAME}}|başqa səhifələr]]dən keçid verilib.",
        "rollback": "əvvəlki halına qaytar",
        "rollbacklink": "əvvəlki halına qaytar",
        "rollbacklinkcount": "$1 {{PLURAL:$1|dəyişikliyi|dəyişikliyi}} geri qaytar",
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..1b3950f 100644 (file)
        "about": "Indik",
        "article": "Kaca daging",
        "newwindow": "(bukak ring jendela anyar)",
-       "cancel": "Buwung",
+       "cancel": "Wangdé",
        "moredotdotdot": "Lianan...",
        "mypage": "Kaca",
        "mytalk": "Pabligbagan",
        "variants": "Varian",
        "navigation-heading": "Menu navigasi",
        "errorpagetitle": "Kaiwangan",
-       "returnto": "mabalik ring $1",
+       "returnto": "Balik ring $1.",
        "tagline": "Saking {{SITENAME}}",
        "help": "Wantuan",
        "help-mediawiki": "Pitulung MediaWiki",
        "pool-errorunknown": "Iwang sané durung kauningin",
        "aboutsite": "Indik {{SITENAME}}",
        "aboutpage": "Project:Indik",
-       "copyrightpage": "{{ns:project}}:hak cipta",
+       "copyrightpage": "{{ns:project}}:Hak cipta",
        "currentevents": "Kawéntenané mangkin",
        "currentevents-url": "Project:Kawéntenané mangkin",
        "disclaimers": "Tulak",
        "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",
        "rcshowhidebots-hide": "Engkebang",
        "rcshowhideliu": "$1 sang anganggé madaptar",
        "rcshowhideliu-show": "Sinahang",
-       "rcshowhideliu-hide": "engkebang",
+       "rcshowhideliu-hide": "Engkebang",
        "rcshowhideanons": "$1 sang anganggé tan kauningin",
        "rcshowhideanons-show": "Sinahang",
        "rcshowhideanons-hide": "Engkebang",
        "rclinks": "Edengang untat $1 gentosan anyar $2 dina kaping untat",
        "diff": "bina",
        "hist": "bbd",
-       "hide": "engkebang",
+       "hide": "Engkebang",
        "show": "Sinahang",
        "minoreditletter": "a",
        "newpageletter": "A",
        "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",
-       "license-header": "kepahan lugra",
+       "license-header": "Lisénsi",
        "listfiles-delete": "usap",
        "imgfile": "depukan",
        "listfiles": "Bacakan depukan",
        "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",
        "filedelete-submit": "Usap",
        "filedelete-success": "<strong>$1</strong> sampun kausapin.",
        "filedelete-maintenance-title": "Nénten prasida ngusapin berkas",
-       "randompage": "Kaca punapi kémanten",
+       "randompage": "Kaca ulah-aluh",
        "statistics": "Statistik",
        "statistics-articles": "Kaca daging",
        "brokenredirects-edit": "uah",
        "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-n-portal": "Indik proyék, sané prasida kalaksanayang, genah ngrereh wantuan",
        "tooltip-n-currentevents": "Rereh pidarta indik kawéntenan sané pinih anyar",
        "tooltip-n-recentchanges": "Bacakan uahan sané mangkin ring wiki",
-       "tooltip-n-randompage": "Cihnayang kaca napi kémanten",
+       "tooltip-n-randompage": "Cihnayang kaca ulah-aluh",
        "tooltip-n-help": "Genah ngrereh wantuan",
        "tooltip-t-whatlinkshere": "Bacakan makasami kaca ring wiki sané nuju iriki",
        "tooltip-t-recentchangeslinked": "Uahan sané mangkin saking kaca-kaca sané linked ring kaca puniki",
        "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",
        "pageinfo-header-restrictions": "Saiban kaca",
        "pageinfo-header-properties": "Properti suratan",
        "pageinfo-display-title": "Edengang judul",
+       "pageinfo-length": "Dawannyané kaca (ring bita)",
        "pageinfo-namespace": "Genah wastan",
        "pageinfo-article-id": "ID kaca",
        "pageinfo-language": "Basa ring daging kaca",
        "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 b3416ca..11f3e1f 100644 (file)
@@ -65,7 +65,7 @@
        "sunday": "Domingo",
        "monday": "Lunes",
        "tuesday": "Martes",
-       "wednesday": "Miyerkoles",
+       "wednesday": "Miyerkules",
        "thursday": "Huwebes",
        "friday": "Biyernes",
        "saturday": "Sabado",
        "mypage": "Pahina",
        "mytalk": "Mag-ulay",
        "anontalk": "Mag-ulay",
-       "navigation": "Paglibotlibot",
+       "navigation": "Paglibot-libot",
        "and": "&#32;asin",
        "faq": "PHK (Pirmehang Hinahapot na mga Kahaputan)",
        "actions": "Mga paghiro",
        "namespaces": "Mga espasyong ngaran",
        "variants": "Mga Kinalaenan",
-       "navigation-heading": "Hihilngan sa paglibotlibot",
+       "navigation-heading": "Hihilngan sa paglibot-libot",
        "errorpagetitle": "Salâ",
        "returnto": "Magbalik sa $1.",
        "tagline": "Gikan sa {{SITENAME}}",
        "protect": "Protektaran",
        "protect_change": "Ribayan",
        "unprotect": "Ribayan an proteksyon",
-       "newpage": "Bàguhong pahina",
+       "newpage": "Bàgo pang pahina",
        "talkpagelinktext": "Mag-ulay",
        "specialpage": "Espesyal na pahina",
        "personaltools": "Pansadiring mga gamiton",
        "talk": "Urulayan",
-       "views": "Mga Tanawon",
+       "views": "Mga pagtànaw",
        "toolbox": "Mga gamiton:",
        "tool-link-userrights": "Ribayan {{GENDER:$1|paragamit}} an grupo",
        "tool-link-userrights-readonly": "Hilingon {{GENDER:$1|paragamit}} an grupo",
        "viewhelppage": "Tànawon an pahina nin pagtabang",
        "categorypage": "Tànawon an pahina nin kategoriya",
        "viewtalkpage": "Tànawon an urulay",
-       "otherlanguages": "Sa ibang mga lengguwahe",
+       "otherlanguages": "Sa ibang mga lengguwahe/tataramon",
        "redirectedfrom": "(Pinagbalikwat gikan sa $1)",
        "redirectpagesub": "Balikwaton an pahina",
        "redirectto": "Balikwaton pasiring sa:",
-       "lastmodifiedat": "Ining pahina huring pinagbago kan $1, alas $2.",
+       "lastmodifiedat": "Huring pigliwat an pahinang ini kan $1, alas $2.",
        "viewcount": "Ining pahina pinaglaog nin {{PLURAL:$1|sarong beses|nin $1 beses}}.",
        "protectedpage": "Protektadong pahina",
        "jumpto": "Maglukso sa:",
-       "jumptonavigation": "paglibotlibot",
+       "jumptonavigation": "paglibot-libot",
        "jumptosearch": "hanapon",
        "view-pool-error": "Sori tabi, an mga server kargado sa oras na ini.\nGrabe kadakol an mga paragamit na pinagprubaran mahiling an pahinang ini.\nMakihalat tabi nin kadikit na panahon bago ka magprubara na makapaglaog sa pahinang ini.\n\n$1",
        "generic-pool-error": "Sori tabi, an mga serbidor grabe kakargado sa oras na ini. Kadakulon na gayo an mga paragamit na minaprubar na hilngon ining kaggikanan. Tabi pakihalat kadikit bago ka magprubar otro na makapaglaog sa kaggikanang ini.",
        "page-rss-feed": "\"$1\" Hungit na RSS",
        "page-atom-feed": "\"$1\" Hungit Atomo",
        "feed-atom": "Atomo",
-       "red-link-title": "$1 (an pahina bako pang eksistido)",
+       "red-link-title": "$1 (bako pang eksistido an pahina)",
        "sort-descending": "Suysoy paibaba",
        "sort-ascending": "Suysoy paitaas",
        "nstab-main": "Pahina",
        "logout-failed": "Dae pa makaluwas ngunyan: $1",
        "cannotlogoutnow-title": "Dae pa makaluwas ngunyan",
        "cannotlogoutnow-text": "Dai posible an paglaog kun magamit nin $1.",
-       "welcomeuser": "Marhayong pag-abot, $1!",
-       "welcomecreation-msg": "An saimong panindog pinagmukna na.\nDae malingaw na liwaton an saimong [[Special:Preferences|{{SITENAME}} mga kamuyahan]].",
+       "welcomeuser": "Maaugmang pag-abot, $1!",
+       "welcomecreation-msg": "An saimong panindog pinagmukna na.\nDai malingaw na liwaton an saimong [[Special:Preferences|{{SITENAME}} mga kamuyahan]].",
        "yourname": "Pangaran kan paragamit:",
-       "userlogin-yourname": "Paragamit-na-Ngaran",
-       "userlogin-yourname-ph": "Ikaag an saimong paragamit-na-ngaran",
+       "userlogin-yourname": "Ngaran nin paragamit",
+       "userlogin-yourname-ph": "Ikaag an saimong ngaran-paragamit",
        "createacct-another-username-ph": "Ikaag an paragamit-na-ngaran",
-       "yourpassword": "Pasa-taramon:",
-       "userlogin-yourpassword": "Pasa-taramon",
+       "yourpassword": "Sekretong taramon:",
+       "userlogin-yourpassword": "Sekretong taramon",
        "userlogin-yourpassword-ph": "Ikaag an saimong sekretong panlaog",
-       "createacct-yourpassword-ph": "Ikaag an sekretong panlaog",
-       "yourpasswordagain": "Pakilaog giraray kan sekretong panlaog:",
+       "createacct-yourpassword-ph": "Ikaag an sekretong taramon",
+       "yourpasswordagain": "Pakilaog giraray kan sekretong taramon:",
        "createacct-yourpasswordagain": "Kumpirmaron an sekretong panlaog",
-       "createacct-yourpasswordagain-ph": "Pakikaag otro an sekretong panlaog",
+       "createacct-yourpasswordagain-ph": "Pakikaag liwat an sekretong taramon",
        "userlogin-remembermypassword": "Dagos mo akong giromdomon na nakalaog",
        "userlogin-signwithsecure": "Gamiton an seguradong koneksyon",
        "cannotlogin-title": "Dai makalaog",
        "nav-login-createaccount": "Maglaog / magmukna nin panindog",
        "logout": "Magluwas",
        "userlogout": "Magluwas",
-       "notloggedin": "Dae ka nakalaog",
+       "notloggedin": "Dai ka nakalaog",
        "userlogin-noaccount": "Mayo ka nin panindog?",
-       "userlogin-joinproject": "Mag-ayon{{SITENAME}}",
-       "createaccount": "Magmukna nin panindog",
-       "userlogin-resetpassword-link": "Nalingawan mo an saimong pasa-taramon?",
+       "userlogin-joinproject": "Mag-ayon sa {{SITENAME}}",
+       "createaccount": "Magmukna nin account",
+       "userlogin-resetpassword-link": "Nalingawan mo an saimong sekretong taramon?",
        "userlogin-helplink2": "Katabangan sa paglalaog",
        "userlogin-loggedin": "Ika nakalaog na tabi bilang si {{GENDER:$1|$1}}.\nGamita an porma sa ibaba sa paglaog bilang ibang paragamit.",
        "userlogin-reauth": "Kaipuhan maglaog ulit para mapatunayan na ika {{GENDER:$1|$1}}.",
-       "userlogin-createanother": "Magmukna nin ibang panindog",
+       "userlogin-createanother": "Magmukna nin ibang account",
        "createacct-emailrequired": "Estada kan e-surat",
-       "createacct-emailoptional": "E-surat na estada (opsyonal)",
-       "createacct-email-ph": "Pakikaag an saimong e-surat na estada",
+       "createacct-emailoptional": "Adres nin e-surat na estada (opsyonal)",
+       "createacct-email-ph": "Pakikaag an adres nin saimong e-surat",
        "createacct-another-email-ph": "Ikaag an estada kan e-surat",
        "createaccountmail": "Gumamit nin sarong temporaryong pampurak na pasa-taramon asin ipadara ini sa pinagsambit na estada kan e-surat",
        "createacct-realname": "Totoong pangaran (opsyonal)",
        "createacct-reason": "Rason",
        "createacct-reason-ph": "Tadaw ta ika magmumukna nin ibang panindog",
-       "createacct-submit": "Muknaon an saimong panindog",
+       "createacct-submit": "Muknaon an saimong account",
        "createacct-another-submit": "Magmukna nin panindog",
-       "createacct-continue-submit": "Magpadagos sa paggibo nin panlaog",
+       "createacct-continue-submit": "Magpadagos sa paggibo nin account",
        "createacct-another-continue-submit": "Magpadagos sa paggibo nin panlaog",
-       "createacct-benefit-heading": "{{SITENAME}} pinaghimo kan mga tawong siring mo.",
+       "createacct-benefit-heading": "An {{SITENAME}} pinaghimò kan mga tawong siring mo.",
        "createacct-benefit-body1": "{{PLURAL:$1|niliwat|mga niliwat}}",
        "createacct-benefit-body2": "{{PLURAL:$1|pahina|mga pahina}}",
        "createacct-benefit-body3": "pinakahuring {{PLURAL:$1|paraambag|mga paraambag}}",
-       "badretype": "An mga sekretong panlaog mong pinagtatak bakong pareho.",
+       "badretype": "An mga sekretong taramon mong pinagtatak bakong pareho.",
        "usernameinprogress": "An pagmukna kaning palaog kan paragamit nagpuon na. Maghalat tabi.",
        "userexists": "Paragamit na ngarang piglaog may naggagamit na.\nPakipili nin ibang ngaran tabi.",
        "loginerror": "An paglaog napasalâ",
-       "createacct-error": "Kasalaan sa pagmumukna nin panindog",
+       "createacct-error": "Salâ sa pagmumukna nin account",
        "createaccounterror": "Dae tabi maimukna an panindog: $1.",
        "nocookiesnew": "An panindog kan paragamit namukna na, pero ika dae pa tabi nakalaog.\n{{SITENAME}} naggagamit nin cookies tanganing makalaog an mga paragamit.\nIka igwang mga cookies na dae pinagana.\nTabi paganaha sinda, dangan maglaog ka sa saimong baguhon na pangaran kan paragamit asin sekretong panlaog.",
        "nocookieslogin": "{{SITENAME}} naggagamit nin mga cookies para sa maglaog na mga paragamit.\nIka igwang mga cookies na dae pinagana.\nTabi paganaha sinda asin otroha giraray.",
        "loginsuccess": "'''Ika ngunyan nakalaog na sa {{SITENAME}} bilang si \"$1\".'''",
        "nosuchuser": "Dae pang paragamit na ginagamit an pangaran na \"$1\".\nAn mga ngaran nin paragamit sensitibo gayo sa tipahan.\nPakireparo kan saimong espeling, o [[Special:CreateAccount|Magmukna nin bagong panindog]].",
        "nosuchusershort": "Mayo po tabing paragamit na an pangaran \"$1\".\nPaki-tsek an saimong espeling.",
-       "nouserspecified": "Kaipuhan mong magkaag nin sarong pangaran nin paragamit.",
-       "login-userblocked": "An paragamit na ini pinagkubkob. An paglaog dae pinagtutugutan.",
+       "nouserspecified": "Kaipohan mong magkaag nin sarong ngaran-paragamit.",
+       "login-userblocked": "An paragamit na ini pinagkubkob. An paglaog dai pinagtutugotan.",
        "wrongpassword": "Salâ an pigtaták na sekretong panlaog.\nTabi probaran giraray.",
        "wrongpasswordempty": "An sekretong panlaog pinagtatak na blangko.\nTabi probaran giraray.",
        "passwordtooshort": "Mga sekretong panlaog dapat igwa nin {{PLURAL:$1|1 karakter|$1 mga karakter}}.",
        "summary": "Sumaryo:",
        "subject": "Tema",
        "minoredit": "Ini sarong dikiton na pagliwat",
-       "watchthis": "Bantayan ining pahina",
+       "watchthis": "Bantayan an pahinang ini",
        "savearticle": "Itagáma an pahina",
        "savechanges": "Itagama an mga kaliwatan",
        "publishpage": "I-publikar an pahina",
        "newarticle": "(Bàgo)",
        "newarticletext": "Ika nakapagsunod sa sarong sugpon pasiring sa sarong pahina na bako pang eksistido. Tanganing makapagmukna nin pahina, magpoon sa pagpindot sa laog nin kahon sa ibaba (hilngon an [$1 pahina nin katabangan] para sa kadugangan na impormasyon).\nKun ika napasalang nakadigde, i-klik an  '''ibalik''' na pindutan kan saimong kilyawan.",
        "anontalkpagetext": "----\n\n<em>''Ini iyo an pahina kan orolayan para an sarong dae bistadong paragamit na dae pa nakapagmukna nin panindog, o dae pa nakapaggamit kaini.</em>\nKaya kami kaipong gumamit nin numerikal na IP address sa pagbisto saiya.\nAn arog kaining IP address puwedeng maikapagheras sa nagkapirang mga paragamit.\nKun ika sarong dae pa bistadong paragamit asin mati mo na igwang irelebanteng sambit na pinanungod saimo, tabi paki [[Special:CreateAccount|mukna nin panindog]] or [[Special:UserLogin|maglaog ka]] tanganing malikayan an pagkaribong sa pag-iriba kan iba pang mga paragamit.''",
-       "noarticletext": "Mayo tabi sa presente nin teksto sa pahinang ini.\nIka puwedeng [[Special:Search/{{PAGENAME}}|maghanap para sa titulo kan pahinang ini]] sa iba pang mga pahina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa magkasurundong mga talaan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} liwaton ining pahina]</span>.",
+       "noarticletext": "Mayo tabi sa ngunyan nin teksto sa pahinang ini.\nPuwede kang [[Special:Search/{{PAGENAME}}|maghanap para sa titulo kan pahinang ini]] sa iba pang mga pahina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa magkasurundong mga talaan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} liwaton an páhinang ini]</span>.",
        "noarticletext-nopermission": "Mayong sa presente nin teksto an pahinang ini.\nIka mapuwedeng [[Special:Search/{{PAGENAME}}|hanapa para kaining titulo kan pahina]] sa iba pang mga pahina,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa magkasurundong mga talaan]</span>.",
        "missing-revision": "An rebisyon #$1 kan pahina pinagngaranan na \"{{FULLPAGENAME}}\" bakong eksistido.\n\nIni pirmihan na pinagkakausa sa paagi nin pagsusunod nin luwas na petsang historiya nin kasugpunan pasiring sa sarong pahinang pinagpura na.\nAn mga detalye matatagboan sa [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} pinagpura na talaan].",
        "userpage-userdoesnotexist": "Paragamit na panindog \"$1\" bako tabing rehistrado.\nPaki-tsek kun ika magustong magmukna/magliwat kaining pahina.",
        "currentrev-asof": "Pinakahuring pagpakarhay kan $1",
        "revisionasof": "Pagpakarhay poon kan $1",
        "revision-info": "Rebisyon poon kan {{GENDER:$6|$2}}$7",
-       "previousrevision": "← Dating pagpakarhay",
+       "previousrevision": "← Kadtong pagpakarhay",
        "nextrevision": "Bagong pagpakarhay →",
        "currentrevisionlink": "Sa ngunyan na rebisyon",
        "cur": "sa ngunyan",
        "diff-multi-otherusers": "({{PLURAL:$1|Sarong intermediate rebisyon|$1 intermediateng mga rebisyon}} kan {{PLURAL:$2|sarong pang paragamit|$2 mga paragamit}} an dae pigpapahiling)",
        "diff-multi-manyusers": "({{PLURAL:$1|Sarong intermediate na pagbabago|$1 mga intermediate na mga pagbabago}} na sobra sa $2 {{PLURAL:$2|paragamit|mga paragamit}} dae pinaghahayag)",
        "difference-missing-revision": "{{PLURAL:$2|sarong rebisyon|$2 mga rebisyon}} kaining diperensiya ($1) {{PLURAL:$2|na iyo an|kaidto na iyo an}} dae nanagboan.\n\nIni pirmihan na pinagkakausa sa paagi nin pagsusunod nin luwas sa petsang diff na kasugponan pasiring sa sarong pahina na pinagpura na.\nAn mga detalye mapuwedeng matatagboan sa [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} talaan kan pinagpuraan].",
-       "searchresults": "Resulta kan paghahánap",
+       "searchresults": "Mga resulta kan paghahánap",
        "search-filter-title-prefix-reset": "Maghanap sa gabos na pahina",
-       "searchresults-title": "Resulta kan paghahanap para sa \"$1\"",
+       "searchresults-title": "Mga resulta kan paghahanap para sa \"$1\"",
        "titlematches": "Angay an título kan artíkulo",
        "textmatches": "Angay an teksto nin páhina",
        "notextmatches": "Mayong ángay na teksto nin páhina",
        "next-page": "sunod na pahina →",
        "prevn-title": "Dati $1 {{PLURAL:$1|resulta|mga resulta}}",
        "nextn-title": "Sunod $1  {{PLURAL:$1|resulta|mga resulta}}",
-       "shown-title": "Ipahiling $1  {{PLURAL:$1|resulta|mga resulta}} sa kada pahina",
+       "shown-title": "Ipahiling an $1  {{PLURAL:$1|resulta|mga resulta}} sa kada pahina",
        "viewprevnext": "Tanawon ($1{{int:pipe-separator}}$2)($3)",
        "searchmenu-exists": "'''Igwa nin sarong pahina na pinagngaranan na \"[[:$1]]\" sa wiking ini.'''",
        "searchmenu-new": "'''Muknaon an pahina \"[[:$1]]\" sa wiking ini!''' {{PLURAL:$2|0=|Hilingon man an pahina na nadugangan sa saimong paghahanap.|Hilingon man an mga resulta kan paghahanap na nadugangan.}}",
        "searchprofile-advanced": "Adbansiyado",
        "searchprofile-articles-tooltip": "Hanapon sa $1",
        "searchprofile-images-tooltip": "Maghanap nin mga sagunson",
-       "searchprofile-everything-tooltip": "Maghanap nin gabos na laog (kabali an mga pahina nin olay)",
+       "searchprofile-everything-tooltip": "Maghanap nin gabos na laog (kabali an mga pahina nin urulayan)",
        "searchprofile-advanced-tooltip": "Maghanap nin pankustombreng espasyong-ngaran",
        "search-result-size": "$1 ({{PLURAL:$2|1 tatarámon|$2 mga tatarámon}})",
        "search-result-category-size": "{{PLURAL:$1|1 miyembro|$1 mga miyembro}} ({{PLURAL:$2|1 subkategorya|$2 mga subkategorya}}, {{PLURAL:$3|1 sagunson|$3 mga sagunson}})",
        "grant-uploadfile": "Magkarga nin bagong mga sagunson",
        "grant-viewdeleted": "Tanawon an pinagpurang mga sagunson asin pahina",
        "grant-viewmywatchlist": "Tanawon an saimong bantay-listahan",
-       "newuserlogpage": "Paragamit na talaan nin pagmukna",
+       "newuserlogpage": "Talaan nin pagmukna kan paragamit",
        "newuserlogpagetext": "Ini an talaan kan mga pagmukna nin paragamit.",
        "rightslog": "Usip nin derechos nin paragamit",
        "rightslogtext": "Ini an historial kan mga pagbabâgo sa mga derecho nin parágamit.",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|poon kaidtong huring bisita}}",
        "enhancedrc-history": "historiya",
        "recentchanges": "Dae pa sana nahahaloy na mga kaliwatan",
-       "recentchanges-legend": "Pinakahuring mga option kan mga pagbabago",
+       "recentchanges-legend": "Pinakahuring mga opsyon kan mga pagbabago",
        "recentchanges-summary": "Hanapon an mga pinahuring pagbabâgo sa wiki digdi sa páhinang ini.",
        "recentchanges-noresult": "Mayong mga kaliwatan sa laog kan itinaong peryodo na nagtutugmad kaining krayterya.",
        "recentchanges-feed-description": "Antabayon an pinakahuring dae pa sana nahaloy na mga kaliwatan sa wiki na yaon sa panhungit na ini.",
        "recentchanges-label-newpage": "Ining pagliwat nakapagmukna nin sarong baguhon na pahina",
-       "recentchanges-label-minor": "Ini saro sanang menor na pagliwat",
-       "recentchanges-label-bot": "Ining pagliwat pinaghimo bilang sarong bot",
-       "recentchanges-label-unpatrolled": "Ining pagliwat dae pa tabi pinagpatrolyahan",
-       "recentchanges-label-plusminus": "An kadakulaan nin pahina pinagliwat sa paagi kaining numero nin mga bayta",
+       "recentchanges-label-minor": "Saro sana ining sadit na pagliwat",
+       "recentchanges-label-bot": "Ining pagliwat pinaghimo kan sarong bot",
+       "recentchanges-label-unpatrolled": "Dae pa tabi pinagpatrolyahan an paglipat na ini",
+       "recentchanges-label-plusminus": "Pinagliwat an kadakulaan nin pahina sa paagi kan numero nin mga bayta kaini",
        "recentchanges-legend-heading": "<strong>Kabalaynan:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (hilngon man [[Special:NewPages|listahan kan mga baguhong pahina]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (hilngon man [[Special:NewPages|listahan kan mga baguhon na pahina]])",
        "recentchanges-legend-plusminus": "(''±saro-duwa-tolo'')",
        "recentchanges-submit": "Ipahiling",
        "rcfilters-tag-remove": "Halion '$1'",
        "minoreditletter": "s",
        "newpageletter": "B",
        "boteditletter": "b",
-       "rc-change-size-new": "$1 {{PLURAL:$1|byte|bytes}} pagtatapos kan pagbabago",
+       "rc-change-size-new": "$1 {{PLURAL:$1|byte|bytes}} pagkatapos kan pagbabago",
        "newsectionsummary": "/* $1 */ bàgong seksyon",
        "rc-enhanced-expand": "Ipahiling an mga detalye",
        "rc-enhanced-hide": "Itago an mga detalye",
        "filehist-revert": "balikon",
        "filehist-current": "sa ngunyan",
        "filehist-datetime": "Petsa/Oras",
-       "filehist-thumb": "Imaheng sadit",
-       "filehist-thumbtext": "Imaheng sadit para sa bersyon kan nakaaging $1",
+       "filehist-thumb": "Ladawang-sadit",
+       "filehist-thumbtext": "Ladawang-sadit para sa bersyon kan nakaaging $1",
        "filehist-nothumb": "Mayo nin imaheng sadit",
        "filehist-user": "Paragamít",
        "filehist-dimensions": "Mga dimensyón",
        "filehist-filesize": "Sokol nin file",
        "filehist-comment": "Komento",
-       "imagelinks": "Sagunsong naggagamit",
-       "linkstoimage": "An minasunod na {{PLURAL:$1|mga takod nin pahina|$1 mga pahinang nakatakod}} kaining sagunson:",
+       "imagelinks": "Nagagamit na sagunson",
+       "linkstoimage": "An minasunod na {{PLURAL:$1|mga piggamit na pahina|$1 piggamit na mga pahina}} kaining sagunson:",
        "linkstoimage-more": "Sobra sa $1 {{PLURAL:$1|mga takod nin pahina|$1 mga pahinang nakatakod}} kaining sagunson.\nAn minasunod na lista nagpapahiling kan {{PLURAL:$1|enot na pahinan|enot na $1 mga pahina}} na piggagamit kaining sagunson sana.\nSarong [[Special:WhatLinksHere/$2|bilog na lista]] an maantabayan.",
        "nolinkstoimage": "Dae nagkaigwa nin mga pahina na masugpon kaining sagunson.",
        "morelinkstoimage": "Hilngon an [[Special:WhatLinksHere/$1|kadagdagang mga takod]] kaining sagunson.",
        "duplicatesoffile": "An minasunod na {{PLURAL:$1|sagunson sarong duplikado|$1 mga sagunsong duplikado}} kaining sagunson ([[Special:FileDuplicateSearch/$2|kadagdagang mga detalye]]):",
        "sharedupload": "Ining sagunson naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto.",
        "sharedupload-desc-there": "Ining sagunson naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto.\nPakihiling tabi sa [$2 sagunsong deskripsyon kan pahina] para sa mga kadagdagang impormasyon.",
-       "sharedupload-desc-here": "Ining sagunson naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto.\nAn deskripsyon na yaon sa [$2 sagunsong deskripsyon kan pahina] ipinapahiling tabi sa ibaba.",
+       "sharedupload-desc-here": "An sagunson na ini naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto. Pinapahiling tabi sa ibaba an deskripsyon na yaon sa [$2 sagunsong deskripsyon kan pahina].",
        "sharedupload-desc-edit": "Ining sagunson naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto.\nMapuwede gayod na magusto kang liwaton an deskripsyon na yaon sa [$2 sagunsong deskripsyon kan pahina] kaini.",
        "sharedupload-desc-create": "Ining sagunson naggikan sa $1 asin mapuwedeng gamiton kan ibang mga proyekto.\nMapuwede gayod na ika magustong liwatong an deskripsyon na yaon sa [$2 sagunsong deskripsyon kan pahina] kaini.",
        "filepage-nofile": "Mayong sagunson sa arog kaining ngaran an yaon.",
        "sp-contributions-newonly": "Ipahiling lang an mga pag-liwat na pigmukna kan pahina",
        "sp-contributions-hideminor": "Itago an saradit na mga pagliwat",
        "sp-contributions-submit": "Hanápon",
-       "whatlinkshere": "Ano an mga makasugpon digde",
+       "whatlinkshere": "Ano an mga makasugpon digdi",
        "whatlinkshere-title": "Mga pahina na nakasugpon sa \"$1\"",
        "whatlinkshere-page": "Pahina:",
        "linkshere": "An mga minasunod na pahina isinusugpon sa '''$2''':",
        "tooltip-pt-preferences": "{{GENDER:|Saimong}} mga kamuyahan",
        "tooltip-pt-watchlist": "Sarong listahan kan mga pahina na saimong inaantabayanan para sa mga kaliwatan",
        "tooltip-pt-mycontris": "Sarong listahan kan {{GENDER:|saimong}} mga kontribusyon",
-       "tooltip-pt-login": "Ika inaagyat na maglaog; alagad, bako tabi ining piriritan",
+       "tooltip-pt-login": "Inaagyat ka na maglaog; alagad, bako tabi ining piriritan",
        "tooltip-pt-logout": "Magluwas",
        "tooltip-pt-createaccount": "Inaalok ika na maggibo nin account asin maglaog; alagad dai man ini kinakaipohan.",
-       "tooltip-ca-talk": "Orolayan dapit sa laog kan pahina",
-       "tooltip-ca-edit": "Liwata ining pahina",
+       "tooltip-ca-talk": "Urulayan dapit sa laog kan pahina",
+       "tooltip-ca-edit": "Liwaton an pahinang ini",
        "tooltip-ca-addsection": "Magpoon nin sarong baguhon na seksyon",
        "tooltip-ca-viewsource": "Ining pahina protektado.\nIka makakatanaw kan pinaggikanan",
-       "tooltip-ca-history": "Mga nakaaging rebisyon kaining pahina",
+       "tooltip-ca-history": "Mga nakaaging rebisyon kan pahinang ini",
        "tooltip-ca-protect": "Protektarán ining pahina",
        "tooltip-ca-unprotect": "Magribay nin proteksyon kaining pahina",
        "tooltip-ca-delete": "Puraon ining pahina",
        "tooltip-ca-undelete": "Bawîon an mga hirá na piggibo sa páhinang ini bâgo ini pigparâ",
        "tooltip-ca-move": "Balyuhon ining pahina",
-       "tooltip-ca-watch": "Idugang ining páhina sa saimong bantay-listahan",
+       "tooltip-ca-watch": "Idugang an páhinang ini sa saimong bantay-listahan",
        "tooltip-ca-unwatch": "Tangkason ining pahina gikan sa saikong bantay-listahan",
        "tooltip-search": "Hanápon an {{SITENAME}}",
-       "tooltip-search-go": "Magduman sa pahina na igwa kaining eksaktong pangaran kun eksistido",
+       "tooltip-search-go": "Magduman sa pahina na igwa kaining eksaktong pangaran kun eksistido ini",
        "tooltip-search-fulltext": "Hanápon an mga pahina para kaining teksto",
        "tooltip-p-logo": "Bisitahon an Panginot na Pahina",
-       "tooltip-n-mainpage": "Bisitahon an Pangenot na Pahina",
-       "tooltip-n-mainpage-description": "Bisitahon an Pangenot na Pahina",
-       "tooltip-n-portal": "Manunungod sa proyekto, ano an saimong maginibo, saen makanumpong nin mga bagay",
-       "tooltip-n-currentevents": "Hanapon an kalikudang impormasyon sa presenteng mga pangyayari",
+       "tooltip-n-mainpage": "Bisitahon an Panginot na Pahina",
+       "tooltip-n-mainpage-description": "Bisitahon an Panginot na Pahina",
+       "tooltip-n-portal": "Manunungod sa proyekto, ano an saimong magigibo, saen makanumpong nin mga bagay",
+       "tooltip-n-currentevents": "Hanapon an kalikudang impormasyon sa ngunyan na mga pangyayari",
        "tooltip-n-recentchanges": "Sarong listahan kan dae pa sana nahaloy na mga kaliwatan sa wiki",
        "tooltip-n-randompage": "Magkarga nin sarong purak na pahina",
        "tooltip-n-help": "An lugar tanganing makanumpong",
-       "tooltip-t-whatlinkshere": "Sarong listahan kan gabos na mga pahina nin wiki na nakasugpon digde",
-       "tooltip-t-recentchangeslinked": "Dae pa sana nahahaloy na mga kaliwatan sa mga pahina na nakasugpon gikan kaining pahina",
+       "tooltip-t-whatlinkshere": "Sarong listahan kan gabos na mga pahina nin wiki na nakasugpon digdi",
+       "tooltip-t-recentchangeslinked": "Dae pa sana nahahaloy na mga kaliwatan sa mga pahina na nakasugpon gikan sa pahinang ini",
        "tooltip-feed-rss": "Hungit na RSS sa pahinang ini",
        "tooltip-feed-atom": "Hungit Atomo para kaining pahina",
        "tooltip-t-contributions": "Sarong listahan kan mga paraambag kan {{GENDER:$1|paragamit na ini}}",
        "tooltip-t-emailuser": "Magpadara nin sarong e-koreo {{GENDER:$1|sa paragamit na ini}}",
        "tooltip-t-upload": "Ikarga an mga sagunson",
        "tooltip-t-specialpages": "Sarong listahan kan gabos na mga espesyal na pahina",
-       "tooltip-t-print": "Maimprentahong bersyon kaining pahina",
+       "tooltip-t-print": "Maimprentahong bersyon kan pahinang ini",
        "tooltip-t-permalink": "Permanenteng sugpon kaining rebisyon kan pahina",
        "tooltip-ca-nstab-main": "Tanawon an laog nin pahina",
        "tooltip-ca-nstab-user": "Hilingón an pahina nin paragamit",
        "tooltip-ca-nstab-media": "Hilingón an pahina kan ''media''",
-       "tooltip-ca-nstab-special": "Sarong espesyal na pahina ini, dae ini maliliwat",
+       "tooltip-ca-nstab-special": "Sarong espesyal na pahina ini, asin dae ini maliliwat",
        "tooltip-ca-nstab-project": "Tanawon an pahina kan proyekto",
-       "tooltip-ca-nstab-image": "Hilnga an pahina kan sagunson",
+       "tooltip-ca-nstab-image": "Hilingon an pahina nin sagunson",
        "tooltip-ca-nstab-mediawiki": "Hilingón an ''system message''",
        "tooltip-ca-nstab-template": "Tanawon an templato",
        "tooltip-ca-nstab-help": "Hilingón an pahina nin tabang",
-       "tooltip-ca-nstab-category": "Tanawon an pahina nin kategoriya",
+       "tooltip-ca-nstab-category": "Tanawon an pahina nin kategorya",
        "tooltip-minoredit": "Markahan ini bilang sarong dikiton na pagliwat",
        "tooltip-save": "Itagáma an saímong mga kaliwatan",
        "tooltip-publish": "I-publikar an mga pagbabago",
        "tooltip-watchlistedit-raw-submit": "Magdugang kan bantay-listahan",
        "tooltip-recreate": "Gibohon giraray an páhina maski na naparâ na ini",
        "tooltip-upload": "Pônan an pagkarga",
-       "tooltip-rollback": "\"Balikon\" an mga pinagbagong pagliliwat sa pahinang ini kan pinakahuring kontributor sa paagi nin sarong klik",
+       "tooltip-rollback": "\"Balikon\" an mga pinagbagong (mga) pagliliwat sa pahinang ini kan pinakahuring kontributor sa paagi nin sarong klik",
        "tooltip-undo": "\"Gibohang ibalik\" an mga pinagbagong pagliliwat asin bukasi an porma nin pagliliwat sa modong patanaw. Ini minatugot na magdadagdag nin rason sa sumaryo.",
        "tooltip-preferences-save": "Itagama an mga kagustuhan",
        "tooltip-summary": "Magkaag nin sarong halipot na sumaryo",
        "svg-long-desc-animated": "Animatadong SVG na sagunson, nangangaranang $1 x $2 piksel, kadakulaan nin sagunson: $3",
        "svg-long-error": "Imbalidong SVG na sagunson: $1",
        "show-big-image": "Orihinal na sagunson",
-       "show-big-image-preview": "Sukol kaining patanaw: $1.",
-       "show-big-image-other": "Ibang {{PLURAL:$2|resolusyon|mga resoluyon}}: $1.",
+       "show-big-image-preview": "Sukol kan patanaw na ini: $1.",
+       "show-big-image-other": "Iba pang {{PLURAL:$2|resolusyon|mga resoluyon}}: $1.",
        "show-big-image-size": "$1 × $2 piksel",
        "file-info-gif-looped": "pinag-otro",
        "file-info-gif-frames": "$1 {{PLURAL:$1|prema|mga prema}}",
        "metadata-help": "Ining sagunson may laog na kadagdagang impormasyon, puwedeng pinagdagdag gikan sa kamerang digital o tagakopyang ginamit sa pagmukna o pagpasadit kaini.\nKun an sagunson pinagbago gikan sa orihinal kaining estado, an ibang mga detalye mapuwedeng dae bilog na minapahiling kan pinagbagong sagunson.",
        "metadata-expand": "Ipahilíng an gabós na detalye",
        "metadata-collapse": "Itagò an gabós na detalye",
-       "metadata-fields": "Mga kinaagan kan imaheng metadata na nakalista sa mensaheng ipinagdadagdag sa pahina kan patanaw nin imahe kunsoaring na an lamesa kan metadata pinagpasadit.\nAn mga iba pagtatagoon sa paagi nin pirmehan.\n* gibo\n* modelo\n* petsaorasorihinal\n* kinaluwasangoras\n* fnumero\n* isobilismarka\n* pokalkalawigan\n* artista\n* copyright\n* imahedeskripsyon\n* gpspabalagbag\n* gpspalaba\n* gpspalangkaw",
+       "metadata-fields": "Mga kinaagan kan imaheng metadata na nakalista sa mensaheng ipinagdadagdag sa pahina kan patanaw nin imahe kunsuaring na an lamesa kan metadata pinagpasadit.\nAn mga iba pagtatagoon sa paagi nin pirmehan.\n* gibo\n* modelo\n* petsaorasorihinal\n* kinaluwasangoras\n* fnumero\n* isobilismarka\n* pokalkalawigan\n* artista\n* copyright\n* imahedeskripsyon\n* gpspabalagbag\n* gpspalaba\n* gpspalangkaw",
        "namespacesall": "gabós",
        "monthsall": "gabos",
        "confirmemail": "Kompirmaron an ''e''-surat",
        "version-libraries-authors": "Mga Kagsurat",
        "redirect": "Palikwaton sa paagi nin sagunson, paragamit, pahina o rebisyon o panlaog na ID",
        "redirect-summary": "Ining espesyal na pahina minalikwat pasiring sa sarong sagunson (ipinagtao an pangaran nin sagunson), sarong pahina (ipinagtao an sarong rebisyon nin ID o pahina nin ID), o sarong pahina nin paragamit (ipinagtao an numerikong ID nin paragamit). Pinagkagamitan: [[{{#Special:Redirect}}/sagunson/Example.jpg]],[[{{#Special:Redirect}}/pahina/64308]],  [[{{#Special:Redirect}}/rebisyon/328429]], [[{{#Special:Redirect}}/paragamit/101]] o [[{{#Special:Redirect}}/logid/186]].",
-       "redirect-submit": "Dumani",
+       "redirect-submit": "Dumanon",
        "redirect-lookup": "Hanapon mo",
        "redirect-value": "Halaga:",
        "redirect-user": "ID nin Paragamit",
index d250293..19585c8 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 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 1da9b96..ba84f4e 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Mostra els canvis a les pàgines que enllacin a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Pàgines que enllacen a</strong> la pàgina seleccionada",
        "rcfilters-target-page-placeholder": "Escriviu el nom d’una pàgina (o d’una categoria)",
+       "rcfilters-allcontents-label": "Tot el contingut",
        "rcnotefrom": "A sota hi ha {{PLURAL:$5|el canvi|els canvis}} a partir de <strong>$3, $4</strong> (fins a <strong>$1</strong>).",
        "rclistfromreset": "Reinicialitza la selecció de data",
        "rclistfrom": "Mostra els canvis nous des de $3, $2",
        "backend-fail-contenttype": "No es pot determinar el tipus de contingut del fitxer per emmagatzemar a «$1».",
        "backend-fail-batchsize": "El rerefons d'emmagatzemament ha rebut un lot {{PLURAL:$1|d'$1 operació|de $1 operacions}} de fitxer; el límit és $2 {{PLURAL:$2|operació|operacions}}.",
        "backend-fail-usable": "No s'ha pogut llegir ni escriure el fitxer \"$1\" a causa de permisos insuficients o perquè hi manquen directoris/contenidors.",
+       "backend-fail-stat": "No s’ha pogut llegir l’estat del fitxer «$1».",
+       "backend-fail-hash": "No s’ha pogut determinar el resum criptogràfic del fitxer «$1».",
        "filejournal-fail-dbconnect": "No es pot connectar amb la base de dades per emmagatzemar el backend \"$1\".",
        "filejournal-fail-dbquery": "No es pot actualitzar la base de dades per a emmagatzemar el backend \"$1\".",
        "lockmanager-notlocked": "No s'ha pogut desblocar «$1»; no és blocat.",
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 6a31fae..10ddb23 100644 (file)
        "confirmemail_body_changed": "Alguien, probablemente usted,\nha modificado la dirección de correo electrónico asociado a la cuenta \"$2\" hacia esta en {{SITENAME}}, desde la dirección IP $1.\n\nPara confirmar que esta cuenta realmente le pertenece y reactivar las funciones de correo electrónico en {{SITENAME}}, abra este enlace en su navegador:\n\n$3\n\nSi la cuenta *no* le pertenece, sigua el siguiente enlace para cancelar la confirmación:\n\n$5\n\nEste código de confirmación expirará el $4.",
        "deletedwhileediting": "'''Aviso''': ¡Esta página fue borrada después de que usted empezara a editar!",
        "confirmrecreate": "{{GENDER:$1|El usuario|La usuaria}} [[User:$1|$1]] ([[User talk:$1|discusión]]) borró esta página después de que usted comenzara a editarla, por este motivo:\n: <em>$2</em>\nPor favor confirme que realmente quiere volver a crearla.",
+       "confirm-purge-top": "¿Quiere vaciar la antememoria de esta página?",
        "watchlistedit-normal-explain": "Los títulos de su lista de seguimiento se muestran debajo.\nPara eliminar un título, marque la casilla junto a él, y haga clic en ''{{int:Watchlistedit-normal-submit}}''.\nTambién puede [[Special:EditWatchlist/raw|editar la lista de en crudo]].",
        "watchlistedit-raw-explain": "Los títulos de su lista de seguimiento se muestran debajo. Esta lista puede ser editada añadiendo o eliminando líneas de la lista;\nun título por línea.\nCuando acabe, haga clic en \"{{int:Watchlistedit-raw-submit}}\".\nTambién puede [[Special:EditWatchlist|usar el editor estándar]].",
        "watchlistedit-raw-done": "Su lista de seguimiento ha sido actualizada.",
index c2d561f..484a448 100644 (file)
        "nocreate-loggedin": "No tienes permiso para crear páginas nuevas.",
        "sectioneditnotsupported-title": "Edición de sección no admitida",
        "sectioneditnotsupported-text": "No se admite la edición de secciones en esta página.",
+       "modeleditnotsupported-title": "No se admite la edición",
+       "modeleditnotsupported-text": "No se admite la edición en el modelo de contenidos $1.",
        "permissionserrors": "Error de permisos",
        "permissionserrorstext": "No tienes permiso para hacer eso, por {{PLURAL:$1|el siguiente motivo|los siguientes motivos}}:",
        "permissionserrorstext-withaction": "No tienes permiso para $2, por {{PLURAL:$1|el siguiente motivo|los siguientes motivos}}:",
        "content-model-json": "JSON",
        "content-json-empty-object": "Objeto vacío",
        "content-json-empty-array": "Matriz vacía",
+       "unsupported-content-model": "<strong>Atención:</strong> en este wiki no se admite el modelo de contenidos $1.</strong>",
+       "unsupported-content-diff": "No se admiten las diferencias en el modelo de contenidos $1.",
+       "unsupported-content-diff2": "En este wiki no se admiten las diferencias entre los modelos de contenidos $1 y $2.",
        "deprecated-self-close-category": "Páginas que utilizan etiquetas HTML autocerradas no válidas",
        "deprecated-self-close-category-desc": "Esta página contiene etiquetas HTML autocerradas no válidas, tales como <code>&lt;b/></code> o <code>&lt;span/></code>. El comportamiento de estas cambiará pronto para ser coherente con la especificación de HTML5, por lo que su utilización en el wikitexto está obsoleta.",
        "duplicate-args-warning": "<strong>Aviso:</strong> [[:$1]] llama a [[:$2]] con más de un valor para el parámetro «$3». Se usará solo el último valor proporcionado.",
        "right-reupload-own": "Subir una nueva versión de un archivo propio",
        "right-reupload-shared": "Sobrescribir localmente archivos presentes en el repositorio multimedia compartido",
        "right-upload_by_url": "Subir un archivo a traves de un URL",
-       "right-purge": "Purgar la caché de una página en el servidor",
+       "right-purge": "Purgar la antememoria de una página en el servidor",
        "right-autoconfirmed": "No resultar afectado por los límites de frecuencia de edición para las IP",
        "right-bot": "Ser tratado como un proceso automático",
        "right-nominornewtalk": "No accionar el aviso de mensajes nuevos al realizar ediciones menores en páginas de discusión",
        "rcfilters-filter-showlinkedto-label": "Mostrar cambios en páginas que enlazan a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Páginas que enlazan hacia</strong> la página seleccionada",
        "rcfilters-target-page-placeholder": "Escribe un nombre de página (o de categoría)",
+       "rcfilters-allcontents-label": "Todo el contenido",
        "rcfilters-alldiscussions-label": "Todas las discusiones",
        "rcnotefrom": "Debajo {{PLURAL:$5|aparece el cambio|aparecen los cambios}} desde <strong>$3, $4</strong> (se muestran hasta <strong>$1</strong>).",
        "rclistfromreset": "Restablecer selección de fecha",
        "backend-fail-contenttype": "No se pudo determinar el tipo de contenido del archivo que se debe guardar en «$1».",
        "backend-fail-batchsize": "Se ha proporcionado al sistema de almacenamiento un lote de $1 {{PLURAL:$1|operación|operaciones}} de archivos; el límite es de $2 {{PLURAL:$2|operación|operaciones}}.",
        "backend-fail-usable": "No se pudo leer o escribir el archivo \"$1\" debido a permisos insuficientes o directorios/contenedores desaparecidos.",
+       "backend-fail-stat": "No se pudo leer el estado del archivo «$1».",
+       "backend-fail-hash": "No se pudo determinar el resumen criptográfico del archivo «$1».",
        "filejournal-fail-dbconnect": "No se pudo conectar con la base de datos del registro del sistema de almacenamiento «$1».",
        "filejournal-fail-dbquery": "No se pudo actualizar la base de datos del registro del sistema de almacenamiento \"$1\".",
        "lockmanager-notlocked": "No se pudo desbloquear \"$1\": no se encontraba bloqueado.",
        "sessionfailure": "Parece que hay un problema con tu sesión;\nse ha cancelado esta acción como medida de precaución contra el robo de sesiones.\nEnvía el formulario otra vez.",
        "changecontentmodel": "Cambiar el modelo de contenido de una página",
        "changecontentmodel-legend": "Cambiar el modelo de contenido",
-       "changecontentmodel-title-label": "Título de página",
+       "changecontentmodel-title-label": "Título de página:",
        "changecontentmodel-current-label": "Modelo de contenido actual:",
-       "changecontentmodel-model-label": "Modelo de contenido nuevo",
+       "changecontentmodel-model-label": "Modelo de contenido nuevo:",
        "changecontentmodel-reason-label": "Motivo:",
        "changecontentmodel-submit": "Cambiar",
        "changecontentmodel-success-title": "Se cambió el modelo de contenido",
        "delete_and_move_reason": "Borrada para permitir el traslado de \"[[$1]]\"",
        "selfmove": "El título es el mismo;\nno se puede trasladar una página sobre sí misma.",
        "immobile-source-namespace": "No se pueden trasladar páginas en el espacio de nombres «$1»",
+       "immobile-source-namespace-iw": "No es posible trasladar páginas de otros wikis desde este.",
        "immobile-target-namespace": "No se puede trasladar páginas al espacio de nombres «$1»",
        "immobile-target-namespace-iw": "Un enlace interwiki no es un destino válido para trasladar una página.",
        "immobile-source-page": "Esta página no se puede renombrar.",
        "immobile-target-page": "No se puede trasladar a ese título.",
+       "movepage-invalid-target-title": "El nombre solicitado no es válido.",
        "bad-target-model": "El destino deseado utiliza un modelo diferente de contenido. No se puede realizar la conversión de $1 a $2.",
        "imagenocrossnamespace": "No se puede trasladar el archivo a un espacio de nombres que no es para archivos.",
        "nonfile-cannot-move-to-file": "No es posible trasladar lo que no es un archivo al espacio de nombres de archivos.",
        "recreate": "Recrear",
        "confirm-purge-title": "Purgar esta página",
        "confirm_purge_button": "Aceptar",
-       "confirm-purge-top": "¿Quieres vaciar la caché de esta página?",
-       "confirm-purge-bottom": "Purgar una página limpia la caché y fuerza a que aparezca la versión más actual.",
+       "confirm-purge-top": "¿Quieres vaciar la antememoria de esta página?",
+       "confirm-purge-bottom": "Purgar una página limpia la antememoria y fuerza a que aparezca la versión más actual.",
        "confirm-watch-button": "Aceptar",
        "confirm-watch-top": "¿Añadir esta página a tu lista de seguimiento?",
        "confirm-unwatch-button": "Aceptar",
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..b8a11cf 100644 (file)
        "exif-scenetype-1": "D'Bild gouf fotograféiert",
        "exif-customrendered-0": "Standard",
        "exif-customrendered-1": "Benotzerdefinéiert",
+       "exif-customrendered-2": "HDR (keen Original gespäichert)",
+       "exif-customrendered-4": "Original (fir HDR)",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-8": "Portrait",
        "exif-exposuremode-0": "Automatesch Beliichtung",
        "exif-exposuremode-1": "Manuell Beliichtung",
        "exif-exposuremode-2": "Beliichtungsserie",
index 1dd8f54..d8c26e5 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 aa5e61f..4f2310a 100644 (file)
        "exif-scenetype-1": "Direktno fotografisana slika",
        "exif-customrendered-0": "Normalni proces",
        "exif-customrendered-1": "Podešeni proces",
+       "exif-customrendered-2": "HDR (bez spremlenog originala)",
+       "exif-customrendered-3": "HDR (sa spremlenim originalom)",
+       "exif-customrendered-4": "Original (za HDR)",
        "exif-exposuremode-0": "Automatska ekpozicija",
        "exif-exposuremode-1": "Ručna ekspozicija",
        "exif-exposuremode-2": "Automatski određen raspon",
index 1d650e2..cf69812 100644 (file)
        "exif-scenetype-1": "Direkt fotograferad bild",
        "exif-customrendered-0": "Normal",
        "exif-customrendered-1": "Anpassad",
+       "exif-customrendered-2": "HDR (inget original sparades)",
+       "exif-customrendered-3": "HDR (original sparades)",
+       "exif-customrendered-4": "Original (för HDR)",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-7": "Porträtt-HDR",
+       "exif-customrendered-8": "Porträtt",
        "exif-exposuremode-0": "Automatisk exponering",
        "exif-exposuremode-1": "Manuell exponering",
        "exif-exposuremode-2": "Automatisk alternativexponering",
index 73eb361..a6fa995 100644 (file)
@@ -10,7 +10,8 @@
                        "Ата",
                        "Максим Підліснюк",
                        "Тест",
-                       "Piramidion"
+                       "Piramidion",
+                       "Movses"
                ]
        },
        "exif-imagewidth": "Ширина",
        "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 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..18c0da7 100644 (file)
        "undo-nochange": "به نظر می‌رسد ویرایش از پیش خنثی‌سازی شده است.",
        "undo-summary": "خنثی‌سازی ویرایش $1 از [[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]])",
        "undo-summary-username-hidden": "خنثی‌سازی نسخهٔ $1 به دست یک کاربر پنهان‌شده",
-       "cantcreateaccount-text": "امكان ساختن حساب کاربری از این این نشانی آی‌پی ('''$1''') توسط [[User:$3|$3]] سلب شده است.\n\nدلیل ارائه شده توسط $3 چنین است: $2",
+       "cantcreateaccount-text": "امکان ساختن حساب کاربری از این این نشانی آی‌پی ('''$1''') توسط [[User:$3|$3]] سلب شده است.\n\nدلیل ارائه شده توسط $3 چنین است: $2",
        "cantcreateaccount-range-text": "ایجاد حساب از آدرس آی‌پی در مجموعه‌ی <strong>$1</strong>، که شامل آدرس آی‌پی شما (<strong>$4</strong>) است، توسط [[User:$3|$3]] متوقف شده‌است.\nدلیل ارائه شده توسط $3، $2 است.",
        "viewpagelogs": "نمایش سیاهه‌های این صفحه",
        "nohistory": "این صفحه تاریخچهٔ ویرایش ندارد.",
        "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 bc32b70..33cddd8 100644 (file)
        "userinvalidconfigtitle": "<strong>Varoitus:</strong> Tyyliä nimeltä ”$1” ei ole olemassa. Muista, että käyttäjän määrittelemät .css-, -.json- ja .js-sivut alkavat pienellä alkukirjaimella, esim. {{ns:user}}:Matti Meikäläinen/vector.css eikä {{ns:user}}:Matti Meikäläinen/Vector.css.",
        "updated": "(Päivitetty)",
        "note": "'''Huomautus:'''",
-       "previewnote": "'''Tämä on vasta sivun esikatselu.'''\nTekemiäsi muutoksia ei ole vielä tallennettu.",
-       "continue-editing": "Siirry muokkauskenttään",
+       "previewnote": "<strong>Tämä on vasta sivun esikatselu.</strong>\nTekemiäsi muutoksia ei ole vielä tallennettu.",
+       "continue-editing": "Siiry mookkauskenttään",
        "previewconflict": "Tämä esikatselu näyttää miltä muokkausalueella oleva teksti näyttää tallennettuna.",
        "session_fail_preview": "Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.\n\nSaatat olla kirjautunut ulos. '''Varmista, että olet edelleen kirjautunut sisään ja yritä uudelleen'''. Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään, ja varmista, että selaimesi sallii evästeet tältä sivustolta.",
        "session_fail_preview_html": "Valitettavasti muokkaustasi ei voitu käsitellä istunnon tietojen katoamisen vuoksi.\n\n<em>Koska {{GRAMMAR:inessive|{{SITENAME}}}} on käytössä suodattamaton HTML-koodi, esikatselu on piilotettu JavaScript-hyökkäyksien torjumiseksi</em>\n\n<strong>Jos tämä on oikea muokkausyritys, yritä uudelleen.</strong> Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään. Tarkista myös, että selaimesi sallii evästeet tältä sivustolta.",
        "sessionfailure": "Näyttää siltä, että tämänhetkisessä istunnossasi on jokin ongelma; \ntämä toiminto on peruutettu varotoimena istunnon kaappaamisen estämiseksi.\nLähetä lomake uudelleen.",
        "changecontentmodel": "Muuta sivun sisältömallia",
        "changecontentmodel-legend": "Muuta sisältömallia",
-       "changecontentmodel-title-label": "Sivun otsikko",
-       "changecontentmodel-model-label": "Uusi sisältömalli",
+       "changecontentmodel-title-label": "Sivun otsikko:",
+       "changecontentmodel-model-label": "Uusi sisältömalli:",
        "changecontentmodel-reason-label": "Syy",
        "changecontentmodel-submit": "Tee muutos",
        "changecontentmodel-success-title": "Sisältömallia on muutettu",
index c3542c0..71e9e88 100644 (file)
@@ -3,7 +3,8 @@
                "authors": [
                        "Kaganer",
                        "Mestos",
-                       "아라"
+                       "아라",
+                       "Pyscowicz"
                ]
        },
        "tog-underline": "Linkitten alleviivaus",
        "tog-extendwatchlist": "Laajena valvontalistaa näyttämhään kaikki tehtyt muutokset eikä vain viimisimät.",
        "tog-usenewrc": "Käytä avanseerattu verekset muutokset (vaatii JavaScript)",
        "tog-numberheadings": "Nymreeraa rypriikit",
-       "tog-showtoolbar": "Näytä työneuvopalkki (JavaScript)",
-       "tog-editondblclick": "Mookkaa sivuja kaksoisknapituksella (JavaScript)",
-       "tog-editsectiononrightclick": "Aktiveeraa seksuuni mookkaus oikeapuolen klikkauksella seksuuni tittelhiin (JavaScript)",
-       "tog-watchcreations": "Lissää sivut mitä luon valvontasivule",
-       "tog-watchdefault": "Lissää sivut mitä mie mookkaan valvontasivule",
-       "tog-watchmoves": "Lissää sivut mitä mie siirän minun valvontasivule",
-       "tog-watchdeletion": "Lissää sivut mitä otan poies valvontasivule",
+       "tog-editondblclick": "Mookkaa sivuja kaksoisknapituksella",
+       "tog-editsectiononrightclick": "Aktiveeraa seksuuni mookkaus oikeapuolen klikkauksella seksuuni tittelhiin",
+       "tog-watchcreations": "Lissää sivut mitä luon ja fiilit mitä ylöslattaan valvontasivule",
+       "tog-watchdefault": "Lissää sivut ja fiilut mitä mie mookkaan valvontasivule",
+       "tog-watchmoves": "Lissää sivut ja fiilit mitä mie siirän minun valvontasivule",
+       "tog-watchdeletion": "Lissää sivut ja fiilit mitä otan poies valvontasivule",
        "tog-minordefault": "Markeeraa auttomaattisesti kaikki muutokset pieneks",
        "tog-previewontop": "Näytä esitarkastelu mookkauspaikan yläpuolela",
        "tog-previewonfirst": "Näytä esitarkastelu kun mookkaus alethaan",
-       "tog-enotifwatchlistpages": "Lähätä e-postipreivi mulle kun sivu minun valvontalistala on muutettu",
-       "tog-enotifusertalkpages": "Lähätä sähköposti, kun käyttäjäsivun keskustelusivu muuttuu",
+       "tog-enotifwatchlistpages": "Lähätä e-postipreivi mulle kun sivu tai fiili minun valvontalistala on muutettu",
+       "tog-enotifusertalkpages": "Lähätä E-posti, kun käyttäjäsivun keskustelusivu muuttuu",
        "tog-enotifminoredits": "Lähätä epostieto pienistäki muutoksista",
-       "tog-enotifrevealaddr": "Näytä minun eposti atressin muile lähetetyissä ilmoituksissa",
+       "tog-enotifrevealaddr": "Näytä minun e-posti atressin muile lähetetyissä ilmoituksissa",
        "tog-shownumberswatching": "Näytä kuinka moni käyttäjä valvoo sivua",
-       "tog-oldsig": "Nykynen allekirjotus",
+       "tog-oldsig": "Sinun nykynen allekirjotus:",
        "tog-fancysig": "Mookkaamaton allekirjotus ilman auttomaattista linkkiä",
        "sunday": "pyhä",
        "monday": "maanantai",
        "thu": "tuo",
        "fri": "pe",
        "sat": "la",
-       "january": "tammikuu",
-       "february": "helmikuu",
+       "january": "janyaari",
+       "february": "febryaari",
        "march": "maaliskuu",
        "april": "huhtikuu",
        "may_long": "toukokuu",
        "june": "kesäkuu",
        "july": "heinäkuu",
        "august": "elokuu",
-       "september": "syyskuu",
+       "september": "septempäri",
        "october": "lokakuu",
        "november": "marraskuu",
-       "december": "joulukuu",
-       "january-gen": "tammikuun",
-       "february-gen": "helmikuun",
+       "december": "tesämperi",
+       "january-gen": "janyaarin",
+       "february-gen": "febryaarin",
        "march-gen": "maaliskuun",
        "april-gen": "huhtikuun",
        "may-gen": "toukokuun",
        "june-gen": "kesäkuun",
        "july-gen": "heinäkuun",
        "august-gen": "elokuun",
-       "september-gen": "syyskuun",
+       "september-gen": "septempäri",
        "october-gen": "lokakuun",
        "november-gen": "marraskuun",
-       "december-gen": "joulukuun",
+       "december-gen": "tesämperin",
        "jan": "tammikuu",
        "feb": "helmikuu",
        "mar": "maaliskuu",
@@ -76,7 +76,7 @@
        "jun": "kesäkuu",
        "jul": "heinäkuu",
        "aug": "elokuu",
-       "sep": "syyskuu",
+       "sep": "septempäri",
        "oct": "lokakuu",
        "nov": "marraskuu",
        "dec": "joulukuu",
        "category_header": "Sivut, jokka on katekuurissa \"$1\"",
        "subcategories": "Alakatekuurit",
        "category-media-header": "Katekuurin ”$1” sisältämät fiilit",
-       "category-empty": "''Tässä katekuuriassa ei ole sivuja eikä fiiliä.''",
+       "category-empty": "<em>Tässä katekuuriassa ei ole sivuja eikä fiiliä.</em>",
        "hidden-categories": "{{PLURAL:$1|Piilotettu katekuuri|Piilotetut katekuurit}}",
+       "hidden-category-category": "Piilotetut katekuurit",
        "category-subcat-count": "{{PLURAL:$2|Tässä katekuurissa on vain seuraava alakatekuuri.|{{PLURAL:$1|Seuraava alakatekuuri kuuluu|Seuraavat $1 alakatekuuria kuuluvat}} tähhään katekuurihaan. Alakatekuuritten kokonaismäärä katekuurissa on $2.}}",
        "category-article-count": "{{PLURAL:$2|Tässä katekuurissa on vain seuraava sivu.|Seuraava {{PLURAL:$1|sivu on|$1 sivut on}} tässä katekuurissa, kahen joukosta $2 }}",
-       "category-file-count": "{{PLURAL:$2|Tässä katekuurissa on vain seuraava sivu.|Seuraava {{PLURAL:$1|fiili|$1 fiilit}} (kaikkians $2) on tässä katekuurissa.}}",
+       "category-file-count": "{{PLURAL:$2|Tässä katekuurissa on vain seuraava fiili.|Seuraava {{PLURAL:$1|fiili|$1 fiilit}} (kaikkians $2) on tässä katekuurissa.}}",
        "listingcontinuesabbrev": "jatkuu",
        "noindex-category": "Ei-indekseerattuja sivuja",
        "about": "Tietoja",
        "newwindow": "(aukasee uuessa klasissa)",
        "cancel": "Lopeta",
-       "mytalk": "Minun keskustelu",
+       "mypage": "Sivu",
+       "mytalk": "Keskustelu",
+       "anontalk": "Keskustelu",
        "navigation": "Navikeerinki",
-       "qbedit": "Mookkaa",
-       "qbpageoptions": "Tämä sivu",
-       "qbmyoptions": "Minun inställninkit",
        "faq": "Useasti kysytyt kysymykset",
-       "faqpage": "Project:Useasti kysytyt kysymykset",
        "actions": "Toiminat",
        "namespaces": "Nimityhjyyet",
        "variants": "Varianttia",
        "searcharticle": "Mene",
        "history": "Sivun histuuria",
        "history_short": "Histuuria",
+       "history_small": "histuuria",
        "printableversion": "Printtausmaholinen versuuni",
        "permalink": "Ikunen linkki",
        "edit": "Mookkaa",
        "protect_change": "muuta",
        "newpage": "Uusi sivu",
        "talkpagelinktext": "Keskustelu",
+       "specialpage": "Spesiaali sivu",
        "personaltools": "Henkilökohtaiset työneuvot",
        "talk": "Keskustelu",
        "views": "Näyttöjä",
        "toolbox": "Työneuvot",
+       "tool-link-userrights": "Mookkaa {{GENDER:$1|käyttäjän}} ryhmiä",
+       "imagepage": "Näytä fiilisivu",
+       "categorypage": "Näytä katekuurisivu",
        "otherlanguages": "Muila kielilä",
        "redirectedfrom": "(Ohjattu sivulta $1)",
-       "lastmodifiedat": "Sivua on viimeksi muutettu $1 kello $2.",
+       "redirectpagesub": "Ohjaussivu",
+       "lastmodifiedat": "Sivua on viimeksi mookattu $1 kello $2.",
        "jumpto": "Hyppää:",
        "jumptonavigation": "Navikeerinki",
        "jumptosearch": "Hae",
        "disclaimers": "Vastuuvaphaus",
        "disclaimerpage": "Project: Ylheinen varoitus",
        "edithelp": "Mookkausapua",
+       "helppage-top-gethelp": "Apua",
        "mainpage": "Alkusivu",
        "mainpage-description": "Alkusivu",
        "portal": "Kaikitten purthaali",
        "privacy": "Tietosuojakäytäntö",
        "privacypage": "Project: Intekriteettisääntö",
        "retrievedfrom": "Nouettu osoitheesta $1",
-       "youhavenewmessages": "Sulla on $1 ($2).",
+       "youhavenewmessages": "{{PLURAL:$3|Sulla on}} $1 ($2).",
        "editsection": "mookkaa",
        "editold": "mookkaa",
        "viewsourceold": "näytä lähekooti",
        "nstab-image": "Fiili",
        "nstab-template": "Malli",
        "nstab-category": "Katekuuri",
+       "mainpage-nstab": "Alkusivu",
        "missing-article": "Sivun sisältöä ei löytyny taattapaasista: $1 $2.\n\nUseimiten tämä johtuu vanhentuneesta vertailu- tai histuuriasivulinkistä poistethuun sivhuun.\n\nJos kysheessä ei ole poistettu sivu, olet piian löytäny virheen ohjelmassa.\nIlmota tämän sivun atressi wikin [[Special:ListUsers/sysop|atministratöörile]].",
        "missingarticle-rev": "(versuuni: $1)",
        "badtitle": "Virheelinen titteli",
        "badtitletext": "Pyytämästi sivurypriikki oli virheelinen, tyhjä eli titteli on väärin linkitetty muusta wikistä. Se saattaa sisältää yhen eli monta sympoolia, joita ei saa käyttää sivutittelissä.",
        "viewsource": "Näytä lähekooti",
+       "welcomeuser": "Tervetuloa, $1!",
        "yourname": "Käyttäjänimi",
+       "userlogin-yourname": "Käyttäjänimi",
+       "userlogin-yourname-ph": "Kirjota sinun käyttäjänimi",
        "yourpassword": "Salasana",
+       "userlogin-yourpassword": "Salasana",
        "yourpasswordagain": "Salasana uuesti",
-       "remembermypassword": "Muista minun lokkauksen tässä taattorissa (korkeinthaans $1 {{PLURAL:$1|päivä|päivää}})",
        "login": "Lokkaa sisäle",
        "nav-login-createaccount": "Lokkaa sisäle / luo konttu",
-       "userlogin": "Lokkaa sisäle/ luo konttu",
+       "logout": "Lokkaa ulos",
        "userlogout": "Lokkaa ulos",
-       "nologin": "Eikos sulla ole käyttäjäkonttua, '''$1'''.",
-       "nologinlink": "Luo käyttäjäkonttu",
+       "userlogin-joinproject": "Liity {{SITENAME}}",
        "createaccount": "Luo käyttäjäkonttu",
-       "gotaccount": "Jos sulla on käyttäjäkonttu,  voit '''$1'''.",
-       "gotaccountlink": "Lokkaa sisäle",
-       "userlogin-resetlink": "Unhoutitko sinun salasanan?",
-       "mailmypassword": "Lähätä e-postissa uusi salasana",
+       "createacct-emailrequired": "E-postin atressi",
+       "createacct-another-submit": "Luo konttu",
+       "createacct-benefit-body1": "{{PLURAL:$1|mookkaus|mookhausta}}",
+       "mailmypassword": "Uusi salasana",
        "loginlanguagelabel": "Kieli: $1",
+       "pt-login": "Lokkaa sisäle",
+       "pt-login-button": "Lokkaa sisäle",
+       "pt-createaccount": "Luo konttu",
+       "pt-userlogout": "Lokkaa ulos",
+       "botpasswords-label-cancel": "Lopeta",
+       "botpasswords-label-delete": "Ota poies",
+       "resetpass-submit-cancel": "Lopeta",
+       "passwordreset-email": "E-postin atressi:",
+       "changeemail-newemail": "Uusi E-postin atressi:",
        "bold_sample": "Lihava teksti",
        "bold_tip": "Lihava teksti",
        "italic_sample": "Kyrsiveerattu teksti",
        "minoredit": "Tämä on pieni muutos",
        "watchthis": "Valvo tätä sivua",
        "savearticle": "Säästä sivu",
-       "preview": "Etukätheen katto",
-       "showpreview": "Näytä esikuvvaus",
+       "savechanges": "Säästä muutokset",
+       "savearticle-start": "Säästä sivu...",
+       "savechanges-start": "Säästä muutokset...",
+       "preview": "Esitarkastelu",
+       "showpreview": "Näytä esitarkastelu",
        "showdiff": "Näytä muutokset",
        "anoneditwarning": "'''Varotus:''' Et ole lokanu sisäle.\nIP-atressi säästethään tämän sivun muutoshistuuriassa.",
+       "loginreqlink": "lokkaa sisäle",
        "newarticle": "(Uusi)",
-       "newarticletext": "Linkki vei sinun sivule, joka ei vielä ole.\nSaatat luoa sivun kirjottamalla alla olehvaan kenthään (katto [$1 apusivu] lisää tietoja).\nJos et halua luoa sivua, käytä browserin \"takashiin\" knappia.",
-       "noarticletext": "Tällä hetkellä tällä sivulla ei ole tekstiä.\nTällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheesheen liittyviä lokkia]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} mookata tätä sivua]</span>.",
+       "newarticletext": "Linkki vei sinun sivule, joka ei vielä ole.\nSaatat luoa sivun kirjottamalla alla olehvaan kenthään (katto [$1 apusivu] lisää tietoja).\nJos et halua luoa sivua, käytä browserin <strong>takashii</strong> knappia.",
+       "noarticletext": "Tällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheesheen liittyviä lokkia]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} luoa tämä sivu]</span>.",
        "noarticletext-nopermission": "Tällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\neli <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea relevantista lokista]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} mookata tätä sivua]</span>.",
        "previewnote": "'''Tämä on vasta sivun etukattelu. Sivua ei ole vielä säästetty!'''",
        "editing": "Mookathaan sivua $1",
        "rev-delundel": "näytä/piilota",
        "revdel-restore": "muuta näkyvyyttä",
        "revertmerge": "Pane takashiin yhistäminen",
-       "history-title": "Sivun $1 muutoshistuuria",
+       "history-title": "Sivun \"$1\" muutoshistuuria",
        "lineno": "Rivi $1:",
        "compareselectedversions": "Vertaile valittuja sivu versuunia",
        "editundo": "kumota",
        "searchprofile-advanced-tooltip": "Hae tietyissä nimityhjyissä",
        "search-result-size": "$1 ({{PLURAL:$2|1 sana|$2 sannaa}})",
        "search-result-category-size": "{{PLURAL:$1|1 jäsen|$1 jäsentä}} ({{PLURAL:$2|1 alakatekuuria|$2 alakatekuuriaa}}, {{PLURAL:$3|1 fiili|$3 fiiliä}})",
-       "search-redirect": "(ohjaus $1)",
+       "search-redirect": "(ohjaus sivulta $1)",
        "search-section": "(seksuuni $1)",
+       "search-category": "(katekuuri $1)",
        "search-suggest": "Tarkoititko: $1",
        "searchrelated": "relateerattu",
        "searchall": "kaikki",
        "search-nonefound": "Ei yhtään resyltaattia sinun kysymyksheen",
-       "mypreferences": "Omat inställninkit",
+       "preferences": "Inställninkit",
+       "mypreferences": "Inställninkit",
+       "prefs-watchlist": "Valvontalista",
+       "saveprefs": "Säästä",
+       "prefs-editing": "Mookkaus",
+       "timezoneregion-america": "Ameriika",
+       "timezoneregion-asia": "Aasia",
+       "timezoneregion-australia": "Austraalia",
+       "timezoneregion-europe": "Euruuppa",
+       "prefs-namespaces": "Nimityhjyyet",
+       "prefs-files": "Fiilit",
        "youremail": "E-posti:",
        "yourrealname": "Oikea nimi",
+       "yournick": "Uusi allekirjotus:",
+       "gender-male": "Äijä",
+       "gender-female": "Nakku",
+       "email": "E-posti",
        "prefs-help-email": "E-postin atressi on vapa, mutta tekkee maholiseks ette lähättää sulle salasanan meilissä, jos unhoutat sen.",
-       "prefs-help-email-others": "Saatat kans antaa muitten käyttäjitten ottaa ottaa yhteyttä sinhuun sähköpostila. Sin atressi ei näy toisen käyttäjän ottaessa sinhuun yhteyttä.",
+       "prefs-help-email-others": "Saatat kans antaa muitten käyttäjitten ottaa yhteyttä sinhuun sähköpostila. Sin atressi ei näy toisen käyttäjän ottaessa sinhuun yhteyttä.",
+       "prefs-signature": "Allekirjotus",
+       "right-move-categorypages": "Siirä katekuurisivuja",
+       "right-movefile": "Siirä fiilit",
+       "right-upload": "Lattaa ylös fiiliä",
        "newuserlogpage": "Uuitten käyttäjitten loki",
        "action-edit": "mookkaa tätä sivua",
+       "action-createaccount": "luoa tätä käyttäjäkontthua",
+       "action-move-categorypages": "siirä katekuurisivuja",
        "nchanges": "$1 {{PLURAL:$1|muutos|muutosta}}",
+       "enhancedrc-history": "histuuria",
        "recentchanges": "Verekset muutokset",
        "recentchanges-legend": "Vereksitten muutoksitten inställninkit",
        "recentchanges-summary": "Seuraa viimiset muutokset wikin tällä sivula",
        "recentchanges-label-minor": "Tämä on pieni muutos",
        "recentchanges-label-bot": "Tämän muutoksen teki botti",
        "recentchanges-label-unpatrolled": "Tätä muutosta ei ole vielä tarkistettu",
-       "rcnotefrom": "Alla on muutokset '''$2'''lähtien. (korkeinthaans '''$1''' näytethään).",
+       "rcfilters-savedqueries-remove": "Ota poies",
+       "rcfilters-savedqueries-cancel-label": "Lopeta",
+       "rcfilters-filter-categorization-label": "Katekuurimuutokset",
+       "rcfilters-target-page-placeholder": "Kirjota sivun nimi (tai katekuuri)",
+       "rcnotefrom": "Alla on {{PLURAL:$5|muutos|muutokset}} <strong>$3, $4</strong> lähtien. (korkeinthaans <strong>$1</strong> näytethään).",
        "rclistfrom": "Näytä uuet muutokset jälkhiin $3 $2",
        "rcshowhideminor": "$1 pienet muutokset",
        "rcshowhidebots": "$1 ropootit",
-       "rcshowhideliu": "\n$1 sisäle lokaattuja käyttäjiä",
+       "rcshowhideliu": "$1 rekisteröityneihtä käyttäjiä",
        "rcshowhideanons": "$1 anonyymit käyttäjät",
        "rcshowhidepatr": "$1 tarkistetut muutokset",
        "rcshowhidemine": "$1 omat muutokset",
        "minoreditletter": "p",
        "newpageletter": "U",
        "boteditletter": "b",
-       "rc-enhanced-expand": "Näytä detaljit (JavaScript)",
+       "rc-enhanced-expand": "Näytä detaljit",
        "rc-enhanced-hide": "Piilota detaljit",
        "recentchangeslinked": "Relateerattuja muutoksia",
        "recentchangeslinked-toolbox": "Relateerattuja muutoksia",
        "recentchangeslinked-page": "Sivun nimi",
        "recentchangeslinked-to": "Näytä muutokset sivhuin, jolla sen eestä on linkki annethuun sivhuun",
        "upload": "Lattaa ylös fiili",
+       "uploadbtn": "Lattaa ylös fiili",
        "uploadlogpage": "Ylöslattauksen loki",
+       "filename": "Fiilinimi",
        "filedesc": "Yhteenveto",
+       "filereuploadsummary": "Fiilimuutokset:",
+       "savefile": "Säästä fiili",
+       "upload-dialog-title": "Lattaa ylös fiili",
+       "upload-dialog-button-cancel": "Lopeta",
+       "upload-dialog-button-save": "Säästä",
+       "upload-dialog-button-upload": "Lattaa ylös",
+       "upload-form-label-usage-filename": "Fiilinimi",
+       "upload-form-label-infoform-categories": "Katekuurit",
        "license": "Lisensi",
        "license-header": "Lisensi",
+       "listfiles-delete": "ota poies",
+       "imgfile": "fiili",
+       "listfiles": "Fiililista",
        "file-anchor-link": "Fiili",
        "filehist": "Fiilin histuuria",
        "filehist-help": "Klikkaa taattymia/aikaa niin näet fiilin kuinka se oli siihen aikhaan",
+       "filehist-deleteall": "ota poies kaikki",
+       "filehist-deleteone": "ota poies",
        "filehist-revert": "pane takashiin",
        "filehist-current": "nykynen",
        "filehist-datetime": "Päivä/Aika",
        "filehist-dimensions": "Timensuunit",
        "filehist-comment": "Komentti",
        "imagelinks": "Fiilin käyttö",
-       "linkstoimage": "Seuraava {{PLURAL:$1|sivu |$1 sivut }} länkkaavat tähhään fiilhiin:",
-       "nolinkstoimage": "Ei ole yhtään sivua joka linkkaa tähhään fiilhiin.",
+       "linkstoimage": "{{PLURAL:$1|Seuraava sivu|Seuraavat $1 sivua}} käytthävät tätä fiilhiä:",
+       "nolinkstoimage": "Ei ole yhtään sivua joka käyttää tätä fiilhiä.",
        "sharedupload-desc-here": "Tämä fiili on jaettu kohtheesta $1 ja muut prujektit saattavat käyttää sitä.\nTiot [$2 fiilin kuvvaussivulta] näkyvät tässä alla.",
+       "filedelete": "Ota poies $1",
+       "filedelete-legend": "Ota poies fiili",
+       "filedelete-submit": "Ota poies",
        "randompage": "Satunhainen sivu",
+       "randomincategory-category": "Katekuuri:",
        "statistics": "Statistiikkaa",
+       "brokenredirects-delete": "ota poies",
        "nbytes": "$1 {{PLURAL:$1|tavu|tavua}}",
+       "ncategories": "$1 {{PLURAL:$1|katekuuri|katekuurit}}",
        "nmembers": "$1 {{PLURAL:$1|jäsen|jäsentä}}",
        "prefixindex": "Kaikki sivut prefiksilä",
-       "usercreated": "Luottu $1 $2",
+       "listusers": "Käyttäjälista",
+       "usercreated": "{{GENDER:$3|Luottu}} $1 kello $2",
        "newpages": "Uuet sivut",
        "move": "Siirä",
        "pager-newer-n": "← {{PLURAL:$1|1 uuempi|$1 uuempaa}}",
        "pager-older-n": "{{PLURAL:$1|1 vanheempi|$1 vanheempaa}} →",
+       "apisandbox-add-multi": "Lissää",
        "booksources": "Kirjalähteet",
        "booksources-search-legend": "Hae kirjalähtheitä",
+       "booksources-search": "Haku",
        "log": "Lokit",
        "allpages": "Kaikki sivut",
        "allarticles": "Kaikki sivut",
        "allpagessubmit": "Mene",
        "categories": "Katekuurit",
+       "linksearch-ns": "Nimityhjyys:",
        "linksearch-line": "$1 on linkattu sivulta $2",
        "listgrouprights-members": "(jäsenlista)",
+       "listgrouprights-namespaceprotection-namespace": "Nimityhjyys",
        "emailuser": "Lähätä e-posti tälle käyttäjälle",
        "watchlist": "Valvontalista",
-       "mywatchlist": "Minun valvontasivu",
+       "mywatchlist": "Valvontalista",
        "watchlistfor2": "Käyttäjälle $1 $2",
+       "addwatch": "Lissää valvontalistale",
        "watch": "Valvo",
        "unwatch": "Lopeta valvonta",
-       "watchlist-details": "Valvontalistala on {{PLURAL:$1|$1 sivu|$1 sivua}} (keskustelusivuja mukhaan laskematta)",
-       "wlshowlast": "Näytä viimiset $1 tiimat eli $2 päivät",
+       "watchlist-details": "Valvontalistala on {{PLURAL:$1|$1 sivu|$1 sivua}} (keskustelusivuja mukhaan laskematta).",
        "watchlist-options": "Valvontalistan altternatiivit",
+       "delete-confirm": "Ota poies \"$1\"",
+       "delete-legend": "Ota poies",
        "actioncomplete": "Tehty",
        "actionfailed": "Tehty epäonnistui",
        "dellogpage": "Poistoloki",
+       "rollback-confirmation-no": "Lopeta",
        "rollbacklink": "rullaa takashiin",
        "protectlogpage": "Suojausloki",
        "protectedarticle": "suojasi sivun [[$1]]",
+       "restriction-edit": "Mookkaa",
+       "restriction-move": "Siirä",
+       "restriction-upload": "Lattaa ylös",
        "undeletelink": "näytä/ota takashiin",
        "undeleteviewlink": "näytä",
        "namespace": "Nimityhjyys:",
        "invert": "Jätä pois valinta",
        "blanknamespace": "(Päätyhjyys)",
-       "contributions": "Omat mookkaukset",
+       "contributions": "{{GENDER:$1|Käyttäjän}} mookkaukset",
        "contributions-title": "Käyttäjän $1 mookkaukset",
-       "mycontris": "Omat mookkaukset",
-       "contribsub2": "Käyttäjän $1 ($2) mookkaukset",
-       "uctop": "(viiminen)",
+       "mycontris": "Mookkaukset",
+       "anoncontribs": "Mookkaukset",
+       "contribsub2": "Käyttäjän {{GENDER:$3|$1}} mookkaukset ($2)",
+       "uctop": "nykynen",
        "month": "Kuukauesta (ja aiemin)",
        "year": "Vuoesta (ja aiemin)",
-       "sp-contributions-newbies": "Näytä uusitten tulokhaitten muutokset",
        "sp-contributions-blocklog": "blokeerinkiloki",
-       "sp-contributions-uploads": "Ylöslattauksia",
+       "sp-contributions-uploads": "ylöslattauksia",
        "sp-contributions-logs": "lokit",
        "sp-contributions-talk": "keskustelu",
        "sp-contributions-search": "Hae käyttäjitten bitraakia",
        "whatlinkshere": "Mitä linkkaa tänne",
        "whatlinkshere-title": "Sivut jokka länkathaan \"$1\"",
        "whatlinkshere-page": "Sivu",
-       "linkshere": "Seuraavila sivuila on linkki sivule <strong>[[:$1]]</strong>:",
-       "nolinkshere": "Sivule \"'[[:$1]]''' ei ole linkkiä.",
+       "linkshere": "Seuraavila sivuila on linkki sivule <strong>$2</strong>:",
+       "nolinkshere": "Sivule \"'$2''' ei ole linkkiä.",
        "isredirect": "ohjaussivu",
        "istemplate": "sisäletty mallina",
        "isimage": "linkki fiilhiin",
        "blocklogpage": "Blokeerinki lokkaus",
        "blocklogentry": "blokeerattu [[$1]] blokeerausaika $2 $3",
        "block-log-flags-nocreate": "toppaa kontturejistreerinkiä",
+       "move-page-legend": "Siirä sivu",
+       "movepagebtn": "Siirä sivu",
        "movelogpage": "Siirtoloki",
        "revertmove": "siirä takashiin",
        "export": "Eksporteeraa sivuja",
        "allmessagesdefault": "Stantartiteksti",
        "thumbnail-more": "Isona",
        "thumbnail_error": "Pienoiskuvan luominen epäonnistui: $1",
+       "import-upload-filename": "Fiilinimi:",
        "tooltip-pt-userpage": "Oma käyttäjäsivu",
        "tooltip-pt-mytalk": "Oma keskustelusivu",
-       "tooltip-pt-preferences": "Omat inställninkit",
+       "tooltip-pt-preferences": "{{GENDER:|Omat inställninkit}}",
        "tooltip-pt-watchlist": "Lista sivuista, joitten mookkauksia valvot",
        "tooltip-pt-mycontris": "Lista omista mookkauksista",
        "tooltip-pt-login": "Lokkaa mielelhään sisäle, mutta ei ole pakko",
        "tooltip-pt-logout": "Lokkaa ulos",
        "tooltip-ca-talk": "Keskustelu sisälöstä",
-       "tooltip-ca-edit": "Voit mookata tätä sivua, mutta käytä esitarkastusknappia ennen kun säästät",
+       "tooltip-ca-edit": "Mookkaa tätä sivua",
        "tooltip-ca-addsection": "Alota keskustelu uuesta asiasta",
        "tooltip-ca-viewsource": "Tämä sivu on suojattu. Saatat nähhä lähekootin",
        "tooltip-ca-history": "Sivun aiemat versuunit",
        "tooltip-p-logo": "Alkusivu",
        "tooltip-n-mainpage": "Mene alkusivule",
        "tooltip-n-mainpage-description": "Mene alkusivule",
-       "tooltip-n-portal": "Keskustelua projektista",
+       "tooltip-n-portal": "Keskustelua prujektista",
        "tooltip-n-currentevents": "Löyä taustatietoja vereksistä tapahtumisista",
        "tooltip-n-recentchanges": "Lista vereksistä muutoksista",
        "tooltip-n-randompage": "Aukase satunhaisen sivun",
        "tooltip-t-whatlinkshere": "Lista wikisivuista jokka on länkattu tänne",
        "tooltip-t-recentchangeslinked": "Verekset mookkaukset sivuissa, jokka on länkattu tästä sivusta",
        "tooltip-feed-atom": "Atom-syöte tälle sivule",
-       "tooltip-t-contributions": "Näytä lista tämän käyttäjän mookkauksista",
-       "tooltip-t-emailuser": "Lähätä sähköposti tälle käyttäjälle",
+       "tooltip-t-contributions": "Näytä lista {{GENDER:$1|tämän käyttäjän}} mookkauksista",
+       "tooltip-t-emailuser": "Lähätä sähköposti {{GENDER:$1|tälle käyttäjälle}}",
        "tooltip-t-upload": "Lattaa ylös fiiliä",
        "tooltip-t-specialpages": "Lista kaikista spesiaalisivuista",
        "tooltip-t-print": "Printtausmaholinen versuuni",
        "tooltip-t-permalink": "Ikunen linkki tämän sivun  versuunhiin",
        "tooltip-ca-nstab-main": "Näytä sisältösivu",
        "tooltip-ca-nstab-user": "Näytä käyttäjäsivu",
-       "tooltip-ca-nstab-special": "Tämä on spesiaalisivu; sie et saata mookata itteä sivua",
+       "tooltip-ca-nstab-special": "Tämä on spesiaalisivu, sitä ei saata mookata",
        "tooltip-ca-nstab-project": "Näytä prujektisivu",
        "tooltip-ca-nstab-image": "Näytä fiilisivu",
        "tooltip-ca-nstab-template": "Näytä mallia",
        "tooltip-ca-nstab-category": "Näytä katekuurisivu",
        "tooltip-minoredit": "Merkitte tämä pieneksi muutokseksi",
        "tooltip-save": "Säästä mookkaukset",
-       "tooltip-preview": "Esikuvvaa sinun muutokset, käytä tätä ennen kun säästät",
+       "tooltip-preview": "Esitarkastele sinun muutokset. Käytä tätä ennen kun säästät.",
        "tooltip-diff": "Näytä sinun muutokset tekstistä",
        "tooltip-compareselectedversions": "Vertaile valitut sivuversuunit",
        "tooltip-watch": "Lissää tämä sivu sinun valvontalistale",
        "tooltip-rollback": "\"Rullaa takashiin\" kaataa yhelä klikilä viimisen mookkaajan muutokset",
        "tooltip-undo": "\"Kumota\" palauttaa tämän muutoksen ja aukasee artikkelin mookkausruutun esitarkastuksen kansa. Antaa maholisuuen kirjottaa mutiveerinkin mookkaajan yhteenvethoon",
+       "tooltip-preferences-save": "Säästä inställninkit",
        "tooltip-summary": "Kirjota lyhy yhteenveto",
+       "pageinfo-header-edits": "Mookkaushistuuria",
+       "pageinfo-hidden-categories": "{{PLURAL:$1|Piilotettu katekuuri|Piilotetut katekuurit}} ($1)",
+       "pageinfo-category-info": "Katekuuridetaljit",
        "previousdiff": "Vanheempi muutos",
        "nextdiff": "Uuempi muutos",
        "file-info-size": "$1 × $2 pikseliä, fiilin koko: $3, MIME-tyyppi: $4",
        "file-nohires": "Tarkempaa kuvvaa ei ole saatavissa.",
        "svg-long-desc": "SVG-fiili; peruskoko $1 × $2 pikseliä, fiilikoko: $3",
-       "show-big-image": "Korkearesulusuuni versuuni",
+       "show-big-image": "Alkuperäinen fiili",
+       "sp-newimages-showfrom": "Näytä uuet fiilit jälkhiin $2 $1",
        "bad_image_list": "Listan muoto on seuraava:\n\nVain *-merkilä alkavat rivit otethaan huomihoon.\nRivin ensimäinen linkki häätyy mennä kehnoon fiilhiin.\nKaikki muut linkit samala rivilä.käsitelthään poikkeuksena, eli toisin sanoen sivuja missä fiilin saapi käyttää.",
        "metadata": "Meettataatta",
        "metadata-help": "Tämä fiili sisältää lisätietoja esimerkiks kuvanlukijan, eli kuvakäsittelyprukrammin lisätietoja. Kaikki tiot ei en´nää välttämättä vastaa toelisuutheen, jos kuvvaa on mookattu sen alkuperäisen luomisen jälkhiin.",
        "metadata-fields": "Seuraavaa meettataatta kentät listattu tässä informasuunissa, sisälethään näkyvänä kuvasivussa, kun meettataatta taulukko kolapsaa. Muut piilotethaan stantartina.\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": "kaikki",
        "monthsall": "kaikki",
+       "imgmultigoto": "Mene sivule $1",
+       "autoredircomment": "Ohjattu sivule [[$1]]",
        "watchlisttools-view": "Näytä muutokset",
        "watchlisttools-edit": "Näytä ja mookkaa valvontalistaa",
        "watchlisttools-raw": "Mookkaa valvontalistaa raakamuoossa",
        "duplicate-defaultsort": "Varotus: Stantartisortteerausavvain ”$2” korvaa aieman stantartisortteerausavvaimen”$1”.",
+       "redirect-submit": "Mene",
+       "redirect-file": "Fiilinimi",
+       "fileduplicatesearch-filename": "Fiilinimi:",
        "specialpages": "Spesiaali sivut",
+       "specialpages-group-pagetools": "Sivutyöneuvot",
        "external_image_whitelist": "#Älä muuta tätä riviä ollenkhaan.<pre>\n#Kirjota rekyljääri frakmentitten meininkit (vain osa, joka mennee //-merkkitten välhiin) tähhään alle\n#Niitä verrathaan ulkoisitten (suoralinkitetyitten) kuvitten URLhin\n#Net jokka sopivat, näytethään kuvina, muuten kuvhiin näytethään vain linkit\n#Rivit, jokka alkavat #-merkilä on komentaaria\n#Tämä on riippumaton puukstavitten kokosta",
-       "tag-filter": "[[Special:Tags|Merkki]] filtteri:"
+       "tag-filter": "[[Special:Tags|Merkki]] filtteri:",
+       "tags-delete": "ota poies",
+       "restore-count-files": "{{PLURAL:$1|1 fiili|$1 fiilit}}",
+       "logentry-upload-upload": "$1 {{GENDER:$2|ylöslattasi}} $3",
+       "searchsuggest-search": "Hae {{SITENAME}}",
+       "mediastatistics-header-total": "Kaikki fiilit",
+       "mw-widgets-categoryselector-add-category-placeholder": "Lissää katekuuri...",
+       "authmanager-email-label": "E-posti",
+       "authmanager-email-help": "E-postin atressi"
 }
index ebed1b0..73a7bc4 100644 (file)
        "backend-fail-contenttype": "Impossible de déterminer le type de contenu du fichier à stocker en « $1 ».",
        "backend-fail-batchsize": "On a fourni au support de stockage un lot de $1 {{PLURAL:$1|opération|opérations}} de fichier; la limite est $2 {{PLURAL:$2|opération|opérations}}.",
        "backend-fail-usable": "Impossible de lire ou d’écrire le fichier « $1 » en raison de droits insuffisants ou de répertoires/conteneurs manquants.",
+       "backend-fail-stat": "Impossible de lire l’état du fichier « $1 ».",
+       "backend-fail-hash": "Impossible de déterminer le hachage cryptographique du fichier « $1 ».",
        "filejournal-fail-dbconnect": "Impossible de se connecter à la base de données du journal pour le terminal de stockage « $1 ».",
        "filejournal-fail-dbquery": "Impossible de mettre à jour la base de données du journal pour le terminal de stockage « $1 ».",
        "lockmanager-notlocked": "Impossible de déverrouiller « $1 » ; elle n'est pas verrouillée.",
        "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 76e0366..853ef7b 100644 (file)
        "contributions": "{{GENDER:$1|वापरपी}} योगदानां",
        "contributions-title": "$1 खातीर वापरप्याचीं योगदानां",
        "mycontris": "योगदान",
+       "anoncontribs": "योगदान",
        "uctop": "हालीचें",
        "month": "ह्या म्हयन्या सावन (आनी आदलें):",
        "year": "ह्या वर्सा सावन (आनी आदलें):",
        "whatlinkshere-links": "← दुवे",
        "whatlinkshere-hideredirs": "$1 पुनर्निर्देशन",
        "whatlinkshere-hidetrans": "$1 दूस्रात-समावेश",
-       "whatlinkshere-hidelinks": "$1 à¤\9cà¥\8bडणà¥\8dयà¥\8b",
+       "whatlinkshere-hidelinks": "$1 à¤¦à¥\81वà¥\87",
        "whatlinkshere-hideimages": "$1 फायल दुवे",
        "whatlinkshere-filters": "गाळणे",
        "ipboptions": "2 वरां: 2hours ,1 दीस:1 day,3 दीस:3 days,1 सुमान:1 week,2 सुमनां:2 weeks,1 म्हयनो:1 month,3 म्हयने:3 months,6 म्हयने:6 months,1 वर्स:1 year,अनिश्चीत:infinte",
        "thumbnail_error": "$1ः लघुप्रतिमा करतांनाची चूक",
        "tooltip-pt-userpage": "तुमचें वापरपाचें पान",
        "tooltip-pt-mytalk": "तुमचें चर्चेचें पान",
-       "tooltip-pt-preferences": "तुमची पसंती",
+       "tooltip-pt-preferences": "{{GENDER:|तुमची}} पसंती",
        "tooltip-pt-watchlist": "तुमी बदल करपा खातीर देखरेख करतात त्या पानांची वळेरी",
        "tooltip-pt-mycontris": "तुमच्या योगदानांची वळेरी",
        "tooltip-pt-login": "सत्रारंभ करप बरें, पूण तशी सक्ती ना.",
        "tooltip-summary": "आपरोसाची नोंदणी करात",
        "simpleantispam-label": "एन्टी-स्पैम तपासप.\nहे भरी<strong>नकाय</strong>!",
        "pageinfo-toolboxlink": "पानाची म्हायती",
+       "pageinfo-contentpage-yes": "हय",
        "previousdiff": "← आदलें संपादन",
        "nextdiff": "नवें संपादन →",
        "file-info-size": "$1 × $2 चित्रतत्व, फायलीचो आकार: $3, माइम प्रकार: $4",
        "watchlisttools-view": "प्रस्तूत बदल पळयात.",
        "watchlisttools-edit": "सादुरवळेरी पळय आनी संपादीत करात",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|उलयात]])",
+       "redirect-submit": "वचात",
+       "redirect-value": "मोल:",
        "specialpages": "विशेश पानां",
        "tag-filter": "[[Special:Tags|कुर्वेचीट]] गाळणो:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|कुरवेचीट|कुरवेचीटी}}]]: $2",
+       "tags-active-yes": "हय",
        "htmlform-title-not-exists": "$1 अस्तित्वांत ना.",
        "logentry-delete-delete": "$1 {{GENDER:$2|काडून उडयल्ले पान}} $3",
        "logentry-move-move": "$1 हाणें $3 पानाक $4 {{GENDER:$2|हालयला}}",
        "logentry-newusers-create": "उपयोगकत्याचें $1 {{GENDER:$2|तयार केलें}}",
        "logentry-upload-upload": "$1 {{GENDER:$2|अपलोड केला}} $3",
-       "searchsuggest-search": "सोद",
+       "searchsuggest-search": "{{SITENAME}} सोद",
        "special-characters-group-latin": "रोमी",
        "special-characters-group-latinextended": "रोमी (आनिंक-उइ)",
        "special-characters-group-ipa": "IPA",
index 3d8840d..83a633a 100644 (file)
        "difference-title": "\"$1\"-chea avrutint ontor",
        "lineno": "Line ank $1:",
        "compareselectedversions": "Nivodloleo uzollneo comparar kor",
+       "showhideselectedversions": "Venchik uzollnnechem disnnem bodol",
        "editundo": "kel'lem portavchem",
        "diff-empty": "(Kaim forok na)‎",
        "diff-multi-sameuser": "(Heach vaporpean {{PLURAL:$1|kel'lo modlo ek bodol dakhounk na|kel'le modle $1 bodol dakhounk nan}})",
        "yourrealname": "Khorem nanv:",
        "prefs-help-email": "Email potto sokticho na, pun tum gupitutor visroxi zalear gupitutor punorsthapon korunk email pottechi goroz podta.",
        "prefs-help-email-others": "Tujean dusreank tujea vapurpeacho panar vo bhasabhasache panar aslele eke email duve vorvim tuje xim sompork korunk diunk zata.\nDusre tuje xim sompork kortat tednam tuzo email potto tankam kollchenam.",
+       "userrights-user-editname": "Ek vapurpeachem nanv ghal:",
        "group-bot": "Robotam",
        "group-sysop": "Karbhari",
        "group-all": "(soglle)",
        "recentchanges-legend-heading": "<strong>Kunji:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|nove pananchi suchi]]-ui polloi)",
        "rcfilters-tag-remove": "'$1' kadd",
+       "rcfilters-legend-heading": "<strong>Sonkxepachi volleri:</strong>",
+       "rcfilters-activefilters": "Kriaxil challnneo",
        "rcfilters-activefilters-hide": "Lipoi",
        "rcfilters-activefilters-show": "Dakhoi",
+       "rcfilters-advancedfilters": "Sudarit challnneo",
+       "rcfilters-limit-title": "Dakhovpache porinnam",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|bodol}}, $2",
        "rcfilters-days-title": "Halinche dis",
-       "rcfilters-hours-title": "Halinche voram",
+       "rcfilters-hours-title": "Halinchim voram",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|dis}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|vor|voram}}",
+       "rcfilters-quickfilters": "Samball’lleleo challnneo",
+       "rcfilters-savedqueries-defaultlabel": "Samball’lleleo challnneo",
        "rcfilters-savedqueries-rename": "Nanv bodol",
+       "rcfilters-savedqueries-setdefault": "Default koxem bosoi",
+       "rcfilters-savedqueries-unsetdefault": "Default aslolem kaddun uddoi",
        "rcfilters-savedqueries-remove": "Kadun udoi",
        "rcfilters-savedqueries-new-name-label": "Nanv",
+       "rcfilters-savedqueries-apply-label": "Challnni roch",
        "rcfilters-savedqueries-cancel-label": "Rod'd kor",
        "rcfilters-show-new-changes": "$1 savn noveo bodol polloi",
+       "rcfilters-search-placeholder-mobile": "Challnneo",
+       "rcfilters-invalid-filter": "Ovoid challnni",
+       "rcfilters-empty-filter": "Kriaxil challneo nant. Sogllem iogdan dakhoilam.",
+       "rcfilters-filterlist-whatsthis": "Heo koxeo kam kortat?",
+       "rcfilters-filterlist-feedbacklink": "Hea challnnechea avtam vixim tuka kitem dista tem amkam sang",
+       "rcfilters-highlightmenu-title": "Ek rong vinch",
+       "rcfilters-filterlist-noresults": "Kosleoch challnneo mellunk nant",
+       "rcfilters-filtergroup-authorship": "Iogdanachem borovp",
        "rcfilters-filter-editsbyself-label": "Tuven kel'leo bodol",
        "rcfilters-filter-editsbyself-description": "Tujeo svotacheo yogdanam.",
        "rcfilters-filter-editsbyother-label": "Dusreanim kel'le bodol",
+       "rcfilters-filter-editsbyother-description": "Tuje khas bhairavn, soglle bodol",
+       "rcfilters-filtergroup-user-experience-level": "Vaporpeachi nondnni ani onnbhov",
+       "rcfilters-filtergroup-automated": "Apoap zalolem iogdan",
        "rcfilters-filter-bots-label": "Robot",
+       "rcfilters-filter-bots-description": "Apoap avtamni kelolem sompadon",
+       "rcfilters-filter-humans-label": "Monxan kelolem (nhoi robotan)",
+       "rcfilters-filter-humans-description": "Monxani kelolem sompadon",
+       "rcfilters-filter-reviewstatus-unpatrolled-description": "Paro kela mhonn khunnavnk naslolem sompadon.",
+       "rcfilters-filtergroup-significance": "Mhotv",
        "rcfilters-filter-minor-label": "Dhakte bodol",
+       "rcfilters-filter-minor-description": "Borovpean dhaktem mhonn khunne chitt kelolem sompadon",
+       "rcfilters-filter-major-label": "Dhakte naslolem sompadon",
+       "rcfilters-filter-major-description": "Dhaktem mhonn khunne chitt korunk naslolem sompadon",
+       "rcfilters-filtergroup-watchlist": "Sadurvollerintlim panam",
+       "rcfilters-filter-watchlist-watchednew-description": "Bodol ghoddlea uprant tuvem bhett divnk na tea sadurvollerintlim panache bodol.",
+       "rcfilters-filtergroup-watchlistactivity": "Sadurvollerichem kario",
+       "rcfilters-filter-watchlistactivity-unseen-label": "Pollovnk naslole bodol",
+       "rcfilters-filter-watchlistactivity-unseen-description": "Bodol ghoddlea uprant tuvem bhett divnk na tea panache bodol.",
+       "rcfilters-filter-watchlistactivity-seen-label": "Polloilole bodol",
+       "rcfilters-filter-watchlistactivity-seen-description": "Bodol ghoddlea uprant tuvem bhett dilolea tea panache bodol.",
+       "rcfilters-filtergroup-changetype": "Bodolacho prokar",
        "rcfilters-filter-pageedits-label": "Panacheo sompadonam",
        "rcfilters-filter-categorization-label": "Vorgache bodol",
+       "rcfilters-filter-categorization-description": "Vorgant savn pana zoddloleachi vo kaddloleachi nond",
+       "rcfilters-filter-logactions-label": "Sotran nond zal’leo kario",
+       "rcfilters-filter-logactions-description": "Karbhari kario, khatem rochop, pana kaddun uddovp, uploads...",
        "rcfilters-filtergroup-lastrevision": "Akherchim uzollnnim",
        "rcfilters-filter-lastrevision-label": "Sogleanvon novi uzollnni",
+       "rcfilters-filter-lastrevision-description": "Ek panak fokot nimannem bodol",
+       "rcfilters-filter-previousrevision-description": "Soglle bodol je \"halinchi uzollnni\" nant.",
        "rcfilters-tag-prefix-namespace-inverted": "$1 <strong>:nhoi</strong>",
+       "rcfilters-view-tags": "Khunnechittichem sompadon",
+       "rcfilters-view-tags-help-icon-tooltip": "Khunnechittichem sompadona babtint odik xikun ghe",
+       "rcfilters-target-page-placeholder": "Ek panache nanv ( vo vorg) ghal",
        "rcnotefrom": "Sokoil <strong>$3, $4<strong> savn {{PLURAL:$5|zalelem bodol dilam|zalelem bodol dileant}} (<strong>$1<strong> meren {{PLURAL:$5|dakhoilam|dakhoileant}}).",
        "rclistfrom": "$3 $2 savn suru zatelim nove bodol dakhoi",
        "rcshowhideminor": "$1 dhakte bodol",
        "prefixindex": "Panam jenche nanvache survatek asa...",
        "shortpages": "Dhaktim panam",
        "longpages": "Lamb panam",
+       "protectedpages-filters": "Challnneo:",
        "listusers": "Vaporpeanchi volleri",
        "usercreated": "$3 hannem $1 disa $2 vaztam rochlelem",
        "newpages": "Novim panam",
        "allpagessubmit": "Voch",
        "allpages-hide-redirects": "Punornirdexonam lipoi",
        "categories": "Vorg",
+       "sp-deletedcontributions-contribs": "iogdan",
        "linksearch-ns": "Nanv-tholl:",
        "linksearch-ok": "Sod",
        "linksearch-line": "$1 $2 savn zoddlelem asa",
        "deleteotherreason": "Dusrem/aniki karon:",
        "rollbacklink": "kovoll",
        "rollbacklinkcount": "$1 {{PLURAL:$1|bodol}} kovoll",
-       "changecontentmodel-title-label": "Panacho mathallo",
+       "changecontentmodel-title-label": "Panacho mathallo:",
        "changecontentmodel-reason-label": "Karonn:",
        "protectlogpage": "Surokxitechem sotr",
        "protectedarticle": "rakhlelem \"[[$1]]\"",
        "tag-filter": "[[Special:Tags|Kurvechit]] challni:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Kurvechit|Kurvechiti}}]]: $2",
        "tags-title": "Kurvechitti",
+       "tags-hitcount-header": "Khunnechittichem bodol",
        "tags-active-yes": "Hoi",
        "tags-active-no": "Na",
        "tags-hitcount": "$1 {{PLURAL:$1|bodol}}",
index f4e6f6f..e4efdb8 100644 (file)
        "backend-fail-contenttype": "לא ניתן היה לקבוע את סוג התוכן של הקובץ לאחסון ב־\"$1\".",
        "backend-fail-batchsize": "למאגר אחסון הקבצים הפנימי הועבר אוסף של {{PLURAL:$1|פעולת קובץ אחת|$1 פעולות קובץ}}; המגבלה היא {{PLURAL:$2|פעולה אחת|$2 פעולות}}.",
        "backend-fail-usable": "קריאת או כתיבת הקובץ \"$1\" לא הצליחה כיוון שההרשאות אינן מספיקות או כיוון שהספריות/המכלים חסרים.",
+       "backend-fail-stat": "לא היה אפשר לקרוא את המצב של הקובץ \"$1\".",
        "filejournal-fail-dbconnect": "לא ניתן היה להתחבר לבסיס הנתונים של היומן עבור מאגר אחסון הקבצים הפנימי \"$1\".",
        "filejournal-fail-dbquery": "לא ניתן היה לעדכן את בסיס הנתונים של היומן עבור מאגר אחסון הקבצים הפנימי \"$1\".",
        "lockmanager-notlocked": "פתיחת הנעילה של \"$1\" לא הצליחה; הוא לא נעול.",
        "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..1f35ef6 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",
        "backend-fail-contenttype": "Non poteva determinar le typo de contento del file a immagazinar in \"$1\".",
        "backend-fail-batchsize": "Le systema de immagazinage ha recipite un lot de $1 {{PLURAL:$1|operation|operationes}} de file; le limite es $2 {{PLURAL:$2|operation|operationes}}.",
        "backend-fail-usable": "Non poteva leger o scriber le file \"$1\" a causa de permissiones insufficiente o directorios/contentores mancante.",
+       "backend-fail-stat": "Impossibile leger le stato del file \"$1\".",
+       "backend-fail-hash": "Impossibile determinar le hash cryptographic del file \"$1\".",
        "filejournal-fail-dbconnect": "Non poteva connecter al base de datos de jornal pro le systema de immagazinage \"$1\".",
        "filejournal-fail-dbquery": "Non poteva actualisar le base de datos de jornal pro le systema de immagazinage \"$1\".",
        "lockmanager-notlocked": "Impossibile disblocar \"$1\"; illo non es blocate.",
        "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",
        "block-log-flags-angry-autoblock": "autoblocadas avantiate activate",
        "block-log-flags-hiddenname": "nomine de usator celate",
        "range_block_disabled": "Le capacitate del administratores a blocar intervallos de adresses IP es disactivate.",
+       "ipb-prevent-user-talk-edit": "Le modification del proprie pagina de discussion debe esser permittite pro un blocada partial, excepte si illo include un restriction sur le spatio de nomines \"Discussion usator\".",
        "ipb_expiry_invalid": "Tempore de expiration invalide.",
        "ipb_expiry_old": "Le hora de expiration es in le passato.",
        "ipb_expiry_temp": "Le blocadas de nomines de usator celate debe esser permanente.",
        "move-page-legend": "Renominar pagina",
        "movepagetext": "Per medio del formulario hic infra tu pote renominar un pagina, transferente tote su historia al nove nomine.\nLe ancian titulo devenira un pagina de redirection verso le nove titulo.\nTu pote actualisar automaticamente le redirectiones que puncta verso le titulo original.\nSi tu prefere non facer isto, non oblida de reparar omne redirectiones [[Special:DoubleRedirects|duple]] o [[Special:BrokenRedirects|rupte]].\nTu ha le responsabilitate de assecurar que le ligamines continua a punctar verso le paginas correcte.\n\nNota que le pagina <strong>non</strong> essera renominate si existe jam un pagina sub le nove titulo, excepte si iste es un redirection sin historia de modificationes passate.\nIsto te lassa le possibilitate de restaurar le titulo original de un pagina si tu ha committite un error, sin permitter te de supplantar un pagina existente.\n\n<strong>Attention:</strong>\nisto pote esser un cambio drastic e inexpectate pro un pagina popular;\nper favor assecura te de haber comprendite le consequentias de isto ante de continuar.",
        "movepagetext-noredirectfixer": "Per medio del formulario infra tu pote renominar un pagina, transferente tote su historia al nove nomine.\nLe ancian titulo devenira un pagina de redirection verso le nove titulo.\nNon oblida de reparar omne redirectiones [[Special:DoubleRedirects|duple]] o [[Special:BrokenRedirects|rupte]].\nTu ha le responsabilitate de assecurar que le ligamines continua a punctar verso le paginas correcte.\n\nNota que le pagina <strong>non</strong> essera renominate si existe jam un pagina sub le nove titulo, excepte si iste es un redirection sin historia de modificationes passate.\nIsto te lassa le possibilitate de restaurar le titulo original de un pagina si tu ha committite un error, sin permitter te de supplantar un pagina existente.\n\n<strong>Attention:</strong>\nisto pote esser un cambio drastic e inexpectate pro un pagina popular;\nper favor assecura te de haber comprendite le consequentias de isto ante de continuar.",
+       "movepagetext-noredirectsupport": "Per medio del formulario hic infra tu pote renominar un pagina, transferente tote su historia al nove nomine.\nTu ha le responsabilitate de assecurar que le ligamines continua a punctar verso le paginas correcte.\n\nNota que le pagina <strong>non</strong> essera renominate si existe jam un pagina sub le nove titulo.\nIsto te lassa le possibilitate de restaurar le titulo original de un pagina si tu ha committite un error, sin permitter te de supplantar un pagina existente.\n\n<strong>Attention:</strong>\nisto pote esser un cambio drastic e inexpectate pro un pagina popular;\nper favor assecura te de haber comprendite le consequentias de isto ante de continuar.",
        "movepagetalktext": "Si tu marca iste quadrato, le pagina de discussion associate essera automaticamente renominate al nove titulo, a minus que un pagina de discussion non vacue ja existe sub le nove nomine.\n\nIn tal caso, tu debera renominar o fusionar le pagina manualmente si desirate.",
        "moveuserpage-warning": "'''Attention:''' Tu es super le puncto de renominar un pagina de usator. Nota ben que solmente le pagina, e ''non'' le usator, essera renominate.",
        "movecategorypage-warning": "<strong>Attention:</strong> Tu es sur le puncto de renominar un pagina de categoria. Nota ben que solmente le pagina essera renominate e tote le paginas in le ancian categoria <em>non</em> essera recategorisate in le nove.",
        "move-subpages": "Renominar le subpaginas (usque a $1)",
        "move-talk-subpages": "Renominar le subpaginas del pagina de discussion (usque a $1)",
        "movepage-page-exists": "Le pagina $1 existe ja e non pote esser automaticamente superscribite.",
+       "movepage-source-doesnt-exist": "Le pagina $1 non existe e non pote esser renominate.",
        "movepage-page-moved": "Le pagina $1 ha essite renominate a $2.",
        "movepage-page-unmoved": "Le pagina $1 non poteva esser renominate a $2.",
        "movepage-max-pages": "Le maximo de $1 {{PLURAL:$1|pagina|paginas}} ha essite renominate e nulle altere pagina pote esser renominate automaticamente.",
        "delete_and_move_reason": "Delite pro permitter le renomination de \"[[$1]]\"",
        "selfmove": "Le titulo es identic;\nnon pote renominar un pagina al mesme titulo.",
        "immobile-source-namespace": "Non pote renominar paginas in le spatio de nomines \"$1\"",
+       "immobile-source-namespace-iw": "Paginas sur altere wikis non pote esser displaciate a iste wiki.",
        "immobile-target-namespace": "Non pote renominar paginas verso le spatio de nomines \"$1\"",
        "immobile-target-namespace-iw": "Un ligamine interwiki non es un destination valide pro le renomination de un pagina.",
        "immobile-source-page": "Iste pagina non es renominabile.",
        "immobile-target-page": "Non pote renominar a iste titulo de destination.",
+       "movepage-invalid-target-title": "Le nomine requestate non es valide.",
        "bad-target-model": "Le destination desirate usa un altere modello de contento. Non es possibile converter de $1 a $2.",
        "imagenocrossnamespace": "Impossibile renominar un file verso un spatio de nomines non-file",
        "nonfile-cannot-move-to-file": "Impossibile renominar un non-file verso le spatio de nomines file",
        "permanentlink": "Ligamine permanente",
        "permanentlink-revid": "ID del version",
        "permanentlink-submit": "Vader al version",
+       "newsection": "Nove section",
+       "newsection-page": "Pagina de destination",
+       "newsection-submit": "Vader al pagina",
        "dberr-problems": "Pardono! Iste sito ha incontrate difficultates technic.",
        "dberr-again": "Proba attender alcun minutas e recargar.",
        "dberr-info": "(Non pote acceder al base de datos: $1)",
        "linkaccounts": "Ligar contos",
        "linkaccounts-success-text": "Le conto ha essite ligate.",
        "linkaccounts-submit": "Ligar contos",
+       "cannotunlink-no-provider-title": "Il non ha contos ligate a disligar",
+       "cannotunlink-no-provider": "Il non ha contos ligate que pote esser disligate.",
        "unlinkaccounts": "Disligar contos",
        "unlinkaccounts-success": "Le conto ha essite disligate.",
        "authenticationdatachange-ignored": "Le cambiamento del datos de authentication non ha succedite. Pote esser que nulle fornitor ha essite configurate?",
        "edit-error-short": "Error: $1",
        "edit-error-long": "Errores:\n\n$1",
        "specialmute": "Silentio",
-       "specialmute-success": "Tu preferentias de silentio ha essite actualisate. Vide tote le usatores silentiate in [[Special:Preferences]].",
+       "specialmute-success": "Tu preferentias de silentio ha essite actualisate. Vide tote le usatores silentiate in [[Special:Preferences|tu preferentias]].",
        "specialmute-submit": "Confirmar",
        "specialmute-label-mute-email": "Silentiar e-mail de iste usator",
-       "specialmute-header": "Selige tu preferentias de silentio pro <b>{{BIDI:[[User:$1]]}}</b>.",
+       "specialmute-header": "Selige tu preferentias de silentio pro le usator <b>{{BIDI:[[User:$1]]}}</b>.",
        "specialmute-error-invalid-user": "Le nomine de usator que tu requestava non pote esser trovate.",
-       "specialmute-email-footer": "Pro gerer le preferentias de e-mail pro {{BIDI:$2}}, visita <$1>.",
+       "specialmute-error-no-options": "Le functionalitate de silentiamento non es disponibile. Isto pote esser perque tu non ha confirmate tu adresse de e-mail, o perque le administrator del wiki ha disactivate le functionalitate de e-mail e/o le lista nigre de e-mail pro iste wiki.",
+       "specialmute-email-footer": "Pro gerer le preferentias de e-mail pro le usator {{BIDI:$2}}, visita <$1>.",
        "specialmute-login-required": "Es necessari aperir session pro cambiar le preferentias de silentio.",
+       "mute-preferences": "Preferentias de silentiamento",
        "revid": "version $1",
        "pageid": "ID de pagina $1",
        "interfaceadmin-info": "$1\n\nLe permissiones pro modificar le files CSS/JS/JSON global del sito ha recentemente essite separate del privilegio <code>editinterface</code>. Si tu non comprende proque tu recipe iste error, vide [[mw:MediaWiki_1.32/interface-admin]].",
        "passwordpolicies-policy-passwordnotinlargeblacklist": "Le contrasigno non pote apparer in le lista del 100.000 contrasignos le plus commun.",
        "passwordpolicies-policyflag-forcechange": "debe cambiar al apertura de session",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suggerer cambio al apertura de session",
+       "mycustomjsredirectprotected": "Tu non ha le permission de modificar iste pagina JavaScript perque illo es un redirection e non puncta a un pagina in tu spatio de usator.",
        "easydeflate-invaliddeflate": "Le contento fornite non es correctemente comprimite",
        "unprotected-js": "Pro motivos de securitate, non es possibile cargar codice JavaScript de paginas non protegite. Crea JavaScript solmente in le spatio de nomines \"MediaWiki:\" o como un subpagina de usator.",
        "userlogout-continue": "Vole tu clauder le session?"
index 9cb6ee1..81ade2a 100644 (file)
        "rcfilters-filter-editsbyself-label": "Vua modifikuri",
        "rcfilters-filter-editsbyself-description": "Vua propra kontributaji",
        "rcfilters-filter-editsbyother-label": "Modifikuri da altri",
-       "rcfilters-filter-editsbyother-description": "Omna modififuri, ecepte vua propra.",
+       "rcfilters-filter-editsbyother-description": "Omna modifikuri, ecepte vua propra.",
        "rcfilters-filtergroup-user-experience-level": "Registro e nivelo di konoco dil uzero",
        "rcfilters-filter-user-experience-level-registered-label": "Enrejistrita",
        "rcfilters-filter-user-experience-level-registered-description": "Enrejistrita redakteri.",
        "rcfilters-filter-watchlist-watched-label": "En mea surveyo-listo",
        "rcfilters-filter-watchlist-watched-description": "Modifikuri en pagini de vua surveyo-listo.",
        "rcfilters-filter-watchlist-watchednew-label": "Nova modifikuri en la surveyo-listo",
+       "rcfilters-filter-watchlist-watchednew-description": "Chanji en pagini quin vu surveyas, ma quin vu ne vizitis pos ke la modifikuri eventis.",
        "rcfilters-filter-watchlist-notwatched-label": "Ne en surveyo-listo",
        "rcfilters-filter-watchlist-notwatched-description": "Omni, ecepte modifikuri en la pagini de vua surveyo-listo.",
        "rcfilters-filter-watchlistactivity-unseen-description": "Modifikuri en la pagini quin vu ne vizitis pos ke la modifikuri facesis.",
        "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 7755186..ad0b23b 100644 (file)
        "backend-fail-contenttype": "Impossibile determinare la tipologia del file da archiviare in \"$1\".",
        "backend-fail-batchsize": "Il backend di memoria ha programmato una serie di $1 {{PLURAL:$1|operazione|operazioni}} su file; il limite è di $2 {{PLURAL:$2|operazione|operazioni}}.",
        "backend-fail-usable": "Impossibile leggere o scrivere il file \"$1\" a causa di autorizzazione insufficienti o directory/contenitori mancanti.",
+       "backend-fail-stat": "Non è possibile leggere lo stato del file \"$1\".",
        "filejournal-fail-dbconnect": "Impossibile connettersi al database journal per l'archiviazione back-end \"$1\".",
        "filejournal-fail-dbquery": "Impossibile aggiornare il database journal per l'archiviazione back-end \"$1\".",
        "lockmanager-notlocked": "Impossibile sbloccare \"$1\"; non è bloccato.",
        "sessionfailure": "Si è verificato un problema nella sessione che identifica l'accesso; il sistema non ha eseguito il comando impartito per precauzione. Invia nuovamente il modulo.",
        "changecontentmodel": "Modifica il modello di contenuto di una pagina",
        "changecontentmodel-legend": "Modifica il modello di contenuto",
-       "changecontentmodel-title-label": "Titolo della pagina",
+       "changecontentmodel-title-label": "Titolo della pagina:",
        "changecontentmodel-current-label": "Modello contenuto attuale:",
-       "changecontentmodel-model-label": "Nuovo modello di contenuto",
+       "changecontentmodel-model-label": "Nuovo modello di contenuto:",
        "changecontentmodel-reason-label": "Motivo:",
        "changecontentmodel-submit": "Modifica",
        "changecontentmodel-success-title": "Il modello di contenuto è stato modificato",
index f22ae2b..52a713f 100644 (file)
        "nocreate-loggedin": "새 문서를 만들 권한이 없습니다.",
        "sectioneditnotsupported-title": "부분 편집이 지원되지 않음",
        "sectioneditnotsupported-text": "이 문서에서는 문단 편집을 지원하지 않습니다.",
+       "modeleditnotsupported-title": "편집이 지원되지 않습니다",
        "permissionserrors": "권한 오류",
        "permissionserrorstext": "해당 명령을 수행할 권한이 없습니다. 다음 {{PLURAL:$1|이유}}를 확인해보세요:",
        "permissionserrorstext-withaction": "$2 권한이 없습니다. 다음 {{PLURAL:$1|이유}}를 확인해주세요:",
        "backend-fail-contenttype": "\"$1\"에 저장하기 위한 파일의 내용 유형을 결정하지 못했습니다.",
        "backend-fail-batchsize": "저장 백엔드에서 파일 {{PLURAL:$1|작업}} $1개가 쌓였습니다. 한계는 {{PLURAL:$2|작업}} $2개입니다.",
        "backend-fail-usable": "파일 읽기/쓰기 권한이 없거나 저장 위치가 빠졌기 때문에 \"$1\" 파일을 읽거나 쓸 수 없습니다.",
+       "backend-fail-stat": "\"$1\" 파일의 상태를 읽지 못했습니다.",
+       "backend-fail-hash": "\"$1\" 파일의 암호화 해시를 결정하지 못했습니다.",
        "filejournal-fail-dbconnect": "저장소 백엔드 \"$1\"에 대한 저널 데이터베이스에 연결할 수 없습니다.",
        "filejournal-fail-dbquery": "저장소 백엔드 \"$1\"에 대한 저널 데이터베이스에서 새로 고칠 수 없습니다.",
        "lockmanager-notlocked": "\"$1\" 경로의 잠금을 풀 수 없습니다. 해당 경로는 잠겨 있지 않습니다.",
        "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": "콘텐츠 모델이 변경되었습니다",
        "move-page-legend": "문서 이동",
        "movepagetext": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 되고,\n원래 이름을 가리키는 넘겨주기는 자동으로 갱신됩니다.\n만약 이 설정을 선택하지 않았다면 [[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
        "movepagetext-noredirectfixer": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 됩니다.\n[[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
+       "movepagetext-noredirectsupport": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 제목을 새 제목으로 입력했을 때는 그 문서가 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의:</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
        "movepagetalktext": "이 칸에 체크하면, 딸린 토론 문서가 자동으로 이동됩니다. 다만 비어있지 않은 토론 문서가 있다면 이동되지 않습니다.\n\n이러한 경우에는 수동으로 이동하거나 합쳐야 합니다.",
        "moveuserpage-warning": "<strong>경고:</strong> 사용자 문서를 이동하려고 하고 있습니다. 사용자 문서만 이동되며 사용자 이름이 바뀌지 <strong>않는다</strong>는 점을 참고하세요.",
        "movecategorypage-warning": "<strong>경고:</strong> 분류 문서를 이동하려고 합니다. 해당 문서만 이동되고 옛 분류에 있는 문서는 새 분류 안에 다시 분류되지 <em>않음</em>을 참고하세요.",
index 304d5b4..643e52f 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",
        "move-subpages": "Ënnersäite (bis zu $1) réckelen",
        "move-talk-subpages": "Ënnersäite vun der Diskussiounssäit (bis zu $1), matréckelen",
        "movepage-page-exists": "D'Säit $1 gëtt et schonn a kann net automatesch iwwerschriwwe ginn.",
+       "movepage-source-doesnt-exist": "D'Säit $1 gëtt et net a kann net geréckelt ginn.",
        "movepage-page-moved": "D'Säit $1 gouf op $2 geréckelt.",
        "movepage-page-unmoved": "D'Säit $1 konnt net op $2 geréckelt ginn.",
        "movepage-max-pages": "Déi Maximalzuel vu(n) $1 {{PLURAL:$1|Säit gouf|Säite goufe}} gouf geréckelt. All déi aner Säite kënnen net automatesch geréckelt ginn.",
        "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 820de6c..b50b012 100644 (file)
        "blockedtext": "'''Tavs lietotāja vārds vai IP adrese ir nobloķēta.'''\n\n$1 nobloķēja tavu lietotāja vārdu vai IP adresi.\nBloķējot norādītais iemesls bija: ''$2''.\n\n*Bloka sākums: $8\n*Bloka beigas: $6\n*Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|administratoru]] lai apspriestu šo bloku.\n\nPievērs uzmanību, tam, ka ja tu neesi norādījis derīgu e-pasta adresi ''[[Special:Preferences|savās izvēlēs]]'', tev nedarbosies \"sūtīt e-pastu\" iespēja.\n\nTava IP adrese ir $3 un bloka identifikators ir #$5. Lūdzu iekļauj vienu no tiem, vai abus, visos turpmākajos pieprasījumos.",
        "autoblockedtext": "Tava IP adrese ir tikusi automātiski nobloķēta, tāpēc, ka to (nupat kā) ir lietojis cits dalībnieks, kuru nobloķēja $1.\nNorādītais bloķēšanas iemesls bija:\n\n:''$2''\n\n* Bloka sākums: $8\n* Bloka beigas: $6\n* Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|adminu]] lai apspriestu šo bloku.\n\nAtceries, ka tu nevari lietot \"sūtīt e-pastu šim dalībniekam\" iespēju, ja tu neesi norādījis derīgu e-pasta adresi savās [[Special:Preferences|dalībnieka izvelēs]] un bloķējot tev nav aizbloķēta iespēja sūtīt e-pastu.\n\nTava pašreizējā IP adrese ir $3 un  bloka ID ir $5.\nLūdzu iekļauj šos visos ziņojumos, kurus sūti adminiem, apspriežot šo bloku.",
        "blockednoreason": "iemesls nav norādīts",
+       "blockedtext-composite-no-ids": "Tava IP adrese ir iekļauta vairākos melnajos sarakstos",
        "whitelistedittext": "Lūdzu $1, lai varētu labot lapas.",
        "confirmedittext": "Lai varētu izmainīt lapas, vispirms jāapstiprina savu e-pasta adresi.\nNorādi un apstiprini e-pasta adresi savos [[Special:Preferences|lietotāja uzstādījumos]].",
        "nosuchsectiontitle": "Nevaru atrast sadaļu",
        "uploadstash-bad-path-unknown-type": "Nezināms tips \"$1\".",
        "uploadstash-bad-path-unrecognized-thumb-name": "Neatpazīts sīktēla nosaukums.",
        "uploadstash-file-not-found-no-thumb": "Nevarēja iegūt sīkbildi.",
+       "uploadstash-no-extension": "Paplašinājums ir tukšs.",
        "uploadstash-zero-length": "Faila garums ir nulle.",
        "img-auth-accessdenied": "Pieeja liegta",
        "img-auth-nopathinfo": "Trūkst PATH_INFO.\nJūsu serveris nav konfigurēts nodot šo informāciju.\nTas var būt bāzēts uz CGI un neatbalstīt img_auth.\nSkatīt https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
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 be18d2e..e63cd39 100644 (file)
        "backend-fail-contenttype": "Не можев да утврдам каква содржина има податотеката што треба да ја складирам во „$1“.",
        "backend-fail-batchsize": "Складишната основа доби блок од $1 {{PLURAL:$1|податотечна постапка|податотечни постапки}}, а ограничувањето е $2 {{PLURAL:$2|постапка|постапки}}.",
        "backend-fail-usable": "Не можев да ја прочитам или запишам податотеката „$1“ бидејќи немате доволно дозволи или поради тоа што недостасуваат именици/содржатели.",
+       "backend-fail-stat": "Не можев да ја прочитам состојбата на податотеката „$1“.",
+       "backend-fail-hash": "Не можев да ја одредам криптографската тараба на податотеката „$1“",
        "filejournal-fail-dbconnect": "Не можев да се поврзам со дневничката база за складишната основа „$1“.",
        "filejournal-fail-dbquery": "Не можев да ја подновам дневничката база за складишната основа „$1“.",
        "lockmanager-notlocked": "Не можев да го отклучам „$1“ бидејќи не е заклучен.",
        "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 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 2797654..7540d5c 100644 (file)
@@ -43,7 +43,7 @@
        "tog-watchlisthidepatrolled": "Yengnaba Parengdagi eigi apikpa semgatlak pa su lotlu",
        "tog-watchlisthidecategorization": "ꯂꯥꯃꯥꯏꯒꯤ ꯃꯊꯪ ꯃꯅꯥꯎ ꯅꯥꯏꯕꯥ ꯂꯣꯠꯄꯥ",
        "tog-ccmeonemails": "Send me copies of emails I send to other users",
-       "tog-diffonly": "ê¯\82ꯥê¯\83ꯥê¯\8fê¯\92ꯤ ê¯\91ê¯\8cꯥê¯\8eê¯\95ꯥê¯\97ꯨ ê¯\83ê¯\88ꯥê¯\92ꯤ diffs ê¯\87ꯥ ꯎꯨꯠꯀꯅꯨ",
+       "tog-diffonly": "ê¯\82ê¯\83ꯥê¯\8fê¯\92ꯤ ê¯\91ê¯\8cꯥê¯\8eê¯\95ꯥê¯\97ꯨ ê¯\83ê¯\88ꯥê¯\92ꯤ ê¯\88ꯦꯠê¯\85ê¯\95ê¯\97ꯨ ꯎꯨꯠꯀꯅꯨ",
        "tog-showhiddencats": "ꯑꯔꯣꯠꯄꯥ ꯃꯊꯪ ꯃꯅꯥꯎ ꯅꯥꯏꯕꯗꯨ ꯎꯨꯠꯂꯨ",
        "tog-norollbackdiff": "Don't show diff after performing a rollback",
        "tog-useeditwarning": "Warn me when I leave an edit page with unsaved changes",
        "virus-scanfailed": "ꯁꯦꯡꯗꯣꯛꯄꯥ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ (code $1)",
        "virus-unknownscanner": "ꯁꯛꯈꯪꯗꯕꯥ ꯑꯦꯟꯇꯤ ꯕꯥꯏꯔꯨꯁ",
        "logouttext": "<strong>You are now logged out.</strong>\n\nNote that some pages may continue to be displayed as if you were still logged in, until you clear your browser cache.",
-       "cannotlogoutnow-title": "ê¯\8dꯧê¯\96ꯤê¯\9b ê¯\82ꯣê¯\92 out ê¯\87ꯧê¯\95ꯥ ê¯\8cꯥê¯\97ꯦ",
-       "cannotlogoutnow-text": "$1 ê¯\81ꯤê¯\96ꯤê¯\9fê¯\85ê¯\94ꯤê¯\89ꯩê¯\97ꯥ ê¯\82ꯣê¯\92ê¯\92ꯤꯡ out ꯁꯤ ꯑꯣꯏꯊꯣꯛꯇꯦ",
+       "cannotlogoutnow-title": "ê¯\8dꯧê¯\96ꯤê¯\9b ê¯\8aꯣê¯\9bê¯\82ê¯\9bê¯\84 ê¯\8cꯥê¯\94ꯣê¯\8f",
+       "cannotlogoutnow-text": "$1 ê¯\81ꯤê¯\96ꯤê¯\9fê¯\85ê¯\94ꯤê¯\89ꯩê¯\97ꯥ ê¯\83ê¯\84ꯥê¯\9f ê¯\8aꯣê¯\9bê¯\82ê¯\9bê¯\84ꯁꯤ ꯑꯣꯏꯊꯣꯛꯇꯦ",
        "welcomeuser": "ꯂꯦꯡꯁꯤꯟꯕꯤꯔꯛꯁꯤ,$1!",
        "welcomecreation-msg": "ꯅꯪꯒꯤ ꯑꯦꯀꯥꯎꯟꯇ ꯁꯤ ꯁꯥꯈꯔꯦ\nꯅꯪꯒꯤ ꯑꯄꯥꯝꯕꯒꯤ ꯃꯇꯨꯡ ꯏꯟꯅꯥ ꯍꯣꯡꯗꯣꯛꯄꯥ ꯌꯥꯔꯦ ꯅꯪꯅꯥ {{SITENAME}} [[Special:Preferences|preferences]] ꯅꯪꯒꯤ ꯑꯄꯥꯝꯕꯒꯤ ꯃꯇꯨꯡꯏꯟꯅꯥ.",
        "yourname": "ꯁꯤꯖꯤꯟꯅꯔꯤꯕꯥ ꯃꯃꯤꯡ:",
        "userlogin-yourpassword-ph": "ꯄꯥꯁꯋ꯭ꯔꯗ ꯏꯔꯛ ꯎ",
        "createacct-yourpassword-ph": "ꯄꯥꯁꯋ꯭ꯔꯗ ꯏꯔꯛ ꯎ",
        "yourpasswordagain": "ꯑꯃꯨꯛꯍꯟꯅꯥ ꯄꯥꯁꯋ꯭ꯔꯗ ꯅꯝꯃꯨ:",
-       "createacct-yourpasswordagain": "Confirm password",
+       "createacct-yourpasswordagain": "ꯄꯥꯁꯋ꯭ꯔꯗ ꯌꯥꯔꯦ",
        "createacct-yourpasswordagain-ph": "ꯑꯃꯨꯛ ꯍꯟꯅꯥ ꯄꯥꯁꯋ꯭ꯇ ꯏꯌꯨ",
        "userlogin-remembermypassword": "ꯑꯩꯕꯨ ꯑꯗꯨꯝ ꯂꯣꯒ ꯏꯟ ꯇꯧꯍꯟꯂꯨ",
        "userlogin-signwithsecure": "ꯁꯣꯏꯔꯣꯏꯗꯕꯥ ꯁꯝꯅꯕꯥ  ꯁꯤꯖꯤꯟ ꯅꯧ",
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 355f391..7ccdd86 100644 (file)
        "nocreate-loggedin": "Du har ikke tillatelse til å opprette sider.",
        "sectioneditnotsupported-title": "Seksjonsredigering støttes ikke",
        "sectioneditnotsupported-text": "Seksjonsredigering støttes ikke på denne siden.",
+       "modeleditnotsupported-title": "Redigering støttes ikke",
+       "modeleditnotsupported-text": "Redigering støttes ikke for innholdsmodellen $1.",
        "permissionserrors": "Rettighetsfeil",
        "permissionserrorstext": "Du har ikke tillatelse til å utføre dette, av følgende {{PLURAL:$1|grunn|grunner}}:",
        "permissionserrorstext-withaction": "Du har ikke tillatelse til å $2 {{PLURAL:$1|fordi|av følgende grunner}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Tomt objekt",
        "content-json-empty-array": "Tom matrise",
+       "unsupported-content-model": "<strong>Advarsel:</strong> Innholdsmodellen $1 støttes ikke på denne wikien.",
+       "unsupported-content-diff": "Differ støttes ikke for innholdsmodellen $1.",
+       "unsupported-content-diff2": "Differ mellom innholdsmodellene $1 og $2 støttes ikke på denne wikien.",
        "deprecated-self-close-category": "Sider som bruker ugyldige, balanserte HTML-tagger.",
        "deprecated-self-close-category-desc": "Siden inneholder balanserte HTML-tagger slike som <code>&lt;b/></code> or <code>&lt;span/></code>. Oppførselen til disse vil snart endret for å være i samsvar med HTML-spesifikasjonen, og da vil de ikke kunne brukes som wikitekst.",
        "duplicate-args-warning": "<strong>Advarsel:</strong> [[:$1]] kaller [[:$2]] med flere enn en verdi for \"$3\"-parameteren. Bare den sist angitte verdien vil brukes.",
        "rcfilters-filter-showlinkedto-label": "Vis endringer på sider som lenker til",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Sider som lenker til</strong> den valgte siden",
        "rcfilters-target-page-placeholder": "Skriv inn et sidenavn (eller en kategori)",
+       "rcfilters-allcontents-label": "Alt innhold",
+       "rcfilters-alldiscussions-label": "Alle diskusjoner",
        "rcnotefrom": "Nedenfor er vist {{PLURAL:$5|endringen|endringene}} som er gjort siden <strong>$3, $4</strong> (frem til <strong>$1</strong>).",
        "rclistfromreset": "Nullstill datovalg",
        "rclistfrom": "Vis nye endringer fra og med $3 $2",
        "backend-fail-contenttype": "Kunne ikke avgjøre innholdstypen til filen som skal lagres på «$1».",
        "backend-fail-batchsize": "Bakgrunnsprosesseringen belastet med {{PLURAL:$1|en filoperasjon|en samling av $1 filoperasjoner}}; grensen er $2.",
        "backend-fail-usable": "Kunne ikke lese eller skrive fila «$1» på grunn av utilstrekkelige tillatelser eller manglende mapper/beholdere.",
+       "backend-fail-stat": "Kunne ikke lese statusen til fila «$1».",
+       "backend-fail-hash": "Kunne ikke bestemme den kryptografiske signaturen til fila «$1».",
        "filejournal-fail-dbconnect": "Kunne ikke koble til journaldatabasen for lagringssystemet «$1».",
        "filejournal-fail-dbquery": "Kunne ikke oppdatere journaldatabasen for lagringssystemet «$1».",
        "lockmanager-notlocked": "Kunne ikke låse opp «$1» fordi den er ikke låst.",
        "sessionfailure": "Det ser ut til å være et problem med innloggingen din, og handlingen ble avbrutt av sikkerhetshensyn. Vennlgist prøv å sende skjemaet en gang til.",
        "changecontentmodel": "Endre innholdsmodell for en side",
        "changecontentmodel-legend": "Endre innholdsmodell",
-       "changecontentmodel-title-label": "Sidetittel",
+       "changecontentmodel-title-label": "Sidetittel:",
        "changecontentmodel-current-label": "Nåværende innholdsmodell:",
-       "changecontentmodel-model-label": "Ny innholdsmodell",
+       "changecontentmodel-model-label": "Ny innholdsmodell:",
        "changecontentmodel-reason-label": "Begrunnelse:",
        "changecontentmodel-submit": "Endre",
        "changecontentmodel-success-title": "Innholdsmodellen ble endret",
        "move-subpages": "Flytt alle undersider (opp til $1)",
        "move-talk-subpages": "Flytt alle undersider av diskusjonssiden (opp til $1)",
        "movepage-page-exists": "Siden $1 finnes allerede og kan ikke overskrives automatisk.",
+       "movepage-source-doesnt-exist": "Sida $1 eksisterer ikke, og kan derfor ikke flyttes.",
        "movepage-page-moved": "Siden $1 har blitt flyttet til $2.",
        "movepage-page-unmoved": "Siden $1 kunne ikke flyttes til $2.",
        "movepage-max-pages": "Grensen på {{PLURAL:$1|én side|$1 sider}} er nådd; ingen flere sider vil bli flyttet automatisk.",
        "delete_and_move_reason": "Slettet for å muliggjøre flytting fra \"[[$1]]\"",
        "selfmove": "Tittelen er den samme; kan ikke flytte en side til seg selv.",
        "immobile-source-namespace": "Kan ikke flytte sider i navnerommet «$1»",
+       "immobile-source-namespace-iw": "Sider på andre wikier kan ikke flyttes fra denne wikien.",
        "immobile-target-namespace": "Kan ikke flytte sider til navnerommet «$1»",
        "immobile-target-namespace-iw": "Du kan ikke flytte en side til et navn som er en interwikilenke.",
        "immobile-source-page": "Denne siden kan ikke flyttes.",
        "immobile-target-page": "Kan ikke flytte til det navnet.",
+       "movepage-invalid-target-title": "Det forespurte navnet er ugyldig.",
        "bad-target-model": "Det ønskede målet bruker en annen innholdsmodell. Kan ikke konvertere fra $1 til $2.",
        "imagenocrossnamespace": "Kan ikke flytte filer til andre navnerom enn filnavnerommet",
        "nonfile-cannot-move-to-file": "Kan ikke flytte ikke-filer til filnavnerom",
index 4c125e5..c56b4df 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": "{{PLURAL:$2|Disse kategory hevt de volgende subkategory.|Disse kategory hevt de volgende {{PLURAL:$1|subkategory|$1 subkategoryen}}, van in totaal $2.}}",
        "category-subcat-count-limited": "Disse kategorie hef de volgende {{PLURAL:$1|subkategorie|$1 subkategorieën}}.",
-       "category-article-count": "{{PLURAL:$2|In disse kategorie steet allinnig de volgende zied.|De volgende {{PLURAL:$1|zied steet|$1 ziejen staon}} in disse kategorie, van in totaal $2.}}",
+       "category-article-count": "{{PLURAL:$2|In disse kategory steyt allinnig de volgende syde.|De volgende {{PLURAL:$1|syde steyt|$1 syden stån}} in disse kategory, van in totaal $2.}}",
        "category-article-count-limited": "In disse kategorie {{PLURAL:$1|steet de volgende zied|staon de volgende $1 ziejen}}.",
        "category-file-count": "In disse kategorie {{PLURAL:$2|steet t volgende bestaand|staon de volgende $1 bestaanden, van in totaal $2}}.",
        "category-file-count-limited": "In disse kategorie {{PLURAL:$1|steet t volgende bestaand|staon de volgende $1 bestaanden}}.",
        "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}}",
        "searchbutton": "Söken",
        "go": "Artikel",
        "searcharticle": "Artikel",
-       "history": "Geschiedenisse",
-       "history_short": "Geschiedenisse",
+       "history": "Geskydenisse",
+       "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",
        "subject": "Onderwarp:",
        "minoredit": "kleine wysiging",
        "watchthis": "volg disse syde",
-       "savearticle": "Zied opslaon",
+       "savearticle": "Syde upslån",
        "savechanges": "Wysigingen upslån",
        "publishpage": "Zied uutbrengen",
        "publishchanges": "Wysigingen üütbrengen",
        "nosuchsectiontitle": "Disse seksie besteet niet",
        "nosuchsectiontext": "Je proberen n seksie te bewarken dat niet besteet.\nt Kan ween dat t herneumd is of dat t vortedaon is to jie t an t bekieken waren.",
        "loginreqtitle": "Anmelden verplicht",
-       "loginreqlink": "Anmelden",
+       "loginreqlink": "anmelden",
        "loginreqpagetext": "Je mutten $1 um disse zied te bekieken.",
        "accmailtitle": "Wachtwoord is verstuurd.",
        "accmailtext": "Der is n willekeurig wachtwoord veur [[User talk:$1|$1]] verstuurd naor $2. t Kan ewiezigd wörden op de zied ''[[Special:ChangePassword|wachtwoord wiezigen]]'' naoda'j an-emeld bin.",
        "newarticle": "(Niej)",
        "newarticletext": "Disse zied besteet nog niet.\nIn t veld hieronder ku'j wat schrieven um disse zied an te maken (meer informasie vie'j op de [$1 hulpzied]).\nA'j hier per ongelok terechtekeumen bin gebruuk dan de knoppe '''veurige''' um weerumme te gaon.",
        "anontalkpagetext": "---- ''Disse overlegzied heurt bie n anonieme gebruker die nog gien gebrukersnaam hef, of t niet gebruukt. We gebruken daorumme t IP-adres um hum of heur te herkennen, mer t kan oek ween dat meerdere personen t zelfde IP-adres gebruken, en da'j hiermee berichten ontvangen die niet veur joe bedoeld bin. A'j dit veurkoemen willen, dan ku'j t best [[Special:CreateAccount|n gebrukersnaam anmaken]] of [[Special:UserLogin|anmelden]].''",
-       "noarticletext": "Der steet noen gien tekste op disse zied.\nJe kunnen [[Special:Search/{{PAGENAME}}|de titel opzeuken]] in aandere ziejen,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zeuken in de logboeken],\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse zied anmaken]</span>.",
+       "noarticletext": "Der steyt nun geen tekst up disse syde.\nJy künnet [[Special:Search/{{PAGENAME}}|de titel upsöken]] in andere syden,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} söken in de logboken],\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse syde anmaken]</span>.",
        "noarticletext-nopermission": "Op disse zied steet gien tekste.\nJe kunnen [[Special:Search/{{PAGENAME}}|zeuken naor disse term]] in aandere ziejen of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken deurzeuken]</span>, mer je hebben gien rechten um disse zied an te maken.",
        "missing-revision": "De versie #$1 van de zied \"{{FULLPAGENAME}} besteet niet.\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].",
        "userpage-userdoesnotexist": "Je bewarken n gebrukerszied van n gebruker die niet besteet (gebruker \"<nowiki>$1</nowiki>\"). Kiek effen nao o'j disse zied wel anmaken/bewarken willen.",
        "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",
+       "cur": "aktueel",
        "next": "Volgende",
-       "last": "leste",
+       "last": "lätste",
        "page_first": "eerste",
        "page_last": "leste",
        "histlegend": "Verklaoring aofkortingen: (noen) = verschil mit de op-esleugen versie, (veurige) = verschil mit de veurige versie, K = kleine wieziging",
        "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",
-       "prevn": "veurige {{PLURAL:$1|$1}}",
+       "prevn": "vöärige {{PLURAL:$1|$1}}",
        "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": "Uutbreided",
+       "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",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|sinds joew leste bezeuk}}",
        "enhancedrc-history": "geschiedenisse",
        "recentchanges": "Lätste wysigingen",
-       "recentchanges-legend": "Opsies veur leste wiezigingen",
+       "recentchanges-legend": "Optys vöär lätste wysigingen",
        "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 sydgroutde 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",
        "rcfilters-liveupdates-button": "Rechtstreakse aktualisering",
        "rcfilters-liveupdates-button-title-off": "Nye wysigingen voorddalik låten seen",
        "rcnotefrom": "Wysigingen sinds <strong>$3, $4</strong> (maximaal <strong>$1</strong> {{PLURAL:$1|wysiging|wysigingen}}).",
-       "rclistfrom": "Bekiek wiezigingen vanaof $3 $2",
-       "rcshowhideminor": "$1 kleine wiezigingen",
+       "rclistfrom": "Bekyk wysigingen vanaf $3 $2",
+       "rcshowhideminor": "$1 kleine wysigingen",
        "rcshowhideminor-show": "Bekiek",
        "rcshowhideminor-hide": "Verbarg",
-       "rcshowhidebots": "$1 botgebrukers",
+       "rcshowhidebots": "$1 botbrukers",
        "rcshowhidebots-show": "Bekiek",
        "rcshowhidebots-hide": "Verbarg",
-       "rcshowhideliu": "$1 eregistreerden gebrukers",
+       "rcshowhideliu": "$1 registreerde brukers",
        "rcshowhideliu-show": "Bekiek",
        "rcshowhideliu-hide": "Verbarg",
-       "rcshowhideanons": "$1 anonieme gebrukers",
+       "rcshowhideanons": "$1 anonime brukers",
        "rcshowhideanons-show": "Bekiek",
        "rcshowhideanons-hide": "Verbarg",
        "rcshowhidepatr": "$1 nao-ekeken bewarkingen",
        "rcshowhidepatr-show": "Bekiek",
        "rcshowhidepatr-hide": "Verbarg",
-       "rcshowhidemine": "$1 mien bewarkingen",
+       "rcshowhidemine": "$1 myn bewarkingen",
        "rcshowhidemine-show": "Bekiek",
        "rcshowhidemine-hide": "Verbarg",
        "rcshowhidecategorization": "$1 kategorisering van ziejen",
        "rcshowhidecategorization-show": "Bekiek",
        "rcshowhidecategorization-hide": "Verbarg",
-       "rclinks": "Bekiek de leste $1 wiezigingen van de aofgeleupen $2 dagen",
+       "rclinks": "Bekyk de lätste $1 wysigingen van de vöärbye $2 dagen",
        "diff": "verskil",
        "hist": "geskydenisse",
        "hide": "verbarg",
        "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",
-       "linkstoimage": "Disse holder wördt up de volgende {{PLURAL:$1|syde|$1 syden}} gebrüked:",
+       "filehist-comment": "Kommentaar",
+       "imagelinks": "Bestandsbruuk",
+       "linkstoimage": "Dit bestand wördt up de volgende {{PLURAL:$1|syde|$1 syden}} bruked:",
        "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.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Meer verwiezingen]] naor dit bestaand bekieken.",
        "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",
-       "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)",
+       "namespace": "Naamruumde:",
+       "invert": "Selekty ümmekeyren",
+       "tooltip-invert": "Vink dit vakjen an um wysigingen an syden binnen de eköäsen naamruumde te verbargen (en de bybehöyrende naamruumde as dat anvinked is)",
+       "namespace_association": "Bybehöyrende naamruumde",
+       "tooltip-namespace_association": "Vink dit vakjen an um ouk de oaverleg- en underwarpnaamruumde in te slüten dee by de selekteerde naamruumde höyret.",
+       "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-addsection": "Niej oonderwaerp tovogen",
+       "tooltip-ca-edit": "Bewark disse syde",
+       "tooltip-ca-addsection": "Ny underwarp 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-protect": "Beveilig disse ziede taegen veraanderen",
        "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-ca-nstab-category": "Loat de rubriekbladziede zeen",
+       "tooltip-ca-nstab-category": "Låt de kategorysyde seen",
        "tooltip-minoredit": "Markeer as n klaene wieziging",
        "tooltip-save": "Wiezigingen opsloan",
        "tooltip-preview": "Bekiek oew versie vuurda'j t opsloan (anbeveulen)!",
        "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",
        "thumbsize": "Grootte van de miniatuuraofbeelding:",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|zied|ziejen}}",
        "file-info": "Bestaandsgrootte: $1, MIME-type: $2",
-       "file-info-size": "$1 × $2 beeldpunten, bestaandsgrootte: $3, MIME-type: $4",
+       "file-info-size": "$1 × $2 bealdpunten, bestandsgroutde: $3, MIME-type: $4",
        "file-info-size-pages": "$1 × $2 beeldpunten, bestaandsgrootte: $3, MIME-type: $4, $5 {{PLURAL:$5|zied|ziejen}}",
        "file-nohires": "Gien hogere resolusie beschikbaor.",
        "svg-long-desc": "SVG-bestaand, uutgangsgrootte $1 × $2 beeldpunten, bestaandsgrootte: $3",
        "svg-long-desc-animated": "Bewegend SVG-bestaand, uutgangsgrootte $1 × $2 beeldpunten, bestaandsgrootte: $3",
        "svg-long-error": "Ongeldig SVG-bestaand: $1",
-       "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": "Oorsprungelik bestand",
+       "show-big-image-preview": "Groutde van disse weadergåve: $1.",
+       "show-big-image-other": "Andere {{PLURAL:$2|resoluty|resolutys}}: $1.",
+       "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 be73968..eb7b6d5 100644 (file)
        "nocreate-loggedin": "U hebt geen rechten om nieuwe pagina's te maken.",
        "sectioneditnotsupported-title": "Het is niet mogelijk om paragrafen te bewerken",
        "sectioneditnotsupported-text": "Het is op deze pagina niet mogelijk om paragrafen te bewerken.",
+       "modeleditnotsupported-title": "Bewerken wordt niet ondersteunt",
+       "modeleditnotsupported-text": "Het inhoudsmodel $1 kan niet worden bewerkt.",
        "permissionserrors": "Fouten in rechten",
        "permissionserrorstext": "U hebt geen rechten om dit te doen om de volgende {{PLURAL:$1|reden|redenen}}:",
        "permissionserrorstext-withaction": "U hebt geen toestemming om $2, {{PLURAL:$1|want}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Leeg object",
        "content-json-empty-array": "Lege reeks",
+       "unsupported-content-model": "<strong>Let op:</strong> het inhoudsmodel $1 wordt niet ondersteunt op deze wiki.",
+       "unsupported-content-diff": "Wijzigingen worden niet ondersteund door het inhoudsmodel $1.",
+       "unsupported-content-diff2": "Wijzigingen tussen de inhoudsmodellen $1 en $2 worden niet ondersteund op deze wiki.",
        "deprecated-self-close-category": "Pagina's met ongeldige zelfsluitende HTML-tags",
        "deprecated-self-close-category-desc": "De pagina bevat ongeldige zelf-afgesloten HTML-tags, zoals <code>&lt;b/&gt;</code> of <code>&lt;span/&gt;</code>. Het gedrag van deze tags zal binnenkort veranderd worden zodat dit overeenkomt met de HTML5-specificatie, dus het gebruik hiervan is verouderd en wordt afgeraden.",
        "duplicate-args-warning": "<strong>Waarschuwing:</strong> [[:$1]] roept [[:$2]] aan met meer dan één waarde voor de parameter \"$3\". Alleen de laatste waarde wordt gebruikt.",
        "backend-fail-contenttype": "Het inhoudstype van het bestand om in de opslag \"$1\" op te slaan kon niet bepaald worden.",
        "backend-fail-batchsize": "Taak met $1 {{PLURAL:$1|bestandshandeling|bestandshandelingen}} in het opslagbackend; de limiet is $2 {{PLURAL:$2|handeling|handelingen}}.",
        "backend-fail-usable": "Het was niet mogelijk naar het bestand $1 te schrijven of eruit te lezen vanwege onvoldoende rechten of niet-aanwezige mappen of containers.",
+       "backend-fail-stat": "Kan de bestandsstatus van \"$1\" niet lezen.",
+       "backend-fail-hash": "Kan de cryptografische hash van het bestand \"$1\" niet bepalen.",
        "filejournal-fail-dbconnect": "Het was niet mogelijk een verbinding te maken met de journaldatabase voor het opslagbackend \"$1\".",
        "filejournal-fail-dbquery": "Het was niet mogelijk de journaldatabase bij te werken voor het opslagbackend \"$1\".",
        "lockmanager-notlocked": "Het was niet mogelijk \"$1\" vrij te geven; dit object is niet vergrendeld.",
        "sessionfailure": "Er lijkt een probleem te zijn met uw aanmeldsessie.\nUw handeling is gestopt uit voorzorg tegen een beveiligingsrisico (dat bestaat uit mogelijke \"hijacking\" van deze sessie).\nProbeer het formulier opnieuw te versturen.",
        "changecontentmodel": "Inhoudsmodel van pagina bewerken",
        "changecontentmodel-legend": "Inhoudsmodel wijzigen",
-       "changecontentmodel-title-label": "Paginanaam",
+       "changecontentmodel-title-label": "Paginanaam:",
        "changecontentmodel-current-label": "Huidige inhoudsmodel:",
-       "changecontentmodel-model-label": "Nieuw inhoudsmodel",
+       "changecontentmodel-model-label": "Nieuw inhoudsmodel:",
        "changecontentmodel-reason-label": "Reden:",
        "changecontentmodel-submit": "Wijzigen",
        "changecontentmodel-success-title": "Het inhoudsmodel is gewijzigd",
        "block-log-flags-angry-autoblock": "uitgebreide automatische blokkade ingeschakeld",
        "block-log-flags-hiddenname": "gebruiker verborgen",
        "range_block_disabled": "De mogelijkheid voor beheerders om een groep IP-adressen te blokkeren is uitgeschakeld.",
-       "ipb-prevent-user-talk-edit": "Het bewerken van de eigen overlegpagina moet toegestaan zijn bij een gedeeltelijke blokkade, tenzij deze blokkade beperkingen oplegt aan het bewerken van de User Talk naamruimte.",
+       "ipb-prevent-user-talk-edit": "Het bewerken van de eigen overlegpagina moet worden toegestaan bij een gedeeltelijke blokkade, tenzij deze blokkade beperkingen oplegt aan het bewerken van de naamruimte \"Overleg gebruiker\".",
        "ipb_expiry_invalid": "Ongeldige duur.",
        "ipb_expiry_old": "Vervaldatum is in het verleden.",
        "ipb_expiry_temp": "Blokkades voor verborgen gebruikers moeten permanent zijn.",
index 4578b9c..8b9debd 100644 (file)
        "nospecialpagetext": "<strong>ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.</strong>\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "ߝߎ߬ߕߎ߲߬ߕߌ",
        "databaseerror": "ߓߟߏߡߟߊ ߟߎ߬ ߝߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
+       "databaseerror-textcl": "ߓߟߏߡߟߊߝߊ߲ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߓߘߊ߫ ߓߌ߬ߟߵߊ߬ ߘߐ߫.",
        "databaseerror-query": "ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ $1",
        "databaseerror-function": "ߗߋߘߊ $1",
        "databaseerror-error": "ߝߎ߬ߕߎ߲߬ߕߌ: $1",
        "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": "ߣߐ߰ߡߊ߲",
        "uploaddisabledtext": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߓߐ߫ ߣߴߊ߬ ߟߊ߫.",
        "php-uploaddisabledtext": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߓߐ߫ ߣߴߊ߬ ߟߊ߫ PHP ߘߐ߫.\nߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߝߛߍ߬ߝߛߍ߫ ߖߊ߰ߣߌ߲߬.",
        "upload-scripted-pi-callback": "ߊ߬ ߕߍߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ ߟߊ߫ XML-stylesheet ߟߐ߲ߣߌ߲ߦߊ ߘߊ߲ߘߊߟߌ.",
+       "uploadvirus": "ߓߊ߰ߙߋ߲ ߦߋ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊ߫߹\nߝߊߙߊ߲ߝߊ߯ߛߟߌ:$1",
        "upload-source": "ߞߐߕߐ߮ ߛߎ߲",
        "sourcefilename": "ߞߐߕߐ߮ ߕߐ߮ ߛߎ߲:",
        "sourceurl": "URL ߛߎ߲:",
index c257885..384e7ae 100644 (file)
        "backend-fail-contenttype": "Nie można określić typ zawartości pliku do przechowywania w \"$1\".",
        "backend-fail-batchsize": "Wewnętrzne funkcje magazynowania otrzymały $1 {{PLURAL:$1|operację|operacje|operacji}} na pliku; limit to $2 {{PLURAL:$2|operacja|operacje|operacji}}.",
        "backend-fail-usable": "Nie można zapisać lub czytać z pliku \"$1\" ze względu na niewystarczające uprawnienia lub brak katalogów/kontenerów.",
+       "backend-fail-stat": "Nie udało się odczytać statusu pliku „$1”.",
+       "backend-fail-hash": "Nie udało się zdefiniować hasha kryptograficznego pliku „$1”.",
        "filejournal-fail-dbconnect": "Nie można połączyć się z bazą danych dziennika dla backendu magazynowania \"$1\".",
        "filejournal-fail-dbquery": "Nie można zaktualizować bazy danych dziennika dla backendu magazynowania\"$1\".",
        "lockmanager-notlocked": "Nie można odblokować \"$1\", ponieważ nie jest on zablokowany.",
index f1622c5..ecc86bb 100644 (file)
        "history": "Version pì veje",
        "history_short": "Stòria",
        "history_small": "stòria",
-       "updatedmarker": "agiornà da l'ùltima vira che i son passà",
+       "updatedmarker": "agiornà da l'ùltima vira ch'it ses passà",
        "printableversion": "Version bon-a për stampé",
        "permalink": "Anliura fissa",
        "print": "Stampé",
        "ns-specialprotected": "As peulo nen modifichesse le pàgine speciaj.",
        "titleprotected": "La creassion ëd pàgine con ës tìtol-sì a l'é stàita proibìa da [[User:$1|$1]].\nComa rason a l'ha butà: <em>$2</em>.",
        "filereadonlyerror": "As peul pa modifichesse l'archivi «$1» përchè ël depòsit d'archivi «$2» a l'é an sola letura.\n\nL'aministrator ch'a l'ha blocalo a l'ha lassà sta spiegassion: «$3».",
+       "invalidtitle": "Cost tìtol a va nen bin!",
        "invalidtitle-knownnamespace": "Tìtol ch'a va nen bin con lë spassi nominal «$2» e ël test «$3»",
        "invalidtitle-unknownnamespace": "Tìtol pa bon con nùmer dë spassi nominal $1 e test «$2» sconossù",
        "exception-nologin": "Nen rintrà ant ël sistema",
        "password-change-forbidden": "A peul pa modifiché le ciav dzora a costa wiki.",
        "externaldberror": "Ò che a l'é rivaje n'eror con la base ëd dàit d'autenticassion esterna, ò pura a l'é chiel che a l'é nen autorisà a agiornesse sò cont estern.",
        "login": "Conession",
+       "login-security": "Verìfica toa identità!",
        "nav-login-createaccount": "Creé un cont o rintré ant ël sistema",
        "logout": "Seurte da 'nt ël sistema",
        "userlogout": "Dësconession",
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..3bcb1fd 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": "Показывать скрытые категории",
        "backend-fail-contenttype": "Не удалось определить тип содержимого файла, чтобы сохранить его в «$1».",
        "backend-fail-batchsize": "Хранилище получило блок из $1 {{PLURAL:$1|файловой операции|файловых операций}}, ограничение составляет $2 {{PLURAL:$1|файловую операцию|файловых операций|файловых операции}}.",
        "backend-fail-usable": "Не удалось прочитать или записать файл «$1» из-за нехватки прав или отсутствия нужных папок.",
+       "backend-fail-stat": "Не удалось прочитать статус файла «$1».",
+       "backend-fail-hash": "Может определить криптографический хеш файла «$1».",
        "filejournal-fail-dbconnect": "Не удалось подключиться к базе данных журнала для хранилища «$1».",
        "filejournal-fail-dbquery": "Не удалось обновить базу данных журнала для хранилища «$1».",
        "lockmanager-notlocked": "Не удалось разблокировать «$1»; он не заблокирован.",
        "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 894b29e..45da11d 100644 (file)
        "hidden-category-category": "لڪل زمرا",
        "category-subcat-count": "{{PLURAL:$2|ھن زمري ۾ رڳو ھيٺيون ذيلي زمرو آهي.|هن زمري ۾ ڪل $2 مان ھيٺيان {{PLURAL:$1|subcategory|$1 ذيلي زمرا}} آھن.}}",
        "category-subcat-count-limited": "هن زمري ۾ هيٺيان {{PLURAL:$1|ننڍا زمرا آهن|$1 subcategories}}.",
-       "category-article-count": "{{PLURAL:$2|Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ ØµØ±Ù\81 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 ØµÙ\81Ø­Ù\88 Ø¢Ù\87Ù\8a.|Ù\87Ù\8aÙºÙ\8aاÙ\86 {{PLURAL:$1|صÙ\81Ø­Ù\88 Ø¢Ù\87Ù\8a|$1 ØµÙ\81حا Ø¢Ù\87Ù\86}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾Ø\8c Ø³Ù\85Ù\88رÙ\86 $2 Ù\85اÙ\86.}}",
+       "category-article-count": "{{PLURAL:$2|Ù\87Ù\8a Ø²Ù\85رÙ\88 Ø±Ú³Ù\88 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 ØµÙ\81Ø­Ù\88 Ø±Ú©Ù\8a Ù¿Ù\88.|سÙ\85Ù\88رÙ\86 $2 Ù\85اÙ\86Ø\8c Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86\8aاÙ\86) {{PLURAL:$1|صÙ\81Ø­Ù\88|$1 ØµÙ\81حا}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ Ø¢Ú¾Ù\8a(Ø¢Ú¾Ù\86).}}",
        "category-article-count-limited": "هيٺِون {{PLURAL:$1|صفحو آهي|$1 صفحا آهن}} تازي زمري ۾.",
-       "category-file-count": "{{PLURAL:$2|Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ ØµØ±Ù\81 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\8a.|Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\8aا Ù\87Ù\8aÙºÙ\8aاÙ\86 {{PLURAL:$1|Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\8a|$1 Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\86}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾Ø\8c Ø³Ù\85Ù\88رÙ\86 $2 Ù\85اÙ\86.}}",
+       "category-file-count": "{{PLURAL:$2|Ù\87Ù\8a Ø²Ù\85رÙ\88 Ø±Ú³Ù\88 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ائÙ\8aÙ\84 Ø±Ú©Ù\8a Ù¿Ù\88.|سÙ\85Ù\88رÙ\86 $2 Ù\85اÙ\86Ø\8c Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86\8aاÙ\86) {{PLURAL:$1|Ù\81ائÙ\8aÙ\84|$1 Ù\81ائÙ\8aÙ\84Ù\8e}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ Ø¢Ú¾Ù\8a(Ø¢Ú¾Ù\86).}}",
        "category-file-count-limited": "هيٺيون يا هيٺيان {{PLURAL:$1|فائيل آهي|$1 فائيل آهن}} هن تازي زمري ۾.",
        "listingcontinuesabbrev": "جاري.",
        "index-category": "ڏسڻيل صفحا",
        "filerenameerror": "\"$1\" نالي فائيل تي نئون نالو \"$2\" رکجي نہ سگھجو.",
        "filedeleteerror": "\"$1\" فائيل ڊهي نہ سگھيو.",
        "directorycreateerror": "ڊائريڪٽري $1 جُڙي نہ سگھي.",
-       "directoryreadonlyerror": "ڊائريڪٽري $1 صرف پڙهي سگھجي ٿي.",
+       "directoryreadonlyerror": "ڊائريڪٽري \"$1\" رڳو-پڙھي-سگھجي-ٿي.",
        "directorynotreadableerror": "ڊائريڪٽري $1 پڙهي نہ ٿي سگھجي.",
        "filenotfound": "\"$1\" نالي فائيل لڀجي نہ سگھيو.",
        "unexpected": "غير متوقع قدر: \"$1\"=\"$2\".",
        "editingcomment": "(نئون ڀاڱو) $1 سنواريندي",
        "editconflict": "سنوار تڪرار: $1",
        "yourtext": "توهان جو متن",
-       "storedversion": "ساÙ\86Ú\8dÙ\8aÙ\84 Ù\85سÙ\88دÙ\88",
+       "storedversion": "ساÙ\86Ú\8dÙ\8aÙ\84 Ù\88رجاءÙ\8f",
        "yourdiff": "تفاوت",
        "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 ھيٺ ڏنل ڄاتيون وڃن ٿيون (تفصيلن لاءِ $1 ڏسندا).\nجيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻيءَ کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ ان کي هتي اماڻيو.<br />\nتوهان اسان سان اھو بہ وچن ڪريو ٿا تہ ھي توهان پاڻ لکيو آھي يا وري ڪنھن مفت وسيلي يا عوامي ڊومين تان نقل ڪيو آهي.\n<strong>حق-۽-واسطا-رکندڙ ڪم کان اجازت سواءِ نہ اماڻيو.</strong>",
        "copyrightwarning2": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيدارين کي ٻيا ڀاڱيدار سنواري، بدلائي، يا ڊاهي سگھن ٿا. جيڪڏهن اوهان نہ ٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريون وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو.</br>\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن اهڙي ئي مفت عوامي وسيلي تان ڪاپي ڪيو آهي. (تفصيلن لاءِ $1 ڏسندا).\n\n<strong>تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ بنان هتي جمع نہ ڪريو.</strong>",
        "nohistory": "هن صفحي جي ڪا بہ سوانح نہ آهي.",
        "currentrev": "تازو-ترين ورجاءُ",
        "currentrev-asof": "تازو-ترين ترين ورجاءُ بمطابق $1",
-       "revisionasof": "$1 Ù\88ارÙ\88 Ù¾Ø±Øª",
-       "revision-info": "$1 Ø¬Ù\88 {{GENDER:$6|$2}}$7 Ø¬Ù\8a Ø³Ù\86Ù\88ار Ø¨Ø¹Ø¯ Ù\85سÙ\88دÙ\88",
-       "previousrevision": "â\86\90اÚ\83ا Ù¾Ø±Ø§Ú»Ù\88 Ù¾Ø±Øª",
-       "nextrevision": "اÚ\83ا Ù\86ئÙ\88Ù\86 Ù¾Ø±Øªâ\86\92",
-       "currentrevisionlink": "هاڻوڪو پرت",
+       "revisionasof": "$1 Ù\88ارÙ\88 Ù\88رجاءÙ\8f",
+       "revision-info": "$1 Ø¬Ù\88 {{GENDER:$6|$2}}$7 Ø·Ø±Ù\81اÙ\86 Ù\88رجاءÙ\8f",
+       "previousrevision": "â\86\92 Ø§Ú\83ا Ù¾Ø±Ø§Ú»Ù\88 Ù\88رجاءÙ\8f",
+       "nextrevision": "اÚ\83ا Ù\86ئÙ\88Ù\86 Ù\88رجاءÙ\8f â\86\90",
+       "currentrevisionlink": "تازو-ترين ورجاءُ",
        "cur": "ھاڻوڪو",
        "next": "اڳيون",
        "last": "پويون",
        "page_first": "پھريون",
        "page_last": "آخري",
-       "history-fieldset-title": "Ù\85سÙ\88دا ڇاڻيو",
-       "history-show-deleted": "رڳو ڊاٺل مسودا",
+       "history-fieldset-title": "Ù\88رجاءÙ\8e ڇاڻيو",
+       "history-show-deleted": "رڳو ڊاھيل ورجاء",
        "histfirst": "اوائلي-ترين",
        "histlast": "نئون-ترين",
        "historysize": "({{PLURAL:$1|1 بائيٽ|$1 بائيٽون}})",
        "rev-deleted-user": "(واپرائيندڙ-نانءُ ڊاٿو ويو)",
        "rev-deleted-event": "(لاگ تفصيل هٽايا ويا)",
        "rev-deleted-user-contribs": "[واپرائيندڙ-نانءُ يا آءِپِي پتو مِٽايو ويو - ڀاڱيدارين مان سنوار لڪائي وئي]",
-       "rev-suppressed-no-diff": "تÙ\88Ù\87اÙ\86 Ø§Ù\87Ù\88 ØªÙ\81اÙ\88ت Ù\86ٿا Ú\8fسÙ\8a Ø³Ú¯Ú¾Ù\88Ø\8c Ú\87اڪاڻ ØªÛ\81 Ù\85سÙ\88دن مان ڪو ھڪ <strong>ڊاھيو ويو آھي</strong>.",
+       "rev-suppressed-no-diff": "تÙ\88Ù\87اÙ\86 Ø§Ù\87Ù\88 ØªÙ\81اÙ\88ت Ù\86ٿا Ú\8fسÙ\8a Ø³Ú¯Ú¾Ù\88Ø\8c Ú\87اڪاڻ ØªÛ\81 Ù\88رجائن مان ڪو ھڪ <strong>ڊاھيو ويو آھي</strong>.",
        "rev-delundel": "نمائش تبديل ڪريو",
        "rev-showdeleted": "ڏيکاريو",
        "revisiondelete": "ورجاءَ ڊاهيو/اڻ‌ڊاهيو",
        "diff-empty": "(ڪو بہ تفاوت ڪونھي)",
        "diff-multi-sameuser": "({{PLURAL:$1|هڪ وچولو ورجاءُ|$1 وچولا ورجاءَ}} ساڳي واپرائيندڙ طرفان ظاھر نہ ٿيندا)",
        "searchresults": "ڳولا نتيجا",
-       "search-filter-title-prefix": "صرÙ\81 انھن صفحن ۾ ڳوليندي جن جو عنوان \"$1\" سان شروع ٿي ٿو.",
+       "search-filter-title-prefix": "رڳÙ\88 انھن صفحن ۾ ڳوليندي جن جو عنوان \"$1\" سان شروع ٿي ٿو.",
        "search-filter-title-prefix-reset": "سڀ صفحا ڳوليو",
        "searchresults-title": "”$1“ لاءِ ڳولا نتيجا",
        "titlematches": "صفحي جو عنوان مشابھت رکي ٿو",
        "rcfilters-filter-logactions-label": "لاگڊ عمل",
        "rcfilters-filtergroup-lastrevision": "تازا-ترين ورجاءَ",
        "rcfilters-filter-lastrevision-label": "تازو-ترين ورجاءُ",
-       "rcfilters-filter-lastrevision-description": "ÚªÙ\86Ú¾Ù\86 ØµÙ\81Ø­Ù\8a Û¾ ØµØ±Ù\81 ØªØ§Ø²Ù\8a ترين تبديلي.",
+       "rcfilters-filter-lastrevision-description": "ÚªÙ\86Ú¾Ù\86 ØµÙ\81Ø­Ù\8a Û¾ Ø±Ú³Ù\88 ØªØ§Ø²Ù\8a-ترين تبديلي.",
        "rcfilters-filter-previousrevision-label": "تازو-ترين ورجاءُ نہ",
        "rcfilters-filter-previousrevision-description": "سڀ تبديليون جيڪي \"تازو-ترين ورجاءُ\" ناھن.",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:نہ</strong> $1",
        "nlinks": "$1 {{PLURAL:$1|ڳنڍڻو|ڳنڍڻا}}",
        "nmembers": "$1 {{PLURAL:$1|رڪن|رڪنَ}}",
        "nmemberschanged": "$1 → $2 {{PLURAL:$2|رڪن|رڪنَ}}",
-       "nrevisions": "$1 {{PLURAL:$1|Ù\85سÙ\88دÙ\88\85سÙ\88دا}}",
+       "nrevisions": "$1 {{PLURAL:$1|Ù\88رجاءÙ\8f\88رجاءÙ\8e}}",
        "nimagelinks": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "ntransclusions": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "specialpage-empty": "ھن رپورٽ لاءِ ڪي بہ نتيجا ناھن.",
        "protectedtitles": "تحفظيل عنوان",
        "protectedtitles-submit": "عنوان ڏيکاريو",
        "listusers": "واپرائيندڙن جي فهرست",
-       "listusers-editsonly": "صرÙ\81 سنوارن وارا واپرائيندڙ ڏيکاريو",
+       "listusers-editsonly": "رڳÙ\88 سنوارن وارا واپرائيندڙ ڏيکاريو",
        "listusers-temporarygroupsonly": "صرف عارضي واپرائيندڙ گروھن ۾ واپرائيندڙ ڏيکاريو",
        "listusers-creationsort": "سرجڻ جي تاريخ سان مرتب ڪريو",
        "listusers-desc": "گھٽجندڙ ترتيب ۾ مرتب ڪريو",
        "protect-default": "سڀ واپرائيندڙن کي اجازت ڏيو",
        "protect-fallback": "\"$1\" جي اجازت وارن واپرائيندڙن کي اجازت ڏيو",
        "protect-level-autoconfirmed": "خودڪار نموني پڪ ڪيل واپرائيندڙن کي اجازت ڏيو",
-       "protect-level-sysop": "صرÙ\81 Ù\85Ù\86تظÙ\85Ù\8aن کي اجازت ڏيو",
+       "protect-level-sysop": "رڳÙ\88 Ù\85Ù\86تظÙ\85ن کي اجازت ڏيو",
        "protect-summary-cascade": "تحفظ در تحفظ",
        "protect-expiry-indefinite": "لامحدود",
        "protect-cascade": "هن صفحي ۾ شامل صفحن کي تحفظيو (تحفظ در تحفظ)",
        "protect-existing-expiry": "موجوده پڄاڻي جو وقت: $3, $2",
        "protect-existing-expiry-infinity": "موجوده پڄاڻي جو وقت: لامحدود",
        "protect-otherreason-op": "ٻيو سبب",
-       "protect-expiry-options": "1 ڪلاڪ:1 hour,1 ڏينهن:1 day,1 هفتو:1 week,2 هفتو:2 weeks,1 مهينا:1 month,3 مهينا:3 months,6 مهينا:6 months,1 سال:1 year,اڻ کٽ:infinite",
+       "protect-expiry-options": "1 ڪلاڪ:1 hour,1 ڏينھن:1 day,1 هفتو:1 week,2 هفتو:2 weeks,1 مھينا:1 month,3 مھينا:3 months,6 مھينا:6 months,1 سال:1 year,اڻ-کٽ:infinite",
        "restriction-type": "اجازتنامو:",
        "restriction-level": "روڪ سطح:",
        "minimum-size": "ننڍي ماپ ۾",
        "restriction-level-autoconfirmed": "نيم تحفظيل",
        "undelete": "ڊاٺل صفحا ڏسو",
        "viewdeletedpage": "ڊاٺل صفحا ڏسو",
-       "undelete-nodiff": "ڪوبہ اڳيون مسودو نہ لڌو",
+       "undelete-nodiff": "ڪوبہ پويون ورجاءُ نہ لڌو.",
        "undeletebtn": "بحاليو",
        "undeleteviewlink": "ڏسو",
        "undeletecomment": "سبب:",
        "sp-contributions-userrights": "{{GENDER:$1|واپرائيندڙ}} حقن-جي سنڀال",
        "sp-contributions-search": "ڀاڱيدارين لاءِ ڳولا ڪريو",
        "sp-contributions-username": "آءِپي پتو يا واپرائيندڙ-نانءُ:",
-       "sp-contributions-toponly": "صرÙ\81 Ø§Ú¾Ù\8a Ø³Ù\86Ù\88ارÙ\88Ù\86 Ú\8fÙ\8aکارÙ\8aÙ\88 Ø¬Ù\8aÚªÙ\8a ØªØ§Ø²Ø§ ØªØ±Ù\8aÙ\86 Ù\85سÙ\88دا آھن",
+       "sp-contributions-toponly": "رڳÙ\88 Ø§Ú¾Ù\8a Ø³Ù\86Ù\88ارÙ\88Ù\86 Ú\8fÙ\8aکارÙ\8aÙ\88 Ø¬Ù\8aÚªÙ\8a ØªØ§Ø²Ø§-ترÙ\8aÙ\86 Ù\88رجاءÙ\8e آھن",
        "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صفحي سرجايون آھن",
        "sp-contributions-hideminor": "معمولي سنوارون لڪايو",
        "sp-contributions-submit": "ڳوليو",
        "movepagetext": "هيٺيون فارم استعمال ڪندي ڪنھن صفحي کي نئون عنوان ڏئي سگھجي ٿو، جنھن سان سمورو صفحو نئين عنوان ڏانھن هليو ويندو. \nاڳوڻو عنوان نئين عنوان ڏانھن چورڻو بڻجي ويندو. \nتوهان  چورڻن کي سنواري سگھو ٿا جيڪي اصل عنوان ڏانهن خودبخود اشارو ڪن ٿا.\nانهي ڳالھ جي پڪ ڪري وٺو تہ [[Special:BrokenRedirects|ٽٽل چورڻا]] يا [[Special:DoubleRedirects|ٻٽا چورڻا]] نہ هجن.\nان ڳالھ جي پڪ ڪرڻ ذميواري توهان تي آهي تہ ڳنڍڻا اتي ئي وٺي وڃن ٿا جتي انھن کي وٺي وڃڻ گھرجي.\n\nياد رکندا تہ جيڪڏهن نئين عنوان سان اڳي ئي ڪو مضمون موجود آهي ته پوءِ صفحو '''نہ''' چوريو ويندو، سواءِ ان جي تہ موجوده صفحو محظ خالي آهي يا ڪا بہ سوانح نہ رکندڙ ڪو چورڻو آهي.\n\n<strong>نوٽ!</strong>\nاها هڪ مقبول صفحي لاءِ ڪا غير متوقع ۽ انتھائي اڻوڻندڙ تبديلي ثابت ٿي سگھي ٿي؛ براءِ مھرباني اڳتي وڌڻ کان اڳ پڪ ڪندا تہ توهان اها تبديلي آڻڻ جي نتيجن کان چڱيءَ ريت واقف آهيو.",
        "movepagetext-noredirectfixer": "هيٺيون فارم استعمال ڪندي ڪنھن صفحي کي نئون عنوان ڏئي سگھجي ٿو، جنھن سان سمورو صفحو نئين عنوان ڏانھن هليو ويندو. \nاڳوڻو عنوان نئين عنوان ڏانھن چورڻو بڻجي ويندو. \nتوهان  چورڻن کي سنواري سگھو ٿا جيڪي اصل عنوان ڏانھن خودبخود اشارو ڪن ٿا.\nانهي ڳالھ جي پڪ ڪري وٺو تہ [[Special:BrokenRedirects|ٽٽل چورڻا]] يا [[Special:DoubleRedirects|ٻٽا چورڻا]] نہ هجن.\nان ڳالھ جي پڪ ڪرڻ ذميواري توهان تي آهي تہ ڳنڍڻا اتي ئي وٺي وڃن ٿا جتي انھن کي وٺي وڃڻ گھرجي.\n\nياد رکندا تہ جيڪڏهن نئين عنوان سان اڳي ئي ڪو مضمون موجود آهي ته پوءِ صفحو '''نہ''' چوريو ويندو، سوا ان جي تہ موجوده صفحو محظ خالي آهي يا ڪا بہ سوانح نہ رکندڙ ڪو چورڻو آهي.\n\n<strong>نوٽ!</strong>\nاها هڪ مقبول صفحي لاءِ ڪا غير متوقع ۽ انتھائي اڻوڻندڙ تبديلي ثابت ٿي سگھي ٿي؛ مھرباني ڪري اڳتي وڌڻ کان اڳ پڪ ڪندا تہ توهان اها تبديلي آڻڻ جي نتيجن کان چڱيءَ ريت واقف آهيو.",
        "movepagetalktext": "جيڪڏهن توهان هن خاني کي نشان لڳائيندئو، واسطيدار مباحثي صفحو پاڻ ئي چوريو ويندو ماسواءِ اتي ڪو اڳ ئي ڪو غيرخالي مباحثي صفحو موجود هجي.\n\nان صورت ۾، جيڪڏهن توهان چاهيو ته صفحي کي پاڻ چوري يا ضم ڪري سگھو ٿا.",
-       "movecategorypage-warning": "<strong>Ú\86تاءÙ\8f:</strong> Ø§Ù\88Ù\87اÙ\86 Ø²Ù\85رÙ\8a Ù\88ارÙ\8a ØµÙ\81Ø­Ù\8a Ú©Ù\8a Ú\86Ù\88رڻ Ù\88Ú\83Ù\8a Ø±Ù\87Ù\8aا Ø¢Ù\87Ù\8aÙ\88. Ù\8aاد Ø±Ú©Ù\88 ØµØ±Ù\81 ØµÙ\81Ø­Ù\88 Ú\86Ù\88رÙ\86دÙ\88Ø\8c Ø¬Ù\8aÚªÚ\8fÙ\87Ù\86 ÚªÙ\8a Ø¨Ù\87 ØµÙ\81حا Ù¾Ø±Ø§Ú»Ù\8a Ø²Ù\85رÙ\8a Û¾ Ø´Ø§Ù\85Ù\84 Ø¢Ù\87Ù\86Ø\8c Ø§Ù\86Ù\87Ù\86 Ø¬Ù\8a Ù\86ئÙ\8aÙ\86 Ø²Ù\85رÙ\8a Û¾ Ø¯Ø±Ø¬Ø§Ø¨Ù\86دÙ\8a <em>Ù\86Ù\87</em> Ù¿يندي.",
+       "movecategorypage-warning": "<strong>Ú\86تاءÙ\8f:</strong> Ø§Ù\88Ù\87اÙ\86 Ø²Ù\85رÙ\8a Ù\88ارÙ\8a ØµÙ\81Ø­Ù\8a Ú©Ù\8a Ú\86Ù\88رڻ Ù\88Ú\83Ù\8a Ø±Ù\87Ù\8aا Ø¢Ù\87Ù\8aÙ\88. Ù\8aاد Ø±Ú©Ù\88 Ø±Ú³Ù\88 ØµÙ\81Ø­Ù\88 Ú\86رÙ\86دÙ\88Ø\8c Ù¾Ø±Ø§Ú»Ù\8a Ø²Ù\85رÙ\8a Û¾ ÚªÙ\86 Ø¨Û\81 ØµÙ\81Ø­Ù\86 Ø¬Ù\8a Ù\86ئÙ\8aÙ\86 ØµÙ\81Ø­Ù\8a Û¾ Ù»Ù\8aھر-زÙ\85راڪارÙ\8a <em>Ù\86Û\81</em> ÚªØ¦Ù\8a Ù\88يندي.",
        "movenotallowed": "توهان کي صفحا چورڻ جي اجازت حاصل ڪانهي.",
        "movenotallowedfile": "توهان کي فائيلس چورڻ جي اجازت حاصل ڪانهي.",
        "newtitle": "نئون عنوان:",
        "redirect-value": "قدر:",
        "redirect-user": "واپرائيندڙ آءِڊي",
        "redirect-page": "صفحي جي آءِڊي",
-       "redirect-revision": "صÙ\81Ø­Ù\8a Ø¬Ù\88 Ù\85سÙ\88دÙ\88",
+       "redirect-revision": "صÙ\81Ø­Ù\8a Ø¬Ù\88 Ù\88رجاءÙ\8f",
        "redirect-file": "فائيل‌نانءُ",
        "fileduplicatesearch-filename": "فائيل‌نانءُ:",
        "fileduplicatesearch-submit": "ڳوليو",
        "htmlform-title-not-exists": "$1 وجود نٿو رکي.",
        "logentry-delete-delete": "$1 {{GENDER:$2|ڊاٿو}} صفحو $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|بحاليو}} صفحو $3 ($4)",
-       "logentry-delete-revision": "$1 $3: $4 ØµÙ\81Ø­Ù\8a ØªÙ\8a {{PLURAL:$5|Ú¾Úª Ù\85سÙ\88دÙ\8a|$5 Ù\85سÙ\88دن}} جي ظاھريت {{GENDER:$2|تبديل ڪئي}}",
+       "logentry-delete-revision": "$1 $3: $4 ØµÙ\81Ø­Ù\8a ØªÙ\8a {{PLURAL:$5|Ú¾Úª Ù\88رجاءÙ\8e|$5 Ù\88رجائن}} جي ظاھريت {{GENDER:$2|تبديل ڪئي}}",
        "revdelete-content-hid": "مواد لڪيل",
        "revdelete-uname-hid": "واپرائيندڙ-نانءُ لڪل",
        "revdelete-unrestricted": "منتظمن تان پابنديون ھٽايون ويون",
index a66ead3..a4c30f8 100644 (file)
        "backend-fail-contenttype": "Ne mogu da utvrdim kakav sadržaj ima datoteka koju treba da smestim u „$1“.",
        "backend-fail-batchsize": "Skladišna osnova je dobila blokadu od $1 {{PLURAL:$1|operacije|operacije|operacija}}; ograničenje je $2 {{PLURAL:$2|operacija|operacije|operacija}}.",
        "backend-fail-usable": "Ne mogu pročitati ni snimiti datoteku $1 zbog nedovoljno dozvola ili nedostattnih direktorija/sadržaoca.",
+       "backend-fail-stat": "Nisam mogao pročitati stanje datoteke \"$1\".",
+       "backend-fail-hash": "Nisam mogao odrediti kriptografsku tarabu datoteke \"$1\".",
        "filejournal-fail-dbconnect": "Ne mogu da se povežem s novinarskom bazom za skladišnu osnovu „$1“.",
        "filejournal-fail-dbquery": "Ne mogu da ažuriram novinarsku bazu za skladišnu osnovu „$1“.",
        "lockmanager-notlocked": "Ne mogu da otključam „$1“ jer nije zaključan.",
        "sessionfailure": "Izgleda da postoji problem sa vašom sesijom;\nova radnja je otkazana kao prevencija protiv napadanja sesija.\nMolimo ponovno pošaljite obrazac.",
        "changecontentmodel": "Promijeni model sadržaja stranice",
        "changecontentmodel-legend": "Promijeni model sadržaja",
-       "changecontentmodel-title-label": "Naslov stranice",
+       "changecontentmodel-title-label": "Naslov stranice:",
        "changecontentmodel-current-label": "Trenutni sadržajni model:",
-       "changecontentmodel-model-label": "Novi model sadržaja",
+       "changecontentmodel-model-label": "Novi model sadržaja:",
        "changecontentmodel-reason-label": "Razlog:",
        "changecontentmodel-submit": "Smijeni",
        "changecontentmodel-success-title": "Model sadržaja je promijenjen",
index be0253c..7cd6220 100644 (file)
        "nocreate-loggedin": "Za ustvarjanje novih strani nimate dovoljenja.",
        "sectioneditnotsupported-title": "Urejanje razdelkov ni podprto",
        "sectioneditnotsupported-text": "Urejanje razdelkov ni podprto na tej strani.",
+       "modeleditnotsupported-title": "Urejanje ni podprto",
+       "modeleditnotsupported-text": "Urejanje ni podprto za model vsebine $1.",
        "permissionserrors": "Napaka dovoljenja",
        "permissionserrorstext": "Za izvedbo dejanja nimate dovoljenja zaradi {{PLURAL:$1|naslednjega razloga|naslednjih razlogov}}:",
        "permissionserrorstext-withaction": "Za $2 zaradi {{PLURAL:$1|naslednjega razloga|naslednjih razlogov}} nimate dovoljenja:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Prazen objekt",
        "content-json-empty-array": "Prazno polje",
+       "unsupported-content-model": "<strong>Opozorilo:</strong> Model vsebine $1 ni podprt na tem wikiju.",
+       "unsupported-content-diff": "Primerjave niso podprte za model vsebine $1.",
+       "unsupported-content-diff2": "Primerjave med modeloma vsebine $1 in $2 niso podprte na tem wikiju.",
        "deprecated-self-close-category": "Strani, ki uporabljajo neveljavne samozaključljive oznake HTML",
        "deprecated-self-close-category-desc": "Stran uporablja neveljavne samozaključljive oznake HTML, kot sta <code>&lt;b/></code> ali <code>&lt;span/></code>. Njihovo vedenje se bo kmalu spremenilo, da bo v skladu s specifikacijo HTML5, zato njihova uporaba v wikibesedilu ni zaželena.",
        "duplicate-args-warning": "<strong>Opozorilo:</strong> [[:$1]] kliče [[:$2]] z več kot eno vrednostjo za parameter »$3«. Uporabili bomo samo zadnjo navedeno vrednost.",
        "backend-fail-contenttype": "Ne morem določiti vrsto vsebine datoteke za shranjevanje pri »$1«.",
        "backend-fail-batchsize": "Skladiščnemu zaledju je dana vrsta $1 {{PLURAL:$1|datotečne operacije|datotečnih operacij}}; omejitev {{PLURAL:$2|je $2 operacija|sta $2 operaciji|so $2 operacije|je $2 operacij}}.",
        "backend-fail-usable": "Ne morem prebrati ali zapisati datoteke »$1« zaradi nezadostnih dovoljenj ali manjkajočega imenika/vsebnika.",
+       "backend-fail-stat": "Ne moremo prebrati stanja datoteke »$1«.",
+       "backend-fail-hash": "Ne moremo določiti kriptografske zgoščene vrednosti datoteke »$1«.",
        "filejournal-fail-dbconnect": "Ne morem se povezati z listovno zbirko podatkov za skladiščno zaledje »$1«.",
        "filejournal-fail-dbquery": "Ne morem posodobiti listovne zbirke podatkov za skladiščno zaledje »$1«.",
        "lockmanager-notlocked": "Ne morem odkleniti »$1«, saj ni zaklenjeno.",
        "sessionfailure": "Zdi se, da z vašo sejo prijave obstaja težava;\nto dejanje smo preklicali, da bi preprečili morebitno ugrabitev seje. Prosimo, ponovno potrdite obrazec.",
        "changecontentmodel": "Spremeni model vsebine strani",
        "changecontentmodel-legend": "Spremeni model vsebine",
-       "changecontentmodel-title-label": "Naslov strani",
+       "changecontentmodel-title-label": "Naslov strani:",
        "changecontentmodel-current-label": "Trenutni model vsebine:",
-       "changecontentmodel-model-label": "Novi model vsebine",
+       "changecontentmodel-model-label": "Novi model vsebine:",
        "changecontentmodel-reason-label": "Razlog:",
        "changecontentmodel-submit": "Spremeni",
        "changecontentmodel-success-title": "Spremenili smo model vsebine",
index ca4c1d8..6f15977 100644 (file)
        "nocreate-loggedin": "Немате дозволу да правите нове странице.",
        "sectioneditnotsupported-title": "Уређивање одељка није подржано",
        "sectioneditnotsupported-text": "Уређивање одељка није подржано на овој страници.",
+       "modeleditnotsupported-title": "Уређивање није подржано",
+       "modeleditnotsupported-text": "Уређивање није подржано за модел садржаја $1.",
        "permissionserrors": "Грешка у дозволи",
        "permissionserrorstext": "Немате дозволу за ову радњу из {{PLURAL:$1|следећег|следећих}} разлога:",
        "permissionserrorstext-withaction": "Немате дозволу да $2 из {{PLURAL:$1|следећег|следећих}} разлога:",
        "editpage-notsupportedcontentformat-title": "Формат садржаја није подржан",
        "editpage-notsupportedcontentformat-text": "Формат садржаја $1 није подржан за модел садржаја $2.",
        "slot-name-main": "Главни",
-       "content-model-wikitext": "викитекста",
+       "content-model-wikitext": "викитекст",
        "content-model-text": "чистог текста",
        "content-model-javascript": "јаваскрипта",
        "content-model-css": "Це-Ес-Еса",
        "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 9f4edad..d9e6449 100644 (file)
        "editpage-notsupportedcontentformat-title": "Format sadržaja nije podržan",
        "editpage-notsupportedcontentformat-text": "Format sadržaja $1 nije podržan za model sadržaja $2.",
        "slot-name-main": "Glavni",
-       "content-model-wikitext": "vikiteksta",
+       "content-model-wikitext": "vikitekst",
        "content-model-text": "čistog teksta",
        "content-model-javascript": "JavaScript-a",
        "content-model-css": "CSS-a",
index f33f48b..5fe33ce 100644 (file)
        "nocreate-loggedin": "Du har inte behörighet att skapa nya sidor.",
        "sectioneditnotsupported-title": "Sektionsredigering stöds inte",
        "sectioneditnotsupported-text": "Sektionsredigering stöds inte på denna sida.",
+       "modeleditnotsupported-title": "Stöd för redigering saknas",
+       "modeleditnotsupported-text": "Stöd för att redigera innehållsmodellen $1 saknas.",
        "permissionserrors": "Behörighetsfel",
        "permissionserrorstext": "Du har inte behörighet att göra det du försöker göra, av följande {{PLURAL:$1|anledning|anledningar}}:",
        "permissionserrorstext-withaction": "Du har inte behörighet att $2, av följande {{PLURAL:$1|anledning|anledningar}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Tomt objekt",
        "content-json-empty-array": "Tomt fält",
+       "unsupported-content-model": "<strong>Varning:</strong> Innehållsmodellen $1 saknar stöd på denna wiki.",
+       "unsupported-content-diff": "Diffar saknar stöd för innehållsmodellen $1.",
+       "unsupported-content-diff2": "Diffar mellan innehållsmodellerna $1 och $2 saknar stöd på denna wiki.",
        "deprecated-self-close-category": "Sidor som använder ogiltiga självstängda HTML-taggar",
        "deprecated-self-close-category-desc": "Sidan använder ogiltiga självstängda HTML-taggar, som <code>&lt;b/></code> eller <code>&lt;span/></code>.  Beteendet för dessa kommer snart att ändras för att bli konsistent med HTML5-specifikationen, så dessa anses vara för föråldrade för att använda i wikitext.",
        "duplicate-args-warning": "<strong>Varning:</strong> [[:$1]] anropar [[:$2]] med mer än ett värde för parametern \"$3\". Endast det sista värdet kommer att användas.",
        "backend-fail-contenttype": "Kunde inte bestämma innehållstypen för filen att spara på \"$1\".",
        "backend-fail-batchsize": "Lagringssystemet gav en batch på $1 fil{{PLURAL:$1|operation|operationer}}; gränsen är $2 {{PLURAL:$2|operation|operationer}}.",
        "backend-fail-usable": "Kunde inte läsa eller skriva filen \"$1\" på grund av otillräckliga behörigheter eller saknade kataloger/containrar.",
+       "backend-fail-stat": "Kunde inte läsa status för filen \"$1\".",
+       "backend-fail-hash": "Kunde inte bestämma den kryptografiska hashvärdet för filen \"$1\".",
        "filejournal-fail-dbconnect": "Kunde inte ansluta till journaldatabasen för lagringssystemet \"$1\".",
        "filejournal-fail-dbquery": "Kunde inte uppdatera journaldatabasen för lagringssystemet \"$1\".",
        "lockmanager-notlocked": "Kunde inte låsa upp \"$1\"; den är inte låst.",
        "sessionfailure": "Någonting med din inloggningssession är på tok;\ndin begärda åtgärd har avbrutits för att förhindra att någon kapar din session.\nSkicka formuläret igen.",
        "changecontentmodel": "Ändra innehållsmodell för en sida",
        "changecontentmodel-legend": "Ändra innehållsmodell",
-       "changecontentmodel-title-label": "Sidtitel",
+       "changecontentmodel-title-label": "Sidtitel:",
        "changecontentmodel-current-label": "Nuvarande innehållsmodell:",
-       "changecontentmodel-model-label": "Ny innehållsmodell",
+       "changecontentmodel-model-label": "Ny innehållsmodell:",
        "changecontentmodel-reason-label": "Orsak:",
        "changecontentmodel-submit": "Ändra",
        "changecontentmodel-success-title": "Innehållsmodellen ändrades",
index 65274bf..d80d061 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",
        "talk": "Dyskusyjŏ",
        "views": "Widoki",
        "toolbox": "Nŏrzyńdzia",
+       "tool-link-emailuser": "Wyślij e-mail do {{GENDER:$1|tego używŏcza|tyj używŏczki|tego używŏcza}}",
        "imagepage": "Uobejrz zajta pliku",
        "mediawikipage": "Zajta komuńikata",
        "templatepage": "Zajta mustra",
        "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}}",
        "logouttext": "'''Terozki jeżeś wylůgowany'''.\n\nDej pozůr, co na ńykerych zajtach przeglůndarka może dali pokozywać co jeżeś zalůgowany, a bydźe tak aże uodśwjyżysz jeij cache.",
        "welcomeuser": "Witej, $1",
        "welcomecreation-msg": "Uotwarli my sam lo Ćebje kůnto.\nPamjyntej coby posztalować [[Special:Preferences|preferencyji]]",
-       "yourname": "Mjano użytkowńika:",
+       "yourname": "Miano ôd używŏcza:",
        "userlogin-yourname": "Miano używŏcza",
        "userlogin-yourname-ph": "Wkludź swoje miano używŏcza",
-       "createacct-another-username-ph": "Wszkryflej mjano użytkowńika",
+       "createacct-another-username-ph": "Wkludź miano ôd używŏcza",
        "yourpassword": "Hasło:",
        "userlogin-yourpassword": "Hasło",
        "userlogin-yourpassword-ph": "Wkludź swoje hasło",
        "loginsuccess": "'''Terozki jeżeś zalogowany do {{SITENAME}} kej \"$1\".'''",
        "nosuchuser": "Niy ma używŏcza ô mianie \"$1\".\nBadnij szrajbōng, abo [[Special:CreateAccount|sprŏw nowe kōnto]].",
        "nosuchusershort": "Ńy mo sam użytkowńika uo mjańe \"$1\".",
-       "nouserspecified": "Podej mjano użytkowńika.",
+       "nouserspecified": "Musisz podać miano ôd używŏcza.",
        "login-userblocked": "Tyn sprowjorz mo zawarte sprowjyńa. Ńy idźe śe zalogować.",
        "wrongpassword": "Hasło kere żeś naszkryfloł je felerne. Poprůbůj naszkryflać je jeszcze roz.",
        "wrongpasswordempty": "Hasło kere żeś podou je uostawjůne blank. Naszkryflej je jeszcze roz.",
        "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-summary": "WycůfaÅ\84y wersyji $1 naszkryflanej bez [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]])",
+       "undo-norev": "Edycyje niy idzie cŏfnōńć, bo ôna niy istniyje abo była wyciepniyntŏ.",
+       "undo-summary": "WycÅ\8ffanie wersyje $1 Ã´d [[Special:Contributions/$2|$2]] ([[User talk:$2|dyskusyjÅ\8f]])",
        "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",
        "nohistory": "Ta zajta ńy mo swojij historyje sprowjyń.",
        "revdelete-modify-no-access": "Feler przy zmjyńe widoczności wersyji $2, $1. Ńy mosz uprawńeń lo ńygo.",
        "revdelete-modify-missing": "Feler. Ńy mo tajli $1 w baźe.",
        "revdelete-no-change": "''''Dej pozůr''': element $2, $1 mo już ustawjonům widoczność.",
-       "revdelete-concurrent-change": "Feler. Pomjyno już element $2, $1. Prosza uoboczyć to w rejerze.",
+       "revdelete-concurrent-change": "Feler przi modyfikacyji elymyntu ze $2 $1: Wyglōndŏ na to, że jego status bōł zmiyniōny ôd kogoś w czasie Twojij roboty.\nWejzdrzij do regestu.",
        "revdelete-only-restricted": "Ńy do śe ukryć tajli $2, $1 przed administracyjom. Wybjer jydnom ze uopcyji.",
        "revdelete-reason-dropdown": "* Kůmyntorze lo wyćepańa\n** NPA\n** Prywatność",
        "revdelete-otherreason": "Inkszy/dodatkowy powůd:",
        "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)",
        "yourrealname": "Prawdźiwe mjano",
        "yourlanguage": "Godka interfejsu",
        "yournick": "Twoja szrajbka:",
-       "badsig": "Felerno szrajbka, sprawdź znaczńiki HTML.",
+       "badsig": "Felerny podpis, wejzdrzij na znaczniki HTML.",
        "badsiglength": "Twojo szrajbka je za dugo. Ji maksymalno dugość to $1 {{PLURAL:$1|buchsztaby|buchsztabůw}}",
        "yourgender": "Płeć:",
        "gender-unknown": "ńyznano",
        "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",
        "recentchangeslinked-page": "Miano strōny:",
        "recentchangeslinked-to": "Pokŏż zmiany na strōnach, co linkujōm do podanyj strōny",
        "upload": "Zaladuj zbiōr",
-       "uploadbtn": "Wćepej sam plik",
+       "uploadbtn": "Prziślij zbiōr",
        "reuploaddesc": "Nazod do formulařa uod wćepywańo.",
        "uploadnologin": "Ńy jest žeś zalogůwany",
        "uploadnologintext": "Muśyš śe [[Special:UserLogin|zalůgować]] ńim wćepńeš pliki.",
        "unusedtemplateswlh": "ku adresatu",
        "randompage": "Losowŏ strōna",
        "randompage-nopages": "We przestrzyńi mjan \"$1\" ńy ma żodnych zajtůw.",
-       "randomredirect": "Losowe překerowańy",
+       "randomredirect": "Losowe przekerowanie",
        "randomredirect-nopages": "We przestrzyńi mjan \"$1\" ńy ma przekerowań.",
        "statistics": "Statystyka",
        "statistics-header-pages": "Statystyka zajtůw",
        "usereditcount": "$1 {{PLURAL:$1|sprowjyńe|sprowjyńa|sprowjyń}}",
        "usercreated": "{{GENDER:$3|Utworzono}} $1 uo $2",
        "newpages": "Nowe strōny",
-       "newpages-username": "Mjano użytkowńika:",
+       "newpages-username": "Miano ôd używŏcza:",
        "ancientpages": "Nojstarše artikle",
        "move": "Przeniyś",
        "movethispage": "Přećepej ta zajta",
        "allpages": "Wszyjske strōny",
        "nextpage": "Nostympno zajta ($1)",
        "prevpage": "Popředńo zajta ($1)",
-       "allpagesfrom": "Zajty začynojůnce śe na:",
+       "allpagesfrom": "Strōny, co sie zaczynajōm ôd:",
        "allpagesto": "Zajty uo titlach kere na zadku majům:",
        "allarticles": "Wszyjske strōny",
        "allinnamespace": "Wszyjstke zajty (we przestrzyńi mjan $1)",
        "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",
        "mailnologin": "Brak adresu",
        "mailnologintext": "Muśyš śe [[Special:UserLogin|zalůgować]] i mjeć wpisany aktualny adres e-brif w swojich [[Special:Preferences|preferyncyjach]], coby můc wysuać e-brif do inkšygo užytkowńika.",
        "emailuser": "Poślij tymu używŏczowi e-mail",
+       "emailuser-title-target": "Wyślij e-mail do {{GENDER:$1|tego używŏcza|tyj używŏczki|tego używŏcza}}",
        "emailpagetext": "Możesz użyć půńiższygo formularza, coby wysłać wjadůmość e-brif do tygo użytkowńika.\nAdres e-brifa, kery zostoł bez Ćebje wkludzůny we [[Special:Preferences|Twojich sztalowańach]], pojawi śe we polu „Uod”, bez cůż uodbjorca bydźe můg Ći uodpedźeć.",
        "defemailsubject": "{{SITENAME}} - e-mail ôd używŏcza \"$1\"",
        "usermaildisabled": "E-mail ôd używŏcza je zastŏwiōny",
        "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>.",
        "protect-expiry-options": "2 godźiny:2 hours,1 dźyń:1 day,3 dńi:3 days,1 tydźyń:1 week,2 tygodńy:2 weeks,1 mjeśůnc:1 month,3 mjeśůnce:3 months,6 mjeśency:6 months,1 rok:1 year,ńyskůńčůny:infińite",
        "restriction-type": "Pozwolyńy:",
        "restriction-level": "Poźům:",
-       "minimum-size": "Min. wjelgość",
-       "maximum-size": "Maksymalno wjelgość",
-       "pagesize": "(bajtůw)",
+       "minimum-size": "Minimalnŏ srogość",
+       "maximum-size": "Maksymalnŏ srogość:",
+       "pagesize": "(bajtÅ\8dw)",
        "restriction-edit": "Edytuj",
        "restriction-move": "Pōnknij",
        "restriction-create": "Stwůř",
        "tooltip-invert": "Ôznŏcz te pole, coby skryć zmiany na strōnach we ôbranyj przestrzyni mian (i swiōnzanōm z niōm inkszōm przestrzyniōm mian, jeźli je ôznaczōnŏ)",
        "namespace_association": "Swiōnzanŏ przestrzyń mian",
        "tooltip-namespace_association": "Ôznŏcz te pole, coby przidać strōnã dyskusyje i tymat swiōnzane ze ôbranōm przestrzyniōm mian",
-       "blanknamespace": "(przodńo)",
+       "blanknamespace": "(Przodniŏ)",
        "contributions": "Wkłŏd ôd {{GENDER:$1|używŏcza|używŏczki}}",
        "contributions-title": "Wkłŏd {{GENDER:$1|używŏcza|używŏczki}} $1",
        "mycontris": "Edycyje",
        "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",
        "table_pager_empty": "Brak wynikůw",
        "autosumm-blank": "POZŮR! Usůńjyńće treśći (zajta pozostoła pusto)!",
        "autosumm-replace": "POZŮR! Zastůmpjyńy treśći hasua bardzo krůtkym tekstym: „$1”",
-       "autoredircomment": "Překerowańy do [[$1]]",
-       "autosumm-new": "Wćepano nowo zajta: \"$1\"",
+       "autoredircomment": "Przekerowanie do [[$1]]",
+       "autosumm-new": "Stworzōnŏ nowõ strōnã: \"$1\"",
        "lag-warn-normal": "Na tyj liśće zmjany nowsze jak {{PLURAL:$1|sekůnda|sekůnd}} můgům ńy być widoczne.",
        "lag-warn-high": "S kuli srogigo uobćůnżyńo serwerůw bazy danych, na tyj liśće zmjany nowše jak {{PLURAL:$1|sekůnda|sekůnd}} můgům ńy być widoczne.",
        "watchlistedit-normal-title": "Sprowjej lista zajtůw na kere dowom pozůr",
        "fileduplicatesearch-summary": "Šnupej za duplikatůma plika na podstawje wartośći fůnkcyji skrůtu.",
        "fileduplicatesearch-filename": "Mjano pliku:",
        "fileduplicatesearch-submit": "Šnupej",
-       "fileduplicatesearch-info": "$1 × $2 pikseli<br />Wjelgość plika: $3<br />Typ MIME: $4",
+       "fileduplicatesearch-info": "$1 × $2 pikselōw<br />Srogość zbioru: $3<br />Typ MIME: $4",
        "fileduplicatesearch-result-1": "Ńy ma duplikatu pliku „$1”.",
        "fileduplicatesearch-result-n": "We {{GRAMMAR:MS.lp|{{SITENAME}}}} {{PLURAL:$2|je dodatkowo kopia|sům $2 dodatkowe kopje|je $2 dodatkowych kopii}} plika „$1”.",
        "specialpages": "Ekstra strōny",
index e07ec43..df3af16 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 360f2ce..c593305 100644 (file)
@@ -45,7 +45,8 @@
                        "Leducthn",
                        "Nhatminh01",
                        "Leduyquang753",
-                       "Ioe2015"
+                       "Ioe2015",
+                       "Awdsweq123"
                ]
        },
        "tog-underline": "Gạch chân liên kết:",
        "sessionfailure": "Dường như có trục trặc với phiên đăng nhập của bạn; thao tác này đã bị hủy để tránh việc cướp quyền đăng nhập. Xin hãy gửi lại biểu mẫu.",
        "changecontentmodel": "Thay đổi kiểu nội dung của một trang",
        "changecontentmodel-legend": "Thay đổi kiểu nội dung",
-       "changecontentmodel-title-label": "Tên trang",
-       "changecontentmodel-model-label": "Kiểu nội dung mới",
+       "changecontentmodel-title-label": "Tên trang:",
+       "changecontentmodel-model-label": "Kiểu nội dung mới:",
        "changecontentmodel-reason-label": "Lý do:",
        "changecontentmodel-submit": "Thay đổi",
        "changecontentmodel-success-title": "Kiểu nội dung đã thay đổi",
index d167277..93db9ca 100644 (file)
        "nocreate-loggedin": "您没有权限创建新页面。",
        "sectioneditnotsupported-title": "段落编辑不支持",
        "sectioneditnotsupported-text": "本页面不支持段落编辑。",
+       "modeleditnotsupported-title": "不支持编辑",
+       "modeleditnotsupported-text": "内容模型$1不支持编辑。",
        "permissionserrors": "权限错误",
        "permissionserrorstext": "因为以下{{PLURAL:$1|原因}},您没有权限这样做:",
        "permissionserrorstext-withaction": "因为以下{{PLURAL:$1|原因}},您没有权限$2:",
        "sessionfailure": "似乎您的登录会话有问题;为了防止会话劫持,这个操作已经被取消。请重新提交表单。",
        "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": "内容模型已更改",
        "revdelete-uname-unhid": "公开用户名",
        "revdelete-restricted": "应用对管理员的限制",
        "revdelete-unrestricted": "删除对管理员的限制",
-       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},到期时间为$5 $6",
+       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},到期时间为$5$6",
        "logentry-block-unblock": "$1{{GENDER:$2|解封了}}{{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1将{{GENDER:$4|$3}}的封禁设置{{GENDER:$2|更改为}}持续时间$5 $6",
        "logentry-partialblock-block-page": "{{PLURAL:$1|页面|页面}}$2",
index bf524b6..dd764d1 100644 (file)
        "nocreate-loggedin": "您沒有權限建立新的頁面。",
        "sectioneditnotsupported-title": "不支援編輯章節",
        "sectioneditnotsupported-text": "此頁面不支援編輯章節。",
+       "modeleditnotsupported-title": "編輯不支援",
+       "modeleditnotsupported-text": "編輯不支援內容模型$1。",
        "permissionserrors": "權限錯誤",
        "permissionserrorstext": "由於下列{{PLURAL:$1|原因}},您沒有權限進行目前的動作:",
        "permissionserrorstext-withaction": "由於下列{{PLURAL:$1|原因}},您沒有權限進行$2的動作:",
        "editpage-invalidcontentmodel-title": "不支援的內容模型",
        "editpage-invalidcontentmodel-text": "不支援內容模型 \"$1\"。",
        "editpage-notsupportedcontentformat-title": "不支援此內容格式",
-       "editpage-notsupportedcontentformat-text": "內容語法 $2 不支援使用 $1 格式的內容。",
+       "editpage-notsupportedcontentformat-text": "內容模型 $2 不支援使用 $1 格式的內容。",
        "slot-name-main": "主頁",
        "content-model-wikitext": "Wikitext",
        "content-model-text": "純文字",
        "content-model-css": "CSS",
        "content-json-empty-object": "空物件",
        "content-json-empty-array": "空陣列",
+       "unsupported-content-model": "<strong>警告:</strong>內容模型$1在此 wiki 上不支援。",
+       "unsupported-content-diff": "Diffs 不支援內容模型$1。",
+       "unsupported-content-diff2": "在此 wiki 上不支援內容模型$1與$2兩者之間的 Diffs。",
        "deprecated-self-close-category": "使用無效 Self-closed HTML 標籤的頁面",
        "deprecated-self-close-category-desc": "頁面包含無效的 Self-closed HTML 標籤,如 <code>&lt;b/></code> or <code>&lt;span/></code>。這些標籤的模式將會更改為與 HTML5 規格一致,因此 wikitext 的這種用法已停用。",
        "duplicate-args-warning": "<strong>警告:</strong> [[:$1]] 呼叫 [[:$2]] 的 \"$3\" 參數使用了超過一次,僅會使用提供的最後一個參數值。",
        "backend-fail-contenttype": "無法辨識儲存於 \"$1\" 的檔案內容類型。",
        "backend-fail-batchsize": "儲存庫後端使用了 $1 個批次檔{{PLURAL:$2|操作}};系統限制為 $2 個{{PLURAL:$2|操作}}。",
        "backend-fail-usable": "由於權限不足或目錄/容器遺失,無法讀取或寫入檔案 \"$1\"。",
+       "backend-fail-stat": "無法讀取檔案「$1」的狀態。",
+       "backend-fail-hash": "無法確定檔案「$1」的加密雜湊。",
        "filejournal-fail-dbconnect": "無法連接到儲存庫後端 \"$1\" 的日誌資料庫。",
        "filejournal-fail-dbquery": "無法更新儲存庫後端 \"$1\" 的日誌資料庫。",
        "lockmanager-notlocked": "無法解除鎖定 \"$1\";該檔案並未被鎖定。",
        "sessionfailure": "您的登入連線階段似乎有問題,為了預防連線階段受到劫持攻擊,此動作已經被取消。請重新提交表單。",
        "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 cd77468..68a0380 100644 (file)
@@ -89,7 +89,7 @@
        "yourname": "用戶名稱:",
        "userlogin-yourname": "用戶名稱",
        "nav-login-createaccount": "登入/創造帳戶",
-       "wrongpassword": "您輸入的密碼有錯誤,請再試一次。",
+       "wrongpassword": "您輸入的用戶名或密碼有錯誤,請再試一次。",
        "pt-login": "登入",
        "pt-createaccount": "建立賬號",
        "botpasswords": "機械人密碼",
        "emailusername": "用戶名稱:",
        "wlshowhidebots": "機械人",
        "blanknamespace": "(主要)",
+       "contributions": "用戶貢獻",
        "blockip": "封鎖{{GENDER:$1|用戶}}",
        "empty-username": "沒有用戶名",
        "contribslink": "貢獻",
index 59706d5..4881086 100644 (file)
@@ -13,6 +13,7 @@
  * @author Ibrahim
  * @author Kaganer
  * @author Soroush
+ * @author ToJack
  * @author Urhixidur
  * @author לערי ריינהארט
  */
@@ -36,6 +37,263 @@ $namespaceNames = [
        NS_CATEGORY_TALK    => 'Баҳси_гурӯҳ',
 ];
 
+$specialPageAliases = [
+       'Activeusers'               => [ 'Корбарони_фаъол' ],
+       'Allmessages'               => [ 'Паёмҳои_системавӣ' ],
+       'AllMyUploads'              => [ 'Тамоми_парвандаҳои_ман' ],
+       'Allpages'                  => [ 'Тамоми_саҳифаҳо' ],
+       'Badtitle'                  => [ 'Номи_номусоид' ],
+       'Blankpage'                 => [ 'Саҳифаи_холӣ' ],
+       'Block'                     => [ 'Бастан' ],
+       'Booksources'               => [ 'Манобеи_китобҳо' ],
+       'BrokenRedirects'           => [ 'Саҳифаҳои_равонакунии_кандашуда' ],
+       'Categories'                => [ 'Гурӯҳҳо' ],
+       'ChangeEmail'               => [ 'Тағйири_почтаи_электронӣ' ],
+       'ChangePassword'            => [ 'Тағйири_гузарвожа' ],
+       'ComparePages'              => [ 'Муқоисаи_саҳафот' ],
+       'Confirmemail'              => [ 'Тасдиқи_почтаи_электронӣ' ],
+       'Contributions'             => [ 'Ҳиссагузориҳо' ],
+       'CreateAccount'             => [ 'Сохтани_ҳисоби_корбарӣ' ],
+       'Deadendpages'              => [ 'Саҳифаҳои_бемаъно' ],
+       'DeletedContributions'      => [ 'Саҳми_ҳазфшуда' ],
+       'Diff'                      => [ 'Тағйирот' ],
+       'DoubleRedirects'           => [ 'Саҳифаҳои_равонакунии_дукарата' ],
+       'EditWatchlist'             => [ 'Таҳрири_феҳристи_пайгириҳо' ],
+       'Emailuser'                 => [ 'Навиштани_мактуб_ба_корбар' ],
+       'ExpandTemplates'           => [ 'Густариши_шаблонҳо' ],
+       'Export'                    => [ 'Экспорт' ],
+       'Fewestrevisions'           => [ 'Камтарин_нусха' ],
+       'FileDuplicateSearch'       => [ 'Ҷустани_парвандаҳои_такрорӣ' ],
+       'Filepath'                  => [ 'Масири_парванда' ],
+       'Import'                    => [ 'Импорт' ],
+       'Invalidateemail'           => [ 'Қатъ_намудани_тасдиқоти_нишонаи_почтаи_электронӣ' ],
+       'JavaScriptTest'            => [ 'Тести_JavaScript' ],
+       'BlockList'                 => [ 'Феҳристи_басташудаҳо' ],
+       'LinkSearch'                => [ 'Ҷустани_пайвандҳо' ],
+       'Listadmins'                => [ 'Феҳристи_мудирон' ],
+       'Listbots'                  => [ 'Феҳристи_ботҳо' ],
+       'Listfiles'                 => [ 'Феҳристи_аксҳо' ],
+       'Listgrouprights'           => [ 'Феҳристи_гурӯҳҳои_корбарӣ' ],
+       'Listredirects'             => [ 'Феҳкристи_саҳифаҳои_равонакунӣ' ],
+       'ListDuplicatedFiles'       => [ 'Феҳристи_парвандаҳои_такрорӣ' ],
+       'Listusers'                 => [ 'Феҳристи_корбарон' ],
+       'Lockdb'                    => [ 'Қуфл_намудани_пойгоҳи_додаҳо' ],
+       'Log'                       => [ 'Гузоришҳо' ],
+       'Lonelypages'               => [ 'Саҳифаҳои_ятим' ],
+       'Longpages'                 => [ 'Саҳифаҳои_калон' ],
+       'MergeHistory'              => [ 'Идғоми_таърихча' ],
+       'MIMEsearch'                => [ 'Ҷустуҷӯи_MIME' ],
+       'Mostcategories'            => [ 'Сергурӯҳтарин_саҳафот' ],
+       'Mostimages'                => [ 'Серистифодашавандатарин_парвандаҳо' ],
+       'Mostinterwikis'            => [ 'Бештарин_миёнавики' ],
+       'Mostlinked'                => [ 'Истифодашавандатарин_саҳифаҳо' ],
+       'Mostlinkedcategories'      => [ 'Истифодашавандатарин_гурӯҳҳо' ],
+       'Mostlinkedtemplates'       => [ 'Истифодашавандатарин_шаблонҳо' ],
+       'Mostrevisions'             => [ 'Саҳифаҳо_бо_бештарин_нусха' ],
+       'Movepage'                  => [ 'Интиқоли_саҳифа' ],
+       'Mycontributions'           => [ 'Саҳми_ман' ],
+       'MyLanguage'                => [ 'Забони_ман' ],
+       'Mypage'                    => [ 'Саҳифаи_ман' ],
+       'Mytalk'                    => [ 'Баҳси_ман' ],
+       'Myuploads'                 => [ 'Парвандаҳои_фиристодаи_ман' ],
+       'Newimages'                 => [ 'Парвандаҳои_нав' ],
+       'Newpages'                  => [ 'Саҳифаҳои_нав' ],
+       'PasswordReset'             => [ 'Партофтани_гузарвожа' ],
+       'PermanentLink'             => [ 'Пайванди_доимӣ' ],
+       'Preferences'               => [ 'Танзимот' ],
+       'Prefixindex'               => [ 'Намои_пешвандӣ' ],
+       'Protectedpages'            => [ 'Саҳифаҳои_муфозатшуда' ],
+       'Protectedtitles'           => [ 'Номҳои_муҳофизатшуда' ],
+       'Randompage'                => [ 'Саҳифаи_тасодуфӣ' ],
+       'Randomredirect'            => [ 'Саҳифаи_равонакунии_тасодуфӣ' ],
+       'Recentchanges'             => [ 'Тағйироти_охирин' ],
+       'Recentchangeslinked'       => [ 'Вироишоти_вобаста' ],
+       'Revisiondelete'            => [ 'Вироишоти_ҳазфшуда' ],
+       'Search'                    => [ 'Ҷустуҷӯ' ],
+       'Shortpages'                => [ 'Саҳифаҳои_хурд' ],
+       'Specialpages'              => [ 'Саҳифаҳои_вижа' ],
+       'Statistics'                => [ 'Омор' ],
+       'Tags'                      => [ 'Барчасбҳо' ],
+       'Unblock'                   => [ 'Боз_кардан' ],
+       'Uncategorizedcategories'   => [ 'Гурӯҳҳои_бе_гурӯҳ' ],
+       'Uncategorizedimages'       => [ 'Парвандаҳои_бе_гурӯҳ' ],
+       'Uncategorizedpages'        => [ 'Саҳифаҳои_бе_гурӯҳ' ],
+       'Uncategorizedtemplates'    => [ 'Шаблонҳои_бе_гурӯҳ' ],
+       'Undelete'                  => [ 'Эҳёи_саҳифаи_ҳазфшуда' ],
+       'Unlockdb'                  => [ 'Боз_кардани_пойгоҳи_додаҳо' ],
+       'Unusedcategories'          => [ 'Гурӯҳҳои_истифоданашуда' ],
+       'Unusedimages'              => [ 'Парвандаҳои_истифоданашуда' ],
+       'Unusedtemplates'           => [ 'Шаблонҳои_истифоданашуда' ],
+       'Upload'                    => [ 'Боргузории_парванда' ],
+       'UploadStash'               => [ 'Боркунии_пинҳонӣ' ],
+       'Userlogin'                 => [ 'Вуруд' ],
+       'Userlogout'                => [ 'Хуруҷ' ],
+       'Userrights'                => [ 'Идораи_гурӯҳҳои_корбарӣ' ],
+       'Version'                   => [ 'Нусха' ],
+       'Wantedcategories'          => [ 'Гурӯҳҳҳои_дархостӣ' ],
+       'Wantedfiles'               => [ 'Парвандаҳои_дархостӣ' ],
+       'Wantedpages'               => [ 'Саҳифаҳои_дархостӣ' ],
+       'Wantedtemplates'           => [ 'Шаблонҳои_дархости' ],
+       'Watchlist'                 => [ 'Феҳристи_пайгириҳо' ],
+       'Whatlinkshere'             => [ 'Пайвандҳо_ба_инҷо' ],
+       'Withoutinterwiki'          => [ 'Бе_интервики' ],
+];
+
+$magicWords = [
+       'redirect'                  => [ '0', '#равона', '#REDIRECT' ],
+       'notoc'                     => [ '0', '__БЕ_ФЕҲРИСТ__', '__NOTOC__' ],
+       'nogallery'                 => [ '0', '__БЕ_НИГОРХОНА__', '__NOGALLERY__' ],
+       'forcetoc'                  => [ '0', '__БО_ФЕҲРИСТ__', '__FORCETOC__' ],
+       'toc'                       => [ '0', '__ФЕҲРИСТ__', '__TOC__' ],
+       'noeditsection'             => [ '0', '__БЕ_ВИРОИШИ_ҶУЗЪӢ__', '__NOEDITSECTION__' ],
+       'currentmonth'              => [ '1', 'МОҲИ_КУНУНӢ', 'МОҲИ_КУНУНӢ_2', 'CURRENTMONTH', 'CURRENTMONTH2' ],
+       'currentmonth1'             => [ '1', 'МОҲИ_КУНУНӢ_1', 'CURRENTMONTH1' ],
+       'currentmonthname'          => [ '1', 'НОМИ_МОҲИ_КУНУНӢ', 'CURRENTMONTHNAME' ],
+       'currentmonthnamegen'       => [ '1', 'НОМИ_МОҲИ_КУНУНӢ_ТАСРИФ', 'CURRENTMONTHNAMEGEN' ],
+       'currentmonthabbrev'        => [ '1', 'НОМИ_МОҲИ_КУНУНӢ_ИХТИСОР', 'CURRENTMONTHABBREV' ],
+       'currentday'                => [ '1', 'РӮЗИ_КУНУНӢ', 'CURRENTDAY' ],
+       'currentday2'               => [ '1', 'РӮЗИ_КУНУНИ_2', 'CURRENTDAY2' ],
+       'currentdayname'            => [ '1', 'НОМИ_РӮЗИ_КУНУНӢ', 'CURRENTDAYNAME' ],
+       'currentyear'               => [ '1', 'СОЛИ_КУНУНӢ', 'CURRENTYEAR' ],
+       'currenttime'               => [ '1', 'ЗАМОНИ_КУНУНӢ', 'CURRENTTIME' ],
+       'currenthour'               => [ '1', 'СОАТИ_КУНУНӢ', 'CURRENTHOUR' ],
+       'localmonth'                => [ '1', 'МОҲИ_МАҲАЛЛӢ', 'МОҲИ_МАҲАЛЛӢ_2', 'LOCALMONTH', 'LOCALMONTH2' ],
+       'localmonth1'               => [ '1', 'МОҲИ_МАҲАЛЛӢ_1', 'LOCALMONTH1' ],
+       'localmonthname'            => [ '1', 'НОМИ_МОҲИ_МАҲАЛЛӢ', 'LOCALMONTHNAME' ],
+       'localmonthnamegen'         => [ '1', 'НОМИ_МОҲИ_МАҲАЛЛӢ_ТАСРИФ', 'LOCALMONTHNAMEGEN' ],
+       'localmonthabbrev'          => [ '1', 'НОМИ_МОҲИ_МАҲАЛЛӢ_ИХТИСОР', 'LOCALMONTHABBREV' ],
+       'localday'                  => [ '1', 'РӮЗИ_МАҲАЛЛӢ', 'LOCALDAY' ],
+       'localday2'                 => [ '1', 'РӮЗИ_МАҲАЛЛӢ_2', 'LOCALDAY2' ],
+       'localdayname'              => [ '1', 'НОМИ_РӮЗИ_МАҲАЛЛӢ', 'LOCALDAYNAME' ],
+       'localyear'                 => [ '1', 'СОЛИ_МАҲАЛЛӢ', 'LOCALYEAR' ],
+       'localtime'                 => [ '1', 'ЗАМОНИ_МАҲАЛЛӢ', 'LOCALTIME' ],
+       'localhour'                 => [ '1', 'СОАТИ_МАҲАЛЛӢ', 'LOCALHOUR' ],
+       'numberofpages'             => [ '1', 'ШУМОРАИ_САҲИФАҲО', 'NUMBEROFPAGES' ],
+       'numberofarticles'          => [ '1', 'ШУМОРАИ_МАҚОЛАҲО', 'NUMBEROFARTICLES' ],
+       'numberoffiles'             => [ '1', 'ШУМОРАИ_ПАРВАНДАҲО', 'NUMBEROFFILES' ],
+       'numberofusers'             => [ '1', 'ШУМОРАИ_КОРБАРОН', 'NUMBEROFUSERS' ],
+       'numberofactiveusers'       => [ '1', 'ШУМОРАИ_КОРБАРОНИ_ФАЪОЛ', 'NUMBEROFACTIVEUSERS' ],
+       'numberofedits'             => [ '1', 'ШУМОРАИ_ВИРОИШОТ', 'NUMBEROFEDITS' ],
+       'pagename'                  => [ '1', 'НОМИ_САҲИФА', 'PAGENAME' ],
+       'pagenamee'                 => [ '1', 'НОМИ_САҲИФА_2', 'PAGENAMEE' ],
+       'namespace'                 => [ '1', 'ФАЗОИ_НОМ', 'NAMESPACE' ],
+       'namespacee'                => [ '1', 'ФАЗОИ_НОМ_2', 'NAMESPACEE' ],
+       'namespacenumber'           => [ '1', 'РАҚАМИ_ФАЗОИ_НОМ', 'NAMESPACENUMBER' ],
+       'talkspace'                 => [ '1', 'ФАЗОИ_БАҲСҲО', 'TALKSPACE' ],
+       'talkspacee'                => [ '1', 'ФАЗОИ_БАҲСҲО_2', 'TALKSPACEE' ],
+       'subjectspace'              => [ '1', 'ФАЗОИ_МАҚОЛАҲО', 'SUBJECTSPACE', 'ARTICLESPACE' ],
+       'subjectspacee'             => [ '1', 'ФАЗОИ_МАҚОЛАҲО_2', 'SUBJECTSPACEE', 'ARTICLESPACEE' ],
+       'fullpagename'              => [ '1', 'НОМИ_ПУРРАИ_САҲИФА', 'FULLPAGENAME' ],
+       'fullpagenamee'             => [ '1', 'НОМИ_ПУРРАИ_САҲИФА_2', 'FULLPAGENAMEE' ],
+       'subpagename'               => [ '1', 'НОМИ_ЗЕРГУРӮҲ', 'SUBPAGENAME' ],
+       'subpagenamee'              => [ '1', 'НОМИ_ЗЕРГУРӮҲ_2', 'SUBPAGENAMEE' ],
+       'basepagename'              => [ '1', 'АСОСИИ_НОМИ_САҲИФА', 'BASEPAGENAME' ],
+       'basepagenamee'             => [ '1', 'АСОСИИ_НОМИ_САҲИФА_2', 'BASEPAGENAMEE' ],
+       'talkpagename'              => [ '1', 'НОМИ_САҲИФАИ_БАҲС', 'TALKPAGENAME' ],
+       'talkpagenamee'             => [ '1', 'НОМИ_САҲИФАИ_БАҲС_2', 'TALKPAGENAMEE' ],
+       'subjectpagename'           => [ '1', 'НОМИ_САҲИФА_МАҚОЛА', 'SUBJECTPAGENAME', 'ARTICLEPAGENAME' ],
+       'subjectpagenamee'          => [ '1', 'НОМИ_САҲИФА_МАҚОЛА_2', 'SUBJECTPAGENAMEE', 'ARTICLEPAGENAMEE' ],
+       'msg'                       => [ '0', 'ПАЁМ:', 'MSG:' ],
+       'subst'                     => [ '0', 'МОНДАН:', 'SUBST:' ],
+       'safesubst'                 => [ '0', 'МОНДАНИҲИФЗ:', 'SAFESUBST:' ],
+       'msgnw'                     => [ '0', 'ПАЁМ_БЕ_ВИКИ:', 'MSGNW:' ],
+       'img_thumbnail'             => [ '1', 'мини', 'миниатюра', 'thumb', 'thumbnail' ],
+       'img_manualthumb'           => [ '1', 'мини=$1', 'миниатюра=$1', 'thumbnail=$1', 'thumb=$1' ],
+       'img_right'                 => [ '1', 'рост', 'right' ],
+       'img_left'                  => [ '1', 'чап', 'left' ],
+       'img_none'                  => [ '1', 'бе', 'none' ],
+       'img_width'                 => [ '1', '$1пкс', '$1px' ],
+       'img_center'                => [ '1', 'марказ', 'center', 'centre' ],
+       'img_framed'                => [ '1', 'чаҳорчӯба', 'рамка', 'frame', 'framed', 'enframed' ],
+       'img_frameless'             => [ '1', 'бе_чаҳорчӯба', 'бе_рамка', 'frameless' ],
+       'img_page'                  => [ '1', 'саҳифа=$1', 'саҳифа $1', 'page=$1', 'page $1' ],
+       'img_upright'               => [ '1', 'болорост', 'боло_рост=$1', 'болорост $1', 'upright', 'upright=$1', 'upright $1' ],
+       'img_border'                => [ '1', 'сарҳад', 'border' ],
+       'img_baseline'              => [ '1', 'асос', 'baseline' ],
+       'img_sub'                   => [ '1', 'поён', 'sub' ],
+       'img_super'                 => [ '1', 'боло', 'super', 'sup' ],
+       'img_top'                   => [ '1', 'аз_боло', 'top' ],
+       'img_text_top'              => [ '1', 'матнболо', 'text-top' ],
+       'img_middle'                => [ '1', 'дарбайн', 'middle' ],
+       'img_bottom'                => [ '1', 'дарпоён', 'bottom' ],
+       'img_text_bottom'           => [ '1', 'матнпоён', 'text-bottom' ],
+       'img_link'                  => [ '1', 'пайванд=$1', 'link=$1' ],
+       'img_alt'                   => [ '1', 'алт=$1', 'alt=$1' ],
+       'int'                       => [ '0', 'ДАРУН:', 'INT:' ],
+       'sitename'                  => [ '1', 'НОМИ_СОМОНА', 'НОМИ_САЙТ', 'SITENAME' ],
+       'ns'                        => [ '0', 'ПИ:', 'NS:' ],
+       'nse'                       => [ '0', 'ПИК:', 'NSE:' ],
+       'localurl'                  => [ '0', 'СУРОҒАИ_ЛОКАЛӢ:', 'LOCALURL:' ],
+       'localurle'                 => [ '0', 'СУРОҒАИ_ЛОКАЛӢ_2:', 'LOCALURLE:' ],
+       'articlepath'               => [ '0', 'МАСИРИ_САҲИФА', 'ARTICLEPATH' ],
+       'pageid'                    => [ '0', 'ИДЕНТИФИКАТОРИ_САҲИФА', 'PAGEID' ],
+       'server'                    => [ '0', 'СЕРВЕР', 'SERVER' ],
+       'servername'                => [ '0', 'НОМИ_СЕРВЕР', 'SERVERNAME' ],
+       'scriptpath'                => [ '0', 'МАСИРИ_СКРИПТ', 'SCRIPTPATH' ],
+       'stylepath'                 => [ '0', 'МАСИРИ_УСЛУБ', 'STYLEPATH' ],
+       'grammar'                   => [ '0', 'ТАСРИФ:', 'GRAMMAR:' ],
+       'gender'                    => [ '0', 'ҶИНС:', 'GENDER:' ],
+       'notitleconvert'            => [ '0', '__БЕ_ТАҒЙИРИ_САРЛАВҲА__', '__NOTITLECONVERT__', '__NOTC__' ],
+       'nocontentconvert'          => [ '0', '__БЕ_ТАҒЙИРИ_МАТН__', '__NOCONTENTCONVERT__', '__NOCC__' ],
+       'currentweek'               => [ '1', 'ХАФТАИ_КУНУНӢ', 'CURRENTWEEK' ],
+       'currentdow'                => [ '1', 'РӮЗИ_КУНУНИИ_ҲАФТА', 'CURRENTDOW' ],
+       'localweek'                 => [ '1', 'ҲАФТАИ_МАҲАЛЛӢ', 'LOCALWEEK' ],
+       'localdow'                  => [ '1', 'РУЗИ_ҲАФТАИ_МАҲАЛЛӢ', 'LOCALDOW' ],
+       'revisionid'                => [ '1', 'ИД_НУСХА', 'REVISIONID' ],
+       'revisionday'               => [ '1', 'РӮЗИ_НУСХА', 'REVISIONDAY' ],
+       'revisionday2'              => [ '1', 'РӮЗИ_НУСХА_2', 'REVISIONDAY2' ],
+       'revisionmonth'             => [ '1', 'МОҲИ_НУСХА', 'REVISIONMONTH' ],
+       'revisionmonth1'            => [ '1', 'МОҲИ_НУСХА_1', 'REVISIONMONTH1' ],
+       'revisionyear'              => [ '1', 'СОЛИ_НУСХА', 'REVISIONYEAR' ],
+       'revisiontimestamp'         => [ '1', 'НИШОНИ_ЗАМОНИ_НУСХА', 'REVISIONTIMESTAMP' ],
+       'revisionuser'              => [ '1', 'НУСХАИ_КОРБАР', 'REVISIONUSER' ],
+       'plural'                    => [ '0', 'ШАКЛИ_ҶАМЪ:', 'PLURAL:' ],
+       'fullurl'                   => [ '0', 'СУРОҒАИ_ПУРРА:', 'FULLURL:' ],
+       'fullurle'                  => [ '0', 'СУРОҒАИ_ПУРРА_2:', 'FULLURLE:' ],
+       'lcfirst'                   => [ '0', 'ҲАРФИ_АВВАЛ_ХУРД:', 'LCFIRST:' ],
+       'ucfirst'                   => [ '0', 'ҲАРФИ_АВВАЛ_КАЛОН:', 'UCFIRST:' ],
+       'lc'                        => [ '0', 'БО_ҲАРФҲОИ_ХУРД:', 'LC:' ],
+       'uc'                        => [ '0', 'БО_ҲАРФҲОИ_КАЛОН:', 'UC:' ],
+       'raw'                       => [ '0', 'ХОМ:', 'RAW:' ],
+       'displaytitle'              => [ '1', 'НАМОИШИ_САРЛАВҲА', 'DISPLAYTITLE' ],
+       'rawsuffix'                 => [ '1', 'Н', 'R' ],
+       'newsectionlink'            => [ '1', '__ПАЙВАНД_БА_ҚИСМАТИ_НАВ__', '__NEWSECTIONLINK__' ],
+       'nonewsectionlink'          => [ '1', '__БЕ_ПАЙВАНД_БА_ҚИСМАТИ_НАВ__', '__NONEWSECTIONLINK__' ],
+       'currentversion'            => [ '1', 'НУСХАИ_КУНУНӢ', 'CURRENTVERSION' ],
+       'urlencode'                 => [ '0', 'СУРОҒАИ_РАМЗ:', 'URLENCODE:' ],
+       'anchorencode'              => [ '0', 'РАМЗКУНИИ_БАРЧАСБ', 'ANCHORENCODE' ],
+       'currenttimestamp'          => [ '1', 'БАРЧАСБИ_ЗАМОНИ_КУНУНӢ', 'CURRENTTIMESTAMP' ],
+       'localtimestamp'            => [ '1', 'БАРЧАСБИ_ЗАМОНИ_МАҲАЛЛӢ', 'LOCALTIMESTAMP' ],
+       'directionmark'             => [ '1', 'МАСИРИ_ПАЁМ', 'DIRECTIONMARK', 'DIRMARK' ],
+       'language'                  => [ '0', '#ЗАБОН:', '#LANGUAGE:' ],
+       'contentlanguage'           => [ '1', 'ЗАБОНИ_МӮҲТАВО', 'CONTENTLANGUAGE', 'CONTENTLANG' ],
+       'pagesinnamespace'          => [ '1', 'САҲИФАҲО_ДАР_ФАЗОҲОИ_НОМ:', 'PAGESINNAMESPACE:', 'PAGESINNS:' ],
+       'numberofadmins'            => [ '1', 'ШУМОРАИ_МУДИРОН', 'NUMBEROFADMINS' ],
+       'formatnum'                 => [ '0', 'ФОРМАТИ_РАҚАМ', 'FORMATNUM' ],
+       'padleft'                   => [ '0', 'АЗ_ТАРАФИ_ЧАП', 'PADLEFT' ],
+       'padright'                  => [ '0', 'АЗ_ТАРАФИ_РОСТ', 'PADRIGHT' ],
+       'special'                   => [ '0', 'ВИЖА', 'special' ],
+       'defaultsort'               => [ '1', 'ТАРТИБ_БА_ТАВРИ_ПЕШФАРЗ:', 'КАЛИДИ_ТАРТИБ:', 'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DEFAULTCATEGORYSORT:' ],
+       'filepath'                  => [ '0', 'МАСИРИ_ПАРВАНДА:', 'FILEPATH:' ],
+       'tag'                       => [ '0', 'барчасб', 'тег', 'тэг', 'tag' ],
+       'hiddencat'                 => [ '1', '__ГУРӮҲИ_ПИНҲОН__', '__HIDDENCAT__' ],
+       'pagesincategory'           => [ '1', 'САҲИФА_ДАР_ГУРӮҲ', 'PAGESINCATEGORY', 'PAGESINCAT' ],
+       'pagesize'                  => [ '1', 'АНДОЗАИ_САҲИФА', 'PAGESIZE' ],
+       'index'                     => [ '1', '__ИНДЕКС__', '__INDEX__' ],
+       'noindex'                   => [ '1', '__БЕ_ИНДЕКС__', '__NOINDEX__' ],
+       'numberingroup'             => [ '1', 'РАҚАМ_ДАР_ГУРӮҲ', 'NUMBERINGROUP', 'NUMINGROUP' ],
+       'staticredirect'            => [ '1', '__РАВОНАИ_СТАТИСТИКӢ__', '__STATICREDIRECT__' ],
+       'protectionlevel'           => [ '1', 'ДАРАҶАИ_МУҲОФИЗАТ', 'PROTECTIONLEVEL' ],
+       'formatdate'                => [ '0', 'форматисана', 'formatdate', 'dateformat' ],
+       'url_path'                  => [ '0', 'МАСИР', 'PATH' ],
+       'url_wiki'                  => [ '0', 'ВИКИ', 'WIKI' ],
+       'url_query'                 => [ '0', 'ДАРХОСТ', 'QUERY' ],
+       'pagesincategory_all'       => [ '0', 'ҳама', 'all' ],
+       'pagesincategory_pages'     => [ '0', 'саҳифаҳо', 'pages' ],
+       'pagesincategory_subcats'   => [ '0', 'зергурӯҳҳо', 'subcats' ],
+       'pagesincategory_files'     => [ '0', 'аксҳо', 'files' ],
+];
+
 $datePreferences = [
        'default',
        'dmy',
index 130d1fb..6d6dbe5 100644 (file)
@@ -89,7 +89,11 @@ abstract class Maintenance {
        // Const for getStdin()
        const STDIN_ALL = 'all';
 
-       // Array of desired/allowed params
+       /**
+        * Array of desired/allowed params
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        protected $mParams = [];
 
        // Array of mapping short parameters to long ones
@@ -128,9 +132,17 @@ abstract class Maintenance {
         */
        protected $mBatchSize = null;
 
-       // Generic options added by addDefaultParams()
+       /**
+        * Generic options added by addDefaultParams()
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        private $mGenericParameters = [];
-       // Generic options which might or not be supported by the script
+       /**
+        * Generic options which might or not be supported by the script
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        private $mDependantParameters = [];
 
        /**
@@ -1235,6 +1247,7 @@ abstract class Maintenance {
         */
        protected function afterFinalSetup() {
                if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
+                       // @phan-suppress-next-line PhanUndeclaredConstant
                        call_user_func( MW_CMDLINE_CALLBACK );
                }
        }
@@ -1324,6 +1337,7 @@ abstract class Maintenance {
                        $res = $dbw->select( 'content', 'content_address', [], __METHOD__, [ 'DISTINCT' ] );
                        $blobStore = MediaWikiServices::getInstance()->getBlobStore();
                        foreach ( $res as $row ) {
+                               // @phan-suppress-next-line PhanUndeclaredMethod
                                $textId = $blobStore->getTextIdFromAddress( $row->content_address );
                                if ( $textId ) {
                                        $cur[] = $textId;
index b0ee966..8fb0d68 100644 (file)
@@ -39,7 +39,10 @@ class AddSite extends Maintenance {
         */
        public function execute() {
                $siteStore = MediaWikiServices::getInstance()->getSiteStore();
-               $siteStore->reset();
+               if ( method_exists( $siteStore, 'reset' ) ) {
+                       // @phan-suppress-next-line PhanUndeclaredMethod
+                       $siteStore->reset();
+               }
 
                $globalId = $this->getArg( 0 );
                $group = $this->getArg( 1 );
@@ -81,6 +84,7 @@ class AddSite extends Maintenance {
                $siteStore->saveSites( [ $site ] );
 
                if ( method_exists( $siteStore, 'reset' ) ) {
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $siteStore->reset();
                }
 
index bcf7023..6faeee8 100644 (file)
@@ -23,8 +23,6 @@
 
 require __DIR__ . '/../commandLine.inc';
 
-use Wikimedia\Rdbms\IMaintainableDatabase;
-
 /**
  * Maintenance script that upgrade for log_id/log_deleted fields in a
  * replication-safe way.
@@ -34,14 +32,14 @@ use Wikimedia\Rdbms\IMaintainableDatabase;
 class UpdateLogging {
 
        /**
-        * @var IMaintainableDatabase
+        * @var Database
         */
        public $dbw;
        public $batchSize = 1000;
        public $minTs = false;
 
        function execute() {
-               $this->dbw = $this->getDB( DB_MASTER );
+               $this->dbw = wfGetDB( DB_MASTER );
                $logging = $this->dbw->tableName( 'logging' );
                $logging_1_10 = $this->dbw->tableName( 'logging_1_10' );
                $logging_pre_1_10 = $this->dbw->tableName( 'logging_pre_1_10' );
index 558fec8..357bf1b 100644 (file)
@@ -59,6 +59,7 @@ class BenchmarkTidy extends Benchmarker {
                $min = $times[0];
                $max = end( $times );
                if ( $n % 2 ) {
+                       // @phan-suppress-next-line PhanTypeMismatchDimFetch
                        $median = $times[ ( $n - 1 ) / 2 ];
                } else {
                        $median = ( $times[$n / 2] + $times[$n / 2 - 1] ) / 2;
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 55ffcb8..62d6680 100644 (file)
@@ -58,6 +58,7 @@ class CheckLess extends Maintenance {
                        "$IP/tests/phpunit/phpunit.php",
                        "$IP/tests/phpunit/suites/LessTestSuite.php"
                ];
+               // @phan-suppress-next-line PhanUndeclaredMethod
                $textUICommand->run( $argv );
        }
 }
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 d255348..b4bfff0 100644 (file)
@@ -147,6 +147,7 @@ class CleanupUploadStash extends Maintenance {
        protected function doOperations( FileRepo $tempRepo, array $ops ) {
                $status = $tempRepo->getBackend()->doQuickOperations( $ops );
                if ( !$status->isOK() ) {
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $this->error( print_r( $status->getErrorsArray(), true ) );
                }
        }
index 2f0bcdf..3f55878 100644 (file)
@@ -144,6 +144,8 @@ class CompareParsers extends DumpIterator {
                        return;
                }
 
+               /** @var WikitextContent $content */
+               '@phan-var WikitextContent $content';
                $text = strval( $content->getText() );
 
                $output1 = $parser1->parse( $text, $title, $this->options );
index 3db0511..409ecdf 100644 (file)
@@ -168,10 +168,8 @@ class ConvertExtensionToRegistration extends Maintenance {
                                $this->fatalError( "Error: Closures cannot be converted to JSON. " .
                                        "Please move your extension function somewhere else."
                                );
-                       }
-                       // check if $func exists in the global scope
-                       if ( function_exists( $func ) ) {
-                               // @phan-suppress-next-next-line PhanTypeSuspiciousStringExpression
+                       } elseif ( function_exists( $func ) ) {
+                               // check if $func exists in the global scope
                                $this->fatalError( "Error: Global functions cannot be converted to JSON. " .
                                        "Please move your extension function ($func) into a class."
                                );
@@ -264,9 +262,8 @@ class ConvertExtensionToRegistration extends Maintenance {
                                        $this->fatalError( "Error: Closures cannot be converted to JSON. " .
                                                "Please move the handler for $hookName somewhere else."
                                        );
-                               }
-                               // Check if $func exists in the global scope
-                               if ( function_exists( $func ) ) {
+                               } elseif ( function_exists( $func ) ) {
+                                       // Check if $func exists in the global scope
                                        $this->fatalError( "Error: Global functions cannot be converted to JSON. " .
                                                "Please move the handler for $hookName inside a class."
                                        );
@@ -279,6 +276,10 @@ class ConvertExtensionToRegistration extends Maintenance {
                $this->json[$realName] = $value;
        }
 
+       /**
+        * @param string $realName
+        * @param array[] $value
+        */
        protected function handleResourceModules( $realName, $value ) {
                $defaults = [];
                $remote = $this->hasOption( 'skin' ) ? 'remoteSkinPath' : 'remoteExtPath';
index 02152f7..23c46bc 100644 (file)
@@ -117,6 +117,7 @@ class ConvertLinks extends Maintenance {
                }
 
                $res = $dbw->query( "SELECT l_from FROM $links LIMIT 1" );
+               // @phan-suppress-next-line PhanUndeclaredMethod
                if ( $dbw->fieldType( $res, 0 ) == "int" ) {
                        $this->output( "Schema already converted\n" );
 
index 1142325..ce40638 100644 (file)
@@ -358,6 +358,7 @@ class CopyFileBackend extends Maintenance {
                        // backends in FileBackendMultiWrite (since they get writes second, they have
                        // higher timestamps). However, when copying the other way, this hits loads of
                        // false positives (possibly 100%) and wastes a bunch of time on GETs/PUTs.
+                       // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                        $same = ( $srcStat['mtime'] <= $dstStat['mtime'] );
                } else {
                        // This is the slowest method which does many per-file HEADs (unless an object
index d010073..cb95b68 100644 (file)
@@ -66,7 +66,6 @@ class DeleteArchivedFiles extends Maintenance {
                                continue;
                        }
 
-                       /** @var LocalFile $file */
                        $file = $repo->newFile( $row->fa_name );
                        try {
                                $file->lock();
index 49fadaa..24e88bc 100644 (file)
@@ -109,6 +109,7 @@ class EraseArchivedFile extends Maintenance {
                                $this->output( "Deleted version '$key' ($ts) of file '$name'\n" );
                        } else {
                                $this->output( "Failed to delete version '$key' ($ts) of file '$name'\n" );
+                               // @phan-suppress-next-line PhanUndeclaredMethod
                                $this->output( print_r( $status->getErrorsArray(), true ) );
                        }
                } else {
index aef45bf..4c3fe7b 100644 (file)
@@ -132,7 +132,7 @@ class GenerateSitemap extends Maintenance {
        /**
         * A resource pointing to a sitemap file
         *
-        * @var resource
+        * @var resource|false
         */
        public $file;
 
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 c2c5ccf..cda16fe 100644 (file)
@@ -41,6 +41,7 @@ class BackupReader extends Maintenance {
        public $uploads = false;
        protected $uploadCount = 0;
        public $imageBasePath = false;
+       /** @var array|false */
        public $nsFilter = false;
 
        function __construct() {
@@ -210,6 +211,7 @@ TEXT
                        }
                        $this->uploadCount++;
                        // $this->report();
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $this->progress( "upload: " . $revision->getFilename() );
 
                        if ( !$this->dryRun ) {
index f5d9359..4065978 100644 (file)
@@ -357,7 +357,7 @@ class ImportImages extends Maintenance {
                                                # Protect the file
                                                $this->output( "\nWaiting for replica DBs...\n" );
                                                // Wait for replica DBs.
-                                               sleep( 2.0 ); # Why this sleep?
+                                               sleep( 2 ); # Why this sleep?
                                                wfWaitForSlaves();
 
                                                $this->output( "\nSetting image restrictions ... " );
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 04767fa..00d7c90 100644 (file)
@@ -64,7 +64,8 @@ class TextPassDumper extends BackupDumper {
 
        protected $bufferSize = 524288; // In bytes. Maximum size to read from the stub in on go.
 
-       protected $php = "php";
+       /** @var array */
+       protected $php = [];
        protected $spawn = false;
 
        /**
@@ -73,14 +74,14 @@ class TextPassDumper extends BackupDumper {
        protected $spawnProc = false;
 
        /**
-        * @var bool|resource
+        * @var resource
         */
-       protected $spawnWrite = false;
+       protected $spawnWrite;
 
        /**
-        * @var bool|resource
+        * @var resource
         */
-       protected $spawnRead = false;
+       protected $spawnRead;
 
        /**
         * @var bool|resource
@@ -96,6 +97,7 @@ class TextPassDumper extends BackupDumper {
        protected $firstPageWritten = false;
        protected $lastPageWritten = false;
        protected $checkpointJustWritten = false;
+       /** @var string[] */
        protected $checkpointFiles = [];
 
        /**
@@ -302,6 +304,7 @@ TEXT
                        $param = $split[1];
                }
                $fileURIs = explode( ';', $param );
+               $newFileURIs = [];
                foreach ( $fileURIs as $URI ) {
                        switch ( $val ) {
                                case "file":
@@ -431,7 +434,7 @@ TEXT
 
        /**
         * @throws MWException Failure to parse XML input
-        * @param string $input
+        * @param resource $input
         * @return bool
         */
        function readDump( $input ) {
@@ -808,11 +811,11 @@ TEXT
                if ( $this->spawnRead ) {
                        fclose( $this->spawnRead );
                }
-               $this->spawnRead = false;
+               $this->spawnRead = null;
                if ( $this->spawnWrite ) {
                        fclose( $this->spawnWrite );
                }
-               $this->spawnWrite = false;
+               $this->spawnWrite = null;
                if ( $this->spawnErr ) {
                        fclose( $this->spawnErr );
                }
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 ee1f59c..05688df 100644 (file)
@@ -88,7 +88,9 @@ class NukeNS extends Maintenance {
                                        $dbw->query( "DELETE FROM $tbl_pag WHERE page_id = $id" );
                                        $this->commitTransaction( $dbw, __METHOD__ );
                                        // Delete revisions as appropriate
+                                       /** @var NukePage $child */
                                        $child = $this->runChild( NukePage::class, 'nukePage.php' );
+                                       '@phan-var NukePage $child';
                                        $child->deleteRevisions( $revs );
                                        $this->purgeRedundantText( true );
                                        $n_deleted++;
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..5d6a19f 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' );
                }
@@ -151,6 +151,8 @@ class PopulateImageSha1 extends LoggedUpdateMaintenance {
                        }
                        // Upgrade the old file versions...
                        foreach ( $file->getHistory() as $oldFile ) {
+                               /** @var OldLocalFile $oldFile */
+                               '@phan-var OldLocalFile $oldFile';
                                $sha1 = $oldFile->getRepo()->getFileSha1( $oldFile->getPath() );
                                if ( strval( $sha1 ) !== '' ) { // file on disk and hashed properly
                                        if ( $isRegen && $oldFile->getSha1() !== $sha1 ) {
index f91a5b6..d1c71de 100644 (file)
@@ -125,7 +125,6 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance {
 
        /**
         * @param MediaWiki\Revision\RevisionStore $revStore
-        * @param string $emptySha1
         * @return int
         */
        protected function doSha1LegacyUpdates( $revStore ) {
index b9e084e..963bfec 100644 (file)
@@ -87,6 +87,8 @@ class PreprocessDump extends DumpIterator {
                if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) {
                        return;
                }
+               /** @var WikitextContent $content */
+               '@phan-var WikitextContent $content';
 
                try {
                        $this->mPreprocessor->preprocessToObj( strval( $content->getText() ), 0 );
index e57e977..68fb643 100644 (file)
@@ -72,7 +72,7 @@ class PPFuzzTester {
                                $passed = 'passed';
                        } catch ( Exception $e ) {
                                $testReport = self::$currentTest->getReport();
-                               $exceptionReport = $e->getText();
+                               $exceptionReport = $e instanceof MWException ? $e->getText() : (string)$e;
                                $hash = md5( $testReport );
                                file_put_contents( "results/ppft-$hash.in", serialize( self::$currentTest ) );
                                file_put_contents( "results/ppft-$hash.fail",
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 dfce202..bfbee9b 100644 (file)
@@ -76,7 +76,7 @@ class ImageBuilder extends Maintenance {
        }
 
        /**
-        * @return FileRepo
+        * @return LocalRepo
         */
        function getRepo() {
                if ( !isset( $this->repo ) ) {
@@ -203,7 +203,8 @@ class ImageBuilder extends Maintenance {
                                $filename = $altname;
                                $this->output( "Estimating transcoding... $altname\n" );
                        } else {
-                               # @todo FIXME: create renameFile()
+                               // @fixme create renameFile()
+                               // @phan-suppress-next-line PhanUndeclaredMethod See comment above...
                                $filename = $this->renameFile( $filename );
                        }
                }
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 d8a8808..060f327 100644 (file)
@@ -106,7 +106,9 @@ class CheckStorage {
                                        [],
                                        [ 'content' => [ 'INNER JOIN', [ 'content_id = slot_content_id' ] ] ]
                                );
+                               /** @var \MediaWiki\Storage\SqlBlobStore $blobStore */
                                $blobStore = MediaWikiServices::getInstance()->getBlobStore();
+                               '@phan-var \MediaWiki\Storage\SqlBlobStore $blobStore';
                                foreach ( $res as $row ) {
                                        $textId = $blobStore->getTextIdFromAddress( $row->content_address );
                                        if ( $textId ) {
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 60f88ba..6a04b98 100644 (file)
@@ -38,7 +38,7 @@ class OrphanStats extends Maintenance {
                        "Show some statistics on the blob_orphans table, created with trackBlobs.php" );
        }
 
-       protected function &getDB( $cluster, $groups = [], $wiki = false ) {
+       protected function getDB( $cluster, $groups = [], $wiki = false ) {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lb = $lbFactory->getExternalLB( $cluster );
 
index 92b6679..316d2d2 100644 (file)
@@ -710,7 +710,7 @@ class CgzCopyTransaction {
        /** @var RecompressTracked */
        public $parent;
        public $blobClass;
-       /** @var ConcatenatedGzipHistoryBlob */
+       /** @var ConcatenatedGzipHistoryBlob|false */
        public $cgz;
        public $referrers;
 
index a3534f8..94ec207 100755 (executable)
@@ -27,7 +27,6 @@
 
 require_once __DIR__ . '/Maintenance.php';
 
-use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\Rdbms\DatabaseSqlite;
 
 /**
@@ -160,7 +159,8 @@ class UpdateMediaWiki extends Maintenance {
                $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
                $this->output( "Going to run database updates for $dbDomain\n" );
                if ( $db->getType() === 'sqlite' ) {
-                       /** @var IMaintainableDatabase|DatabaseSqlite $db */
+                       /** @var DatabaseSqlite $db */
+                       '@phan-var DatabaseSqlite $db';
                        $this->output( "Using SQLite file: '{$db->getDbFilePath()}'\n" );
                }
                $this->output( "Depending on the size of your database this may take a while!\n" );
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..18c71a3 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" );
                }
 
@@ -34,6 +34,7 @@ class UpdateExtensionJsonSchema extends Maintenance {
                while ( $json['manifest_version'] !== ExtensionRegistry::MANIFEST_VERSION ) {
                        $json['manifest_version'] += 1;
                        $func = "updateTo{$json['manifest_version']}";
+                       // @phan-suppress-next-line PhanUndeclaredMethod
                        $this->$func( $json );
                }
 
index 5f7f9d5..038ef23 100644 (file)
@@ -36,7 +36,7 @@ use Wikimedia\Rdbms\IMaintainableDatabase;
  */
 class UserDupes {
        /**
-        * @var IMaintainableDatabase
+        * @var Database
         */
        private $db;
        private $reassigned;
index 00046d3..544c071 100644 (file)
@@ -95,8 +95,10 @@ class WrapOldPasswords extends Maintenance {
                                $user = User::newFromId( $row->user_id );
                                /** @var ParameterizedPassword $password */
                                $password = $passwordFactory->newFromCiphertext( $row->user_password );
+                               '@phan-var ParameterizedPassword $password';
                                /** @var LayeredParameterizedPassword $layeredPassword */
                                $layeredPassword = $passwordFactory->newFromType( $layeredType );
+                               '@phan-var LayeredParameterizedPassword $layeredPassword';
                                $layeredPassword->partialCrypt( $password );
 
                                $updateUsers[] = $user;
index 4c8880c..415cabd 100644 (file)
                        } );
        };
 
+       // Skeleton user object, extended by the 'mediawiki.user' module.
+       /**
+        * @class mw.user
+        * @singleton
+        */
+       mw.user = {
+               /**
+                * @property {mw.Map}
+                */
+               options: new mw.Map(),
+               /**
+                * @property {mw.Map}
+                */
+               tokens: new mw.Map()
+       };
+
        // Alias $j to jQuery for backwards compatibility
        // @deprecated since 1.23 Use $ or jQuery instead
        mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
index ae8ac5f..eccc5df 100644 (file)
@@ -259,7 +259,7 @@ ItemModel.prototype.getIdentifiers = function () {
  * @return {boolean}
  */
 ItemModel.prototype.isHighlightSupported = function () {
-       return !!this.getCssClass();
+       return !!this.getCssClass() && !OO.ui.isMobile();
 };
 
 /**
index 1a5ae6c..f92685b 100644 (file)
@@ -101,7 +101,7 @@ ChangesListWrapperWidget.prototype.onModelUpdate = function (
                isEmpty = $changesListContent === 'NO_RESULTS',
                // For enhanced mode, we have to load these modules, which are
                // not loaded for the 'regular' mode in the backend
-               loaderPromise = mw.user.options.get( 'usenewrc' ) ?
+               loaderPromise = mw.user.options.get( 'usenewrc' ) && !OO.ui.isMobile() ?
                        mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
                        $.Deferred().resolve(),
                widget = this;
index 710bd65..7ac981b 100644 (file)
@@ -20,6 +20,7 @@ ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
        controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
 ) {
        var layout,
+               $widgetRow,
                classes = [],
                $label = $( '<div>' )
                        .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
@@ -93,29 +94,34 @@ ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
        // defaults on 'click' as well.
        layout.$label.on( 'click', false );
 
-       this.$element
-               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
-               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+       $widgetRow = $( '<div>' )
+               .addClass( 'mw-rcfilters-ui-table' )
                .append(
                        $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-table' )
+                               .addClass( 'mw-rcfilters-ui-row' )
                                .append(
                                        $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-row' )
-                                               .append(
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
-                                                               .append( layout.$element ),
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
-                                                               .append( this.excludeLabel.$element ),
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
-                                                               .append( this.highlightButton.$element )
-                                               )
+                                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
+                                               .append( layout.$element )
                                )
                );
 
+       if ( !OO.ui.isMobile() ) {
+               $widgetRow.find( '.mw-rcfilters-ui-row' ).append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
+                               .append( this.excludeLabel.$element ),
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
+                               .append( this.highlightButton.$element )
+               );
+       }
+
+       this.$element
+               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
+               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+               .append( $widgetRow );
+
        if ( this.itemModel.getIdentifiers() ) {
                this.itemModel.getIdentifiers().forEach( function ( ident ) {
                        classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
index a4ee488..21d95df 100644 (file)
@@ -37,8 +37,8 @@
                        hash ^= str.charCodeAt( i );
                }
 
-               hash = ( hash >>> 0 ).toString( 36 );
-               while ( hash.length < 7 ) {
+               hash = ( hash >>> 0 ).toString( 36 ).slice( 0, 5 );
+               while ( hash.length < 5 ) {
                        hash = '0' + hash;
                }
                /* eslint-enable no-bitwise */
 
                                                // In addition to currReqBase, doRequest() will also add 'modules' and 'version'.
                                                // > '&modules='.length === 9
-                                               // > '&version=1234567'.length === 16
-                                               // > 9 + 16 = 25
-                                               currReqBaseLength = makeQueryString( currReqBase ).length + 25;
+                                               // > '&version=12345'.length === 14
+                                               // > 9 + 14 = 23
+                                               currReqBaseLength = makeQueryString( currReqBase ).length + 23;
 
                                                // We may need to split up the request to honor the query string length limit,
                                                // so build it piece by piece.
                                        }() )
                                }
                        };
-               }() ),
-
-               // Skeleton user object, extended by the 'mediawiki.user' module.
-               /**
-                * @class mw.user
-                * @singleton
-                */
-               user: {
-                       /**
-                        * @property {mw.Map}
-                        */
-                       options: new Map(),
-                       /**
-                        * @property {mw.Map}
-                        */
-                       tokens: new Map()
-               }
-
+               }() )
        };
 
        // Attach to window and globally alias
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..41c65b2 100644 (file)
@@ -10,6 +10,7 @@ use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
  * @since 1.18
@@ -182,8 +183,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 +587,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,14 +663,17 @@ 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 );
                }
 
+               // If anything faked the time, reset it
+               ConvertibleTimestamp::setFakeTime( false );
+
                parent::tearDown();
        }
 
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 2a21351..6b4b4d4 100644 (file)
@@ -7,7 +7,10 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
        // Version hash for a blank file module.
        // Result of ResourceLoader::makeHash(), ResourceLoaderTestModule
        // and ResourceLoaderFileModule::getDefinitionSummary().
-       const BLANK_VERSION = '09p30q0';
+       const BLANK_VERSION = '9p30q';
+       // Result of ResoureLoader::makeVersionQuery() for a blank file module.
+       // In other words, result of ResourceLoader::makeHash( BLANK_VERSION );
+       const BLANK_COMBI = 'rbml8';
 
        /**
         * @param array|string $options Language code or options array
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 );
index 6520fc5..aa6e494 100644 (file)
@@ -2606,7 +2606,7 @@ class OutputPageTest extends MediaWikiTestCase {
                        [
                                [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
                                "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
-                                       . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
+                                       . "mw.loader.implement(\"test.quux@1ev0i\",function($,jQuery,require,module){"
                                        . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
                                        . "\"]});});</script>"
                        ],
@@ -2669,6 +2669,9 @@ class OutputPageTest extends MediaWikiTestCase {
 
                $op = TestingAccessWrapper::newFromObject( $op );
                $op->rlExemptStyleModules = $exemptStyleModules;
+               $expect = strtr( $expect, [
+                       '{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
+               ] );
                $this->assertEquals(
                        $expect,
                        strval( $op->buildExemptModules() )
@@ -2695,7 +2698,7 @@ class OutputPageTest extends MediaWikiTestCase {
                                'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
                                '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
                                '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
+                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=15pue"/>',
                        ],
                        'custom modules' => [
                                'exemptStyleModules' => [
@@ -2705,8 +2708,8 @@ class OutputPageTest extends MediaWikiTestCase {
                                '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
                                '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.site.a%2Cb&amp;only=styles"/>' . "\n" .
                                '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;version=0a56zyi"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
+                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;version={blankCombi}"/>' . "\n" .
+                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=15pue"/>',
                        ],
                ];
                // phpcs:enable
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() );
+       }
+
+}
index 7d301a9..c0ff44b 100644 (file)
@@ -197,4 +197,46 @@ class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
                return [ 'slot_revision_id' => $revId ];
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        * @throws \MWException
+        */
+       public function testNewRevisionsFromBatch_error() {
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'b-ä';
+               /** @var Revision $rev1 */
+               $rev1 = $page->doEditContent(
+                       new WikitextContent( $text . '1' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $invalidRow = $this->revisionToRow( $rev1 );
+               $invalidRow->rev_id = 100500;
+               $result = MediaWikiServices::getInstance()->getRevisionStore()
+                       ->newRevisionsFromBatch(
+                               [ $this->revisionToRow( $rev1 ), $invalidRow ],
+                               [
+                                       'slots' => [ SlotRecord::MAIN ],
+                                       'content' => true
+                               ]
+                       );
+               $this->assertFalse( $result->isGood() );
+               $this->assertNotEmpty( $result->getErrors() );
+               $records = $result->getValue();
+               $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+               $this->assertSame( $text . '1',
+                       $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertEquals( $page->getTitle()->getDBkey(),
+                       $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+               $this->assertNull( $records[$invalidRow->rev_id] );
+               $this->assertSame( [ [
+                       'type' => 'warning',
+                       'message' => 'internalerror',
+                       'params' => [
+                               "Couldn't find slots for rev 100500"
+                       ]
+               ] ], $result->getErrors() );
+       }
 }
index 55bfab7..b0b9ddf 100644 (file)
@@ -102,23 +102,24 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        /**
+        * @param string|null $pageTitle whether to force-create a new page
         * @return WikiPage
         */
-       protected function getTestPage() {
-               if ( $this->testPage ) {
+       protected function getTestPage( $pageTitle = null ) {
+               if ( !is_null( $pageTitle ) && $this->testPage ) {
                        return $this->testPage;
                }
 
-               $title = $this->getTestPageTitle();
-               $this->testPage = WikiPage::factory( $title );
+               $title = is_null( $pageTitle ) ? $this->getTestPageTitle() : Title::newFromText( $pageTitle );
+               $page = WikiPage::factory( $title );
 
-               if ( !$this->testPage->exists() ) {
+               if ( !$page->exists() ) {
                        // Make sure we don't write to the live db.
                        $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
 
                        $user = static::getTestSysop()->getUser();
 
-                       $this->testPage->doEditContent(
+                       $page->doEditContent(
                                new WikitextContent( 'UTContent-' . __CLASS__ ),
                                'UTPageSummary-' . __CLASS__,
                                EDIT_NEW | EDIT_SUPPRESS_RC,
@@ -127,7 +128,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        );
                }
 
-               return $this->testPage;
+               if ( is_null( $pageTitle ) ) {
+                       $this->testPage = $page;
+               }
+               return $page;
        }
 
        /**
@@ -1959,4 +1963,96 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter );
        }
 
+       public function provideNewRevisionsFromBatchOptions() {
+               yield 'No preload slots or content, single page' => [
+                       null,
+                       []
+               ];
+               yield 'Preload slots and content, single page' => [
+                       null,
+                       [
+                               'slots' => [ SlotRecord::MAIN ],
+                               'content' => true
+                       ]
+               ];
+               yield 'No preload slots or content, multiple pages' => [
+                       'Other_Page',
+                       []
+               ];
+               yield 'Preload slots and content, multiple pages' => [
+                       'Other_Page',
+                       [
+                               'slots' => [ SlotRecord::MAIN ],
+                               'content' => true
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewRevisionsFromBatchOptions
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        * @param string|null $otherPageTitle
+        * @param array|null $options
+        * @throws \MWException
+        */
+       public function testNewRevisionsFromBatch_preloadContent(
+               $otherPageTitle = null,
+               array $options = []
+       ) {
+               $page1 = $this->getTestPage();
+               $text = __METHOD__ . 'b-ä';
+               /** @var Revision $rev1 */
+               $rev1 = $page1->doEditContent(
+                       new WikitextContent( $text . '1' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $page2 = $this->getTestPage( $otherPageTitle );
+               /** @var Revision $rev2 */
+               $rev2 = $page2->doEditContent(
+                       new WikitextContent( $text . '2' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->newRevisionsFromBatch(
+                       [ $this->revisionToRow( $rev1 ), $this->revisionToRow( $rev2 ) ],
+                       $options
+               );
+               $this->assertTrue( $result->isGood() );
+               $this->assertEmpty( $result->getErrors() );
+               $records = $result->getValue();
+               $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+               $this->assertRevisionRecordMatchesRevision( $rev2, $records[$rev2->getId()] );
+
+               $this->assertSame( $text . '1',
+                       $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertSame( $text . '2',
+                       $records[$rev2->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertEquals( $page1->getTitle()->getDBkey(),
+                       $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+               $this->assertEquals( $page2->getTitle()->getDBkey(),
+                       $records[$rev2->getId()]->getPageAsLinkTarget()->getDBkey() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        */
+       public function testNewRevisionsFromBatch_emptyBatch() {
+               $result = MediaWikiServices::getInstance()->getRevisionStore()
+                       ->newRevisionsFromBatch(
+                               [],
+                               [
+                                       'slots' => [ SlotRecord::MAIN ],
+                                       'content' => true
+                               ]
+                       );
+               $this->assertTrue( $result->isGood() );
+               $this->assertEmpty( $result->getValue() );
+               $this->assertEmpty( $result->getErrors() );
+       }
 }
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..ea0cb8a 100644 (file)
@@ -889,10 +889,24 @@ class ApiBaseTest extends ApiTestCase {
                                [],
                                [ 'internalmode' => false ],
                        ],
-                       'Limit with parseLimits false' => [
+                       'Limit with parseLimits false (numeric)' => [
                                '100',
                                [ ApiBase::PARAM_TYPE => 'limit' ],
-                               '100',
+                               100,
+                               [],
+                               [ 'parseLimits' => false ],
+                       ],
+                       'Limit with parseLimits false (max)' => [
+                               'max',
+                               [ ApiBase::PARAM_TYPE => 'limit' ],
+                               'max',
+                               [],
+                               [ 'parseLimits' => false ],
+                       ],
+                       'Limit with parseLimits false (invalid)' => [
+                               'kitten',
+                               [ ApiBase::PARAM_TYPE => 'limit' ],
+                               0,
                                [],
                                [ 'parseLimits' => false ],
                        ],
@@ -901,7 +915,6 @@ class ApiBaseTest extends ApiTestCase {
                                [
                                        ApiBase::PARAM_TYPE => 'limit',
                                        ApiBase::PARAM_MAX2 => 10,
-                                       ApiBase::PARAM_ISMULTI => true,
                                ],
                                new MWException(
                                        'Internal error in ApiBase::getParameterFromSettings: ' .
@@ -913,7 +926,6 @@ class ApiBaseTest extends ApiTestCase {
                                [
                                        ApiBase::PARAM_TYPE => 'limit',
                                        ApiBase::PARAM_MAX => 10,
-                                       ApiBase::PARAM_ISMULTI => true,
                                ],
                                new MWException(
                                        'Internal error in ApiBase::getParameterFromSettings: ' .
@@ -1443,7 +1455,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,
+                       ],
+               ];
+       }
+
+}
index 8548fde..062087d 100644 (file)
@@ -29,7 +29,6 @@ use Wikimedia\TestingAccessWrapper;
  * @covers FileBackendStoreShardDirIterator
  * @covers FileBackendStoreShardFileIterator
  * @covers FileBackendStoreShardListIterator
- * @covers FileJournal
  * @covers FileOp
  * @covers FileOpBatch
  * @covers HTTPFileStreamer
@@ -37,7 +36,6 @@ use Wikimedia\TestingAccessWrapper;
  * @covers MemoryFileBackend
  * @covers MoveFileOp
  * @covers MySqlLockManager
- * @covers NullFileJournal
  * @covers NullFileOp
  * @covers StoreFileOp
  * @covers TempFSFile
@@ -295,7 +293,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
 
-               $this->assertBackendPathsConsistent( [ $dest ] );
+               $this->assertBackendPathsConsistent( [ $dest ], true );
        }
 
        public static function provider_testStore() {
@@ -320,19 +318,19 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testCopy
         */
-       public function testCopy( $op ) {
+       public function testCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op );
+               $this->doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op );
+               $this->doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestCopy( $op ) {
+       private function doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
@@ -340,99 +338,128 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->prepare( [ 'dir' => dirname( $source ) ] );
                $this->prepare( [ 'dir' => dirname( $dest ) ] );
 
-               if ( isset( $op['ignoreMissingSource'] ) ) {
-                       $status = $this->backend->doOperation( $op );
-                       $this->assertGoodStatus( $status,
-                               "Move from $source to $dest succeeded without warnings ($backendName)." );
-                       $this->assertEquals( [ 0 => true ], $status->success,
-                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                               "Source file $source does not exist ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
-                               "Destination file $dest does not exist ($backendName)." );
-
-                       return;
+               if ( is_string( $srcContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $srcContent, 'dst' => $source ] );
+                       $this->assertGoodStatus( $status, "Creation of $source succeeded ($backendName)." );
                }
-
-               $status = $this->backend->doOperation(
-                       [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-
-               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
-                       $this->backend->copy( $op );
+               if ( is_string( $dstContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $dstContent, 'dst' => $dest ] );
+                       $this->assertGoodStatus( $status, "Creation of $dest succeeded ($backendName)." );
                }
 
                $status = $this->backend->doOperation( $op );
 
-               $this->assertGoodStatus( $status,
-                       "Copy from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
-                       "Copy from $source to $dest succeeded ($backendName)." );
-               $this->assertEquals( [ 0 => true ], $status->success,
-                       "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $source ] ),
-                       "Source file $source still exists ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
-                       "Destination file $dest exists after copy ($backendName)." );
-
-               $this->assertEquals(
-                       $this->backend->getFileSize( [ 'src' => $source ] ),
-                       $this->backend->getFileSize( [ 'src' => $dest ] ),
-                       "Destination file $dest has correct size ($backendName)." );
+               if ( $okStatus ) {
+                       $this->assertGoodStatus(
+                               $status,
+                               "Copy from $source to $dest succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Copy from $source to $dest succeeded ($backendName)." );
+                       $this->assertEquals( [ 0 => true ], $status->success,
+                               "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
+                       if ( !is_string( $srcContent ) ) {
+                               $this->assertSame(
+                                       is_string( $dstContent ),
+                                       $this->backend->fileExists( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op copy ($backendName)." );
+                               $this->assertSame(
+                                       $dstContent,
+                                       $this->backend->getFileContents( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op copy ($backendName)." );
+                       } else {
+                               $this->assertEquals(
+                                       $this->backend->getFileSize( [ 'src' => $source ] ),
+                                       $this->backend->getFileSize( [ 'src' => $dest ] ),
+                                       "Destination file $dest has correct size ($backendName)." );
+                               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
+                               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
+                               $this->assertEquals(
+                                       $props1,
+                                       $props2,
+                                       "Source and destination have the same props ($backendName)." );
+                       }
+               } else {
+                       $this->assertBadStatus(
+                               $status,
+                               "Copy from $source to $dest fails ($backendName)." );
+                       $this->assertSame(
+                               is_string( $dstContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed copy ($backendName)." );
+                       $this->assertSame(
+                               $dstContent,
+                               $this->backend->getFileContents( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed copy ($backendName)." );
+               }
 
-               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
-               $this->assertEquals( $props1, $props2,
-                       "Source and destination have the same props ($backendName)." );
+               $this->assertSame(
+                       is_string( $srcContent ),
+                       (bool)$this->backend->fileExists( [ 'src' => $source ] ),
+                       "Source file $source unchanged after copy ($backendName)."
+               );
+               $this->assertSame(
+                       $srcContent,
+                       $this->backend->getFileContents( [ 'src' => $source ] ),
+                       "Source file $source unchanged after copy ($backendName)."
+               );
+               if ( is_string( $dstContent ) ) {
+                       $this->assertTrue(
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest exists after copy ($backendName)." );
+               }
 
-               $this->assertBackendPathsConsistent( [ $source, $dest ] );
+               $this->assertBackendPathsConsistent( [ $source, $dest ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source exists, dest exists, op succeeds, sync check succeeds)
+        */
        public static function provider_testCopy() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
-               $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+               $dest = self::baseStorePath() . '/unittest-cont2/a/fileCopied.txt';
+               $opBase = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ];
 
-               $op = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ];
-               $cases[] = [
-                       $op, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwrite'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwriteSame'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', 'xxx', true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'yyy', true, true ];
+
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'zzz', false, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, 'xxx', true, true ];
+
+               $op = $opBase;
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, false, false ];
 
                return $cases;
        }
@@ -440,19 +467,19 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testMove
         */
-       public function testMove( $op ) {
+       public function testMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op );
+               $this->doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op );
+               $this->doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestMove( $op ) {
+       private function doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
@@ -460,100 +487,128 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->prepare( [ 'dir' => dirname( $source ) ] );
                $this->prepare( [ 'dir' => dirname( $dest ) ] );
 
-               if ( isset( $op['ignoreMissingSource'] ) ) {
-                       $status = $this->backend->doOperation( $op );
-                       $this->assertGoodStatus( $status,
-                               "Move from $source to $dest succeeded without warnings ($backendName)." );
-                       $this->assertEquals( [ 0 => true ], $status->success,
-                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                               "Source file $source does not exist ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
-                               "Destination file $dest does not exist ($backendName)." );
-
-                       return;
+               if ( is_string( $srcContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $srcContent, 'dst' => $source ] );
+                       $this->assertGoodStatus( $status, "Creation of $source succeeded ($backendName)." );
                }
-
-               $status = $this->backend->doOperation(
-                       [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-
-               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
-                       $this->backend->copy( $op );
+               if ( is_string( $dstContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $dstContent, 'dst' => $dest ] );
+                       $this->assertGoodStatus( $status, "Creation of $dest succeeded ($backendName)." );
                }
 
+               $oldSrcProps = $this->backend->getFileProps( [ 'src' => $source ] );
+
                $status = $this->backend->doOperation( $op );
-               $this->assertGoodStatus( $status,
-                       "Move from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
-                       "Move from $source to $dest succeeded ($backendName)." );
-               $this->assertEquals( [ 0 => true ], $status->success,
-                       "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-               $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                       "Source file $source does not still exists ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
-                       "Destination file $dest exists after move ($backendName)." );
 
-               $this->assertNotEquals(
-                       $this->backend->getFileSize( [ 'src' => $source ] ),
-                       $this->backend->getFileSize( [ 'src' => $dest ] ),
-                       "Destination file $dest has correct size ($backendName)." );
+               if ( $okStatus ) {
+                       $this->assertGoodStatus(
+                               $status,
+                               "Move from $source to $dest succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Move from $source to $dest succeeded ($backendName)." );
+                       $this->assertEquals( [ 0 => true ], $status->success,
+                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+                       if ( !is_string( $srcContent ) ) {
+                               $this->assertSame(
+                                       is_string( $dstContent ),
+                                       $this->backend->fileExists( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op move ($backendName)." );
+                               $this->assertSame(
+                                       $dstContent,
+                                       $this->backend->getFileContents( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op move ($backendName)." );
+                       } else {
+                               $this->assertEquals(
+                                       $this->backend->getFileSize( [ 'src' => $dest ] ),
+                                       strlen( $srcContent ),
+                                       "Destination file $dest has correct size ($backendName)." );
+                               $this->assertEquals(
+                                       $oldSrcProps,
+                                       $this->backend->getFileProps( [ 'src' => $dest ] ),
+                                       "Source and destination have the same props ($backendName)." );
+                       }
+               } else {
+                       $this->assertBadStatus(
+                               $status,
+                               "Move from $source to $dest fails ($backendName)." );
+                       $this->assertSame(
+                               is_string( $dstContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed move ($backendName)." );
+                       $this->assertSame(
+                               $dstContent,
+                               $this->backend->getFileContents( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed move ($backendName)." );
+                       $this->assertSame(
+                               is_string( $srcContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $source ] ),
+                               "Source file $source unchanged after failed move ($backendName)."
+                       );
+                       $this->assertSame(
+                               $srcContent,
+                               $this->backend->getFileContents( [ 'src' => $source ] ),
+                               "Source file $source unchanged after failed move ($backendName)."
+                       );
+               }
 
-               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
-               $this->assertEquals( false, $props1['fileExists'],
-                       "Source file does not exist accourding to props ($backendName)." );
-               $this->assertEquals( true, $props2['fileExists'],
-                       "Destination file exists accourding to props ($backendName)." );
+               if ( is_string( $dstContent ) ) {
+                       $this->assertTrue(
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest exists after move ($backendName)." );
+               }
 
-               $this->assertBackendPathsConsistent( [ $source, $dest ] );
+               $this->assertBackendPathsConsistent( [ $source, $dest ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source exists, dest exists, op succeeds, sync check succeeds)
+        */
        public static function provider_testMove() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
                $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+               $opBase = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ];
 
-               $op = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ];
-               $cases[] = [
-                       $op, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwrite'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwriteSame'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', 'xxx', true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'yyy', true, true ];
+
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'zzz', false, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, 'xxx', true, true ];
+
+               $op = $opBase;
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, false, false ];
 
                return $cases;
        }
@@ -561,27 +616,27 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testDelete
         */
-       public function testDelete( $op, $withSource, $okStatus ) {
+       public function testDelete( $op, $srcContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $withSource, $okStatus );
+               $this->doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $withSource, $okStatus );
+               $this->doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestDelete( $op, $withSource, $okStatus ) {
+       private function doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
                $this->prepare( [ 'dir' => dirname( $source ) ] );
 
-               if ( $withSource ) {
+               if ( is_string( $srcContent ) ) {
                        $status = $this->backend->doOperation(
-                               [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
+                               [ 'op' => 'create', 'content' => $srcContent, 'dst' => $source ] );
                        $this->assertGoodStatus( $status,
                                "Creation of file at $source succeeded ($backendName)." );
                }
@@ -599,7 +654,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Deletion of file at $source failed ($backendName)." );
                }
 
-               $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
+               $this->assertFalse(
+                       (bool)$this->backend->fileExists( [ 'src' => $source ] ),
                        "Source file $source does not exist after move ($backendName)." );
 
                $this->assertFalse(
@@ -607,44 +663,40 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Source file $source has correct size (false) ($backendName)." );
 
                $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $this->assertFalse( $props1['fileExists'],
+               $this->assertFalse(
+                       $props1['fileExists'],
                        "Source file $source does not exist according to props ($backendName)." );
 
-               $this->assertBackendPathsConsistent( [ $source ] );
+               $this->assertBackendPathsConsistent( [ $source ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source content, op succeeds, sync check succeeds)
+        */
        public static function provider_testDelete() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+               $baseOp = [ 'op' => 'delete', 'src' => $source ];
 
-               $op = [ 'op' => 'delete', 'src' => $source ];
-               $cases[] = [
-                       $op, // operation
-                       true, // with source
-                       true // succeeds
-               ];
+               $op = $baseOp;
+               $cases[] = [ $op, 'xxx', true, true ];
 
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       false // fails
-               ];
+               $op = $baseOp;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', true, true ];
+
+               $op = $baseOp;
+               $cases[] = [ $op, false, false, true ];
 
+               $op = $baseOp;
                $op['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       true // succeeds
-               ];
+               $cases[] = [ $op, false, true, true ];
 
+               $op = $baseOp;
                $op['ignoreMissingSource'] = true;
-               $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt';
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       true // succeeds
-               ];
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $cases[] = [ $op, false, false, false ];
 
                return $cases;
        }
@@ -710,7 +762,7 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Describe of file at $source failed ($backendName)." );
                }
 
-               $this->assertBackendPathsConsistent( [ $source ] );
+               $this->assertBackendPathsConsistent( [ $source ], true );
        }
 
        private function assertHasHeaders( array $headers, array $attr ) {
@@ -811,7 +863,7 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Destination file $dest has original size according to props ($backendName)." );
                }
 
-               $this->assertBackendPathsConsistent( [ $dest ] );
+               $this->assertBackendPathsConsistent( [ $dest ], true );
        }
 
        /**
@@ -2610,7 +2662,7 @@ class FileBackendTest extends MediaWikiTestCase {
        }
 
        function tearDownFiles() {
-               $containers = [ 'unittest-cont1', 'unittest-cont2', 'unittest-cont-bad' ];
+               $containers = [ 'unittest-cont1', 'unittest-cont2' ];
                foreach ( $containers as $container ) {
                        $this->deleteFiles( $container );
                }
@@ -2629,14 +2681,24 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->backend->clean( [ 'dir' => "$base/$container", 'recursive' => 1 ] );
        }
 
-       function assertBackendPathsConsistent( array $paths ) {
-               if ( $this->backend instanceof FileBackendMultiWrite ) {
-                       $status = $this->backend->consistencyCheck( $paths );
+       private function assertBackendPathsConsistent( array $paths, $okSyncStatus ) {
+               if ( !$this->backend instanceof FileBackendMultiWrite ) {
+                       return;
+               }
+
+               $status = $this->backend->consistencyCheck( $paths );
+               if ( $okSyncStatus ) {
                        $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+               } else {
+                       $this->assertBadStatus( $status, "Files not synced: " . implode( ',', $paths ) );
                }
        }
 
-       function assertGoodStatus( StatusValue $status, $msg ) {
+       private function assertGoodStatus( StatusValue $status, $msg ) {
                $this->assertEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
        }
+
+       private function assertBadStatus( StatusValue $status, $msg ) {
+               $this->assertNotEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
+       }
 }
diff --git a/tests/phpunit/includes/filebackend/filejournal/DBFileJournalIntegrationTest.php b/tests/phpunit/includes/filebackend/filejournal/DBFileJournalIntegrationTest.php
new file mode 100644 (file)
index 0000000..9a0ba1c
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
+/**
+ * @coversDefaultClass DBFileJournal
+ * @covers ::__construct
+ * @covers ::getMasterDB
+ * @group Database
+ */
+class DBFileJournalIntegrationTest extends MediaWikiIntegrationTestCase {
+       public function addDBDataOnce() {
+               global $IP;
+               $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
+               if ( $db->getType() !== 'mysql' ) {
+                       return;
+               }
+               if ( !$db->tableExists( 'filejournal' ) ) {
+                       $db->sourceFile( "$IP/maintenance/archives/patch-filejournal.sql" );
+               }
+       }
+
+       protected function setUp() {
+               parent::setUp();
+
+               $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
+               if ( $db->getType() !== 'mysql' ) {
+                       $this->markTestSkipped( 'No filejournal schema available for this database type' );
+               }
+
+               $this->tablesUsed[] = 'filejournal';
+       }
+
+       private function getJournal( $options = [] ) {
+               return FileJournal::factory(
+                       $options + [ 'class' => DBFileJournal::class, 'domain' => wfWikiID() ],
+                       'local-backend' );
+       }
+
+       /**
+        * @covers ::doLogChangeBatch
+        */
+       public function testDoLogChangeBatch_exceptionDbConnect() {
+               $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
+
+               $this->assertEquals(
+                       StatusValue::newFatal( 'filejournal-fail-dbconnect', 'local-backend' ),
+                       $journal->logChangeBatch( [ [] ], 'batch' ) );
+       }
+
+       /**
+        * @covers ::doLogChangeBatch
+        */
+       public function testDoLogChangeBatch_exceptionDbQuery() {
+               MediaWikiServices::getInstance()->getConfiguredReadOnlyMode()->setReason( 'testing' );
+
+               $journal = $this->getJournal();
+
+               $this->assertEquals(
+                       StatusValue::newFatal( 'filejournal-fail-dbquery', 'local-backend' ),
+                       $journal->logChangeBatch(
+                               [ [ 'op' => null, 'path' => '', 'newSha1' => false ] ], 'batch' ) );
+       }
+
+       /**
+        * @covers ::doLogChangeBatch
+        * @covers ::doGetCurrentPosition
+        */
+       public function testDoGetCurrentPosition() {
+               $journal = $this->getJournal();
+
+               $this->assertNull( $journal->getCurrentPosition() );
+
+               $journal->logChangeBatch(
+                       [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
+
+               $this->assertSame( '1', $journal->getCurrentPosition() );
+
+               $journal->logChangeBatch(
+                       [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
+
+               $this->assertSame( '2', $journal->getCurrentPosition() );
+       }
+
+       /**
+        * @covers ::doLogChangeBatch
+        * @covers ::doGetPositionAtTime
+        */
+       public function testDoGetPositionAtTime() {
+               $journal = $this->getJournal();
+
+               $now = time();
+
+               $this->assertFalse( $journal->getPositionAtTime( $now ) );
+
+               ConvertibleTimestamp::setFakeTime( $now - 86400 );
+
+               $journal->logChangeBatch(
+                       [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
+
+               ConvertibleTimestamp::setFakeTime( $now - 3600 );
+
+               $journal->logChangeBatch(
+                       [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
+
+               $this->assertFalse( $journal->getPositionAtTime( $now - 86401 ) );
+               $this->assertSame( '1', $journal->getPositionAtTime( $now - 86400 ) );
+               $this->assertSame( '1', $journal->getPositionAtTime( $now - 3601 ) );
+               $this->assertSame( '2', $journal->getPositionAtTime( $now - 3600 ) );
+       }
+
+       /**
+        * @param int $expectedStart First index expected to be returned (0-based)
+        * @param int|null $expectedCount Number of entries expected to be returned (null for all)
+        * @param string|null|false $expectedNext Expected value of $next, or false not to pass
+        * @param array $args If any third argument is present, $next will also be tested
+        * @dataProvider provideDoGetChangeEntries
+        * @covers ::doLogChangeBatch
+        * @covers ::doGetChangeEntries
+        */
+       public function testDoGetChangeEntries(
+               $expectedStart, $expectedCount, $expectedNext, array $args
+       ) {
+               $journal = $this->getJournal();
+
+               $i = 0;
+               $makeExpectedEntry = function ( $op, $path, $newSha1, $batch, $time ) use ( &$i ) {
+                       $i++;
+                       return [
+                               'id' => (string)$i,
+                               'batch_uuid' => $batch,
+                               'backend' => 'local-backend',
+                               'path' => $path,
+                               'op' => $op ?? '',
+                               'new_sha1' => $newSha1 !== false ? $newSha1 : '0',
+                               'timestamp' => ConvertibleTimestamp::convert( TS_MW, $time ),
+                       ];
+               };
+
+               $expectedEntries = [];
+
+               $now = time();
+
+               ConvertibleTimestamp::setFakeTime( $now - 3600 );
+               $changes = [
+                       [ 'op' => 'create', 'path' => '/path1',
+                               'newSha1' => base_convert( sha1( 'a' ), 16, 36 ) ],
+                       [ 'op' => 'delete', 'path' => '/path2', 'newSha1' => false ],
+                       [ 'op' => 'null', 'path' => '', 'newSha1' => false ],
+               ];
+               $this->assertEquals( StatusValue::newGood(),
+                       $journal->logChangeBatch( $changes, 'batch1' ) );
+               foreach ( $changes as $change ) {
+                       $expectedEntries[] = $makeExpectedEntry(
+                               ...array_merge( array_values( $change ), [ 'batch1', $now - 3600 ] ) );
+               }
+
+               ConvertibleTimestamp::setFakeTime( $now - 60 );
+               $change = [ 'op' => 'update', 'path' => '/path1',
+                       'newSha1' => base_convert( sha1( 'b' ), 16, 36 ) ];
+               $this->assertEquals(
+                  StatusValue::newGood(), $journal->logChangeBatch( [ $change ], 'batch2' ) );
+               $expectedEntries[] = $makeExpectedEntry(
+                       ...array_merge( array_values( $change ), [ 'batch2', $now - 60 ] ) );
+
+               if ( $expectedNext === false ) {
+                       $this->assertSame(
+                               array_slice( $expectedEntries, $expectedStart, $expectedCount ),
+                               $journal->getChangeEntries( ...$args )
+                       );
+               } else {
+                       $next = false;
+                       $this->assertSame(
+                               array_slice( $expectedEntries, $expectedStart, $expectedCount ),
+                               $journal->getChangeEntries( $args[0], $args[1], $next )
+                       );
+                       $this->assertSame( $expectedNext, $next );
+               }
+       }
+
+       public static function provideDoGetChangeEntries() {
+               return [
+                       'No args' => [ 0, 4, false, [] ],
+                       'null' => [ 0, 4, false, [ null ] ],
+                       '1' => [ 0, 4, false, [ 1 ] ],
+                       '2' => [ 1, 3, false, [ 2 ] ],
+                       '4' => [ 3, 1, false, [ 4 ] ],
+                       '5' => [ 0, 0, false, [ 5 ] ],
+                       'null, 0' => [ 0, 4, null, [ null, 0 ] ],
+                       '1, 0' => [ 0, 4, null, [ 1, 0 ] ],
+                       '2, 0' => [ 1, 3, null, [ 2, 0 ] ],
+                       '4, 0' => [ 3, 1, null, [ 4, 0 ] ],
+                       '5, 0' => [ 0, 0, null, [ 5, 0 ] ],
+                       '1, 1' => [ 0, 1, '2', [ 1, 1 ] ],
+                       '1, 2' => [ 0, 2, '3', [ 1, 2 ] ],
+                       '1, 4' => [ 0, 4, null, [ 1, 4 ] ],
+                       '1, 5' => [ 0, 4, null, [ 1, 5 ] ],
+                       '2, 2' => [ 1, 2, '4', [ 2, 2 ] ],
+                       '1, 2 with no $next' => [ 0, 2, false, [ 1, 2 ] ],
+               ];
+       }
+
+       /**
+        * @covers ::doPurgeOldLogs
+        */
+       public function testDoPurgeOldLogs_noop() {
+               // If we tried to access the database, it would throw, because the domain doesn't exist
+               $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
+               $this->assertEquals( StatusValue::newGood(), $journal->purgeOldLogs() );
+       }
+
+       /**
+        * @covers ::doPurgeOldLogs
+        * @covers ::doLogChangeBatch
+        * @covers ::doGetChangeEntries
+        */
+       public function testDoPurgeOldLogs() {
+               $journal = $this->getJournal( [ 'ttlDays' => 1 ] );
+               $now = time();
+
+               // One day and one second ago
+               ConvertibleTimestamp::setFakeTime( $now - 86401 );
+               $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
+                       [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch1' ) );
+
+               // One day ago exactly, won't get purged
+               ConvertibleTimestamp::setFakeTime( $now - 86400 );
+               $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
+                       [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch2' ) );
+
+               ConvertibleTimestamp::setFakeTime( $now );
+               $this->assertCount( 2, $journal->getChangeEntries() );
+               $journal->purgeOldLogs();
+               $this->assertCount( 1, $journal->getChangeEntries() );
+       }
+}
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 ];
+       }
+}
index bed739b..ab8f2f0 100644 (file)
@@ -136,6 +136,7 @@ class LocalRepoTest extends MediaWikiIntegrationTestCase {
                        [ '.e.x', 'e' ],
                        [ '..f.x', 'f' ],
                        [ 'g..x', 'g' ],
+                       [ '01234567890123456789012345678901.x', '1234567890123456789012345678901' ],
                ];
        }
 
diff --git a/tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php b/tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php
new file mode 100644 (file)
index 0000000..a68ac83
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\NullLogger;
+
+/**
+ * @group Database
+ * @coversDefaultClass ImportableOldRevisionImporter
+ */
+class ImportableOldRevisionImporterTest extends MediaWikiIntegrationTestCase {
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->tablesUsed[] = 'change_tag';
+               $this->tablesUsed[] = 'change_tag_def';
+
+               ChangeTags::defineTag( 'tag1' );
+       }
+
+       public function provideTestCases() {
+               yield [ [] ];
+               yield [ [ "tag1" ] ];
+       }
+
+       /**
+        * @covers ::import
+        * @param $expectedTags
+        * @dataProvider provideTestCases
+        */
+       public function testImport( $expectedTags ) {
+               $services = MediaWikiServices::getInstance();
+
+               $title = Title::newFromText( __CLASS__ . rand() );
+               $revision = new WikiRevision( $services->getMainConfig() );
+               $revision->setTitle( $title );
+               $revision->setTags( $expectedTags );
+               $revision->setText( "dummy edit" );
+
+               $importer = new ImportableOldRevisionImporter(
+                       true,
+                       new NullLogger(),
+                       $services->getDBLoadBalancer()
+               );
+               $result = $importer->import( $revision );
+               $this->assertTrue( $result );
+
+               $page = WikiPage::factory( $title );
+               $tags = ChangeTags::getTags(
+                       $services->getDBLoadBalancer()->getConnection( DB_MASTER ),
+                       null,
+                       $page->getLatest()
+               );
+               $this->assertSame( $expectedTags, $tags );
+       }
+}
index 4afe3b5..8ddb7c9 100644 (file)
@@ -24,6 +24,32 @@ class HashRingTest extends PHPUnit\Framework\TestCase {
                }
        }
 
+       public function testHashRingSingleLocation() {
+               // SHA-1 based and weighted
+               $ring = new HashRing( [ 's1' => 1 ], 'sha1' );
+
+               $this->assertEquals(
+                       [ 's1' => 1 ],
+                       $ring->getLocationWeights(),
+                       'Normalized location weights'
+               );
+
+               for ( $i = 0; $i < 5; $i++ ) {
+                       $this->assertEquals(
+                               's1',
+                               $ring->getLocation( "hello$i" ),
+                               'Items placed at proper locations'
+                       );
+                       $this->assertEquals(
+                               [ 's1' ],
+                               $ring->getLocations( "hello$i", 2 ),
+                               'Items placed at proper locations'
+                       );
+               }
+
+               $this->assertEquals( [], $ring->getLocations( "helloX", 0 ), "Limit of 0" );
+       }
+
        public function testHashRingMapping() {
                // SHA-1 based and weighted
                $ring = new HashRing(
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 [];
+       }
+}
index e1ee324..7a669e1 100644 (file)
@@ -108,7 +108,7 @@ Deprecation message.' ]
 
                // phpcs:disable Generic.Files.LineLength
                $expected = '<script>'
-                       . 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
+                       . 'document.documentElement.className="client-js";'
                        . 'RLCONF={"key":"value"};'
                        . 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
                        . 'RLPAGEMODULES=["test"];'
@@ -135,7 +135,7 @@ Deprecation message.' ]
                );
 
                // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+               $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
                        . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;target=example"></script>';
                // phpcs:enable
 
@@ -152,7 +152,7 @@ Deprecation message.' ]
                );
 
                // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+               $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
                        . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;safemode=1"></script>';
                // phpcs:enable
 
@@ -169,7 +169,7 @@ Deprecation message.' ]
                );
 
                // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+               $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
                        . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
                // phpcs:enable
 
@@ -242,14 +242,14 @@ Deprecation message.' ]
                                'modules' => [ 'test.scripts.user' ],
                                'only' => ResourceLoaderModule::TYPE_SCRIPTS,
                                'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version=0a56zyi");});</script>',
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version={blankCombi}");});</script>',
                        ],
                        [
                                'context' => [],
                                'modules' => [ 'test.user' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
                                'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version={blankCombi}");});</script>',
                        ],
                        [
                                'context' => [ 'debug' => 'true' ],
@@ -278,7 +278,7 @@ Deprecation message.' ]
                                'modules' => [ 'test.shouldembed' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
                                'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@{blankVer}",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
@@ -299,7 +299,7 @@ Deprecation message.' ]
                                'modules' => [ 'test', 'test.shouldembed' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
                                'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@{blankVer}",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
@@ -351,6 +351,7 @@ Deprecation message.' ]
 
        private static function expandVariables( $text ) {
                return strtr( $text, [
+                       '{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
                        '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
                ] );
        }
index d4462e9..9bbf14d 100644 (file)
@@ -261,7 +261,7 @@ mw.loader.state({
                                                'factory' => function () {
                                                        $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
                                                                ->setMethods( [ 'getVersionHash' ] )->getMock();
-                                                       $mock->method( 'getVersionHash' )->willReturn( '1234567' );
+                                                       $mock->method( 'getVersionHash' )->willReturn( '12345' );
                                                        return $mock;
                                                }
                                        ]
@@ -273,7 +273,7 @@ mw.loader.addSource({
 mw.loader.register([
     [
         "test.version",
-        "1234567"
+        "12345"
     ]
 ]);',
                        ] ],
@@ -296,7 +296,7 @@ mw.loader.addSource({
 mw.loader.register([
     [
         "test.version",
-        "016es8l"
+        "16es8"
     ]
 ]);',
                        ] ],
index 428778e..94e3461 100644 (file)
@@ -725,7 +725,7 @@ END
                );
 
                $this->assertEquals(
-                       ResourceLoader::makeHash( self::BLANK_VERSION ),
+                       self::BLANK_COMBI,
                        $rl->getCombinedVersion( $context, [ 'foo' ] ),
                        'compute foo'
                );
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/libs/filebackend/filejournal/FileJournalTest.php b/tests/phpunit/unit/includes/libs/filebackend/filejournal/FileJournalTest.php
new file mode 100644 (file)
index 0000000..10eef7e
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+
+require_once __DIR__ . '/TestFileJournal.php';
+
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
+/**
+ * @coversDefaultClass FileJournal
+ */
+class FileJournalTest extends MediaWikiUnitTestCase {
+       private function newObj( $options = [], $backend = '' ) {
+               return FileJournal::factory(
+                       $options + [ 'class' => TestFileJournal::class ],
+                       $backend
+               );
+       }
+
+       /**
+        * @covers ::factory
+        */
+       public function testConstructor_backend() {
+               $this->assertSame( 'some_backend', $this->newObj( [], 'some_backend' )->getBackend() );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::factory
+        */
+       public function testConstructor_ttlDays() {
+               $this->assertSame( 42, $this->newObj( [ 'ttlDays' => 42 ] )->getTtlDays() );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::factory
+        */
+       public function testConstructor_noTtlDays() {
+               $this->assertSame( false, $this->newObj()->getTtlDays() );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::factory
+        */
+       public function testConstructor_nullTtlDays() {
+               $this->assertSame( false, $this->newObj( [ 'ttlDays' => null ] )->getTtlDays() );
+       }
+
+       /**
+        * @covers ::factory
+        */
+       public function testFactory_invalidClass() {
+               $this->setExpectedException( UnexpectedValueException::class,
+                       'Expected instance of FileJournal, got stdClass' );
+
+               FileJournal::factory( [ 'class' => 'stdclass' ], '' );
+       }
+
+       /**
+        * @covers ::getTimestampedUUID
+        */
+       public function testGetTimestampedUUID() {
+               $obj = FileJournal::factory( [ 'class' => 'NullFileJournal' ], '' );
+               $uuids = [];
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $time1 = time();
+                       $uuid = $obj->getTimestampedUUID();
+                       $time2 = time();
+                       $this->assertRegexp( '/^[0-9a-z]{31}$/', $uuid );
+                       $this->assertArrayNotHasKey( $uuid, $uuids );
+                       $uuids[$uuid] = true;
+
+                       // Now test that the timestamp portion is as expected.
+                       $time = ConvertibleTimestamp::convert( TS_UNIX, Wikimedia\base_convert(
+                               substr( $uuid, 0, 9 ), 36, 10 ) );
+
+                       $this->assertGreaterThanOrEqual( $time1, $time );
+                       $this->assertLessThanOrEqual( $time2, $time );
+               }
+       }
+
+       /**
+        * @covers ::logChangeBatch
+        */
+       public function testLogChangeBatch() {
+               $this->assertEquals(
+                       StatusValue::newGood( 'Logged' ), $this->newObj()->logChangeBatch( [ 1 ], '' ) );
+       }
+
+       /**
+        * @covers ::logChangeBatch
+        */
+       public function testLogChangeBatch_empty() {
+               $this->assertEquals( StatusValue::newGood(), $this->newObj()->logChangeBatch( [], '' ) );
+       }
+
+       /**
+        * @covers ::getCurrentPosition
+        */
+       public function testGetCurrentPosition() {
+               $this->assertEquals( 613, $this->newObj()->getCurrentPosition() );
+       }
+
+       /**
+        * @covers ::getPositionAtTime
+        */
+       public function testGetPositionAtTime() {
+               $this->assertEquals( 248, $this->newObj()->getPositionAtTime( 0 ) );
+       }
+
+       /**
+        * @dataProvider provideGetChangeEntries
+        * @covers ::getChangeEntries
+        * @param int|null $start
+        * @param int $limit
+        * @param string|null $expectedNext
+        * @param string[] $expectedReturn Expected id's of returned values
+        */
+       public function testGetChangeEntries( $start, $limit, $expectedNext, array $expectedReturn ) {
+               $expectedReturn = array_map(
+                       function ( $val ) {
+                               return [ 'id' => $val ];
+                       }, $expectedReturn
+               );
+               $next = "Different from $expectedNext";
+               $ret = $this->newObj()->getChangeEntries( $start, $limit, $next );
+               $this->assertSame( $expectedNext, $next );
+               $this->assertSame( $expectedReturn, $ret );
+       }
+
+       public static function provideGetChangeEntries() {
+               return [
+                       [ null, 0, null, [ 1, 2, 3 ] ],
+                       [ null, 1, 2, [ 1 ] ],
+                       [ null, 2, 3, [ 1, 2 ] ],
+                       [ null, 3, null, [ 1, 2, 3 ] ],
+                       [ 1, 0, null, [ 1, 2, 3 ] ],
+                       [ 1, 2, 3, [ 1, 2 ] ],
+                       [ 1, 1, 2, [ 1 ] ],
+                       [ 2, 2, null, [ 2, 3 ] ],
+               ];
+       }
+
+       /**
+        * @covers ::purgeOldLogs
+        */
+       public function testPurgeOldLogs() {
+               $obj = $this->newObj();
+               $this->assertFalse( $obj->getPurged() );
+               $obj->purgeOldLogs();
+               $this->assertTrue( $obj->getPurged() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/filebackend/filejournal/NullFileJournalTest.php b/tests/phpunit/unit/includes/libs/filebackend/filejournal/NullFileJournalTest.php
new file mode 100644 (file)
index 0000000..c0b782b
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @coversDefaultClass NullFileJournal
+ */
+class NullFileJournalTest extends MediaWikiUnitTestCase {
+       public function newObj() : NullFileJournal {
+               return FileJournal::factory( [ 'class' => NullFileJournal::class ], '' );
+       }
+
+       /**
+        * @covers ::doLogChangeBatch
+        */
+       public function testLogChangeBatch() {
+               $this->assertEquals( StatusValue::newGood(), $this->newObj()->logChangeBatch( [ 1 ], '' ) );
+       }
+
+       /**
+        * @covers ::doGetCurrentPosition
+        */
+       public function testGetCurrentPosition() {
+               $this->assertFalse( $this->newObj()->getCurrentPosition() );
+       }
+
+       /**
+        * @covers ::doGetPositionAtTime
+        */
+       public function testGetPositionAtTime() {
+               $this->assertFalse( $this->newObj()->getPositionAtTime( 2 ) );
+       }
+
+       /**
+        * @covers ::doGetChangeEntries
+        */
+       public function testGetChangeEntries() {
+               $next = 1;
+               $entries = $this->newObj()->getChangeEntries( null, 0, $next );
+               $this->assertSame( [], $entries );
+               $this->assertNull( $next );
+       }
+
+       /**
+        * @covers ::doPurgeOldLogs
+        */
+       public function testPurgeOldLogs() {
+               $this->assertEquals( StatusValue::newGood(), $this->newObj()->purgeOldLogs() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/filebackend/filejournal/TestFileJournal.php b/tests/phpunit/unit/includes/libs/filebackend/filejournal/TestFileJournal.php
new file mode 100644 (file)
index 0000000..c115925
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+class TestFileJournal extends NullFileJournal {
+       /** @var bool */
+       private $purged = false;
+
+       public function getTtlDays() {
+               return $this->ttlDays;
+       }
+
+       public function getBackend() {
+               return $this->backend;
+       }
+
+       protected function doLogChangeBatch( array $entries, $batchId ) {
+               return StatusValue::newGood( 'Logged' );
+       }
+
+       protected function doGetCurrentPosition() {
+               return 613;
+       }
+
+       protected function doGetPositionAtTime( $time ) {
+               return 248;
+       }
+
+       protected function doGetChangeEntries( $start, $limit ) {
+               return array_slice( [
+                       [ 'id' => 1 ],
+                       [ 'id' => 2 ],
+                       [ 'id' => 3 ],
+               ], $start === null ? 0 : $start - 1, $limit ? $limit : null );
+       }
+
+       protected function doPurgeOldLogs() {
+               $this->purged = true;
+       }
+
+       public function getPurged() {
+               return $this->purged;
+       }
+}
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' );
-       }
-}
index 3258f8e..4c6e7c9 100644 (file)
                                require( 'testUrlIncDump' ).query,
                                {
                                        modules: 'testUrlIncDump',
-                                       // Expected: Wrapped hash just for this one module
-                                       //   $hash = hash( 'fnv132', 'dump');
-                                       //   base_convert( $hash, 16, 36 ); // "13e9zzn"
-                                       // Previously: Wrapped hash for both modules, despite being in separate requests
-                                       //   $hash = hash( 'fnv132', 'urldump' );
-                                       //   base_convert( $hash, 16, 36 ); // "18kz9ca"
-                                       version: '13e9zzn'
+                                       // Expected: Combine hashes only for the module in the specific HTTP request
+                                       //   hash fnv132 => "13e9zzn"
+                                       // Wrong: Combine hashes for all requested modules, before request-splitting
+                                       //   hash fnv132 => "18kz9ca"
+                                       version: '13e9z'
                                },
                                'Query parameters'
                        );
                                require( 'testUrlOrderDump' ).query,
                                {
                                        modules: 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b',
-                                       // Expected: Combined in order after string packing
-                                       //   $hash = hash( 'fnv132', 'urldump12' );
-                                       //   base_convert( $hash, 16, 36 ); // "1knqzan"
-                                       // Previously: Combined in order of before string packing
-                                       //   $hash = hash( 'fnv132', 'url12dump' );
-                                       //   base_convert( $hash, 16, 36 ); // "11eo3in"
-                                       version: '1knqzan'
+                                       // Expected: Combined by sorting names after string packing
+                                       //   hash fnv132 = "1knqzan"
+                                       // Wrong: Combined by sorting names before string packing
+                                       //   hash fnv132 => "11eo3in"
+                                       version: '1knqz'
                                },
                                'Query parameters'
                        );