Merge "Add Special:Mute as a shortcut for muting notifications"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 25 Jun 2019 17:15:39 +0000 (17:15 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 25 Jun 2019 17:15:39 +0000 (17:15 +0000)
447 files changed:
.phan/config.php
.phan/internal_stubs/README [new file with mode: 0644]
.phan/internal_stubs/imagick.phan_php [new file with mode: 0644]
.phan/internal_stubs/pcntl.phan_php [new file with mode: 0644]
.phan/internal_stubs/redis.phan_php [new file with mode: 0644]
.phan/internal_stubs/sockets.phan_php [new file with mode: 0644]
.travis.yml
RELEASE-NOTES-1.34
autoload.php
composer.json
docs/extension.schema.v2.json
docs/hooks.txt
docs/linkcache.txt
docs/magicword.txt
docs/memcached.txt
docs/php-memcached/Documentation
img_auth.php
includes/AutoLoader.php
includes/Autopromote.php
includes/DefaultSettings.php
includes/Defines.php
includes/DevelopmentSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/Html.php
includes/Linker.php
includes/MediaWiki.php
includes/Message.php [deleted file]
includes/OutputHandler.php
includes/OutputPage.php
includes/PHPVersionCheck.php
includes/PathRouter.php
includes/Permissions/PermissionManager.php
includes/Rest/CopyableStreamInterface.php [new file with mode: 0644]
includes/Rest/EntryPoint.php [new file with mode: 0644]
includes/Rest/Handler.php [new file with mode: 0644]
includes/Rest/Handler/HelloHandler.php [new file with mode: 0644]
includes/Rest/HeaderContainer.php [new file with mode: 0644]
includes/Rest/HttpException.php [new file with mode: 0644]
includes/Rest/JsonEncodingException.php [new file with mode: 0644]
includes/Rest/PathTemplateMatcher/PathConflict.php [new file with mode: 0644]
includes/Rest/PathTemplateMatcher/PathMatcher.php [new file with mode: 0644]
includes/Rest/RequestBase.php [new file with mode: 0644]
includes/Rest/RequestData.php [new file with mode: 0644]
includes/Rest/RequestFromGlobals.php [new file with mode: 0644]
includes/Rest/RequestInterface.php [new file with mode: 0644]
includes/Rest/Response.php [new file with mode: 0644]
includes/Rest/ResponseFactory.php [new file with mode: 0644]
includes/Rest/ResponseInterface.php [new file with mode: 0644]
includes/Rest/Router.php [new file with mode: 0644]
includes/Rest/SimpleHandler.php [new file with mode: 0644]
includes/Rest/Stream.php [new file with mode: 0644]
includes/Rest/StringStream.php [new file with mode: 0644]
includes/Rest/coreRoutes.json [new file with mode: 0644]
includes/Revision/RenderedRevision.php
includes/Revision/RevisionRecord.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/Setup.php
includes/Storage/BlobStoreFactory.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/PageEditStash.php
includes/Storage/PageUpdater.php
includes/Storage/SqlBlobStore.php
includes/Title.php
includes/TrackingCategories.php
includes/WebRequest.php
includes/actions/HistoryAction.php
includes/actions/InfoAction.php
includes/actions/McrUndoAction.php
includes/actions/RawAction.php
includes/api/ApiCSPReport.php
includes/api/ApiLogin.php
includes/api/ApiMain.php
includes/api/ApiPageSet.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryLanguageinfo.php
includes/api/i18n/ar.json
includes/api/i18n/en.json
includes/api/i18n/es.json
includes/api/i18n/fa.json
includes/api/i18n/ja.json
includes/api/i18n/ko.json
includes/api/i18n/zh-hant.json
includes/block/BlockManager.php
includes/block/CompositeBlock.php [new file with mode: 0644]
includes/block/DatabaseBlock.php
includes/cache/LinkBatch.php
includes/changes/ChangesFeed.php
includes/changetags/ChangeTags.php
includes/config/ServiceOptions.php
includes/debug/logger/monolog/CeeFormatter.php
includes/deferred/LinksUpdate.php
includes/deferred/UserEditCountUpdate.php
includes/diff/DifferenceEngine.php
includes/export/WikiExporter.php
includes/externalstore/ExternalStore.php
includes/externalstore/ExternalStoreDB.php
includes/externalstore/ExternalStoreMwstore.php
includes/filerepo/file/ForeignDBFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/filerepo/file/UnregisteredLocalFile.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLSelectAndOtherField.php
includes/import/WikiImporter.php
includes/installer/CliInstaller.php
includes/installer/Installer.php
includes/installer/PostgresUpdater.php
includes/installer/WebInstaller.php
includes/installer/WebInstallerOptions.php
includes/installer/WebInstallerOutput.php
includes/installer/i18n/ar.json
includes/installer/i18n/be-tarask.json
includes/installer/i18n/ca.json
includes/installer/i18n/cs.json
includes/installer/i18n/fa.json
includes/installer/i18n/fr.json
includes/installer/i18n/io.json
includes/installer/i18n/it.json
includes/installer/i18n/ja.json
includes/installer/i18n/nl.json
includes/installer/i18n/pl.json
includes/installer/i18n/pt-br.json
includes/installer/i18n/sl.json
includes/installer/i18n/sr-ec.json
includes/jobqueue/JobRunner.php
includes/jobqueue/JobSpecification.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/language/LanguageCode.php [new file with mode: 0644]
includes/language/Message.php [new file with mode: 0644]
includes/language/MessageLocalizer.php [new file with mode: 0644]
includes/libs/ParamValidator/Callbacks.php [new file with mode: 0644]
includes/libs/ParamValidator/ParamValidator.php [new file with mode: 0644]
includes/libs/ParamValidator/README.md [new file with mode: 0644]
includes/libs/ParamValidator/SimpleCallbacks.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/BooleanDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/EnumDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/FloatDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/IntegerDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/LimitDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/PasswordDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/PresenceBooleanDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/StringDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/TimestampDef.php [new file with mode: 0644]
includes/libs/ParamValidator/TypeDef/UploadDef.php [new file with mode: 0644]
includes/libs/ParamValidator/Util/UploadedFile.php [new file with mode: 0644]
includes/libs/ParamValidator/Util/UploadedFileStream.php [new file with mode: 0644]
includes/libs/ParamValidator/ValidationException.php [new file with mode: 0644]
includes/libs/StatusValue.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/APCUBagOStuff.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/MemcachedClient.php
includes/libs/objectcache/MemcachedPeclBagOStuff.php
includes/libs/objectcache/MemcachedPhpBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/RESTBagOStuff.php
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/objectcache/serialized/SerializedValueContainer.php [new file with mode: 0644]
includes/libs/rdbms/ChronologyProtector.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/libs/replacers/DoubleReplacer.php [deleted file]
includes/libs/replacers/HashtableReplacer.php [deleted file]
includes/libs/replacers/RegexlikeReplacer.php [deleted file]
includes/libs/replacers/Replacer.php [deleted file]
includes/libs/stats/BufferingStatsdDataFactory.php
includes/logging/BlockLogFormatter.php
includes/logging/DeleteLogFormatter.php
includes/media/TransformationalImageHandler.php
includes/objectcache/SqlBagOStuff.php
includes/page/ImagePage.php
includes/page/PageArchive.php
includes/page/WikiFilePage.php
includes/page/WikiPage.php
includes/parser/PPCustomFrame_DOM.php
includes/parser/PPFrame_DOM.php
includes/parser/PPNode_DOM.php
includes/parser/PPTemplateFrame_DOM.php
includes/parser/Parser.php
includes/parser/ParserOptions.php
includes/parser/ParserOutput.php
includes/parser/Preprocessor_DOM.php
includes/parser/Sanitizer.php
includes/password/LayeredParameterizedPassword.php
includes/rcfeed/RedisPubSubFeedEngine.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderCircularDependencyError.php [new file with mode: 0644]
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderImage.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/resourceloader/ResourceLoaderLessVarFileModule.php
includes/resourceloader/ResourceLoaderModule.php
includes/resourceloader/ResourceLoaderOOUIIconPackModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/PrefixSearch.php
includes/search/SearchEngine.php
includes/search/SearchHighlighter.php
includes/search/SearchResult.php
includes/search/SearchResultSet.php
includes/search/SqlSearchResultSet.php
includes/session/SessionProvider.php
includes/shell/Command.php
includes/skins/Skin.php
includes/skins/SkinFactory.php
includes/skins/SkinTemplate.php
includes/specialpage/QueryPage.php
includes/specialpage/SpecialPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialEmailUser.php
includes/specials/SpecialExport.php
includes/specials/SpecialJavaScriptTest.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialVersion.php
includes/specials/pagers/ImageListPager.php
includes/upload/UploadStash.php
includes/user/BotPassword.php
includes/user/User.php
includes/utils/ClassCollector.php
includes/widget/search/FullSearchResultWidget.php
includes/widget/search/InterwikiSearchResultWidget.php
includes/widget/search/SearchResultWidget.php
includes/widget/search/SimpleSearchResultWidget.php
languages/Language.php
languages/LanguageCode.php [deleted file]
languages/MessageLocalizer.php [deleted file]
languages/classes/LanguageZh.php
languages/i18n/aeb-arab.json
languages/i18n/an.json
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/arz.json
languages/i18n/as.json
languages/i18n/ast.json
languages/i18n/az.json
languages/i18n/bcc.json
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/ca.json
languages/i18n/cdo.json
languages/i18n/ckb.json
languages/i18n/crh-cyrl.json
languages/i18n/crh-latn.json
languages/i18n/cs.json
languages/i18n/cv.json
languages/i18n/diq.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/exif/mai.json
languages/i18n/exif/qqq.json
languages/i18n/exif/sr-ec.json
languages/i18n/exif/zh-hans.json
languages/i18n/fa.json
languages/i18n/fr.json
languages/i18n/fy.json
languages/i18n/gcr.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/ht.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/hyw.json
languages/i18n/ia.json
languages/i18n/io.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/kiu.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/lki.json
languages/i18n/lrc.json
languages/i18n/luz.json
languages/i18n/lv.json
languages/i18n/lzh.json
languages/i18n/mai.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/my.json
languages/i18n/nan.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/nqo.json
languages/i18n/or.json
languages/i18n/pl.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/rue.json
languages/i18n/sdc.json
languages/i18n/sh.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sv.json
languages/i18n/sw.json
languages/i18n/tr.json
languages/i18n/uk.json
languages/i18n/vec.json
languages/i18n/yo.json
languages/i18n/yue.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesAz.php
maintenance/convertUserOptions.php
maintenance/deduplicateArchiveRevId.php
maintenance/generateSitemap.php
maintenance/importTextFiles.php
maintenance/migrateArchiveText.php
maintenance/namespaceDupes.php
maintenance/populateArchiveRevId.php
maintenance/populateContentTables.php
maintenance/purgeChangedPages.php
maintenance/sql.php
maintenance/userDupes.inc
mw-config/config.css
mw-config/config.js
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.textSelection.js
resources/src/mediawiki.Uri/Uri.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less
resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.js
resources/src/mediawiki.special.userlogin.signup.styles/signup.css
resources/src/startup/startup.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/MediaWikiUnitTestCase.php [new file with mode: 0644]
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php
tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Rest/EntryPointTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/Handler/HelloHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/HeaderContainerTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/ResponseFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/StringStreamTest.php [new file with mode: 0644]
tests/phpunit/includes/Rest/testRoutes.json [new file with mode: 0644]
tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
tests/phpunit/includes/Revision/RevisionStoreTest.php
tests/phpunit/includes/Revision/SlotRoleHandlerTest.php
tests/phpunit/includes/Revision/SlotRoleRegistryTest.php
tests/phpunit/includes/StatusTest.php
tests/phpunit/includes/Storage/NameTableStoreTest.php
tests/phpunit/includes/TestLogger.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/api/ApiQueryLanguageinfoTest.php
tests/phpunit/includes/api/ApiStashEditTest.php
tests/phpunit/includes/api/query/ApiQueryTestBase.php
tests/phpunit/includes/block/BlockManagerTest.php
tests/phpunit/includes/block/CompositeBlockTest.php [new file with mode: 0644]
tests/phpunit/includes/db/DatabaseSqliteTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/db/LoadBalancerTest.php
tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php [new file with mode: 0644]
tests/phpunit/includes/jobqueue/JobQueueTest.php
tests/phpunit/includes/libs/HashRingTest.php
tests/phpunit/includes/libs/IPTest.php
tests/phpunit/includes/libs/ParamValidator/ParamValidatorTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/SimpleCallbacksTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/BooleanDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/EnumDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/FloatDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/IntegerDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/LimitDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/PasswordDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/PresenceBooleanDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/StringDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/TimestampDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/TypeDefTestCase.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDef/UploadDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/TypeDefTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileStreamTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTestBase.php [new file with mode: 0644]
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php
tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php
tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/logging/BlockLogFormatterTest.php
tests/phpunit/includes/logging/DeleteLogFormatterTest.php
tests/phpunit/includes/logging/LogFormatterTestCase.php
tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
tests/phpunit/includes/parser/PreprocessorTest.php
tests/phpunit/includes/parser/SanitizerTest.php
tests/phpunit/includes/password/PasswordFactoryTest.php [deleted file]
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php
tests/phpunit/includes/shell/CommandTest.php
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/includes/user/PasswordResetTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/utils/BatchRowUpdateTest.php
tests/phpunit/includes/utils/ClassCollectorTest.php
tests/phpunit/includes/utils/UIDGeneratorTest.php
tests/phpunit/maintenance/categoriesRdfTest.php
tests/phpunit/structure/AutoLoaderStructureTest.php
tests/phpunit/structure/ResourcesTest.php
tests/phpunit/suite.xml
tests/phpunit/unit-tests.xml [new file with mode: 0644]
tests/phpunit/unit/includes/password/PasswordFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/initUnitTests.php [new file with mode: 0644]
tests/qunit/data/load.mock.php
tests/qunit/data/styleTest.css.php
tests/selenium/specs/rollback.js
thumb.php

index 3478977..8746ada 100644 (file)
@@ -43,8 +43,12 @@ $cfg['file_list'] = array_merge(
 );
 
 $cfg['autoload_internal_extension_signatures'] = [
+       'imagick' => '.phan/internal_stubs/imagick.phan_php',
        'memcached' => '.phan/internal_stubs/memcached.phan_php',
        'oci8' => '.phan/internal_stubs/oci8.phan_php',
+       'pcntl' => '.phan/internal_stubs/pcntl.phan_php',
+       'redis' => '.phan/internal_stubs/redis.phan_php',
+       'sockets' => '.phan/internal_stubs/sockets.phan_php',
        'sqlsrv' => '.phan/internal_stubs/sqlsrv.phan_php',
        'tideways' => '.phan/internal_stubs/tideways.phan_php',
 ];
diff --git a/.phan/internal_stubs/README b/.phan/internal_stubs/README
new file mode 100644 (file)
index 0000000..c57d596
--- /dev/null
@@ -0,0 +1,5 @@
+See <https://github.com/phan/phan/wiki/How-To-Use-Stubs#generating-stubs> for
+how to generate internal stubs for phan.
+
+The stubs should be generated using the PHP version that is our lowest
+requirement.
diff --git a/.phan/internal_stubs/imagick.phan_php b/.phan/internal_stubs/imagick.phan_php
new file mode 100644 (file)
index 0000000..c4f355b
--- /dev/null
@@ -0,0 +1,1204 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension imagick@3.4.3RC2
+
+namespace {
+class Imagick implements \Iterator, \Traversable, \Countable {
+
+    // constants
+    const COLOR_BLACK = 11;
+    const COLOR_BLUE = 12;
+    const COLOR_CYAN = 13;
+    const COLOR_GREEN = 14;
+    const COLOR_RED = 15;
+    const COLOR_YELLOW = 16;
+    const COLOR_MAGENTA = 17;
+    const COLOR_OPACITY = 18;
+    const COLOR_ALPHA = 19;
+    const COLOR_FUZZ = 20;
+    const IMAGICK_EXTNUM = 30403;
+    const IMAGICK_EXTVER = '3.4.3RC2';
+    const QUANTUM_RANGE = 65535;
+    const USE_ZEND_MM = 0;
+    const COMPOSITE_DEFAULT = 40;
+    const COMPOSITE_UNDEFINED = 0;
+    const COMPOSITE_NO = 1;
+    const COMPOSITE_ADD = 2;
+    const COMPOSITE_ATOP = 3;
+    const COMPOSITE_BLEND = 4;
+    const COMPOSITE_BUMPMAP = 5;
+    const COMPOSITE_CLEAR = 7;
+    const COMPOSITE_COLORBURN = 8;
+    const COMPOSITE_COLORDODGE = 9;
+    const COMPOSITE_COLORIZE = 10;
+    const COMPOSITE_COPYBLACK = 11;
+    const COMPOSITE_COPYBLUE = 12;
+    const COMPOSITE_COPY = 13;
+    const COMPOSITE_COPYCYAN = 14;
+    const COMPOSITE_COPYGREEN = 15;
+    const COMPOSITE_COPYMAGENTA = 16;
+    const COMPOSITE_COPYOPACITY = 17;
+    const COMPOSITE_COPYRED = 18;
+    const COMPOSITE_COPYYELLOW = 19;
+    const COMPOSITE_DARKEN = 20;
+    const COMPOSITE_DSTATOP = 21;
+    const COMPOSITE_DST = 22;
+    const COMPOSITE_DSTIN = 23;
+    const COMPOSITE_DSTOUT = 24;
+    const COMPOSITE_DSTOVER = 25;
+    const COMPOSITE_DIFFERENCE = 26;
+    const COMPOSITE_DISPLACE = 27;
+    const COMPOSITE_DISSOLVE = 28;
+    const COMPOSITE_EXCLUSION = 29;
+    const COMPOSITE_HARDLIGHT = 30;
+    const COMPOSITE_HUE = 31;
+    const COMPOSITE_IN = 32;
+    const COMPOSITE_LIGHTEN = 33;
+    const COMPOSITE_LUMINIZE = 35;
+    const COMPOSITE_MINUS = 36;
+    const COMPOSITE_MODULATE = 37;
+    const COMPOSITE_MULTIPLY = 38;
+    const COMPOSITE_OUT = 39;
+    const COMPOSITE_OVER = 40;
+    const COMPOSITE_OVERLAY = 41;
+    const COMPOSITE_PLUS = 42;
+    const COMPOSITE_REPLACE = 43;
+    const COMPOSITE_SATURATE = 44;
+    const COMPOSITE_SCREEN = 45;
+    const COMPOSITE_SOFTLIGHT = 46;
+    const COMPOSITE_SRCATOP = 47;
+    const COMPOSITE_SRC = 48;
+    const COMPOSITE_SRCIN = 49;
+    const COMPOSITE_SRCOUT = 50;
+    const COMPOSITE_SRCOVER = 51;
+    const COMPOSITE_SUBTRACT = 52;
+    const COMPOSITE_THRESHOLD = 53;
+    const COMPOSITE_XOR = 54;
+    const COMPOSITE_CHANGEMASK = 6;
+    const COMPOSITE_LINEARLIGHT = 34;
+    const COMPOSITE_DIVIDE = 55;
+    const COMPOSITE_DISTORT = 56;
+    const COMPOSITE_BLUR = 57;
+    const COMPOSITE_PEGTOPLIGHT = 58;
+    const COMPOSITE_VIVIDLIGHT = 59;
+    const COMPOSITE_PINLIGHT = 60;
+    const COMPOSITE_LINEARDODGE = 61;
+    const COMPOSITE_LINEARBURN = 62;
+    const COMPOSITE_MATHEMATICS = 63;
+    const COMPOSITE_MODULUSADD = 2;
+    const COMPOSITE_MODULUSSUBTRACT = 52;
+    const COMPOSITE_MINUSDST = 36;
+    const COMPOSITE_DIVIDEDST = 55;
+    const COMPOSITE_DIVIDESRC = 64;
+    const COMPOSITE_MINUSSRC = 65;
+    const COMPOSITE_DARKENINTENSITY = 66;
+    const COMPOSITE_LIGHTENINTENSITY = 67;
+    const COMPOSITE_HARDMIX = 68;
+    const MONTAGEMODE_FRAME = 1;
+    const MONTAGEMODE_UNFRAME = 2;
+    const MONTAGEMODE_CONCATENATE = 3;
+    const STYLE_NORMAL = 1;
+    const STYLE_ITALIC = 2;
+    const STYLE_OBLIQUE = 3;
+    const STYLE_ANY = 4;
+    const FILTER_UNDEFINED = 0;
+    const FILTER_POINT = 1;
+    const FILTER_BOX = 2;
+    const FILTER_TRIANGLE = 3;
+    const FILTER_HERMITE = 4;
+    const FILTER_HANNING = 5;
+    const FILTER_HAMMING = 6;
+    const FILTER_BLACKMAN = 7;
+    const FILTER_GAUSSIAN = 8;
+    const FILTER_QUADRATIC = 9;
+    const FILTER_CUBIC = 10;
+    const FILTER_CATROM = 11;
+    const FILTER_MITCHELL = 12;
+    const FILTER_LANCZOS = 22;
+    const FILTER_BESSEL = 13;
+    const FILTER_SINC = 14;
+    const FILTER_KAISER = 16;
+    const FILTER_WELSH = 17;
+    const FILTER_PARZEN = 18;
+    const FILTER_LAGRANGE = 21;
+    const FILTER_SENTINEL = 31;
+    const FILTER_BOHMAN = 19;
+    const FILTER_BARTLETT = 20;
+    const FILTER_JINC = 13;
+    const FILTER_SINCFAST = 15;
+    const FILTER_ROBIDOUX = 26;
+    const FILTER_LANCZOSSHARP = 23;
+    const FILTER_LANCZOS2 = 24;
+    const FILTER_LANCZOS2SHARP = 25;
+    const FILTER_ROBIDOUXSHARP = 27;
+    const FILTER_COSINE = 28;
+    const FILTER_SPLINE = 29;
+    const FILTER_LANCZOSRADIUS = 30;
+    const IMGTYPE_UNDEFINED = 0;
+    const IMGTYPE_BILEVEL = 1;
+    const IMGTYPE_GRAYSCALE = 2;
+    const IMGTYPE_GRAYSCALEMATTE = 3;
+    const IMGTYPE_PALETTE = 4;
+    const IMGTYPE_PALETTEMATTE = 5;
+    const IMGTYPE_TRUECOLOR = 6;
+    const IMGTYPE_TRUECOLORMATTE = 7;
+    const IMGTYPE_COLORSEPARATION = 8;
+    const IMGTYPE_COLORSEPARATIONMATTE = 9;
+    const IMGTYPE_OPTIMIZE = 10;
+    const IMGTYPE_PALETTEBILEVELMATTE = 11;
+    const RESOLUTION_UNDEFINED = 0;
+    const RESOLUTION_PIXELSPERINCH = 1;
+    const RESOLUTION_PIXELSPERCENTIMETER = 2;
+    const COMPRESSION_UNDEFINED = 0;
+    const COMPRESSION_NO = 1;
+    const COMPRESSION_BZIP = 2;
+    const COMPRESSION_FAX = 6;
+    const COMPRESSION_GROUP4 = 7;
+    const COMPRESSION_JPEG = 8;
+    const COMPRESSION_JPEG2000 = 9;
+    const COMPRESSION_LOSSLESSJPEG = 10;
+    const COMPRESSION_LZW = 11;
+    const COMPRESSION_RLE = 12;
+    const COMPRESSION_ZIP = 13;
+    const COMPRESSION_DXT1 = 3;
+    const COMPRESSION_DXT3 = 4;
+    const COMPRESSION_DXT5 = 5;
+    const COMPRESSION_ZIPS = 14;
+    const COMPRESSION_PIZ = 15;
+    const COMPRESSION_PXR24 = 16;
+    const COMPRESSION_B44 = 17;
+    const COMPRESSION_B44A = 18;
+    const COMPRESSION_LZMA = 19;
+    const COMPRESSION_JBIG1 = 20;
+    const COMPRESSION_JBIG2 = 21;
+    const PAINT_POINT = 1;
+    const PAINT_REPLACE = 2;
+    const PAINT_FLOODFILL = 3;
+    const PAINT_FILLTOBORDER = 4;
+    const PAINT_RESET = 5;
+    const GRAVITY_NORTHWEST = 1;
+    const GRAVITY_NORTH = 2;
+    const GRAVITY_NORTHEAST = 3;
+    const GRAVITY_WEST = 4;
+    const GRAVITY_CENTER = 5;
+    const GRAVITY_EAST = 6;
+    const GRAVITY_SOUTHWEST = 7;
+    const GRAVITY_SOUTH = 8;
+    const GRAVITY_SOUTHEAST = 9;
+    const GRAVITY_FORGET = 0;
+    const GRAVITY_STATIC = 10;
+    const STRETCH_NORMAL = 1;
+    const STRETCH_ULTRACONDENSED = 2;
+    const STRETCH_EXTRACONDENSED = 3;
+    const STRETCH_CONDENSED = 4;
+    const STRETCH_SEMICONDENSED = 5;
+    const STRETCH_SEMIEXPANDED = 6;
+    const STRETCH_EXPANDED = 7;
+    const STRETCH_EXTRAEXPANDED = 8;
+    const STRETCH_ULTRAEXPANDED = 9;
+    const STRETCH_ANY = 10;
+    const ALIGN_UNDEFINED = 0;
+    const ALIGN_LEFT = 1;
+    const ALIGN_CENTER = 2;
+    const ALIGN_RIGHT = 3;
+    const DECORATION_NO = 1;
+    const DECORATION_UNDERLINE = 2;
+    const DECORATION_OVERLINE = 3;
+    const DECORATION_LINETROUGH = 4;
+    const DECORATION_LINETHROUGH = 4;
+    const NOISE_UNIFORM = 1;
+    const NOISE_GAUSSIAN = 2;
+    const NOISE_MULTIPLICATIVEGAUSSIAN = 3;
+    const NOISE_IMPULSE = 4;
+    const NOISE_LAPLACIAN = 5;
+    const NOISE_POISSON = 6;
+    const NOISE_RANDOM = 7;
+    const CHANNEL_UNDEFINED = 0;
+    const CHANNEL_RED = 1;
+    const CHANNEL_GRAY = 1;
+    const CHANNEL_CYAN = 1;
+    const CHANNEL_GREEN = 2;
+    const CHANNEL_MAGENTA = 2;
+    const CHANNEL_BLUE = 4;
+    const CHANNEL_YELLOW = 4;
+    const CHANNEL_ALPHA = 8;
+    const CHANNEL_OPACITY = 8;
+    const CHANNEL_MATTE = 8;
+    const CHANNEL_BLACK = 32;
+    const CHANNEL_INDEX = 32;
+    const CHANNEL_ALL = 134217727;
+    const CHANNEL_DEFAULT = 134217719;
+    const CHANNEL_RGBA = 15;
+    const CHANNEL_TRUEALPHA = 64;
+    const CHANNEL_RGBS = 128;
+    const CHANNEL_GRAY_CHANNELS = 128;
+    const CHANNEL_SYNC = 256;
+    const CHANNEL_COMPOSITES = 47;
+    const METRIC_UNDEFINED = 0;
+    const METRIC_ABSOLUTEERRORMETRIC = 1;
+    const METRIC_MEANABSOLUTEERROR = 2;
+    const METRIC_MEANERRORPERPIXELMETRIC = 3;
+    const METRIC_MEANSQUAREERROR = 4;
+    const METRIC_PEAKABSOLUTEERROR = 5;
+    const METRIC_PEAKSIGNALTONOISERATIO = 6;
+    const METRIC_ROOTMEANSQUAREDERROR = 7;
+    const METRIC_NORMALIZEDCROSSCORRELATIONERRORMETRIC = 8;
+    const METRIC_FUZZERROR = 9;
+    const METRIC_PERCEPTUALHASH_ERROR = 255;
+    const PIXEL_CHAR = 1;
+    const PIXEL_DOUBLE = 2;
+    const PIXEL_FLOAT = 3;
+    const PIXEL_INTEGER = 4;
+    const PIXEL_LONG = 5;
+    const PIXEL_QUANTUM = 6;
+    const PIXEL_SHORT = 7;
+    const EVALUATE_UNDEFINED = 0;
+    const EVALUATE_ADD = 1;
+    const EVALUATE_AND = 2;
+    const EVALUATE_DIVIDE = 3;
+    const EVALUATE_LEFTSHIFT = 4;
+    const EVALUATE_MAX = 5;
+    const EVALUATE_MIN = 6;
+    const EVALUATE_MULTIPLY = 7;
+    const EVALUATE_OR = 8;
+    const EVALUATE_RIGHTSHIFT = 9;
+    const EVALUATE_SET = 10;
+    const EVALUATE_SUBTRACT = 11;
+    const EVALUATE_XOR = 12;
+    const EVALUATE_POW = 13;
+    const EVALUATE_LOG = 14;
+    const EVALUATE_THRESHOLD = 15;
+    const EVALUATE_THRESHOLDBLACK = 16;
+    const EVALUATE_THRESHOLDWHITE = 17;
+    const EVALUATE_GAUSSIANNOISE = 18;
+    const EVALUATE_IMPULSENOISE = 19;
+    const EVALUATE_LAPLACIANNOISE = 20;
+    const EVALUATE_MULTIPLICATIVENOISE = 21;
+    const EVALUATE_POISSONNOISE = 22;
+    const EVALUATE_UNIFORMNOISE = 23;
+    const EVALUATE_COSINE = 24;
+    const EVALUATE_SINE = 25;
+    const EVALUATE_ADDMODULUS = 26;
+    const EVALUATE_MEAN = 27;
+    const EVALUATE_ABS = 28;
+    const EVALUATE_EXPONENTIAL = 29;
+    const EVALUATE_MEDIAN = 30;
+    const EVALUATE_SUM = 31;
+    const EVALUATE_ROOT_MEAN_SQUARE = 32;
+    const COLORSPACE_UNDEFINED = 0;
+    const COLORSPACE_RGB = 1;
+    const COLORSPACE_GRAY = 2;
+    const COLORSPACE_TRANSPARENT = 3;
+    const COLORSPACE_OHTA = 4;
+    const COLORSPACE_LAB = 5;
+    const COLORSPACE_XYZ = 6;
+    const COLORSPACE_YCBCR = 7;
+    const COLORSPACE_YCC = 8;
+    const COLORSPACE_YIQ = 9;
+    const COLORSPACE_YPBPR = 10;
+    const COLORSPACE_YUV = 11;
+    const COLORSPACE_CMYK = 12;
+    const COLORSPACE_SRGB = 13;
+    const COLORSPACE_HSB = 14;
+    const COLORSPACE_HSL = 15;
+    const COLORSPACE_HWB = 16;
+    const COLORSPACE_REC601LUMA = 17;
+    const COLORSPACE_REC709LUMA = 19;
+    const COLORSPACE_LOG = 21;
+    const COLORSPACE_CMY = 22;
+    const COLORSPACE_LUV = 23;
+    const COLORSPACE_HCL = 24;
+    const COLORSPACE_LCH = 25;
+    const COLORSPACE_LMS = 26;
+    const COLORSPACE_LCHAB = 27;
+    const COLORSPACE_LCHUV = 28;
+    const COLORSPACE_SCRGB = 29;
+    const COLORSPACE_HSI = 30;
+    const COLORSPACE_HSV = 31;
+    const COLORSPACE_HCLP = 32;
+    const COLORSPACE_YDBDR = 33;
+    const COLORSPACE_REC601YCBCR = 18;
+    const COLORSPACE_REC709YCBCR = 20;
+    const COLORSPACE_XYY = 34;
+    const VIRTUALPIXELMETHOD_UNDEFINED = 0;
+    const VIRTUALPIXELMETHOD_BACKGROUND = 1;
+    const VIRTUALPIXELMETHOD_CONSTANT = 2;
+    const VIRTUALPIXELMETHOD_EDGE = 4;
+    const VIRTUALPIXELMETHOD_MIRROR = 5;
+    const VIRTUALPIXELMETHOD_TILE = 7;
+    const VIRTUALPIXELMETHOD_TRANSPARENT = 8;
+    const VIRTUALPIXELMETHOD_MASK = 9;
+    const VIRTUALPIXELMETHOD_BLACK = 10;
+    const VIRTUALPIXELMETHOD_GRAY = 11;
+    const VIRTUALPIXELMETHOD_WHITE = 12;
+    const VIRTUALPIXELMETHOD_HORIZONTALTILE = 13;
+    const VIRTUALPIXELMETHOD_VERTICALTILE = 14;
+    const VIRTUALPIXELMETHOD_HORIZONTALTILEEDGE = 15;
+    const VIRTUALPIXELMETHOD_VERTICALTILEEDGE = 16;
+    const VIRTUALPIXELMETHOD_CHECKERTILE = 17;
+    const PREVIEW_UNDEFINED = 0;
+    const PREVIEW_ROTATE = 1;
+    const PREVIEW_SHEAR = 2;
+    const PREVIEW_ROLL = 3;
+    const PREVIEW_HUE = 4;
+    const PREVIEW_SATURATION = 5;
+    const PREVIEW_BRIGHTNESS = 6;
+    const PREVIEW_GAMMA = 7;
+    const PREVIEW_SPIFF = 8;
+    const PREVIEW_DULL = 9;
+    const PREVIEW_GRAYSCALE = 10;
+    const PREVIEW_QUANTIZE = 11;
+    const PREVIEW_DESPECKLE = 12;
+    const PREVIEW_REDUCENOISE = 13;
+    const PREVIEW_ADDNOISE = 14;
+    const PREVIEW_SHARPEN = 15;
+    const PREVIEW_BLUR = 16;
+    const PREVIEW_THRESHOLD = 17;
+    const PREVIEW_EDGEDETECT = 18;
+    const PREVIEW_SPREAD = 19;
+    const PREVIEW_SOLARIZE = 20;
+    const PREVIEW_SHADE = 21;
+    const PREVIEW_RAISE = 22;
+    const PREVIEW_SEGMENT = 23;
+    const PREVIEW_SWIRL = 24;
+    const PREVIEW_IMPLODE = 25;
+    const PREVIEW_WAVE = 26;
+    const PREVIEW_OILPAINT = 27;
+    const PREVIEW_CHARCOALDRAWING = 28;
+    const PREVIEW_JPEG = 29;
+    const RENDERINGINTENT_UNDEFINED = 0;
+    const RENDERINGINTENT_SATURATION = 1;
+    const RENDERINGINTENT_PERCEPTUAL = 2;
+    const RENDERINGINTENT_ABSOLUTE = 3;
+    const RENDERINGINTENT_RELATIVE = 4;
+    const INTERLACE_UNDEFINED = 0;
+    const INTERLACE_NO = 1;
+    const INTERLACE_LINE = 2;
+    const INTERLACE_PLANE = 3;
+    const INTERLACE_PARTITION = 4;
+    const INTERLACE_GIF = 5;
+    const INTERLACE_JPEG = 6;
+    const INTERLACE_PNG = 7;
+    const FILLRULE_UNDEFINED = 0;
+    const FILLRULE_EVENODD = 1;
+    const FILLRULE_NONZERO = 2;
+    const PATHUNITS_UNDEFINED = 0;
+    const PATHUNITS_USERSPACE = 1;
+    const PATHUNITS_USERSPACEONUSE = 2;
+    const PATHUNITS_OBJECTBOUNDINGBOX = 3;
+    const LINECAP_UNDEFINED = 0;
+    const LINECAP_BUTT = 1;
+    const LINECAP_ROUND = 2;
+    const LINECAP_SQUARE = 3;
+    const LINEJOIN_UNDEFINED = 0;
+    const LINEJOIN_MITER = 1;
+    const LINEJOIN_ROUND = 2;
+    const LINEJOIN_BEVEL = 3;
+    const RESOURCETYPE_UNDEFINED = 0;
+    const RESOURCETYPE_AREA = 1;
+    const RESOURCETYPE_DISK = 2;
+    const RESOURCETYPE_FILE = 3;
+    const RESOURCETYPE_MAP = 4;
+    const RESOURCETYPE_MEMORY = 5;
+    const RESOURCETYPE_TIME = 7;
+    const RESOURCETYPE_THROTTLE = 8;
+    const RESOURCETYPE_THREAD = 6;
+    const RESOURCETYPE_WIDTH = 9;
+    const RESOURCETYPE_HEIGHT = 10;
+    const DISPOSE_UNRECOGNIZED = 0;
+    const DISPOSE_UNDEFINED = 0;
+    const DISPOSE_NONE = 1;
+    const DISPOSE_BACKGROUND = 2;
+    const DISPOSE_PREVIOUS = 3;
+    const INTERPOLATE_UNDEFINED = 0;
+    const INTERPOLATE_AVERAGE = 1;
+    const INTERPOLATE_BICUBIC = 2;
+    const INTERPOLATE_BILINEAR = 3;
+    const INTERPOLATE_FILTER = 4;
+    const INTERPOLATE_INTEGER = 5;
+    const INTERPOLATE_MESH = 6;
+    const INTERPOLATE_NEARESTNEIGHBOR = 7;
+    const INTERPOLATE_SPLINE = 8;
+    const INTERPOLATE_AVERAGE_9 = 9;
+    const INTERPOLATE_AVERAGE_16 = 10;
+    const INTERPOLATE_BLEND = 11;
+    const INTERPOLATE_BACKGROUND_COLOR = 12;
+    const INTERPOLATE_CATROM = 13;
+    const LAYERMETHOD_UNDEFINED = 0;
+    const LAYERMETHOD_COALESCE = 1;
+    const LAYERMETHOD_COMPAREANY = 2;
+    const LAYERMETHOD_COMPARECLEAR = 3;
+    const LAYERMETHOD_COMPAREOVERLAY = 4;
+    const LAYERMETHOD_DISPOSE = 5;
+    const LAYERMETHOD_OPTIMIZE = 6;
+    const LAYERMETHOD_OPTIMIZEPLUS = 8;
+    const LAYERMETHOD_OPTIMIZETRANS = 9;
+    const LAYERMETHOD_COMPOSITE = 12;
+    const LAYERMETHOD_OPTIMIZEIMAGE = 7;
+    const LAYERMETHOD_REMOVEDUPS = 10;
+    const LAYERMETHOD_REMOVEZERO = 11;
+    const LAYERMETHOD_TRIMBOUNDS = 16;
+    const ORIENTATION_UNDEFINED = 0;
+    const ORIENTATION_TOPLEFT = 1;
+    const ORIENTATION_TOPRIGHT = 2;
+    const ORIENTATION_BOTTOMRIGHT = 3;
+    const ORIENTATION_BOTTOMLEFT = 4;
+    const ORIENTATION_LEFTTOP = 5;
+    const ORIENTATION_RIGHTTOP = 6;
+    const ORIENTATION_RIGHTBOTTOM = 7;
+    const ORIENTATION_LEFTBOTTOM = 8;
+    const DISTORTION_UNDEFINED = 0;
+    const DISTORTION_AFFINE = 1;
+    const DISTORTION_AFFINEPROJECTION = 2;
+    const DISTORTION_ARC = 9;
+    const DISTORTION_BILINEAR = 6;
+    const DISTORTION_PERSPECTIVE = 4;
+    const DISTORTION_PERSPECTIVEPROJECTION = 5;
+    const DISTORTION_SCALEROTATETRANSLATE = 3;
+    const DISTORTION_POLYNOMIAL = 8;
+    const DISTORTION_POLAR = 10;
+    const DISTORTION_DEPOLAR = 11;
+    const DISTORTION_BARREL = 14;
+    const DISTORTION_SHEPARDS = 16;
+    const DISTORTION_SENTINEL = 18;
+    const DISTORTION_BARRELINVERSE = 15;
+    const DISTORTION_BILINEARFORWARD = 6;
+    const DISTORTION_BILINEARREVERSE = 7;
+    const DISTORTION_RESIZE = 17;
+    const DISTORTION_CYLINDER2PLANE = 12;
+    const DISTORTION_PLANE2CYLINDER = 13;
+    const LAYERMETHOD_MERGE = 13;
+    const LAYERMETHOD_FLATTEN = 14;
+    const LAYERMETHOD_MOSAIC = 15;
+    const ALPHACHANNEL_ACTIVATE = 1;
+    const ALPHACHANNEL_RESET = 7;
+    const ALPHACHANNEL_SET = 8;
+    const ALPHACHANNEL_UNDEFINED = 0;
+    const ALPHACHANNEL_COPY = 3;
+    const ALPHACHANNEL_DEACTIVATE = 4;
+    const ALPHACHANNEL_EXTRACT = 5;
+    const ALPHACHANNEL_OPAQUE = 6;
+    const ALPHACHANNEL_SHAPE = 9;
+    const ALPHACHANNEL_TRANSPARENT = 10;
+    const ALPHACHANNEL_ASSOCIATE = 13;
+    const ALPHACHANNEL_DISSOCIATE = 14;
+    const SPARSECOLORMETHOD_UNDEFINED = 0;
+    const SPARSECOLORMETHOD_BARYCENTRIC = 1;
+    const SPARSECOLORMETHOD_BILINEAR = 7;
+    const SPARSECOLORMETHOD_POLYNOMIAL = 8;
+    const SPARSECOLORMETHOD_SPEPARDS = 16;
+    const SPARSECOLORMETHOD_VORONOI = 18;
+    const SPARSECOLORMETHOD_INVERSE = 19;
+    const SPARSECOLORMETHOD_MANHATTAN = 20;
+    const DITHERMETHOD_UNDEFINED = 0;
+    const DITHERMETHOD_NO = 1;
+    const DITHERMETHOD_RIEMERSMA = 2;
+    const DITHERMETHOD_FLOYDSTEINBERG = 3;
+    const FUNCTION_UNDEFINED = 0;
+    const FUNCTION_POLYNOMIAL = 1;
+    const FUNCTION_SINUSOID = 2;
+    const ALPHACHANNEL_BACKGROUND = 2;
+    const FUNCTION_ARCSIN = 3;
+    const FUNCTION_ARCTAN = 4;
+    const ALPHACHANNEL_FLATTEN = 11;
+    const ALPHACHANNEL_REMOVE = 12;
+    const STATISTIC_GRADIENT = 1;
+    const STATISTIC_MAXIMUM = 2;
+    const STATISTIC_MEAN = 3;
+    const STATISTIC_MEDIAN = 4;
+    const STATISTIC_MINIMUM = 5;
+    const STATISTIC_MODE = 6;
+    const STATISTIC_NONPEAK = 7;
+    const STATISTIC_STANDARD_DEVIATION = 8;
+    const STATISTIC_ROOT_MEAN_SQUARE = 9;
+    const MORPHOLOGY_CONVOLVE = 1;
+    const MORPHOLOGY_CORRELATE = 2;
+    const MORPHOLOGY_ERODE = 3;
+    const MORPHOLOGY_DILATE = 4;
+    const MORPHOLOGY_ERODE_INTENSITY = 5;
+    const MORPHOLOGY_DILATE_INTENSITY = 6;
+    const MORPHOLOGY_DISTANCE = 7;
+    const MORPHOLOGY_OPEN = 8;
+    const MORPHOLOGY_CLOSE = 9;
+    const MORPHOLOGY_OPEN_INTENSITY = 10;
+    const MORPHOLOGY_CLOSE_INTENSITY = 11;
+    const MORPHOLOGY_SMOOTH = 12;
+    const MORPHOLOGY_EDGE_IN = 13;
+    const MORPHOLOGY_EDGE_OUT = 14;
+    const MORPHOLOGY_EDGE = 15;
+    const MORPHOLOGY_TOP_HAT = 16;
+    const MORPHOLOGY_BOTTOM_HAT = 17;
+    const MORPHOLOGY_HIT_AND_MISS = 18;
+    const MORPHOLOGY_THINNING = 19;
+    const MORPHOLOGY_THICKEN = 20;
+    const MORPHOLOGY_VORONOI = 21;
+    const MORPHOLOGY_ITERATIVE = 22;
+    const KERNEL_UNITY = 1;
+    const KERNEL_GAUSSIAN = 2;
+    const KERNEL_DIFFERENCE_OF_GAUSSIANS = 3;
+    const KERNEL_LAPLACIAN_OF_GAUSSIANS = 4;
+    const KERNEL_BLUR = 5;
+    const KERNEL_COMET = 6;
+    const KERNEL_LAPLACIAN = 7;
+    const KERNEL_SOBEL = 8;
+    const KERNEL_FREI_CHEN = 9;
+    const KERNEL_ROBERTS = 10;
+    const KERNEL_PREWITT = 11;
+    const KERNEL_COMPASS = 12;
+    const KERNEL_KIRSCH = 13;
+    const KERNEL_DIAMOND = 14;
+    const KERNEL_SQUARE = 15;
+    const KERNEL_RECTANGLE = 16;
+    const KERNEL_OCTAGON = 17;
+    const KERNEL_DISK = 18;
+    const KERNEL_PLUS = 19;
+    const KERNEL_CROSS = 20;
+    const KERNEL_RING = 21;
+    const KERNEL_PEAKS = 22;
+    const KERNEL_EDGES = 23;
+    const KERNEL_CORNERS = 24;
+    const KERNEL_DIAGONALS = 25;
+    const KERNEL_LINE_ENDS = 26;
+    const KERNEL_LINE_JUNCTIONS = 27;
+    const KERNEL_RIDGES = 28;
+    const KERNEL_CONVEX_HULL = 29;
+    const KERNEL_THIN_SE = 30;
+    const KERNEL_SKELETON = 31;
+    const KERNEL_CHEBYSHEV = 32;
+    const KERNEL_MANHATTAN = 33;
+    const KERNEL_OCTAGONAL = 34;
+    const KERNEL_EUCLIDEAN = 35;
+    const KERNEL_USER_DEFINED = 36;
+    const KERNEL_BINOMIAL = 37;
+    const DIRECTION_LEFT_TO_RIGHT = 2;
+    const DIRECTION_RIGHT_TO_LEFT = 1;
+    const NORMALIZE_KERNEL_NONE = 0;
+    const NORMALIZE_KERNEL_VALUE = 8192;
+    const NORMALIZE_KERNEL_CORRELATE = 65536;
+    const NORMALIZE_KERNEL_PERCENT = 4096;
+
+    // methods
+    public function optimizeimagelayers() {}
+    public function compareimagelayers($LAYER) {}
+    public function pingimageblob($imageContents) {}
+    public function pingimagefile($fp) {}
+    public function transposeimage() {}
+    public function transverseimage() {}
+    public function trimimage($fuzz) {}
+    public function waveimage($amplitude, $waveLenght) {}
+    public function vignetteimage($blackPoint, $whitePoint, $x, $y) {}
+    public function uniqueimagecolors() {}
+    public function getimagematte() {}
+    public function setimagematte($enable) {}
+    public function adaptiveresizeimage($columns, $rows, $bestfit = null, $legacy = null) {}
+    public function sketchimage($radius, $sigma, $angle) {}
+    public function shadeimage($gray, $azimuth, $elevation) {}
+    public function getsizeoffset() {}
+    public function setsizeoffset($columns, $rows, $offset) {}
+    public function adaptiveblurimage($radius, $sigma, $CHANNEL = null) {}
+    public function contraststretchimage($blackPoint, $whitePoint, $CHANNEL = null) {}
+    public function adaptivesharpenimage($radius, $sigma, $CHANNEL = null) {}
+    public function randomthresholdimage($low, $high, $CHANNELTYPE = null) {}
+    public function roundcornersimage($xRounding, $yRounding, $strokeWidth = null, $displace = null, $sizeCorrection = null) {}
+    public function roundcorners($xRounding, $yRounding, $strokeWidth = null, $displace = null, $sizeCorrection = null) {}
+    public function setiteratorindex($index) {}
+    public function getiteratorindex() {}
+    public function transformimage($crop, $geometry) {}
+    public function setimageopacity($opacity) {}
+    public function orderedposterizeimage($threshold_map, $CHANNEL = null) {}
+    public function polaroidimage(\ImagickDraw $ImagickDraw, $angle) {}
+    public function getimageproperty($name) {}
+    public function setimageproperty($name, $value) {}
+    public function deleteimageproperty($name) {}
+    public function identifyformat($embedText) {}
+    public function setimageinterpolatemethod($INTERPOLATE) {}
+    public function getimageinterpolatemethod() {}
+    public function linearstretchimage($blackPoint, $whitePoint) {}
+    public function getimagelength() {}
+    public function extentimage($width, $height, $x, $y) {}
+    public function getimageorientation() {}
+    public function setimageorientation($ORIENTATION) {}
+    public function paintfloodfillimage($CHANNEL, $fill, $fuzz, $bordercolor, $x, $y) {}
+    public function clutimage(\Imagick $Imagick, $CHANNELTYPE = null) {}
+    public function getimageproperties($pattern = null, $values = null) {}
+    public function getimageprofiles($pattern = null, $values = null) {}
+    public function distortimage($method, $arguments, $bestfit) {}
+    public function writeimagefile($handle, $format = null) {}
+    public function writeimagesfile($handle, $format = null) {}
+    public function resetimagepage($page) {}
+    public function setimageclipmask(\Imagick $Imagick) {}
+    public function getimageclipmask() {}
+    public function animateimages($server_name) {}
+    public function recolorimage($matrix) {}
+    public function setfont($font) {}
+    public function getfont() {}
+    public function setpointsize($pointsize) {}
+    public function getpointsize() {}
+    public function mergeimagelayers($LAYERMETHOD) {}
+    public function setimagealphachannel($ALPHACHANNELTYPE) {}
+    public function floodfillpaintimage($fill, $fuzz, $bordercolor, $x, $y, $invert, $CHANNEL = null) {}
+    public function opaquepaintimage($target, $fill, $fuzz, $invert, $CHANNEL = null) {}
+    public function transparentpaintimage($target, $alpha, $fuzz, $invert) {}
+    public function liquidrescaleimage($columns, $rows, $delta_x, $rigidity) {}
+    public function encipherimage($passphrase) {}
+    public function decipherimage($passphrase) {}
+    public function setgravity($GRAVITY) {}
+    public function getgravity() {}
+    public function getimagechannelrange($CHANNEL) {}
+    public function getimagealphachannel() {}
+    public function getimagechanneldistortions(\Imagick $Imagick, $METRICTYPE = null, $CHANNEL = null) {}
+    public function setimagegravity($GRAVITY) {}
+    public function getimagegravity() {}
+    public function importimagepixels($x, $y, $width, $height, $map, $storage, $PIXEL) {}
+    public function deskewimage($threshold) {}
+    public function segmentimage($COLORSPACE, $cluster_threshold, $smooth_threshold, $verbose = null) {}
+    public function sparsecolorimage($SPARSE_METHOD, $arguments, $CHANNEL = null) {}
+    public function remapimage(\Imagick $Imagick, $DITHER) {}
+    public function exportimagepixels($x, $y, $width, $height, $map, $STORAGE) {}
+    public function getimagechannelkurtosis($CHANNEL = null) {}
+    public function functionimage($FUNCTION, $arguments) {}
+    public function transformimagecolorspace($COLORSPACE) {}
+    public function haldclutimage(\Imagick $Imagick, $CHANNEL = null) {}
+    public function autolevelimage($CHANNEL = null) {}
+    public function blueshiftimage($factor = null) {}
+    public function getimageartifact($artifact) {}
+    public function setimageartifact($artifact, $value) {}
+    public function deleteimageartifact($artifact) {}
+    public function getcolorspace() {}
+    public function setcolorspace($COLORSPACE) {}
+    public function clampimage($CHANNEL = null) {}
+    public function smushimages($stack, $offset) {}
+    public function __construct($files = null) {}
+    public function __toString() {}
+    public function count($mode = null) {}
+    public function getpixeliterator() {}
+    public function getpixelregioniterator($x, $y, $columns, $rows, $modify) {}
+    public function readimage($filename) {}
+    public function readimages($filenames) {}
+    public function readimageblob($imageContents, $filename = null) {}
+    public function setimageformat($imageFormat) {}
+    public function scaleimage($width, $height, $bestfit = null, $legacy = null) {}
+    public function writeimage($filename = null) {}
+    public function writeimages($filename, $adjoin) {}
+    public function blurimage($radius, $sigma, $CHANNELTYPE = null) {}
+    public function thumbnailimage($width, $height, $bestfit = null, $fill = null, $legacy = null) {}
+    public function cropthumbnailimage($width, $height, $legacy = null) {}
+    public function getimagefilename() {}
+    public function setimagefilename($filename) {}
+    public function getimageformat() {}
+    public function getimagemimetype() {}
+    public function removeimage() {}
+    public function destroy() {}
+    public function clear() {}
+    public function clone() {}
+    public function getimagesize() {}
+    public function getimageblob() {}
+    public function getimagesblob() {}
+    public function setfirstiterator() {}
+    public function setlastiterator() {}
+    public function resetiterator() {}
+    public function previousimage() {}
+    public function nextimage() {}
+    public function haspreviousimage() {}
+    public function hasnextimage() {}
+    public function setimageindex($index) {}
+    public function getimageindex() {}
+    public function commentimage($comment) {}
+    public function cropimage($width, $height, $x, $y) {}
+    public function labelimage($label) {}
+    public function getimagegeometry() {}
+    public function drawimage(\ImagickDraw $ImagickDraw) {}
+    public function setimagecompressionquality($quality) {}
+    public function getimagecompressionquality() {}
+    public function setimagecompression($COMPRESSION) {}
+    public function getimagecompression() {}
+    public function annotateimage(\ImagickDraw $ImagickDraw, $x, $y, $angle, $text) {}
+    public function compositeimage(\Imagick $Imagick, $COMPOSITE, $x, $y, $CHANNELTYPE = null) {}
+    public function modulateimage($brightness, $saturation, $hue) {}
+    public function getimagecolors() {}
+    public function montageimage(\ImagickDraw $ImagickDraw, $tileGeometry, $thumbnailGeometry, $MONTAGEMODE, $frame) {}
+    public function identifyimage($appendRawOutput = null) {}
+    public function thresholdimage($threshold, $CHANNELTYPE = null) {}
+    public function adaptivethresholdimage($width, $height, $offset) {}
+    public function blackthresholdimage($color) {}
+    public function whitethresholdimage($color) {}
+    public function appendimages($stack) {}
+    public function charcoalimage($radius, $sigma) {}
+    public function normalizeimage($CHANNEL = null) {}
+    public function oilpaintimage($radius) {}
+    public function posterizeimage($levels, $dither) {}
+    public function radialblurimage($angle, $CHANNEL = null) {}
+    public function raiseimage($width, $height, $x, $y, $raise) {}
+    public function resampleimage($xResolution, $yResolution, $FILTER, $blur) {}
+    public function resizeimage($x, $y, $filter = null, $blur = null, $bestfit = null, $legacy = null) {}
+    public function rollimage($x, $y) {}
+    public function rotateimage($color, $degrees) {}
+    public function sampleimage($columns, $rows) {}
+    public function solarizeimage($threshold) {}
+    public function shadowimage($opacity, $sigma, $x, $y) {}
+    public function setimageattribute($key, $value) {}
+    public function setimagebackgroundcolor($color) {}
+    public function setimagecompose($COMPOSITE) {}
+    public function setimagedelay($delay) {}
+    public function setimagedepth($depth) {}
+    public function setimagegamma($gamma) {}
+    public function setimageiterations($iterations) {}
+    public function setimagemattecolor($color) {}
+    public function setimagepage($width, $height, $x, $y) {}
+    public function setimageprogressmonitor($filename) {}
+    public function setprogressmonitor($callback) {}
+    public function setimageresolution($xResolution, $yResolution) {}
+    public function setimagescene($scene) {}
+    public function setimagetickspersecond($ticksPerSecond) {}
+    public function setimagetype($IMGTYPE) {}
+    public function setimageunits($RESOLUTION) {}
+    public function sharpenimage($radius, $sigma, $CHANNEL = null) {}
+    public function shaveimage($columns, $rows) {}
+    public function shearimage($color, $xShear, $yShear) {}
+    public function spliceimage($width, $height, $x, $y) {}
+    public function pingimage($filename) {}
+    public function readimagefile($fp) {}
+    public function displayimage($serverName) {}
+    public function displayimages($serverName) {}
+    public function spreadimage($radius) {}
+    public function swirlimage($degrees) {}
+    public function stripimage() {}
+    public static function queryformats($pattern) {}
+    public static function queryfonts($pattern) {}
+    public function queryfontmetrics(\ImagickDraw $ImagickDraw, $text, $multiline = null) {}
+    public function steganoimage(\Imagick $Imagick, $offset) {}
+    public function addnoiseimage($NOISE, $CHANNEL = null) {}
+    public function motionblurimage($radius, $sigma, $angle, $CHANNEL = null) {}
+    public function mosaicimages() {}
+    public function morphimages($frames) {}
+    public function minifyimage() {}
+    public function affinetransformimage(\ImagickDraw $ImagickDraw) {}
+    public function averageimages() {}
+    public function borderimage($color, $width, $height) {}
+    public static function calculatecrop($orig_width, $orig_height, $desired_width, $desired_height, $legacy = null) {}
+    public function chopimage($width, $height, $x, $y) {}
+    public function clipimage() {}
+    public function clippathimage($pathname, $inside) {}
+    public function clipimagepath($pathname, $inside) {}
+    public function coalesceimages() {}
+    public function colorfloodfillimage($fill_color, $fuzz, $border_color, $y, $x) {}
+    public function colorizeimage($colorize_color, $opacity, $legacy = null) {}
+    public function compareimagechannels(\Imagick $Imagick, $CHANNEL, $METRIC) {}
+    public function compareimages(\Imagick $Imagick, $METRIC) {}
+    public function contrastimage($sharpen) {}
+    public function combineimages() {}
+    public function convolveimage($kernel, $CHANNEL = null) {}
+    public function cyclecolormapimage($displace) {}
+    public function deconstructimages() {}
+    public function despeckleimage() {}
+    public function edgeimage($radius) {}
+    public function embossimage($radius, $sigma) {}
+    public function enhanceimage() {}
+    public function equalizeimage() {}
+    public function evaluateimage($EVALUATE, $constant, $CHANNEL = null) {}
+    public function evaluateimages($EVALUATE) {}
+    public function flattenimages() {}
+    public function flipimage() {}
+    public function flopimage() {}
+    public function forwardfouriertransformimage($magnitude) {}
+    public function frameimage($color, $width, $height, $innerBevel, $outerBevel) {}
+    public function fximage($expression, $CHANNEL = null) {}
+    public function gammaimage($gamma, $CHANNEL = null) {}
+    public function gaussianblurimage($radius, $sigma, $CHANNEL = null) {}
+    public function getimageattribute($key) {}
+    public function getimagebackgroundcolor() {}
+    public function getimageblueprimary() {}
+    public function getimagebordercolor() {}
+    public function getimagechanneldepth($CHANNEL) {}
+    public function getimagechanneldistortion(\Imagick $Imagick, $CHANNEL, $METRIC) {}
+    public function getimagechannelextrema($CHANNEL) {}
+    public function getimagechannelmean($CHANNEL) {}
+    public function getimagechannelstatistics() {}
+    public function getimagecolormapcolor($index) {}
+    public function getimagecolorspace() {}
+    public function getimagecompose() {}
+    public function getimagedelay() {}
+    public function getimagedepth() {}
+    public function getimagedistortion(\Imagick $Imagick, $METRIC) {}
+    public function getimageextrema() {}
+    public function getimagedispose() {}
+    public function getimagegamma() {}
+    public function getimagegreenprimary() {}
+    public function getimageheight() {}
+    public function getimagehistogram() {}
+    public function getimageinterlacescheme() {}
+    public function getimageiterations() {}
+    public function getimagemattecolor() {}
+    public function getimagepage() {}
+    public function getimagepixelcolor($x, $y) {}
+    public function getimageprofile($name) {}
+    public function getimageredprimary() {}
+    public function getimagerenderingintent() {}
+    public function getimageresolution() {}
+    public function getimagescene() {}
+    public function getimagesignature() {}
+    public function getimagetickspersecond() {}
+    public function getimagetype() {}
+    public function getimageunits() {}
+    public function getimagevirtualpixelmethod() {}
+    public function getimagewhitepoint() {}
+    public function getimagewidth() {}
+    public function getnumberimages() {}
+    public function getimagetotalinkdensity() {}
+    public function getimageregion($width, $height, $x, $y) {}
+    public function implodeimage($radius) {}
+    public function inversefouriertransformimage($complement, $magnitude) {}
+    public function levelimage($blackPoint, $gamma, $whitePoint, $CHANNEL = null) {}
+    public function magnifyimage() {}
+    public function mapimage(\Imagick $Imagick, $dither) {}
+    public function mattefloodfillimage($alpha, $fuzz, $color, $x, $y) {}
+    public function medianfilterimage($radius) {}
+    public function negateimage($gray, $CHANNEL = null) {}
+    public function paintopaqueimage($target_color, $fill_color, $fuzz, $CHANNEL = null) {}
+    public function painttransparentimage($target_color, $alpha, $fuzz) {}
+    public function previewimages($PREVIEW) {}
+    public function profileimage($name, $profile) {}
+    public function quantizeimage($numColors, $COLORSPACE, $treeDepth, $dither, $measureError) {}
+    public function quantizeimages($numColors, $COLORSPACE, $treeDepth, $dither, $measureError) {}
+    public function reducenoiseimage($radius) {}
+    public function removeimageprofile($name) {}
+    public function separateimagechannel($CHANNEL) {}
+    public function sepiatoneimage($threshold) {}
+    public function setimagebias($bias) {}
+    public function setimagebiasquantum($bias) {}
+    public function setimageblueprimary($x, $y) {}
+    public function setimagebordercolor($color) {}
+    public function setimagechanneldepth($CHANNEL, $depth) {}
+    public function setimagecolormapcolor($index, $color) {}
+    public function setimagecolorspace($COLORSPACE) {}
+    public function setimagedispose($DISPOSETYPE) {}
+    public function setimageextent($columns, $rows) {}
+    public function setimagegreenprimary($x, $y) {}
+    public function setimageinterlacescheme($INTERLACE) {}
+    public function setimageprofile($name, $profile) {}
+    public function setimageredprimary($x, $y) {}
+    public function setimagerenderingintent($RENDERINGINTENT) {}
+    public function setimagevirtualpixelmethod($VIRTUALPIXELMETHOD) {}
+    public function setimagewhitepoint($x, $y) {}
+    public function sigmoidalcontrastimage($sharpen, $contrast, $midpoint, $CHANNEL = null) {}
+    public function stereoimage(\Imagick $Imagick) {}
+    public function textureimage(\Imagick $Imagick) {}
+    public function tintimage($tint_color, $opacity, $legacy = null) {}
+    public function unsharpmaskimage($radius, $sigma, $amount, $threshold, $CHANNEL = null) {}
+    public function getimage() {}
+    public function addimage(\Imagick $Imagick) {}
+    public function setimage(\Imagick $Imagick) {}
+    public function newimage($columns, $rows, $background_color, $format = null) {}
+    public function newpseudoimage($columns, $rows, $pseudoString) {}
+    public function getcompression() {}
+    public function getcompressionquality() {}
+    public static function getcopyright() {}
+    public static function getconfigureoptions($pattern = null) {}
+    public static function getfeatures() {}
+    public function getfilename() {}
+    public function getformat() {}
+    public static function gethomeurl() {}
+    public function getinterlacescheme() {}
+    public function getoption($key) {}
+    public static function getpackagename() {}
+    public function getpage() {}
+    public static function getquantum() {}
+    public static function gethdrienabled() {}
+    public static function getquantumdepth() {}
+    public static function getquantumrange() {}
+    public static function getreleasedate() {}
+    public static function getresource($resource_type) {}
+    public static function getresourcelimit($resource_type) {}
+    public function getsamplingfactors() {}
+    public function getsize() {}
+    public static function getversion() {}
+    public function setbackgroundcolor($color) {}
+    public function setcompression($compression) {}
+    public function setcompressionquality($compressionquality) {}
+    public function setfilename($filename) {}
+    public function setformat($format) {}
+    public function setinterlacescheme($INTERLACE) {}
+    public function setoption($key, $value) {}
+    public function setpage($width, $height, $x, $y) {}
+    public static function setresourcelimit($RESOURCETYPE, $limit) {}
+    public function setresolution($xResolution, $yResolution) {}
+    public function setsamplingfactors($factors) {}
+    public function setsize($columns, $rows) {}
+    public function settype($IMGTYPE) {}
+    public function key() {}
+    public function next() {}
+    public function rewind() {}
+    public function valid() {}
+    public function current() {}
+    public function brightnesscontrastimage($brightness, $contrast, $CHANNEL = null) {}
+    public function colormatriximage($color_matrix) {}
+    public function selectiveblurimage($radius, $sigma, $threshold, $CHANNEL) {}
+    public function rotationalblurimage($angle, $CHANNEL = null) {}
+    public function statisticimage($type, $width, $height, $CHANNEL = null) {}
+    public function subimagematch(\Imagick $Imagick, &$offset = null, &$similarity = null, &$similarity_threshold = null, &$metric = null) {}
+    public function similarityimage(\Imagick $Imagick, &$offset = null, &$similarity = null, &$similarity_threshold = null, &$metric = null) {}
+    public static function setregistry($key, $value) {}
+    public static function getregistry($key) {}
+    public static function listregistry() {}
+    public function morphology($morphologyMethod, $iterations, \ImagickKernel $ImagickKernel, $CHANNEL = null) {}
+    public function filter(\ImagickKernel $ImagickKernel, $CHANNEL = null) {}
+    public function setantialias($antialias) {}
+    public function getantialias() {}
+    public function colordecisionlistimage($antialias) {}
+    public function autogammaimage($CHANNEL) {}
+    public function autoorient() {}
+    public function compositeimagegravity(\Imagick $Imagick, $COMPOSITE, $GRAVITY) {}
+    public function localcontrastimage($radius, $strength) {}
+}
+
+class ImagickDraw {
+
+    // methods
+    public function resetvectorgraphics() {}
+    public function gettextkerning() {}
+    public function settextkerning($kerning) {}
+    public function gettextinterwordspacing() {}
+    public function settextinterwordspacing($spacing) {}
+    public function gettextinterlinespacing() {}
+    public function settextinterlinespacing($spacing) {}
+    public function __construct() {}
+    public function setfillcolor($color) {}
+    public function setfillalpha($alpha) {}
+    public function setresolution($x_resolution, $y_resolution) {}
+    public function setstrokecolor($color) {}
+    public function setstrokealpha($alpha) {}
+    public function setstrokewidth($width) {}
+    public function clear() {}
+    public function circle($ox, $oy, $px, $py) {}
+    public function annotation($x, $y, $text) {}
+    public function settextantialias($antialias) {}
+    public function settextencoding($encoding) {}
+    public function setfont($font) {}
+    public function setfontfamily($fontfamily) {}
+    public function setfontsize($pointsize) {}
+    public function setfontstyle($STYLE) {}
+    public function setfontweight($weight) {}
+    public function getfont() {}
+    public function getfontfamily() {}
+    public function getfontsize() {}
+    public function getfontstyle() {}
+    public function getfontweight() {}
+    public function destroy() {}
+    public function rectangle($x1, $y1, $x2, $y2) {}
+    public function roundrectangle($x1, $y1, $x2, $y2, $rx, $ry) {}
+    public function ellipse($ox, $oy, $px, $py, $start, $end) {}
+    public function skewx($degrees) {}
+    public function skewy($degrees) {}
+    public function translate($x, $y) {}
+    public function line($sx, $sy, $ex, $ey) {}
+    public function arc($sx, $sy, $ex, $ey, $sd, $ed) {}
+    public function matte($x, $y, $METHOD) {}
+    public function polygon($coordinates) {}
+    public function point($x, $y) {}
+    public function gettextdecoration() {}
+    public function gettextencoding() {}
+    public function getfontstretch() {}
+    public function setfontstretch($STRETCH) {}
+    public function setstrokeantialias($antialias) {}
+    public function settextalignment($ALIGN) {}
+    public function settextdecoration($DECORATION) {}
+    public function settextundercolor($color) {}
+    public function setviewbox($sx, $sy, $ex, $ey) {}
+    public function clone() {}
+    public function affine($affineMatrix) {}
+    public function bezier($coordinateArray) {}
+    public function composite($COMPOSE, $x, $y, $width, $height, \Imagick $Imagick) {}
+    public function color($x, $y, $PAINTMETHOD) {}
+    public function comment($comment) {}
+    public function getclippath() {}
+    public function getcliprule() {}
+    public function getclipunits() {}
+    public function getfillcolor() {}
+    public function getfillopacity() {}
+    public function getfillrule() {}
+    public function getgravity() {}
+    public function getstrokeantialias() {}
+    public function getstrokecolor() {}
+    public function getstrokedasharray() {}
+    public function getstrokedashoffset() {}
+    public function getstrokelinecap() {}
+    public function getstrokelinejoin() {}
+    public function getstrokemiterlimit() {}
+    public function getstrokeopacity() {}
+    public function getstrokewidth() {}
+    public function gettextalignment() {}
+    public function gettextantialias() {}
+    public function getvectorgraphics() {}
+    public function gettextundercolor() {}
+    public function pathclose() {}
+    public function pathcurvetoabsolute($x1, $y1, $x2, $y2, $x, $y) {}
+    public function pathcurvetorelative($x1, $y1, $x2, $y2, $x, $y) {}
+    public function pathcurvetoquadraticbezierabsolute($x1, $y1, $x, $y) {}
+    public function pathcurvetoquadraticbezierrelative($x1, $y1, $x, $y) {}
+    public function pathcurvetoquadraticbeziersmoothabsolute($x, $y) {}
+    public function pathcurvetoquadraticbeziersmoothrelative($x, $y) {}
+    public function pathcurvetosmoothabsolute($x1, $y1, $x, $y) {}
+    public function pathcurvetosmoothrelative($x1, $y1, $x, $y) {}
+    public function pathellipticarcabsolute($rx, $ry, $xAxisRotation, $largeArc, $sweep, $x, $y) {}
+    public function pathellipticarcrelative($rx, $ry, $xAxisRotation, $largeArc, $sweep, $x, $y) {}
+    public function pathfinish() {}
+    public function pathlinetoabsolute($x, $y) {}
+    public function pathlinetorelative($x, $y) {}
+    public function pathlinetohorizontalabsolute($y) {}
+    public function pathlinetohorizontalrelative($x) {}
+    public function pathlinetoverticalabsolute($y) {}
+    public function pathlinetoverticalrelative($x) {}
+    public function pathmovetoabsolute($x, $y) {}
+    public function pathmovetorelative($x, $y) {}
+    public function pathstart() {}
+    public function polyline($coordinateArray) {}
+    public function popclippath() {}
+    public function popdefs() {}
+    public function poppattern() {}
+    public function pushclippath($clipMask) {}
+    public function pushdefs() {}
+    public function pushpattern($pattern_id, $x, $y, $width, $height) {}
+    public function render() {}
+    public function rotate($degrees) {}
+    public function scale($x, $y) {}
+    public function setclippath($clipMask) {}
+    public function setcliprule($FILLRULE) {}
+    public function setclipunits($PATHUNITS) {}
+    public function setfillopacity($fillOpacity) {}
+    public function setfillpatternurl($url) {}
+    public function setfillrule($FILLRULE) {}
+    public function setgravity($GRAVITY) {}
+    public function setstrokepatternurl($url) {}
+    public function setstrokedashoffset($offset) {}
+    public function setstrokelinecap($LINECAP) {}
+    public function setstrokelinejoin($LINEJOIN) {}
+    public function setstrokemiterlimit($miterLimit) {}
+    public function setstrokeopacity($strokeOpacity) {}
+    public function setvectorgraphics($xml) {}
+    public function pop() {}
+    public function push() {}
+    public function setstrokedasharray($dashArray) {}
+    public function getopacity() {}
+    public function setopacity($opacity) {}
+    public function getfontresolution() {}
+    public function setfontresolution($x, $y) {}
+    public function getbordercolor() {}
+    public function setbordercolor($bordercolor) {}
+    public function setdensity($density) {}
+    public function getdensity() {}
+    public function gettextdirection() {}
+    public function settextdirection($direction) {}
+}
+
+class ImagickDrawException extends \Exception {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+class ImagickException extends \Exception {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+class ImagickKernel {
+
+    // methods
+    private function __construct() {}
+    public static function frommatrix($array, $array = null) {}
+    public static function frombuiltin($kerneltype, $paramstring) {}
+    public function addkernel(\ImagickKernel $ImagickKernel) {}
+    public function getmatrix() {}
+    public function separate() {}
+    public function scale() {}
+    public function addunitykernel() {}
+}
+
+class ImagickKernelException extends \Exception {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+class ImagickPixel {
+
+    // methods
+    public function gethsl() {}
+    public function sethsl($hue, $saturation, $luminosity) {}
+    public function getcolorvaluequantum($color) {}
+    public function setcolorvaluequantum($color_value) {}
+    public function getindex() {}
+    public function setindex($index) {}
+    public function __construct($color = null) {}
+    public function setcolor($color) {}
+    public function setcolorvalue($color, $value) {}
+    public function getcolorvalue($color) {}
+    public function clear() {}
+    public function destroy() {}
+    public function issimilar($color, $fuzz = null) {}
+    public function ispixelsimilarquantum($color, $fuzz = null) {}
+    public function ispixelsimilar($color, $fuzz = null) {}
+    public function getcolor($normalized = null) {}
+    public function getcolorquantum() {}
+    public function getcolorasstring() {}
+    public function getcolorcount() {}
+    public function setcolorcount($colorCount) {}
+    public function clone() {}
+    public function setcolorfrompixel(\ImagickPixel $srcPixel) {}
+}
+
+class ImagickPixelException extends \Exception {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+class ImagickPixelIterator implements \Iterator, \Traversable {
+
+    // methods
+    public function __construct(\Imagick $Imagick) {}
+    public function newpixeliterator() {}
+    public function newpixelregioniterator() {}
+    public function getiteratorrow() {}
+    public function setiteratorrow($row) {}
+    public function setiteratorfirstrow() {}
+    public function setiteratorlastrow() {}
+    public function getpreviousiteratorrow() {}
+    public function getcurrentiteratorrow() {}
+    public function getnextiteratorrow() {}
+    public function resetiterator() {}
+    public function synciterator() {}
+    public function destroy() {}
+    public function clear() {}
+    public static function getpixeliterator(\Imagick $Imagick) {}
+    public static function getpixelregioniterator(\Imagick $Imagick, $x, $y, $columns, $rows) {}
+    public function key() {}
+    public function next() {}
+    public function rewind() {}
+    public function current() {}
+    public function valid() {}
+}
+
+class ImagickPixelIteratorException extends \Exception {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+}
diff --git a/.phan/internal_stubs/pcntl.phan_php b/.phan/internal_stubs/pcntl.phan_php
new file mode 100644 (file)
index 0000000..392dc30
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension pcntl@7.0.33-0+deb9u3
+
+namespace {
+function pcntl_alarm($seconds) {}
+function pcntl_errno() {}
+function pcntl_exec($path, $args = null, $envs = null) {}
+function pcntl_fork() {}
+function pcntl_get_last_error() {}
+function pcntl_getpriority($pid = null, $process_identifier = null) {}
+function pcntl_setpriority($priority, $pid = null, $process_identifier = null) {}
+function pcntl_signal($signo, $handler, $restart_syscalls = null) {}
+function pcntl_signal_dispatch() {}
+function pcntl_sigprocmask($how, $set, &$oldset = null) {}
+function pcntl_sigtimedwait($set, &$info = null, $seconds = null, $nanoseconds = null) {}
+function pcntl_sigwaitinfo($set, &$info = null) {}
+function pcntl_strerror($errno) {}
+function pcntl_wait(&$status, $options = null, &$rusage = null) {}
+function pcntl_waitpid($pid, &$status, $options = null, &$rusage = null) {}
+function pcntl_wexitstatus($status) {}
+function pcntl_wifcontinued($status) {}
+function pcntl_wifexited($status) {}
+function pcntl_wifsignaled($status) {}
+function pcntl_wifstopped($status) {}
+function pcntl_wstopsig($status) {}
+function pcntl_wtermsig($status) {}
+const BUS_ADRALN = 1;
+const BUS_ADRERR = 2;
+const BUS_OBJERR = 3;
+const CLD_CONTINUED = 6;
+const CLD_DUMPED = 3;
+const CLD_EXITED = 1;
+const CLD_KILLED = 2;
+const CLD_STOPPED = 5;
+const CLD_TRAPPED = 4;
+const FPE_FLTDIV = 3;
+const FPE_FLTINV = 7;
+const FPE_FLTOVF = 4;
+const FPE_FLTRES = 6;
+const FPE_FLTSUB = 8;
+const FPE_FLTUND = 7;
+const FPE_INTDIV = 1;
+const FPE_INTOVF = 2;
+const ILL_BADSTK = 8;
+const ILL_COPROC = 7;
+const ILL_ILLADR = 3;
+const ILL_ILLOPC = 1;
+const ILL_ILLOPN = 2;
+const ILL_ILLTRP = 4;
+const ILL_PRVOPC = 5;
+const ILL_PRVREG = 6;
+const PCNTL_E2BIG = 7;
+const PCNTL_EACCES = 13;
+const PCNTL_EAGAIN = 11;
+const PCNTL_ECHILD = 10;
+const PCNTL_EFAULT = 14;
+const PCNTL_EINTR = 4;
+const PCNTL_EINVAL = 22;
+const PCNTL_EIO = 5;
+const PCNTL_EISDIR = 21;
+const PCNTL_ELIBBAD = 80;
+const PCNTL_ELOOP = 40;
+const PCNTL_EMFILE = 24;
+const PCNTL_ENAMETOOLONG = 36;
+const PCNTL_ENFILE = 23;
+const PCNTL_ENOENT = 2;
+const PCNTL_ENOEXEC = 8;
+const PCNTL_ENOMEM = 12;
+const PCNTL_ENOTDIR = 20;
+const PCNTL_EPERM = 1;
+const PCNTL_ESRCH = 3;
+const PCNTL_ETXTBSY = 26;
+const POLL_ERR = 4;
+const POLL_HUP = 6;
+const POLL_IN = 1;
+const POLL_MSG = 3;
+const POLL_OUT = 2;
+const POLL_PRI = 5;
+const PRIO_PGRP = 1;
+const PRIO_PROCESS = 0;
+const PRIO_USER = 2;
+const SEGV_ACCERR = 2;
+const SEGV_MAPERR = 1;
+const SIGABRT = 6;
+const SIGALRM = 14;
+const SIGBABY = 31;
+const SIGBUS = 7;
+const SIGCHLD = 17;
+const SIGCLD = 17;
+const SIGCONT = 18;
+const SIGFPE = 8;
+const SIGHUP = 1;
+const SIGILL = 4;
+const SIGINT = 2;
+const SIGIO = 29;
+const SIGIOT = 6;
+const SIGKILL = 9;
+const SIGPIPE = 13;
+const SIGPOLL = 29;
+const SIGPROF = 27;
+const SIGPWR = 30;
+const SIGQUIT = 3;
+const SIGSEGV = 11;
+const SIGSTKFLT = 16;
+const SIGSTOP = 19;
+const SIGSYS = 31;
+const SIGTERM = 15;
+const SIGTRAP = 5;
+const SIGTSTP = 20;
+const SIGTTIN = 21;
+const SIGTTOU = 22;
+const SIGURG = 23;
+const SIGUSR1 = 10;
+const SIGUSR2 = 12;
+const SIGVTALRM = 26;
+const SIGWINCH = 28;
+const SIGXCPU = 24;
+const SIGXFSZ = 25;
+const SIG_BLOCK = 0;
+const SIG_DFL = 0;
+const SIG_ERR = -1;
+const SIG_IGN = 1;
+const SIG_SETMASK = 2;
+const SIG_UNBLOCK = 1;
+const SI_ASYNCIO = -4;
+const SI_KERNEL = 128;
+const SI_MESGQ = -3;
+const SI_QUEUE = -1;
+const SI_SIGIO = -5;
+const SI_TIMER = -2;
+const SI_TKILL = -6;
+const SI_USER = 0;
+const TRAP_BRKPT = 1;
+const TRAP_TRACE = 2;
+const WCONTINUED = 8;
+const WNOHANG = 1;
+const WUNTRACED = 2;
+}
diff --git a/.phan/internal_stubs/redis.phan_php b/.phan/internal_stubs/redis.phan_php
new file mode 100644 (file)
index 0000000..29efb47
--- /dev/null
@@ -0,0 +1,490 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension redis@3.1.1
+
+namespace {
+class Redis {
+
+    // constants
+    const REDIS_NOT_FOUND = 0;
+    const REDIS_STRING = 1;
+    const REDIS_SET = 2;
+    const REDIS_LIST = 3;
+    const REDIS_ZSET = 4;
+    const REDIS_HASH = 5;
+    const PIPELINE = 2;
+    const ATOMIC = 0;
+    const MULTI = 1;
+    const OPT_SERIALIZER = 1;
+    const OPT_PREFIX = 2;
+    const OPT_READ_TIMEOUT = 3;
+    const SERIALIZER_NONE = 0;
+    const SERIALIZER_PHP = 1;
+    const SERIALIZER_IGBINARY = 2;
+    const OPT_SCAN = 4;
+    const SCAN_RETRY = 1;
+    const SCAN_NORETRY = 0;
+    const AFTER = 'after';
+    const BEFORE = 'before';
+
+    // methods
+    public function __construct() {}
+    public function __destruct() {}
+    public function connect() {}
+    public function pconnect() {}
+    public function close() {}
+    public function ping() {}
+    public function echo() {}
+    public function get() {}
+    public function set() {}
+    public function setex() {}
+    public function psetex() {}
+    public function setnx() {}
+    public function getSet() {}
+    public function randomKey() {}
+    public function renameKey() {}
+    public function renameNx() {}
+    public function getMultiple() {}
+    public function exists() {}
+    public function delete() {}
+    public function incr() {}
+    public function incrBy() {}
+    public function incrByFloat() {}
+    public function decr() {}
+    public function decrBy() {}
+    public function type() {}
+    public function append() {}
+    public function getRange() {}
+    public function setRange() {}
+    public function getBit() {}
+    public function setBit() {}
+    public function strlen() {}
+    public function getKeys() {}
+    public function sort() {}
+    public function sortAsc() {}
+    public function sortAscAlpha() {}
+    public function sortDesc() {}
+    public function sortDescAlpha() {}
+    public function lPush() {}
+    public function rPush() {}
+    public function lPushx() {}
+    public function rPushx() {}
+    public function lPop() {}
+    public function rPop() {}
+    public function blPop() {}
+    public function brPop() {}
+    public function lSize() {}
+    public function lRemove() {}
+    public function listTrim() {}
+    public function lGet() {}
+    public function lGetRange() {}
+    public function lSet() {}
+    public function lInsert() {}
+    public function sAdd() {}
+    public function sAddArray() {}
+    public function sSize() {}
+    public function sRemove() {}
+    public function sMove() {}
+    public function sPop() {}
+    public function sRandMember() {}
+    public function sContains() {}
+    public function sMembers() {}
+    public function sInter() {}
+    public function sInterStore() {}
+    public function sUnion() {}
+    public function sUnionStore() {}
+    public function sDiff() {}
+    public function sDiffStore() {}
+    public function setTimeout() {}
+    public function save() {}
+    public function bgSave() {}
+    public function lastSave() {}
+    public function flushDB() {}
+    public function flushAll() {}
+    public function dbSize() {}
+    public function auth() {}
+    public function ttl() {}
+    public function pttl() {}
+    public function persist() {}
+    public function info() {}
+    public function select() {}
+    public function move() {}
+    public function bgrewriteaof() {}
+    public function slaveof() {}
+    public function object() {}
+    public function bitop() {}
+    public function bitcount() {}
+    public function bitpos() {}
+    public function mset() {}
+    public function msetnx() {}
+    public function rpoplpush() {}
+    public function brpoplpush() {}
+    public function zAdd() {}
+    public function zDelete() {}
+    public function zRange() {}
+    public function zRevRange() {}
+    public function zRangeByScore() {}
+    public function zRevRangeByScore() {}
+    public function zRangeByLex() {}
+    public function zRevRangeByLex() {}
+    public function zLexCount() {}
+    public function zRemRangeByLex() {}
+    public function zCount() {}
+    public function zDeleteRangeByScore() {}
+    public function zDeleteRangeByRank() {}
+    public function zCard() {}
+    public function zScore() {}
+    public function zRank() {}
+    public function zRevRank() {}
+    public function zInter() {}
+    public function zUnion() {}
+    public function zIncrBy() {}
+    public function expireAt() {}
+    public function pexpire() {}
+    public function pexpireAt() {}
+    public function hGet() {}
+    public function hSet() {}
+    public function hSetNx() {}
+    public function hDel() {}
+    public function hLen() {}
+    public function hKeys() {}
+    public function hVals() {}
+    public function hGetAll() {}
+    public function hExists() {}
+    public function hIncrBy() {}
+    public function hIncrByFloat() {}
+    public function hMset() {}
+    public function hMget() {}
+    public function multi() {}
+    public function discard() {}
+    public function exec() {}
+    public function pipeline() {}
+    public function watch() {}
+    public function unwatch() {}
+    public function publish() {}
+    public function subscribe() {}
+    public function psubscribe() {}
+    public function unsubscribe() {}
+    public function punsubscribe() {}
+    public function time() {}
+    public function role() {}
+    public function eval() {}
+    public function evalsha() {}
+    public function script() {}
+    public function debug() {}
+    public function dump() {}
+    public function restore() {}
+    public function migrate() {}
+    public function getLastError() {}
+    public function clearLastError() {}
+    public function _prefix() {}
+    public function _serialize() {}
+    public function _unserialize() {}
+    public function client() {}
+    public function command() {}
+    public function scan(&$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function hscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function zscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function sscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function pfadd() {}
+    public function pfcount() {}
+    public function pfmerge() {}
+    public function getOption() {}
+    public function setOption() {}
+    public function config() {}
+    public function slowlog() {}
+    public function rawcommand() {}
+    public function geoadd() {}
+    public function geohash() {}
+    public function geopos() {}
+    public function geodist() {}
+    public function georadius() {}
+    public function georadiusbymember() {}
+    public function getHost() {}
+    public function getPort() {}
+    public function getDBNum() {}
+    public function getTimeout() {}
+    public function getReadTimeout() {}
+    public function getPersistentID() {}
+    public function getAuth() {}
+    public function isConnected() {}
+    public function getMode() {}
+    public function wait() {}
+    public function pubsub() {}
+    public function open() {}
+    public function popen() {}
+    public function lLen() {}
+    public function sGetMembers() {}
+    public function mget() {}
+    public function expire() {}
+    public function zunionstore() {}
+    public function zinterstore() {}
+    public function zRemove() {}
+    public function zRem() {}
+    public function zRemoveRangeByScore() {}
+    public function zRemRangeByScore() {}
+    public function zRemRangeByRank() {}
+    public function zSize() {}
+    public function substr() {}
+    public function rename() {}
+    public function del() {}
+    public function keys() {}
+    public function lrem() {}
+    public function ltrim() {}
+    public function lindex() {}
+    public function lrange() {}
+    public function scard() {}
+    public function srem() {}
+    public function sismember() {}
+    public function zReverseRange() {}
+    public function sendEcho() {}
+    public function evaluate() {}
+    public function evaluateSha() {}
+}
+
+class RedisArray {
+
+    // methods
+    public function __construct() {}
+    public function __call($function_name, $arguments) {}
+    public function _hosts() {}
+    public function _target() {}
+    public function _instance() {}
+    public function _function() {}
+    public function _distributor() {}
+    public function _rehash() {}
+    public function select() {}
+    public function info() {}
+    public function ping() {}
+    public function flushdb() {}
+    public function flushall() {}
+    public function mget() {}
+    public function mset() {}
+    public function del() {}
+    public function getOption() {}
+    public function setOption() {}
+    public function keys() {}
+    public function save() {}
+    public function bgsave() {}
+    public function multi() {}
+    public function exec() {}
+    public function discard() {}
+    public function unwatch() {}
+    public function delete() {}
+    public function getMultiple() {}
+}
+
+class RedisCluster {
+
+    // constants
+    const REDIS_NOT_FOUND = 0;
+    const REDIS_STRING = 1;
+    const REDIS_SET = 2;
+    const REDIS_LIST = 3;
+    const REDIS_ZSET = 4;
+    const REDIS_HASH = 5;
+    const ATOMIC = 0;
+    const MULTI = 1;
+    const OPT_SERIALIZER = 1;
+    const OPT_PREFIX = 2;
+    const OPT_READ_TIMEOUT = 3;
+    const SERIALIZER_NONE = 0;
+    const SERIALIZER_PHP = 1;
+    const SERIALIZER_IGBINARY = 2;
+    const OPT_SCAN = 4;
+    const SCAN_RETRY = 1;
+    const SCAN_NORETRY = 0;
+    const OPT_SLAVE_FAILOVER = 5;
+    const FAILOVER_NONE = 0;
+    const FAILOVER_ERROR = 1;
+    const FAILOVER_DISTRIBUTE = 2;
+    const FAILOVER_DISTRIBUTE_SLAVES = 3;
+    const AFTER = 'after';
+    const BEFORE = 'before';
+
+    // methods
+    public function __construct() {}
+    public function close() {}
+    public function get() {}
+    public function set() {}
+    public function mget() {}
+    public function mset() {}
+    public function msetnx() {}
+    public function del() {}
+    public function setex() {}
+    public function psetex() {}
+    public function setnx() {}
+    public function getset() {}
+    public function exists() {}
+    public function keys() {}
+    public function type() {}
+    public function lpop() {}
+    public function rpop() {}
+    public function lset() {}
+    public function spop() {}
+    public function lpush() {}
+    public function rpush() {}
+    public function blpop() {}
+    public function brpop() {}
+    public function rpushx() {}
+    public function lpushx() {}
+    public function linsert() {}
+    public function lindex() {}
+    public function lrem() {}
+    public function brpoplpush() {}
+    public function rpoplpush() {}
+    public function llen() {}
+    public function scard() {}
+    public function smembers() {}
+    public function sismember() {}
+    public function sadd() {}
+    public function saddarray() {}
+    public function srem() {}
+    public function sunion() {}
+    public function sunionstore() {}
+    public function sinter() {}
+    public function sinterstore() {}
+    public function sdiff() {}
+    public function sdiffstore() {}
+    public function srandmember() {}
+    public function strlen() {}
+    public function persist() {}
+    public function ttl() {}
+    public function pttl() {}
+    public function zcard() {}
+    public function zcount() {}
+    public function zremrangebyscore() {}
+    public function zscore() {}
+    public function zadd() {}
+    public function zincrby() {}
+    public function hlen() {}
+    public function hkeys() {}
+    public function hvals() {}
+    public function hget() {}
+    public function hgetall() {}
+    public function hexists() {}
+    public function hincrby() {}
+    public function hset() {}
+    public function hsetnx() {}
+    public function hmget() {}
+    public function hmset() {}
+    public function hdel() {}
+    public function hincrbyfloat() {}
+    public function dump() {}
+    public function zrank() {}
+    public function zrevrank() {}
+    public function incr() {}
+    public function decr() {}
+    public function incrby() {}
+    public function decrby() {}
+    public function incrbyfloat() {}
+    public function expire() {}
+    public function pexpire() {}
+    public function expireat() {}
+    public function pexpireat() {}
+    public function append() {}
+    public function getbit() {}
+    public function setbit() {}
+    public function bitop() {}
+    public function bitpos() {}
+    public function bitcount() {}
+    public function lget() {}
+    public function getrange() {}
+    public function ltrim() {}
+    public function lrange() {}
+    public function zremrangebyrank() {}
+    public function publish() {}
+    public function rename() {}
+    public function renamenx() {}
+    public function pfcount() {}
+    public function pfadd() {}
+    public function pfmerge() {}
+    public function setrange() {}
+    public function restore() {}
+    public function smove() {}
+    public function zrange() {}
+    public function zrevrange() {}
+    public function zrangebyscore() {}
+    public function zrevrangebyscore() {}
+    public function zrangebylex() {}
+    public function zrevrangebylex() {}
+    public function zlexcount() {}
+    public function zremrangebylex() {}
+    public function zunionstore() {}
+    public function zinterstore() {}
+    public function zrem() {}
+    public function sort() {}
+    public function object() {}
+    public function subscribe() {}
+    public function psubscribe() {}
+    public function unsubscribe() {}
+    public function punsubscribe() {}
+    public function eval() {}
+    public function evalsha() {}
+    public function scan(&$i_iterator, $str_node, $str_pattern = null, $i_count = null) {}
+    public function sscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function zscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function hscan($str_key, &$i_iterator, $str_pattern = null, $i_count = null) {}
+    public function getmode() {}
+    public function getlasterror() {}
+    public function clearlasterror() {}
+    public function getoption() {}
+    public function setoption() {}
+    public function _prefix() {}
+    public function _serialize() {}
+    public function _unserialize() {}
+    public function _masters() {}
+    public function _redir() {}
+    public function multi() {}
+    public function exec() {}
+    public function discard() {}
+    public function watch() {}
+    public function unwatch() {}
+    public function save() {}
+    public function bgsave() {}
+    public function flushdb() {}
+    public function flushall() {}
+    public function dbsize() {}
+    public function bgrewriteaof() {}
+    public function lastsave() {}
+    public function info() {}
+    public function role() {}
+    public function time() {}
+    public function randomkey() {}
+    public function ping() {}
+    public function echo() {}
+    public function command() {}
+    public function rawcommand() {}
+    public function cluster() {}
+    public function client() {}
+    public function config() {}
+    public function pubsub() {}
+    public function script() {}
+    public function slowlog() {}
+    public function geoadd() {}
+    public function geohash() {}
+    public function geopos() {}
+    public function geodist() {}
+    public function georadius() {}
+    public function georadiusbymember() {}
+}
+
+class RedisClusterException extends \RuntimeException {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+class RedisException extends \RuntimeException {
+
+    // properties
+    protected $message;
+    protected $code;
+    protected $file;
+    protected $line;
+}
+
+}
diff --git a/.phan/internal_stubs/sockets.phan_php b/.phan/internal_stubs/sockets.phan_php
new file mode 100644 (file)
index 0000000..d16f363
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension sockets@7.0.33-0+deb9u3
+
+namespace {
+function socket_accept($socket) {}
+function socket_bind($socket, $addr, $port = null) {}
+function socket_clear_error($socket = null) {}
+function socket_close($socket) {}
+function socket_cmsg_space($level, $type) {}
+function socket_connect($socket, $addr, $port = null) {}
+function socket_create($domain, $type, $protocol) {}
+function socket_create_listen($port, $backlog = null) {}
+function socket_create_pair($domain, $type, $protocol, &$fd) {}
+function socket_export_stream($socket) {}
+function socket_get_option($socket, $level, $optname) {}
+function socket_getopt($socket, $level, $optname) {}
+function socket_getpeername($socket, &$addr, &$port = null) {}
+function socket_getsockname($socket, &$addr, &$port = null) {}
+function socket_import_stream($stream) {}
+function socket_last_error($socket = null) {}
+function socket_listen($socket, $backlog = null) {}
+function socket_read($socket, $length, $type = null) {}
+function socket_recv($socket, &$buf, $len, $flags) {}
+function socket_recvfrom($socket, &$buf, $len, $flags, &$name, &$port = null) {}
+function socket_recvmsg($socket, &$msghdr, $flags) {}
+function socket_select(&$read_fds, &$write_fds, &$except_fds, $tv_sec, $tv_usec = null) {}
+function socket_send($socket, $buf, $len, $flags) {}
+function socket_sendmsg($socket, $msghdr, $flags) {}
+function socket_sendto($socket, $buf, $len, $flags, $addr, $port = null) {}
+function socket_set_block($socket) {}
+function socket_set_nonblock($socket) {}
+function socket_set_option($socket, $level, $optname, $optval) {}
+function socket_setopt($socket, $level, $optname, $optval) {}
+function socket_shutdown($socket, $how = null) {}
+function socket_strerror($errno) {}
+function socket_write($socket, $buf, $length = null) {}
+const AF_INET = 2;
+const AF_INET6 = 10;
+const AF_UNIX = 1;
+const IPPROTO_IP = 0;
+const IPPROTO_IPV6 = 41;
+const IPV6_HOPLIMIT = 52;
+const IPV6_MULTICAST_HOPS = 18;
+const IPV6_MULTICAST_IF = 17;
+const IPV6_MULTICAST_LOOP = 19;
+const IPV6_PKTINFO = 50;
+const IPV6_RECVHOPLIMIT = 51;
+const IPV6_RECVPKTINFO = 49;
+const IPV6_RECVTCLASS = 66;
+const IPV6_TCLASS = 67;
+const IPV6_UNICAST_HOPS = 16;
+const IPV6_V6ONLY = 26;
+const IP_MULTICAST_IF = 32;
+const IP_MULTICAST_LOOP = 34;
+const IP_MULTICAST_TTL = 33;
+const MCAST_BLOCK_SOURCE = 43;
+const MCAST_JOIN_GROUP = 42;
+const MCAST_JOIN_SOURCE_GROUP = 46;
+const MCAST_LEAVE_GROUP = 45;
+const MCAST_LEAVE_SOURCE_GROUP = 47;
+const MCAST_UNBLOCK_SOURCE = 44;
+const MSG_CMSG_CLOEXEC = 1073741824;
+const MSG_CONFIRM = 2048;
+const MSG_CTRUNC = 8;
+const MSG_DONTROUTE = 4;
+const MSG_DONTWAIT = 64;
+const MSG_EOF = 512;
+const MSG_EOR = 128;
+const MSG_ERRQUEUE = 8192;
+const MSG_MORE = 32768;
+const MSG_NOSIGNAL = 16384;
+const MSG_OOB = 1;
+const MSG_PEEK = 2;
+const MSG_TRUNC = 32;
+const MSG_WAITALL = 256;
+const MSG_WAITFORONE = 65536;
+const PHP_BINARY_READ = 2;
+const PHP_NORMAL_READ = 1;
+const SCM_CREDENTIALS = 2;
+const SCM_RIGHTS = 1;
+const SOCKET_E2BIG = 7;
+const SOCKET_EACCES = 13;
+const SOCKET_EADDRINUSE = 98;
+const SOCKET_EADDRNOTAVAIL = 99;
+const SOCKET_EADV = 68;
+const SOCKET_EAFNOSUPPORT = 97;
+const SOCKET_EAGAIN = 11;
+const SOCKET_EALREADY = 114;
+const SOCKET_EBADE = 52;
+const SOCKET_EBADF = 9;
+const SOCKET_EBADFD = 77;
+const SOCKET_EBADMSG = 74;
+const SOCKET_EBADR = 53;
+const SOCKET_EBADRQC = 56;
+const SOCKET_EBADSLT = 57;
+const SOCKET_EBUSY = 16;
+const SOCKET_ECHRNG = 44;
+const SOCKET_ECOMM = 70;
+const SOCKET_ECONNABORTED = 103;
+const SOCKET_ECONNREFUSED = 111;
+const SOCKET_ECONNRESET = 104;
+const SOCKET_EDESTADDRREQ = 89;
+const SOCKET_EDQUOT = 122;
+const SOCKET_EEXIST = 17;
+const SOCKET_EFAULT = 14;
+const SOCKET_EHOSTDOWN = 112;
+const SOCKET_EHOSTUNREACH = 113;
+const SOCKET_EIDRM = 43;
+const SOCKET_EINPROGRESS = 115;
+const SOCKET_EINTR = 4;
+const SOCKET_EINVAL = 22;
+const SOCKET_EIO = 5;
+const SOCKET_EISCONN = 106;
+const SOCKET_EISDIR = 21;
+const SOCKET_EISNAM = 120;
+const SOCKET_EL2HLT = 51;
+const SOCKET_EL2NSYNC = 45;
+const SOCKET_EL3HLT = 46;
+const SOCKET_EL3RST = 47;
+const SOCKET_ELNRNG = 48;
+const SOCKET_ELOOP = 40;
+const SOCKET_EMEDIUMTYPE = 124;
+const SOCKET_EMFILE = 24;
+const SOCKET_EMLINK = 31;
+const SOCKET_EMSGSIZE = 90;
+const SOCKET_EMULTIHOP = 72;
+const SOCKET_ENAMETOOLONG = 36;
+const SOCKET_ENETDOWN = 100;
+const SOCKET_ENETRESET = 102;
+const SOCKET_ENETUNREACH = 101;
+const SOCKET_ENFILE = 23;
+const SOCKET_ENOANO = 55;
+const SOCKET_ENOBUFS = 105;
+const SOCKET_ENOCSI = 50;
+const SOCKET_ENODATA = 61;
+const SOCKET_ENODEV = 19;
+const SOCKET_ENOENT = 2;
+const SOCKET_ENOLCK = 37;
+const SOCKET_ENOLINK = 67;
+const SOCKET_ENOMEDIUM = 123;
+const SOCKET_ENOMEM = 12;
+const SOCKET_ENOMSG = 42;
+const SOCKET_ENONET = 64;
+const SOCKET_ENOPROTOOPT = 92;
+const SOCKET_ENOSPC = 28;
+const SOCKET_ENOSR = 63;
+const SOCKET_ENOSTR = 60;
+const SOCKET_ENOSYS = 38;
+const SOCKET_ENOTBLK = 15;
+const SOCKET_ENOTCONN = 107;
+const SOCKET_ENOTDIR = 20;
+const SOCKET_ENOTEMPTY = 39;
+const SOCKET_ENOTSOCK = 88;
+const SOCKET_ENOTTY = 25;
+const SOCKET_ENOTUNIQ = 76;
+const SOCKET_ENXIO = 6;
+const SOCKET_EOPNOTSUPP = 95;
+const SOCKET_EPERM = 1;
+const SOCKET_EPFNOSUPPORT = 96;
+const SOCKET_EPIPE = 32;
+const SOCKET_EPROTO = 71;
+const SOCKET_EPROTONOSUPPORT = 93;
+const SOCKET_EPROTOTYPE = 91;
+const SOCKET_EREMCHG = 78;
+const SOCKET_EREMOTE = 66;
+const SOCKET_EREMOTEIO = 121;
+const SOCKET_ERESTART = 85;
+const SOCKET_EROFS = 30;
+const SOCKET_ESHUTDOWN = 108;
+const SOCKET_ESOCKTNOSUPPORT = 94;
+const SOCKET_ESPIPE = 29;
+const SOCKET_ESRMNT = 69;
+const SOCKET_ESTRPIPE = 86;
+const SOCKET_ETIME = 62;
+const SOCKET_ETIMEDOUT = 110;
+const SOCKET_ETOOMANYREFS = 109;
+const SOCKET_EUNATCH = 49;
+const SOCKET_EUSERS = 87;
+const SOCKET_EWOULDBLOCK = 11;
+const SOCKET_EXDEV = 18;
+const SOCKET_EXFULL = 54;
+const SOCK_DGRAM = 2;
+const SOCK_RAW = 3;
+const SOCK_RDM = 4;
+const SOCK_SEQPACKET = 5;
+const SOCK_STREAM = 1;
+const SOL_SOCKET = 1;
+const SOL_TCP = 6;
+const SOL_UDP = 17;
+const SOMAXCONN = 128;
+const SO_BINDTODEVICE = 25;
+const SO_BROADCAST = 6;
+const SO_DEBUG = 1;
+const SO_DONTROUTE = 5;
+const SO_ERROR = 4;
+const SO_KEEPALIVE = 9;
+const SO_LINGER = 13;
+const SO_OOBINLINE = 10;
+const SO_PASSCRED = 16;
+const SO_RCVBUF = 8;
+const SO_RCVLOWAT = 18;
+const SO_RCVTIMEO = 20;
+const SO_REUSEADDR = 2;
+const SO_REUSEPORT = 15;
+const SO_SNDBUF = 7;
+const SO_SNDLOWAT = 19;
+const SO_SNDTIMEO = 21;
+const SO_TYPE = 3;
+const TCP_NODELAY = 1;
+}
index ada60e4..bf905e0 100644 (file)
@@ -24,18 +24,10 @@ cache:
 matrix:
   fast_finish: true
   include:
-    # On Trusty, mysql user 'travis' doesn't have create database rights
-    # Postgres has no user called 'root'.
-    - env: dbtype=mysql dbuser=root
-      php: 7.3
-    - env: dbtype=mysql dbuser=root
-      php: 7.2
-    - env: dbtype=mysql dbuser=root
-      php: 7.1
-    - env: dbtype=postgres dbuser=travis
-      php: 7.1
-    - env: dbtype=mysql dbuser=root
-      php: 7
+    - php: 7.3
+    - php: 7.2
+    - php: 7.1
+    - php: 7
   allow_failures:
     - php: 7.3
 
@@ -60,13 +52,13 @@ addons:
 before_script:
   - echo 'opcache.enable_cli = 1' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
   - composer install --prefer-source --quiet --no-interaction
-  - if [ "$dbtype" = postgres ]; then psql -c "CREATE DATABASE traviswiki WITH OWNER travis;" -U postgres; fi
+  # At Travis CI, the mysql user 'travis' doesn't have create database rights, use 'root' instead.
   - >
       php maintenance/install.php traviswiki admin
       --pass travis
-      --dbtype "$dbtype"
+      --dbtype "mysql"
       --dbname traviswiki
-      --dbuser "$dbuser"
+      --dbuser "root"
       --dbpass ""
       --scriptpath "/w"
   - echo -en "\n\nrequire_once __DIR__ . '/includes/DevelopmentSettings.php';\n" >> ./LocalSettings.php
index c85a14c..549f7e8 100644 (file)
@@ -81,6 +81,10 @@ For notes on 1.33.x and older releases, see HISTORY.
 * Updated cssjanus/cssjanus from 1.2.1 to 1.3.0.
 * Updated wikimedia/at-ease from 1.2.0 to 2.0.0.
 * Updated wikimedia/remex-html from 2.0.1 to 2.0.3.
+* Updated monolog/monolog from 1.22.1 to 1.24.0 (dev-only).
+* Updated wikimedia/object-factory from 1.0.0 to 2.0.0.
+* Updated wikimedia/timestamp from 2.2.0 to 3.0.0.
+* Updated wikimedia/xmp-reader from 0.6.2 to 0.6.3.
 * …
 
 ==== Removed external libraries ====
@@ -156,9 +160,10 @@ because of Phabricator reports.
 * User::makeGroupLinkWiki(), deprecated in 1.29, has been removed. Use
   UserGroupMembership::getLink() instead.
 * SavepointPostgres, deprecated in 1.31, has been removed.
-* Output::sectionEditLinksEnabled(), ParserOutput::getEditSectionTokens,
-  ::getTOCEnabled, ::setEditSectionTokens, ::setTOCEnabled, deprecated in 1.31,
-  have been removed.
+* OutputPage::enableSectionEditLinks(), OutputPage::sectionEditLinksEnabled(),
+  ParserOptions::getEditSection(), ParserOptions::setEditSection(), and
+  ParserOutput::getEditSectionTokens, ::getTOCEnabled, ::setEditSectionTokens,
+  and ::setTOCEnabled, deprecated in 1.31, have been removed.
 * EditPage::safeUnicodeInput() and ::safeUnicodeOutput(), deprecated in 1.30,
   have been removed.
 * Four methods in OutputPage, deprecated in 1.32, have been removed. You should
@@ -209,6 +214,39 @@ because of Phabricator reports.
 * jquery.ui.effect-bounce, jquery.ui.effect-explode, jquery.ui.effect-fold
   jquery.ui.effect-pulsate, jquery.ui.effect-slide, jquery.ui.effect-transfer,
   which are no longer used, have now been removed.
+* SpecialEmailUser::validateTarget(), ::getTarget() without a sender/user
+  specified, deprecated in 1.30, have been removed.
+* BufferingStatsdDataFactory::getBuffer(), deprecated in 1.30, has been removed.
+* The constant DB_SLAVE, deprecated in 1.28, has been removed. Use DB_REPLICA.
+* Replacer, DoubleReplacer, HashtableReplacer and RegexlikeReplacer
+  (deprecated in 1.32) have been removed. Closures should be used instead.
+* OutputPage::addWikiText(), ::addWikiTextWithTitle(), ::addWikiTextTitleTidy(),
+  ::addWikiTextTidy(), ::addWikiTextTitle(), deprecated in 1.32, have been
+  removed.
+* The $wgUseKeyHeader configuration option and the OutputPage::getKeyHeader()
+  method, deprecated in 1.32, have been removed.
+* WebInstallerOutput::addWikiText(), deprecated in 1.32, has been removed.
+* Parser::fetchFile(), deprecated in 1.32, has been removed. Use the method
+  Parser::fetchFileAndTitle() instead.
+* The global function wfBCP47, deprecated in 1.31, has been removed.
+* wfCountDown() function, deprecated in 1.31, has been removed. Use
+  \Maintenance::countDown() method instead.
+* OutputPage::wrapWikiMsg() no longer accepts an options parameter. This was
+  deprecated since 1.20.
+* Skin::outputPage() no longer accepts a context. This was deprecated in 1.20.
+* Linker::link() no longer accepts a string for the query array, as was
+  deprecated in 1.20.
+* PrefixSearch::titleSearch(), deprecated in 1.23, has been removed. Use the
+  SearchEngine::defaultPrefixSearch or ::completionSearch() methods instead.
+* The UserRights hook, deprecated in 1.26, has been removed. Instead, use the
+  UserGroupsChanged hook.
+* Skin::getDefaultInstance(), deprecated in 1.27, has been removed. Get the
+  instance from MediaWikiServices instead.
+* The UserLoadFromSession hook, deprecated in 1.27, has been removed.
+* The wfResetSessionID global function, deprecated in 1.27, has been removed.
+  Use MediaWiki\Session\SessionManager instead.
+* The wfGetLBFactory global function, deprecated in 1.27, has been removed.
+  Use MediaWikiServices::getInstance()->getDBLoadBalancerFactory().
 * …
 
 === Deprecations in 1.34 ===
@@ -262,6 +300,14 @@ because of Phabricator reports.
 * DatabaseBlock::setCookie, DatabaseBlock::getCookieValue,
   DatabaseBlock::getIdFromCookieValue and AbstractBlock::shouldTrackWithCookie
   are moved to internal helper methods for BlockManager::trackBlockWithCookie.
+* ResourceLoaderContext::getConfig and ResourceLoaderContext::getLogger have
+  been deprecated. Inside ResourceLoaderModule subclasses, use the local methods
+  instead. Elsewhere, use the methods from the ResourceLoader class.
+* The Preprocessor_DOM implementation has been deprecated.  It will be
+  removed in a future release.  Use the Preprocessor_Hash implementation
+  instead.
+* Sanitizer::attributeWhitelist() and Sanitizer::setupAttributeWhitelist()
+  have been deprecated; they will be made private in the future.
 
 === Other changes in 1.34 ===
 * …
index 5d3adc8..0b93f49 100644 (file)
@@ -416,7 +416,6 @@ $wgAutoloadLocalClasses = [
        'DnsSrvDiscoverer' => __DIR__ . '/includes/libs/DnsSrvDiscoverer.php',
        'DoubleRedirectJob' => __DIR__ . '/includes/jobqueue/jobs/DoubleRedirectJob.php',
        'DoubleRedirectsPage' => __DIR__ . '/includes/specials/SpecialDoubleRedirects.php',
-       'DoubleReplacer' => __DIR__ . '/includes/libs/replacers/DoubleReplacer.php',
        'DummyLinker' => __DIR__ . '/includes/DummyLinker.php',
        'DummySearchIndexFieldDefinition' => __DIR__ . '/includes/search/DummySearchIndexFieldDefinition.php',
        'DummyTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php',
@@ -631,7 +630,6 @@ $wgAutoloadLocalClasses = [
        'HashConfig' => __DIR__ . '/includes/config/HashConfig.php',
        'HashRing' => __DIR__ . '/includes/libs/HashRing.php',
        'HashSiteStore' => __DIR__ . '/includes/site/HashSiteStore.php',
-       'HashtableReplacer' => __DIR__ . '/includes/libs/replacers/HashtableReplacer.php',
        'HistoryAction' => __DIR__ . '/includes/actions/HistoryAction.php',
        'HistoryBlob' => __DIR__ . '/includes/historyblob/HistoryBlob.php',
        'HistoryBlobCurStub' => __DIR__ . '/includes/historyblob/HistoryBlobCurStub.php',
@@ -738,7 +736,7 @@ $wgAutoloadLocalClasses = [
        'LanguageAz' => __DIR__ . '/languages/classes/LanguageAz.php',
        'LanguageBe_tarask' => __DIR__ . '/languages/classes/LanguageBe_tarask.php',
        'LanguageBs' => __DIR__ . '/languages/classes/LanguageBs.php',
-       'LanguageCode' => __DIR__ . '/languages/LanguageCode.php',
+       'LanguageCode' => __DIR__ . '/includes/language/LanguageCode.php',
        'LanguageConverter' => __DIR__ . '/languages/LanguageConverter.php',
        'LanguageCrh' => __DIR__ . '/languages/classes/LanguageCrh.php',
        'LanguageCu' => __DIR__ . '/languages/classes/LanguageCu.php',
@@ -978,12 +976,12 @@ $wgAutoloadLocalClasses = [
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
        'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php',
        'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php',
-       'Message' => __DIR__ . '/includes/Message.php',
+       'Message' => __DIR__ . '/includes/language/Message.php',
        'MessageBlobStore' => __DIR__ . '/includes/resourceloader/MessageBlobStore.php',
        'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php',
        'MessageCacheUpdate' => __DIR__ . '/includes/deferred/MessageCacheUpdate.php',
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
-       'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php',
+       'MessageLocalizer' => __DIR__ . '/includes/language/MessageLocalizer.php',
        'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php',
        'MigrateActors' => __DIR__ . '/maintenance/includes/MigrateActors.php',
        'MigrateArchiveText' => __DIR__ . '/maintenance/migrateArchiveText.php',
@@ -1218,14 +1216,12 @@ $wgAutoloadLocalClasses = [
        'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php',
        'RefreshLinks' => __DIR__ . '/maintenance/refreshLinks.php',
        'RefreshLinksJob' => __DIR__ . '/includes/jobqueue/jobs/RefreshLinksJob.php',
-       'RegexlikeReplacer' => __DIR__ . '/includes/libs/replacers/RegexlikeReplacer.php',
        'RemexStripTagHandler' => __DIR__ . '/includes/parser/RemexStripTagHandler.php',
        'RemoveInvalidEmails' => __DIR__ . '/maintenance/removeInvalidEmails.php',
        'RemoveUnusedAccounts' => __DIR__ . '/maintenance/removeUnusedAccounts.php',
        'RenameDbPrefix' => __DIR__ . '/maintenance/renameDbPrefix.php',
        'RenderAction' => __DIR__ . '/includes/actions/RenderAction.php',
        'ReplacementArray' => __DIR__ . '/includes/libs/ReplacementArray.php',
-       'Replacer' => __DIR__ . '/includes/libs/replacers/Replacer.php',
        'ReplicatedBagOStuff' => __DIR__ . '/includes/libs/objectcache/ReplicatedBagOStuff.php',
        'RepoGroup' => __DIR__ . '/includes/filerepo/RepoGroup.php',
        'RequestContext' => __DIR__ . '/includes/context/RequestContext.php',
@@ -1235,6 +1231,7 @@ $wgAutoloadLocalClasses = [
        'ResetUserTokens' => __DIR__ . '/maintenance/resetUserTokens.php',
        'ResourceFileCache' => __DIR__ . '/includes/cache/ResourceFileCache.php',
        'ResourceLoader' => __DIR__ . '/includes/resourceloader/ResourceLoader.php',
+       'ResourceLoaderCircularDependencyError' => __DIR__ . '/includes/resourceloader/ResourceLoaderCircularDependencyError.php',
        'ResourceLoaderClientHtml' => __DIR__ . '/includes/resourceloader/ResourceLoaderClientHtml.php',
        'ResourceLoaderContext' => __DIR__ . '/includes/resourceloader/ResourceLoaderContext.php',
        'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
@@ -1324,6 +1321,7 @@ $wgAutoloadLocalClasses = [
        'SearchUpdate' => __DIR__ . '/includes/deferred/SearchUpdate.php',
        'SectionProfileCallback' => __DIR__ . '/includes/profiler/SectionProfileCallback.php',
        'SectionProfiler' => __DIR__ . '/includes/profiler/SectionProfiler.php',
+       'SerializedValueContainer' => __DIR__ . '/includes/libs/objectcache/serialized/SerializedValueContainer.php',
        'SevenZipStream' => __DIR__ . '/maintenance/includes/SevenZipStream.php',
        'ShiConverter' => __DIR__ . '/languages/classes/LanguageShi.php',
        'ShortPagesPage' => __DIR__ . '/includes/specials/SpecialShortpages.php',
index e224412..59873ef 100644 (file)
@@ -43,7 +43,7 @@
                "wikimedia/html-formatter": "1.0.2",
                "wikimedia/ip-set": "2.0.1",
                "wikimedia/less.php": "1.8.0",
-               "wikimedia/object-factory": "1.0.0",
+               "wikimedia/object-factory": "2.0.0",
                "wikimedia/password-blacklist": "0.1.4",
                "wikimedia/php-session-serializer": "1.0.7",
                "wikimedia/purtle": "1.0.7",
                "wikimedia/running-stat": "1.2.1",
                "wikimedia/scoped-callback": "3.0.0",
                "wikimedia/utfnormal": "2.0.0",
-               "wikimedia/timestamp": "2.2.0",
+               "wikimedia/timestamp": "3.0.0",
                "wikimedia/wait-condition-loop": "1.0.1",
                "wikimedia/wrappedstring": "3.0.1",
-               "wikimedia/xmp-reader": "0.6.2",
+               "wikimedia/xmp-reader": "0.6.3",
                "zordius/lightncandy": "0.23"
        },
        "require-dev": {
@@ -67,7 +67,7 @@
                "jakub-onderka/php-parallel-lint": "0.9.2",
                "justinrainbow/json-schema": "~5.2",
                "mediawiki/mediawiki-codesniffer": "26.0.0",
-               "monolog/monolog": "~1.22.1",
+               "monolog/monolog": "~1.24.0",
                "nikic/php-parser": "3.1.5",
                "seld/jsonlint": "1.7.1",
                "nmred/kafka-php": "0.1.5",
@@ -77,7 +77,8 @@
                "wikimedia/testing-access-wrapper": "~1.0",
                "wmde/hamcrest-html-matchers": "^0.1.0",
                "mediawiki/mediawiki-phan-config": "0.6.0",
-               "symfony/yaml": "3.4.28"
+               "symfony/yaml": "3.4.28",
+               "johnkary/phpunit-speedtrap": "^1.0 | ^2.0"
        },
        "replace": {
                "symfony/polyfill-ctype": "1.99",
index 6076581..c1db2b6 100644 (file)
                        "type": "array",
                        "description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
                },
+               "RestRoutes": {
+                       "type": "array",
+                       "description": "List of route specifications to be added to the REST API",
+                       "items": {
+                               "type": "object",
+                               "properties": {
+                                       "method": {
+                                               "oneOf": [
+                                                       {
+                                                               "type": "string",
+                                                               "description": "The HTTP method name"
+                                                       },
+                                                       {
+                                                               "type": "array",
+                                                               "items": {
+                                                                       "type": "string",
+                                                                       "description": "An acceptable HTTP method name"
+                                                               }
+                                                       }
+                                               ]
+                                       },
+                                       "path": {
+                                               "type": "string",
+                                               "description": "The path template. This should start with an initial slash, designating the root of the REST API. Path parameters are enclosed in braces, for example /endpoint/{param}."
+                                       },
+                                       "factory": {
+                                               "type": ["string", "array"],
+                                               "description": "A factory function to be called to create the handler for this route"
+                                       },
+                                       "class": {
+                                               "type": "string",
+                                               "description": "The fully-qualified class name of the handler. This should be omitted if a factory is specified."
+                                       },
+                                       "args": {
+                                               "type": "array",
+                                               "description": "The arguments passed to the handler constructor or factory"
+                                       }
+                               }
+                       }
+               },
                "attributes": {
                        "description":"Registration information for other extensions",
                        "type": "object",
index 976d5c2..99a3d1a 100644 (file)
@@ -3004,7 +3004,8 @@ $terms: Search terms, for highlighting
 &$titleSnippet: Label for the link representing the search result. Typically the
   article title.
 $result: The SearchResult object
-$terms: String of the search terms entered
+$terms: array of search terms extracted by SearchDatabase search engines
+  (may not be populated by other search engines).
 $specialSearch: The SpecialSearch object
 &$query: Array of query string parameters for the link representing the search
   result.
@@ -3743,14 +3744,6 @@ $name: user name
 $user: user object
 &$s: database query object
 
-'UserLoadFromSession': DEPRECATED since 1.27! Create a
-MediaWiki\Session\SessionProvider instead.
-Called to authenticate users on external/environmental means; occurs before
-session is loaded.
-$user: user object being loaded
-&$result: set this to a boolean value to abort the normal authentication
-  process
-
 'UserLoadOptions': When user options/preferences are being loaded from the
 database.
 $user: User object
@@ -3831,12 +3824,6 @@ message(s).
 &$user: user retrieving new talks messages
 &$talks: array of new talks page(s)
 
-'UserRights': DEPRECATED since 1.26! Use UserGroupsChanged instead.
-After a user's group memberships are changed.
-&$user: User object that was changed
-$add: Array of strings corresponding to groups added
-$remove: Array of strings corresponding to groups removed
-
 'UserSaveOptions': Called just before saving user preferences. Hook handlers can
 either add or manipulate options, or reset one back to it's default to block
 changing it. Hook handlers are also allowed to abort the process by returning
index 13b6961..cf28762 100644 (file)
@@ -13,8 +13,8 @@ purposes of updating the link tables. This application is now deprecated.
 
 To create a batch, you can use the following code:
 
-$pages = array( 'Main Page', 'Project:Help', /* ... */ );
-$titles = array();
+$pages = [ 'Main Page', 'Project:Help', /* ... */ ];
+$titles = [];
 
 foreach( $pages as $page ){
        $titles[] = Title::newFromText( $page );
index 6b4d37e..42f701c 100644 (file)
@@ -28,16 +28,16 @@ Create a file called ExtensionName.i18n.magic.php with the following contents:
 ----
 <?php
 
-$magicWords = array();
+$magicWords = [];
 
-$magicWords['en'] = array(
+$magicWords['en'] = [
        // Case sensitive.
-       'mag_custom' => array( 1, 'CUSTOM' ),
-);
+       'mag_custom' => [ 1, 'CUSTOM' ],
+];
 
-$magicWords['es'] = array(
-       'mag_custom' => array( 1, 'ADUANERO' ),
-);
+$magicWords['es'] = [
+       'mag_custom' => [ 1, 'ADUANERO' ],
+];
 ----
 
 $wgExtensionMessagesFiles['ExtensionNameMagic'] = __DIR__ . '/ExtensionName.i18n.magic.php';
@@ -62,16 +62,16 @@ Create a file called ExtensionName.i18n.magic.php with the following contents:
 ----
 <?php
 
-$magicWords = array();
+$magicWords = [];
 
-$magicWords['en'] = array(
+$magicWords['en'] = [
        // Case insensitive.
-       'mag_custom' => array( 0, 'custom' ),
-);
+       'mag_custom' => [ 0, 'custom' ],
+];
 
-$magicWords['es'] = array(
-       'mag_custom' => array( 0, 'aduanero' ),
-);
+$magicWords['es'] = [
+       'mag_custom' => [ 0, 'aduanero' ],
+];
 ----
 
 $wgExtensionMessagesFiles['ExtensionNameMagic'] = __DIR__ . '/ExtensionName.i18n.magic.php';
index 1e68fb7..ba325fe 100644 (file)
@@ -61,7 +61,7 @@ on port 11211, using up to 64MB of memory)
 In your LocalSettings.php file, set:
 
        $wgMainCacheType = CACHE_MEMCACHED;
-       $wgMemCachedServers = array( "127.0.0.1:11211" );
+       $wgMemCachedServers = [ "127.0.0.1:11211" ];
 
 The wiki should then use memcached to cache various data. To use
 multiple servers (physically separate boxes or multiple caches
@@ -70,10 +70,10 @@ to the array. To increase the weight of a server (say, because
 it has twice the memory of the others and you want to spread
 usage evenly), make its entry a subarray:
 
-  $wgMemCachedServers = array(
+  $wgMemCachedServers = [
     "127.0.0.1:11211", # one gig on this box
-    array("192.168.0.1:11211", 2 ) # two gigs on the other box
-  );
+    [ "192.168.0.1:11211", 2 ] # two gigs on the other box
+  ];
 
 == PHP client for memcached ==
 
index 6a0dce6..ef9724b 100644 (file)
@@ -166,7 +166,7 @@ EXAMPLE:
 require 'MemCachedClient.inc.php';
 
 // set the servers, with the last one having an integer weight value of 3
-$options["servers"] = array("10.0.0.15:11000","10.0.0.16:11001",array("10.0.0.17:11002", 3));
+$options["servers"] = ["10.0.0.15:11000","10.0.0.16:11001",["10.0.0.17:11002", 3]];
 $options["debug"] = false;
 
 $memc = new MemCachedClient($options);
@@ -175,7 +175,7 @@ $memc = new MemCachedClient($options);
 /***********************
  * STORE AN ARRAY
  ***********************/
-$myarr = array("one","two", 3);
+$myarr = ["one","two", 3];
 $memc->set("key_one", $myarr);
 $val = $memc->get("key_one");
 print $val[0]."\n";    // prints 'one'
index ba4ed74..1434125 100644 (file)
@@ -79,6 +79,8 @@ function wfImageAuthMain() {
                return;
        }
 
+       $user = RequestContext::getMain()->getUser();
+
        // Various extensions may have their own backends that need access.
        // Check if there is a special backend and storage base path for this file.
        foreach ( $wgImgAuthUrlPathMap as $prefix => $storageDir ) {
@@ -87,7 +89,7 @@ function wfImageAuthMain() {
                        $be = FileBackendGroup::singleton()->backendFromPath( $storageDir );
                        $filename = $storageDir . substr( $path, strlen( $prefix ) ); // strip prefix
                        // Check basic user authorization
-                       if ( !RequestContext::getMain()->getUser()->isAllowed( 'read' ) ) {
+                       if ( !$user->isAllowed( 'read' ) ) {
                                wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $path );
                                return;
                        }
@@ -157,7 +159,9 @@ function wfImageAuthMain() {
 
                // Check user authorization for this title
                // Checks Whitelist too
-               if ( !$title->userCan( 'read' ) ) {
+               $permissionManager = \MediaWiki\MediaWikiServices::getInstance()->getPermissionManager();
+
+               if ( !$permissionManager->userCan( 'read', $user, $title ) ) {
                        wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $name );
                        return;
                }
index fa11bcb..b893bc9 100644 (file)
@@ -136,12 +136,14 @@ class AutoLoader {
                        'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
                        'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
                        'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
+                       'MediaWiki\\Rest\\' => __DIR__ . '/Rest/',
                        'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
                        'MediaWiki\\Session\\' => __DIR__ . '/session/',
                        'MediaWiki\\Shell\\' => __DIR__ . '/shell/',
                        'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/',
                        'MediaWiki\\Storage\\' => __DIR__ . '/Storage/',
                        'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/',
+                       'Wikimedia\\ParamValidator\\' => __DIR__ . '/libs/ParamValidator/',
                        'Wikimedia\\Services\\' => __DIR__ . '/libs/services/',
                ];
        }
index 02c9d01..a413037 100644 (file)
@@ -89,12 +89,12 @@ class Autopromote {
 
        /**
         * Recursively check a condition.  Conditions are in the form
-        *   array( '&' or '|' or '^' or '!', cond1, cond2, ... )
+        *   [ '&' or '|' or '^' or '!', cond1, cond2, ... ]
         * where cond1, cond2, ... are themselves conditions; *OR*
         *   APCOND_EMAILCONFIRMED, *OR*
-        *   array( APCOND_EMAILCONFIRMED ), *OR*
-        *   array( APCOND_EDITCOUNT, number of edits ), *OR*
-        *   array( APCOND_AGE, seconds since registration ), *OR*
+        *   [ APCOND_EMAILCONFIRMED ], *OR*
+        *   [ APCOND_EDITCOUNT, number of edits ], *OR*
+        *   [ APCOND_AGE, seconds since registration ], *OR*
         *   similar constructs defined by extensions.
         * This function evaluates the former type recursively, and passes off to
         * self::checkCondition for evaluation of the latter type.
index 96d9726..10155f6 100644 (file)
@@ -193,6 +193,13 @@ $wgScript = false;
  */
 $wgLoadScript = false;
 
+/**
+ * The URL path to the REST API
+ * Defaults to "{$wgScriptPath}/rest.php"
+ * @since 1.34
+ */
+$wgRestPath = false;
+
 /**
  * The URL path of the skins directory.
  * Defaults to "{$wgResourceBasePath}/skins".
@@ -2740,14 +2747,6 @@ $wgUseCdn = false;
  */
 $wgUseESI = false;
 
-/**
- * Send the Key HTTP header for better caching.
- * See https://datatracker.ietf.org/doc/draft-ietf-httpbis-key/ for details.
- * @since 1.27
- * @deprecated in 1.32, the IETF spec expired without becoming a standard.
- */
-$wgUseKeyHeader = false;
-
 /**
  * Add X-Forwarded-Proto to the Vary and Key headers for API requests and
  * RSS/Atom feeds. Use this if you have an SSL termination setup
@@ -4153,8 +4152,10 @@ $wgInvalidRedirectTargets = [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect' ];
  *                    temporary storage. Preprocessor_DOM generally uses less memory;
  *                    the speed of the two is roughly the same.
  *
- *                    If this parameter is not given, it uses Preprocessor_DOM if the
- *                    DOM module is available, otherwise it uses Preprocessor_Hash.
+ *                    If this parameter is not given, it uses Preprocessor_Hash.
+ *
+ * The Preprocessor_DOM class is deprecated, and will be removed in a future
+ * release.
  *
  * The entire associative array will be passed through to the constructor as
  * the first parameter. Note that only Setup.php can use this variable --
@@ -5427,20 +5428,20 @@ $wgAutoConfirmCount = 0;
  *
  * The basic syntax for `$wgAutopromote` is:
  *
- *     $wgAutopromote = array(
+ *     $wgAutopromote = [
  *         'groupname' => cond,
  *         'group2' => cond2,
- *     );
+ *     ];
  *
  * A `cond` may be:
  *  - a single condition without arguments:
  *      Note that Autopromote wraps a single non-array value into an array
  *      e.g. `APCOND_EMAILCONFIRMED` OR
- *           array( `APCOND_EMAILCONFIRMED` )
+ *           [ `APCOND_EMAILCONFIRMED` ]
  *  - a single condition with arguments:
- *      e.g. `array( APCOND_EDITCOUNT, 100 )`
+ *      e.g. `[ APCOND_EDITCOUNT, 100 ]`
  *  - a set of conditions:
- *      e.g. `array( 'operand', cond1, cond2, ... )`
+ *      e.g. `[ 'operand', cond1, cond2, ... ]`
  *
  * When constructing a set of conditions, the following conditions are available:
  *  - `&` (**AND**):
@@ -5451,25 +5452,25 @@ $wgAutoConfirmCount = 0;
  *      promote if user matches **ONLY ONE OF THE CONDITIONS**
  *  - `!` (**NOT**):
  *      promote if user matces **NO** condition
- *  - array( APCOND_EMAILCONFIRMED ):
+ *  - [ APCOND_EMAILCONFIRMED ]:
  *      true if user has a confirmed e-mail
- *  - array( APCOND_EDITCOUNT, number of edits ):
+ *  - [ APCOND_EDITCOUNT, number of edits ]:
  *      true if user has the at least the number of edits as the passed parameter
- *  - array( APCOND_AGE, seconds since registration ):
+ *  - [ APCOND_AGE, seconds since registration ]:
  *      true if the length of time since the user created his/her account
  *      is at least the same length of time as the passed parameter
- *  - array( APCOND_AGE_FROM_EDIT, seconds since first edit ):
+ *  - [ APCOND_AGE_FROM_EDIT, seconds since first edit ]:
  *      true if the length of time since the user made his/her first edit
  *      is at least the same length of time as the passed parameter
- *  - array( APCOND_INGROUPS, group1, group2, ... ):
+ *  - [ APCOND_INGROUPS, group1, group2, ... ]:
  *      true if the user is a member of each of the passed groups
- *  - array( APCOND_ISIP, ip ):
+ *  - [ APCOND_ISIP, ip ]:
  *      true if the user has the passed IP address
- *  - array( APCOND_IPINRANGE, range ):
+ *  - [ APCOND_IPINRANGE, range ]:
  *      true if the user has an IP address in the range of the passed parameter
- *  - array( APCOND_BLOCKED ):
+ *  - [ APCOND_BLOCKED ]:
  *      true if the user is blocked
- *  - array( APCOND_ISBOT ):
+ *  - [ APCOND_ISBOT ]:
  *      true if the user is a bot
  *  - similar constructs can be defined by extensions
  *
@@ -6423,7 +6424,7 @@ $wgDeprecationReleaseLimit = false;
  *
  * @code
  *   $wgProfiler['class'] = 'ProfilerXhprof';
- *   $wgProfiler['output'] = array( 'ProfilerOutputDb' );
+ *   $wgProfiler['output'] = [ 'ProfilerOutputDb' ];
  *   $wgProfiler['sampling'] = 50; // one every 50 requests
  * @endcode
  *
@@ -8096,10 +8097,10 @@ $wgExemptFromUserRobotsControl = null;
 /** @} */ # End robot policy }
 
 /************************************************************************//**
- * @name   AJAX and API
+ * @name   AJAX, Action API and REST API
  * Note: The AJAX entry point which this section refers to is gradually being
- * replaced by the API entry point, api.php. They are essentially equivalent.
- * Both of them are used for dynamic client-side features, via XHR.
+ * replaced by the Action API entry point, api.php. They are essentially
+ * equivalent. Both of them are used for dynamic client-side features, via XHR.
  * @{
  */
 
index 5f98b44..e5cd5ed 100644 (file)
@@ -30,10 +30,6 @@ use Wikimedia\Rdbms\IDatabase;
  */
 
 # Obsolete aliases
-/**
- * @deprecated since 1.28, use DB_REPLICA instead
- */
-define( 'DB_SLAVE', -1 );
 
 /**@{
  * Obsolete IDatabase::makeList() constants
index 882047b..d2f26b3 100644 (file)
@@ -14,7 +14,7 @@
  */
 
 /**
- * Debugging: PHP
+ * Debugging for PHP
  */
 
 // Enable showing of errors
@@ -22,7 +22,7 @@ error_reporting( -1 );
 ini_set( 'display_errors', 1 );
 
 /**
- * Debugging: MediaWiki
+ * Debugging for MediaWiki
  */
 global $wgDevelopmentWarnings, $wgShowExceptionDetails, $wgShowHostnames,
        $wgDebugRawPage, $wgSQLMode, $wgCommandLineMode, $wgDebugLogFile,
@@ -53,9 +53,7 @@ if ( $logDir ) {
        $wgDebugLogGroups['error'] = "$logDir/mw-error.log";
 }
 unset( $logDir );
-// Make caching faster
-$wgMainCacheType = CACHE_ACCEL;
-$wgMessageCacheType = CACHE_ACCEL;
-$wgParserCacheType = CACHE_ACCEL;
-$wgSessionCacheType = CACHE_ACCEL;
-$wgLanguageConverterCacheType = CACHE_ACCEL;
+
+// Disable rate-limiting to allow integration tests to run unthrottled
+// in CI and for devs locally (T225796)
+$wgRateLimits = [];
index e4adb48..d27ef9c 100644 (file)
@@ -4130,7 +4130,7 @@ ERROR;
 
                if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
                        return null;
-               };
+               }
                // Don't add a pointless `<div>` to the page unless a hook caller populated it
                return ( $toolbar === $startingToolbar ) ? null : $toolbar;
        }
index 759732f..05c4655 100644 (file)
@@ -30,7 +30,6 @@ use MediaWiki\MediaWikiServices;
 use MediaWiki\ProcOpenError;
 use MediaWiki\Session\SessionManager;
 use MediaWiki\Shell\Shell;
-use Wikimedia\ScopedCallback;
 use Wikimedia\WrappedString;
 use Wikimedia\AtEase\AtEase;
 
@@ -1882,10 +1881,9 @@ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
 /**
  * Convenience function; returns MediaWiki timestamp for the present time.
  *
- * @return string
+ * @return string TS_MW timestamp
  */
 function wfTimestampNow() {
-       # return NOW
        return MWTimestamp::now( TS_MW );
 }
 
@@ -2431,28 +2429,6 @@ function wfRelativePath( $path, $from ) {
        return implode( DIRECTORY_SEPARATOR, $pieces );
 }
 
-/**
- * Reset the session id
- *
- * @deprecated since 1.27, use MediaWiki\Session\SessionManager instead
- * @since 1.22
- */
-function wfResetSessionID() {
-       wfDeprecated( __FUNCTION__, '1.27' );
-       $session = SessionManager::getGlobalSession();
-       $delay = $session->delaySave();
-
-       $session->resetId();
-
-       // Make sure a session is started, since that's what the old
-       // wfResetSessionID() did.
-       if ( session_id() !== $session->getId() ) {
-               wfSetupSession( $session->getId() );
-       }
-
-       ScopedCallback::consume( $delay );
-}
-
 /**
  * Initialise php session
  *
@@ -2601,19 +2577,6 @@ function wfGetLB( $wiki = false ) {
        }
 }
 
-/**
- * Get the load balancer factory object
- *
- * @deprecated since 1.27, use MediaWikiServices::getInstance()->getDBLoadBalancerFactory() instead.
- * TODO: Remove in MediaWiki 1.35
- *
- * @return \Wikimedia\Rdbms\LBFactory
- */
-function wfGetLBFactory() {
-       wfDeprecated( __METHOD__, '1.27' );
-       return MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-}
-
 /**
  * Find a file.
  * @deprecated since 1.34, use MediaWikiServices
@@ -2762,30 +2725,6 @@ function wfWaitForSlaves(
        return $lbFactory->waitForReplication( $opts );
 }
 
-/**
- * Count down from $seconds to zero on the terminal, with a one-second pause
- * between showing each number. For use in command-line scripts.
- *
- * @deprecated since 1.31, use Maintenance::countDown()
- *
- * @codeCoverageIgnore
- * @param int $seconds
- */
-function wfCountDown( $seconds ) {
-       wfDeprecated( __FUNCTION__, '1.31' );
-       for ( $i = $seconds; $i >= 0; $i-- ) {
-               if ( $i != $seconds ) {
-                       echo str_repeat( "\x08", strlen( $i + 1 ) );
-               }
-               echo $i;
-               flush();
-               if ( $i ) {
-                       sleep( 1 );
-               }
-       }
-       echo "\n";
-}
-
 /**
  * Replace all invalid characters with '-'.
  * Additional characters can be defined in $wgIllegalFileChars (see T22489).
@@ -2885,21 +2824,6 @@ function wfShorthandToInteger( $string = '', $default = -1 ) {
        return $val;
 }
 
-/**
- * Get the normalised IETF language tag
- * See unit test for examples.
- * See mediawiki.language.bcp47 for the JavaScript implementation.
- *
- * @deprecated since 1.31, use LanguageCode::bcp47() directly.
- *
- * @param string $code The language code.
- * @return string The language code which complying with BCP 47 standards.
- */
-function wfBCP47( $code ) {
-       wfDeprecated( __METHOD__, '1.31' );
-       return LanguageCode::bcp47( $code );
-}
-
 /**
  * Get a specific cache object.
  *
index aa51243..fdc348b 100644 (file)
@@ -518,7 +518,7 @@ class Html {
                                        $newValue = [];
                                        foreach ( $value as $k => $v ) {
                                                if ( is_string( $v ) ) {
-                                                       // String values should be normal `array( 'foo' )`
+                                                       // String values should be normal `[ 'foo' ]`
                                                        // Just append them
                                                        if ( !isset( $value[$v] ) ) {
                                                                // As a special case don't set 'foo' if a
index 39f4394..1980154 100644 (file)
@@ -89,12 +89,6 @@ class Linker {
                        return "<!-- ERROR -->$html";
                }
 
-               if ( is_string( $query ) ) {
-                       // some functions withing core using this still hand over query strings
-                       wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
-                       $query = wfCgiToArray( $query );
-               }
-
                $services = MediaWikiServices::getInstance();
                $options = (array)$options;
                if ( $options ) {
index ca77121..69f23c1 100644 (file)
@@ -23,8 +23,8 @@
 use MediaWiki\Logger\LoggerFactory;
 use Psr\Log\LoggerInterface;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\ILBFactory;
 use Wikimedia\Rdbms\ChronologyProtector;
-use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\DBConnectionError;
 use Liuggio\StatsdClient\Sender\SocketSender;
 
@@ -580,15 +580,15 @@ class MediaWiki {
        public static function preOutputCommit(
                IContextSource $context, callable $postCommitWork = null
        ) {
-               // Either all DBs should commit or none
-               ignore_user_abort( true );
-
                $config = $context->getConfig();
                $request = $context->getRequest();
                $output = $context->getOutput();
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
 
-               // Commit all changes
+               // Try to make sure that all RDBMs, session, and other storage updates complete
+               ignore_user_abort( true );
+
+               // Commit all RDBMs changes from the main transaction round
                $lbFactory->commitMasterChanges(
                        __METHOD__,
                        // Abort if any transaction was too big
@@ -596,47 +596,31 @@ class MediaWiki {
                );
                wfDebug( __METHOD__ . ': primary transaction round committed' );
 
-               // Run updates that need to block the user or affect output (this is the last chance)
+               // Run updates that need to block the client or affect output (this is the last chance)
                DeferredUpdates::doUpdates( 'run', DeferredUpdates::PRESEND );
                wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
-               // T214471: persist the session to avoid race conditions on subsequent requests
-               $request->getSession()->save();
-
-               // Should the client return, their request should observe the new ChronologyProtector
-               // DB positions. This request might be on a foreign wiki domain, so synchronously update
-               // the DB positions in all datacenters to be safe. If this output is not a redirect,
-               // then OutputPage::output() will be relatively slow, meaning that running it in
-               // $postCommitWork should help mask the latency of those updates.
-               $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
-               $strategy = 'cookie+sync';
-
-               $allowHeaders = !( $output->isDisabled() || headers_sent() );
-               if ( $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
-                       // OutputPage::output() will be fast, so $postCommitWork is useless for masking
-                       // the latency of synchronously updating the DB positions in all datacenters.
-                       // Try to make use of the time the client spends following redirects instead.
-                       $domainDistance = self::getUrlDomainDistance( $output->getRedirect() );
-                       if ( $domainDistance === 'local' && $allowHeaders ) {
-                               $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
-                               $strategy = 'cookie'; // use same-domain cookie and keep the URL uncluttered
-                       } elseif ( $domainDistance === 'remote' ) {
-                               $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
-                               $strategy = 'cookie+url'; // cross-domain cookie might not work
-                       }
-               }
-
+               // Persist the session to avoid race conditions on subsequent requests by the client
+               $request->getSession()->save(); // T214471
+               wfDebug( __METHOD__ . ': session changes committed' );
+
+               // Figure out whether to wait for DB replication now or to use some method that assures
+               // that subsequent requests by the client will use the DB replication positions written
+               // during the shutdown() call below; the later requires working around replication lag
+               // of the store containing DB replication positions (e.g. dynomite, mcrouter).
+               list( $flags, $strategy ) = self::getChronProtStrategy( $lbFactory, $output );
                // Record ChronologyProtector positions for DBs affected in this request at this point
                $cpIndex = null;
                $cpClientId = null;
                $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex, $cpClientId );
                wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
 
+               $allowHeaders = !( $output->isDisabled() || headers_sent() );
                if ( $cpIndex > 0 ) {
                        if ( $allowHeaders ) {
                                $now = time();
                                $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL;
                                $options = [ 'prefix' => '' ];
-                               $value = LBFactory::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId );
+                               $value = $lbFactory::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId );
                                $request->response()->setCookie( 'cpPosIndex', $value, $expires, $options );
                        }
 
@@ -654,31 +638,66 @@ class MediaWiki {
                        }
                }
 
-               // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
-               // POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
-               // ChronologyProtector works for cacheable URLs.
-               if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
-                       $expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
-                       $options = [ 'prefix' => '' ];
-                       $request->response()->setCookie( 'UseDC', 'master', $expires, $options );
-                       $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
-               }
+               if ( $allowHeaders ) {
+                       // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that
+                       // handles this POST request (e.g. the "master" data center). Also have the user
+                       // briefly bypass CDN so ChronologyProtector works for cacheable URLs.
+                       if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
+                               $expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
+                               $options = [ 'prefix' => '' ];
+                               $request->response()->setCookie( 'UseDC', 'master', $expires, $options );
+                               $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
+                       }
+
+                       // Avoid letting a few seconds of replica DB lag cause a month of stale data.
+                       // This logic is also intimately related to the value of $wgCdnReboundPurgeDelay.
+                       if ( $lbFactory->laggedReplicaUsed() ) {
+                               $maxAge = $config->get( 'CdnMaxageLagged' );
+                               $output->lowerCdnMaxage( $maxAge );
+                               $request->response()->header( "X-Database-Lagged: true" );
+                               wfDebugLog( 'replication',
+                                       "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
+                       }
 
-               // Avoid letting a few seconds of replica DB lag cause a month of stale data. This logic is
-               // also intimately related to the value of $wgCdnReboundPurgeDelay.
-               if ( $lbFactory->laggedReplicaUsed() ) {
-                       $maxAge = $config->get( 'CdnMaxageLagged' );
-                       $output->lowerCdnMaxage( $maxAge );
-                       $request->response()->header( "X-Database-Lagged: true" );
-                       wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
+                       // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
+                       if ( MessageCache::singleton()->isDisabled() ) {
+                               $maxAge = $config->get( 'CdnMaxageSubstitute' );
+                               $output->lowerCdnMaxage( $maxAge );
+                               $request->response()->header( "X-Response-Substitute: true" );
+                       }
                }
+       }
+
+       /**
+        * @param ILBFactory $lbFactory
+        * @param OutputPage $output
+        * @return array
+        */
+       private static function getChronProtStrategy( ILBFactory $lbFactory, OutputPage $output ) {
+               // Should the client return, their request should observe the new ChronologyProtector
+               // DB positions. This request might be on a foreign wiki domain, so synchronously update
+               // the DB positions in all datacenters to be safe. If this output is not a redirect,
+               // then OutputPage::output() will be relatively slow, meaning that running it in
+               // $postCommitWork should help mask the latency of those updates.
+               $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+               $strategy = 'cookie+sync';
 
-               // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
-               if ( MessageCache::singleton()->isDisabled() ) {
-                       $maxAge = $config->get( 'CdnMaxageSubstitute' );
-                       $output->lowerCdnMaxage( $maxAge );
-                       $request->response()->header( "X-Response-Substitute: true" );
+               $allowHeaders = !( $output->isDisabled() || headers_sent() );
+               if ( $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
+                       // OutputPage::output() will be fast, so $postCommitWork is useless for masking
+                       // the latency of synchronously updating the DB positions in all datacenters.
+                       // Try to make use of the time the client spends following redirects instead.
+                       $domainDistance = self::getUrlDomainDistance( $output->getRedirect() );
+                       if ( $domainDistance === 'local' && $allowHeaders ) {
+                               $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+                               $strategy = 'cookie'; // use same-domain cookie and keep the URL uncluttered
+                       } elseif ( $domainDistance === 'remote' ) {
+                               $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+                               $strategy = 'cookie+url'; // cross-domain cookie might not work
+                       }
                }
+
+               return [ $flags, $strategy ];
        }
 
        /**
@@ -917,7 +936,7 @@ class MediaWiki {
 
                // Commit and close up!
                $lbFactory->commitMasterChanges( __METHOD__ );
-               $lbFactory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT );
+               $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
 
                wfDebug( "Request ended normally\n" );
        }
diff --git a/includes/Message.php b/includes/Message.php
deleted file mode 100644 (file)
index 0b3113f..0000000
+++ /dev/null
@@ -1,1396 +0,0 @@
-<?php
-/**
- * Fetching and processing of interface messages.
- *
- * 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
- * @author Niklas Laxström
- */
-use MediaWiki\MediaWikiServices;
-
-/**
- * The Message class provides methods which fulfil two basic services:
- *  - fetching interface messages
- *  - processing messages into a variety of formats
- *
- * First implemented with MediaWiki 1.17, the Message class is intended to
- * replace the old wfMsg* functions that over time grew unusable.
- * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
- * between old and new functions.
- *
- * You should use the wfMessage() global function which acts as a wrapper for
- * the Message class. The wrapper let you pass parameters as arguments.
- *
- * The most basic usage cases would be:
- *
- * @code
- *     // Initialize a Message object using the 'some_key' message key
- *     $message = wfMessage( 'some_key' );
- *
- *     // Using two parameters those values are strings 'value1' and 'value2':
- *     $message = wfMessage( 'some_key',
- *          'value1', 'value2'
- *     );
- * @endcode
- *
- * @section message_global_fn Global function wrapper:
- *
- * Since wfMessage() returns a Message instance, you can chain its call with
- * a method. Some of them return a Message instance too so you can chain them.
- * You will find below several examples of wfMessage() usage.
- *
- * Fetching a message text for interface message:
- *
- * @code
- *    $button = Xml::button(
- *         wfMessage( 'submit' )->text()
- *    );
- * @endcode
- *
- * A Message instance can be passed parameters after it has been constructed,
- * use the params() method to do so:
- *
- * @code
- *     wfMessage( 'welcome-to' )
- *         ->params( $wgSitename )
- *         ->text();
- * @endcode
- *
- * {{GRAMMAR}} and friends work correctly:
- *
- * @code
- *    wfMessage( 'are-friends',
- *        $user, $friend
- *    );
- *    wfMessage( 'bad-message' )
- *         ->rawParams( '<script>...</script>' )
- *         ->escaped();
- * @endcode
- *
- * @section message_language Changing language:
- *
- * Messages can be requested in a different language or in whatever current
- * content language is being used. The methods are:
- *     - Message->inContentLanguage()
- *     - Message->inLanguage()
- *
- * Sometimes the message text ends up in the database, so content language is
- * needed:
- *
- * @code
- *    wfMessage( 'file-log',
- *        $user, $filename
- *    )->inContentLanguage()->text();
- * @endcode
- *
- * Checking whether a message exists:
- *
- * @code
- *    wfMessage( 'mysterious-message' )->exists()
- *    // returns a boolean whether the 'mysterious-message' key exist.
- * @endcode
- *
- * If you want to use a different language:
- *
- * @code
- *    $userLanguage = $user->getOption( 'language' );
- *    wfMessage( 'email-header' )
- *         ->inLanguage( $userLanguage )
- *         ->plain();
- * @endcode
- *
- * @note You can parse the text only in the content or interface languages
- *
- * @section message_compare_old Comparison with old wfMsg* functions:
- *
- * Use full parsing:
- *
- * @code
- *     // old style:
- *     wfMsgExt( 'key', [ 'parseinline' ], 'apple' );
- *     // new style:
- *     wfMessage( 'key', 'apple' )->parse();
- * @endcode
- *
- * Parseinline is used because it is more useful when pre-building HTML.
- * In normal use it is better to use OutputPage::(add|wrap)WikiMsg.
- *
- * Places where HTML cannot be used. {{-transformation is done.
- * @code
- *     // old style:
- *     wfMsgExt( 'key', [ 'parsemag' ], 'apple', 'pear' );
- *     // new style:
- *     wfMessage( 'key', 'apple', 'pear' )->text();
- * @endcode
- *
- * Shortcut for escaping the message too, similar to wfMsgHTML(), but
- * parameters are not replaced after escaping by default.
- * @code
- *     $escaped = wfMessage( 'key' )
- *          ->rawParams( 'apple' )
- *          ->escaped();
- * @endcode
- *
- * @section message_appendix Appendix:
- *
- * @todo
- * - test, can we have tests?
- * - this documentation needs to be extended
- *
- * @see https://www.mediawiki.org/wiki/WfMessage()
- * @see https://www.mediawiki.org/wiki/New_messages_API
- * @see https://www.mediawiki.org/wiki/Localisation
- *
- * @since 1.17
- */
-class Message implements MessageSpecifier, Serializable {
-       /** Use message text as-is */
-       const FORMAT_PLAIN = 'plain';
-       /** Use normal wikitext -> HTML parsing (the result will be wrapped in a block-level HTML tag) */
-       const FORMAT_BLOCK_PARSE = 'block-parse';
-       /** Use normal wikitext -> HTML parsing but strip the block-level wrapper */
-       const FORMAT_PARSE = 'parse';
-       /** Transform {{..}} constructs but don't transform to HTML */
-       const FORMAT_TEXT = 'text';
-       /** Transform {{..}} constructs, HTML-escape the result */
-       const FORMAT_ESCAPED = 'escaped';
-
-       /**
-        * Mapping from Message::listParam() types to Language methods.
-        * @var array
-        */
-       protected static $listTypeMap = [
-               'comma' => 'commaList',
-               'semicolon' => 'semicolonList',
-               'pipe' => 'pipeList',
-               'text' => 'listToText',
-       ];
-
-       /**
-        * In which language to get this message. True, which is the default,
-        * means the current user language, false content language.
-        *
-        * @var bool
-        */
-       protected $interface = true;
-
-       /**
-        * In which language to get this message. Overrides the $interface setting.
-        *
-        * @var Language|bool Explicit language object, or false for user language
-        */
-       protected $language = false;
-
-       /**
-        * @var string The message key. If $keysToTry has more than one element,
-        * this may change to one of the keys to try when fetching the message text.
-        */
-       protected $key;
-
-       /**
-        * @var string[] List of keys to try when fetching the message.
-        */
-       protected $keysToTry;
-
-       /**
-        * @var array List of parameters which will be substituted into the message.
-        */
-       protected $parameters = [];
-
-       /**
-        * @var string
-        * @deprecated
-        */
-       protected $format = 'parse';
-
-       /**
-        * @var bool Whether database can be used.
-        */
-       protected $useDatabase = true;
-
-       /**
-        * @var Title Title object to use as context.
-        */
-       protected $title = null;
-
-       /**
-        * @var Content Content object representing the message.
-        */
-       protected $content = null;
-
-       /**
-        * @var string
-        */
-       protected $message;
-
-       /**
-        * @since 1.17
-        * @param string|string[]|MessageSpecifier $key Message key, or array of
-        * message keys to try and use the first non-empty message for, or a
-        * MessageSpecifier to copy from.
-        * @param array $params Message parameters.
-        * @param Language|null $language [optional] Language to use (defaults to current user language).
-        * @throws InvalidArgumentException
-        */
-       public function __construct( $key, $params = [], Language $language = null ) {
-               if ( $key instanceof MessageSpecifier ) {
-                       if ( $params ) {
-                               throw new InvalidArgumentException(
-                                       '$params must be empty if $key is a MessageSpecifier'
-                               );
-                       }
-                       $params = $key->getParams();
-                       $key = $key->getKey();
-               }
-
-               if ( !is_string( $key ) && !is_array( $key ) ) {
-                       throw new InvalidArgumentException( '$key must be a string or an array' );
-               }
-
-               $this->keysToTry = (array)$key;
-
-               if ( empty( $this->keysToTry ) ) {
-                       throw new InvalidArgumentException( '$key must not be an empty list' );
-               }
-
-               $this->key = reset( $this->keysToTry );
-
-               $this->parameters = array_values( $params );
-               // User language is only resolved in getLanguage(). This helps preserve the
-               // semantic intent of "user language" across serialize() and unserialize().
-               $this->language = $language ?: false;
-       }
-
-       /**
-        * @see Serializable::serialize()
-        * @since 1.26
-        * @return string
-        */
-       public function serialize() {
-               return serialize( [
-                       'interface' => $this->interface,
-                       'language' => $this->language ? $this->language->getCode() : false,
-                       'key' => $this->key,
-                       'keysToTry' => $this->keysToTry,
-                       'parameters' => $this->parameters,
-                       'format' => $this->format,
-                       'useDatabase' => $this->useDatabase,
-                       'titlestr' => $this->title ? $this->title->getFullText() : null,
-               ] );
-       }
-
-       /**
-        * @see Serializable::unserialize()
-        * @since 1.26
-        * @param string $serialized
-        */
-       public function unserialize( $serialized ) {
-               $data = unserialize( $serialized );
-               if ( !is_array( $data ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ': Invalid serialized data' );
-               }
-
-               $this->interface = $data['interface'];
-               $this->key = $data['key'];
-               $this->keysToTry = $data['keysToTry'];
-               $this->parameters = $data['parameters'];
-               $this->format = $data['format'];
-               $this->useDatabase = $data['useDatabase'];
-               $this->language = $data['language'] ? Language::factory( $data['language'] ) : false;
-
-               if ( isset( $data['titlestr'] ) ) {
-                       $this->title = Title::newFromText( $data['titlestr'] );
-               } elseif ( isset( $data['title'] ) && $data['title'] instanceof Title ) {
-                       // Old serializations from before December 2018
-                       $this->title = $data['title'];
-               } else {
-                       $this->title = null; // Explicit for sanity
-               }
-       }
-
-       /**
-        * @since 1.24
-        *
-        * @return bool True if this is a multi-key message, that is, if the key provided to the
-        * constructor was a fallback list of keys to try.
-        */
-       public function isMultiKey() {
-               return count( $this->keysToTry ) > 1;
-       }
-
-       /**
-        * @since 1.24
-        *
-        * @return string[] The list of keys to try when fetching the message text,
-        * in order of preference.
-        */
-       public function getKeysToTry() {
-               return $this->keysToTry;
-       }
-
-       /**
-        * Returns the message key.
-        *
-        * If a list of multiple possible keys was supplied to the constructor, this method may
-        * return any of these keys. After the message has been fetched, this method will return
-        * the key that was actually used to fetch the message.
-        *
-        * @since 1.21
-        *
-        * @return string
-        */
-       public function getKey() {
-               return $this->key;
-       }
-
-       /**
-        * Returns the message parameters.
-        *
-        * @since 1.21
-        *
-        * @return array
-        */
-       public function getParams() {
-               return $this->parameters;
-       }
-
-       /**
-        * Returns the message format.
-        *
-        * @since 1.21
-        *
-        * @return string
-        * @deprecated since 1.29 formatting is not stateful
-        */
-       public function getFormat() {
-               wfDeprecated( __METHOD__, '1.29' );
-               return $this->format;
-       }
-
-       /**
-        * Returns the Language of the Message.
-        *
-        * @since 1.23
-        *
-        * @return Language
-        */
-       public function getLanguage() {
-               // Defaults to false which means current user language
-               return $this->language ?: RequestContext::getMain()->getLanguage();
-       }
-
-       /**
-        * Factory function that is just wrapper for the real constructor. It is
-        * intended to be used instead of the real constructor, because it allows
-        * chaining method calls, while new objects don't.
-        *
-        * @since 1.17
-        *
-        * @param string|string[]|MessageSpecifier $key
-        * @param mixed $param,... Parameters as strings.
-        *
-        * @return Message
-        */
-       public static function newFromKey( $key /*...*/ ) {
-               $params = func_get_args();
-               array_shift( $params );
-               return new self( $key, $params );
-       }
-
-       /**
-        * Transform a MessageSpecifier or a primitive value used interchangeably with
-        * specifiers (a message key string, or a key + params array) into a proper Message.
-        *
-        * Also accepts a MessageSpecifier inside an array: that's not considered a valid format
-        * but is an easy error to make due to how StatusValue stores messages internally.
-        * Further array elements are ignored in that case.
-        *
-        * @param string|array|MessageSpecifier $value
-        * @return Message
-        * @throws InvalidArgumentException
-        * @since 1.27
-        */
-       public static function newFromSpecifier( $value ) {
-               $params = [];
-               if ( is_array( $value ) ) {
-                       $params = $value;
-                       $value = array_shift( $params );
-               }
-
-               if ( $value instanceof Message ) { // Message, RawMessage, ApiMessage, etc
-                       $message = clone $value;
-               } elseif ( $value instanceof MessageSpecifier ) {
-                       $message = new Message( $value );
-               } elseif ( is_string( $value ) ) {
-                       $message = new Message( $value, $params );
-               } else {
-                       throw new InvalidArgumentException( __METHOD__ . ': invalid argument type '
-                               . gettype( $value ) );
-               }
-
-               return $message;
-       }
-
-       /**
-        * Factory function accepting multiple message keys and returning a message instance
-        * for the first message which is non-empty. If all messages are empty then an
-        * instance of the first message key is returned.
-        *
-        * @since 1.18
-        *
-        * @param string|string[] $keys,... Message keys, or first argument as an array of all the
-        * message keys.
-        *
-        * @return Message
-        */
-       public static function newFallbackSequence( /*...*/ ) {
-               $keys = func_get_args();
-               if ( func_num_args() == 1 ) {
-                       if ( is_array( $keys[0] ) ) {
-                               // Allow an array to be passed as the first argument instead
-                               $keys = array_values( $keys[0] );
-                       } else {
-                               // Optimize a single string to not need special fallback handling
-                               $keys = $keys[0];
-                       }
-               }
-               return new self( $keys );
-       }
-
-       /**
-        * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace.
-        * The title will be for the current language, if the message key is in
-        * $wgForceUIMsgAsContentMsg it will be append with the language code (except content
-        * language), because Message::inContentLanguage will also return in user language.
-        *
-        * @see $wgForceUIMsgAsContentMsg
-        * @return Title
-        * @since 1.26
-        */
-       public function getTitle() {
-               global $wgForceUIMsgAsContentMsg;
-
-               $contLang = MediaWikiServices::getInstance()->getContentLanguage();
-               $lang = $this->getLanguage();
-               $title = $this->key;
-               if (
-                       !$lang->equals( $contLang )
-                       && in_array( $this->key, (array)$wgForceUIMsgAsContentMsg )
-               ) {
-                       $title .= '/' . $lang->getCode();
-               }
-
-               return Title::makeTitle(
-                       NS_MEDIAWIKI, $contLang->ucfirst( strtr( $title, ' ', '_' ) ) );
-       }
-
-       /**
-        * Adds parameters to the parameter list of this message.
-        *
-        * @since 1.17
-        *
-        * @param mixed $args,... Parameters as strings or arrays from
-        *  Message::numParam() and the like, or a single array of parameters.
-        *
-        * @return Message $this
-        */
-       public function params( /*...*/ ) {
-               $args = func_get_args();
-
-               // If $args has only one entry and it's an array, then it's either a
-               // non-varargs call or it happens to be a call with just a single
-               // "special" parameter. Since the "special" parameters don't have any
-               // numeric keys, we'll test that to differentiate the cases.
-               if ( count( $args ) === 1 && isset( $args[0] ) && is_array( $args[0] ) ) {
-                       if ( $args[0] === [] ) {
-                               $args = [];
-                       } else {
-                               foreach ( $args[0] as $key => $value ) {
-                                       if ( is_int( $key ) ) {
-                                               $args = $args[0];
-                                               break;
-                                       }
-                               }
-                       }
-               }
-
-               $this->parameters = array_merge( $this->parameters, array_values( $args ) );
-               return $this;
-       }
-
-       /**
-        * Add parameters that are substituted after parsing or escaping.
-        * In other words the parsing process cannot access the contents
-        * of this type of parameter, and you need to make sure it is
-        * sanitized beforehand.  The parser will see "$n", instead.
-        *
-        * @since 1.17
-        *
-        * @param mixed $params,... Raw parameters as strings, or a single argument that is
-        * an array of raw parameters.
-        *
-        * @return Message $this
-        */
-       public function rawParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::rawParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are numeric and will be passed through
-        * Language::formatNum before substitution
-        *
-        * @since 1.18
-        *
-        * @param mixed $param,... Numeric parameters, or a single argument that is
-        * an array of numeric parameters.
-        *
-        * @return Message $this
-        */
-       public function numParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::numParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are durations of time and will be passed through
-        * Language::formatDuration before substitution
-        *
-        * @since 1.22
-        *
-        * @param int|int[] $param,... Duration parameters, or a single argument that is
-        * an array of duration parameters.
-        *
-        * @return Message $this
-        */
-       public function durationParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::durationParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are expiration times and will be passed through
-        * Language::formatExpiry before substitution
-        *
-        * @since 1.22
-        *
-        * @param string|string[] $param,... Expiry parameters, or a single argument that is
-        * an array of expiry parameters.
-        *
-        * @return Message $this
-        */
-       public function expiryParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::expiryParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are time periods and will be passed through
-        * Language::formatTimePeriod before substitution
-        *
-        * @since 1.22
-        *
-        * @param int|int[] $param,... Time period parameters, or a single argument that is
-        * an array of time period parameters.
-        *
-        * @return Message $this
-        */
-       public function timeperiodParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::timeperiodParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are file sizes and will be passed through
-        * Language::formatSize before substitution
-        *
-        * @since 1.22
-        *
-        * @param int|int[] $param,... Size parameters, or a single argument that is
-        * an array of size parameters.
-        *
-        * @return Message $this
-        */
-       public function sizeParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::sizeParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are bitrates and will be passed through
-        * Language::formatBitrate before substitution
-        *
-        * @since 1.22
-        *
-        * @param int|int[] $param,... Bit rate parameters, or a single argument that is
-        * an array of bit rate parameters.
-        *
-        * @return Message $this
-        */
-       public function bitrateParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::bitrateParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Add parameters that are plaintext and will be passed through without
-        * the content being evaluated.  Plaintext parameters are not valid as
-        * arguments to parser functions. This differs from self::rawParams in
-        * that the Message class handles escaping to match the output format.
-        *
-        * @since 1.25
-        *
-        * @param string|string[] $param,... plaintext parameters, or a single argument that is
-        * an array of plaintext parameters.
-        *
-        * @return Message $this
-        */
-       public function plaintextParams( /*...*/ ) {
-               $params = func_get_args();
-               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-               foreach ( $params as $param ) {
-                       $this->parameters[] = self::plaintextParam( $param );
-               }
-               return $this;
-       }
-
-       /**
-        * Set the language and the title from a context object
-        *
-        * @since 1.19
-        *
-        * @param IContextSource $context
-        *
-        * @return Message $this
-        */
-       public function setContext( IContextSource $context ) {
-               $this->inLanguage( $context->getLanguage() );
-               $this->title( $context->getTitle() );
-               $this->interface = true;
-
-               return $this;
-       }
-
-       /**
-        * Request the message in any language that is supported.
-        *
-        * As a side effect interface message status is unconditionally
-        * turned off.
-        *
-        * @since 1.17
-        * @param Language|string $lang Language code or Language object.
-        * @return Message $this
-        * @throws MWException
-        */
-       public function inLanguage( $lang ) {
-               $previousLanguage = $this->language;
-
-               if ( $lang instanceof Language ) {
-                       $this->language = $lang;
-               } elseif ( is_string( $lang ) ) {
-                       if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) {
-                               $this->language = Language::factory( $lang );
-                       }
-               } elseif ( $lang instanceof StubUserLang ) {
-                       $this->language = false;
-               } else {
-                       $type = gettype( $lang );
-                       throw new MWException( __METHOD__ . " must be "
-                               . "passed a String or Language object; $type given"
-                       );
-               }
-
-               if ( $this->language !== $previousLanguage ) {
-                       // The language has changed. Clear the message cache.
-                       $this->message = null;
-               }
-               $this->interface = false;
-               return $this;
-       }
-
-       /**
-        * Request the message in the wiki's content language,
-        * unless it is disabled for this message.
-        *
-        * @since 1.17
-        * @see $wgForceUIMsgAsContentMsg
-        *
-        * @return Message $this
-        */
-       public function inContentLanguage() {
-               global $wgForceUIMsgAsContentMsg;
-               if ( in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) {
-                       return $this;
-               }
-
-               $this->inLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
-               return $this;
-       }
-
-       /**
-        * Allows manipulating the interface message flag directly.
-        * Can be used to restore the flag after setting a language.
-        *
-        * @since 1.20
-        *
-        * @param bool $interface
-        *
-        * @return Message $this
-        */
-       public function setInterfaceMessageFlag( $interface ) {
-               $this->interface = (bool)$interface;
-               return $this;
-       }
-
-       /**
-        * Enable or disable database use.
-        *
-        * @since 1.17
-        *
-        * @param bool $useDatabase
-        *
-        * @return Message $this
-        */
-       public function useDatabase( $useDatabase ) {
-               $this->useDatabase = (bool)$useDatabase;
-               $this->message = null;
-               return $this;
-       }
-
-       /**
-        * Set the Title object to use as context when transforming the message
-        *
-        * @since 1.18
-        *
-        * @param Title $title
-        *
-        * @return Message $this
-        */
-       public function title( $title ) {
-               $this->title = $title;
-               return $this;
-       }
-
-       /**
-        * Returns the message as a Content object.
-        *
-        * @return Content
-        */
-       public function content() {
-               if ( !$this->content ) {
-                       $this->content = new MessageContent( $this );
-               }
-
-               return $this->content;
-       }
-
-       /**
-        * Returns the message parsed from wikitext to HTML.
-        *
-        * @since 1.17
-        *
-        * @param string|null $format One of the FORMAT_* constants. Null means use whatever was used
-        *   the last time (this is for B/C and should be avoided).
-        *
-        * @return string HTML
-        * @suppress SecurityCheck-DoubleEscaped phan false positive
-        */
-       public function toString( $format = null ) {
-               if ( $format === null ) {
-                       $ex = new LogicException( __METHOD__ . ' using implicit format: ' . $this->format );
-                       \MediaWiki\Logger\LoggerFactory::getInstance( 'message-format' )->warning(
-                               $ex->getMessage(), [ 'exception' => $ex, 'format' => $this->format, 'key' => $this->key ] );
-                       $format = $this->format;
-               }
-               $string = $this->fetchMessage();
-
-               if ( $string === false ) {
-                       // Err on the side of safety, ensure that the output
-                       // is always html safe in the event the message key is
-                       // missing, since in that case its highly likely the
-                       // message key is user-controlled.
-                       // '⧼' is used instead of '<' to side-step any
-                       // double-escaping issues.
-                       // (Keep synchronised with mw.Message#toString in JS.)
-                       return '⧼' . htmlspecialchars( $this->key ) . '⧽';
-               }
-
-               # Replace $* with a list of parameters for &uselang=qqx.
-               if ( strpos( $string, '$*' ) !== false ) {
-                       $paramlist = '';
-                       if ( $this->parameters !== [] ) {
-                               $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
-                       }
-                       $string = str_replace( '$*', $paramlist, $string );
-               }
-
-               # Replace parameters before text parsing
-               $string = $this->replaceParameters( $string, 'before', $format );
-
-               # Maybe transform using the full parser
-               if ( $format === self::FORMAT_PARSE ) {
-                       $string = $this->parseText( $string );
-                       $string = Parser::stripOuterParagraph( $string );
-               } elseif ( $format === self::FORMAT_BLOCK_PARSE ) {
-                       $string = $this->parseText( $string );
-               } elseif ( $format === self::FORMAT_TEXT ) {
-                       $string = $this->transformText( $string );
-               } elseif ( $format === self::FORMAT_ESCAPED ) {
-                       $string = $this->transformText( $string );
-                       $string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
-               }
-
-               # Raw parameter replacement
-               $string = $this->replaceParameters( $string, 'after', $format );
-
-               return $string;
-       }
-
-       /**
-        * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
-        *     $foo = new Message( $key );
-        *     $string = "<abbr>$foo</abbr>";
-        *
-        * @since 1.18
-        *
-        * @return string
-        */
-       public function __toString() {
-               // PHP doesn't allow __toString to throw exceptions and will
-               // trigger a fatal error if it does. So, catch any exceptions.
-
-               try {
-                       return $this->toString( self::FORMAT_PARSE );
-               } catch ( Exception $ex ) {
-                       try {
-                               trigger_error( "Exception caught in " . __METHOD__ . " (message " . $this->key . "): "
-                                       . $ex, E_USER_WARNING );
-                       } catch ( Exception $ex ) {
-                               // Doh! Cause a fatal error after all?
-                       }
-
-                       return '⧼' . htmlspecialchars( $this->key ) . '⧽';
-               }
-       }
-
-       /**
-        * Fully parse the text from wikitext to HTML.
-        *
-        * @since 1.17
-        *
-        * @return string Parsed HTML.
-        */
-       public function parse() {
-               $this->format = self::FORMAT_PARSE;
-               return $this->toString( self::FORMAT_PARSE );
-       }
-
-       /**
-        * Returns the message text. {{-transformation is done.
-        *
-        * @since 1.17
-        *
-        * @return string Unescaped message text.
-        */
-       public function text() {
-               $this->format = self::FORMAT_TEXT;
-               return $this->toString( self::FORMAT_TEXT );
-       }
-
-       /**
-        * Returns the message text as-is, only parameters are substituted.
-        *
-        * @since 1.17
-        *
-        * @return string Unescaped untransformed message text.
-        */
-       public function plain() {
-               $this->format = self::FORMAT_PLAIN;
-               return $this->toString( self::FORMAT_PLAIN );
-       }
-
-       /**
-        * Returns the parsed message text which is always surrounded by a block element.
-        *
-        * @since 1.17
-        *
-        * @return string HTML
-        */
-       public function parseAsBlock() {
-               $this->format = self::FORMAT_BLOCK_PARSE;
-               return $this->toString( self::FORMAT_BLOCK_PARSE );
-       }
-
-       /**
-        * Returns the message text. {{-transformation is done and the result
-        * is escaped excluding any raw parameters.
-        *
-        * @since 1.17
-        *
-        * @return string Escaped message text.
-        */
-       public function escaped() {
-               $this->format = self::FORMAT_ESCAPED;
-               return $this->toString( self::FORMAT_ESCAPED );
-       }
-
-       /**
-        * Check whether a message key has been defined currently.
-        *
-        * @since 1.17
-        *
-        * @return bool
-        */
-       public function exists() {
-               return $this->fetchMessage() !== false;
-       }
-
-       /**
-        * Check whether a message does not exist, or is an empty string
-        *
-        * @since 1.18
-        * @todo FIXME: Merge with isDisabled()?
-        *
-        * @return bool
-        */
-       public function isBlank() {
-               $message = $this->fetchMessage();
-               return $message === false || $message === '';
-       }
-
-       /**
-        * Check whether a message does not exist, is an empty string, or is "-".
-        *
-        * @since 1.18
-        *
-        * @return bool
-        */
-       public function isDisabled() {
-               $message = $this->fetchMessage();
-               return $message === false || $message === '' || $message === '-';
-       }
-
-       /**
-        * @since 1.17
-        *
-        * @param mixed $raw
-        *
-        * @return array Array with a single "raw" key.
-        */
-       public static function rawParam( $raw ) {
-               return [ 'raw' => $raw ];
-       }
-
-       /**
-        * @since 1.18
-        *
-        * @param mixed $num
-        *
-        * @return array Array with a single "num" key.
-        */
-       public static function numParam( $num ) {
-               return [ 'num' => $num ];
-       }
-
-       /**
-        * @since 1.22
-        *
-        * @param int $duration
-        *
-        * @return int[] Array with a single "duration" key.
-        */
-       public static function durationParam( $duration ) {
-               return [ 'duration' => $duration ];
-       }
-
-       /**
-        * @since 1.22
-        *
-        * @param string $expiry
-        *
-        * @return string[] Array with a single "expiry" key.
-        */
-       public static function expiryParam( $expiry ) {
-               return [ 'expiry' => $expiry ];
-       }
-
-       /**
-        * @since 1.22
-        *
-        * @param int $period
-        *
-        * @return int[] Array with a single "period" key.
-        */
-       public static function timeperiodParam( $period ) {
-               return [ 'period' => $period ];
-       }
-
-       /**
-        * @since 1.22
-        *
-        * @param int $size
-        *
-        * @return int[] Array with a single "size" key.
-        */
-       public static function sizeParam( $size ) {
-               return [ 'size' => $size ];
-       }
-
-       /**
-        * @since 1.22
-        *
-        * @param int $bitrate
-        *
-        * @return int[] Array with a single "bitrate" key.
-        */
-       public static function bitrateParam( $bitrate ) {
-               return [ 'bitrate' => $bitrate ];
-       }
-
-       /**
-        * @since 1.25
-        *
-        * @param string $plaintext
-        *
-        * @return string[] Array with a single "plaintext" key.
-        */
-       public static function plaintextParam( $plaintext ) {
-               return [ 'plaintext' => $plaintext ];
-       }
-
-       /**
-        * @since 1.29
-        *
-        * @param array $list
-        * @param string $type 'comma', 'semicolon', 'pipe', 'text'
-        * @return array Array with "list" and "type" keys.
-        */
-       public static function listParam( array $list, $type = 'text' ) {
-               if ( !isset( self::$listTypeMap[$type] ) ) {
-                       throw new InvalidArgumentException(
-                               "Invalid type '$type'. Known types are: " . implode( ', ', array_keys( self::$listTypeMap ) )
-                       );
-               }
-               return [ 'list' => $list, 'type' => $type ];
-       }
-
-       /**
-        * Substitutes any parameters into the message text.
-        *
-        * @since 1.17
-        *
-        * @param string $message The message text.
-        * @param string $type Either "before" or "after".
-        * @param string $format One of the FORMAT_* constants.
-        *
-        * @return string
-        */
-       protected function replaceParameters( $message, $type, $format ) {
-               // A temporary marker for $1 parameters that is only valid
-               // in non-attribute contexts. However if the entire message is escaped
-               // then we don't want to use it because it will be mangled in all contexts
-               // and its unnessary as ->escaped() messages aren't html.
-               $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
-               $replacementKeys = [];
-               foreach ( $this->parameters as $n => $param ) {
-                       list( $paramType, $value ) = $this->extractParam( $param, $format );
-                       if ( $type === 'before' ) {
-                               if ( $paramType === 'before' ) {
-                                       $replacementKeys['$' . ( $n + 1 )] = $value;
-                               } else /* $paramType === 'after' */ {
-                                       // To protect against XSS from replacing parameters
-                                       // inside html attributes, we convert $1 to $'"1.
-                                       // In the event that one of the parameters ends up
-                                       // in an attribute, either the ' or the " will be
-                                       // escaped, breaking the replacement and avoiding XSS.
-                                       $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
-                               }
-                       } elseif ( $paramType === 'after' ) {
-                               $replacementKeys[$marker . ( $n + 1 )] = $value;
-                       }
-               }
-               return strtr( $message, $replacementKeys );
-       }
-
-       /**
-        * Extracts the parameter type and preprocessed the value if needed.
-        *
-        * @since 1.18
-        *
-        * @param mixed $param Parameter as defined in this class.
-        * @param string $format One of the FORMAT_* constants.
-        *
-        * @return array Array with the parameter type (either "before" or "after") and the value.
-        */
-       protected function extractParam( $param, $format ) {
-               if ( is_array( $param ) ) {
-                       if ( isset( $param['raw'] ) ) {
-                               return [ 'after', $param['raw'] ];
-                       } elseif ( isset( $param['num'] ) ) {
-                               // Replace number params always in before step for now.
-                               // No support for combined raw and num params
-                               return [ 'before', $this->getLanguage()->formatNum( $param['num'] ) ];
-                       } elseif ( isset( $param['duration'] ) ) {
-                               return [ 'before', $this->getLanguage()->formatDuration( $param['duration'] ) ];
-                       } elseif ( isset( $param['expiry'] ) ) {
-                               return [ 'before', $this->getLanguage()->formatExpiry( $param['expiry'] ) ];
-                       } elseif ( isset( $param['period'] ) ) {
-                               return [ 'before', $this->getLanguage()->formatTimePeriod( $param['period'] ) ];
-                       } elseif ( isset( $param['size'] ) ) {
-                               return [ 'before', $this->getLanguage()->formatSize( $param['size'] ) ];
-                       } elseif ( isset( $param['bitrate'] ) ) {
-                               return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ];
-                       } elseif ( isset( $param['plaintext'] ) ) {
-                               return [ 'after', $this->formatPlaintext( $param['plaintext'], $format ) ];
-                       } elseif ( isset( $param['list'] ) ) {
-                               return $this->formatListParam( $param['list'], $param['type'], $format );
-                       } else {
-                               if ( !is_scalar( $param ) ) {
-                                       $param = serialize( $param );
-                               }
-                               \MediaWiki\Logger\LoggerFactory::getInstance( 'Bug58676' )->warning(
-                                       'Invalid parameter for message "{msgkey}": {param}',
-                                       [
-                                               'exception' => new Exception,
-                                               'msgkey' => $this->getKey(),
-                                               'param' => htmlspecialchars( $param ),
-                                       ]
-                               );
-
-                               return [ 'before', '[INVALID]' ];
-                       }
-               } elseif ( $param instanceof Message ) {
-                       // Match language, flags, etc. to the current message.
-                       $msg = clone $param;
-                       if ( $msg->language !== $this->language || $msg->useDatabase !== $this->useDatabase ) {
-                               // Cache depends on these parameters
-                               $msg->message = null;
-                       }
-                       $msg->interface = $this->interface;
-                       $msg->language = $this->language;
-                       $msg->useDatabase = $this->useDatabase;
-                       $msg->title = $this->title;
-
-                       // DWIM
-                       if ( $format === 'block-parse' ) {
-                               $format = 'parse';
-                       }
-                       $msg->format = $format;
-
-                       // Message objects should not be before parameters because
-                       // then they'll get double escaped. If the message needs to be
-                       // escaped, it'll happen right here when we call toString().
-                       return [ 'after', $msg->toString( $format ) ];
-               } else {
-                       return [ 'before', $param ];
-               }
-       }
-
-       /**
-        * Wrapper for what ever method we use to parse wikitext.
-        *
-        * @since 1.17
-        *
-        * @param string $string Wikitext message contents.
-        *
-        * @return string Wikitext parsed into HTML.
-        */
-       protected function parseText( $string ) {
-               $out = MessageCache::singleton()->parse(
-                       $string,
-                       $this->title,
-                       /*linestart*/true,
-                       $this->interface,
-                       $this->getLanguage()
-               );
-
-               return $out instanceof ParserOutput
-                       ? $out->getText( [
-                               'enableSectionEditLinks' => false,
-                               // Wrapping messages in an extra <div> is probably not expected. If
-                               // they're outside the content area they probably shouldn't be
-                               // targeted by CSS that's targeting the parser output, and if
-                               // they're inside they already are from the outer div.
-                               'unwrap' => true,
-                       ] )
-                       : $out;
-       }
-
-       /**
-        * Wrapper for what ever method we use to {{-transform wikitext.
-        *
-        * @since 1.17
-        *
-        * @param string $string Wikitext message contents.
-        *
-        * @return string Wikitext with {{-constructs replaced with their values.
-        */
-       protected function transformText( $string ) {
-               return MessageCache::singleton()->transform(
-                       $string,
-                       $this->interface,
-                       $this->getLanguage(),
-                       $this->title
-               );
-       }
-
-       /**
-        * Wrapper for what ever method we use to get message contents.
-        *
-        * @since 1.17
-        *
-        * @return string
-        * @throws MWException If message key array is empty.
-        */
-       protected function fetchMessage() {
-               if ( $this->message === null ) {
-                       $cache = MessageCache::singleton();
-
-                       foreach ( $this->keysToTry as $key ) {
-                               $message = $cache->get( $key, $this->useDatabase, $this->getLanguage() );
-                               if ( $message !== false && $message !== '' ) {
-                                       break;
-                               }
-                       }
-
-                       // NOTE: The constructor makes sure keysToTry isn't empty,
-                       //       so we know that $key and $message are initialized.
-                       $this->key = $key;
-                       $this->message = $message;
-               }
-               return $this->message;
-       }
-
-       /**
-        * Formats a message parameter wrapped with 'plaintext'. Ensures that
-        * the entire string is displayed unchanged when displayed in the output
-        * format.
-        *
-        * @since 1.25
-        *
-        * @param string $plaintext String to ensure plaintext output of
-        * @param string $format One of the FORMAT_* constants.
-        *
-        * @return string Input plaintext encoded for output to $format
-        */
-       protected function formatPlaintext( $plaintext, $format ) {
-               switch ( $format ) {
-                       case self::FORMAT_TEXT:
-                       case self::FORMAT_PLAIN:
-                               return $plaintext;
-
-                       case self::FORMAT_PARSE:
-                       case self::FORMAT_BLOCK_PARSE:
-                       case self::FORMAT_ESCAPED:
-                       default:
-                               return htmlspecialchars( $plaintext, ENT_QUOTES );
-               }
-       }
-
-       /**
-        * Formats a list of parameters as a concatenated string.
-        * @since 1.29
-        * @param array $params
-        * @param string $listType
-        * @param string $format One of the FORMAT_* constants.
-        * @return array Array with the parameter type (either "before" or "after") and the value.
-        */
-       protected function formatListParam( array $params, $listType, $format ) {
-               if ( !isset( self::$listTypeMap[$listType] ) ) {
-                       $warning = 'Invalid list type for message "' . $this->getKey() . '": '
-                               . htmlspecialchars( $listType )
-                               . ' (params are ' . htmlspecialchars( serialize( $params ) ) . ')';
-                       trigger_error( $warning, E_USER_WARNING );
-                       $e = new Exception;
-                       wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
-                       return [ 'before', '[INVALID]' ];
-               }
-               $func = self::$listTypeMap[$listType];
-
-               // Handle an empty list sensibly
-               if ( !$params ) {
-                       return [ 'before', $this->getLanguage()->$func( [] ) ];
-               }
-
-               // First, determine what kinds of list items we have
-               $types = [];
-               $vars = [];
-               $list = [];
-               foreach ( $params as $n => $p ) {
-                       list( $type, $value ) = $this->extractParam( $p, $format );
-                       $types[$type] = true;
-                       $list[] = $value;
-                       $vars[] = '$' . ( $n + 1 );
-               }
-
-               // Easy case: all are 'before' or 'after', so just join the
-               // values and use the same type.
-               if ( count( $types ) === 1 ) {
-                       return [ key( $types ), $this->getLanguage()->$func( $list ) ];
-               }
-
-               // Hard case: We need to process each value per its type, then
-               // return the concatenated values as 'after'. We handle this by turning
-               // the list into a RawMessage and processing that as a parameter.
-               $vars = $this->getLanguage()->$func( $vars );
-               return $this->extractParam( new RawMessage( $vars, $params ), $format );
-       }
-}
index ba9e2d7..1d1a193 100644 (file)
@@ -123,10 +123,6 @@ class OutputHandler {
                }
                if ( !$foundVary ) {
                        header( 'Vary: Accept-Encoding' );
-                       global $wgUseKeyHeader;
-                       if ( $wgUseKeyHeader ) {
-                               header( 'Key: Accept-Encoding;match=gzip' );
-                       }
                }
                return $s;
        }
index 5227aa1..b8cbff1 100644 (file)
@@ -265,11 +265,12 @@ class OutputPage extends ContextSource {
        private $mFollowPolicy = 'follow';
 
        /**
-        * @var array Headers that cause the cache to vary.  Key is header name, value is an array of
-        * options for the Key header.
+        * @var array Headers that cause the cache to vary.  Key is header name,
+        * value should always be null.  (Value was an array of options for
+        * the `Key` header, which was deprecated in 1.32 and removed in 1.34.)
         */
        private $mVaryHeader = [
-               'Accept-Encoding' => [ 'match=gzip' ],
+               'Accept-Encoding' => null,
        ];
 
        /**
@@ -1723,34 +1724,13 @@ class OutputPage extends ContextSource {
        /**
         * Get the files used on this page
         *
-        * @return array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or ''))
+        * @return array [ dbKey => [ 'time' => MW timestamp or null, 'sha1' => sha1 or '' ] ]
         * @since 1.18
         */
        public function getFileSearchOptions() {
                return $this->mImageTimeKeys;
        }
 
-       /**
-        * Convert wikitext to HTML and add it to the buffer
-        * Default assumes that the current page title will be used.
-        *
-        * @param string $text
-        * @param bool $linestart Is this the start of a line?
-        * @param bool $interface Is this text in the user interface language?
-        * @throws MWException
-        * @deprecated since 1.32 due to untidy output; use
-        *    addWikiTextAsInterface() if $interface is default value or true,
-        *    or else addWikiTextAsContent() if $interface is false.
-        */
-       public function addWikiText( $text, $linestart = true, $interface = true ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $title = $this->getTitle();
-               if ( !$title ) {
-                       throw new MWException( 'Title is null' );
-               }
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, $interface );
-       }
-
        /**
         * Convert wikitext *in the user interface language* to HTML and
         * add it to the buffer. The result will not be
@@ -1776,7 +1756,7 @@ class OutputPage extends ContextSource {
                if ( !$title ) {
                        throw new MWException( 'Title is null' );
                }
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/true );
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
        }
 
        /**
@@ -1797,7 +1777,7 @@ class OutputPage extends ContextSource {
        ) {
                $this->addWikiTextTitleInternal(
                        $text, $this->getTitle(),
-                       /*linestart*/true, /*tidy*/true, /*interface*/true,
+                       /*linestart*/true, /*interface*/true,
                        $wrapperClass
                );
        }
@@ -1826,79 +1806,7 @@ class OutputPage extends ContextSource {
                if ( !$title ) {
                        throw new MWException( 'Title is null' );
                }
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
-       }
-
-       /**
-        * Add wikitext with a custom Title object
-        *
-        * @param string $text Wikitext
-        * @param Title $title
-        * @param bool $linestart Is this the start of a line?
-        * @deprecated since 1.32 due to untidy output; use
-        *   addWikiTextAsInterface()
-        */
-       public function addWikiTextWithTitle( $text, Title $title, $linestart = true ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, /*interface*/false );
-       }
-
-       /**
-        * Add wikitext *in content language* with a custom Title object.
-        * Output will be tidy.
-        *
-        * @param string $text Wikitext in content language
-        * @param Title $title
-        * @param bool $linestart Is this the start of a line?
-        * @deprecated since 1.32 to rename methods consistently; use
-        *   addWikiTextAsContent()
-        */
-       function addWikiTextTitleTidy( $text, Title $title, $linestart = true ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
-       }
-
-       /**
-        * Add wikitext *in content language*. Output will be tidy.
-        *
-        * @param string $text Wikitext in content language
-        * @param bool $linestart Is this the start of a line?
-        * @deprecated since 1.32 to rename methods consistently; use
-        *   addWikiTextAsContent()
-        */
-       public function addWikiTextTidy( $text, $linestart = true ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $title = $this->getTitle();
-               if ( !$title ) {
-                       throw new MWException( 'Title is null' );
-               }
-               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
-       }
-
-       /**
-        * Add wikitext with a custom Title object.
-        * Output is unwrapped.
-        *
-        * @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.
-        *             For backwards-compatibility with prior MW releases,
-        *             you may wish to invoke this method but set $tidy=true;
-        *             this will result in equivalent output to the non-deprecated
-        *             addWikiTextAsContent()/addWikiTextAsInterface() methods.
-        * @param bool $interface Whether it is an interface message
-        *   (for example disables conversion)
-        * @deprecated since 1.32, use addWikiTextAsContent() or
-        *   addWikiTextAsInterface() (depending on $interface)
-        */
-       public function addWikiTextTitle( $text, Title $title, $linestart,
-               $tidy = false, $interface = false
-       ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               return $this->addWikiTextTitleInternal( $text, $title, $linestart, $tidy, $interface );
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
        }
 
        /**
@@ -1918,14 +1826,10 @@ class OutputPage extends ContextSource {
         * @private
         */
        private function addWikiTextTitleInternal(
-               $text, Title $title, $linestart, $tidy, $interface, $wrapperClass = null
+               $text, Title $title, $linestart, $interface, $wrapperClass = null
        ) {
-               if ( !$tidy ) {
-                       wfDeprecated( 'disabling tidy', '1.32' );
-               }
-
                $parserOutput = $this->parseInternal(
-                       $text, $title, $linestart, $tidy, $interface, /*language*/null
+                       $text, $title, $linestart, true, $interface, /*language*/null
                );
 
                $this->addParserOutput( $parserOutput, [
@@ -2319,19 +2223,18 @@ class OutputPage extends ContextSource {
         * Add an HTTP header that will influence on the cache
         *
         * @param string $header Header name
-        * @param string[]|null $option Options for the Key header. See
-        * https://datatracker.ietf.org/doc/draft-fielding-http-key/
-        * for the list of valid options.
+        * @param string[]|null $option Deprecated; formerly options for the
+        *  Key header, deprecated in 1.32 and removed in 1.34. See
+        *   https://datatracker.ietf.org/doc/draft-fielding-http-key/
+        *   for the list of formerly-valid options.
         */
        public function addVaryHeader( $header, array $option = null ) {
-               if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
-                       $this->mVaryHeader[$header] = [];
+               if ( $option !== null && count( $option ) > 0 ) {
+                       wfDeprecated( 'addVaryHeader $option is ignored', '1.34' );
                }
-               if ( !is_array( $option ) ) {
-                       $option = [];
+               if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
+                       $this->mVaryHeader[$header] = null;
                }
-               $this->mVaryHeader[$header] =
-                       array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
        }
 
        /**
@@ -2374,41 +2277,6 @@ class OutputPage extends ContextSource {
                return 'Link: ' . implode( ',', $this->mLinkHeader );
        }
 
-       /**
-        * Get a complete Key header
-        *
-        * @return string
-        * @deprecated in 1.32; the IETF spec for this header expired w/o becoming
-        *   a standard.
-        */
-       public function getKeyHeader() {
-               wfDeprecated( '$wgUseKeyHeader', '1.32' );
-
-               $cvCookies = $this->getCacheVaryCookies();
-
-               $cookiesOption = [];
-               foreach ( $cvCookies as $cookieName ) {
-                       $cookiesOption[] = 'param=' . $cookieName;
-               }
-               $this->addVaryHeader( 'Cookie', $cookiesOption );
-
-               foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
-                       $this->addVaryHeader( $header, $options );
-               }
-
-               $headers = [];
-               foreach ( $this->mVaryHeader as $header => $option ) {
-                       $newheader = $header;
-                       if ( is_array( $option ) && count( $option ) > 0 ) {
-                               $newheader .= ';' . implode( ';', $option );
-                       }
-                       $headers[] = $newheader;
-               }
-               $key = 'Key: ' . implode( ',', $headers );
-
-               return $key;
-       }
-
        /**
         * T23672: Add Accept-Language to Vary and Key headers if there's no 'variant' parameter in GET.
         *
@@ -2424,33 +2292,7 @@ class OutputPage extends ContextSource {
 
                $lang = $title->getPageLanguage();
                if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
-                       $variants = $lang->getVariants();
-                       $aloption = [];
-                       foreach ( $variants as $variant ) {
-                               if ( $variant === $lang->getCode() ) {
-                                       continue;
-                               }
-
-                               // XXX Note that this code is not strictly correct: we
-                               // do a case-insensitive match in
-                               // LanguageConverter::getHeaderVariant() while the
-                               // (abandoned, draft) spec for the `Key` header only
-                               // allows case-sensitive matches.  To match the logic
-                               // in LanguageConverter::getHeaderVariant() we should
-                               // also be looking at fallback variants and deprecated
-                               // mediawiki-internal codes, as well as BCP 47
-                               // normalized forms.
-
-                               $aloption[] = "substr=$variant";
-
-                               // IE and some other browsers use BCP 47 standards in their Accept-Language header,
-                               // like "zh-CN" or "zh-Hant".  We should handle these too.
-                               $variantBCP47 = LanguageCode::bcp47( $variant );
-                               if ( $variantBCP47 !== $variant ) {
-                                       $aloption[] = "substr=$variantBCP47";
-                               }
-                       }
-                       $this->addVaryHeader( 'Accept-Language', $aloption );
+                       $this->addVaryHeader( 'Accept-Language' );
                }
        }
 
@@ -2561,10 +2403,6 @@ class OutputPage extends ContextSource {
                # maintain different caches for logged-in users and non-logged in ones
                $response->header( $this->getVaryHeader() );
 
-               if ( $config->get( 'UseKeyHeader' ) ) {
-                       $response->header( $this->getKeyHeader() );
-               }
-
                if ( $this->mEnableClientCache ) {
                        if (
                                $config->get( 'UseCdn' ) &&
@@ -2882,8 +2720,11 @@ class OutputPage extends ContextSource {
                                        $query['returntoquery'] = wfArrayToCgi( $returntoquery );
                                }
                        }
+
+                       $services = MediaWikiServices::getInstance();
+
                        $title = SpecialPage::getTitleFor( 'Userlogin' );
-                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+                       $linkRenderer = $services->getLinkRenderer();
                        $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
                        $loginLink = $linkRenderer->makeKnownLink(
                                $title,
@@ -2895,9 +2736,13 @@ class OutputPage extends ContextSource {
                        $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
                        $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
 
+                       $permissionManager = $services->getPermissionManager();
+
                        # Don't return to a page the user can't read otherwise
                        # we'll end up in a pointless loop
-                       if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
+                       if ( $displayReturnto && $permissionManager->userCan(
+                               'read', $this->getUser(), $displayReturnto
+                       ) ) {
                                $this->returnToMain( null, $displayReturnto );
                        }
                } else {
@@ -3215,7 +3060,7 @@ class OutputPage extends ContextSource {
                                ),
                                [ 'html5shiv' ],
                                ResourceLoaderModule::TYPE_SCRIPTS,
-                               [ 'sync' => true ],
+                               [ 'raw' => '1', 'sync' => '1' ],
                                $this->getCSPNonce()
                        ) .
                        '<![endif]-->';
@@ -4147,13 +3992,6 @@ class OutputPage extends ContextSource {
                        if ( is_array( $spec ) ) {
                                $args = $spec;
                                $name = array_shift( $args );
-                               if ( isset( $args['options'] ) ) {
-                                       unset( $args['options'] );
-                                       wfDeprecated(
-                                               'Adding "options" to ' . __METHOD__ . ' is no longer supported',
-                                               '1.20'
-                                       );
-                               }
                        } else {
                                $args = [];
                                $name = $spec;
@@ -4172,16 +4010,6 @@ class OutputPage extends ContextSource {
                return $this->mEnableTOC;
        }
 
-       /**
-        * Enables/disables section edit links, doesn't override __NOEDITSECTION__
-        * @param bool $flag
-        * @since 1.23
-        * @deprecated since 1.31, use $poOptions to addParserOutput() instead.
-        */
-       public function enableSectionEditLinks( $flag = true ) {
-               wfDeprecated( __METHOD__, '1.31' );
-       }
-
        /**
         * Helper function to setup the PHP implementation of OOUI to use in this request.
         *
index 2d9216d..b63a84d 100644 (file)
@@ -123,10 +123,7 @@ class PHPVersionCheck {
                $phpInfo = $this->getPHPInfo();
                $minimumVersion = $phpInfo['minSupported'];
                $otherInfo = $this->getPHPInfo( $phpInfo['implementation'] === 'HHVM' ? 'PHP' : 'HHVM' );
-               if (
-                       !function_exists( 'version_compare' )
-                       || version_compare( $phpInfo['version'], $minimumVersion ) < 0
-               ) {
+               if ( version_compare( $phpInfo['version'], $minimumVersion ) < 0 ) {
                        $shortText = "MediaWiki $this->mwVersion requires at least {$phpInfo['implementation']}"
                                . " version $minimumVersion or {$otherInfo['implementation']} version "
                                . "{$otherInfo['minSupported']}, you are using {$phpInfo['implementation']} "
index eb52d7c..2882e66 100644 (file)
@@ -53,8 +53,8 @@
  *   - In a pattern $1, $2, etc... will be replaced with the relevant contents
  *   - If you used a keyed array as a path pattern, $key will be replaced with
  *     the relevant contents
- *   - The default behavior is equivalent to `array( 'title' => '$1' )`,
- *     if you don't want the title parameter you can explicitly use `array( 'title' => false )`
+ *   - The default behavior is equivalent to `[ 'title' => '$1' ]`,
+ *     if you don't want the title parameter you can explicitly use `[ 'title' => false ]`
  *   - You can specify a value that won't have replacements in it
  *     using `'foo' => [ 'value' => 'bar' ];`
  *
@@ -80,7 +80,7 @@ class PathRouter {
        /**
         * Protected helper to do the actual bulk work of adding a single pattern.
         * This is in a separate method so that add() can handle the difference between
-        * a single string $path and an array() $path that contains multiple path
+        * a single string $path and an array $path that contains multiple path
         * patterns each with an associated $key to pass on.
         * @param string $path
         * @param array $params
@@ -247,9 +247,9 @@ class PathRouter {
                }
 
                // We know the difference between null (no matches) and
-               // array() (a match with no data) but our WebRequest caller
-               // expects array() even when we have no matches so return
-               // a array() when we have null
+               // [] (a match with no data) but our WebRequest caller
+               // expects [] even when we have no matches so return
+               // a [] when we have null
                return $matches ?? [];
        }
 
index e443803..202014f 100644 (file)
@@ -324,7 +324,7 @@ class PermissionManager {
         * Add the resulting error code to the errors array
         *
         * @param array $errors List of current errors
-        * @param array $result Result of errors
+        * @param array|string|MessageSpecifier|false $result Result of errors
         *
         * @return array List of errors
         */
diff --git a/includes/Rest/CopyableStreamInterface.php b/includes/Rest/CopyableStreamInterface.php
new file mode 100644 (file)
index 0000000..3e18e16
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * An interface for a stream with a copyToStream() function.
+ */
+interface CopyableStreamInterface extends \Psr\Http\Message\StreamInterface {
+       /**
+        * Copy this stream to a specified stream resource. For some streams,
+        * this can be implemented without a tight loop in PHP code.
+        *
+        * Equivalent to reading from the object until EOF and writing the
+        * resulting data to $stream. The position will be advanced to the end.
+        *
+        * Note that $stream is not a StreamInterface object.
+        *
+        * @param resource $stream Destination
+        */
+       function copyToStream( $stream );
+}
diff --git a/includes/Rest/EntryPoint.php b/includes/Rest/EntryPoint.php
new file mode 100644 (file)
index 0000000..795999a
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use ExtensionRegistry;
+use MediaWiki\MediaWikiServices;
+use RequestContext;
+use Title;
+use WebResponse;
+
+class EntryPoint {
+       /** @var RequestInterface */
+       private $request;
+       /** @var WebResponse */
+       private $webResponse;
+       /** @var Router */
+       private $router;
+
+       public static function main() {
+               // URL safety checks
+               global $wgRequest;
+               if ( !$wgRequest->checkUrlExtension() ) {
+                       return;
+               }
+
+               // Set $wgTitle and the title in RequestContext, as in api.php
+               global $wgTitle;
+               $wgTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/rest.php' );
+               RequestContext::getMain()->setTitle( $wgTitle );
+
+               $services = MediaWikiServices::getInstance();
+               $conf = $services->getMainConfig();
+
+               $request = new RequestFromGlobals( [
+                       'cookiePrefix' => $conf->get( 'CookiePrefix' )
+               ] );
+
+               global $IP;
+               $router = new Router(
+                       [ "$IP/includes/Rest/coreRoutes.json" ],
+                       ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
+                       $conf->get( 'RestPath' ),
+                       $services->getLocalServerObjectCache(),
+                       new ResponseFactory
+               );
+
+               $entryPoint = new self(
+                       $request,
+                       $wgRequest->response(),
+                       $router );
+               $entryPoint->execute();
+       }
+
+       public function __construct( RequestInterface $request, WebResponse $webResponse,
+               Router $router
+       ) {
+               $this->request = $request;
+               $this->webResponse = $webResponse;
+               $this->router = $router;
+       }
+
+       public function execute() {
+               $response = $this->router->execute( $this->request );
+
+               $this->webResponse->header(
+                       'HTTP/' . $response->getProtocolVersion() . ' ' .
+                       $response->getStatusCode() . ' ' .
+                       $response->getReasonPhrase() );
+
+               foreach ( $response->getRawHeaderLines() as $line ) {
+                       $this->webResponse->header( $line );
+               }
+
+               foreach ( $response->getCookies() as $cookie ) {
+                       $this->webResponse->setCookie(
+                               $cookie['name'],
+                               $cookie['value'],
+                               $cookie['expiry'],
+                               $cookie['options'] );
+               }
+
+               $stream = $response->getBody();
+               $stream->rewind();
+               if ( $stream instanceof CopyableStreamInterface ) {
+                       $stream->copyToStream( fopen( 'php://output', 'w' ) );
+               } else {
+                       while ( true ) {
+                               $buffer = $stream->read( 65536 );
+                               if ( $buffer === '' ) {
+                                       break;
+                               }
+                               echo $buffer;
+                       }
+               }
+       }
+}
diff --git a/includes/Rest/Handler.php b/includes/Rest/Handler.php
new file mode 100644 (file)
index 0000000..472e1cc
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+abstract class Handler {
+       /** @var RequestInterface */
+       private $request;
+
+       /** @var array */
+       private $config;
+
+       /** @var ResponseFactory */
+       private $responseFactory;
+
+       /**
+        * Initialise with dependencies from the Router. This is called after construction.
+        */
+       public function init( RequestInterface $request, array $config,
+               ResponseFactory $responseFactory
+       ) {
+               $this->request = $request;
+               $this->config = $config;
+               $this->responseFactory = $responseFactory;
+       }
+
+       /**
+        * Get the current request. The return type declaration causes it to raise
+        * a fatal error if init() has not yet been called.
+        *
+        * @return RequestInterface
+        */
+       public function getRequest(): RequestInterface {
+               return $this->request;
+       }
+
+       /**
+        * Get the configuration array for the current route. The return type
+        * declaration causes it to raise a fatal error if init() has not
+        * been called.
+        *
+        * @return array
+        */
+       public function getConfig(): array {
+               return $this->config;
+       }
+
+       /**
+        * Get the ResponseFactory which can be used to generate Response objects.
+        * This will raise a fatal error if init() has not been
+        * called.
+        *
+        * @return ResponseFactory
+        */
+       public function getResponseFactory(): ResponseFactory {
+               return $this->responseFactory;
+       }
+
+       /**
+        * The subclass should override this to provide the maximum last modified
+        * timestamp for the current request. This is called before execute() in
+        * order to decide whether to send a 304.
+        *
+        * The timestamp can be in any format accepted by ConvertibleTimestamp, or
+        * null to indicate that the timestamp is unknown.
+        *
+        * @return bool|string|int|float|\DateTime|null
+        */
+       protected function getLastModified() {
+               return null;
+       }
+
+       /**
+        * The subclass should override this to provide an ETag for the current
+        * request. This is called before execute() in order to decide whether to
+        * send a 304.
+        *
+        * See RFC 7232 § 2.3 for semantics.
+        *
+        * @return string|null
+        */
+       protected function getETag() {
+               return null;
+       }
+
+       /**
+        * Execute the handler. This is called after parameter validation. The
+        * return value can either be a Response or any type accepted by
+        * ResponseFactory::createFromReturnValue().
+        *
+        * To automatically construct an error response, execute() should throw a
+        * RestException. Such exceptions will not be logged like a normal exception.
+        *
+        * If execute() throws any other kind of exception, the exception will be
+        * logged and a generic 500 error page will be shown.
+        *
+        * @return mixed
+        */
+       abstract public function execute();
+}
diff --git a/includes/Rest/Handler/HelloHandler.php b/includes/Rest/Handler/HelloHandler.php
new file mode 100644 (file)
index 0000000..6e119dd
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+namespace MediaWiki\Rest\Handler;
+
+use MediaWiki\Rest\SimpleHandler;
+
+/**
+ * Example handler
+ * @unstable
+ */
+class HelloHandler extends SimpleHandler {
+       public function run( $name ) {
+               return [ 'message' => "Hello, $name!" ];
+       }
+}
diff --git a/includes/Rest/HeaderContainer.php b/includes/Rest/HeaderContainer.php
new file mode 100644 (file)
index 0000000..a71f6a6
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * This is a container for storing headers. The header names are case-insensitive,
+ * but the case is preserved for methods that return headers in bulk. The
+ * header values are a comma-separated list, or equivalently, an array of strings.
+ *
+ * Unlike PSR-7, the container is mutable.
+ */
+class HeaderContainer {
+       private $headerLists = [];
+       private $headerLines = [];
+       private $headerNames = [];
+
+       /**
+        * Erase any existing headers and replace them with the specified
+        * header arrays or values.
+        *
+        * @param array $headers
+        */
+       public function resetHeaders( $headers = [] ) {
+               $this->headerLines = [];
+               $this->headerLists = [];
+               $this->headerNames = [];
+               foreach ( $headers as $name => $value ) {
+                       $this->headerNames[ strtolower( $name ) ] = $name;
+                       list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
+                       $this->headerLines[$name] = $valueLine;
+                       $this->headerLists[$name] = $valueParts;
+               }
+       }
+
+       /**
+        * Take an input header value, which may either be a string or an array,
+        * and convert it to an array of header values and a header line.
+        *
+        * The return value is an array where element 0 has the array of header
+        * values, and element 1 has the header line.
+        *
+        * Theoretically, if the input is a string, this could parse the string
+        * and split it on commas. Doing this is complicated, because some headers
+        * can contain double-quoted strings containing commas. The User-Agent
+        * header allows commas in comments delimited by parentheses. So it is not
+        * just explode(",", $value), we would need to parse a grammar defined by
+        * RFC 7231 appendix D which depends on header name.
+        *
+        * It's unclear how much it would help handlers to have fully spec-aware
+        * HTTP header handling just to split on commas. They would probably be
+        * 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
+        */
+       private function convertToListAndString( $value ) {
+               if ( is_array( $value ) ) {
+                       return [ array_values( $value ), implode( ', ', $value ) ];
+               } else {
+                       return [ [ $value ], $value ];
+               }
+       }
+
+       /**
+        * Set or replace a header
+        *
+        * @param string $name
+        * @param string|string[] $value
+        */
+       public function setHeader( $name, $value ) {
+               list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
+               $lowerName = strtolower( $name );
+               $origName = $this->headerNames[$lowerName] ?? null;
+               if ( $origName !== null ) {
+                       unset( $this->headerLines[$origName] );
+                       unset( $this->headerLists[$origName] );
+               }
+               $this->headerNames[$lowerName] = $name;
+               $this->headerLines[$name] = $valueLine;
+               $this->headerLists[$name] = $valueParts;
+       }
+
+       /**
+        * Set a header or append to an existing header
+        *
+        * @param string $name
+        * @param string|string[] $value
+        */
+       public function addHeader( $name, $value ) {
+               list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
+               $lowerName = strtolower( $name );
+               $origName = $this->headerNames[$lowerName] ?? null;
+               if ( $origName === null ) {
+                       $origName = $name;
+                       $this->headerNames[$lowerName] = $origName;
+                       $this->headerLines[$origName] = $valueLine;
+                       $this->headerLists[$origName] = $valueParts;
+               } else {
+                       $this->headerLines[$origName] .= ', ' . $valueLine;
+                       $this->headerLists[$origName] = array_merge( $this->headerLists[$origName],
+                               $valueParts );
+               }
+       }
+
+       /**
+        * Remove a header
+        *
+        * @param string $name
+        */
+       public function removeHeader( $name ) {
+               $lowerName = strtolower( $name );
+               $origName = $this->headerNames[$lowerName] ?? null;
+               if ( $origName !== null ) {
+                       unset( $this->headerNames[$lowerName] );
+                       unset( $this->headerLines[$origName] );
+                       unset( $this->headerLists[$origName] );
+               }
+       }
+
+       /**
+        * Get header arrays indexed by original name
+        *
+        * @return string[][]
+        */
+       public function getHeaders() {
+               return $this->headerLists;
+       }
+
+       /**
+        * Get the header with a particular name, or an empty array if there is no
+        * such header.
+        *
+        * @param string $name
+        * @return string[]
+        */
+       public function getHeader( $name ) {
+               $headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
+               if ( $headerName === null ) {
+                       return [];
+               }
+               return $this->headerLists[$headerName];
+       }
+
+       /**
+        * Return true if the header exists, false otherwise
+        * @param string $name
+        * @return bool
+        */
+       public function hasHeader( $name ) {
+               return isset( $this->headerNames[ strtolower( $name ) ] );
+       }
+
+       /**
+        * Get the specified header concatenated into a comma-separated string.
+        * If the header does not exist, an empty string is returned.
+        *
+        * @param string $name
+        * @return string
+        */
+       public function getHeaderLine( $name ) {
+               $headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
+               if ( $headerName === null ) {
+                       return '';
+               }
+               return $this->headerLines[$headerName];
+       }
+
+       /**
+        * Get all header lines
+        *
+        * @return string[]
+        */
+       public function getHeaderLines() {
+               return $this->headerLines;
+       }
+
+       /**
+        * Get an array of strings of the form "Name: Value", suitable for passing
+        * directly to header() to set response headers. The PHP manual describes
+        * these strings as "raw HTTP headers", so we adopt that terminology.
+        *
+        * @return string[] Header list (integer indexed)
+        */
+       public function getRawHeaderLines() {
+               $lines = [];
+               foreach ( $this->headerNames as $lowerName => $name ) {
+                       if ( $lowerName === 'set-cookie' ) {
+                               // As noted by RFC 7230 section 3.2.2, Set-Cookie is the only
+                               // header for which multiple values cannot be concatenated into
+                               // a single comma-separated line.
+                               foreach ( $this->headerLists[$name] as $value ) {
+                                       $lines[] = "$name: $value";
+                               }
+                       } else {
+                               $lines[] = "$name: " . $this->headerLines[$name];
+                       }
+               }
+               return $lines;
+       }
+}
diff --git a/includes/Rest/HttpException.php b/includes/Rest/HttpException.php
new file mode 100644 (file)
index 0000000..ae6dde2
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * This is the base exception class for non-fatal exceptions thrown from REST
+ * handlers. The exception is not logged, it is merely converted to an
+ * error response.
+ */
+class HttpException extends \Exception {
+       public function __construct( $message, $code = 500 ) {
+               parent::__construct( $message, $code );
+       }
+}
diff --git a/includes/Rest/JsonEncodingException.php b/includes/Rest/JsonEncodingException.php
new file mode 100644 (file)
index 0000000..e731ac3
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+class JsonEncodingException extends \RuntimeException {
+       public function __construct( $message, $code ) {
+               parent::__construct( "JSON encoding error: $message", $code );
+       }
+}
diff --git a/includes/Rest/PathTemplateMatcher/PathConflict.php b/includes/Rest/PathTemplateMatcher/PathConflict.php
new file mode 100644 (file)
index 0000000..dd9f34a
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace MediaWiki\Rest\PathTemplateMatcher;
+
+use Exception;
+
+class PathConflict extends Exception {
+       public $newTemplate;
+       public $newUserData;
+       public $existingTemplate;
+       public $existingUserData;
+
+       public function __construct( $template, $userData, $existingNode ) {
+               $this->newTemplate = $template;
+               $this->newUserData = $userData;
+               $this->existingTemplate = $existingNode['template'];
+               $this->existingUserData = $existingNode['userData'];
+               parent::__construct( "Unable to add path template \"$template\" since it conflicts " .
+                       "with the existing template \"{$this->existingTemplate}\"" );
+       }
+}
diff --git a/includes/Rest/PathTemplateMatcher/PathMatcher.php b/includes/Rest/PathTemplateMatcher/PathMatcher.php
new file mode 100644 (file)
index 0000000..69987e0
--- /dev/null
@@ -0,0 +1,221 @@
+<?php
+
+namespace MediaWiki\Rest\PathTemplateMatcher;
+
+/**
+ * A tree-based path routing algorithm.
+ *
+ * This container builds defined routing templates into a tree, allowing
+ * paths to be efficiently matched against all templates. The match time is
+ * independent of the number of registered path templates.
+ *
+ * Efficient matching comes at the cost of a potentially significant setup time.
+ * We measured ~10ms for 1000 templates. Using getCacheData() and
+ * newFromCache(), this setup time may be amortized over multiple requests.
+ */
+class PathMatcher {
+       /**
+        * An array of trees indexed by the number of path components in the input.
+        *
+        * A tree node consists of an associative array in which the key is a match
+        * specifier string, and the value is another node. A leaf node, which is
+        * identifiable by its fixed depth in the tree, consists of an associative
+        * array with the following keys:
+        *   - template: The path template string
+        *   - paramNames: A list of parameter names extracted from the template
+        *   - userData: The user data supplied to add()
+        *
+        * A match specifier string may be either "*", which matches any path
+        * component, or a literal string prefixed with "=", which matches the
+        * specified deprefixed string literal.
+        *
+        * @var array
+        */
+       private $treesByLength = [];
+
+       /**
+        * Create a PathMatcher from cache data
+        *
+        * @param array $data The data array previously returned by getCacheData()
+        * @return PathMatcher
+        */
+       public static function newFromCache( $data ) {
+               $matcher = new self;
+               $matcher->treesByLength = $data;
+               return $matcher;
+       }
+
+       /**
+        * Get a data array for later use by newFromCache().
+        *
+        * The internal format is private to PathMatcher, but note that it includes
+        * any data passed as $userData to add(). The array returned will be
+        * serializable as long as all $userData values are serializable.
+        *
+        * @return array
+        */
+       public function getCacheData() {
+               return $this->treesByLength;
+       }
+
+       /**
+        * Determine whether a path template component is a parameter
+        *
+        * @param string $part
+        * @return bool
+        */
+       private function isParam( $part ) {
+               $partLength = strlen( $part );
+               return $partLength > 2 && $part[0] === '{' && $part[$partLength - 1] === '}';
+       }
+
+       /**
+        * If a path template component is a parameter, return the parameter name.
+        * Otherwise, return false.
+        *
+        * @param string $part
+        * @return string|false
+        */
+       private function getParamName( $part ) {
+               if ( $this->isParam( $part ) ) {
+                       return substr( $part, 1, -1 );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Recursively search the match tree, checking whether the proposed path
+        * template, passed as an array of component parts, can be added to the
+        * matcher without ambiguity.
+        *
+        * Ambiguity means that a path exists which matches multiple templates.
+        *
+        * The function calls itself recursively, incrementing $index so as to
+        * ignore a prefix of the input, in order to check deeper parts of the
+        * match tree.
+        *
+        * If a conflict is discovered, the conflicting leaf node is returned.
+        * Otherwise, false is returned.
+        *
+        * @param array $node The tree node to check against
+        * @param string[] $parts The array of path template parts
+        * @param int $index The current index into $parts
+        * @return array|false
+        */
+       private function findConflict( $node, $parts, $index = 0 ) {
+               if ( $index >= count( $parts ) ) {
+                       // If we reached the leaf node then a conflict is detected
+                       return $node;
+               }
+               $part = $parts[$index];
+               $result = false;
+               if ( $this->isParam( $part ) ) {
+                       foreach ( $node as $key => $childNode ) {
+                               $result = $this->findConflict( $childNode, $parts, $index + 1 );
+                               if ( $result !== false ) {
+                                       break;
+                               }
+                       }
+               } else {
+                       if ( isset( $node["=$part"] ) ) {
+                               $result = $this->findConflict( $node["=$part"], $parts, $index + 1 );
+                       }
+                       if ( $result === false && isset( $node['*'] ) ) {
+                               $result = $this->findConflict( $node['*'], $parts, $index + 1 );
+                       }
+               }
+               return $result;
+       }
+
+       /**
+        * Add a template to the matcher.
+        *
+        * The path template consists of components separated by "/". Each component
+        * may be either a parameter of the form {paramName}, or a literal string.
+        * A parameter matches any input path component, whereas a literal string
+        * matches itself.
+        *
+        * Path templates must not conflict with each other, that is, any input
+        * path must match at most one path template. If a path template conflicts
+        * with another already registered, this function throws a PathConflict
+        * exception.
+        *
+        * @param string $template The path template
+        * @param mixed $userData User data used to identify the matched route to
+        *   the caller of match()
+        * @throws PathConflict
+        */
+       public function add( $template, $userData ) {
+               $parts = explode( '/', $template );
+               $length = count( $parts );
+               if ( !isset( $this->treesByLength[$length] ) ) {
+                       $this->treesByLength[$length] = [];
+               }
+               $tree =& $this->treesByLength[$length];
+               $conflict = $this->findConflict( $tree, $parts );
+               if ( $conflict !== false ) {
+                       throw new PathConflict( $template, $userData, $conflict );
+               }
+
+               $params = [];
+               foreach ( $parts as $index => $part ) {
+                       $paramName = $this->getParamName( $part );
+                       if ( $paramName !== false ) {
+                               $params[] = $paramName;
+                               $key = '*';
+                       } else {
+                               $key = "=$part";
+                       }
+                       if ( $index === $length - 1 ) {
+                               $tree[$key] = [
+                                       'template' => $template,
+                                       'paramNames' => $params,
+                                       'userData' => $userData
+                               ];
+                       } elseif ( !isset( $tree[$key] ) ) {
+                               $tree[$key] = [];
+                       }
+                       $tree =& $tree[$key];
+               }
+       }
+
+       /**
+        * Match a path against the current match trees.
+        *
+        * If the path matches a previously added path template, an array will be
+        * returned with the following keys:
+        *   - params: An array mapping parameter names to their detected values
+        *   - userData: The user data passed to add(), which identifies the route
+        *
+        * If the path does not match any template, false is returned.
+        *
+        * @param string $path
+        * @return array|false
+        */
+       public function match( $path ) {
+               $parts = explode( '/', $path );
+               $length = count( $parts );
+               if ( !isset( $this->treesByLength[$length] ) ) {
+                       return false;
+               }
+               $node = $this->treesByLength[$length];
+
+               $paramValues = [];
+               foreach ( $parts as $part ) {
+                       if ( isset( $node["=$part"] ) ) {
+                               $node = $node["=$part"];
+                       } elseif ( isset( $node['*'] ) ) {
+                               $node = $node['*'];
+                               $paramValues[] = $part;
+                       } else {
+                               return false;
+                       }
+               }
+
+               return [
+                       'params' => array_combine( $node['paramNames'], $paramValues ),
+                       'userData' => $node['userData']
+               ];
+       }
+}
diff --git a/includes/Rest/RequestBase.php b/includes/Rest/RequestBase.php
new file mode 100644 (file)
index 0000000..4bed899
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * Shared code between RequestData and RequestFromGlobals
+ */
+abstract class RequestBase implements RequestInterface {
+       /**
+        * @var HeaderContainer|null
+        */
+       private $headerCollection;
+
+       /** @var array */
+       private $pathParams = [];
+
+       /** @var string */
+       private $cookiePrefix;
+
+       /**
+        * @internal
+        * @param string $cookiePrefix
+        */
+       protected function __construct( $cookiePrefix ) {
+               $this->cookiePrefix = $cookiePrefix;
+       }
+
+       /**
+        * Override this in the implementation class if lazy initialisation of
+        * header values is desired. It should call setHeaders().
+        *
+        * @internal
+        */
+       protected function initHeaders() {
+       }
+
+       public function __clone() {
+               if ( $this->headerCollection !== null ) {
+                       $this->headerCollection = clone $this->headerCollection;
+               }
+       }
+
+       /**
+        * Erase any existing headers and replace them with the specified header
+        * lines.
+        *
+        * Call this either from the constructor or from initHeaders() of the
+        * implementing class.
+        *
+        * @internal
+        * @param string[] $headers The header lines
+        */
+       protected function setHeaders( $headers ) {
+               $this->headerCollection = new HeaderContainer;
+               $this->headerCollection->resetHeaders( $headers );
+       }
+
+       public function getHeaders() {
+               if ( $this->headerCollection === null ) {
+                       $this->initHeaders();
+               }
+               return $this->headerCollection->getHeaders();
+       }
+
+       public function getHeader( $name ) {
+               if ( $this->headerCollection === null ) {
+                       $this->initHeaders();
+               }
+               return $this->headerCollection->getHeader( $name );
+       }
+
+       public function hasHeader( $name ) {
+               if ( $this->headerCollection === null ) {
+                       $this->initHeaders();
+               }
+               return $this->headerCollection->hasHeader( $name );
+       }
+
+       public function getHeaderLine( $name ) {
+               if ( $this->headerCollection === null ) {
+                       $this->initHeaders();
+               }
+               return $this->headerCollection->getHeaderLine( $name );
+       }
+
+       public function setPathParams( $params ) {
+               $this->pathParams = $params;
+       }
+
+       public function getPathParams() {
+               return $this->pathParams;
+       }
+
+       public function getPathParam( $name ) {
+               return $this->pathParams[$name] ?? null;
+       }
+
+       public function getCookiePrefix() {
+               return $this->cookiePrefix;
+       }
+
+       public function getCookie( $name, $default = null ) {
+               $cookies = $this->getCookieParams();
+               $prefixedName = $this->getCookiePrefix() . $name;
+               if ( array_key_exists( $prefixedName, $cookies ) ) {
+                       return $cookies[$prefixedName];
+               } else {
+                       return $default;
+               }
+       }
+}
diff --git a/includes/Rest/RequestData.php b/includes/Rest/RequestData.php
new file mode 100644 (file)
index 0000000..997350c
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use GuzzleHttp\Psr7\Uri;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use Psr\Http\Message\UriInterface;
+
+/**
+ * This is a Request class that allows data to be injected, for the purposes
+ * of testing or internal requests.
+ */
+class RequestData extends RequestBase {
+       private $method;
+
+       /** @var UriInterface */
+       private $uri;
+
+       private $protocolVersion;
+
+       /** @var StreamInterface */
+       private $body;
+
+       private $serverParams;
+
+       private $cookieParams;
+
+       private $queryParams;
+
+       /** @var UploadedFileInterface[] */
+       private $uploadedFiles;
+
+       private $postParams;
+
+       /**
+        * Construct a RequestData from an array of parameters.
+        *
+        * @param array $params An associative array of parameters. All parameters
+        *   have defaults. Parameters are:
+        *     - method: The HTTP method
+        *     - uri: The URI
+        *     - protocolVersion: The HTTP protocol version number
+        *     - bodyContents: A string giving the request body
+        *     - serverParams: Equivalent to $_SERVER
+        *     - cookieParams: Equivalent to $_COOKIE
+        *     - queryParams: Equivalent to $_GET
+        *     - uploadedFiles: An array of objects implementing UploadedFileInterface
+        *     - postParams: Equivalent to $_POST
+        *     - pathParams: The path template parameters
+        *     - headers: An array with the the key being the header name
+        *     - cookiePrefix: A prefix to add to cookie names in getCookie()
+        */
+       public function __construct( $params = [] ) {
+               $this->method = $params['method'] ?? 'GET';
+               $this->uri = $params['uri'] ?? new Uri;
+               $this->protocolVersion = $params['protocolVersion'] ?? '1.1';
+               $this->body = new StringStream( $params['bodyContents'] ?? '' );
+               $this->serverParams = $params['serverParams'] ?? [];
+               $this->cookieParams = $params['cookieParams'] ?? [];
+               $this->queryParams = $params['queryParams'] ?? [];
+               $this->uploadedFiles = $params['uploadedFiles'] ?? [];
+               $this->postParams = $params['postParams'] ?? [];
+               $this->setPathParams( $params['pathParams'] ?? [] );
+               $this->setHeaders( $params['headers'] ?? [] );
+               parent::__construct( $params['cookiePrefix'] ?? '' );
+       }
+
+       public function getMethod() {
+               return $this->method;
+       }
+
+       public function getUri() {
+               return $this->uri;
+       }
+
+       public function getProtocolVersion() {
+               return $this->protocolVersion;
+       }
+
+       public function getBody() {
+               return $this->body;
+       }
+
+       public function getServerParams() {
+               return $this->serverParams;
+       }
+
+       public function getCookieParams() {
+               return $this->cookieParams;
+       }
+
+       public function getQueryParams() {
+               return $this->queryParams;
+       }
+
+       public function getUploadedFiles() {
+               return $this->uploadedFiles;
+       }
+
+       public function getPostParams() {
+               return $this->postParams;
+       }
+}
diff --git a/includes/Rest/RequestFromGlobals.php b/includes/Rest/RequestFromGlobals.php
new file mode 100644 (file)
index 0000000..c73427b
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use GuzzleHttp\Psr7\LazyOpenStream;
+use GuzzleHttp\Psr7\ServerRequest;
+use GuzzleHttp\Psr7\Uri;
+
+// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
+
+/**
+ * This is a request class that gets data directly from the superglobals and
+ * other global PHP state, notably php://input.
+ */
+class RequestFromGlobals extends RequestBase {
+       private $uri;
+       private $protocol;
+       private $uploadedFiles;
+
+       /**
+        * @param array $params Associative array of parameters:
+        *   - cookiePrefix: The prefix for cookie names used by getCookie()
+        */
+       public function __construct( $params = [] ) {
+               parent::__construct( $params['cookiePrefix'] ?? '' );
+       }
+
+       // RequestInterface
+
+       public function getMethod() {
+               return $_SERVER['REQUEST_METHOD'] ?? 'GET';
+       }
+
+       public function getUri() {
+               if ( $this->uri === null ) {
+                       $this->uri = new Uri( \WebRequest::getGlobalRequestURL() );
+               }
+               return $this->uri;
+       }
+
+       // MessageInterface
+
+       public function getProtocolVersion() {
+               if ( $this->protocol === null ) {
+                       $serverProtocol = $_SERVER['SERVER_PROTOCOL'] ?? '';
+                       $prefixLength = strlen( 'HTTP/' );
+                       if ( strncmp( $serverProtocol, 'HTTP/', $prefixLength ) === 0 ) {
+                               $this->protocol = substr( $serverProtocol, $prefixLength );
+                       } else {
+                               $this->protocol = '1.1';
+                       }
+               }
+               return $this->protocol;
+       }
+
+       protected function initHeaders() {
+               if ( function_exists( 'apache_request_headers' ) ) {
+                       $this->setHeaders( apache_request_headers() );
+               } else {
+                       $headers = [];
+                       foreach ( $_SERVER as $name => $value ) {
+                               if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
+                                       $name = strtolower( str_replace( '_', '-', substr( $name, 5 ) ) );
+                                       $headers[$name] = $value;
+                               } elseif ( $name === 'CONTENT_LENGTH' ) {
+                                       $headers['content-length'] = $value;
+                               }
+                       }
+                       $this->setHeaders( $headers );
+               }
+       }
+
+       public function getBody() {
+               return new LazyOpenStream( 'php://input', 'r' );
+       }
+
+       // ServerRequestInterface
+
+       public function getServerParams() {
+               return $_SERVER;
+       }
+
+       public function getCookieParams() {
+               return $_COOKIE;
+       }
+
+       public function getQueryParams() {
+               return $_GET;
+       }
+
+       public function getUploadedFiles() {
+               if ( $this->uploadedFiles === null ) {
+                       $this->uploadedFiles = ServerRequest::normalizeFiles( $_FILES );
+               }
+               return $this->uploadedFiles;
+       }
+
+       public function getPostParams() {
+               return $_POST;
+       }
+}
diff --git a/includes/Rest/RequestInterface.php b/includes/Rest/RequestInterface.php
new file mode 100644 (file)
index 0000000..eba389a
--- /dev/null
@@ -0,0 +1,265 @@
+<?php
+
+/**
+ * Copyright (c) 2019 Wikimedia Foundation.
+ *
+ * This file is partly derived from PSR-7, which requires the following copyright notice:
+ *
+ * Copyright (c) 2014 PHP Framework Interoperability Group
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Rest;
+
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
+
+/**
+ * A request interface similar to PSR-7's ServerRequestInterface
+ */
+interface RequestInterface {
+       // RequestInterface
+
+       /**
+        * Retrieves the HTTP method of the request.
+        *
+        * @return string Returns the request method.
+        */
+       function getMethod();
+
+       /**
+        * Retrieves the URI instance.
+        *
+        * This method MUST return a UriInterface instance.
+        *
+        * @link http://tools.ietf.org/html/rfc3986#section-4.3
+        * @return UriInterface Returns a UriInterface instance
+        *     representing the URI of the request.
+        */
+       function getUri();
+
+       // MessageInterface
+
+       /**
+        * Retrieves the HTTP protocol version as a string.
+        *
+        * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+        *
+        * @return string HTTP protocol version.
+        */
+       function getProtocolVersion();
+
+       /**
+        * Retrieves all message header values.
+        *
+        * The keys represent the header name as it will be sent over the wire, and
+        * each value is an array of strings associated with the header.
+        *
+        *     // Represent the headers as a string
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         echo $name . ": " . implode(", ", $values);
+        *     }
+        *
+        *     // Emit headers iteratively:
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         foreach ($values as $value) {
+        *             header(sprintf('%s: %s', $name, $value), false);
+        *         }
+        *     }
+        *
+        * While header names are not case-sensitive, getHeaders() will preserve the
+        * exact case in which headers were originally specified.
+        *
+        * A single header value may be a string containing a comma-separated list.
+        * Lists will not necessarily be split into arrays. See the comment on
+        * HeaderContainer::convertToListAndString().
+        *
+        * @return string[][] Returns an associative array of the message's headers. Each
+        *     key MUST be a header name, and each value MUST be an array of strings
+        *     for that header.
+        */
+       function getHeaders();
+
+       /**
+        * Retrieves a message header value by the given case-insensitive name.
+        *
+        * This method returns an array of all the header values of the given
+        * case-insensitive header name.
+        *
+        * If the header does not appear in the message, this method MUST return an
+        * empty array.
+        *
+        * A single header value may be a string containing a comma-separated list.
+        * Lists will not necessarily be split into arrays. See the comment on
+        * HeaderContainer::convertToListAndString().
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string[] An array of string values as provided for the given
+        *    header. If the header does not appear in the message, this method MUST
+        *    return an empty array.
+        */
+       function getHeader( $name );
+
+       /**
+        * Checks if a header exists by the given case-insensitive name.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return bool Returns true if any header names match the given header
+        *     name using a case-insensitive string comparison. Returns false if
+        *     no matching header name is found in the message.
+        */
+       function hasHeader( $name );
+
+       /**
+        * Retrieves a comma-separated string of the values for a single header.
+        *
+        * This method returns all of the header values of the given
+        * case-insensitive header name as a string concatenated together using
+        * a comma.
+        *
+        * NOTE: Not all header values may be appropriately represented using
+        * comma concatenation. For such headers, use getHeader() instead
+        * and supply your own delimiter when concatenating.
+        *
+        * If the header does not appear in the message, this method MUST return
+        * an empty string.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string A string of values as provided for the given header
+        *    concatenated together using a comma. If the header does not appear in
+        *    the message, this method MUST return an empty string.
+        */
+       function getHeaderLine( $name );
+
+       /**
+        * Gets the body of the message.
+        *
+        * @return StreamInterface Returns the body as a stream.
+        */
+       function getBody();
+
+       // ServerRequestInterface
+
+       /**
+        * Retrieve server parameters.
+        *
+        * Retrieves data related to the incoming request environment,
+        * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+        * REQUIRED to originate from $_SERVER.
+        *
+        * @return array
+        */
+       function getServerParams();
+
+       /**
+        * Retrieve cookies.
+        *
+        * Retrieves cookies sent by the client to the server.
+        *
+        * The data MUST be compatible with the structure of the $_COOKIE
+        * superglobal.
+        *
+        * @return array
+        */
+       function getCookieParams();
+
+       /**
+        * Retrieve query string arguments.
+        *
+        * Retrieves the deserialized query string arguments, if any.
+        *
+        * Note: the query params might not be in sync with the URI or server
+        * params. If you need to ensure you are only getting the original
+        * values, you may need to parse the query string from `getUri()->getQuery()`
+        * or from the `QUERY_STRING` server param.
+        *
+        * @return array
+        */
+       function getQueryParams();
+
+       /**
+        * Retrieve normalized file upload data.
+        *
+        * This method returns upload metadata in a normalized tree, with each leaf
+        * an instance of Psr\Http\Message\UploadedFileInterface.
+        *
+        * @return array An array tree of UploadedFileInterface instances; an empty
+        *     array MUST be returned if no data is present.
+        */
+       function getUploadedFiles();
+
+       // MediaWiki extensions to PSR-7
+
+       /**
+        * Get the parameters derived from the path template match
+        *
+        * @return string[]
+        */
+       function getPathParams();
+
+       /**
+        * Retrieve a single path parameter.
+        *
+        * Retrieves a single path parameter as described in getPathParams(). If
+        * the attribute has not been previously set, returns null.
+        *
+        * @see getPathParams()
+        * @param string $name The parameter name.
+        * @return string|null
+        */
+       function getPathParam( $name );
+
+       /**
+        * Erase all path parameters from the object and set the parameter array
+        * to the one specified.
+        *
+        * @param string[] $params
+        */
+       function setPathParams( $params );
+
+       /**
+        * Get the current cookie prefix
+        *
+        * @return string
+        */
+       function getCookiePrefix();
+
+       /**
+        * Add the cookie prefix to a specified cookie name and get the value of
+        * the resulting prefixed cookie. If the cookie does not exist, $default
+        * is returned.
+        *
+        * @param string $name
+        * @param mixed|null $default
+        * @return mixed The cookie value as a string, or $default
+        */
+       function getCookie( $name, $default = null );
+
+       /**
+        * Retrieve POST form parameters.
+        *
+        * This will return an array of parameters in the format of $_POST.
+        *
+        * @return array The deserialized POST parameters
+        */
+       function getPostParams();
+}
diff --git a/includes/Rest/Response.php b/includes/Rest/Response.php
new file mode 100644 (file)
index 0000000..3b01028
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use HttpStatus;
+use Psr\Http\Message\StreamInterface;
+
+class Response implements ResponseInterface {
+       /** @var int */
+       private $statusCode = 200;
+
+       /** @var string */
+       private $reasonPhrase = 'OK';
+
+       /** @var string */
+       private $protocolVersion = '1.1';
+
+       /** @var StreamInterface */
+       private $body;
+
+       /** @var HeaderContainer */
+       private $headerContainer;
+
+       /** @var array */
+       private $cookies = [];
+
+       /**
+        * @internal Use ResponseFactory
+        * @param string $bodyContents
+        */
+       public function __construct( $bodyContents = '' ) {
+               $this->body = new StringStream( $bodyContents );
+               $this->headerContainer = new HeaderContainer;
+       }
+
+       public function getStatusCode() {
+               return $this->statusCode;
+       }
+
+       public function getReasonPhrase() {
+               return $this->reasonPhrase;
+       }
+
+       public function setStatus( $code, $reasonPhrase = '' ) {
+               $this->statusCode = $code;
+               if ( $reasonPhrase === '' ) {
+                       $reasonPhrase = HttpStatus::getMessage( $code ) ?? '';
+               }
+               $this->reasonPhrase = $reasonPhrase;
+       }
+
+       public function getProtocolVersion() {
+               return $this->protocolVersion;
+       }
+
+       public function getHeaders() {
+               return $this->headerContainer->getHeaders();
+       }
+
+       public function hasHeader( $name ) {
+               return $this->headerContainer->hasHeader( $name );
+       }
+
+       public function getHeader( $name ) {
+               return $this->headerContainer->getHeader( $name );
+       }
+
+       public function getHeaderLine( $name ) {
+               return $this->headerContainer->getHeaderLine( $name );
+       }
+
+       public function getBody() {
+               return $this->body;
+       }
+
+       public function setProtocolVersion( $version ) {
+               $this->protocolVersion = $version;
+       }
+
+       public function setHeader( $name, $value ) {
+               $this->headerContainer->setHeader( $name, $value );
+       }
+
+       public function addHeader( $name, $value ) {
+               $this->headerContainer->addHeader( $name, $value );
+       }
+
+       public function removeHeader( $name ) {
+               $this->headerContainer->removeHeader( $name );
+       }
+
+       public function setBody( StreamInterface $body ) {
+               $this->body = $body;
+       }
+
+       public function getRawHeaderLines() {
+               return $this->headerContainer->getRawHeaderLines();
+       }
+
+       public function setCookie( $name, $value, $expire = 0, $options = [] ) {
+               $this->cookies[] = [
+                       'name' => $name,
+                       'value' => $value,
+                       'expire' => $expire,
+                       'options' => $options
+               ];
+       }
+
+       public function getCookies() {
+               return $this->cookies;
+       }
+}
diff --git a/includes/Rest/ResponseFactory.php b/includes/Rest/ResponseFactory.php
new file mode 100644 (file)
index 0000000..7ccb612
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use Exception;
+use HttpStatus;
+use InvalidArgumentException;
+use MWExceptionHandler;
+use stdClass;
+use Throwable;
+
+/**
+ * Generates standardized response objects.
+ */
+class ResponseFactory {
+
+       const CT_PLAIN = 'text/plain; charset=utf-8';
+       const CT_HTML = 'text/html; charset=utf-8';
+       const CT_JSON = 'application/json';
+
+       /**
+        * Encode a stdClass object or array to a JSON string
+        *
+        * @param array|stdClass $value
+        * @return string
+        * @throws JsonEncodingException
+        */
+       public function encodeJson( $value ) {
+               $json = json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+               if ( $json === false ) {
+                       throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
+               }
+               return $json;
+       }
+
+       /**
+        * Create an unspecified response. It is the caller's responsibility to set specifics
+        * like response code, content type etc.
+        * @return Response
+        */
+       public function create() {
+               return new Response();
+       }
+
+       /**
+        * Create a successful JSON response.
+        * @param array|stdClass $value JSON value
+        * @param string|null $contentType HTTP content type (should be 'application/json+...')
+        *   or null for plain 'application/json'
+        * @return Response
+        */
+       public function createJson( $value, $contentType = null ) {
+               $contentType = $contentType ?? self::CT_JSON;
+               $response = new Response( $this->encodeJson( $value ) );
+               $response->setHeader( 'Content-Type', $contentType );
+               return $response;
+       }
+
+       /**
+        * Create a 204 (No Content) response, used to indicate that an operation which does
+        * not return anything (e.g. a PUT request) was successful.
+        *
+        * Headers are generally interpreted to refer to the target of the operation. E.g. if
+        * this was a PUT request, the caller of this method might want to add an ETag header
+        * describing the created resource.
+        *
+        * @return Response
+        */
+       public function createNoContent() {
+               $response = new Response();
+               $response->setStatus( 204 );
+               return $response;
+       }
+
+       /**
+        * Creates a permanent (301) redirect.
+        * This indicates that the caller of the API should update their indexes and call
+        * the new URL in the future. 301 redirects tend to get cached and are hard to undo.
+        * Client behavior for methods other than GET/HEAD is not well-defined and this type
+        * of response should be avoided in such cases.
+        * @param string $target Redirect URL (can be relative)
+        * @return Response
+        */
+       public function createPermanentRedirect( $target ) {
+               $response = $this->createRedirectBase( $target );
+               $response->setStatus( 301 );
+               return $response;
+       }
+
+       /**
+        * Creates a temporary (307) redirect.
+        * This indicates that the operation the client was trying to perform can temporarily
+        * be achieved by using a different URL. Clients will preserve the request method when
+        * retrying the request with the new URL.
+        * @param string $target Redirect URL (can be relative)
+        * @return Response
+        */
+       public function createTemporaryRedirect( $target ) {
+               $response = $this->createRedirectBase( $target );
+               $response->setStatus( 307 );
+               return $response;
+       }
+
+       /**
+        * Creates a See Other (303) redirect.
+        * This indicates that the target resource might be of interest to the client, without
+        * necessarily implying that it is the same resource. The client will always use GET
+        * (or HEAD) when following the redirection. Useful for GET-after-POST.
+        * @param string $target Redirect URL (can be relative)
+        * @return Response
+        */
+       public function createSeeOther( $target ) {
+               $response = $this->createRedirectBase( $target );
+               $response->setStatus( 303 );
+               return $response;
+       }
+
+       /**
+        * Create a 304 (Not Modified) response, used when the client has an up-to-date cached response.
+        *
+        * Per RFC 7232 the response should contain all Cache-Control, Content-Location, Date,
+        * ETag, Expires, and Vary headers that would have been sent with the 200 OK answer
+        * if the requesting client did not have a valid cached response. This is the responsibility
+        * of the caller of this method.
+        *
+        * @return Response
+        */
+       public function createNotModified() {
+               $response = new Response();
+               $response->setStatus( 304 );
+               return $response;
+       }
+
+       /**
+        * Create a HTTP 4xx or 5xx response.
+        * @param int $errorCode HTTP error code
+        * @param array $bodyData An array of data to be included in the JSON response
+        * @return Response
+        * @throws InvalidArgumentException
+        */
+       public function createHttpError( $errorCode, array $bodyData = [] ) {
+               if ( $errorCode < 400 || $errorCode >= 600 ) {
+                       throw new InvalidArgumentException( 'error code must be 4xx or 5xx' );
+               }
+               $response = $this->createJson( $bodyData + [
+                       'httpCode' => $errorCode,
+                       'httpReason' => HttpStatus::getMessage( $errorCode )
+               ] );
+               // TODO add link to error code documentation
+               $response->setStatus( $errorCode );
+               return $response;
+       }
+
+       /**
+        * Turn an exception into a JSON error response.
+        * @param Exception|Throwable $exception
+        * @return Response
+        */
+       public function createFromException( $exception ) {
+               if ( $exception instanceof HttpException ) {
+                       // FIXME can HttpException represent 2xx or 3xx responses?
+                       $response = $this->createHttpError( $exception->getCode(),
+                               [ 'message' => $exception->getMessage() ] );
+               } else {
+                       $response = $this->createHttpError( 500, [
+                               'message' => 'Error: exception of type ' . get_class( $exception ),
+                               'exception' => MWExceptionHandler::getStructuredExceptionData( $exception )
+                       ] );
+                       // FIXME should we try to do something useful with ILocalizedException?
+                       // FIXME should we try to do something useful with common MediaWiki errors like ReadOnlyError?
+               }
+               return $response;
+       }
+
+       /**
+        * Create a JSON response from an arbitrary value.
+        * This is a fallback; it's preferable to use createJson() instead.
+        * @param mixed $value A structure containing only scalars, arrays and stdClass objects
+        * @return Response
+        * @throws InvalidArgumentException When $value cannot be reasonably represented as JSON
+        */
+       public function createFromReturnValue( $value ) {
+               $originalValue = $value;
+               if ( is_scalar( $value ) ) {
+                       $data = [ 'value' => $value ];
+               } elseif ( is_array( $value ) || $value instanceof stdClass ) {
+                       $data = $value;
+               } else {
+                       $type = gettype( $originalValue );
+                       if ( $type === 'object' ) {
+                               $type = get_class( $originalValue );
+                       }
+                       throw new InvalidArgumentException( __METHOD__ . ": Invalid return value type $type" );
+               }
+               $response = $this->createJson( $data );
+               return $response;
+       }
+
+       /**
+        * Create a redirect response with type / response code unspecified.
+        * @param string $target Redirect target (an absolute URL)
+        * @return Response
+        */
+       protected function createRedirectBase( $target ) {
+               $response = new Response( $this->getHyperLink( $target ) );
+               $response->setHeader( 'Content-Type', self::CT_HTML );
+               $response->setHeader( 'Location', $target );
+               return $response;
+       }
+
+       /**
+        * Returns a minimal HTML document that links to the given URL, as suggested by
+        * RFC 7231 for 3xx responses.
+        * @param string $url An absolute URL
+        * @return string
+        */
+       protected function getHyperLink( $url ) {
+               $url = htmlspecialchars( $url );
+               return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
+       }
+
+}
diff --git a/includes/Rest/ResponseInterface.php b/includes/Rest/ResponseInterface.php
new file mode 100644 (file)
index 0000000..797b96f
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ * Copyright (c) 2019 Wikimedia Foundation.
+ *
+ * This file is partly derived from PSR-7, which requires the following copyright notice:
+ *
+ * Copyright (c) 2014 PHP Framework Interoperability Group
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Rest;
+
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * An interface similar to PSR-7's ResponseInterface, the primary difference
+ * being that it is mutable.
+ */
+interface ResponseInterface {
+       // ResponseInterface
+
+       /**
+        * Gets the response status code.
+        *
+        * The status code is a 3-digit integer result code of the server's attempt
+        * to understand and satisfy the request.
+        *
+        * @return int Status code.
+        */
+       function getStatusCode();
+
+       /**
+        * Gets the response reason phrase associated with the status code.
+        *
+        * Because a reason phrase is not a required element in a response
+        * status line, the reason phrase value MAY be empty. Implementations MAY
+        * choose to return the default RFC 7231 recommended reason phrase (or those
+        * listed in the IANA HTTP Status Code Registry) for the response's
+        * status code.
+        *
+        * @see http://tools.ietf.org/html/rfc7231#section-6
+        * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+        * @return string Reason phrase; must return an empty string if none present.
+        */
+       function getReasonPhrase();
+
+       // ResponseInterface mutation
+
+       /**
+        * Set the status code and, optionally, reason phrase.
+        *
+        * If no reason phrase is specified, implementations MAY choose to default
+        * to the RFC 7231 or IANA recommended reason phrase for the response's
+        * status code.
+        *
+        * @see http://tools.ietf.org/html/rfc7231#section-6
+        * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+        * @param int $code The 3-digit integer result code to set.
+        * @param string $reasonPhrase The reason phrase to use with the
+        *     provided status code; if none is provided, implementations MAY
+        *     use the defaults as suggested in the HTTP specification.
+        * @throws \InvalidArgumentException For invalid status code arguments.
+        */
+       function setStatus( $code, $reasonPhrase = '' );
+
+       // MessageInterface
+
+       /**
+        * Retrieves the HTTP protocol version as a string.
+        *
+        * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+        *
+        * @return string HTTP protocol version.
+        */
+       function getProtocolVersion();
+
+       /**
+        * Retrieves all message header values.
+        *
+        * The keys represent the header name as it will be sent over the wire, and
+        * each value is an array of strings associated with the header.
+        *
+        *     // Represent the headers as a string
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         echo $name . ': ' . implode(', ', $values);
+        *     }
+        *
+        *     // Emit headers iteratively:
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         foreach ($values as $value) {
+        *             header(sprintf('%s: %s', $name, $value), false);
+        *         }
+        *     }
+        *
+        * While header names are not case-sensitive, getHeaders() will preserve the
+        * exact case in which headers were originally specified.
+        *
+        * @return string[][] Returns an associative array of the message's headers.
+        *     Each key MUST be a header name, and each value MUST be an array of
+        *     strings for that header.
+        */
+       function getHeaders();
+
+       /**
+        * Checks if a header exists by the given case-insensitive name.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return bool Returns true if any header names match the given header
+        *     name using a case-insensitive string comparison. Returns false if
+        *     no matching header name is found in the message.
+        */
+       function hasHeader( $name );
+
+       /**
+        * Retrieves a message header value by the given case-insensitive name.
+        *
+        * This method returns an array of all the header values of the given
+        * case-insensitive header name.
+        *
+        * If the header does not appear in the message, this method MUST return an
+        * empty array.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string[] An array of string values as provided for the given
+        *    header. If the header does not appear in the message, this method MUST
+        *    return an empty array.
+        */
+       function getHeader( $name );
+
+       /**
+        * Retrieves a comma-separated string of the values for a single header.
+        *
+        * This method returns all of the header values of the given
+        * case-insensitive header name as a string concatenated together using
+        * a comma.
+        *
+        * NOTE: Not all header values may be appropriately represented using
+        * comma concatenation. For such headers, use getHeader() instead
+        * and supply your own delimiter when concatenating.
+        *
+        * If the header does not appear in the message, this method MUST return
+        * an empty string.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string A string of values as provided for the given header
+        *    concatenated together using a comma. If the header does not appear in
+        *    the message, this method MUST return an empty string.
+        */
+       function getHeaderLine( $name );
+
+       /**
+        * Gets the body of the message.
+        *
+        * @return StreamInterface Returns the body as a stream.
+        */
+       function getBody();
+
+       // MessageInterface mutation
+
+       /**
+        * Set the HTTP protocol version.
+        *
+        * The version string MUST contain only the HTTP version number (e.g.,
+        * "1.1", "1.0").
+        *
+        * @param string $version HTTP protocol version
+        */
+       function setProtocolVersion( $version );
+
+       /**
+        * Set or replace the specified header.
+        *
+        * While header names are case-insensitive, the casing of the header will
+        * be preserved by this function, and returned from getHeaders().
+        *
+        * @param string $name Case-insensitive header field name.
+        * @param string|string[] $value Header value(s).
+        * @throws \InvalidArgumentException for invalid header names or values.
+        */
+       function setHeader( $name, $value );
+
+       /**
+        * Append the given value to the specified header.
+        *
+        * Existing values for the specified header will be maintained. The new
+        * value(s) will be appended to the existing list. If the header did not
+        * exist previously, it will be added.
+        *
+        * @param string $name Case-insensitive header field name to add.
+        * @param string|string[] $value Header value(s).
+        * @throws \InvalidArgumentException for invalid header names.
+        * @throws \InvalidArgumentException for invalid header values.
+        */
+       function addHeader( $name, $value );
+
+       /**
+        * Remove the specified header.
+        *
+        * Header resolution MUST be done without case-sensitivity.
+        *
+        * @param string $name Case-insensitive header field name to remove.
+        */
+       function removeHeader( $name );
+
+       /**
+        * Set the message body
+        *
+        * The body MUST be a StreamInterface object.
+        *
+        * @param StreamInterface $body Body.
+        * @throws \InvalidArgumentException When the body is not valid.
+        */
+       function setBody( StreamInterface $body );
+
+       // MediaWiki extensions to PSR-7
+
+       /**
+        * Get the full header lines including colon-separated name and value, for
+        * passing directly to header(). Not including the status line.
+        *
+        * @return string[]
+        */
+       function getRawHeaderLines();
+
+       /**
+        * Set a cookie
+        *
+        * The name will have the cookie prefix added to it before it is sent over
+        * the network.
+        *
+        * @param string $name The name of the cookie, not including prefix.
+        * @param string $value The value to be stored in the cookie.
+        * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
+        *        0 (the default) causes it to expire $wgCookieExpiration seconds from now.
+        *        null causes it to be a session cookie.
+        * @param array $options Assoc of additional cookie options:
+        *     prefix: string, name prefix ($wgCookiePrefix)
+        *     domain: string, cookie domain ($wgCookieDomain)
+        *     path: string, cookie path ($wgCookiePath)
+        *     secure: bool, secure attribute ($wgCookieSecure)
+        *     httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
+        */
+       public function setCookie( $name, $value, $expire = 0, $options = [] );
+
+       /**
+        * Get all previously set cookies as a list of associative arrays with
+        * the following keys:
+        *
+        *  - name: The cookie name
+        *  - value: The cookie value
+        *  - expire: The requested expiry time
+        *  - options: An associative array of further options
+        *
+        * @return array
+        */
+       public function getCookies();
+}
diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php
new file mode 100644 (file)
index 0000000..39bee89
--- /dev/null
@@ -0,0 +1,262 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use AppendIterator;
+use BagOStuff;
+use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
+use Wikimedia\ObjectFactory;
+
+/**
+ * The REST router is responsible for gathering handler configuration, matching
+ * an input path and HTTP method against the defined routes, and constructing
+ * and executing the relevant handler for a request.
+ */
+class Router {
+       /** @var string[] */
+       private $routeFiles;
+
+       /** @var array */
+       private $extraRoutes;
+
+       /** @var array|null */
+       private $routesFromFiles;
+
+       /** @var int[]|null */
+       private $routeFileTimestamps;
+
+       /** @var string */
+       private $rootPath;
+
+       /** @var \BagOStuff */
+       private $cacheBag;
+
+       /** @var PathMatcher[]|null Path matchers by method */
+       private $matchers;
+
+       /** @var string|null */
+       private $configHash;
+
+       /** @var ResponseFactory */
+       private $responseFactory;
+
+       /**
+        * @param string[] $routeFiles List of names of JSON files containing routes
+        * @param array $extraRoutes Extension route array
+        * @param string $rootPath The base URL path
+        * @param BagOStuff $cacheBag A cache in which to store the matcher trees
+        * @param ResponseFactory $responseFactory
+        */
+       public function __construct( $routeFiles, $extraRoutes, $rootPath,
+               BagOStuff $cacheBag, ResponseFactory $responseFactory
+       ) {
+               $this->routeFiles = $routeFiles;
+               $this->extraRoutes = $extraRoutes;
+               $this->rootPath = $rootPath;
+               $this->cacheBag = $cacheBag;
+               $this->responseFactory = $responseFactory;
+       }
+
+       /**
+        * Get the cache data, or false if it is missing or invalid
+        *
+        * @return bool|array
+        */
+       private function fetchCacheData() {
+               $cacheData = $this->cacheBag->get( $this->getCacheKey() );
+               if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
+                       unset( $cacheData['CONFIG-HASH'] );
+                       return $cacheData;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * @return string The cache key
+        */
+       private function getCacheKey() {
+               return $this->cacheBag->makeKey( __CLASS__, '1' );
+       }
+
+       /**
+        * Get a config version hash for cache invalidation
+        *
+        * @return string
+        */
+       private function getConfigHash() {
+               if ( $this->configHash === null ) {
+                       $this->configHash = md5( json_encode( [
+                               $this->extraRoutes,
+                               $this->getRouteFileTimestamps()
+                       ] ) );
+               }
+               return $this->configHash;
+       }
+
+       /**
+        * Load the defined JSON files and return the merged routes
+        *
+        * @return array
+        */
+       private function getRoutesFromFiles() {
+               if ( $this->routesFromFiles === null ) {
+                       $this->routeFileTimestamps = [];
+                       foreach ( $this->routeFiles as $fileName ) {
+                               $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
+                               $routes = json_decode( file_get_contents( $fileName ), true );
+                               if ( $this->routesFromFiles === null ) {
+                                       $this->routesFromFiles = $routes;
+                               } else {
+                                       $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
+                               }
+                       }
+               }
+               return $this->routesFromFiles;
+       }
+
+       /**
+        * Get an array of last modification times of the defined route files.
+        *
+        * @return int[] Last modification times
+        */
+       private function getRouteFileTimestamps() {
+               if ( $this->routeFileTimestamps === null ) {
+                       $this->routeFileTimestamps = [];
+                       foreach ( $this->routeFiles as $fileName ) {
+                               $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
+                       }
+               }
+               return $this->routeFileTimestamps;
+       }
+
+       /**
+        * Get an iterator for all defined routes, including loading the routes from
+        * the JSON files.
+        *
+        * @return AppendIterator
+        */
+       private function getAllRoutes() {
+               $iterator = new AppendIterator;
+               $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
+               $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
+               return $iterator;
+       }
+
+       /**
+        * Get an array of PathMatcher objects indexed by HTTP method
+        *
+        * @return PathMatcher[]
+        */
+       private function getMatchers() {
+               if ( $this->matchers === null ) {
+                       $cacheData = $this->fetchCacheData();
+                       $matchers = [];
+                       if ( $cacheData ) {
+                               foreach ( $cacheData as $method => $data ) {
+                                       $matchers[$method] = PathMatcher::newFromCache( $data );
+                               }
+                       } else {
+                               foreach ( $this->getAllRoutes() as $spec ) {
+                                       $methods = $spec['method'] ?? [ 'GET' ];
+                                       if ( !is_array( $methods ) ) {
+                                               $methods = [ $methods ];
+                                       }
+                                       foreach ( $methods as $method ) {
+                                               if ( !isset( $matchers[$method] ) ) {
+                                                       $matchers[$method] = new PathMatcher;
+                                               }
+                                               $matchers[$method]->add( $spec['path'], $spec );
+                                       }
+                               }
+
+                               $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
+                               foreach ( $matchers as $method => $matcher ) {
+                                       $cacheData[$method] = $matcher->getCacheData();
+                               }
+                               $this->cacheBag->set( $this->getCacheKey(), $cacheData );
+                       }
+                       $this->matchers = $matchers;
+               }
+               return $this->matchers;
+       }
+
+       /**
+        * Remove the path prefix $this->rootPath. Return the part of the path with the
+        * prefix removed, or false if the prefix did not match.
+        *
+        * @param string $path
+        * @return false|string
+        */
+       private function getRelativePath( $path ) {
+               if ( substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0 ) {
+                       return false;
+               }
+               return substr( $path, strlen( $this->rootPath ) );
+       }
+
+       /**
+        * Find the handler for a request and execute it
+        *
+        * @param RequestInterface $request
+        * @return ResponseInterface
+        */
+       public function execute( RequestInterface $request ) {
+               $path = $request->getUri()->getPath();
+               $relPath = $this->getRelativePath( $path );
+               if ( $relPath === false ) {
+                       return $this->responseFactory->createHttpError( 404 );
+               }
+
+               $matchers = $this->getMatchers();
+               $matcher = $matchers[$request->getMethod()] ?? null;
+               $match = $matcher ? $matcher->match( $relPath ) : null;
+
+               if ( !$match ) {
+                       // Check for 405 wrong method
+                       $allowed = [];
+                       foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
+                               if ( $allowedMethod === $request->getMethod() ) {
+                                       continue;
+                               }
+                               if ( $allowedMatcher->match( $relPath ) ) {
+                                       $allowed[] = $allowedMethod;
+                               }
+                       }
+                       if ( $allowed ) {
+                               $response = $this->responseFactory->createHttpError( 405 );
+                               $response->setHeader( 'Allow', $allowed );
+                               return $response;
+                       } else {
+                               // Did not match with any other method, must be 404
+                               return $this->responseFactory->createHttpError( 404 );
+                       }
+               }
+
+               $request->setPathParams( $match['params'] );
+               $spec = $match['userData'];
+               $objectFactorySpec = array_intersect_key( $spec,
+                       [ 'factory' => true, 'class' => true, 'args' => true ] );
+               $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec );
+               $handler->init( $request, $spec, $this->responseFactory );
+
+               try {
+                       return $this->executeHandler( $handler );
+               } catch ( HttpException $e ) {
+                       return $this->responseFactory->createFromException( $e );
+               }
+       }
+
+       /**
+        * Execute a fully-constructed handler
+        * @param Handler $handler
+        * @return ResponseInterface
+        */
+       private function executeHandler( $handler ): ResponseInterface {
+               $response = $handler->execute();
+               if ( !( $response instanceof ResponseInterface ) ) {
+                       $response = $this->responseFactory->createFromReturnValue( $response );
+               }
+               return $response;
+       }
+}
diff --git a/includes/Rest/SimpleHandler.php b/includes/Rest/SimpleHandler.php
new file mode 100644 (file)
index 0000000..85749c6
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * A handler base class which unpacks parameters from the path template and
+ * passes them as formal parameters to run().
+ *
+ * run() must be declared in the subclass. It cannot be declared as abstract
+ * here because it has a variable parameter list.
+ *
+ * @package MediaWiki\Rest
+ */
+class SimpleHandler extends Handler {
+       public function execute() {
+               $params = array_values( $this->getRequest()->getPathParams() );
+               return $this->run( ...$params );
+       }
+}
diff --git a/includes/Rest/Stream.php b/includes/Rest/Stream.php
new file mode 100644 (file)
index 0000000..1169875
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use GuzzleHttp\Psr7;
+
+class Stream extends Psr7\Stream implements CopyableStreamInterface {
+       private $stream;
+
+       public function __construct( $stream, $options = [] ) {
+               $this->stream = $stream;
+               parent::__construct( $stream, $options );
+       }
+
+       public function copyToStream( $target ) {
+               stream_copy_to_stream( $this->stream, $target );
+       }
+}
diff --git a/includes/Rest/StringStream.php b/includes/Rest/StringStream.php
new file mode 100644 (file)
index 0000000..3ad0d96
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+/**
+ * A stream class which uses a string as the underlying storage. Surprisingly,
+ * Guzzle does not appear to have one of these. BufferStream does not do what
+ * we want.
+ *
+ * The normal use of this class should be to first write to the stream, then
+ * rewind, then read back the whole buffer with getContents().
+ *
+ * Seeking is supported, however seeking past the end of the string does not
+ * fill with null bytes as in a real file, it throws an exception instead.
+ */
+class StringStream implements CopyableStreamInterface {
+       private $contents = '';
+       private $offset = 0;
+
+       /**
+        * Construct a StringStream with the given contents.
+        *
+        * The offset will start at 0, ready for reading. If appending to the
+        * given string is desired, you should first seek to the end.
+        *
+        * @param string $contents
+        */
+       public function __construct( $contents = '' ) {
+               $this->contents = $contents;
+       }
+
+       public function copyToStream( $stream ) {
+               fwrite( $stream, $this->getContents() );
+       }
+
+       public function __toString() {
+               return $this->contents;
+       }
+
+       public function close() {
+       }
+
+       public function detach() {
+               return null;
+       }
+
+       public function getSize() {
+               return strlen( $this->contents );
+       }
+
+       public function tell() {
+               return $this->offset;
+       }
+
+       public function eof() {
+               return $this->offset >= strlen( $this->contents );
+       }
+
+       public function isSeekable() {
+               return true;
+       }
+
+       public function seek( $offset, $whence = SEEK_SET ) {
+               switch ( $whence ) {
+                       case SEEK_SET:
+                               $this->offset = $offset;
+                               break;
+
+                       case SEEK_CUR:
+                               $this->offset += $offset;
+                               break;
+
+                       case SEEK_END:
+                               $this->offset = strlen( $this->contents ) + $offset;
+                               break;
+
+                       default:
+                               throw new \InvalidArgumentException( "Invalid value for \$whence" );
+               }
+               if ( $this->offset > strlen( $this->contents ) ) {
+                       throw new \InvalidArgumentException( "Cannot seek beyond the end of a StringStream" );
+               }
+               if ( $this->offset < 0 ) {
+                       throw new \InvalidArgumentException( "Cannot seek before the start of a StringStream" );
+               }
+       }
+
+       public function rewind() {
+               $this->offset = 0;
+       }
+
+       public function isWritable() {
+               return true;
+       }
+
+       public function write( $string ) {
+               if ( $this->offset === strlen( $this->contents ) ) {
+                       $this->contents .= $string;
+               } else {
+                       $this->contents = substr_replace( $this->contents, $string,
+                               $this->offset, strlen( $string ) );
+               }
+               $this->offset += strlen( $string );
+               return strlen( $string );
+       }
+
+       public function isReadable() {
+               return true;
+       }
+
+       public function read( $length ) {
+               if ( $this->offset === 0 && $length >= strlen( $this->contents ) ) {
+                       $ret = $this->contents;
+               } elseif ( $this->offset >= strlen( $this->contents ) ) {
+                       $ret = '';
+               } else {
+                       $ret = substr( $this->contents, $this->offset, $length );
+               }
+               $this->offset += strlen( $ret );
+               return $ret;
+       }
+
+       public function getContents() {
+               if ( $this->offset === 0 ) {
+                       $ret = $this->contents;
+               } elseif ( $this->offset >= strlen( $this->contents ) ) {
+                       $ret = '';
+               } else {
+                       $ret = substr( $this->contents, $this->offset );
+               }
+               $this->offset = strlen( $this->contents );
+               return $ret;
+       }
+
+       public function getMetadata( $key = null ) {
+               return null;
+       }
+}
diff --git a/includes/Rest/coreRoutes.json b/includes/Rest/coreRoutes.json
new file mode 100644 (file)
index 0000000..6b440f7
--- /dev/null
@@ -0,0 +1,6 @@
+[
+       {
+               "path": "/user/{name}/hello",
+               "class": "MediaWiki\\Rest\\Handler\\HelloHandler"
+       }
+]
index c4a0054..4acb9c0 100644 (file)
@@ -291,19 +291,28 @@ class RenderedRevision implements SlotRenderingProvider {
 
                $this->setRevisionInternal( $rev );
 
-               $this->pruneRevisionSensitiveOutput( $this->revision->getId() );
+               $this->pruneRevisionSensitiveOutput(
+                       $this->revision->getId(),
+                       $this->revision->getTimestamp()
+               );
        }
 
        /**
         * Prune any output that depends on the revision ID.
         *
-        * @param int|bool  $actualRevId The actual rev id, to check the used speculative rev ID
+        * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID
         *        against, or false to not purge on vary-revision-id, or true to purge on
         *        vary-revision-id unconditionally.
+        * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the
+        *        parser output revision timestamp, or false to not purge on vary-revision-timestamp
         */
-       private function pruneRevisionSensitiveOutput( $actualRevId ) {
+       private function pruneRevisionSensitiveOutput( $actualRevId, $actualRevTimestamp ) {
                if ( $this->revisionOutput ) {
-                       if ( $this->outputVariesOnRevisionMetaData( $this->revisionOutput, $actualRevId ) ) {
+                       if ( $this->outputVariesOnRevisionMetaData(
+                               $this->revisionOutput,
+                               $actualRevId,
+                               $actualRevTimestamp
+                       ) ) {
                                $this->revisionOutput = null;
                        }
                } else {
@@ -311,7 +320,11 @@ class RenderedRevision implements SlotRenderingProvider {
                }
 
                foreach ( $this->slotsOutput as $role => $output ) {
-                       if ( $this->outputVariesOnRevisionMetaData( $output, $actualRevId ) ) {
+                       if ( $this->outputVariesOnRevisionMetaData(
+                               $output,
+                               $actualRevId,
+                               $actualRevTimestamp
+                       ) ) {
                                unset( $this->slotsOutput[$role] );
                        }
                }
@@ -372,19 +385,24 @@ class RenderedRevision implements SlotRenderingProvider {
        /**
         * @param ParserOutput $out
         * @param int|bool  $actualRevId The actual rev id, to check the used speculative rev ID
-        *        against, or false to not purge on vary-revision-id, or true to purge on
+        *        against, false to not purge on vary-revision-id, or true to purge on
         *        vary-revision-id unconditionally.
+        * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the
+        *        parser output revision timestamp, false to not purge on vary-revision-timestamp,
+        *        or true to purge on vary-revision-timestamp unconditionally.
         * @return bool
         */
-       private function outputVariesOnRevisionMetaData( ParserOutput $out, $actualRevId ) {
+       private function outputVariesOnRevisionMetaData(
+               ParserOutput $out,
+               $actualRevId,
+               $actualRevTimestamp
+       ) {
                $method = __METHOD__;
 
                if ( $out->getFlag( 'vary-revision' ) ) {
-                       // If {{PAGEID}} resolved to 0 or {{REVISIONTIMESTAMP}} used the current
-                       // timestamp rather than that of an actual revision, then those words need
-                       // to resolve to the actual page ID or revision timestamp, respectively.
+                       // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID
                        $this->saveParseLogger->info(
-                               "$method: Prepared output has vary-revision...\n"
+                               "$method: Prepared output has vary-revision..."
                        );
                        return true;
                } elseif ( $out->getFlag( 'vary-revision-id' )
@@ -392,7 +410,16 @@ class RenderedRevision implements SlotRenderingProvider {
                        && ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId )
                ) {
                        $this->saveParseLogger->info(
-                               "$method: Prepared output has vary-revision-id with wrong ID...\n"
+                               "$method: Prepared output has vary-revision-id with wrong ID..."
+                       );
+                       return true;
+               } elseif ( $out->getFlag( 'vary-revision-timestamp' )
+                       && $actualRevTimestamp !== false
+                       && ( $actualRevTimestamp === true ||
+                               $out->getRevisionTimestampUsed() !== $actualRevTimestamp )
+               ) {
+                       $this->saveParseLogger->info(
+                               "$method: Prepared output has vary-revision-timestamp with wrong timestamp..."
                        );
                        return true;
                } elseif ( $out->getFlag( 'vary-revision-exists' ) ) {
@@ -400,7 +427,7 @@ class RenderedRevision implements SlotRenderingProvider {
                        // Note that edit stashing always uses '-', which can be used for both
                        // edit filter checks and canonical parser cache.
                        $this->saveParseLogger->info(
-                               "$method: Prepared output has vary-revision-exists...\n"
+                               "$method: Prepared output has vary-revision-exists..."
                        );
                        return true;
                } else {
@@ -412,7 +439,7 @@ class RenderedRevision implements SlotRenderingProvider {
                        // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called
                        // with the old, existing revision.
 
-                       wfDebug( "$method: Keeping prepared output...\n" );
+                       $this->saveParseLogger->debug( "$method: Keeping prepared output..." );
                        return false;
                }
        }
index 95749c5..70a891c 100644 (file)
@@ -27,6 +27,7 @@ use Content;
 use InvalidArgumentException;
 use LogicException;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\User\UserIdentity;
 use MWException;
 use Title;
@@ -521,8 +522,11 @@ abstract class RevisionRecord {
                        } else {
                                $text = $title->getPrefixedText();
                                wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
+
+                               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                                foreach ( $permissions as $perm ) {
-                                       if ( $title->userCan( $perm, $user ) ) {
+                                       if ( $permissionManager->userCan( $perm, $user, $title ) ) {
                                                return true;
                                        }
                                }
@@ -550,7 +554,7 @@ abstract class RevisionRecord {
                // null if mSlots is not empty.
 
                // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
-               //check them.
+               // check them.
 
                return $this->getTimestamp() !== null
                        && $this->getComment( self::RAW ) !== null
index f97390a..a63e4f1 100644 (file)
@@ -132,6 +132,13 @@ class RevisionRenderer {
                        return $this->getSpeculativeRevId( $dbIndex );
                } );
 
+               if ( !$rev->getId() && $rev->getTimestamp() ) {
+                       // This is an unsaved revision with an already determined timestamp.
+                       // Make the "current" time used during parsing match that of the revision.
+                       // Any REVISION* parser variables will match up if the revision is saved.
+                       $options->setTimestamp( $rev->getTimestamp() );
+               }
+
                $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
 
                $renderedRevision = new RenderedRevision(
index 29d7848..faa162a 100644 (file)
@@ -1644,7 +1644,7 @@ class RevisionStore
                        throw new RevisionAccessException(
                                'Main slot of revision ' . $revId . ' not found in database!'
                        );
-               };
+               }
 
                return $slots;
        }
@@ -2131,7 +2131,7 @@ class RevisionStore
                // within web requests to certain avoid bugs like T93866 and T94407.
                if ( !$rev
                        && !( $flags & self::READ_LATEST )
-                       && $lb->getServerCount() > 1
+                       && $lb->hasStreamingReplicaServers()
                        && $lb->hasOrMadeRecentMasterChanges()
                ) {
                        $flags = self::READ_LATEST;
index f367fc2..54e6795 100644 (file)
@@ -143,6 +143,9 @@ if ( $wgScript === false ) {
 if ( $wgLoadScript === false ) {
        $wgLoadScript = "$wgScriptPath/load.php";
 }
+if ( $wgRestPath === false ) {
+       $wgRestPath = "$wgScriptPath/rest.php";
+}
 
 if ( $wgArticlePath === false ) {
        if ( $wgUsePathInfo ) {
index 5e99454..368ca48 100644 (file)
@@ -23,7 +23,7 @@ namespace MediaWiki\Storage;
 use Language;
 use MediaWiki\Config\ServiceOptions;
 use WANObjectCache;
-use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\Rdbms\ILBFactory;
 
 /**
  * Service for instantiating BlobStores
@@ -35,7 +35,7 @@ use Wikimedia\Rdbms\LBFactory;
 class BlobStoreFactory {
 
        /**
-        * @var LBFactory
+        * @var ILBFactory
         */
        private $lbFactory;
 
@@ -68,7 +68,7 @@ class BlobStoreFactory {
        ];
 
        public function __construct(
-               LBFactory $lbFactory,
+               ILBFactory $lbFactory,
                WANObjectCache $cache,
                ServiceOptions $options,
                Language $contLang
index 53fe615..b4d6f05 100644 (file)
@@ -52,6 +52,9 @@ use MWCallableUpdate;
 use ParserCache;
 use ParserOptions;
 use ParserOutput;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 use RecentChangesUpdateJob;
 use ResourceLoaderWikiModule;
 use Revision;
@@ -60,7 +63,7 @@ use SiteStatsUpdate;
 use Title;
 use User;
 use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\Rdbms\ILBFactory;
 use WikiPage;
 
 /**
@@ -94,7 +97,7 @@ use WikiPage;
  * @since 1.32
  * @ingroup Page
  */
-class DerivedPageDataUpdater implements IDBAccessObject {
+class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
 
        /**
         * @var UserIdentity|null
@@ -132,10 +135,15 @@ class DerivedPageDataUpdater implements IDBAccessObject {
        private $messageCache;
 
        /**
-        * @var LBFactory
+        * @var ILBFactory
         */
        private $loadbalancerFactory;
 
+       /**
+        * @var LoggerInterface
+        */
+       private $logger;
+
        /**
         * @var string see $wgArticleCountMethod
         */
@@ -268,7 +276,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
         * @param JobQueueGroup $jobQueueGroup
         * @param MessageCache $messageCache
         * @param Language $contLang
-        * @param LBFactory $loadbalancerFactory
+        * @param ILBFactory $loadbalancerFactory
         */
        public function __construct(
                WikiPage $wikiPage,
@@ -279,7 +287,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                JobQueueGroup $jobQueueGroup,
                MessageCache $messageCache,
                Language $contLang,
-               LBFactory $loadbalancerFactory
+               ILBFactory $loadbalancerFactory
        ) {
                $this->wikiPage = $wikiPage;
 
@@ -293,6 +301,11 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                // XXX only needed for waiting for replicas to catch up; there should be a narrower
                // interface for that.
                $this->loadbalancerFactory = $loadbalancerFactory;
+               $this->logger = new NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
        }
 
        /**
@@ -850,11 +863,12 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                if ( $stashedEdit ) {
                        /** @var ParserOutput $output */
                        $output = $stashedEdit->output;
-
                        // TODO: this should happen when stashing the ParserOutput, not now!
                        $output->setCacheTime( $stashedEdit->timestamp );
 
                        $renderHints['known-revision-output'] = $output;
+
+                       $this->logger->debug( __METHOD__ . ': using stashed edit output...' );
                }
 
                // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
index 46f957f..2285f4a 100644 (file)
@@ -280,6 +280,12 @@ class PageEditStash {
                                "Cache for key '{key}' has vary_revision_id; post-insertion parse possible.",
                                $context
                        );
+               } elseif ( $editInfo->output->getFlag( 'vary-revision-timestamp' ) ) {
+                       // Similar to the above if we didn't guess the timestamp correctly.
+                       $logger->debug(
+                               "Cache for key '{key}' has vary_revision_timestamp; post-insertion parse possible.",
+                               $context
+                       );
                }
 
                return $editInfo;
@@ -338,7 +344,12 @@ class PageEditStash {
        public function stashInputText( $text, $textHash ) {
                $textKey = $this->cache->makeKey( 'stashedit', 'text', $textHash );
 
-               return $this->cache->set( $textKey, $text, self::MAX_CACHE_TTL );
+               return $this->cache->set(
+                       $textKey,
+                       $text,
+                       self::MAX_CACHE_TTL,
+                       BagOStuff::WRITE_ALLOW_SEGMENTS
+               );
        }
 
        /**
@@ -388,7 +399,7 @@ class PageEditStash {
         */
        private function getStashKey( Title $title, $contentHash, User $user ) {
                return $this->cache->makeKey(
-                       'stashed-edit-info',
+                       'stashedit-info-v1',
                        md5( $title->getPrefixedDBkey() ),
                        // Account for the edit model/text
                        $contentHash,
@@ -397,29 +408,13 @@ class PageEditStash {
                );
        }
 
-       /**
-        * @param string $hash
-        * @return string
-        */
-       private function getStashParserOutputKey( $hash ) {
-               return $this->cache->makeKey( 'stashed-edit-output', $hash );
-       }
-
        /**
         * @param string $key
         * @return stdClass|bool Object map (pstContent,output,outputID,timestamp,edits) or false
         */
        private function getStashValue( $key ) {
                $stashInfo = $this->cache->get( $key );
-               if ( !is_object( $stashInfo ) ) {
-                       return false;
-               }
-
-               $parserOutputKey = $this->getStashParserOutputKey( $stashInfo->outputID );
-               $parserOutput = $this->cache->get( $parserOutputKey );
-               if ( $parserOutput instanceof ParserOutput ) {
-                       $stashInfo->output = $parserOutput;
-
+               if ( is_object( $stashInfo ) && $stashInfo->output instanceof ParserOutput ) {
                        return $stashInfo;
                }
 
@@ -459,23 +454,14 @@ class PageEditStash {
                }
 
                // Store what is actually needed and split the output into another key (T204742)
-               $parserOutputID = md5( $key );
                $stashInfo = (object)[
                        'pstContent' => $pstContent,
-                       'outputID'   => $parserOutputID,
+                       'output'     => $parserOutput,
                        'timestamp'  => $timestamp,
                        'edits'      => $user->getEditCount()
                ];
 
-               $ok = $this->cache->set( $key, $stashInfo, $ttl );
-               if ( $ok ) {
-                       $ok = $this->cache->set(
-                               $this->getStashParserOutputKey( $parserOutputID ),
-                               $parserOutput,
-                               $ttl
-                       );
-               }
-
+               $ok = $this->cache->set( $key, $stashInfo, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
                if ( $ok ) {
                        // These blobs can waste slots in low cardinality memcached slabs
                        $this->pruneExcessStashedEntries( $user, $key );
@@ -494,7 +480,7 @@ class PageEditStash {
                $keyList = $this->cache->get( $key ) ?: [];
                if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
                        $oldestKey = array_shift( $keyList );
-                       $this->cache->delete( $oldestKey );
+                       $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS );
                }
 
                $keyList[] = $newKey;
index e25f0f0..7246238 100644 (file)
@@ -51,7 +51,7 @@ use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\DBConnRef;
 use Wikimedia\Rdbms\DBUnexpectedError;
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\ILoadBalancer;
 use WikiPage;
 
 /**
@@ -87,7 +87,7 @@ class PageUpdater {
        private $derivedDataUpdater;
 
        /**
-        * @var LoadBalancer
+        * @var ILoadBalancer
         */
        private $loadBalancer;
 
@@ -151,7 +151,7 @@ class PageUpdater {
         * @param User $user
         * @param WikiPage $wikiPage
         * @param DerivedPageDataUpdater $derivedDataUpdater
-        * @param LoadBalancer $loadBalancer
+        * @param ILoadBalancer $loadBalancer
         * @param RevisionStore $revisionStore
         * @param SlotRoleRegistry $slotRoleRegistry
         */
@@ -159,7 +159,7 @@ class PageUpdater {
                User $user,
                WikiPage $wikiPage,
                DerivedPageDataUpdater $derivedDataUpdater,
-               LoadBalancer $loadBalancer,
+               ILoadBalancer $loadBalancer,
                RevisionStore $revisionStore,
                SlotRoleRegistry $slotRoleRegistry
        ) {
index 82410cc..e0e14b0 100644 (file)
@@ -36,7 +36,7 @@ use MWException;
 use WANObjectCache;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\ILoadBalancer;
 
 /**
  * Service for storing and loading Content objects.
@@ -52,7 +52,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
        const TEXT_CACHE_GROUP = 'revisiontext:10';
 
        /**
-        * @var LoadBalancer
+        * @var ILoadBalancer
         */
        private $dbLoadBalancer;
 
@@ -92,7 +92,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
        private $useExternalStore = false;
 
        /**
-        * @param LoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
+        * @param ILoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
         * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local
         *        wiki's default instance even if $wikiId refers to a different wiki, since
         *        makeGlobalKey() is used to constructed a key that allows cached blobs from the
@@ -102,7 +102,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
         * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
         */
        public function __construct(
-               LoadBalancer $dbLoadBalancer,
+               ILoadBalancer $dbLoadBalancer,
                WANObjectCache $cache,
                $wikiId = false
        ) {
@@ -186,7 +186,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
        }
 
        /**
-        * @return LoadBalancer
+        * @return ILoadBalancer
         */
        private function getDBLoadBalancer() {
                return $this->dbLoadBalancer;
@@ -220,9 +220,6 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
                        if ( $this->useExternalStore ) {
                                // Store and get the URL
                                $data = ExternalStore::insertToDefault( $data );
-                               if ( !$data ) {
-                                       throw new BlobAccessException( "Failed to store text to external storage" );
-                               }
                                if ( $flags ) {
                                        $flags .= ',';
                                }
index b7b28af..f69f1a4 100644 (file)
@@ -1979,7 +1979,7 @@ class Title implements LinkTarget, IDBAccessObject {
         *
         * @param string|string[] $query An optional query string,
         *   not used for interwiki links. Can be specified as an associative array as well,
-        *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+        *   e.g., [ 'action' => 'edit' ] (keys and values will be URL-escaped).
         *   Some query patterns will trigger various shorturl path replacements.
         * @param string|string[]|bool $query2 An optional secondary query array. This one MUST
         *   be an array. If a string is passed it will be interpreted as a deprecated
@@ -2256,7 +2256,7 @@ class Title implements LinkTarget, IDBAccessObject {
         * Add the resulting error code to the errors array
         *
         * @param array $errors List of current errors
-        * @param array $result Result of errors
+        * @param array|string|MessageSpecifier|false $result Result of errors
         *
         * @return array List of errors
         */
index ebdbc42..d9e185e 100644 (file)
@@ -60,7 +60,7 @@ class TrackingCategories {
 
        /**
         * Read the global and extract title objects from the corresponding messages
-        * @return array Array( 'msg' => Title, 'cats' => Title[] )
+        * @return array [ 'msg' => Title, 'cats' => Title[] ]
         */
        public function getTrackingCategories() {
                $categories = array_merge(
index 76d94b2..6593e49 100644 (file)
@@ -1140,7 +1140,7 @@ HTML;
        /**
         * Parse the Accept-Language header sent by the client into an array
         *
-        * @return array Array( languageCode => q-value ) sorted by q-value in
+        * @return array [ languageCode => q-value ] sorted by q-value in
         *   descending order then appearing time in the header in ascending order.
         * May contain the "language" '*', which applies to languages other than those explicitly listed.
         * This is aligned with rfc2616 section 14.4
index 538b0a1..b1d5a50 100644 (file)
@@ -142,6 +142,7 @@ class HistoryAction extends FormlessAction {
 
        /**
         * Print the history page for an article.
+        * @return string|null
         */
        function onView() {
                $out = $this->getOutput();
@@ -151,7 +152,7 @@ class HistoryAction extends FormlessAction {
                 * Allow client caching.
                 */
                if ( $out->checkLastModified( $this->page->getTouched() ) ) {
-                       return; // Client cache fresh and headers sent, nothing more to do.
+                       return null; // Client cache fresh and headers sent, nothing more to do.
                }
 
                $this->preCacheMessages();
@@ -185,7 +186,7 @@ class HistoryAction extends FormlessAction {
                $feedType = $request->getRawVal( 'feed' );
                if ( $feedType !== null ) {
                        $this->feed( $feedType );
-                       return;
+                       return null;
                }
 
                $this->addHelpLink(
@@ -216,7 +217,7 @@ class HistoryAction extends FormlessAction {
                                ]
                        );
 
-                       return;
+                       return null;
                }
 
                $ts = $this->getTimestampFromRequest( $request );
@@ -300,6 +301,8 @@ class HistoryAction extends FormlessAction {
                        $pager->getNavigationBar()
                );
                $out->preventClickjacking( $pager->getPreventClickjacking() );
+
+               return null;
        }
 
        /**
index e91863a..f8ba08c 100644 (file)
@@ -279,11 +279,13 @@ class InfoAction extends FormlessAction {
                // Language in which the page content is (supposed to be) written
                $pageLang = $title->getPageLanguage()->getCode();
 
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                $pageLangHtml = $pageLang . ' - ' .
                        Language::fetchLanguageName( $pageLang, $lang->getCode() );
                // Link to Special:PageLanguage with pre-filled page title if user has permissions
                if ( $config->get( 'PageLanguageUseDB' )
-                       && $title->userCan( 'pagelang', $user )
+                       && $permissionManager->userCan( 'pagelang', $user, $title )
                ) {
                        $pageLangHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
                                SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
@@ -300,7 +302,7 @@ class InfoAction extends FormlessAction {
                $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
                // If the user can change it, add a link to Special:ChangeContentModel
                if ( $config->get( 'ContentHandlerUseDB' )
-                       && $title->userCan( 'editcontentmodel', $user )
+                       && $permissionManager->userCan( 'editcontentmodel', $user, $title )
                ) {
                        $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
                                SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
index e9de846..41cd24e 100644 (file)
@@ -336,8 +336,14 @@ class McrUndoAction extends FormAction {
                        $updater->setOriginalRevisionId( false );
                        $updater->setUndidRevisionId( $this->undo );
 
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                        // TODO: Ugh.
-                       if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $this->getUser() ) ) {
+                       if ( $wgUseRCPatrol && $permissionManager->userCan(
+                               'autopatrol',
+                               $this->getUser(),
+                               $this->getTitle() )
+                       ) {
                                $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
                        }
 
index 505c9d5..f6c4472 100644 (file)
@@ -50,6 +50,7 @@ class RawAction extends FormlessAction {
 
        /**
         * @suppress SecurityCheck-XSS Non html mime type
+        * @return string|null
         */
        function onView() {
                $this->getOutput()->disable();
@@ -58,11 +59,11 @@ class RawAction extends FormlessAction {
                $config = $this->context->getConfig();
 
                if ( !$request->checkUrlExtension() ) {
-                       return;
+                       return null;
                }
 
                if ( $this->getOutput()->checkLastModified( $this->page->getTouched() ) ) {
-                       return; // Client cache fresh and headers sent, nothing more to do.
+                       return null; // Client cache fresh and headers sent, nothing more to do.
                }
 
                $contentType = $this->getContentType();
@@ -87,9 +88,6 @@ class RawAction extends FormlessAction {
 
                // Set standard Vary headers so cache varies on cookies and such (T125283)
                $response->header( $this->getOutput()->getVaryHeader() );
-               if ( $config->get( 'UseKeyHeader' ) ) {
-                       $response->header( $this->getOutput()->getKeyHeader() );
-               }
 
                // Output may contain user-specific data;
                // vary generated content for open sessions on private wikis
@@ -173,6 +171,8 @@ class RawAction extends FormlessAction {
                }
 
                echo $text;
+
+               return null;
        }
 
        /**
index 6271128..f53d2b9 100644 (file)
@@ -54,7 +54,7 @@ class ApiCSPReport extends ApiBase {
                        // XXX Is it ok to put untrusted data into log??
                        'csp-report' => $report,
                        'method' => __METHOD__,
-                       'user' => $this->getUser()->getName(),
+                       'user_id' => $this->getUser()->getId() || 'logged-out',
                        'user-agent' => $userAgent,
                        'source' => $this->getParameter( 'source' ),
                ] );
@@ -104,11 +104,11 @@ class ApiCSPReport extends ApiBase {
                        ) ||
                        (
                                isset( $report['blocked-uri'] ) &&
-                               isset( $falsePositives[$report['blocked-uri']] )
+                               $this->matchUrlPattern( $report['blocked-uri'], $falsePositives )
                        ) ||
                        (
                                isset( $report['source-file'] ) &&
-                               isset( $falsePositives[$report['source-file']] )
+                               $this->matchUrlPattern( $report['source-file'], $falsePositives )
                        )
                ) {
                        // False positive due to:
@@ -119,6 +119,39 @@ class ApiCSPReport extends ApiBase {
                return $flags;
        }
 
+       /**
+        * @param string $url
+        * @param string[] $patterns
+        * @return bool
+        */
+       private function matchUrlPattern( $url, array $patterns ) {
+               if ( isset( $patterns[ $url ] ) ) {
+                       return true;
+               }
+
+               $bits = wfParseUrl( $url );
+               unset( $bits['user'], $bits['pass'], $bits['query'], $bits['fragment'] );
+               $bits['path'] = '';
+               $serverUrl = wfAssembleUrl( $bits );
+               if ( isset( $patterns[$serverUrl] ) ) {
+                       // The origin of the url matches a pattern,
+                       // e.g. "https://example.org" matches "https://example.org/foo/b?a#r"
+                       return true;
+               }
+               foreach ( $patterns as $pattern => $val ) {
+                       // We only use this pattern if it ends in a slash, this prevents
+                       // "/foos" from matching "/foo", and "https://good.combo.bad" matching
+                       // "https://good.com".
+                       if ( substr( $pattern, -1 ) === '/' && strpos( $url, $pattern ) === 0 ) {
+                               // The pattern starts with the same as the url
+                               // e.g. "https://example.org/foo/" matches "https://example.org/foo/b?a#r"
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * Output an api error if post body is obviously not OK.
         */
@@ -176,15 +209,32 @@ class ApiCSPReport extends ApiBase {
                        $flagText = '[' . implode( ', ', $flags ) . ']';
                }
 
-               $blockedFile = $report['blocked-uri'] ?? 'n/a';
+               $blockedOrigin = isset( $report['blocked-uri'] )
+                       ? $this->originFromUrl( $report['blocked-uri'] )
+                       : 'n/a';
                $page = $report['document-uri'] ?? 'n/a';
-               $line = isset( $report['line-number'] ) ? ':' . $report['line-number'] : '';
+               $line = isset( $report['line-number'] )
+                       ? ':' . $report['line-number']
+                       : '';
                $warningText = $flagText .
-                       ' Received CSP report: <' . $blockedFile .
-                       '> blocked from being loaded on <' . $page . '>' . $line;
+                       ' Received CSP report: <' . $blockedOrigin . '>' .
+                       ' blocked from being loaded on <' . $page . '>' . $line;
                return $warningText;
        }
 
+       /**
+        * @param string $url
+        * @return string
+        */
+       private function originFromUrl( $url ) {
+               $bits = wfParseUrl( $url );
+               unset( $bits['user'], $bits['pass'], $bits['query'], $bits['fragment'] );
+               $bits['path'] = '';
+               $serverUrl = wfAssembleUrl( $bits );
+               // e.g. "https://example.org" from "https://example.org/foo/b?a#r"
+               return $serverUrl;
+       }
+
        /**
         * Stop processing the request, and output/log an error
         *
index c3c5318..de5257e 100644 (file)
@@ -287,7 +287,7 @@ class ApiLogin extends ApiBase {
                ];
                if ( $response->message ) {
                        $ret['message'] = $response->message->inLanguage( 'en' )->plain();
-               };
+               }
                $reqs = [
                        'neededRequests' => $response->neededRequests,
                        'createRequest' => $response->createRequest,
index b845c57..a77136d 100644 (file)
@@ -916,32 +916,20 @@ class ApiMain extends ApiBase {
                        return;
                }
 
-               $useKeyHeader = $config->get( 'UseKeyHeader' );
                if ( $this->mCacheMode == 'anon-public-user-private' ) {
                        $out->addVaryHeader( 'Cookie' );
                        $response->header( $out->getVaryHeader() );
-                       if ( $useKeyHeader ) {
-                               $response->header( $out->getKeyHeader() );
-                               if ( $out->haveCacheVaryCookies() ) {
-                                       // Logged in, mark this request private
-                                       $response->header( "Cache-Control: $privateCache" );
-                                       return;
-                               }
-                               // Logged out, send normal public headers below
-                       } elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
+                       if ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
                                // Logged in or otherwise has session (e.g. anonymous users who have edited)
                                // Mark request private
                                $response->header( "Cache-Control: $privateCache" );
 
                                return;
-                       } // else no Key and anonymous, send public headers below
+                       } // else anonymous, send public headers below
                }
 
                // Send public headers
                $response->header( $out->getVaryHeader() );
-               if ( $useKeyHeader ) {
-                       $response->header( $out->getKeyHeader() );
-               }
 
                // If nobody called setCacheMaxAge(), use the (s)maxage parameters
                if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
@@ -1629,24 +1617,17 @@ class ApiMain extends ApiBase {
         */
        protected function logRequest( $time, $e = null ) {
                $request = $this->getRequest();
-               $legacyLogCtx = [
-                       'ts' => time(),
-                       'ip' => $request->getIP(),
-                       'userAgent' => $this->getUserAgent(),
-                       'wiki' => WikiMap::getCurrentWikiDbDomain()->getId(),
-                       'timeSpentBackend' => (int)round( $time * 1000 ),
-                       'hadError' => $e !== null,
-                       'errorCodes' => [],
-                       'params' => [],
-               ];
 
                $logCtx = [
+                       // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
                        '$schema' => '/mediawiki/api/request/0.0.1',
                        'meta' => [
                                'request_id' => WebRequest::getRequestId(),
-                               'id' => UIDGenerator::newUUIDv1(),
+                               'id' => UIDGenerator::newUUIDv4(),
                                'dt' => wfTimestamp( TS_ISO_8601 ),
                                'domain' => $this->getConfig()->get( 'ServerName' ),
+                               // If using the EventBus extension (as intended) with this log channel,
+                               // this stream name will map to a Kafka topic.
                                'stream' => 'mediawiki.api-request'
                        ],
                        'http' => [
@@ -1669,7 +1650,6 @@ class ApiMain extends ApiBase {
                if ( $e ) {
                        $logCtx['api_error_codes'] = [];
                        foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
-                               $legacyLogCtx['errorCodes'][] = $msg->getApiCode();
                                $logCtx['api_error_codes'][] = $msg->getApiCode();
                        }
                }
@@ -1677,8 +1657,8 @@ class ApiMain extends ApiBase {
                // Construct space separated message for 'api' log channel
                $msg = "API {$request->getMethod()} " .
                        wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
-                       " {$legacyLogCtx['ip']} " .
-                       "T={$legacyLogCtx['timeSpentBackend']}ms";
+                       " {$logCtx['http']['client_ip']} " .
+                       "T={$logCtx['backend_time_ms']}ms";
 
                $sensitive = array_flip( $this->getSensitiveParams() );
                foreach ( $this->getParamsUsed() as $name ) {
@@ -1697,16 +1677,14 @@ class ApiMain extends ApiBase {
                                $encValue = $this->encodeRequestLogValue( $value );
                        }
 
-                       $legacyLogCtx['params'][$name] = $value;
                        $logCtx['params'][$name] = $value;
                        $msg .= " {$name}={$encValue}";
                }
 
+               // Log an unstructured message to the api channel.
                wfDebugLog( 'api', $msg, 'private' );
-               // ApiAction channel is for structured data consumers.
-               // The ApiAction was using logging channel is deprecated and is replaced
-               // by the api-request channel.
-               wfDebugLog( 'ApiAction', '', 'private', $legacyLogCtx );
+
+               // The api-request channel a structured data log channel.
                wfDebugLog( 'api-request', '', 'private', $logCtx );
        }
 
index 64c6f45..6b24b63 100644 (file)
@@ -20,7 +20,7 @@
  * @file
  */
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -735,7 +735,7 @@ class ApiPageSet extends ApiBase {
         * $this->getPageTableFields().
         *
         * @param IDatabase $db
-        * @param ResultWrapper $queryResult
+        * @param IResultWrapper $queryResult
         */
        public function populateFromQueryResult( $db, $queryResult ) {
                $this->initFromQueryResult( $queryResult );
@@ -854,7 +854,7 @@ class ApiPageSet extends ApiBase {
        /**
         * Iterate through the result of the query on 'page' table,
         * and for each row create and store title object and save any extra fields requested.
-        * @param ResultWrapper $res DB Query result
+        * @param IResultWrapper $res DB Query result
         * @param array $remaining Array of either pageID or ns/title elements (optional).
         *        If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
         * @param bool $processTitles Must be provided together with $remaining.
index 47ff0fb..ec432d8 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 
 /**
  * This is a base class for all Query modules.
@@ -252,7 +252,7 @@ abstract class ApiQueryBase extends ApiBase {
        }
 
        /**
-        * Equivalent to addWhere(array($field => $value))
+        * Equivalent to addWhere( [ $field => $value ] )
         * @param string $field Field name
         * @param string|string[] $value Value; ignored if null or empty array
         */
@@ -368,7 +368,7 @@ abstract class ApiQueryBase extends ApiBase {
         * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
         *  ApiQueryBaseAfterQuery hooks will be called, and the
         *  ApiQueryBaseProcessRow hook will be expected.
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        protected function select( $method, $extraQuery = [], array &$hookData = null ) {
                $tables = array_merge(
index 72b59b0..1c2d490 100644 (file)
@@ -233,7 +233,7 @@ class ApiQueryLanguageinfo extends ApiQueryBase {
                return [
                        "$pathUrl"
                                => "apihelp-$pathMsg-example-simple",
-                       "$pathUrl&{$prefix}prop=autonym|name&lang=de"
+                       "$pathUrl&{$prefix}prop=autonym|name&uselang=de"
                                => "apihelp-$pathMsg-example-autonym-name-de",
                        "$pathUrl&{$prefix}prop=fallbacks|variants&{$prefix}code=oc"
                                => "apihelp-$pathMsg-example-fallbacks-variants-oc",
index eadf3f7..8d7aaa3 100644 (file)
        "api-help-param-templated-var-first": "يجب استبدال <var>&#x7B;$1&#x7D;</var> في اسم الوسيط بقيم <var>$2</var>",
        "api-help-param-templated-var": "<var>&#x7B;$1&#x7D;</var> بقيم <var>$2</var>",
        "api-help-datatypes-header": "أنواع البيانات",
-       "api-help-datatypes": "Ù\8aجب Ø£Ù\86 Ù\8aÙ\83Ù\88Ù\86 Ø§Ù\84إدخاÙ\84 Ø¥Ù\84Ù\89 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a Ù\87Ù\88 UTF-8 Ø§Ù\84Ù\85عÙ\8aÙ\8eÙ\91Ù\86 Ù\84Ù\80NFCØ\8c Ù\82د Ù\8aحاÙ\88Ù\84 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a ØªØ­Ù\88Ù\8aÙ\84 Ù\85دخÙ\84ات Ø£Ø®Ø±Ù\89Ø\8c Ù\88Ù\84Ù\83Ù\86 Ù\82د Ù\8aتسبب Ø°Ù\84Ù\83 Ù\81Ù\8a Ù\81Ø´Ù\84 Ø¨Ø¹Ø¶ Ø§Ù\84عÙ\85Ù\84Ù\8aات (Ù\85Ø«Ù\84 [[Special:ApiHelp/edit|اÙ\84تعدÙ\8aÙ\84ات]] Ù\85ع Ø¹Ù\85Ù\84Ù\8aات Ù\81حص MD5).\n\nتحتاج Ø¨Ø¹Ø¶ Ø£Ù\86Ù\88اع Ø§Ù\84Ù\88سائط Ù\81Ù\8a Ø·Ù\84بات API Ø¥Ù\84Ù\89 Ù\85زÙ\8aد Ù\85Ù\86 Ø§Ù\84شرح:\n;Ù\85Ù\86Ø·Ù\82Ù\8aØ©\n:تعÙ\85Ù\84 Ø§Ù\84Ù\88سائط Ø§Ù\84Ù\85Ù\86Ø·Ù\82Ù\8aØ© Ù\85Ø«Ù\84 ØµÙ\86ادÙ\8aÙ\82 Ø§Ø®ØªÙ\8aار HTML: Ø¥Ø°Ø§ ØªÙ\85 ØªØ­Ø¯Ù\8aد Ø§Ù\84Ù\88سÙ\8aØ·Ø\8c Ø¨ØºØ¶ Ø§Ù\84Ù\86ظر Ø¹Ù\86 Ø§Ù\84Ù\82Ù\8aÙ\85Ø©Ø\8c Ù\81Ù\8aتÙ\85 Ø§Ø¹ØªØ¨Ø§Ø±Ù\87 ØµØ­Ù\8aحاØ\8c Ù\84Ù\84حصÙ\88Ù\84 Ø¹Ù\84Ù\89 Ù\82Ù\8aÙ\85Ø© Ø®Ø§Ø·Ø¦Ø©; Ø§Ø­Ø°Ù\81 Ø§Ù\84Ù\88سÙ\8aØ· ØªÙ\85اÙ\85ا.\n;اÙ\84طابع Ø§Ù\84زÙ\85Ù\86Ù\8a\n:Ù\82د Ù\8aتÙ\85 ØªØ­Ø¯Ù\8aد Ø§Ù\84Ø·Ù\88ابع Ø§Ù\84زÙ\85Ù\86Ù\8aØ© Ø¨ØªÙ\86سÙ\8aÙ\82ات Ù\85تعددةØ\8c Ù\8aÙ\8fÙ\88صÙ\8eÙ\89 Ø¨Ù\80ISO 8601 Ø§Ù\84تارÙ\8aØ® Ù\88اÙ\84Ù\88Ù\82تØ\8c Ø¬Ù\85Ù\8aع Ø§Ù\84Ø£Ù\88Ù\82ات Ø¨Ø§Ù\84تÙ\88Ù\82Ù\8aت Ø§Ù\84عاÙ\84Ù\85Ù\8a Ø§Ù\84Ù\85Ù\86سÙ\82Ø\8c Ù\8aتÙ\85 ØªØ¬Ø§Ù\87Ù\84 Ø£Ù\8aØ© Ù\85Ù\86Ø·Ù\82Ø© Ø²Ù\85Ù\86Ù\8aØ© Ù\85ضÙ\85Ù\86Ø©.\n:* ØªØ§Ø±Ù\8aØ® Ù\88Ù\88Ù\82ت ISO 8601Ø\8c <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (عÙ\84اÙ\85ات Ø§Ù\84ترÙ\82Ù\8aÙ\85 Ù\88<kbd>Z</kbd> Ø§Ø®ØªÙ\8aارÙ\8aØ©)\n:* ØªØ§Ø±Ù\8aØ® Ù\88Ù\88Ù\82ت ISO 8601Ù\85ع Ø§Ù\84Ø«Ù\88اÙ\86Ù\8a Ø§Ù\84Ù\85جزأة (Ù\85تجاÙ\87Ù\84Ø©)Ø\8c <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd>  (اÙ\84شرطاتØ\8c Ù\88اÙ\84Ù\86Ù\82طتاÙ\86 Ø§Ù\84رأسÙ\8aتاÙ\86 Ø§Ø®ØªÙ\8aارÙ\8aØ© Ù\88<kbd>Z</kbd>)\n:* ØªÙ\86سÙ\8aÙ\82 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8aØ\8c <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* ØªÙ\86سÙ\8aÙ\82 Ø±Ù\82Ù\85Ù\8a Ø¹Ø§Ù\85Ø\8c (تÙ\88Ù\82Ù\8aت Ø§Ø®ØªÙ\8aارÙ\8aØ\8c Ø£Ù\88 Ù\8aتÙ\85 ØªØ¬Ø§Ù\87Ù\84) <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (Ù\85Ù\86Ø·Ù\82Ø© Ø²Ù\85Ù\86Ù\8aØ© Ø§Ø®ØªÙ\8aارÙ\8aØ© Ù\84Ù\80<kbd>GMT</kbd>Ø\8c <kbd>+<var>##</var></kbd>Ø\8c Ø£Ù\88 Ù\8aتÙ\85 ØªØ¬Ø§Ù\87Ù\84 <kbd>-<var>##</var></kbd>)\n:* ØªÙ\86سÙ\8aÙ\82 EXIFØ\8c <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*تÙ\86سÙ\8aÙ\82 RFC 2822 (Ù\82د Ù\8aتÙ\85 Ø­Ø°Ù\81 Ø§Ù\84Ù\85Ù\86Ø·Ù\82Ø© Ø§Ù\84زÙ\85Ù\86Ù\8aØ©)Ø\8c <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*تÙ\86سÙ\8aÙ\82 RFC 850 format (Ù\82د Ù\8aتÙ\85 Ø­Ø°Ù\81 Ø§Ù\84Ù\85Ù\86Ø·Ù\82Ø© Ø§Ù\84زÙ\85Ù\86Ù\8aØ©)Ø\8c <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ØªÙ\86سÙ\8aÙ\82 C ctime format, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Ø§Ù\84Ø«Ù\88اÙ\86Ù\8a Ù\85Ù\86Ø° 1970-01-01T00:00:00Z Ù\83عدد ØµØ­Ù\8aØ­ Ù\8aتراÙ\88Ø­ Ø¨Ù\8aÙ\86 1 Ù\8813 (باستثÙ\86اء <kbd>0</kbd>)\n:* Ø§Ù\84سÙ\84سÙ\84Ø© <kbd>now</kbd>\n;فاصل بديل متعدد القيم\n:يتم عادةً إرسال الوسائط التي تأخذ قيم متعددة مع القيم المفصولة باستخدام حرف الأنبوب، على سبيل المثال <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd> إذا كانت القيمة يجب أن تحتوي على حرف الأنبوب، فاستخدم U+001F (فاصل الوحدة) مثل الفاصل ''و'' بادئة القيمة بـU+001F، على سبيل المثال <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
+       "api-help-datatypes": "Ù\8aجب Ø£Ù\86 Ù\8aÙ\83Ù\88Ù\86 Ø§Ù\84إدخاÙ\84 Ø¥Ù\84Ù\89 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a Ù\87Ù\88 UTF-8 Ø§Ù\84Ù\85عÙ\8aÙ\8eÙ\91Ù\86 Ù\84Ù\80NFCØ\8c Ù\82د Ù\8aحاÙ\88Ù\84 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a ØªØ­Ù\88Ù\8aÙ\84 Ù\85دخÙ\84ات Ø£Ø®Ø±Ù\89Ø\8c Ù\88Ù\84Ù\83Ù\86 Ù\82د Ù\8aتسبب Ø°Ù\84Ù\83 Ù\81Ù\8a Ù\81Ø´Ù\84 Ø¨Ø¹Ø¶ Ø§Ù\84عÙ\85Ù\84Ù\8aات (Ù\85Ø«Ù\84 [[Special:ApiHelp/edit|اÙ\84تعدÙ\8aÙ\84ات]] Ù\85ع Ø¹Ù\85Ù\84Ù\8aات Ù\81حص MD5).\n\nتحتاج Ø¨Ø¹Ø¶ Ø£Ù\86Ù\88اع Ø§Ù\84Ù\88سائط Ù\81Ù\8a Ø·Ù\84بات API Ø¥Ù\84Ù\89 Ù\85زÙ\8aد Ù\85Ù\86 Ø§Ù\84شرح:\n;Ù\85Ù\86Ø·Ù\82Ù\8aØ©\n:تعÙ\85Ù\84 Ø§Ù\84Ù\88سائط Ø§Ù\84Ù\85Ù\86Ø·Ù\82Ù\8aØ© Ù\85Ø«Ù\84 ØµÙ\86ادÙ\8aÙ\82 Ø§Ø®ØªÙ\8aار HTML: Ø¥Ø°Ø§ ØªÙ\85 ØªØ­Ø¯Ù\8aد Ø§Ù\84Ù\88سÙ\8aØ·Ø\8c Ø¨ØºØ¶ Ø§Ù\84Ù\86ظر Ø¹Ù\86 Ø§Ù\84Ù\82Ù\8aÙ\85Ø©Ø\8c Ù\81Ù\8aتÙ\85 Ø§Ø¹ØªØ¨Ø§Ø±Ù\87 ØµØ­Ù\8aحاØ\8c Ù\84Ù\84حصÙ\88Ù\84 Ø¹Ù\84Ù\89 Ù\82Ù\8aÙ\85Ø© Ø®Ø§Ø·Ø¦Ø©; Ø§Ø­Ø°Ù\81 Ø§Ù\84Ù\88سÙ\8aØ· ØªÙ\85اÙ\85ا.\n;اÙ\84طابع Ø§Ù\84زÙ\85Ù\86Ù\8a\n:Ù\8aÙ\85Ù\83Ù\86 ØªØ­Ø¯Ù\8aد Ø§Ù\84Ø·Ù\88ابع Ø§Ù\84زÙ\85Ù\86Ù\8aØ© Ø¨ØªÙ\86سÙ\8aÙ\82ات Ù\85تعددةØ\8c Ø±Ø§Ø¬Ø¹ [[mw:Special:MyLanguage/Timestamp|تÙ\86سÙ\8aÙ\82ات Ø¥Ø¯Ø®Ø§Ù\84 Ù\85Ù\83تبة Ø§Ù\84طابع Ø§Ù\84زÙ\85Ù\86Ù\8a Ø§Ù\84Ù\85Ù\88Ø«Ù\82Ø© Ø¹Ù\84Ù\89 mediawiki.org]] Ù\84Ù\84تÙ\81اصÙ\8aÙ\84Ø\8c Ù\8aÙ\8fÙ\88صÙ\8eÙ\89 Ø¨ØªØ§Ø±Ù\8aØ® ISO 8601 Ù\88Ù\88Ù\82ت: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>Ø\8c Ø¨Ø§Ù\84إضاÙ\81Ø© Ø¥Ù\84Ù\89 Ø°Ù\84Ù\83Ø\8c Ù\8aÙ\85Ù\83Ù\86 Ø§Ø³ØªØ®Ø¯Ø§Ù\85 Ø§Ù\84سÙ\84سÙ\84Ø© <kbd>now</kbd> Ù\84تحدÙ\8aد Ø§Ù\84طابع Ø§Ù\84زÙ\85Ù\86Ù\8a Ø§Ù\84حاÙ\84Ù\8a.\n;فاصل بديل متعدد القيم\n:يتم عادةً إرسال الوسائط التي تأخذ قيم متعددة مع القيم المفصولة باستخدام حرف الأنبوب، على سبيل المثال <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd> إذا كانت القيمة يجب أن تحتوي على حرف الأنبوب، فاستخدم U+001F (فاصل الوحدة) مثل الفاصل ''و'' بادئة القيمة بـU+001F، على سبيل المثال <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
        "api-help-templatedparams-header": "وسائط القالب",
        "api-help-templatedparams": "تدعم وسائط القوالب الحالات التي تحتاج فيها API إلى قيمة لكل قيمة من وسيط آخر، على سبيل المثال، إذا كانت هناك وحدة API لطلب الفاكهة، فإنه قد يكون لديك وسيط <var>fruits</var>  لتحديد أي الفواكه تم طلبها ووسيط قالب <var>{fruit}-quantity</var>لتحديد عدد الفواكه لكل طلب، يمكن لعميل API الذي يريد 1 تفاحة، 5 موز، 20 فراولة ثم تقديم طلب مثل <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>.",
        "api-help-param-type-limit": "النوع: عدد صحيح أو <kbd>max</kbd>",
index b0dd49a..9843af4 100644 (file)
        "api-help-param-templated-var-first": "<var>&#x7B;$1&#x7D;</var> in the parameter's name should be replaced with values of <var>$2</var>",
        "api-help-param-templated-var": "<var>&#x7B;$1&#x7D;</var> with values of <var>$2</var>",
        "api-help-datatypes-header": "Data types",
-       "api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats. ISO 8601 date and time is recommended. All times are in UTC, any included timezone is ignored.\n:* ISO 8601 date and time, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (punctuation and <kbd>Z</kbd> are optional)\n:* ISO 8601 date and time with (ignored) fractional seconds, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (dashes, colons, and <kbd>Z</kbd> are optional)\n:* MediaWiki format, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Generic numeric format, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (optional timezone of <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, or <kbd>-<var>##</var></kbd> is ignored)\n:* EXIF format, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*RFC 2822 format (timezone may be omitted), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 format (timezone may be omitted), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime format, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Seconds since 1970-01-01T00:00:00Z as a 1 to 13 digit integer (excluding <kbd>0</kbd>)\n:* The string <kbd>now</kbd>\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
+       "api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats, see [[mw:Special:MyLanguage/Timestamp|the Timestamp library input formats documented on mediawiki.org]] for details. ISO 8601 date and time is recommended: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>. Additionally, the string <kbd>now</kbd> may be used to specify the current timestamp.\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
        "api-help-templatedparams-header": "Templated parameters",
        "api-help-templatedparams": "Templated parameters support cases where an API module needs a value for each value of some other parameter. For example, if there were an API module to request fruit, it might have a parameter <var>fruits</var> to specify which fruits are being requested and a templated parameter <var>{fruit}-quantity</var> to specify how many of each fruit to request. An API client that wants 1 apple, 5 bananas, and 20 strawberries could then make a request like <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>.",
        "api-help-param-type-limit": "Type: integer or <kbd>max</kbd>",
index d4ffd44..af345d5 100644 (file)
        "apihelp-query+langlinks-param-dir": "La dirección en que ordenar la lista.",
        "apihelp-query+langlinks-param-inlanguagecode": "Código de idioma para los nombres de idiomas localizados.",
        "apihelp-query+langlinks-example-simple": "Obtener los enlaces interlingüísticos de la página <kbd>Main Page</kbd>.",
+       "apihelp-query+languageinfo-summary": "Devolver información sobre los idiomas disponibles.",
+       "apihelp-query+languageinfo-paramvalue-prop-code": "El código lingüístico (es específico de MediaWiki, pero existen coincidencias con otras normas.)",
+       "apihelp-query+languageinfo-paramvalue-prop-dir": "La dirección de escritura del idioma (bien <code>ltr</code> o bien <code>rtl</code>).",
+       "apihelp-query+languageinfo-example-autonym-name-de": "Obtener los endónimos y los nombres alemanes de todos los idiomas compatibles.",
+       "apihelp-query+languageinfo-example-fallbacks-variants-oc": "Obtener los idiomas de reserva y las variantes del occitano.",
+       "apihelp-query+languageinfo-example-bcp47-dir": "Obtener el código lingüístico BCP-47 y la dirección de todos los idiomas compatibles.",
        "apihelp-query+links-summary": "Devuelve todos los enlaces de las páginas dadas.",
        "apihelp-query+links-param-namespace": "Mostrar solo los enlaces en estos espacios de nombres.",
        "apihelp-query+links-param-limit": "Cuántos enlaces se devolverán.",
index 993e75c..b645a43 100644 (file)
@@ -14,7 +14,8 @@
                        "Huji",
                        "Ladsgroup",
                        "Freshman404",
-                       "Alifakoor"
+                       "Alifakoor",
+                       "FarsiNevis"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|مستندات]]\n* [[mw:API:FAQ|پرسش‌های متداول]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api فهرست پست الکترونیکی]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce اعلانات رابط برنامه‌نویسی کاربردی]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R ایرادها و درخواست‌ها]\n</div>\n\n<strong>وضعیت:</strong> تمام ویژگی‌هایی که در این صفحه نمایش یافته‌اند باید کار بکنند، ولی رابط برنامه‌نویسی کاربردی کماکان در حال توسعه است، و ممکن است در هر زمان تغییر بکند. به عضویت [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ فهرست پست الکترونیکی mediawiki-api-announce] در بیایید تا از تغییرات باخبر شوید.\n\n<strong>درخواست‌های معیوب:</strong> وقتی درخواست‌های معیوب به رابط برنامه‌نویسی کاربردی فرستاده شوند، یک سرایند اچ‌تی‌تی‌پی با کلید «MediaWiki-API-Erorr» فرستاده می‌شود و بعد هم مقدار سرایند و هم کد خطای بازگردانده شده  هر دو به یک مقدار نسبت داده می‌شوند. برای اطلاعات بیشتر [[mw:API:Errors_and_warnings|API: Errors and warnings]] را ببینید.\n\n<strong>آزمایش:</strong> برای انجام درخواست‌های API آزمایشی [[Special:ApiSandbox]] را ببینید.",
@@ -52,6 +53,8 @@
        "apihelp-compare-param-fromtitle": "عنوان اول برای مقایسه.",
        "apihelp-compare-param-fromid": "شناسه صفحه اول برای مقایسه.",
        "apihelp-compare-param-fromrev": "نسخه اول برای مقایسه.",
+       "apihelp-compare-param-fromcontentmodel": "<kbd>fromslots=main</kbd> را تعیین کنید و در عوض، <var>fromcontentmodel-main</var> را به کار ببر.",
+       "apihelp-compare-param-fromcontentformat": "<kbd>fromslots=main</kbd> را تعیین کن و در عوض، <var>fromcontentformat-main</var> را به کار ببر.",
        "apihelp-compare-param-totitle": "عنوان دوم برای مقایسه.",
        "apihelp-compare-param-toid": "شناسه صفحه دوم برای مقایسه.",
        "apihelp-compare-param-torev": "نسخه دوم برای مقایسه.",
@@ -75,9 +78,9 @@
        "apihelp-edit-param-sectiontitle": "عنوان برای بخش جدید.",
        "apihelp-edit-param-text": "محتوای صفحه.",
        "apihelp-edit-param-summary": "خلاصه را ویرایش کنید. همچنین عنوان بخش را زمانی که $1section=تازه و $1sectiontitle تنظیم نشده‌است.",
-       "apihelp-edit-param-minor": "ویرایش جزئی.",
+       "apihelp-edit-param-minor": "این ویرایش را به‌عنوان «ویرایش جزئی» نشانه‌گذاری کن.",
        "apihelp-edit-param-notminor": "ویرایش غیر جزئی.",
-       "apihelp-edit-param-bot": "عÙ\84اÙ\85ت Ø²Ø¯Ù\86 Ø§Û\8cÙ\86 Ù\88Û\8cراÛ\8cØ´ Ø¨Ù\87 Ø¹Ù\86Ù\88اÙ\86 Ù\88Û\8cراÛ\8cØ´ Ø±Ø¨Ø§Øª.",
+       "apihelp-edit-param-bot": "اÛ\8cÙ\86 Ù\88Û\8cراÛ\8cØ´ Ø±Ø§ Ø¨Ù\87â\80\8cعÙ\86Ù\88اÙ\86 Â«Ù\88Û\8cراÛ\8cØ´ Ø±Ø¨Ø§ØªÂ» Ù\86شاÙ\86Ù\87â\80\8cگذارÛ\8c Ú©Ù\86.",
        "apihelp-edit-param-createonly": "اگر صفحه موجود بود، ویرایش نکن.",
        "apihelp-edit-param-nocreate": "رها کردن خطا در صورتی که صفحه وجود ندارد.",
        "apihelp-edit-param-watch": "افزودن صفحه به فهرست پی‌گیری شما",
        "apihelp-feedcontributions-param-deletedonly": "فقط مشارکت‌های حذف شده نمایش داده شود.",
        "apihelp-feedcontributions-param-toponly": "فقط ویرایش‌هایی که آخرین نسخه‌اند نمایش داده شود.",
        "apihelp-feedcontributions-param-newonly": "فقط نمایش ویرایش‌هایی که تولید‌های صفحه هستند.",
+       "apihelp-feedcontributions-param-hideminor": "ویرایش‌های جزئی را پنهان کن.",
        "apihelp-feedcontributions-param-showsizediff": "نمایش تفاوت حجم تغییرات بین نسخه‌ها.",
        "apihelp-feedcontributions-example-simple": "مشارکت‌های [[کاربر:نمونه]] را برگردان",
        "apihelp-feedrecentchanges-summary": "خوراک تغییرات اخیر را برمی‌گرداند.",
        "apihelp-logout-summary": "خروج به همراه پاک نمودن اطلاعات این نشست",
        "apihelp-logout-example-logout": "خروج کاربر فعلی",
        "apihelp-mergehistory-summary": "ادغام تاریخچه صفحات",
+       "apihelp-mergehistory-param-reason": "علت ادغام تاریخچه",
+       "apihelp-mergehistory-example-merge": "کلّ تاریخچهٔ <kbd>Oldpage</kbd> را در <kbd>Newpage</kbd> ادغام کن.",
        "apihelp-move-summary": "انتقال صفحه",
        "apihelp-move-param-to": "عنوانی که قصد دارید صفحه را به آن نام تغییر دهید.",
        "apihelp-move-param-reason": "دلیل انتقال",
        "apihelp-options-param-reset": "ترجیحات را به مقادیر پیش فرض سایت بازمی گرداند.",
        "apihelp-options-example-reset": "بازنشانی همه تنظیمات.",
        "apihelp-paraminfo-param-helpformat": "ساختار راهنمای رشته‌ها",
+       "apihelp-parse-param-disablepp": "به جایش از <var>$1disablelimitreport</var> استفاده کن.",
        "apihelp-parse-example-page": "تجزیه یک صفحه.",
        "apihelp-parse-example-text": "تجزیه متن ویکی.",
        "apihelp-parse-example-summary": "تجزیه خلاصه.",
        "apihelp-protect-example-protect": "محافظت از صفحه",
        "apihelp-protect-example-unprotect": "خارج ساختن صفحه از حفاظت با تغییر سطح حفاظتی به <kbd>all</kbd>.",
        "apihelp-protect-example-unprotect2": "خارج ساختن صفحه از حفاظت با قراردادن هیچ‌گونه محدودیت‌حفاظتی",
-       "apihelp-purge-param-forcelinkupdate": "بÙ\87â\80\8cرÙ\88زرساÙ\86Û\8c Ø¬Ø¯Ø§Ù\88Ù\84 پیوندها.",
-       "apihelp-purge-param-forcerecursivelinkupdate": "جدول پیوندها را به‌روز رسانی کنید، و جدول‌های پیوندهای هر صفحه‌ای را که از این صفحه به عنوان الگو استفاده می‌کند به‌روز رسانی کنید.",
+       "apihelp-purge-param-forcelinkupdate": "رÙ\88زآÙ\85دسازÛ\8c Ø¬Ø¯Ù\88Ù\84â\80\8cÙ\87اÛ\8c پیوندها.",
+       "apihelp-purge-param-forcerecursivelinkupdate": "جدول پیوندهای این صفحه و جدول پیوندهای هر صفحه‌ای را که از این صفحه به‌عنوان الگو استفاده می‌کند، روزآمدسازی کنید.",
        "apihelp-query-param-list": "کدام فهرست‌ها دریافت شود.",
        "apihelp-query-param-meta": "کدام فراداده‌ها دریافت شود.",
        "apihelp-query+allcategories-param-prefix": "عنوان همهٔ رده‌ها را که با این مقدار آغاز می‌شود جستجو کنید.",
        "apihelp-query+allcategories-param-limit": "میزان رده‌ها برای بازگرداندن.",
        "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "نمی‌تواند همراه <var>$3user</var> به کار رود.",
+       "apihelp-query+allfileusages-paramvalue-prop-title": "عنوان پرونده را درج می‌کند.",
        "apihelp-query+allfileusages-param-limit": "تعداد آیتم‌ها برای بازگرداندن.",
        "apihelp-query+allfileusages-param-dir": "جهتی که باید فهرست شود.",
        "apihelp-query+allfileusages-example-unique": "فهرست پرونده‌های با عنوان یکتا",
index 70fa9da..a7ff703 100644 (file)
@@ -14,7 +14,8 @@
                        "ネイ",
                        "Omotecho",
                        "Yusuke1109",
-                       "Suyama"
+                       "Suyama",
+                       "Yuukin0248"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|説明文書]]\n* [[mw:Special:MyLanguage/API:FAQ|よくある質問]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api メーリングリスト]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 告知]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R バグの報告とリクエスト]\n</div>\n<strong>状態:</strong> MediaWiki APIは、積極的にサポートされ、改善された成熟した安定したインターフェースです。避けようとはしていますが、時には壊れた変更が加えられるかもしれません。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n<strong>誤ったリクエスト:</strong> 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\n<p class=\"mw-apisandbox-link\"><strong>テスト:</strong> API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。</p>",
@@ -45,6 +46,8 @@
        "apihelp-block-param-watchuser": "その利用者またはIPアドレスの利用者ページとトークページをウォッチします。",
        "apihelp-block-param-tags": "ブロック記録の項目に適用する変更タグ。",
        "apihelp-block-param-partial": "サイト全体ではなく特定のページまたは名前空間での編集をブロックします。",
+       "apihelp-block-param-pagerestrictions": "利用者が編集できないようにするページのタイトルのリスト。<var>partial</var> に true が設定されている場合のみ適用します。",
+       "apihelp-block-param-namespacerestrictions": "利用者が編集できないようにする名前空間のID。<var>partial</var> に true が設定されている場合のみ適用します。",
        "apihelp-block-example-ip-simple": "IPアドレス <kbd>192.0.2.5</kbd> を <kbd>First strike<kbd> という理由で3日ブロックする",
        "apihelp-block-example-user-complex": "利用者 <kbd>Vandal</kbd> を <kbd>Vandalism</kbd> という理由で無期限ブロックし、新たなアカウント作成とメールの送信を禁止する。",
        "apihelp-changeauthenticationdata-summary": "現在の利用者の認証データを変更します。",
        "apihelp-edit-param-text": "ページの本文。",
        "apihelp-edit-param-summary": "編集の要約。$1section=new で $1sectiontitle が設定されていない場合は節名としても利用されます。",
        "apihelp-edit-param-tags": "この版に適用する変更タグ。",
-       "apihelp-edit-param-minor": "細部の編集",
+       "apihelp-edit-param-minor": "この編集に細部の変更の印を付ける",
        "apihelp-edit-param-notminor": "細部の編集ではない。",
        "apihelp-edit-param-bot": "この編集をボットの編集としてマークする。",
        "apihelp-edit-param-basetimestamp": "編集前の版のタイムスタンプ。編集競合を検出するために使用されます。\n[[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]] で取得できます。",
        "apihelp-query+tags-paramvalue-prop-description": "タグの説明を追加します。",
        "apihelp-query+tags-paramvalue-prop-hitcount": "版の記録項目の数と、このタグを持っている記録項目の数を、追加します。",
        "apihelp-query+tags-example-simple": "利用可能なタグを一覧表示する。",
-       "apihelp-query+templates-summary": "与えられたページでトランスクルードされているすべてのページを返します。",
+       "apihelp-query+templates-summary": "与えられたページで参照読み込みされているすべてのページを返します。",
        "apihelp-query+templates-param-namespace": "この名前空間のテンプレートのみ表示する。",
        "apihelp-query+templates-param-limit": "返すテンプレートの数。",
        "apihelp-query+templates-param-dir": "昇順・降順の別。",
        "apihelp-query+templates-example-simple": "<kbd>Main Page</kbd> で使用されているテンプレートを取得する。",
        "apihelp-query+templates-example-generator": "<kbd>Main Page</kbd> で使用されているテンプレートに関する情報を取得する。",
-       "apihelp-query+templates-example-namespaces": "<kbd>Main Page</kbd> でトランスクルードされている {{ns:user}} および {{ns:template}} 名前空間のページを取得する。",
+       "apihelp-query+templates-example-namespaces": "<kbd>Main Page</kbd> で参照読み込みされている {{ns:user}} および {{ns:template}} 名前空間のページを取得する。",
        "apihelp-query+tokens-summary": "データ変更操作用のトークンを取得します。",
        "apihelp-query+tokens-param-type": "リクエストするトークンの種類。",
        "apihelp-query+tokens-example-simple": "csrfトークンを取得する (既定)。",
        "apihelp-query+tokens-example-types": "ウォッチトークンおよび巡回トークンを取得する。",
-       "apihelp-query+transcludedin-summary": "与えられたページをトランスクルードしているすべてのページを検索します。",
+       "apihelp-query+transcludedin-summary": "与えられたページを参照読み込みしているすべてのページを検索します。",
        "apihelp-query+transcludedin-param-prop": "取得するプロパティ:",
        "apihelp-query+transcludedin-paramvalue-prop-pageid": "各ページのページID。",
        "apihelp-query+transcludedin-paramvalue-prop-title": "各ページのページ名。",
        "apihelp-query+transcludedin-paramvalue-prop-redirect": "ページがリダイレクトである場合マークします。",
        "apihelp-query+transcludedin-param-namespace": "この名前空間に含まれるページのみを一覧表示します。",
        "apihelp-query+transcludedin-param-limit": "返す数。",
-       "apihelp-query+transcludedin-example-simple": "<kbd>Main Page</kbd> をトランスクルードしているページの一覧を取得する。",
+       "apihelp-query+transcludedin-example-simple": "<kbd>Main Page</kbd> を参照読み込みしているページの一覧を取得する。",
        "apihelp-query+transcludedin-example-generator": "<kbd>Main Page</kbd> を参照読み込みしているページに関する情報を取得する。",
        "apihelp-query+usercontribs-summary": "利用者によるすべての編集を取得します。",
        "apihelp-query+usercontribs-param-limit": "返す投稿記録の最大数。",
index 0bc67bc..c029636 100644 (file)
        "apihelp-delete-param-oldimage": "[[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]]에 지정된 바대로 삭제할 오래된 그림의 이름입니다.",
        "apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.",
        "apihelp-delete-example-reason": "<kbd>Preparing for move</kbd> 라는 이유로 <kbd>Main Page</kbd>를 삭제하기.",
-       "apihelp-disabled-summary": "이 모듈은 해제되었습니다.",
+       "apihelp-disabled-summary": "이 모듈은 비활성화되었습니다.",
        "apihelp-edit-summary": "문서를 만들고 편집합니다.",
        "apihelp-edit-param-title": "편집할 문서의 제목. <var>$1pageid</var>과 같이 사용할 수 없습니다.",
        "apihelp-edit-param-pageid": "편집할 문서의 문서 ID입니다. <var>$1title</var>과 함께 사용할 수 없습니다.",
index a1d7cb9..9647675 100644 (file)
        "apihelp-query+langlinks-param-dir": "列出時所採用的方向。",
        "apihelp-query+langlinks-param-inlanguagecode": "用於本地化語言名稱的語言代碼。",
        "apihelp-query+langlinks-example-simple": "從頁面 <kbd>Main Page</kbd> 取得跨語言連結。",
+       "apihelp-query+languageinfo-paramvalue-prop-bcp47": "BCP-47 語言代碼。",
+       "apihelp-query+languageinfo-paramvalue-prop-autonym": "語言的本語稱呼,也就是該語言用自己語言本身寫出的名稱。",
+       "apihelp-query+languageinfo-example-simple": "取得所有支援語言的語言代碼。",
+       "apihelp-query+languageinfo-example-autonym-name-de": "取得所有支援語言的本語稱呼和德語名稱。",
        "apihelp-query+links-summary": "回傳指定頁面的所有連結。",
        "apihelp-query+links-param-namespace": "僅顯示在這些命名空間的連結。",
        "apihelp-query+links-param-limit": "要回傳的連結數量。",
index be240ca..abd2db2 100644 (file)
@@ -110,7 +110,9 @@ class BlockManager {
        }
 
        /**
-        * Get the blocks that apply to a user and return the most relevant one.
+        * Get the blocks that apply to a user. If there is only one, return that, otherwise
+        * return a composite block that combines the strictest features of the applicable
+        * blocks.
         *
         * TODO: $user should be UserIdentity instead of User
         *
@@ -143,29 +145,28 @@ class BlockManager {
                }
 
                // User/IP blocking
+               // After this, $blocks is an array of blocks or an empty array
                // TODO: remove dependency on DatabaseBlock
-               $block = DatabaseBlock::newFromTarget( $user, $ip, !$fromReplica );
+               $blocks = DatabaseBlock::newListFromTarget( $user, $ip, !$fromReplica );
 
                // Cookie blocking
-               if ( !$block instanceof AbstractBlock ) {
-                       $block = $this->getBlockFromCookieValue( $user, $request );
+               $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
+               if ( $cookieBlock instanceof AbstractBlock ) {
+                       $blocks[] = $cookieBlock;
                }
 
                // Proxy blocking
-               if ( !$block instanceof AbstractBlock
-                       && $ip !== null
-                       && !in_array( $ip, $this->proxyWhitelist )
-               ) {
+               if ( $ip !== null && !in_array( $ip, $this->proxyWhitelist ) ) {
                        // Local list
                        if ( $this->isLocallyBlockedProxy( $ip ) ) {
-                               $block = new SystemBlock( [
+                               $blocks[] = new SystemBlock( [
                                        'byText' => wfMessage( 'proxyblocker' )->text(),
                                        'reason' => wfMessage( 'proxyblockreason' )->plain(),
                                        'address' => $ip,
                                        'systemBlock' => 'proxy',
                                ] );
                        } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
-                               $block = new SystemBlock( [
+                               $blocks[] = new SystemBlock( [
                                        'byText' => wfMessage( 'sorbs' )->text(),
                                        'reason' => wfMessage( 'sorbsreason' )->plain(),
                                        'address' => $ip,
@@ -175,8 +176,7 @@ class BlockManager {
                }
 
                // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
-               if ( !$block instanceof AbstractBlock
-                       && $this->applyIpBlocksToXff
+               if ( $this->applyIpBlocksToXff
                        && $ip !== null
                        && !in_array( $ip, $this->proxyWhitelist )
                ) {
@@ -185,21 +185,15 @@ class BlockManager {
                        $xff = array_diff( $xff, [ $ip ] );
                        // TODO: remove dependency on DatabaseBlock
                        $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
-                       // TODO: remove dependency on DatabaseBlock
-                       $block = DatabaseBlock::chooseBlock( $xffblocks, $xff );
-                       if ( $block instanceof AbstractBlock ) {
-                               # Mangle the reason to alert the user that the block
-                               # originated from matching the X-Forwarded-For header.
-                               $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
-                       }
+                       $blocks = array_merge( $blocks, $xffblocks );
                }
 
-               if ( !$block instanceof AbstractBlock
-                       && $ip !== null
+               // Soft blocking
+               if ( $ip !== null
                        && $isAnon
                        && IP::isInRanges( $ip, $this->softBlockRanges )
                ) {
-                       $block = new SystemBlock( [
+                       $blocks[] = new SystemBlock( [
                                'address' => $ip,
                                'byText' => 'MediaWiki default',
                                'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
@@ -208,7 +202,53 @@ class BlockManager {
                        ] );
                }
 
-               return $block;
+               // Filter out any duplicated blocks, e.g. from the cookie
+               $blocks = $this->getUniqueBlocks( $blocks );
+
+               if ( count( $blocks ) > 0 ) {
+                       if ( count( $blocks ) === 1 ) {
+                               $block = $blocks[ 0 ];
+                       } else {
+                               $block = new CompositeBlock( [
+                                       'address' => $ip,
+                                       'byText' => 'MediaWiki default',
+                                       'reason' => wfMessage( 'blockedtext-composite-reason' )->plain(),
+                                       'originalBlocks' => $blocks,
+                               ] );
+                       }
+                       return $block;
+               }
+
+               return null;
+       }
+
+       /**
+        * Given a list of blocks, return a list of unique blocks.
+        *
+        * This usually means that each block has a unique ID. For a block with ID null,
+        * if it's an autoblock, it will be filtered out if the parent block is present;
+        * if not, it is assumed to be a unique system block, and kept.
+        *
+        * @param AbstractBlock[] $blocks
+        * @return AbstractBlock[]
+        */
+       private function getUniqueBlocks( $blocks ) {
+               $systemBlocks = [];
+               $databaseBlocks = [];
+
+               foreach ( $blocks as $block ) {
+                       if ( $block instanceof SystemBlock ) {
+                               $systemBlocks[] = $block;
+                       } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
+                               if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
+                                       $databaseBlocks[$block->getParentBlockId()] = $block;
+                               }
+                       } else {
+                               $databaseBlocks[$block->getId()] = $block;
+                       }
+               }
+
+               return array_merge( $systemBlocks, $databaseBlocks );
        }
 
        /**
@@ -393,13 +433,23 @@ class BlockManager {
        public function trackBlockWithCookie( User $user ) {
                $block = $user->getBlock();
                $request = $user->getRequest();
-
-               if (
-                       $block &&
-                       $request->getCookie( 'BlockID' ) === null &&
-                       $this->shouldTrackBlockWithCookie( $block, $user->isAnon() )
-               ) {
-                       $this->setBlockCookie( $block, $request->response() );
+               $response = $request->response();
+               $isAnon = $user->isAnon();
+
+               if ( $block && $request->getCookie( 'BlockID' ) === null ) {
+                       if ( $block instanceof CompositeBlock ) {
+                               // TODO: Improve on simply tracking the first trackable block (T225654)
+                               foreach ( $block->getOriginalBlocks() as $originalBlock ) {
+                                       if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
+                                               $this->setBlockCookie( $originalBlock, $response );
+                                               return;
+                                       }
+                               }
+                       } else {
+                               if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
+                                       $this->setBlockCookie( $block, $response );
+                               }
+                       }
                }
        }
 
diff --git a/includes/block/CompositeBlock.php b/includes/block/CompositeBlock.php
new file mode 100644 (file)
index 0000000..8efd7de
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+/**
+ * Class for blocks composed from multiple blocks.
+ *
+ * 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
+ */
+
+namespace MediaWiki\Block;
+
+use IContextSource;
+use Title;
+
+/**
+ * Multiple Block class.
+ *
+ * Multiple blocks exist to enforce restrictions from more than one block, if several
+ * blocks apply to a user/IP. Multiple blocks are created temporarily on enforcement.
+ *
+ * @since 1.34
+ */
+class CompositeBlock extends AbstractBlock {
+       /** @var AbstractBlock[] */
+       private $originalBlocks;
+
+       /**
+        * Create a new block with specified parameters on a user, IP or IP range.
+        *
+        * @param array $options Parameters of the block:
+        *     originalBlocks Block[] Blocks that this block is composed from
+        */
+       function __construct( $options = [] ) {
+               parent::__construct( $options );
+
+               $defaults = [
+                       'originalBlocks' => [],
+               ];
+
+               $options += $defaults;
+
+               $this->originalBlocks = $options[ 'originalBlocks' ];
+
+               $this->setHideName( $this->propHasValue( 'mHideName', true ) );
+               $this->isSitewide( $this->propHasValue( 'isSitewide', true ) );
+               $this->isEmailBlocked( $this->propHasValue( 'mBlockEmail', true ) );
+               $this->isCreateAccountBlocked( $this->propHasValue( 'blockCreateAccount', true ) );
+               $this->isUsertalkEditAllowed( !$this->propHasValue( 'allowUsertalk', false ) );
+       }
+
+       /**
+        * Determine whether any original blocks have a particular property set to a
+        * particular value.
+        *
+        * @param string $prop
+        * @param mixed $value
+        * @return bool At least one block has the property set to the value
+        */
+       private function propHasValue( $prop, $value ) {
+               foreach ( $this->originalBlocks as $block ) {
+                       if ( $block->$prop == $value ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Determine whether any original blocks have a particular method returning a
+        * particular value.
+        *
+        * @param string $method
+        * @param mixed $value
+        * @param mixed ...$params
+        * @return bool At least one block has the method returning the value
+        */
+       private function methodReturnsValue( $method, $value, ...$params ) {
+               foreach ( $this->originalBlocks as $block ) {
+                       if ( $block->$method( ...$params ) == $value ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Get the original blocks from which this block is composed
+        *
+        * @since 1.34
+        * @return AbstractBlock[]
+        */
+       public function getOriginalBlocks() {
+               return $this->originalBlocks;
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function getExpiry() {
+               $maxExpiry = null;
+               foreach ( $this->originalBlocks as $block ) {
+                       $expiry = $block->getExpiry();
+                       if ( $maxExpiry === null || $expiry === '' || $expiry > $maxExpiry ) {
+                               $maxExpiry = $expiry;
+                       }
+               }
+               return $maxExpiry;
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function getPermissionsError( IContextSource $context ) {
+               $params = $this->getBlockErrorParams( $context );
+
+               $msg = 'blockedtext-composite';
+
+               array_unshift( $params, $msg );
+
+               return $params;
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToRight( $right ) {
+               return $this->methodReturnsValue( __FUNCTION__, true, $right );
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToUsertalk( Title $usertalk = null ) {
+               return $this->methodReturnsValue( __FUNCTION__, true, $usertalk );
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToTitle( Title $title ) {
+               return $this->methodReturnsValue( __FUNCTION__, true, $title );
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToNamespace( $ns ) {
+               return $this->methodReturnsValue( __FUNCTION__, true, $ns );
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToPage( $pageId ) {
+               return $this->methodReturnsValue( __FUNCTION__, true, $pageId );
+       }
+
+       /**
+        * @inheritDoc
+        */
+       public function appliesToPasswordReset() {
+               return $this->methodReturnsValue( __FUNCTION__, true );
+       }
+
+}
index 876a81f..0f19324 100644 (file)
@@ -1045,6 +1045,14 @@ class DatabaseBlock extends AbstractBlock {
                return $this;
        }
 
+       /**
+        * @since 1.34
+        * @return int|null If this is an autoblock, ID of the parent block; otherwise null
+        */
+       public function getParentBlockId() {
+               return $this->mParentBlockId;
+       }
+
        /**
         * Get/set a flag determining whether the master is used for reads
         *
@@ -1159,26 +1167,40 @@ class DatabaseBlock extends AbstractBlock {
         *     not be the same as the target you gave if you used $vagueTarget!
         */
        public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) {
+               $blocks = self::newListFromTarget( $specificTarget, $vagueTarget, $fromMaster );
+               return self::chooseMostSpecificBlock( $blocks );
+       }
+
+       /**
+        * This is similar to DatabaseBlock::newFromTarget, but it returns all the relevant blocks.
+        *
+        * @since 1.34
+        * @param string|User|int|null $specificTarget
+        * @param string|User|int|null $vagueTarget
+        * @param bool $fromMaster
+        * @return DatabaseBlock[] Any relevant blocks
+        */
+       public static function newListFromTarget(
+               $specificTarget,
+               $vagueTarget = null,
+               $fromMaster = false
+       ) {
                list( $target, $type ) = self::parseTarget( $specificTarget );
                if ( $type == self::TYPE_ID || $type == self::TYPE_AUTO ) {
-                       return self::newFromID( $target );
-
+                       $block = self::newFromID( $target );
+                       return $block ? [ $block ] : [];
                } elseif ( $target === null && $vagueTarget == '' ) {
                        # We're not going to find anything useful here
                        # Be aware that the == '' check is explicit, since empty values will be
                        # passed by some callers (T31116)
-                       return null;
-
+                       return [];
                } elseif ( in_array(
                        $type,
                        [ self::TYPE_USER, self::TYPE_IP, self::TYPE_RANGE, null ] )
                ) {
-                       $blocks = self::newLoad( $target, $type, $fromMaster, $vagueTarget );
-                       if ( !empty( $blocks ) ) {
-                               return self::chooseMostSpecificBlock( $blocks );
-                       }
+                       return self::newLoad( $target, $type, $fromMaster, $vagueTarget );
                }
-               return null;
+               return [];
        }
 
        /**
index 2573f8a..3edfe1b 100644 (file)
@@ -146,7 +146,7 @@ class LinkBatch {
        }
 
        /**
-        * Add a ResultWrapper containing IDs and titles to a LinkCache object.
+        * Add a result wrapper containing IDs and titles to a LinkCache object.
         * As normal, titles will go into the static Title cache field.
         * This function *also* stores extra fields of the title used for link
         * parsing to avoid extra DB queries.
@@ -187,7 +187,7 @@ class LinkBatch {
        }
 
        /**
-        * Perform the existence test query, return a ResultWrapper with page_id fields
+        * Perform the existence test query, return a result wrapper with page_id fields
         * @return bool|IResultWrapper
         */
        public function doQuery() {
index bb9114a..69c709c 100644 (file)
@@ -93,7 +93,7 @@ class ChangesFeed {
                $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach ( $sorted as $obj ) {
                        $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
-                       $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace )
+                       $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace ) && $title->isValid()
                                ? $title->getTalkPage()->getFullURL()
                                : '';
 
index 9146429..cf6ed17 100644 (file)
@@ -603,8 +603,8 @@ class ChangeTags {
         * ChangeTags::updateTags() instead, unless directly handling a user request
         * to add or remove tags from an existing revision or log entry.
         *
-        * @param array|null $tagsToAdd If none, pass array() or null
-        * @param array|null $tagsToRemove If none, pass array() or null
+        * @param array|null $tagsToAdd If none, pass [] or null
+        * @param array|null $tagsToRemove If none, pass [] or null
         * @param int|null $rc_id The rc_id of the change to add the tags to
         * @param int|null $rev_id The rev_id of the change to add the tags to
         * @param int|null $log_id The log_id of the change to add the tags to
@@ -1229,11 +1229,13 @@ class ChangeTags {
                $dbw = wfGetDB( DB_MASTER );
                $dbw->startAtomic( __METHOD__ );
 
+               // fetch tag id, this must be done before calling undefineTag(), see T225564
+               $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
+
                // set ctd_user_defined = 0
                self::undefineTag( $tag );
 
                // delete from change_tag
-               $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
                $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
                $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
                $dbw->endAtomic( __METHOD__ );
index 0f3743f..6ae059e 100644 (file)
@@ -23,6 +23,7 @@ use Wikimedia\Assert\Assert;
  * @since 1.34
  */
 class ServiceOptions {
+       private $keys = [];
        private $options = [];
 
        /**
@@ -33,6 +34,7 @@ class ServiceOptions {
         * @throws InvalidArgumentException if one of $keys is not found in any of $sources
         */
        public function __construct( array $keys, ...$sources ) {
+               $this->keys = $keys;
                foreach ( $keys as $key ) {
                        foreach ( $sources as $source ) {
                                if ( $source instanceof Config ) {
@@ -58,20 +60,21 @@ class ServiceOptions {
         * @param string[] $expectedKeys
         */
        public function assertRequiredOptions( array $expectedKeys ) {
-               $actualKeys = array_keys( $this->options );
-               $extraKeys = array_diff( $actualKeys, $expectedKeys );
-               $missingKeys = array_diff( $expectedKeys, $actualKeys );
-               Assert::precondition( !$extraKeys && !$missingKeys,
-                       (
-                       $extraKeys
-                               ? 'Unsupported options passed: ' . implode( ', ', $extraKeys ) . '!'
-                               : ''
-                       ) . ( $extraKeys && $missingKeys ? ' ' : '' ) . (
-                       $missingKeys
-                               ? 'Required options missing: ' . implode( ', ', $missingKeys ) . '!'
-                               : ''
-                       )
-               );
+               if ( $this->keys !== $expectedKeys ) {
+                       $extraKeys = array_diff( $this->keys, $expectedKeys );
+                       $missingKeys = array_diff( $expectedKeys, $this->keys );
+                       Assert::precondition( !$extraKeys && !$missingKeys,
+                               (
+                               $extraKeys
+                                       ? 'Unsupported options passed: ' . implode( ', ', $extraKeys ) . '!'
+                                       : ''
+                               ) . ( $extraKeys && $missingKeys ? ' ' : '' ) . (
+                               $missingKeys
+                                       ? 'Required options missing: ' . implode( ', ', $missingKeys ) . '!'
+                                       : ''
+                               )
+                       );
+               }
        }
 
        /**
index 4b0c6cb..dc50543 100644 (file)
@@ -15,7 +15,7 @@ class CeeFormatter extends LogstashFormatter {
        /**
         * Format records with a cee cookie
         * @param array $record
-        * @return array
+        * @return mixed
         */
        public function format( array $record ) {
                return "@cee: " . parent::format( $record );
index 9adb2b0..266d768 100644 (file)
@@ -965,7 +965,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
        /**
         * Get an array of existing inline interwiki links, as a 2-D array
-        * @return array (prefix => array(dbkey => 1))
+        * @return array [ prefix => [ dbkey => 1 ] ]
         */
        private function getExistingInterwikis() {
                $res = $this->getDB()->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
index 4ce0b18..e9ebabb 100644 (file)
@@ -89,7 +89,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate {
                                                // This method runs after the new revisions were committed.
                                                // Wait for the replica to catch up so they will all be counted.
                                                $dbr->flushSnapshot( $fname );
-                                               $lb->safeWaitForMasterPos( $dbr );
+                                               $lb->waitForMasterPos( $dbr );
                                        }
                                        $affectedInstances[0]->initEditCountInternal();
                                }
index f7658fc..86d1a43 100644 (file)
@@ -22,6 +22,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\NameTableAccessException;
@@ -538,8 +539,14 @@ class DifferenceEngine extends ContextSource {
                                $samePage = false;
                        }
 
-                       if ( $samePage && $this->mNewPage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
-                               if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+                       if ( $samePage && $this->mNewPage && $permissionManager->userCan(
+                               'edit', $user, $this->mNewPage, PermissionManager::RIGOR_QUICK
+                       ) ) {
+                               if ( $this->mNewRev->isCurrent() && $permissionManager->userCan(
+                                       'rollback', $user, $this->mNewPage
+                               ) ) {
                                        $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
                                        if ( $rollbackLink ) {
                                                $out->preventClickjacking();
index e8044af..fb1053c 100644 (file)
@@ -27,7 +27,7 @@
  * @defgroup Dump Dump
  */
 
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -320,7 +320,7 @@ class WikiExporter {
                        }
 
                        $lastLogId = $this->outputLogStream( $result );
-               };
+               }
        }
 
        /**
@@ -468,7 +468,7 @@ class WikiExporter {
         * The result set should be sorted/grouped by page to avoid duplicate
         * page records in the output.
         *
-        * @param ResultWrapper $results
+        * @param IResultWrapper $results
         * @param object $lastRow the last row output from the previous call (or null if none)
         * @return object the last row processed
         */
@@ -517,7 +517,7 @@ class WikiExporter {
        }
 
        /**
-        * @param ResultWrapper $resultset
+        * @param IResultWrapper $resultset
         * @return int the log_id value of the last item output, or null if none
         */
        protected function outputLogStream( $resultset ) {
index 9cf8e15..76f20f0 100644 (file)
@@ -159,7 +159,7 @@ class ExternalStore {
         *
         * @param string $data
         * @param array $params Map of ExternalStoreMedium::__construct context parameters
-        * @return string|bool The URL of the stored data item, or false on error
+        * @return string The URL of the stored data item
         * @throws MWException
         */
        public static function insertToDefault( $data, array $params = [] ) {
@@ -177,7 +177,7 @@ class ExternalStore {
         * @param array $tryStores Refer to $wgDefaultExternalStore
         * @param string $data
         * @param array $params Map of ExternalStoreMedium::__construct context parameters
-        * @return string|bool The URL of the stored data item, or false on error
+        * @return string The URL of the stored data item
         * @throws MWException
         */
        public static function insertWithFallback( array $tryStores, $data, array $params = [] ) {
@@ -245,7 +245,7 @@ class ExternalStore {
        /**
         * @param string $data
         * @param string $wiki
-        * @return string|bool The URL of the stored data item, or false on error
+        * @return string The URL of the stored data item
         * @throws MWException
         */
        public static function insertToForeignDefault( $data, $wiki ) {
index cac16b6..15bc3e0 100644 (file)
@@ -92,6 +92,9 @@ class ExternalStoreDB extends ExternalStoreMedium {
                return $ret;
        }
 
+       /**
+        * @inheritDoc
+        */
        public function store( $location, $data ) {
                $dbw = $this->getMaster( $location );
                $dbw->insert( $this->getTable( $dbw ),
@@ -105,6 +108,9 @@ class ExternalStoreDB extends ExternalStoreMedium {
                return "DB://$location/$id";
        }
 
+       /**
+        * @inheritDoc
+        */
        public function isReadOnly( $location ) {
                $lb = $this->getLoadBalancer( $location );
                $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
index 30c742d..7414f23 100644 (file)
@@ -73,29 +73,32 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
                return $blobs;
        }
 
+       /**
+        * @inheritDoc
+        */
        public function store( $backend, $data ) {
                $be = FileBackendGroup::singleton()->get( $backend );
-               if ( $be instanceof FileBackend ) {
-                       // Get three random base 36 characters to act as shard directories
-                       $rand = Wikimedia\base_convert( mt_rand( 0, 46655 ), 10, 36, 3 );
-                       // Make sure ID is roughly lexicographically increasing for performance
-                       $id = str_pad( UIDGenerator::newTimestampedUID128( 32 ), 26, '0', STR_PAD_LEFT );
-                       // Segregate items by wiki ID for the sake of bookkeeping
-                       // @FIXME: this does not include the domain for b/c but it ideally should
-                       $wiki = $this->params['wiki'] ?? wfWikiID();
+               // Get three random base 36 characters to act as shard directories
+               $rand = Wikimedia\base_convert( mt_rand( 0, 46655 ), 10, 36, 3 );
+               // Make sure ID is roughly lexicographically increasing for performance
+               $id = str_pad( UIDGenerator::newTimestampedUID128( 32 ), 26, '0', STR_PAD_LEFT );
+               // Segregate items by wiki ID for the sake of bookkeeping
+               // @FIXME: this does not include the domain for b/c but it ideally should
+               $wiki = $this->params['wiki'] ?? wfWikiID();
 
-                       $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $wiki );
-                       $url .= ( $be instanceof FSFileBackend )
-                               ? "/{$rand[0]}/{$rand[1]}/{$rand[2]}/{$id}" // keep directories small
-                               : "/{$rand[0]}/{$rand[1]}/{$id}"; // container sharding is only 2-levels
+               $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $wiki );
+               $url .= ( $be instanceof FSFileBackend )
+                       ? "/{$rand[0]}/{$rand[1]}/{$rand[2]}/{$id}" // keep directories small
+                       : "/{$rand[0]}/{$rand[1]}/{$id}"; // container sharding is only 2-levels
 
-                       $be->prepare( [ 'dir' => dirname( $url ), 'noAccess' => 1, 'noListing' => 1 ] );
-                       if ( $be->create( [ 'dst' => $url, 'content' => $data ] )->isOK() ) {
-                               return $url;
-                       }
-               }
+               $be->prepare( [ 'dir' => dirname( $url ), 'noAccess' => 1, 'noListing' => 1 ] );
+               $status = $be->create( [ 'dst' => $url, 'content' => $data ] );
 
-               return false;
+               if ( $status->isOK() ) {
+                       return $url;
+               } else {
+                       throw new MWException( __METHOD__ . ": operation failed: $status" );
+               }
        }
 
        public function isReadOnly( $backend ) {
index e083a4e..7edefd5 100644 (file)
@@ -31,31 +31,6 @@ use Wikimedia\Rdbms\DBUnexpectedError;
  * @ingroup FileAbstraction
  */
 class ForeignDBFile extends LocalFile {
-       /**
-        * @param Title $title
-        * @param FileRepo $repo
-        * @param null $unused
-        * @return ForeignDBFile
-        */
-       static function newFromTitle( $title, $repo, $unused = null ) {
-               return new self( $title, $repo );
-       }
-
-       /**
-        * Create a ForeignDBFile from a title
-        * Do not call this except from inside a repo class.
-        *
-        * @param stdClass $row
-        * @param FileRepo $repo
-        * @return ForeignDBFile
-        */
-       static function newFromRow( $row, $repo ) {
-               $title = Title::makeTitle( NS_FILE, $row->img_name );
-               $file = new self( $title, $repo );
-               $file->loadFromRow( $row );
-
-               return $file;
-       }
 
        /**
         * @param string $srcPath
index 54bcea3..1e1bde3 100644 (file)
@@ -150,10 +150,10 @@ class LocalFile extends File {
         * @param FileRepo $repo
         * @param null $unused
         *
-        * @return self
+        * @return static
         */
        static function newFromTitle( $title, $repo, $unused = null ) {
-               return new self( $title, $repo );
+               return new static( $title, $repo );
        }
 
        /**
@@ -163,11 +163,11 @@ class LocalFile extends File {
         * @param stdClass $row
         * @param FileRepo $repo
         *
-        * @return self
+        * @return static
         */
        static function newFromRow( $row, $repo ) {
                $title = Title::makeTitle( NS_FILE, $row->img_name );
-               $file = new self( $title, $repo );
+               $file = new static( $title, $repo );
                $file->loadFromRow( $row );
 
                return $file;
@@ -190,12 +190,12 @@ class LocalFile extends File {
                        $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
                }
 
-               $fileQuery = self::getQueryInfo();
+               $fileQuery = static::getQueryInfo();
                $row = $dbr->selectRow(
                        $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
                );
                if ( $row ) {
-                       return self::newFromRow( $row, $repo );
+                       return static::newFromRow( $row, $repo );
                } else {
                        return false;
                }
index 3cdbfc2..584e001 100644 (file)
@@ -42,7 +42,7 @@ class OldLocalFile extends LocalFile {
         * @param Title $title
         * @param FileRepo $repo
         * @param string|int|null $time
-        * @return self
+        * @return static
         * @throws MWException
         */
        static function newFromTitle( $title, $repo, $time = null ) {
@@ -51,27 +51,27 @@ class OldLocalFile extends LocalFile {
                        throw new MWException( __METHOD__ . ' got null for $time parameter' );
                }
 
-               return new self( $title, $repo, $time, null );
+               return new static( $title, $repo, $time, null );
        }
 
        /**
         * @param Title $title
         * @param FileRepo $repo
         * @param string $archiveName
-        * @return self
+        * @return static
         */
        static function newFromArchiveName( $title, $repo, $archiveName ) {
-               return new self( $title, $repo, null, $archiveName );
+               return new static( $title, $repo, null, $archiveName );
        }
 
        /**
         * @param stdClass $row
         * @param FileRepo $repo
-        * @return self
+        * @return static
         */
        static function newFromRow( $row, $repo ) {
                $title = Title::makeTitle( NS_FILE, $row->oi_name );
-               $file = new self( $title, $repo, null, $row->oi_archive_name );
+               $file = new static( $title, $repo, null, $row->oi_archive_name );
                $file->loadFromRow( $row, 'oi_' );
 
                return $file;
@@ -95,12 +95,12 @@ class OldLocalFile extends LocalFile {
                        $conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
                }
 
-               $fileQuery = self::getQueryInfo();
+               $fileQuery = static::getQueryInfo();
                $row = $dbr->selectRow(
                        $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
                );
                if ( $row ) {
-                       return self::newFromRow( $row, $repo );
+                       return static::newFromRow( $row, $repo );
                } else {
                        return false;
                }
index fde68bb..2865ce5 100644 (file)
@@ -55,19 +55,19 @@ class UnregisteredLocalFile extends File {
        /**
         * @param string $path Storage path
         * @param string $mime
-        * @return UnregisteredLocalFile
+        * @return static
         */
        static function newFromPath( $path, $mime ) {
-               return new self( false, false, $path, $mime );
+               return new static( false, false, $path, $mime );
        }
 
        /**
         * @param Title $title
         * @param FileRepo $repo
-        * @return UnregisteredLocalFile
+        * @return static
         */
        static function newFromTitle( $title, $repo ) {
-               return new self( $title, $repo, false, false );
+               return new static( $title, $repo, false, false );
        }
 
        /**
index 16dc465..ff805d8 100644 (file)
@@ -866,7 +866,7 @@ abstract class HTMLFormField {
         * that return value has no taint.
         *
         * @param string $value The value of the input
-        * @return array array( $errors, $errorClass )
+        * @return array [ $errors, $errorClass ]
         * @return-taint none
         */
        public function getErrorsAndErrorClass( $value ) {
index f137bf1..85cbbb1 100644 (file)
@@ -141,6 +141,35 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
                return new MediaWiki\Widget\SelectWithInputWidget( $params );
        }
 
+       /**
+        * @inheritDoc
+        */
+       public function getDefault() {
+               $default = parent::getDefault();
+
+               // Default values of empty form
+               $final = '';
+               $list = 'other';
+               $text = '';
+
+               if ( $default !== null ) {
+                       $final = $default;
+                       // Assume the default is a text value, with the 'other' option selected.
+                       // Then check if that assumption is correct, and update $list and $text if not.
+                       $text = $final;
+                       foreach ( $this->mFlatOptions as $option ) {
+                               $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
+                               if ( strpos( $final, $match ) === 0 ) {
+                                       $list = $option;
+                                       $text = substr( $final, strlen( $match ) );
+                                       break;
+                               }
+                       }
+               }
+
+               return [ $final, $list, $text ];
+       }
+
        /**
         * @param WebRequest $request
         *
@@ -163,22 +192,9 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
                        } else {
                                $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text;
                        }
-               } else {
-                       $final = $this->getDefault();
-
-                       $list = 'other';
-                       $text = $final;
-                       foreach ( $this->mFlatOptions as $option ) {
-                               $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
-                               if ( strpos( $text, $match ) === 0 ) {
-                                       $list = $option;
-                                       $text = substr( $text, strlen( $match ) );
-                                       break;
-                               }
-                       }
+                       return [ $final, $list, $text ];
                }
-
-               return [ $final, $list, $text ];
+               return $this->getDefault();
        }
 
        public function getSize() {
@@ -197,7 +213,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
 
                if ( isset( $this->mParams['required'] )
                        && $this->mParams['required'] !== false
-                       && $value[1] === ''
+                       && $value[0] === ''
                ) {
                        return $this->msg( 'htmlform-required' );
                }
index 8f58344..00bb61f 100644 (file)
@@ -1099,14 +1099,23 @@ class WikiImporter {
                } elseif ( !$title->canExist() ) {
                        $this->notice( 'import-error-special', $title->getPrefixedText() );
                        return false;
-               } elseif ( !$title->userCan( 'edit' ) && !$commandLineMode ) {
-                       # Do not import if the importing wiki user cannot edit this page
-                       $this->notice( 'import-error-edit', $title->getPrefixedText() );
-                       return false;
-               } elseif ( !$title->exists() && !$title->userCan( 'create' ) && !$commandLineMode ) {
-                       # Do not import if the importing wiki user cannot create this page
-                       $this->notice( 'import-error-create', $title->getPrefixedText() );
-                       return false;
+               } elseif ( !$commandLineMode ) {
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+                       $user = RequestContext::getMain()->getUser();
+
+                       if ( !$permissionManager->userCan( 'edit', $user, $title ) ) {
+                               # Do not import if the importing wiki user cannot edit this page
+                               $this->notice( 'import-error-edit', $title->getPrefixedText() );
+
+                               return false;
+                       }
+
+                       if ( !$title->exists() && !$permissionManager->userCan( 'create', $user, $title ) ) {
+                               # Do not import if the importing wiki user cannot create this page
+                               $this->notice( 'import-error-create', $title->getPrefixedText() );
+
+                               return false;
+                       }
                }
 
                return [ $title, $foreignTitle ];
index c008333..567fb10 100644 (file)
@@ -165,6 +165,15 @@ class CliInstaller extends Installer {
         * Main entry point.
         */
        public function execute() {
+               // If APC is available, use that as the MainCacheType, instead of nothing.
+               // This is hacky and should be consolidated with WebInstallerOptions.
+               // This is here instead of in __construct(), because it should run run after
+               // doEnvironmentChecks(), which populates '_Caches'.
+               if ( count( $this->getVar( '_Caches' ) ) ) {
+                       // We detected a CACHE_ACCEL implementation, use it.
+                       $this->setVar( '_MainCacheType', 'accel' );
+               }
+
                $vars = Installer::getExistingLocalSettings();
                if ( $vars ) {
                        $this->showStatusMessage(
index 26f9bf0..33d4fcc 100644 (file)
@@ -1803,7 +1803,7 @@ abstract class Installer {
        /**
         * Add an installation step following the given step.
         *
-        * @param callable $callback A valid installation callback array, in this form:
+        * @param array $callback A valid installation callback array, in this form:
         *    [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
         * @param string $findStep The step to find. Omit to put the step at the beginning
         */
index 008240a..31827a1 100644 (file)
@@ -1071,7 +1071,7 @@ END;
                        $this->db->query( $command );
                } else {
                        $this->output( "...foreign key constraint on '$table.$field' already does not exist\n" );
-               };
+               }
        }
 
        protected function changeFkeyDeferrable( $table, $field, $clause ) {
@@ -1235,7 +1235,7 @@ END;
                if ( $this->updateRowExists( 'patch-textsearch_bug66650.sql' ) ) {
                        $this->output( "...T68650 already fixed or not applicable.\n" );
                        return;
-               };
+               }
                $this->applyPatch( 'patch-textsearch_bug66650.sql', false,
                        'Rebuilding text search for T68650' );
        }
index 0a6be86..20018d0 100644 (file)
@@ -124,6 +124,13 @@ class WebInstaller extends Installer {
         */
        protected $tabIndex = 1;
 
+       /**
+        * Numeric index of the help box
+        *
+        * @var int
+        */
+       protected $helpBoxId = 1;
+
        /**
         * Name of the page we're on
         *
@@ -680,11 +687,13 @@ class WebInstaller extends Installer {
                $args = array_map( 'htmlspecialchars', $args );
                $text = wfMessage( $msg, $args )->useDatabase( false )->plain();
                $html = $this->parse( $text, true );
+               $id = 'helpBox-' . $this->helpBoxId++;
 
                return "<div class=\"config-help-field-container\">\n" .
-                       "<span class=\"config-help-field-hint\" title=\"" .
+                       "<input type=\"checkbox\" class=\"config-help-field-checkbox\" id=\"$id\" />" .
+                       "<label class=\"config-help-field-hint\" for=\"$id\" title=\"" .
                        wfMessage( 'config-help-tooltip' )->escaped() . "\">" .
-                       wfMessage( 'config-help' )->escaped() . "</span>\n" .
+                       wfMessage( 'config-help' )->escaped() . "</label>\n" .
                        "<div class=\"config-help-field-data\">" . $html . "</div>\n" .
                        "</div>\n";
        }
index bc25179..2412319 100644 (file)
@@ -364,7 +364,7 @@ class WebInstallerOptions extends WebInstallerPage {
                ] );
                $styleUrl = $server . dirname( dirname( $this->parent->getUrl() ) ) .
                        '/mw-config/config-cc.css';
-               $iframeUrl = '//creativecommons.org/license/?' .
+               $iframeUrl = 'https://creativecommons.org/license/?' .
                        wfArrayToCgi( [
                                'partner' => 'MediaWiki',
                                'exit_url' => $exitUrl,
index b061d0d..cbb9b57 100644 (file)
@@ -90,15 +90,6 @@ class WebInstallerOutput {
                $this->flush();
        }
 
-       /**
-        * @param string $text
-        * @deprecated since 1.32; use addWikiTextAsInterface instead
-        */
-       public function addWikiText( $text ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->addWikiTextAsInterface( $text );
-       }
-
        /**
         * @param string $text
         * @since 1.32
@@ -285,7 +276,7 @@ class WebInstallerOutput {
 <?php echo Html::openElement( 'body', [ 'class' => $this->getLanguage()->getDir() ] ) . "\n"; ?>
 <div id="mw-page-base"></div>
 <div id="mw-head-base"></div>
-<div id="content" class="mw-body">
+<div id="content" class="mw-body" role="main">
 <div id="bodyContent" class="mw-body-content">
 
 <h1><?php $this->outputTitle(); ?></h1>
@@ -304,9 +295,7 @@ class WebInstallerOutput {
 
 <div id="mw-panel">
        <div class="portal" id="p-logo">
-               <a style="background-image: url(images/installer-logo.png);"
-                       href="https://www.mediawiki.org/"
-                       title="Main Page"></a>
+               <a href="https://www.mediawiki.org/" title="Main Page"></a>
        </div>
 <?php
        $message = wfMessage( 'config-sidebar' )->plain();
@@ -325,13 +314,14 @@ class WebInstallerOutput {
        public function outputShortHeader() {
 ?>
 <?php echo Html::htmlHeader( $this->getHeadAttribs() ); ?>
+
 <head>
-       <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <meta name="robots" content="noindex, nofollow" />
+       <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <title><?php $this->outputTitle(); ?></title>
        <?php echo $this->getCssUrl() . "\n"; ?>
-       <?php echo $this->getJQuery(); ?>
-       <?php echo Html::linkedScript( 'config.js' ); ?>
+       <?php echo $this->getJQuery() . "\n"; ?>
+       <?php echo Html::linkedScript( 'config.js' ) . "\n"; ?>
 </head>
 
 <body style="background-image: none">
index d676a04..0f78c62 100644 (file)
@@ -57,8 +57,8 @@
        "config-env-bad": "جرى التحقق من البيئة. لا يمكنك تنصيب ميدياويكي.",
        "config-env-php": "بي إتش بي $1 مثبت.",
        "config-env-hhvm": "نصبت HHVM $1.",
-       "config-unicode-using-intl": "باستخدام [https://pecl.php.net/intl امتداد intl PECL] لتسوية يونيكود.",
-       "config-unicode-pure-php-warning": "<strong>تحذير:</strong> لا يتوفر [https://pecl.php.net/intl امتداد intl PECL] للتعامل مع تطبيع يونيكود; حيث يتراجع لإبطاء تنفيذ Pure-Pure;\nإذا كنت تدير موقعا عالي الزيارات، فتجب عليك القراءة قليلا في [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations تطبيع يونيكود].",
+       "config-unicode-using-intl": "باستخدام [https://php.net/manual/en/book.intl.php امتداد PHP intl] لتسوية يونيكود.",
+       "config-unicode-pure-php-warning": "<strong>تحذير:</strong> لا يتوفر [https://php.net/manual/en/book.intl.php امتداد PHP intl] للتعامل مع تطبيع يونيكود; حيث يتراجع لإبطاء تنفيذ Pure-Pure;\nإذا كنت تدير موقعا عالي الزيارات، فتجب عليك القراءة قليلا في [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations تطبيع يونيكود].",
        "config-unicode-update-warning": "<strong>تحذير:</strong> يستخدم الإصدار المثبت من برنامج تطبيع نظام يونيكود إصدارًا قديما من مكتبة [http://site.icu-project.org/ مشروع ICU];\nتجب عليك [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations الترقية] إذا كنت مهتما باستخدام يونيكود.",
        "config-no-db": "لا يمكن العثور على مشغل قاعدة بيانات مناسب! تحتاج إلى تثبيت مشغل قاعدة بيانات PHP، \n{{PLURAL:$2|نوع قاعدة البيانات التالي مدعوم|أنواع قاعدة البيانات التالية مدعومة}} البيانات التالية مدعومة: $1.\n\nإذا قمت بتجميع PHP بنفسك، فقم بتكوينها مع تمكين عميل قاعدة البيانات، على سبيل المثال، باستخدام <code>./configure --with-mysqli</code>.\nإذا قمت بتثبيت PHP من حزمة Debian أو Ubuntu، فستحتاج أيضا إلى تثبيت، على سبيل المثال، حزمة <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>تحذير:</strong> لديك SQLite $2، وهو أقل من الحد الأدنى المطلوب للنسخة $1، SQLite سوف يكون غير متوفر.",
index 80e063e..4146ce4 100644 (file)
@@ -53,8 +53,8 @@
        "config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
        "config-env-php": "Усталяваны PHP $1.",
        "config-env-hhvm": "HHVM $1 усталяваная.",
-       "config-unicode-using-intl": "Выкарыстоўваецца [https://pecl.php.net/intl intl пашырэньне з PECL] для Unicode-нармалізацыі",
-       "config-unicode-pure-php-warning": "'''Папярэджаньне''': [https://pecl.php.net/intl Пашырэньне intl з PECL] — ня слушнае для Unicode-нармалізацыі, цяпер выкарыстоўваецца марудная PHP-рэалізацыя.\nКалі ў Вас сайт з высокай наведвальнасьцю, раім пачытаць пра [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-нармалізацыю].",
+       "config-unicode-using-intl": "Выкарыстоўваецца [https://php.net/manual/en/book.intl.php PHP-пашырэньне intl] для Unicode-нармалізацыі.",
+       "config-unicode-pure-php-warning": "<strong>Папярэджаньне</strong>: [https://php.net/manual/en/book.intl.php PHP-пашырэньне intl] — ня слушнае для Unicode-нармалізацыі, цяпер выкарыстоўваецца марудная PHP-рэалізацыя.\nКалі ў вас сайт з высокай наведвальнасьцю, раім пачытаць пра [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-нармалізацыю].",
        "config-unicode-update-warning": "'''Папярэджаньне''': усталяваная вэрсія бібліятэкі для Unicode-нармалізацыі выкарыстоўвае састарэлую вэрсію бібліятэкі з [http://site.icu-project.org/ праекту ICU].\nРаім [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations абнавіць], калі ваш сайт будзе працаваць з Unicode.",
        "config-no-db": "Немагчыма знайсьці адпаведны драйвэр базы зьвестак. Вам неабходна ўсталяваць драйвэр базы зьвестак для PHP.\n{{PLURAL:$2|Падтрымліваецца наступны тып базы|Падтрымліваюцца наступныя тыпы базаў}} зьвестак: $1.\n\nКалі вы скампілявалі PHP самастойна, зьмяніце канфігурацыю, каб уключыць кліента базы зьвестак, напрыклад, з дапамогай <code>./configure --with-mysqli</code>.\nКалі вы ўсталявалі PHP з пакунку Debian або Ubuntu, тады вам трэба дадаткова ўсталяваць, напрыклад, пакунак <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Папярэджаньне</strong>: усталяваны SQLite $2, у той час, калі мінімальная сумяшчальная вэрсія — $1. SQLite ня будзе даступны.",
index c9abc94..44cd45e 100644 (file)
        "config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.",
        "config-env-php": "El PHP $1 està instal·lat.",
        "config-env-hhvm": "L’HHVM $1 és instal·lat.",
-       "config-unicode-using-intl": "S'utilitza l'[https://pecl.php.net/intl extensió intl PECL] per a la normalització de l'Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Avís:</strong> L'[https://pecl.php.net/intl extensió intl PECL] no és disponible per gestionar la normalització d'Unicode. Es reverteix a una implementació més lenta en PHP pur.\nSi administreu un lloc web amb molt de trànsit, hauríeu de consultar la guia de [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalització d'Unicode].",
+       "config-unicode-using-intl": "S'utilitza l'[https://php.net/manual/en/book.intl.php extensió intl de PHP] per a la normalització de l'Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Avís:</strong> L'[https://php.net/manual/en/book.intl.php extensió intl de PHP] no és disponible per gestionar la normalització d'Unicode. Es reverteix a una implementació més lenta en PHP pur.\nSi administreu un lloc web amb molt de trànsit, hauríeu de consultar la guia de [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalització d'Unicode].",
        "config-unicode-update-warning": "<strong>Avís:</strong> La versió instal·lada del contenidor de normalització d'Unicode utilitza una versió antiga de la biblioteca [http://site.icu-project.org/ del projecte ICU].\nHauríeu [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations d'actualitzar-la] si us importa poder utilitzar Unicode.",
        "config-no-db": "No s'ha pogut trobar un controlador adequat per a la base de dades. Instal·leu-ne un per al PHP.\nHi ha suport per {{PLURAL:$2|al tipus de base de dades següent|als tipus de base de dades següents}}: $1\n\nSi heu compilat el PHP manualment, torneu a configurar-lo amb un client de base de dades habilitat, per exemple fent servir <code>./configure --with-mysqli</code>.\nSi heu instal·lat el PHP d'un paquet de Debian o Ubuntu, també cal que instal·leu, per exemple, el paquet <code>php-mysql</code>.",
-       "config-outdated-sqlite": "<strong>Avís:</strong> teniu el SQLite $1, que és menor que la versió mínima necessària $2. SQLite no estarà disponible.",
+       "config-outdated-sqlite": "<strong>Avís:</strong> teniu el SQLite $2, que és menor que la versió mínima necessària $1. SQLite no estarà disponible.",
        "config-no-fts3": "<strong>Avís:</strong> SQLite està compilat sense el [//sqlite.org/fts3.html mòdul FTS3], per tant les funcionalitats de cerca no estaran disponibles en aquesta instal·lació.",
        "config-pcre-old": "<strong>Error fatal:</strong> Cal el PCRE $1 o superior.\nEl binari PHP que utilitzeu està enllaçat amb el PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Més informació].",
+       "config-pcre-no-utf8": "<strong>Fatal:</strong> El mòdul PCRE de PHP sembla que no va compilar-se per funcionar amb PCRE_UTF8.\nMediaWiki necessita que UTF-8 funcioni correctament.",
        "config-memory-raised": "El <code>memory_limit</code> del PHP és $1 i s'ha aixecat a $2.",
        "config-memory-bad": "<strong>Avís:</strong> El <code>memory_limit</code> del PHP és $1.\nAixò és probablement massa baix.\nLa instal·lació pot fallar!",
        "config-apc": "L'[https://www.php.net/apc APC] està instal·lat",
        "config-apcu": "L'[https://www.php.net/apcu APCu] està instal·lat",
        "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] està instal·lat",
+       "config-no-cache-apcu": "<strong>Avís:</strong> no s'ha pogut trobar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nNo s'habilitarà la memòria cau d'objectes.",
        "config-diff3-bad": "No s'ha trobat el GNU diff3. Podeu ignorar-ho per ara, però us podeu trobar amb conflictes d'edició més habitualment.",
        "config-git": "S'ha trobat el programari de control de versions Git: <code>$1</code>.",
        "config-git-bad": "No s'ha trobat el programari de control de versions Git. Podeu ignorar-ho per ara, però la pàgina Especial:Versió no mostrarà els resums de publicacions.",
        "config-admin-error-password": "S'ha produït un error intern en definir una contrasenya per a l'administrador «<nowiki>$1</nowiki>»: <pre>$2</pre>",
        "config-admin-error-bademail": "Heu introduït una adreça electrònica no vàlida.",
        "config-subscribe": "Subscriu a la [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce llista de correu d'anunci de noves versions].",
+       "config-subscribe-noemail": "Us heu provat de subscriure a la llista de correu d'anuncis de noves versions sense proporcionar-hi una adreça electrònica.\nProporcioneu-ne una si voleu subscriure-us a la llista de correu electrònic.",
        "config-pingback": "Comparteix dades d'aquesta instal·lació amb els desenvolupadors de MediaWiki.",
        "config-almost-done": "Gairebé ja heu acabat!\nPodeu ometre el que queda de la configuració i procedir amb la instal·lació del wiki.",
        "config-optional-continue": "Fes-me més preguntes.",
        "config-email-auth": "Habilita l'autenticació per correu electrònic",
        "config-email-auth-help": "Si s'habilita l'opció, els usuaris hauran de confirmar llur adreça electrònica utilitzant un enllaç que els enviarem quan la defineixin o la canviïn.\nNomés les adreces electròniques autenticades poden rebre correus d'altres usuaris o canviar les notificacions de correu.\nDefinir aquesta opció és <strong>recomanat</strong> per a wikis públics per tal d'evitar els possibles abusos de l'ús del correu.",
        "config-email-sender": "Adreça electrònica de retorn:",
+       "config-email-sender-help": "Introduïu una adreça electrònica per utilitzar-la com a adreça de retorn dels missatges electrònics de sortida.\nAquí és on s'enviaran els missatges que no arribin a lloc.\nMolts servidors de correu electrònic necessiten com a mínim que la part del nom de domini sigui vàlida.",
        "config-upload-settings": "Imatges i càrregues de fitxers",
        "config-upload-enable": "Habilita la càrrega de fitxers",
        "config-upload-help": "Les càrregues de fitxers potencialment exposen el vostre servidor a riscos de seguretat.\nPer a més informació, llegiu la [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security secció de seguretat] del manual.\n\nPer habilitar les càrregues de fitxer, canvieu el mode del subdirectori <code>images</code> del directori arrel de MediaWiki per tal que el servidor web pugui escriure-hi.\nA continuació, habiliteu-ne l'opció.",
        "config-upload-deleted": "Directori pels arxius suprimits:",
        "config-upload-deleted-help": "Trieu un directori per a arxivar els fitxers suprimits.\nIdealment no hauria de ser accessible des del web.",
        "config-logo": "URL del logo:",
+       "config-logo-help": "L'aparença per defecte de MediaWiki inclou un espai per a un logotip de 135x160 píxels sobre el menú de la barra lateral.\nCarregueu una imatge de la mida apropiada i introduïu un URL aquí.\n\nPodeu utilitzar <code>$wgStylePath</code> o <code>$wgScriptPath</code> si el vostre logotip es relatiu a aquests camins.\n\nSi no voleu cap logotip, deixeu el quadre en blanc.",
        "config-instantcommons": "Habilita Instant Commons",
        "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] és una característica que permet que els wikis utilitzin imatges, sons i altres fitxers multimèdies que es troben al lloc web de [https://commons.wikimedia.org/ Wikimedia Commons].\nPer a això, cal que el MediaWiki tingui accés a Internet.\n\nPer a més informació d'aquesta característica, amb instuccions de com definir altres wikis apart de Wikimedia Commons, consulteu [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos el manual].",
        "config-cc-error": "El selector de llicència Creative Commons no ha donat cap resultat.\nIntroduïu la llicència manualment.",
        "config-extensions": "Extensions",
        "config-extensions-help": "Les extensions que es llisten a dalt s'han detecta en el directori <code>./extensions</code>.\n\nPoden necessitar configuració addicional, però ja podeu habilitar-les.",
        "config-skins": "Aparences",
-       "config-skins-help": "S'han detectat els temes llistats a dalt en el directori <code>./skins</code>. Heu d'habilitar-ne com a mínim un i trieu-ne el predeterminat.",
-       "config-skins-use-as-default": "Utilitza aquest tema per defecte",
-       "config-skins-missing": "No s'ha trobat cap tema; MediaWiki utilitzarà el tema per defecte fins que hi instal·leu alguns adequats.",
+       "config-skins-help": "S'han detectat les aparences llistades a dalt en el directori <code>./skins</code>. Heu d'habilitar-ne com a mínim un i trieu-ne el predeterminat.",
+       "config-skins-use-as-default": "Utilitza aquesta aparença per defecte",
+       "config-skins-missing": "No s'ha trobat cap aparença; MediaWiki utilitzarà l'aparença per defecte fins que hi instal·leu algunes adequades.",
        "config-skins-must-enable-some": "Heu de triar com a mínim un tema per habilitar.",
-       "config-skins-must-enable-default": "Cal habilitar el tema triat per defecte.",
+       "config-skins-must-enable-default": "Cal habilitar l'aparença triada per defecte.",
        "config-install-alreadydone": "<strong>Avís:</strong> Sembla que ja havíeu instal·lat MediaWiki i esteu provant d'instal·lar-lo de nou.\nProcediu a la pàgina següent.",
        "config-install-begin": "En fer clic a «{{int:config-continue}}» s’iniciarà la instal·lació del MediaWiki. Si encara voleu fer canvis, feu clic a «{{int:config-back}}».",
        "config-install-step-done": "fet",
index 8f62c4f..cf341e4 100644 (file)
@@ -12,7 +12,8 @@
                        "Seb35",
                        "Ilimanaq29",
                        "Dvorapa",
-                       "Patriccck"
+                       "Patriccck",
+                       "Tchoř"
                ]
        },
        "config-desc": "Instalační program pro MediaWiki",
@@ -58,8 +59,8 @@
        "config-env-bad": "Prostředí bylo zkontrolováno.\nMediaWiki nelze nainstalovat.",
        "config-env-php": "Je nainstalováno PHP $1.",
        "config-env-hhvm": "Je nainstalováno HHVM $1.",
-       "config-unicode-using-intl": "Pro normalizaci Unicode se používá [https://pecl.php.net/intl PECL rozšíření intl].",
-       "config-unicode-pure-php-warning": "<strong>Upozornění:</strong> Není dostupné [https://pecl.php.net/intl PECL rozšíření intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst něco o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
+       "config-unicode-using-intl": "Pro normalizaci Unicode se používá [https://php.net/manual/en/book.intl.php rozšíření PHP intl].",
+       "config-unicode-pure-php-warning": "<strong>Upozornění:</strong> Není dostupné [https://php.net/manual/en/book.intl.php rozšíření PHP intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
        "config-unicode-update-warning": "<strong>Upozornění:</strong> Nainstalovaná verze vrstvy pro normalizaci Unicode používá starší verzi knihovny [http://site.icu-project.org/ projektu ICU].\nPokud vám aspoň trochu záleží na používání Unicode, měli byste [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ji aktualizovat].",
        "config-no-db": "Nepodařilo se nalézt vhodný databázový ovladač! Musíte nainstalovat databázový ovladač pro PHP.\n{{PLURAL:$2|Je podporován následující typ databáze|Jsou podporovány následující typy databází}}: $1.\n\nPokud jste si PHP přeložili sami, překonfigurujte ho se zapnutým databázovým klientem, například pomocí <code>./configure --with-mysqli</code>.\nPokud jste PHP nainstalovali z balíčku Debian či Ubuntu, potřebujete nainstalovat také modul <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Upozornění:</strong> Máte SQLite $2, které je starší než minimálně vyžadovaná verze $1. SQLite nebude dostupné.",
        "config-missing-db-name": "Musíte zadat hodnotu pro „{{int:config-db-name}}“.",
        "config-missing-db-host": "Musíte zadat hodnotu pro „{{int:config-db-host}}“.",
        "config-missing-db-server-oracle": "Musíte zadat hodnotu pro „{{int:config-db-host-oracle}}“.",
-       "config-invalid-db-server-oracle": "Chybné databázové TNS „$1“.\nPoužívejte buď „TNS Name“ nebo „Easy Connect“ (viz [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).",
+       "config-invalid-db-server-oracle": "Chybné databázové TNS „$1“.\nPoužívejte buď „TNS Name“ nebo „Easy Connect“ (vizte [http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).",
        "config-invalid-db-name": "Chybné jméno databáze „$1“.\nPoužívejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a spojovník (-).",
        "config-invalid-db-prefix": "Chybný databázový prefix „$1“.\nPoužívejte pouze ASCII písmena (a-z, A-Z), čísla (0-9), podtržítko (_) a spojovník (-).",
        "config-connection-error": "$1.\n\nZkontrolujte server, uživatelské jméno a heslo a zkuste to znovu. Pokud jako adresu databázového serveru používáte „localhost“, zkuste použít „127.0.0.1“ (a naopak).",
        "config-profile-no-anon": "Vyžadována registrace uživatelů",
        "config-profile-fishbowl": "Editace jen pro vybrané",
        "config-profile-private": "Soukromá wiki",
-       "config-profile-help": "Wiki fungují nejlépe, když je necháte editovat co největším možným počtem lidí.\nV MediaWiki můžete snadno kontrolovat poslední změny a vracet zpět libovolnou škodu způsobenou hloupými nebo zlými uživateli.\n\nMnoho lidí však zjistilo, že je MediaWiki užitečné v širokém spektru rolí a někdy není snadné všechny přesvědčit o výhodách wikizvyklostí.\nTakže si můžete vybrat.\n\nModel '''{{int:config-profile-wiki}}''' dovoluje editovat všem, aniž by se museli přihlašovat.\nNa wiki, kde je '''{{int:config-profile-no-anon}}''', se lépe řídí zodpovědnost, ale může to odradit náhodné přispěvatele.\n\nProfil '''{{int:config-profile-fishbowl}}''' umožňuje schváleným uživatelům editovat, ale veřejnost si může stránky prohlížet včetně jejich historie.\n'''{{int:config-profile-private}}''' dovoluje stránky prohlížet jen schváleným uživatelům, kteří je i mohou editovat.\n\nPo instalaci je možná komplexní konfigurace uživatelských práv; viz [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights odpovídající stránku příručky].",
+       "config-profile-help": "Wiki fungují nejlépe, když je necháte editovat co největším možným počtem lidí.\nV MediaWiki můžete snadno kontrolovat poslední změny a vracet zpět libovolnou škodu způsobenou hloupými nebo zlými uživateli.\n\nMnoho lidí však zjistilo, že je MediaWiki užitečné v širokém spektru rolí a někdy není snadné všechny přesvědčit o výhodách wikizvyklostí.\nTakže si můžete vybrat.\n\nModel '''{{int:config-profile-wiki}}''' dovoluje editovat všem, aniž by se museli přihlašovat.\nNa wiki, kde je '''{{int:config-profile-no-anon}}''', se lépe řídí zodpovědnost, ale může to odradit náhodné přispěvatele.\n\nProfil '''{{int:config-profile-fishbowl}}''' umožňuje schváleným uživatelům editovat, ale veřejnost si může stránky prohlížet včetně jejich historie.\n'''{{int:config-profile-private}}''' dovoluje stránky prohlížet jen schváleným uživatelům, kteří je i mohou editovat.\n\nPo instalaci je možná komplexní konfigurace uživatelských práv; vizte [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights odpovídající stránku příručky].",
        "config-license": "Autorská práva a licence:",
        "config-license-none": "Bez patičky s licencí",
        "config-license-cc-by-sa": "Creative Commons Uveďte autora-Zachovejte licenci",
index b17b093..daa4de9 100644 (file)
        "config-install-stats": "شروع آمار",
        "config-install-keys": "تولید کلیدهای مخفی",
        "config-install-updates": "جلوگیری از به روز رسانی‌های غیر ضروری در حال اجرا",
-       "config-install-updates-failed": "<strong>خطا:</strong> قراردادن کلیدهای به روز رسانی به داخل جداول با خطای روبرو مواجه شد: $1",
+       "config-install-updates-failed": "<strong>خطا:</strong> قرار دادن کلیدهای روزآمدسازی در جدول‌ها با شکست و این خطا مواجه شد: $1",
        "config-install-sysop": "ایجاد حساب کاربری مدیر",
        "config-install-subscribe-fail": "قادر تصدیق اعلام مدیاویکی نیست:$1",
        "config-install-subscribe-notpossible": "سی‌یوآر‌ال نصب نشده‌است و <code>allow_url_fopen</code> در دسترس نیست.",
index 623e624..1c69e65 100644 (file)
@@ -77,8 +77,8 @@
        "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
        "config-env-php": "PHP $1 est installé.",
        "config-env-hhvm": "HHVM $1 est installé.",
-       "config-unicode-using-intl": "Utilisation de [https://pecl.php.net/intl l’extension PECL intl] pour la normalisation Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Attention :</strong> L’[https://pecl.php.net/intl extension PECL intl] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
+       "config-unicode-using-intl": "Utilisation de [https://php.net/manual/en/book.intl.php extension intl de PHP] pour la normalisation Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Attention :</strong> L’[https://php.net/manual/en/book.intl.php extension intl de PHP] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
        "config-unicode-update-warning": "<strong>Attention :</strong> la version installée du normalisateur Unicode utilise une ancienne version de la bibliothèque logicielle du [http://site.icu-project.org/ ''Projet ICU''].\nVous devriez faire une [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations mise à jour] si vous êtes concerné par l’usage d’Unicode.",
        "config-no-db": "Impossible de trouver un pilote de base de données approprié ! Vous devez installer un pilote de base de données pour PHP. {{PLURAL:$2|Le type suivant|Les types suivants}} de bases de données {{PLURAL:$2|est reconnu|sont reconnus}} : $1.\n\nSi vous avez compilé PHP vous-même, reconfigurez-le avec un client de base de données activé, par exemple en utilisant <code>./configure --with-mysqli</code>.  \nSi vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Attention :</strong> vous avez SQLite $2, qui est inférieur à la version minimale requise $1. SQLite sera indisponible.",
index 4e9781b..f1e441b 100644 (file)
@@ -10,6 +10,8 @@
        "config-title": "Instalo di MediaWiki $1",
        "config-information": "Informo",
        "config-localsettings-upgrade": "L'arkivo <code>LocalSettings.php</code> trovesis.\nPor plubonigar l'instaluro, voluntez informar la valoro dil  <code>$wgUpgradeKey</code> en l'infra buxo.\nVu trovos ol en <code>LocalSettings.php</code>.",
+       "config-session-error": "Eroro dum komenco di seciono: $1",
+       "config-session-expired": "Vua sesiono probable finis.\nSesioni programesis por durar $1\nVu povas augmentar to per modifiko di <code>session.gc_maxlifetime</code> en php.ini.\nRikomencez l'instalo-procedo.",
        "config-your-language": "Vua idiomo:",
        "config-your-language-help": "Selektez l'idiomo por uzar dum l'instalo-procedo.",
        "config-wiki-language": "Wiki linguo:",
        "config-env-bad": "Omno verifikesis.\nVu NE POVAS intalar MediaWiki.",
        "config-env-php": "PHP $1 instalesis.",
        "config-env-hhvm": "HHVM $1 instalesis.",
+       "config-unicode-pure-php-warning": "<strong>Atencez:</strong> La [https://php.net/manual/en/book.intl.php prolonguro PHP intl] ne esas disponebla por traktar skribo-normaligo \"Unicode\". Vice, uzesas la plu lenta laborado en pura PHP.\nSe vu administras pagini multe vizitata, vu mustas lektar la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations skribo-normaligo Unicode].",
+       "config-memory-raised": "Parametro <code>memory_limit</code> esas $1, modifikata a $2.",
+       "config-memory-bad": "<strong>Atences:</strong> la limito por PHP <code>memory_limit</code> esas $1.\nTo probable esas nesuficanta.\nL'instalo-procedo povas faliar!",
        "config-apc": "[https://www.php.net/apc APC] instalesis",
        "config-apcu": "[https://www.php.net/apcu APCu] instalesis",
+       "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] instalesis",
+       "config-using-uri": "Ret-adreso (URL) dil servero \"<nowiki>$1$2</nowiki>\".",
+       "config-db-wiki-settings": "Identifikez ca wiki",
+       "config-db-name": "Nomo dil datumaro (sen strekteti):",
+       "config-db-install-account": "Konto dil uzero por instalo",
+       "config-db-username": "Uzero-nomo dil datumaro:",
+       "config-db-password": "Pasovorto dil datumaro:",
+       "config-type-mssql": "Microsoft SQL Server",
+       "config-header-oracle": "Ajusti por Oracle-sistemo:",
+       "config-header-mssql": "Ajusti por Microsoft SQL Server",
+       "config-invalid-db-type": "Nevalida tipo di datumaro.",
+       "config-mysql-myisam": "MyISAM",
+       "config-ns-generic": "Projeto",
+       "config-ns-site-name": "Sama kam la wiki-nomo: $1",
+       "config-ns-other": "Altra (definez precise)",
+       "config-ns-other-default": "MyWiki",
+       "config-admin-name": "Vua uzero-nomo:",
+       "config-admin-password": "Pasovorto:",
+       "config-admin-password-confirm": "Riskribez la pasovorto:",
+       "config-admin-email": "E-postal adreso:",
+       "config-profile-wiki": "Aperta wiki",
+       "config-profile-no-anon": "Bezonas krear konto",
+       "config-profile-fishbowl": "Nur permisata redakteri",
        "config-profile-private": "Privata wiki",
+       "config-profile-help": "Wikis work best when you let as many people edit them as possible.\nIn MediaWiki, it is easy to review the recent changes, and to revert any damage that is done by naive or malicious users.\n\nHowever, many have found MediaWiki to be useful in a wide variety of roles, and sometimes it is not easy to convince everyone of the benefits of the wiki way.\nSo you have the choice.\n\nThe <strong>{{int:config-profile-wiki}}</strong> model allows anyone to edit, without even logging in.\nA wiki with <strong>{{int:config-profile-no-anon}}</strong> provides extra accountability, but may deter casual contributors.\n\nThe <strong>{{int:config-profile-fishbowl}}</strong> scenario allows approved users to edit, but the public can view the pages, including history.\nA <strong>{{int:config-profile-private}}</strong> only allows approved users to view pages, with the same group allowed to edit.\n\nMore complex user rights configurations are available after installation, see the [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights relevant manual entry].",
        "config-license": "Autoroyuro e permiso:",
        "config-license-cc-0": "Creative Commons Zero (Publika domeno)",
+       "config-license-pd": "Publika domeno",
        "config-install-step-done": "Facita",
        "config-install-step-failed": "faliis",
        "config-install-extensions": "Komplementi inkluzita",
index c878979..dd9c2ec 100644 (file)
@@ -68,8 +68,8 @@
        "config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
        "config-env-php": "PHP $1 è installato.",
        "config-env-hhvm": "HHVM $1 è installato.",
-       "config-unicode-using-intl": "Usa [https://pecl.php.net/intl l'estensione PECL intl] per la normalizzazione Unicode.",
-       "config-unicode-pure-php-warning": "'''Attenzione:''' [https://pecl.php.net/intl l'estensione PECL intl] non è disponibile per gestire la normalizzazione Unicode, quindi si torna alla lenta implementazione in PHP puro.\nSe esegui un sito ad alto traffico, dovresti leggere alcune considerazioni sulla [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzazione Unicode].",
+       "config-unicode-using-intl": "Usa [https://php.net/manual/en/book.intl.php l'estensione PHP intl] per la normalizzazione Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Attenzione:</strong> [https://php.net/manual/en/book.intl.php l'estensione PHP intl] non è disponibile per gestire la normalizzazione Unicode, quindi si torna alla lenta implementazione in PHP puro.\nSe esegui un sito ad alto traffico, dovresti leggere [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzazione Unicode].",
        "config-unicode-update-warning": "'''Attenzione:''' la versione installata del gestore per la normalizzazione Unicode usa una vecchia versione della libreria [http://site.icu-project.org/ del progetto ICU].\nDovresti [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aggiornare] se vuoi usare l'Unicode.",
        "config-no-db": "Impossibile trovare un driver adatto per il database! È necessario installare un driver per PHP.\n{{PLURAL:$2|Il seguente formato di database è supportato|I seguenti formati di database sono supportati}}: $1.\n\nSe compili PHP autonomamente, riconfiguralo attivando un client database, per esempio utilizzando <code>./configure --with-mysqli</code>.\nQualora avessi installato PHP per mezzo di un pacchetto Debian o Ubuntu, allora devi installare anche il pacchetto <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Attenzione</strong>: è presente SQLite $2 mentre è richiesta la versione $1, SQLite non sarà disponibile.",
index f8318f0..ec17b0b 100644 (file)
        "config-env-bad": "環境を確認しました。\nMediaWiki のインストールはできません。",
        "config-env-php": "PHP $1がインストールされています。",
        "config-env-hhvm": "HHVM $1 がインストールされています。",
-       "config-unicode-using-intl": "Unicode正規化に[https://pecl.php.net/intl intl PECL 拡張機能]を使用。",
-       "config-unicode-pure-php-warning": "<strong>警告:</strong> Unicode 正規化の処理に [https://pecl.php.net/intl intl PECL 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。\n高トラフィックのサイトを運営する場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正規化]をお読みください。",
+       "config-unicode-using-intl": "Unicode正規化に[https://php.net/manual/en/book.intl.php PHP intl \n 拡張機能]を使用。",
+       "config-unicode-pure-php-warning": "<strong>警告:</strong> Unicode 正規化の処理に[https://php.net/manual/en/book.intl.php PHP intl 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。\n高トラフィックのサイトを運営する場合、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正規化]は必ず読むよう推奨されます。",
        "config-unicode-update-warning": "<strong>警告:</strong> インストールされているバージョンの Unicode 正規化ラッパーは、[http://site.icu-project.org/ ICU プロジェクト]のライブラリの古いバージョンを使用しています。\nUnicode を少しでも利用する可能性がある場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations アップグレード]してください。",
        "config-no-db": "適切なデータベース ドライバーが見つかりませんでした! PHP にデータベース ドライバーをインストールする必要があります。\n以下の種類のデータベース{{PLURAL:$2|のタイプ}}に対応しています: $1\n\nPHP を自分でコンパイルした場合は、例えば <code>./configure --with-mysqli</code> を実行して、データベース クライアントを使用できるように再設定してください。\nDebian または Ubuntu のパッケージから PHP をインストールした場合は、モジュール (例: <code>php-mysql</code>) もインストールする必要があります。",
-       "config-outdated-sqlite": "<strong>警告:</strong> あなたは SQLite $1 を使用していますが、最低限必要なバージョン $2 より古いバージョンです。SQLite は利用できません。",
+       "config-outdated-sqlite": "<strong>警告:</strong> ご利用の SQLite $2 は容認されている最古の版 $1 よりも古い版です。SQLite が対応しません。",
        "config-no-fts3": "<strong>警告:</strong> SQLite は [//sqlite.org/fts3.html FTS3] モジュールなしでコンパイルされており、このバックエンドでは検索機能は利用できなくなります。",
        "config-pcre-old": "<strong>致命的エラー:</strong> PCRE $1 以降が必要です。\nご使用中の PHP のバイナリは PCRE $2 とリンクされています。\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE 詳細情報]",
        "config-pcre-no-utf8": "<strong>致命的エラー:</strong> PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。\nMediaWiki を正しく動作させるには、UTF-8 対応が必要です。",
index 1b34fc6..6a62fb6 100644 (file)
@@ -69,8 +69,8 @@
        "config-env-bad": "De omgeving is gecontroleerd.\nU kunt MediaWiki niet installeren.",
        "config-env-php": "PHP $1 is geïnstalleerd.",
        "config-env-hhvm": "HHVM $1 is geïnstalleerd.",
-       "config-unicode-using-intl": "Voor Unicode-normalisatie wordt de [https://pecl.php.net/intl PECL-extensie intl] gebruikt.",
-       "config-unicode-pure-php-warning": "<strong>Waarschuwing:</strong> de [https://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzamere PHP-implementatie gebruikt.\nAls u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
+       "config-unicode-using-intl": "Voor Unicode-normalisatie wordt de [https://php.net/manual/en/book.intl.php PHP-extensie intl] gebruikt.",
+       "config-unicode-pure-php-warning": "<strong>Waarschuwing:</strong> de [https://php.net/manual/en/book.intl.php PHP-uitbreiding intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzamere PHP-implementatie gebruikt.\nAls u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
        "config-unicode-update-warning": "<strong>Waarschuwing:</strong> de geïnstalleerde versie van de Unicodenormalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].\nU moet [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations bijwerken] als Unicode voor u van belang is.",
        "config-no-db": "Het was niet mogelijk een geschikte databasedriver te vinden voor PHP! U moet een databasedriver installeren voor PHP.\n{{PLURAL:$2|Het volgende databasetype wordt|De volgende databasetypes worden}} ondersteund: $1.\n\nAls u PHP zelf hebt gecompileerd, wijzig dan uw instellingen zodat een databasedriver wordt geactiveerd, bijvoorbeeld via <code>./configure --with-mysqli</code>.\nAls u PHP hebt geïnstalleerd via een Debian- of Ubuntu-package, installeer dan ook bijvoorbeeld de module <code>php-mysql</code>.",
        "config-outdated-sqlite": "''' Waarschuwing:''' u gebruikt SQLite $2. SQLite is niet beschikbaar omdat de minimaal vereiste versie $1 is.",
index e8f9078..18dc924 100644 (file)
@@ -70,8 +70,8 @@
        "config-env-bad": "Środowisko oprogramowania zostało sprawdzone.\nNie możesz zainstalować MediaWiki.",
        "config-env-php": "Zainstalowane jest PHP w wersji $1.",
        "config-env-hhvm": "Zainstalowany jest HHVM $1.",
-       "config-unicode-using-intl": "Korzystanie z [https://pecl.php.net/intl rozszerzenia intl PECL] do normalizacji Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Uwaga:<strong> [https://pecl.php.net/intl Rozszerzenie intl PECL] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
+       "config-unicode-using-intl": "Korzystanie z [https://php.net/manual/en/book.intl.php rozszerzenia PHP intl] do normalizacji Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Uwaga:<strong> [https://php.net/manual/en/book.intl.php rozszerzenie PHP intl] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
        "config-unicode-update-warning": "<strong>Uwaga:</strong> zainstalowana wersja normalizacji Unicode korzysta z nieaktualnej biblioteki [http://site.icu-project.org/ projektu ICU].\nPowinieneś [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations wykonać aktualizację], jeśli chcesz korzystać w pełni z Unicode.",
        "config-no-db": "Nie można odnaleźć właściwego sterownika bazy danych! Musisz zainstalować sterownik bazy danych dla PHP.\nMożna użyć {{PLURAL:$2|następującego typu bazy|następujących typów baz}} danych: $1.\n\nJeśli skompilowałeś PHP samodzielnie, skonfiguruj go ponownie z włączonym klientem bazy danych, na przykład za pomocą polecenia <code>./configure --with-mysqli</code>.\nJeśli zainstalowałeś PHP jako pakiet Debiana lub Ubuntu, musisz również zainstalować np. moduł <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Ostrzeżenie</strong>: masz SQLite  $2, która jest niższa od minimalnej wymaganej wersji  $1 . SQLite będzie niedostępne.",
index a17ca69..e9bb22b 100644 (file)
@@ -69,8 +69,8 @@
        "config-env-bad": "O ambiente foi verificado.\nVocê não pode instalar o MediaWiki.",
        "config-env-php": "O PHP $1 está instalado.",
        "config-env-hhvm": "O HHVM $1 está instalado.",
-       "config-unicode-using-intl": "Usando a [https://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [https://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
+       "config-unicode-using-intl": "Usando a [https://www.php.net/manual/pt_BR/book.intl.php extensão intl PHP] para a normalização Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [https://www.php.net/manual/pt_BR/book.intl.php extensão intl PHP] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
        "config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://www.site.icu-project.org/projeto ICU].\nVocê deve [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizar] se você tem quaisquer preocupações com o uso do Unicode.",
        "config-no-db": "Não foi possível encontrar um driver apropriado para a banco de dados! Você precisa instalar um driver de banco de dados para PHP. {{PLURAL:$2|É aceito o seguinte tipo|São aceitos os seguintes tipos}} de banco de dados: $1.\n\nSe você compilou o PHP, reconfigure-o com um cliente de banco de dados ativado, por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP a partir de um pacote Debian ou Ubuntu, então também precisa instalar, por exemplo, o pacote <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Aviso:</strong> você tem o SQLite versão $2, que é menor do que a versão mínima necessária $1. O SQLite não estará disponível.",
index 5471fdb..7543691 100644 (file)
@@ -50,7 +50,7 @@
        "config-env-bad": "Okolje je pregledano.\nNe morete namestiti MediaWiki.",
        "config-env-php": "Nameščen je PHP $1.",
        "config-env-hhvm": "HHVM $1 je nameščen.",
-       "config-unicode-using-intl": "Uporaba [https://pecl.php.net/intl razširitve PECL intl] za normalizacijo unikoda.",
+       "config-unicode-using-intl": "Uporaba [https://php.net/manual/en/book.intl.php PHP-razširitve intl] za normalizacijo unikoda.",
        "config-memory-raised": "PHP-jev <code>memory_limit</code> je $1, dvignjen na $2.",
        "config-apc": "[https://www.php.net/apc APC] je nameščen",
        "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] je nameščen",
index 3ba7928..12b5620 100644 (file)
@@ -55,8 +55,8 @@
        "config-env-bad": "Окружење је проверено.\nНе можете да инсталирате MediaWiki.",
        "config-env-php": "PHP $1 је инсталиран.",
        "config-env-hhvm": "HHVM $1 је инсталиран.",
-       "config-unicode-using-intl": "Користи се [https://pecl.php.net/intl додатак intl PECL] за нормализацију Уникода.",
-       "config-outdated-sqlite": "<strong>Упозорење:</strong> имате SQLite $1, који је нижи од најмање тражене верзије ($2). SQLite ће бити недоступан.",
+       "config-unicode-using-intl": "Користи се [https://php.net/manual/en/book.intl.php PHP intl додатак] за нормализацију Уникода.",
+       "config-outdated-sqlite": "<strong>Упозорење:</strong> имате SQLite $2, који је нижи од најмање тражене верзије $1. SQLite ће бити недоступан.",
        "config-no-fts3": "<strong>Упозорење:</strong> SQLite је компајлиран без [//sqlite.org/fts3.html FTS3 модула], функције претраге биће недоступне на овој бази података.",
        "config-pcre-old": "<strong>Неотклоњива грешка:</strong> Неопходан је PCRE $1 или новији.\nВаш бинарни PHP је повезан са PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Више информација].",
        "config-pcre-no-utf8": "<strong>Неотклоњива грешка:</strong> Изгледа да је PCRE модул PHP-а  компајлиран без PCRE_UTF8 подршке.\nMediaWiki захтева UTF-8 подршку за исправно функционисање.",
        "config-install-done": "<strong>Честитамо!</strong>\nИнсталирали сте MediaWiki.\n\nИнсталациони програм је генерисао датотеку <code>LocalSettings.php</code>.\nОна садржи сву вашу конфигурацију.\n\nМораћете да је преузмете и ставите у базу ваше вики инсталације (исти директоријум као index.php). Преузимање би аутоматски требало почети.\n\nАко преузимање није понуђено, или ако га откажете, можете поново покренути преузимање тако што ћете кликнути на доленаведену везу:\n\n$3\n\n<strong>Напомена:</strong> Ако то одмах не урадите, ова генерисана конфигурациона датотека неће вам бити доступна касније ако изађете из инсталације без преузимања.\n\nКада је то учињено, можете да <strong>[$2 посетите свој вики]</strong>.",
        "config-install-done-path": "<strong>Честитамо!</strong>\nИнсталирали сте MediaWiki.\n\nИнсталациони програм је генерисао датотеку <code>LocalSettings.php</code>.\nОна садржи сву вашу конфигурацију.\n\nМораћете да је преузмете и ставите у <code>$4</code>. Преузимање би аутоматски требало почети.\n\nАко преузимање није понуђено, или ако га откажете, можете поново покренути преузимање тако што ћете кликнути на доленаведену везу:\n\n$3\n\n<strong>Напомена:</strong> Ако то одмах не урадите, ова генерисана конфигурациона датотека неће вам бити доступна касније ако изађете из инсталације без преузимања.\n\nКада је то учињено, можете да <strong>[$2 посетите свој вики]</strong>.",
        "config-install-success": "MediaWiki је успешно инсталиран. Сада можете посетити <$1$2> да бисте видели свој вики.\nАко имате питања, погледајте нашу листу често постављаних питања: <https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ> или користите један од форума за подршку који су повезани на тој страници.",
+       "config-install-db-success": "База података је успешно подешена",
        "config-download-localsettings": "Преузми датотеку <code>LocalSettings.php</code>",
        "config-help": "помоћ",
        "config-help-tooltip": "кликните да бисте проширили",
index 676659f..454f694 100644 (file)
@@ -535,7 +535,7 @@ class JobRunner implements LoggerAwareInterface {
 
                $time = false;
                $lb = $lbFactory->getMainLB();
-               if ( $syncThreshold !== false && $lb->getServerCount() > 1 ) {
+               if ( $syncThreshold !== false && $lb->hasStreamingReplicaServers() ) {
                        // Generally, there is one master connection to the local DB
                        $dbwSerial = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
                        // We need natively blocking fast locks
index 80a46d0..19ff967 100644 (file)
@@ -27,8 +27,8 @@
  * @code
  * $job = new JobSpecification(
  *             'null',
- *             array( 'lives' => 1, 'usleep' => 100, 'pi' => 3.141569 ),
- *             array( 'removeDuplicates' => 1 )
+ *             [ 'lives' => 1, 'usleep' => 100, 'pi' => 3.141569 ],
+ *             [ 'removeDuplicates' => 1 ]
  * );
  * JobQueueGroup::singleton()->push( $job )
  * @endcode
index 3aedc38..be76fc6 100644 (file)
@@ -91,9 +91,9 @@ class CategoryMembershipChangeJob extends Job {
                        return false; // deleted?
                }
 
-               // Cut down on the time spent in safeWaitForMasterPos() in the critical section
+               // Cut down on the time spent in waitForMasterPos() in the critical section
                $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
-               if ( !$lb->safeWaitForMasterPos( $dbr ) ) {
+               if ( !$lb->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( "Timed out while pre-waiting for replica DB to catch up" );
                        return false;
                }
@@ -107,7 +107,7 @@ class CategoryMembershipChangeJob extends Job {
                }
 
                // Wait till replica DB is caught up so that jobs for this page see each others' changes
-               if ( !$lb->safeWaitForMasterPos( $dbr ) ) {
+               if ( !$lb->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( "Timed out while waiting for replica DB to catch up" );
                        return false;
                }
index 0cb1a52..1793053 100644 (file)
@@ -44,7 +44,7 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
                $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] );
 
                // Wait before lock to try to reduce time waiting in the lock.
-               if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) {
+               if ( !$loadBalancer->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( 'Timed out waiting for replica to catch up before lock' );
                        return false;
                }
@@ -57,7 +57,7 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
                        return false;
                }
 
-               if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) {
+               if ( !$loadBalancer->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( 'Timed out waiting for replica to catch up within lock' );
                        return false;
                }
diff --git a/includes/language/LanguageCode.php b/includes/language/LanguageCode.php
new file mode 100644 (file)
index 0000000..7d954d3
--- /dev/null
@@ -0,0 +1,204 @@
+<?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 Language
+ */
+
+/**
+ * Methods for dealing with language codes.
+ * @todo Move some of the code-related static methods out of Language into this class
+ *
+ * @since 1.29
+ * @ingroup Language
+ */
+class LanguageCode {
+       /**
+        * Mapping of deprecated language codes that were used in previous
+        * versions of MediaWiki to up-to-date, current language codes.
+        * These may or may not be valid BCP 47 codes; they are included here
+        * because MediaWiki renamed these particular codes at some point.
+        *
+        * @var array Mapping from deprecated MediaWiki-internal language code
+        *   to replacement MediaWiki-internal language code.
+        *
+        * @since 1.30
+        * @see https://meta.wikimedia.org/wiki/Special_language_codes
+        */
+       private static $deprecatedLanguageCodeMapping = [
+               // Note that als is actually a valid ISO 639 code (Tosk Albanian), but it
+               // was previously used in MediaWiki for Alsatian, which comes under gsw
+               'als' => 'gsw', // T25215
+               'bat-smg' => 'sgs', // T27522
+               'be-x-old' => 'be-tarask', // T11823
+               'fiu-vro' => 'vro', // T31186
+               'roa-rup' => 'rup', // T17988
+               'zh-classical' => 'lzh', // T30443
+               'zh-min-nan' => 'nan', // T30442
+               'zh-yue' => 'yue', // T30441
+       ];
+
+       /**
+        * Mapping of non-standard language codes used in MediaWiki to
+        * standardized BCP 47 codes.  These are not deprecated (yet?):
+        * IANA may eventually recognize the subtag, in which case the `-x-`
+        * infix could be removed, or else we could rename the code in
+        * MediaWiki, in which case they'd move up to the above mapping
+        * of deprecated codes.
+        *
+        * As a rule, we preserve all distinctions made by MediaWiki
+        * internally.  For example, `de-formal` becomes `de-x-formal`
+        * instead of just `de` because MediaWiki distinguishes `de-formal`
+        * from `de` (for example, for interface translations).  Similarly,
+        * BCP 47 indicates that `kk-Cyrl` SHOULD not be used because it
+        * "typically does not add information", but in our case MediaWiki
+        * LanguageConverter distinguishes `kk` (render content in a mix of
+        * Kurdish variants) from `kk-Cyrl` (convert content to be uniformly
+        * Cyrillic).  As the BCP 47 requirement is a SHOULD not a MUST,
+        * `kk-Cyrl` is a valid code, although some validators may emit
+        * a warning note.
+        *
+        * @var array Mapping from nonstandard MediaWiki-internal codes to
+        *   BCP 47 codes
+        *
+        * @since 1.32
+        * @see https://meta.wikimedia.org/wiki/Special_language_codes
+        * @see https://phabricator.wikimedia.org/T125073
+        */
+       private static $nonstandardLanguageCodeMapping = [
+               // All codes returned by Language::fetchLanguageNames() validated
+               // against IANA registry at
+               //   https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
+               // with help of validator at
+               //   http://schneegans.de/lv/
+               'cbk-zam' => 'cbk', // T124657
+               'de-formal' => 'de-x-formal',
+               'eml' => 'egl', // T36217
+               'en-rtl' => 'en-x-rtl',
+               'es-formal' => 'es-x-formal',
+               'hu-formal' => 'hu-x-formal',
+               'map-bms' => 'jv-x-bms', // [[en:Banyumasan_dialect]] T125073
+               'mo' => 'ro-Cyrl-MD', // T125073
+               'nrm' => 'nrf', // [[en:Norman_language]] T25216
+               'nl-informal' => 'nl-x-informal',
+               'roa-tara' => 'nap-x-tara', // [[en:Tarantino_dialect]]
+               'simple' => 'en-simple',
+               'sr-ec' => 'sr-Cyrl', // T117845
+               'sr-el' => 'sr-Latn', // T117845
+
+               // Although these next codes aren't *wrong* per se, including
+               // both the script and the country code helps compatibility with
+               // other BCP 47 users. Note that MW also uses `zh-Hans`/`zh-Hant`,
+               // without a country code, and those should be left alone.
+               // (See $variantfallbacks in LanguageZh.php for Hans/Hant id.)
+               'zh-cn' => 'zh-Hans-CN',
+               'zh-sg' => 'zh-Hans-SG',
+               'zh-my' => 'zh-Hans-MY',
+               'zh-tw' => 'zh-Hant-TW',
+               'zh-hk' => 'zh-Hant-HK',
+               'zh-mo' => 'zh-Hant-MO',
+       ];
+
+       /**
+        * Returns a mapping of deprecated language codes that were used in previous
+        * versions of MediaWiki to up-to-date, current language codes.
+        *
+        * This array is merged into $wgDummyLanguageCodes in Setup.php, along with
+        * the fake language codes 'qqq' and 'qqx', which are used internally by
+        * MediaWiki's localisation system.
+        *
+        * @return string[]
+        *
+        * @since 1.29
+        */
+       public static function getDeprecatedCodeMapping() {
+               return self::$deprecatedLanguageCodeMapping;
+       }
+
+       /**
+        * Returns a mapping of non-standard language codes used by
+        * (current and previous version of) MediaWiki, mapped to standard
+        * BCP 47 names.
+        *
+        * This array is exported to JavaScript to ensure
+        * mediawiki.language.bcp47 stays in sync with LanguageCode::bcp47().
+        *
+        * @return string[]
+        *
+        * @since 1.32
+        */
+       public static function getNonstandardLanguageCodeMapping() {
+               $result = [];
+               foreach ( self::$deprecatedLanguageCodeMapping as $code => $ignore ) {
+                       $result[$code] = self::bcp47( $code );
+               }
+               foreach ( self::$nonstandardLanguageCodeMapping as $code => $ignore ) {
+                       $result[$code] = self::bcp47( $code );
+               }
+               return $result;
+       }
+
+       /**
+        * Replace deprecated language codes that were used in previous
+        * versions of MediaWiki to up-to-date, current language codes.
+        * Other values will returned unchanged.
+        *
+        * @param string $code Old language code
+        * @return string New language code
+        *
+        * @since 1.30
+        */
+       public static function replaceDeprecatedCodes( $code ) {
+               return self::$deprecatedLanguageCodeMapping[$code] ?? $code;
+       }
+
+       /**
+        * Get the normalised IETF language tag
+        * See unit test for examples.
+        * See mediawiki.language.bcp47 for the JavaScript implementation.
+        *
+        * @param string $code The language code.
+        * @return string A language code complying with BCP 47 standards.
+        *
+        * @since 1.31
+        */
+       public static function bcp47( $code ) {
+               $code = self::replaceDeprecatedCodes( strtolower( $code ) );
+               if ( isset( self::$nonstandardLanguageCodeMapping[$code] ) ) {
+                       $code = self::$nonstandardLanguageCodeMapping[$code];
+               }
+               $codeSegment = explode( '-', $code );
+               $codeBCP = [];
+               foreach ( $codeSegment as $segNo => $seg ) {
+                       // when previous segment is x, it is a private segment and should be lc
+                       if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) {
+                               $codeBCP[$segNo] = strtolower( $seg );
+                       // ISO 3166 country code
+                       } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) {
+                               $codeBCP[$segNo] = strtoupper( $seg );
+                       // ISO 15924 script code
+                       } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) {
+                               $codeBCP[$segNo] = ucfirst( strtolower( $seg ) );
+                       // Use lowercase for other cases
+                       } else {
+                               $codeBCP[$segNo] = strtolower( $seg );
+                       }
+               }
+               $langCode = implode( '-', $codeBCP );
+               return $langCode;
+       }
+}
diff --git a/includes/language/Message.php b/includes/language/Message.php
new file mode 100644 (file)
index 0000000..0b3113f
--- /dev/null
@@ -0,0 +1,1396 @@
+<?php
+/**
+ * Fetching and processing of interface messages.
+ *
+ * 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
+ * @author Niklas Laxström
+ */
+use MediaWiki\MediaWikiServices;
+
+/**
+ * The Message class provides methods which fulfil two basic services:
+ *  - fetching interface messages
+ *  - processing messages into a variety of formats
+ *
+ * First implemented with MediaWiki 1.17, the Message class is intended to
+ * replace the old wfMsg* functions that over time grew unusable.
+ * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
+ * between old and new functions.
+ *
+ * You should use the wfMessage() global function which acts as a wrapper for
+ * the Message class. The wrapper let you pass parameters as arguments.
+ *
+ * The most basic usage cases would be:
+ *
+ * @code
+ *     // Initialize a Message object using the 'some_key' message key
+ *     $message = wfMessage( 'some_key' );
+ *
+ *     // Using two parameters those values are strings 'value1' and 'value2':
+ *     $message = wfMessage( 'some_key',
+ *          'value1', 'value2'
+ *     );
+ * @endcode
+ *
+ * @section message_global_fn Global function wrapper:
+ *
+ * Since wfMessage() returns a Message instance, you can chain its call with
+ * a method. Some of them return a Message instance too so you can chain them.
+ * You will find below several examples of wfMessage() usage.
+ *
+ * Fetching a message text for interface message:
+ *
+ * @code
+ *    $button = Xml::button(
+ *         wfMessage( 'submit' )->text()
+ *    );
+ * @endcode
+ *
+ * A Message instance can be passed parameters after it has been constructed,
+ * use the params() method to do so:
+ *
+ * @code
+ *     wfMessage( 'welcome-to' )
+ *         ->params( $wgSitename )
+ *         ->text();
+ * @endcode
+ *
+ * {{GRAMMAR}} and friends work correctly:
+ *
+ * @code
+ *    wfMessage( 'are-friends',
+ *        $user, $friend
+ *    );
+ *    wfMessage( 'bad-message' )
+ *         ->rawParams( '<script>...</script>' )
+ *         ->escaped();
+ * @endcode
+ *
+ * @section message_language Changing language:
+ *
+ * Messages can be requested in a different language or in whatever current
+ * content language is being used. The methods are:
+ *     - Message->inContentLanguage()
+ *     - Message->inLanguage()
+ *
+ * Sometimes the message text ends up in the database, so content language is
+ * needed:
+ *
+ * @code
+ *    wfMessage( 'file-log',
+ *        $user, $filename
+ *    )->inContentLanguage()->text();
+ * @endcode
+ *
+ * Checking whether a message exists:
+ *
+ * @code
+ *    wfMessage( 'mysterious-message' )->exists()
+ *    // returns a boolean whether the 'mysterious-message' key exist.
+ * @endcode
+ *
+ * If you want to use a different language:
+ *
+ * @code
+ *    $userLanguage = $user->getOption( 'language' );
+ *    wfMessage( 'email-header' )
+ *         ->inLanguage( $userLanguage )
+ *         ->plain();
+ * @endcode
+ *
+ * @note You can parse the text only in the content or interface languages
+ *
+ * @section message_compare_old Comparison with old wfMsg* functions:
+ *
+ * Use full parsing:
+ *
+ * @code
+ *     // old style:
+ *     wfMsgExt( 'key', [ 'parseinline' ], 'apple' );
+ *     // new style:
+ *     wfMessage( 'key', 'apple' )->parse();
+ * @endcode
+ *
+ * Parseinline is used because it is more useful when pre-building HTML.
+ * In normal use it is better to use OutputPage::(add|wrap)WikiMsg.
+ *
+ * Places where HTML cannot be used. {{-transformation is done.
+ * @code
+ *     // old style:
+ *     wfMsgExt( 'key', [ 'parsemag' ], 'apple', 'pear' );
+ *     // new style:
+ *     wfMessage( 'key', 'apple', 'pear' )->text();
+ * @endcode
+ *
+ * Shortcut for escaping the message too, similar to wfMsgHTML(), but
+ * parameters are not replaced after escaping by default.
+ * @code
+ *     $escaped = wfMessage( 'key' )
+ *          ->rawParams( 'apple' )
+ *          ->escaped();
+ * @endcode
+ *
+ * @section message_appendix Appendix:
+ *
+ * @todo
+ * - test, can we have tests?
+ * - this documentation needs to be extended
+ *
+ * @see https://www.mediawiki.org/wiki/WfMessage()
+ * @see https://www.mediawiki.org/wiki/New_messages_API
+ * @see https://www.mediawiki.org/wiki/Localisation
+ *
+ * @since 1.17
+ */
+class Message implements MessageSpecifier, Serializable {
+       /** Use message text as-is */
+       const FORMAT_PLAIN = 'plain';
+       /** Use normal wikitext -> HTML parsing (the result will be wrapped in a block-level HTML tag) */
+       const FORMAT_BLOCK_PARSE = 'block-parse';
+       /** Use normal wikitext -> HTML parsing but strip the block-level wrapper */
+       const FORMAT_PARSE = 'parse';
+       /** Transform {{..}} constructs but don't transform to HTML */
+       const FORMAT_TEXT = 'text';
+       /** Transform {{..}} constructs, HTML-escape the result */
+       const FORMAT_ESCAPED = 'escaped';
+
+       /**
+        * Mapping from Message::listParam() types to Language methods.
+        * @var array
+        */
+       protected static $listTypeMap = [
+               'comma' => 'commaList',
+               'semicolon' => 'semicolonList',
+               'pipe' => 'pipeList',
+               'text' => 'listToText',
+       ];
+
+       /**
+        * In which language to get this message. True, which is the default,
+        * means the current user language, false content language.
+        *
+        * @var bool
+        */
+       protected $interface = true;
+
+       /**
+        * In which language to get this message. Overrides the $interface setting.
+        *
+        * @var Language|bool Explicit language object, or false for user language
+        */
+       protected $language = false;
+
+       /**
+        * @var string The message key. If $keysToTry has more than one element,
+        * this may change to one of the keys to try when fetching the message text.
+        */
+       protected $key;
+
+       /**
+        * @var string[] List of keys to try when fetching the message.
+        */
+       protected $keysToTry;
+
+       /**
+        * @var array List of parameters which will be substituted into the message.
+        */
+       protected $parameters = [];
+
+       /**
+        * @var string
+        * @deprecated
+        */
+       protected $format = 'parse';
+
+       /**
+        * @var bool Whether database can be used.
+        */
+       protected $useDatabase = true;
+
+       /**
+        * @var Title Title object to use as context.
+        */
+       protected $title = null;
+
+       /**
+        * @var Content Content object representing the message.
+        */
+       protected $content = null;
+
+       /**
+        * @var string
+        */
+       protected $message;
+
+       /**
+        * @since 1.17
+        * @param string|string[]|MessageSpecifier $key Message key, or array of
+        * message keys to try and use the first non-empty message for, or a
+        * MessageSpecifier to copy from.
+        * @param array $params Message parameters.
+        * @param Language|null $language [optional] Language to use (defaults to current user language).
+        * @throws InvalidArgumentException
+        */
+       public function __construct( $key, $params = [], Language $language = null ) {
+               if ( $key instanceof MessageSpecifier ) {
+                       if ( $params ) {
+                               throw new InvalidArgumentException(
+                                       '$params must be empty if $key is a MessageSpecifier'
+                               );
+                       }
+                       $params = $key->getParams();
+                       $key = $key->getKey();
+               }
+
+               if ( !is_string( $key ) && !is_array( $key ) ) {
+                       throw new InvalidArgumentException( '$key must be a string or an array' );
+               }
+
+               $this->keysToTry = (array)$key;
+
+               if ( empty( $this->keysToTry ) ) {
+                       throw new InvalidArgumentException( '$key must not be an empty list' );
+               }
+
+               $this->key = reset( $this->keysToTry );
+
+               $this->parameters = array_values( $params );
+               // User language is only resolved in getLanguage(). This helps preserve the
+               // semantic intent of "user language" across serialize() and unserialize().
+               $this->language = $language ?: false;
+       }
+
+       /**
+        * @see Serializable::serialize()
+        * @since 1.26
+        * @return string
+        */
+       public function serialize() {
+               return serialize( [
+                       'interface' => $this->interface,
+                       'language' => $this->language ? $this->language->getCode() : false,
+                       'key' => $this->key,
+                       'keysToTry' => $this->keysToTry,
+                       'parameters' => $this->parameters,
+                       'format' => $this->format,
+                       'useDatabase' => $this->useDatabase,
+                       'titlestr' => $this->title ? $this->title->getFullText() : null,
+               ] );
+       }
+
+       /**
+        * @see Serializable::unserialize()
+        * @since 1.26
+        * @param string $serialized
+        */
+       public function unserialize( $serialized ) {
+               $data = unserialize( $serialized );
+               if ( !is_array( $data ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ': Invalid serialized data' );
+               }
+
+               $this->interface = $data['interface'];
+               $this->key = $data['key'];
+               $this->keysToTry = $data['keysToTry'];
+               $this->parameters = $data['parameters'];
+               $this->format = $data['format'];
+               $this->useDatabase = $data['useDatabase'];
+               $this->language = $data['language'] ? Language::factory( $data['language'] ) : false;
+
+               if ( isset( $data['titlestr'] ) ) {
+                       $this->title = Title::newFromText( $data['titlestr'] );
+               } elseif ( isset( $data['title'] ) && $data['title'] instanceof Title ) {
+                       // Old serializations from before December 2018
+                       $this->title = $data['title'];
+               } else {
+                       $this->title = null; // Explicit for sanity
+               }
+       }
+
+       /**
+        * @since 1.24
+        *
+        * @return bool True if this is a multi-key message, that is, if the key provided to the
+        * constructor was a fallback list of keys to try.
+        */
+       public function isMultiKey() {
+               return count( $this->keysToTry ) > 1;
+       }
+
+       /**
+        * @since 1.24
+        *
+        * @return string[] The list of keys to try when fetching the message text,
+        * in order of preference.
+        */
+       public function getKeysToTry() {
+               return $this->keysToTry;
+       }
+
+       /**
+        * Returns the message key.
+        *
+        * If a list of multiple possible keys was supplied to the constructor, this method may
+        * return any of these keys. After the message has been fetched, this method will return
+        * the key that was actually used to fetch the message.
+        *
+        * @since 1.21
+        *
+        * @return string
+        */
+       public function getKey() {
+               return $this->key;
+       }
+
+       /**
+        * Returns the message parameters.
+        *
+        * @since 1.21
+        *
+        * @return array
+        */
+       public function getParams() {
+               return $this->parameters;
+       }
+
+       /**
+        * Returns the message format.
+        *
+        * @since 1.21
+        *
+        * @return string
+        * @deprecated since 1.29 formatting is not stateful
+        */
+       public function getFormat() {
+               wfDeprecated( __METHOD__, '1.29' );
+               return $this->format;
+       }
+
+       /**
+        * Returns the Language of the Message.
+        *
+        * @since 1.23
+        *
+        * @return Language
+        */
+       public function getLanguage() {
+               // Defaults to false which means current user language
+               return $this->language ?: RequestContext::getMain()->getLanguage();
+       }
+
+       /**
+        * Factory function that is just wrapper for the real constructor. It is
+        * intended to be used instead of the real constructor, because it allows
+        * chaining method calls, while new objects don't.
+        *
+        * @since 1.17
+        *
+        * @param string|string[]|MessageSpecifier $key
+        * @param mixed $param,... Parameters as strings.
+        *
+        * @return Message
+        */
+       public static function newFromKey( $key /*...*/ ) {
+               $params = func_get_args();
+               array_shift( $params );
+               return new self( $key, $params );
+       }
+
+       /**
+        * Transform a MessageSpecifier or a primitive value used interchangeably with
+        * specifiers (a message key string, or a key + params array) into a proper Message.
+        *
+        * Also accepts a MessageSpecifier inside an array: that's not considered a valid format
+        * but is an easy error to make due to how StatusValue stores messages internally.
+        * Further array elements are ignored in that case.
+        *
+        * @param string|array|MessageSpecifier $value
+        * @return Message
+        * @throws InvalidArgumentException
+        * @since 1.27
+        */
+       public static function newFromSpecifier( $value ) {
+               $params = [];
+               if ( is_array( $value ) ) {
+                       $params = $value;
+                       $value = array_shift( $params );
+               }
+
+               if ( $value instanceof Message ) { // Message, RawMessage, ApiMessage, etc
+                       $message = clone $value;
+               } elseif ( $value instanceof MessageSpecifier ) {
+                       $message = new Message( $value );
+               } elseif ( is_string( $value ) ) {
+                       $message = new Message( $value, $params );
+               } else {
+                       throw new InvalidArgumentException( __METHOD__ . ': invalid argument type '
+                               . gettype( $value ) );
+               }
+
+               return $message;
+       }
+
+       /**
+        * Factory function accepting multiple message keys and returning a message instance
+        * for the first message which is non-empty. If all messages are empty then an
+        * instance of the first message key is returned.
+        *
+        * @since 1.18
+        *
+        * @param string|string[] $keys,... Message keys, or first argument as an array of all the
+        * message keys.
+        *
+        * @return Message
+        */
+       public static function newFallbackSequence( /*...*/ ) {
+               $keys = func_get_args();
+               if ( func_num_args() == 1 ) {
+                       if ( is_array( $keys[0] ) ) {
+                               // Allow an array to be passed as the first argument instead
+                               $keys = array_values( $keys[0] );
+                       } else {
+                               // Optimize a single string to not need special fallback handling
+                               $keys = $keys[0];
+                       }
+               }
+               return new self( $keys );
+       }
+
+       /**
+        * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace.
+        * The title will be for the current language, if the message key is in
+        * $wgForceUIMsgAsContentMsg it will be append with the language code (except content
+        * language), because Message::inContentLanguage will also return in user language.
+        *
+        * @see $wgForceUIMsgAsContentMsg
+        * @return Title
+        * @since 1.26
+        */
+       public function getTitle() {
+               global $wgForceUIMsgAsContentMsg;
+
+               $contLang = MediaWikiServices::getInstance()->getContentLanguage();
+               $lang = $this->getLanguage();
+               $title = $this->key;
+               if (
+                       !$lang->equals( $contLang )
+                       && in_array( $this->key, (array)$wgForceUIMsgAsContentMsg )
+               ) {
+                       $title .= '/' . $lang->getCode();
+               }
+
+               return Title::makeTitle(
+                       NS_MEDIAWIKI, $contLang->ucfirst( strtr( $title, ' ', '_' ) ) );
+       }
+
+       /**
+        * Adds parameters to the parameter list of this message.
+        *
+        * @since 1.17
+        *
+        * @param mixed $args,... Parameters as strings or arrays from
+        *  Message::numParam() and the like, or a single array of parameters.
+        *
+        * @return Message $this
+        */
+       public function params( /*...*/ ) {
+               $args = func_get_args();
+
+               // If $args has only one entry and it's an array, then it's either a
+               // non-varargs call or it happens to be a call with just a single
+               // "special" parameter. Since the "special" parameters don't have any
+               // numeric keys, we'll test that to differentiate the cases.
+               if ( count( $args ) === 1 && isset( $args[0] ) && is_array( $args[0] ) ) {
+                       if ( $args[0] === [] ) {
+                               $args = [];
+                       } else {
+                               foreach ( $args[0] as $key => $value ) {
+                                       if ( is_int( $key ) ) {
+                                               $args = $args[0];
+                                               break;
+                                       }
+                               }
+                       }
+               }
+
+               $this->parameters = array_merge( $this->parameters, array_values( $args ) );
+               return $this;
+       }
+
+       /**
+        * Add parameters that are substituted after parsing or escaping.
+        * In other words the parsing process cannot access the contents
+        * of this type of parameter, and you need to make sure it is
+        * sanitized beforehand.  The parser will see "$n", instead.
+        *
+        * @since 1.17
+        *
+        * @param mixed $params,... Raw parameters as strings, or a single argument that is
+        * an array of raw parameters.
+        *
+        * @return Message $this
+        */
+       public function rawParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::rawParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are numeric and will be passed through
+        * Language::formatNum before substitution
+        *
+        * @since 1.18
+        *
+        * @param mixed $param,... Numeric parameters, or a single argument that is
+        * an array of numeric parameters.
+        *
+        * @return Message $this
+        */
+       public function numParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::numParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are durations of time and will be passed through
+        * Language::formatDuration before substitution
+        *
+        * @since 1.22
+        *
+        * @param int|int[] $param,... Duration parameters, or a single argument that is
+        * an array of duration parameters.
+        *
+        * @return Message $this
+        */
+       public function durationParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::durationParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are expiration times and will be passed through
+        * Language::formatExpiry before substitution
+        *
+        * @since 1.22
+        *
+        * @param string|string[] $param,... Expiry parameters, or a single argument that is
+        * an array of expiry parameters.
+        *
+        * @return Message $this
+        */
+       public function expiryParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::expiryParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are time periods and will be passed through
+        * Language::formatTimePeriod before substitution
+        *
+        * @since 1.22
+        *
+        * @param int|int[] $param,... Time period parameters, or a single argument that is
+        * an array of time period parameters.
+        *
+        * @return Message $this
+        */
+       public function timeperiodParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::timeperiodParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are file sizes and will be passed through
+        * Language::formatSize before substitution
+        *
+        * @since 1.22
+        *
+        * @param int|int[] $param,... Size parameters, or a single argument that is
+        * an array of size parameters.
+        *
+        * @return Message $this
+        */
+       public function sizeParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::sizeParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are bitrates and will be passed through
+        * Language::formatBitrate before substitution
+        *
+        * @since 1.22
+        *
+        * @param int|int[] $param,... Bit rate parameters, or a single argument that is
+        * an array of bit rate parameters.
+        *
+        * @return Message $this
+        */
+       public function bitrateParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::bitrateParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Add parameters that are plaintext and will be passed through without
+        * the content being evaluated.  Plaintext parameters are not valid as
+        * arguments to parser functions. This differs from self::rawParams in
+        * that the Message class handles escaping to match the output format.
+        *
+        * @since 1.25
+        *
+        * @param string|string[] $param,... plaintext parameters, or a single argument that is
+        * an array of plaintext parameters.
+        *
+        * @return Message $this
+        */
+       public function plaintextParams( /*...*/ ) {
+               $params = func_get_args();
+               if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+               foreach ( $params as $param ) {
+                       $this->parameters[] = self::plaintextParam( $param );
+               }
+               return $this;
+       }
+
+       /**
+        * Set the language and the title from a context object
+        *
+        * @since 1.19
+        *
+        * @param IContextSource $context
+        *
+        * @return Message $this
+        */
+       public function setContext( IContextSource $context ) {
+               $this->inLanguage( $context->getLanguage() );
+               $this->title( $context->getTitle() );
+               $this->interface = true;
+
+               return $this;
+       }
+
+       /**
+        * Request the message in any language that is supported.
+        *
+        * As a side effect interface message status is unconditionally
+        * turned off.
+        *
+        * @since 1.17
+        * @param Language|string $lang Language code or Language object.
+        * @return Message $this
+        * @throws MWException
+        */
+       public function inLanguage( $lang ) {
+               $previousLanguage = $this->language;
+
+               if ( $lang instanceof Language ) {
+                       $this->language = $lang;
+               } elseif ( is_string( $lang ) ) {
+                       if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) {
+                               $this->language = Language::factory( $lang );
+                       }
+               } elseif ( $lang instanceof StubUserLang ) {
+                       $this->language = false;
+               } else {
+                       $type = gettype( $lang );
+                       throw new MWException( __METHOD__ . " must be "
+                               . "passed a String or Language object; $type given"
+                       );
+               }
+
+               if ( $this->language !== $previousLanguage ) {
+                       // The language has changed. Clear the message cache.
+                       $this->message = null;
+               }
+               $this->interface = false;
+               return $this;
+       }
+
+       /**
+        * Request the message in the wiki's content language,
+        * unless it is disabled for this message.
+        *
+        * @since 1.17
+        * @see $wgForceUIMsgAsContentMsg
+        *
+        * @return Message $this
+        */
+       public function inContentLanguage() {
+               global $wgForceUIMsgAsContentMsg;
+               if ( in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) {
+                       return $this;
+               }
+
+               $this->inLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
+               return $this;
+       }
+
+       /**
+        * Allows manipulating the interface message flag directly.
+        * Can be used to restore the flag after setting a language.
+        *
+        * @since 1.20
+        *
+        * @param bool $interface
+        *
+        * @return Message $this
+        */
+       public function setInterfaceMessageFlag( $interface ) {
+               $this->interface = (bool)$interface;
+               return $this;
+       }
+
+       /**
+        * Enable or disable database use.
+        *
+        * @since 1.17
+        *
+        * @param bool $useDatabase
+        *
+        * @return Message $this
+        */
+       public function useDatabase( $useDatabase ) {
+               $this->useDatabase = (bool)$useDatabase;
+               $this->message = null;
+               return $this;
+       }
+
+       /**
+        * Set the Title object to use as context when transforming the message
+        *
+        * @since 1.18
+        *
+        * @param Title $title
+        *
+        * @return Message $this
+        */
+       public function title( $title ) {
+               $this->title = $title;
+               return $this;
+       }
+
+       /**
+        * Returns the message as a Content object.
+        *
+        * @return Content
+        */
+       public function content() {
+               if ( !$this->content ) {
+                       $this->content = new MessageContent( $this );
+               }
+
+               return $this->content;
+       }
+
+       /**
+        * Returns the message parsed from wikitext to HTML.
+        *
+        * @since 1.17
+        *
+        * @param string|null $format One of the FORMAT_* constants. Null means use whatever was used
+        *   the last time (this is for B/C and should be avoided).
+        *
+        * @return string HTML
+        * @suppress SecurityCheck-DoubleEscaped phan false positive
+        */
+       public function toString( $format = null ) {
+               if ( $format === null ) {
+                       $ex = new LogicException( __METHOD__ . ' using implicit format: ' . $this->format );
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'message-format' )->warning(
+                               $ex->getMessage(), [ 'exception' => $ex, 'format' => $this->format, 'key' => $this->key ] );
+                       $format = $this->format;
+               }
+               $string = $this->fetchMessage();
+
+               if ( $string === false ) {
+                       // Err on the side of safety, ensure that the output
+                       // is always html safe in the event the message key is
+                       // missing, since in that case its highly likely the
+                       // message key is user-controlled.
+                       // '⧼' is used instead of '<' to side-step any
+                       // double-escaping issues.
+                       // (Keep synchronised with mw.Message#toString in JS.)
+                       return '⧼' . htmlspecialchars( $this->key ) . '⧽';
+               }
+
+               # Replace $* with a list of parameters for &uselang=qqx.
+               if ( strpos( $string, '$*' ) !== false ) {
+                       $paramlist = '';
+                       if ( $this->parameters !== [] ) {
+                               $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
+                       }
+                       $string = str_replace( '$*', $paramlist, $string );
+               }
+
+               # Replace parameters before text parsing
+               $string = $this->replaceParameters( $string, 'before', $format );
+
+               # Maybe transform using the full parser
+               if ( $format === self::FORMAT_PARSE ) {
+                       $string = $this->parseText( $string );
+                       $string = Parser::stripOuterParagraph( $string );
+               } elseif ( $format === self::FORMAT_BLOCK_PARSE ) {
+                       $string = $this->parseText( $string );
+               } elseif ( $format === self::FORMAT_TEXT ) {
+                       $string = $this->transformText( $string );
+               } elseif ( $format === self::FORMAT_ESCAPED ) {
+                       $string = $this->transformText( $string );
+                       $string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
+               }
+
+               # Raw parameter replacement
+               $string = $this->replaceParameters( $string, 'after', $format );
+
+               return $string;
+       }
+
+       /**
+        * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
+        *     $foo = new Message( $key );
+        *     $string = "<abbr>$foo</abbr>";
+        *
+        * @since 1.18
+        *
+        * @return string
+        */
+       public function __toString() {
+               // PHP doesn't allow __toString to throw exceptions and will
+               // trigger a fatal error if it does. So, catch any exceptions.
+
+               try {
+                       return $this->toString( self::FORMAT_PARSE );
+               } catch ( Exception $ex ) {
+                       try {
+                               trigger_error( "Exception caught in " . __METHOD__ . " (message " . $this->key . "): "
+                                       . $ex, E_USER_WARNING );
+                       } catch ( Exception $ex ) {
+                               // Doh! Cause a fatal error after all?
+                       }
+
+                       return '⧼' . htmlspecialchars( $this->key ) . '⧽';
+               }
+       }
+
+       /**
+        * Fully parse the text from wikitext to HTML.
+        *
+        * @since 1.17
+        *
+        * @return string Parsed HTML.
+        */
+       public function parse() {
+               $this->format = self::FORMAT_PARSE;
+               return $this->toString( self::FORMAT_PARSE );
+       }
+
+       /**
+        * Returns the message text. {{-transformation is done.
+        *
+        * @since 1.17
+        *
+        * @return string Unescaped message text.
+        */
+       public function text() {
+               $this->format = self::FORMAT_TEXT;
+               return $this->toString( self::FORMAT_TEXT );
+       }
+
+       /**
+        * Returns the message text as-is, only parameters are substituted.
+        *
+        * @since 1.17
+        *
+        * @return string Unescaped untransformed message text.
+        */
+       public function plain() {
+               $this->format = self::FORMAT_PLAIN;
+               return $this->toString( self::FORMAT_PLAIN );
+       }
+
+       /**
+        * Returns the parsed message text which is always surrounded by a block element.
+        *
+        * @since 1.17
+        *
+        * @return string HTML
+        */
+       public function parseAsBlock() {
+               $this->format = self::FORMAT_BLOCK_PARSE;
+               return $this->toString( self::FORMAT_BLOCK_PARSE );
+       }
+
+       /**
+        * Returns the message text. {{-transformation is done and the result
+        * is escaped excluding any raw parameters.
+        *
+        * @since 1.17
+        *
+        * @return string Escaped message text.
+        */
+       public function escaped() {
+               $this->format = self::FORMAT_ESCAPED;
+               return $this->toString( self::FORMAT_ESCAPED );
+       }
+
+       /**
+        * Check whether a message key has been defined currently.
+        *
+        * @since 1.17
+        *
+        * @return bool
+        */
+       public function exists() {
+               return $this->fetchMessage() !== false;
+       }
+
+       /**
+        * Check whether a message does not exist, or is an empty string
+        *
+        * @since 1.18
+        * @todo FIXME: Merge with isDisabled()?
+        *
+        * @return bool
+        */
+       public function isBlank() {
+               $message = $this->fetchMessage();
+               return $message === false || $message === '';
+       }
+
+       /**
+        * Check whether a message does not exist, is an empty string, or is "-".
+        *
+        * @since 1.18
+        *
+        * @return bool
+        */
+       public function isDisabled() {
+               $message = $this->fetchMessage();
+               return $message === false || $message === '' || $message === '-';
+       }
+
+       /**
+        * @since 1.17
+        *
+        * @param mixed $raw
+        *
+        * @return array Array with a single "raw" key.
+        */
+       public static function rawParam( $raw ) {
+               return [ 'raw' => $raw ];
+       }
+
+       /**
+        * @since 1.18
+        *
+        * @param mixed $num
+        *
+        * @return array Array with a single "num" key.
+        */
+       public static function numParam( $num ) {
+               return [ 'num' => $num ];
+       }
+
+       /**
+        * @since 1.22
+        *
+        * @param int $duration
+        *
+        * @return int[] Array with a single "duration" key.
+        */
+       public static function durationParam( $duration ) {
+               return [ 'duration' => $duration ];
+       }
+
+       /**
+        * @since 1.22
+        *
+        * @param string $expiry
+        *
+        * @return string[] Array with a single "expiry" key.
+        */
+       public static function expiryParam( $expiry ) {
+               return [ 'expiry' => $expiry ];
+       }
+
+       /**
+        * @since 1.22
+        *
+        * @param int $period
+        *
+        * @return int[] Array with a single "period" key.
+        */
+       public static function timeperiodParam( $period ) {
+               return [ 'period' => $period ];
+       }
+
+       /**
+        * @since 1.22
+        *
+        * @param int $size
+        *
+        * @return int[] Array with a single "size" key.
+        */
+       public static function sizeParam( $size ) {
+               return [ 'size' => $size ];
+       }
+
+       /**
+        * @since 1.22
+        *
+        * @param int $bitrate
+        *
+        * @return int[] Array with a single "bitrate" key.
+        */
+       public static function bitrateParam( $bitrate ) {
+               return [ 'bitrate' => $bitrate ];
+       }
+
+       /**
+        * @since 1.25
+        *
+        * @param string $plaintext
+        *
+        * @return string[] Array with a single "plaintext" key.
+        */
+       public static function plaintextParam( $plaintext ) {
+               return [ 'plaintext' => $plaintext ];
+       }
+
+       /**
+        * @since 1.29
+        *
+        * @param array $list
+        * @param string $type 'comma', 'semicolon', 'pipe', 'text'
+        * @return array Array with "list" and "type" keys.
+        */
+       public static function listParam( array $list, $type = 'text' ) {
+               if ( !isset( self::$listTypeMap[$type] ) ) {
+                       throw new InvalidArgumentException(
+                               "Invalid type '$type'. Known types are: " . implode( ', ', array_keys( self::$listTypeMap ) )
+                       );
+               }
+               return [ 'list' => $list, 'type' => $type ];
+       }
+
+       /**
+        * Substitutes any parameters into the message text.
+        *
+        * @since 1.17
+        *
+        * @param string $message The message text.
+        * @param string $type Either "before" or "after".
+        * @param string $format One of the FORMAT_* constants.
+        *
+        * @return string
+        */
+       protected function replaceParameters( $message, $type, $format ) {
+               // A temporary marker for $1 parameters that is only valid
+               // in non-attribute contexts. However if the entire message is escaped
+               // then we don't want to use it because it will be mangled in all contexts
+               // and its unnessary as ->escaped() messages aren't html.
+               $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
+               $replacementKeys = [];
+               foreach ( $this->parameters as $n => $param ) {
+                       list( $paramType, $value ) = $this->extractParam( $param, $format );
+                       if ( $type === 'before' ) {
+                               if ( $paramType === 'before' ) {
+                                       $replacementKeys['$' . ( $n + 1 )] = $value;
+                               } else /* $paramType === 'after' */ {
+                                       // To protect against XSS from replacing parameters
+                                       // inside html attributes, we convert $1 to $'"1.
+                                       // In the event that one of the parameters ends up
+                                       // in an attribute, either the ' or the " will be
+                                       // escaped, breaking the replacement and avoiding XSS.
+                                       $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
+                               }
+                       } elseif ( $paramType === 'after' ) {
+                               $replacementKeys[$marker . ( $n + 1 )] = $value;
+                       }
+               }
+               return strtr( $message, $replacementKeys );
+       }
+
+       /**
+        * Extracts the parameter type and preprocessed the value if needed.
+        *
+        * @since 1.18
+        *
+        * @param mixed $param Parameter as defined in this class.
+        * @param string $format One of the FORMAT_* constants.
+        *
+        * @return array Array with the parameter type (either "before" or "after") and the value.
+        */
+       protected function extractParam( $param, $format ) {
+               if ( is_array( $param ) ) {
+                       if ( isset( $param['raw'] ) ) {
+                               return [ 'after', $param['raw'] ];
+                       } elseif ( isset( $param['num'] ) ) {
+                               // Replace number params always in before step for now.
+                               // No support for combined raw and num params
+                               return [ 'before', $this->getLanguage()->formatNum( $param['num'] ) ];
+                       } elseif ( isset( $param['duration'] ) ) {
+                               return [ 'before', $this->getLanguage()->formatDuration( $param['duration'] ) ];
+                       } elseif ( isset( $param['expiry'] ) ) {
+                               return [ 'before', $this->getLanguage()->formatExpiry( $param['expiry'] ) ];
+                       } elseif ( isset( $param['period'] ) ) {
+                               return [ 'before', $this->getLanguage()->formatTimePeriod( $param['period'] ) ];
+                       } elseif ( isset( $param['size'] ) ) {
+                               return [ 'before', $this->getLanguage()->formatSize( $param['size'] ) ];
+                       } elseif ( isset( $param['bitrate'] ) ) {
+                               return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ];
+                       } elseif ( isset( $param['plaintext'] ) ) {
+                               return [ 'after', $this->formatPlaintext( $param['plaintext'], $format ) ];
+                       } elseif ( isset( $param['list'] ) ) {
+                               return $this->formatListParam( $param['list'], $param['type'], $format );
+                       } else {
+                               if ( !is_scalar( $param ) ) {
+                                       $param = serialize( $param );
+                               }
+                               \MediaWiki\Logger\LoggerFactory::getInstance( 'Bug58676' )->warning(
+                                       'Invalid parameter for message "{msgkey}": {param}',
+                                       [
+                                               'exception' => new Exception,
+                                               'msgkey' => $this->getKey(),
+                                               'param' => htmlspecialchars( $param ),
+                                       ]
+                               );
+
+                               return [ 'before', '[INVALID]' ];
+                       }
+               } elseif ( $param instanceof Message ) {
+                       // Match language, flags, etc. to the current message.
+                       $msg = clone $param;
+                       if ( $msg->language !== $this->language || $msg->useDatabase !== $this->useDatabase ) {
+                               // Cache depends on these parameters
+                               $msg->message = null;
+                       }
+                       $msg->interface = $this->interface;
+                       $msg->language = $this->language;
+                       $msg->useDatabase = $this->useDatabase;
+                       $msg->title = $this->title;
+
+                       // DWIM
+                       if ( $format === 'block-parse' ) {
+                               $format = 'parse';
+                       }
+                       $msg->format = $format;
+
+                       // Message objects should not be before parameters because
+                       // then they'll get double escaped. If the message needs to be
+                       // escaped, it'll happen right here when we call toString().
+                       return [ 'after', $msg->toString( $format ) ];
+               } else {
+                       return [ 'before', $param ];
+               }
+       }
+
+       /**
+        * Wrapper for what ever method we use to parse wikitext.
+        *
+        * @since 1.17
+        *
+        * @param string $string Wikitext message contents.
+        *
+        * @return string Wikitext parsed into HTML.
+        */
+       protected function parseText( $string ) {
+               $out = MessageCache::singleton()->parse(
+                       $string,
+                       $this->title,
+                       /*linestart*/true,
+                       $this->interface,
+                       $this->getLanguage()
+               );
+
+               return $out instanceof ParserOutput
+                       ? $out->getText( [
+                               'enableSectionEditLinks' => false,
+                               // Wrapping messages in an extra <div> is probably not expected. If
+                               // they're outside the content area they probably shouldn't be
+                               // targeted by CSS that's targeting the parser output, and if
+                               // they're inside they already are from the outer div.
+                               'unwrap' => true,
+                       ] )
+                       : $out;
+       }
+
+       /**
+        * Wrapper for what ever method we use to {{-transform wikitext.
+        *
+        * @since 1.17
+        *
+        * @param string $string Wikitext message contents.
+        *
+        * @return string Wikitext with {{-constructs replaced with their values.
+        */
+       protected function transformText( $string ) {
+               return MessageCache::singleton()->transform(
+                       $string,
+                       $this->interface,
+                       $this->getLanguage(),
+                       $this->title
+               );
+       }
+
+       /**
+        * Wrapper for what ever method we use to get message contents.
+        *
+        * @since 1.17
+        *
+        * @return string
+        * @throws MWException If message key array is empty.
+        */
+       protected function fetchMessage() {
+               if ( $this->message === null ) {
+                       $cache = MessageCache::singleton();
+
+                       foreach ( $this->keysToTry as $key ) {
+                               $message = $cache->get( $key, $this->useDatabase, $this->getLanguage() );
+                               if ( $message !== false && $message !== '' ) {
+                                       break;
+                               }
+                       }
+
+                       // NOTE: The constructor makes sure keysToTry isn't empty,
+                       //       so we know that $key and $message are initialized.
+                       $this->key = $key;
+                       $this->message = $message;
+               }
+               return $this->message;
+       }
+
+       /**
+        * Formats a message parameter wrapped with 'plaintext'. Ensures that
+        * the entire string is displayed unchanged when displayed in the output
+        * format.
+        *
+        * @since 1.25
+        *
+        * @param string $plaintext String to ensure plaintext output of
+        * @param string $format One of the FORMAT_* constants.
+        *
+        * @return string Input plaintext encoded for output to $format
+        */
+       protected function formatPlaintext( $plaintext, $format ) {
+               switch ( $format ) {
+                       case self::FORMAT_TEXT:
+                       case self::FORMAT_PLAIN:
+                               return $plaintext;
+
+                       case self::FORMAT_PARSE:
+                       case self::FORMAT_BLOCK_PARSE:
+                       case self::FORMAT_ESCAPED:
+                       default:
+                               return htmlspecialchars( $plaintext, ENT_QUOTES );
+               }
+       }
+
+       /**
+        * Formats a list of parameters as a concatenated string.
+        * @since 1.29
+        * @param array $params
+        * @param string $listType
+        * @param string $format One of the FORMAT_* constants.
+        * @return array Array with the parameter type (either "before" or "after") and the value.
+        */
+       protected function formatListParam( array $params, $listType, $format ) {
+               if ( !isset( self::$listTypeMap[$listType] ) ) {
+                       $warning = 'Invalid list type for message "' . $this->getKey() . '": '
+                               . htmlspecialchars( $listType )
+                               . ' (params are ' . htmlspecialchars( serialize( $params ) ) . ')';
+                       trigger_error( $warning, E_USER_WARNING );
+                       $e = new Exception;
+                       wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
+                       return [ 'before', '[INVALID]' ];
+               }
+               $func = self::$listTypeMap[$listType];
+
+               // Handle an empty list sensibly
+               if ( !$params ) {
+                       return [ 'before', $this->getLanguage()->$func( [] ) ];
+               }
+
+               // First, determine what kinds of list items we have
+               $types = [];
+               $vars = [];
+               $list = [];
+               foreach ( $params as $n => $p ) {
+                       list( $type, $value ) = $this->extractParam( $p, $format );
+                       $types[$type] = true;
+                       $list[] = $value;
+                       $vars[] = '$' . ( $n + 1 );
+               }
+
+               // Easy case: all are 'before' or 'after', so just join the
+               // values and use the same type.
+               if ( count( $types ) === 1 ) {
+                       return [ key( $types ), $this->getLanguage()->$func( $list ) ];
+               }
+
+               // Hard case: We need to process each value per its type, then
+               // return the concatenated values as 'after'. We handle this by turning
+               // the list into a RawMessage and processing that as a parameter.
+               $vars = $this->getLanguage()->$func( $vars );
+               return $this->extractParam( new RawMessage( $vars, $params ), $format );
+       }
+}
diff --git a/includes/language/MessageLocalizer.php b/includes/language/MessageLocalizer.php
new file mode 100644 (file)
index 0000000..9a1796b
--- /dev/null
@@ -0,0 +1,43 @@
+<?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 Language
+ */
+
+/**
+ * Interface for localizing messages in MediaWiki
+ *
+ * @since 1.30
+ * @ingroup Language
+ */
+interface MessageLocalizer {
+
+       /**
+        * This is the method for getting translated interface messages.
+        *
+        * @see https://www.mediawiki.org/wiki/Manual:Messages_API
+        * @see Message::__construct
+        *
+        * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
+        *   or a MessageSpecifier.
+        * @param mixed $params,... Normal message parameters
+        * @return Message
+        */
+       public function msg( $key /*...*/ );
+
+}
diff --git a/includes/libs/ParamValidator/Callbacks.php b/includes/libs/ParamValidator/Callbacks.php
new file mode 100644 (file)
index 0000000..d94a81f
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use Psr\Http\Message\UploadedFileInterface;
+
+/**
+ * Interface defining callbacks needed by ParamValidator
+ *
+ * The user of ParamValidator is expected to pass an object implementing this
+ * interface to ParamValidator's constructor.
+ *
+ * All methods in this interface accept an "options array". This is the same `$options`
+ * passed to ParamValidator::getValue(), ParamValidator::validateValue(), and the like
+ * and is intended for communication of non-global state.
+ *
+ * @since 1.34
+ */
+interface Callbacks {
+
+       /**
+        * Test if a parameter exists in the request
+        * @param string $name Parameter name
+        * @param array $options Options array
+        * @return bool True if present, false if absent.
+        *  Return false for file upload parameters.
+        */
+       public function hasParam( $name, array $options );
+
+       /**
+        * Fetch a value from the request
+        *
+        * Return `$default` for file-upload parameters.
+        *
+        * @param string $name Parameter name to fetch
+        * @param mixed $default Default value to return if the parameter is unset.
+        * @param array $options Options array
+        * @return string|string[]|mixed A string or string[] if the parameter was found,
+        *  or $default if it was not.
+        */
+       public function getValue( $name, $default, array $options );
+
+       /**
+        * Test if a parameter exists as an upload in the request
+        * @param string $name Parameter name
+        * @param array $options Options array
+        * @return bool True if present, false if absent.
+        */
+       public function hasUpload( $name, array $options );
+
+       /**
+        * Fetch data for a file upload
+        * @param string $name Parameter name of the upload
+        * @param array $options Options array
+        * @return UploadedFileInterface|null Uploaded file, or null if there is no file for $name.
+        */
+       public function getUploadedFile( $name, array $options );
+
+       /**
+        * Record non-fatal conditions.
+        * @param ValidationException $condition
+        * @param array $options Options array
+        */
+       public function recordCondition( ValidationException $condition, array $options );
+
+       /**
+        * Indicate whether "high limits" should be used.
+        *
+        * Some settings have multiple limits, one for "normal" users and a higher
+        * one for "privileged" users. This is used to determine which class the
+        * current user is in when necessary.
+        *
+        * @param array $options Options array
+        * @return bool Whether the current user is privileged to use high limits
+        */
+       public function useHighLimits( array $options );
+
+}
diff --git a/includes/libs/ParamValidator/ParamValidator.php b/includes/libs/ParamValidator/ParamValidator.php
new file mode 100644 (file)
index 0000000..1085375
--- /dev/null
@@ -0,0 +1,522 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use DomainException;
+use InvalidArgumentException;
+use Wikimedia\Assert\Assert;
+use Wikimedia\ObjectFactory;
+
+/**
+ * Service for formatting and validating API parameters
+ *
+ * A settings array is simply an array with keys being the relevant PARAM_*
+ * constants from this class, TypeDef, and its subclasses.
+ *
+ * As a general overview of the architecture here:
+ *  - ParamValidator handles some general validation of the parameter,
+ *    then hands off to a TypeDef subclass to validate the specific representation
+ *    based on the parameter's type.
+ *  - TypeDef subclasses handle conversion between the string representation
+ *    submitted by the client and the output PHP data types, validating that the
+ *    strings are valid representations of the intended type as they do so.
+ *  - ValidationException is used to report fatal errors in the validation back
+ *    to the caller, since the return value represents the successful result of
+ *    the validation and might be any type or class.
+ *  - The Callbacks interface allows ParamValidator to reach out and fetch data
+ *    it needs to perform the validation. Currently that includes:
+ *    - Fetching the value of the parameter being validated (largely since a generic
+ *      caller cannot know whether it needs to fetch a string from $_GET/$_POST or
+ *      an array from $_FILES).
+ *    - Reporting of non-fatal warnings back to the caller.
+ *    - Fetching the "high limits" flag when necessary, to avoid the need for loading
+ *      the user unnecessarily.
+ *
+ * @since 1.34
+ */
+class ParamValidator {
+
+       /**
+        * @name Constants for parameter settings arrays
+        * These constants are keys in the settings array that define how the
+        * parameters coming in from the request are to be interpreted.
+        *
+        * If a constant is associated with a ValidationException, the failure code
+        * and data are described. ValidationExceptions are typically thrown, but
+        * those indicated as "non-fatal" are instead passed to
+        * Callbacks::recordCondition().
+        *
+        * Additional constants may be defined by TypeDef subclasses, or by other
+        * libraries for controlling things like auto-generated parameter documentation.
+        * For purposes of namespacing the constants, the values of all constants
+        * defined by this library begin with 'param-'.
+        *
+        * @{
+        */
+
+       /** (mixed) Default value of the parameter. If omitted, null is the default. */
+       const PARAM_DEFAULT = 'param-default';
+
+       /**
+        * (string|array) Type of the parameter.
+        * Must be a registered type or an array of enumerated values (in which case the "enum"
+        * type must be registered). If omitted, the default is the PHP type of the default value
+        * (see PARAM_DEFAULT).
+        */
+       const PARAM_TYPE = 'param-type';
+
+       /**
+        * (bool) Indicate that the parameter is required.
+        *
+        * ValidationException codes:
+        *  - 'missingparam': The parameter is omitted/empty (and no default was set). No data.
+        */
+       const PARAM_REQUIRED = 'param-required';
+
+       /**
+        * (bool) Indicate that the parameter is multi-valued.
+        *
+        * A multi-valued parameter may be submitted in one of several formats. All
+        * of the following result a value of `[ 'a', 'b', 'c' ]`.
+        *  - "a|b|c", i.e. pipe-separated.
+        *  - "\x1Fa\x1Fb\x1Fc", i.e. separated by U+001F, with a signalling U+001F at the start.
+        *  - As a string[], e.g. from a query string like "foo[]=a&foo[]=b&foo[]=c".
+        *
+        * Each of the multiple values is passed individually to the TypeDef.
+        * $options will contain a 'values-list' key holding the entire list.
+        *
+        * By default duplicates are removed from the resulting parameter list. Use
+        * PARAM_ALLOW_DUPLICATES to override that behavior.
+        *
+        * ValidationException codes:
+        *  - 'toomanyvalues': More values were supplied than are allowed. See
+        *    PARAM_ISMULTI_LIMIT1, PARAM_ISMULTI_LIMIT2, and constructor option
+        *    'ismultiLimits'. Data:
+        *     - 'limit': The limit that was exceeded.
+        *  - 'unrecognizedvalues': Non-fatal. Invalid values were passed and
+        *    PARAM_IGNORE_INVALID_VALUES was set. Data:
+        *     - 'values': The unrecognized values.
+        */
+       const PARAM_ISMULTI = 'param-ismulti';
+
+       /**
+        * (int) Maximum number of multi-valued parameter values allowed
+        *
+        * PARAM_ISMULTI_LIMIT1 is the normal limit, and PARAM_ISMULTI_LIMIT2 is
+        * the limit when useHighLimits() returns true.
+        *
+        * ValidationException codes:
+        *  - 'toomanyvalues': The limit was exceeded. Data:
+        *     - 'limit': The limit that was exceeded.
+        */
+       const PARAM_ISMULTI_LIMIT1 = 'param-ismulti-limit1';
+
+       /**
+        * (int) Maximum number of multi-valued parameter values allowed for users
+        * allowed high limits.
+        *
+        * PARAM_ISMULTI_LIMIT1 is the normal limit, and PARAM_ISMULTI_LIMIT2 is
+        * the limit when useHighLimits() returns true.
+        *
+        * ValidationException codes:
+        *  - 'toomanyvalues': The limit was exceeded. Data:
+        *     - 'limit': The limit that was exceeded.
+        */
+       const PARAM_ISMULTI_LIMIT2 = 'param-ismulti-limit2';
+
+       /**
+        * (bool|string) Whether a magic "all values" value exists for multi-valued
+        * enumerated types, and if so what that value is.
+        *
+        * When PARAM_TYPE has a defined set of values and PARAM_ISMULTI is true,
+        * this allows for an asterisk ('*') to be passed in place of a pipe-separated list of
+        * every possible value. If a string is set, it will be used in place of the asterisk.
+        */
+       const PARAM_ALL = 'param-all';
+
+       /**
+        * (bool) Allow the same value to be set more than once when PARAM_ISMULTI is true?
+        *
+        * If not truthy, the set of values will be passed through
+        * `array_values( array_unique() )`. The default is falsey.
+        */
+       const PARAM_ALLOW_DUPLICATES = 'param-allow-duplicates';
+
+       /**
+        * (bool) Indicate that the parameter's value should not be logged.
+        *
+        * ValidationException codes: (non-fatal)
+        *  - 'param-sensitive': Always recorded.
+        */
+       const PARAM_SENSITIVE = 'param-sensitive';
+
+       /**
+        * (bool) Indicate that a deprecated parameter was used.
+        *
+        * ValidationException codes: (non-fatal)
+        *  - 'param-deprecated': Always recorded.
+        */
+       const PARAM_DEPRECATED = 'param-deprecated';
+
+       /**
+        * (bool) Whether to ignore invalid values.
+        *
+        * This controls whether certain ValidationExceptions are considered fatal
+        * or non-fatal. The default is false.
+        */
+       const PARAM_IGNORE_INVALID_VALUES = 'param-ignore-invalid-values';
+
+       /**@}*/
+
+       /** Magic "all values" value when PARAM_ALL is true. */
+       const ALL_DEFAULT_STRING = '*';
+
+       /** A list of standard type names and types that may be passed as `$typeDefs` to __construct(). */
+       public static $STANDARD_TYPES = [
+               'boolean' => [ 'class' => TypeDef\BooleanDef::class ],
+               'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ],
+               'integer' => [ 'class' => TypeDef\IntegerDef::class ],
+               'limit' => [ 'class' => TypeDef\LimitDef::class ],
+               'float' => [ 'class' => TypeDef\FloatDef::class ],
+               'double' => [ 'class' => TypeDef\FloatDef::class ],
+               'string' => [ 'class' => TypeDef\StringDef::class ],
+               'password' => [ 'class' => TypeDef\PasswordDef::class ],
+               'NULL' => [
+                       'class' => TypeDef\StringDef::class,
+                       'args' => [ [
+                               'allowEmptyWhenRequired' => true,
+                       ] ],
+               ],
+               'timestamp' => [ 'class' => TypeDef\TimestampDef::class ],
+               'upload' => [ 'class' => TypeDef\UploadDef::class ],
+               'enum' => [ 'class' => TypeDef\EnumDef::class ],
+       ];
+
+       /** @var Callbacks */
+       private $callbacks;
+
+       /** @var ObjectFactory */
+       private $objectFactory;
+
+       /** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */
+       private $typeDefs = [];
+
+       /** @var int Default values for PARAM_ISMULTI_LIMIT1 */
+       private $ismultiLimit1;
+
+       /** @var int Default values for PARAM_ISMULTI_LIMIT2 */
+       private $ismultiLimit2;
+
+       /**
+        * @param Callbacks $callbacks
+        * @param ObjectFactory $objectFactory To turn specs into TypeDef objects
+        * @param array $options Associative array of additional settings
+        *  - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::$STANDARD_TYPES will be used.
+        *    Pass an empty array if you want to start with no registered types.
+        *  - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and
+        *    PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`.
+        */
+       public function __construct(
+               Callbacks $callbacks,
+               ObjectFactory $objectFactory,
+               array $options = []
+       ) {
+               $this->callbacks = $callbacks;
+               $this->objectFactory = $objectFactory;
+
+               $this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES );
+               $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50;
+               $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500;
+       }
+
+       /**
+        * List known type names
+        * @return string[]
+        */
+       public function knownTypes() {
+               return array_keys( $this->typeDefs );
+       }
+
+       /**
+        * Register multiple type handlers
+        *
+        * @see addTypeDef()
+        * @param array $typeDefs Associative array mapping `$name` to `$typeDef`.
+        */
+       public function addTypeDefs( array $typeDefs ) {
+               foreach ( $typeDefs as $name => $def ) {
+                       $this->addTypeDef( $name, $def );
+               }
+       }
+
+       /**
+        * Register a type handler
+        *
+        * To allow code to omit PARAM_TYPE in settings arrays to derive the type
+        * from PARAM_DEFAULT, it is strongly recommended that the following types be
+        * registered: "boolean", "integer", "double", "string", "NULL", and "enum".
+        *
+        * When using ObjectFactory specs, the following extra arguments are passed:
+        * - The Callbacks object for this ParamValidator instance.
+        *
+        * @param string $name Type name
+        * @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one.
+        */
+       public function addTypeDef( $name, $typeDef ) {
+               Assert::parameterType(
+                       implode( '|', [ TypeDef::class, 'array' ] ),
+                       $typeDef,
+                       '$typeDef'
+               );
+
+               if ( isset( $this->typeDefs[$name] ) ) {
+                       throw new InvalidArgumentException( "Type '$name' is already registered" );
+               }
+               $this->typeDefs[$name] = $typeDef;
+       }
+
+       /**
+        * Register a type handler, overriding any existing handler
+        * @see addTypeDef
+        * @param string $name Type name
+        * @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type.
+        */
+       public function overrideTypeDef( $name, $typeDef ) {
+               Assert::parameterType(
+                       implode( '|', [ TypeDef::class, 'array', 'null' ] ),
+                       $typeDef,
+                       '$typeDef'
+               );
+
+               if ( $typeDef === null ) {
+                       unset( $this->typeDefs[$name] );
+               } else {
+                       $this->typeDefs[$name] = $typeDef;
+               }
+       }
+
+       /**
+        * Test if a type is registered
+        * @param string $name Type name
+        * @return bool
+        */
+       public function hasTypeDef( $name ) {
+               return isset( $this->typeDefs[$name] );
+       }
+
+       /**
+        * Get the TypeDef for a type
+        * @param string|array $type Any array is considered equivalent to the string "enum".
+        * @return TypeDef|null
+        */
+       public function getTypeDef( $type ) {
+               if ( is_array( $type ) ) {
+                       $type = 'enum';
+               }
+
+               if ( !isset( $this->typeDefs[$type] ) ) {
+                       return null;
+               }
+
+               $def = $this->typeDefs[$type];
+               if ( !$def instanceof TypeDef ) {
+                       $def = $this->objectFactory->createObject( $def, [
+                               'extraArgs' => [ $this->callbacks ],
+                               'assertClass' => TypeDef::class,
+                       ] );
+                       $this->typeDefs[$type] = $def;
+               }
+
+               return $def;
+       }
+
+       /**
+        * Normalize a parameter settings array
+        * @param array|mixed $settings Default value or an array of settings
+        *  using PARAM_* constants.
+        * @return array
+        */
+       public function normalizeSettings( $settings ) {
+               // Shorthand
+               if ( !is_array( $settings ) ) {
+                       $settings = [
+                               self::PARAM_DEFAULT => $settings,
+                       ];
+               }
+
+               // When type is not given, determine it from the type of the PARAM_DEFAULT
+               if ( !isset( $settings[self::PARAM_TYPE] ) ) {
+                       $settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null );
+               }
+
+               $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
+               if ( $typeDef ) {
+                       $settings = $typeDef->normalizeSettings( $settings );
+               }
+
+               return $settings;
+       }
+
+       /**
+        * Fetch and valiate a parameter value using a settings array
+        *
+        * @param string $name Parameter name
+        * @param array|mixed $settings Default value or an array of settings
+        *  using PARAM_* constants.
+        * @param array $options Options array, passed through to the TypeDef and Callbacks.
+        * @return mixed Validated parameter value
+        * @throws ValidationException if the value is invalid
+        */
+       public function getValue( $name, $settings, array $options = [] ) {
+               $settings = $this->normalizeSettings( $settings );
+
+               $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
+               if ( !$typeDef ) {
+                       throw new DomainException(
+                               "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
+                       );
+               }
+
+               $value = $typeDef->getValue( $name, $settings, $options );
+
+               if ( $value !== null ) {
+                       if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
+                               $this->callbacks->recordCondition(
+                                       new ValidationException( $name, $value, $settings, 'param-sensitive', [] ),
+                                       $options
+                               );
+                       }
+
+                       // Set a warning if a deprecated parameter has been passed
+                       if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
+                               $this->callbacks->recordCondition(
+                                       new ValidationException( $name, $value, $settings, 'param-deprecated', [] ),
+                                       $options
+                               );
+                       }
+               } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) {
+                       $value = $settings[self::PARAM_DEFAULT];
+               }
+
+               return $this->validateValue( $name, $value, $settings, $options );
+       }
+
+       /**
+        * Valiate a parameter value using a settings array
+        *
+        * @param string $name Parameter name
+        * @param null|mixed $value Parameter value
+        * @param array|mixed $settings Default value or an array of settings
+        *  using PARAM_* constants.
+        * @param array $options Options array, passed through to the TypeDef and Callbacks.
+        *  - An additional option, 'values-list', will be set when processing the
+        *    values of a multi-valued parameter.
+        * @return mixed Validated parameter value(s)
+        * @throws ValidationException if the value is invalid
+        */
+       public function validateValue( $name, $value, $settings, array $options = [] ) {
+               $settings = $this->normalizeSettings( $settings );
+
+               $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
+               if ( !$typeDef ) {
+                       throw new DomainException(
+                               "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
+                       );
+               }
+
+               if ( $value === null ) {
+                       if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
+                               throw new ValidationException( $name, $value, $settings, 'missingparam', [] );
+                       }
+                       return null;
+               }
+
+               // Non-multi
+               if ( empty( $settings[self::PARAM_ISMULTI] ) ) {
+                       return $typeDef->validate( $name, $value, $settings, $options );
+               }
+
+               // Split the multi-value and validate each parameter
+               $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
+               $limit2 = $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2;
+               $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 );
+
+               // Handle PARAM_ALL
+               $enumValues = $typeDef->getEnumValues( $name, $settings, $options );
+               if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) &&
+                       count( $valuesList ) === 1
+               ) {
+                       $allValue = is_string( $settings[self::PARAM_ALL] )
+                               ? $settings[self::PARAM_ALL]
+                               : self::ALL_DEFAULT_STRING;
+                       if ( $valuesList[0] === $allValue ) {
+                               return $enumValues;
+                       }
+               }
+
+               // Avoid checking useHighLimits() unless it's actually necessary
+               $sizeLimit = count( $valuesList ) > $limit1 && $this->callbacks->useHighLimits( $options )
+                       ? $limit2
+                       : $limit1;
+               if ( count( $valuesList ) > $sizeLimit ) {
+                       throw new ValidationException( $name, $valuesList, $settings, 'toomanyvalues', [
+                               'limit' => $sizeLimit
+                       ] );
+               }
+
+               $options['values-list'] = $valuesList;
+               $validValues = [];
+               $invalidValues = [];
+               foreach ( $valuesList as $v ) {
+                       try {
+                               $validValues[] = $typeDef->validate( $name, $v, $settings, $options );
+                       } catch ( ValidationException $ex ) {
+                               if ( empty( $settings[self::PARAM_IGNORE_INVALID_VALUES] ) ) {
+                                       throw $ex;
+                               }
+                               $invalidValues[] = $v;
+                       }
+               }
+               if ( $invalidValues ) {
+                       $this->callbacks->recordCondition(
+                               new ValidationException( $name, $value, $settings, 'unrecognizedvalues', [
+                                       'values' => $invalidValues,
+                               ] ),
+                               $options
+                       );
+               }
+
+               // Throw out duplicates if requested
+               if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
+                       $validValues = array_values( array_unique( $validValues ) );
+               }
+
+               return $validValues;
+       }
+
+       /**
+        * Split a multi-valued parameter string, like explode()
+        *
+        * Note that, unlike explode(), this will return an empty array when given
+        * an empty string.
+        *
+        * @param string $value
+        * @param int $limit
+        * @return string[]
+        */
+       public static function explodeMultiValue( $value, $limit ) {
+               if ( $value === '' || $value === "\x1f" ) {
+                       return [];
+               }
+
+               if ( substr( $value, 0, 1 ) === "\x1f" ) {
+                       $sep = "\x1f";
+                       $value = substr( $value, 1 );
+               } else {
+                       $sep = '|';
+               }
+
+               return explode( $sep, $value, $limit );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/README.md b/includes/libs/ParamValidator/README.md
new file mode 100644 (file)
index 0000000..dd992a4
--- /dev/null
@@ -0,0 +1,58 @@
+Wikimedia API Parameter Validator
+=================================
+
+This library implements a system for processing and validating parameters to an
+API from data like that in PHP's `$_GET`, `$_POST`, and `$_FILES` arrays, based
+on a declarative definition of available parameters.
+
+Usage
+-----
+
+<pre lang="php">
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\TypeDef\IntegerDef;
+use Wikimedia\ParamValidator\SimpleCallbacks as ParamValidatorCallbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+$validator = new ParamValidator(
+       new ParamValidatorCallbacks( $_POST + $_GET, $_FILES ),
+       $serviceContainer->getObjectFactory()
+);
+
+try {
+       $intValue = $validator->getValue( 'intParam', [
+                       ParamValidator::PARAM_TYPE => 'integer',
+                       ParamValidator::PARAM_DEFAULT => 0,
+                       IntegerDef::PARAM_MIN => 0,
+                       IntegerDef::PARAM_MAX => 5,
+       ] );
+} catch ( ValidationException $ex ) {
+       $error = lookupI18nMessage( 'param-validator-error-' . $ex->getFailureCode() );
+       echo "Validation error: $error\n";
+}
+</pre>
+
+I18n
+----
+
+This library is designed to generate output in a manner suited to use with an
+i18n system. To that end, errors and such are indicated by means of "codes"
+consisting of ASCII lowercase letters, digits, and hyphen (and always beginning
+with a letter).
+
+Additional details about each error, such as the allowed range for an integer
+value, are similarly returned by means of associative arrays with keys being
+similar "code" strings and values being strings, integers, or arrays of strings
+that are intended to be formatted as a list (e.g. joined with commas). The
+details for any particular "message" will also always have the same keys in the
+same order to facilitate use with i18n systems using positional rather than
+named parameters.
+
+For possible codes and their parameters, see the documentation of the relevant
+`PARAM_*` constants and TypeDef classes.
+
+Running tests
+-------------
+
+    composer install --prefer-dist
+    composer test
diff --git a/includes/libs/ParamValidator/SimpleCallbacks.php b/includes/libs/ParamValidator/SimpleCallbacks.php
new file mode 100644 (file)
index 0000000..77dab92
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use Wikimedia\ParamValidator\Util\UploadedFile;
+
+/**
+ * Simple Callbacks implementation for $_GET/$_POST/$_FILES data
+ *
+ * Options array keys used by this class:
+ *  - 'useHighLimits': (bool) Return value from useHighLimits()
+ *
+ * @since 1.34
+ */
+class SimpleCallbacks implements Callbacks {
+
+       /** @var (string|string[])[] $_GET/$_POST data */
+       private $params;
+
+       /** @var (array|UploadedFile)[] $_FILES data or UploadedFile instances */
+       private $files;
+
+       /** @var array Any recorded conditions */
+       private $conditions = [];
+
+       /**
+        * @param (string|string[])[] $params Data from $_POST + $_GET
+        * @param array[] $files Data from $_FILES
+        */
+       public function __construct( array $params, array $files = [] ) {
+               $this->params = $params;
+               $this->files = $files;
+       }
+
+       public function hasParam( $name, array $options ) {
+               return isset( $this->params[$name] );
+       }
+
+       public function getValue( $name, $default, array $options ) {
+               return $this->params[$name] ?? $default;
+       }
+
+       public function hasUpload( $name, array $options ) {
+               return isset( $this->files[$name] );
+       }
+
+       public function getUploadedFile( $name, array $options ) {
+               $file = $this->files[$name] ?? null;
+               if ( $file && !$file instanceof UploadedFile ) {
+                       $file = new UploadedFile( $file );
+                       $this->files[$name] = $file;
+               }
+               return $file;
+       }
+
+       public function recordCondition( ValidationException $condition, array $options ) {
+               $this->conditions[] = $condition;
+       }
+
+       /**
+        * Fetch any recorded conditions
+        * @return array[]
+        */
+       public function getRecordedConditions() {
+               return $this->conditions;
+       }
+
+       /**
+        * Clear any recorded conditions
+        */
+       public function clearRecordedConditions() {
+               $this->conditions = [];
+       }
+
+       public function useHighLimits( array $options ) {
+               return !empty( $options['useHighLimits'] );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef.php b/includes/libs/ParamValidator/TypeDef.php
new file mode 100644 (file)
index 0000000..0d54add
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+/**
+ * Base definition for ParamValidator types.
+ *
+ * All methods in this class accept an "options array". This is just the `$options`
+ * passed to ParamValidator::getValue(), ParamValidator::validateValue(), and the like
+ * and is intended for communication of non-global state.
+ *
+ * @since 1.34
+ */
+abstract class TypeDef {
+
+       /** @var Callbacks */
+       protected $callbacks;
+
+       public function __construct( Callbacks $callbacks ) {
+               $this->callbacks = $callbacks;
+       }
+
+       /**
+        * Get the value from the request
+        *
+        * @note Only override this if you need to use something other than
+        *  $this->callbacks->getValue() to fetch the value. Reformatting from a
+        *  string should typically be done by self::validate().
+        * @note Handling of ParamValidator::PARAM_DEFAULT should be left to ParamValidator,
+        *  as should PARAM_REQUIRED and the like.
+        *
+        * @param string $name Parameter name being fetched.
+        * @param array $settings Parameter settings array.
+        * @param array $options Options array.
+        * @return null|mixed Return null if the value wasn't present, otherwise a
+        *  value to be passed to self::validate().
+        */
+       public function getValue( $name, array $settings, array $options ) {
+               return $this->callbacks->getValue( $name, null, $options );
+       }
+
+       /**
+        * Validate the value
+        *
+        * When ParamValidator is processing a multi-valued parameter, this will be
+        * called once for each of the supplied values. Which may mean zero calls.
+        *
+        * When getValue() returned null, this will not be called.
+        *
+        * @param string $name Parameter name being validated.
+        * @param mixed $value Value to validate, from getValue().
+        * @param array $settings Parameter settings array.
+        * @param array $options Options array. Note the following values that may be set
+        *  by ParamValidator:
+        *   - values-list: (string[]) If defined, values of a multi-valued parameter are being processed
+        *     (and this array holds the full set of values).
+        * @return mixed Validated value
+        * @throws ValidationException if the value is invalid
+        */
+       abstract public function validate( $name, $value, array $settings, array $options );
+
+       /**
+        * Normalize a settings array
+        * @param array $settings
+        * @return array
+        */
+       public function normalizeSettings( array $settings ) {
+               return $settings;
+       }
+
+       /**
+        * Get the values for enum-like parameters
+        *
+        * This is primarily intended for documentation and implementation of
+        * PARAM_ALL; it is the responsibility of the TypeDef to ensure that validate()
+        * accepts the values returned here.
+        *
+        * @param string $name Parameter name being validated.
+        * @param array $settings Parameter settings array.
+        * @param array $options Options array.
+        * @return array|null All possible enumerated values, or null if this is
+        *  not an enumeration.
+        */
+       public function getEnumValues( $name, array $settings, array $options ) {
+               return null;
+       }
+
+       /**
+        * Convert a value to a string representation.
+        *
+        * This is intended as the inverse of getValue() and validate(): this
+        * should accept anything returned by those methods or expected to be used
+        * as PARAM_DEFAULT, and if the string from this method is passed in as client
+        * input or PARAM_DEFAULT it should give equivalent output from validate().
+        *
+        * @param string $name Parameter name being converted.
+        * @param mixed $value Parameter value being converted. Do not pass null.
+        * @param array $settings Parameter settings array.
+        * @param array $options Options array.
+        * @return string|null Return null if there is no representation of $value
+        *  reasonably satisfying the description given.
+        */
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               return (string)$value;
+       }
+
+       /**
+        * "Describe" a settings array
+        *
+        * This is intended to format data about a settings array using this type
+        * in a way that would be useful for automatically generated documentation
+        * or a machine-readable interface specification.
+        *
+        * Keys in the description array should follow the same guidelines as the
+        * code described for ValidationException.
+        *
+        * By default, each value in the description array is a single string,
+        * integer, or array. When `$options['compact']` is supplied, each value is
+        * instead an array of such and related values may be combined. For example,
+        * a non-compact description for an integer type might include
+        * `[ 'default' => 0, 'min' => 0, 'max' => 5 ]`, while in compact mode it might
+        * instead report `[ 'default' => [ 'value' => 0 ], 'minmax' => [ 'min' => 0, 'max' => 5 ] ]`
+        * to facilitate auto-generated documentation turning that 'minmax' into
+        * "Value must be between 0 and 5" rather than disconnected statements
+        * "Value must be >= 0" and "Value must be <= 5".
+        *
+        * @param string $name Parameter name being described.
+        * @param array $settings Parameter settings array.
+        * @param array $options Options array. Defined options for this base class are:
+        *  - 'compact': (bool) Enable compact mode, as described above.
+        * @return array
+        */
+       public function describeSettings( $name, array $settings, array $options ) {
+               $compact = !empty( $options['compact'] );
+
+               $ret = [];
+
+               if ( isset( $settings[ParamValidator::PARAM_DEFAULT] ) ) {
+                       $value = $this->stringifyValue(
+                               $name, $settings[ParamValidator::PARAM_DEFAULT], $settings, $options
+                       );
+                       $ret['default'] = $compact ? [ 'value' => $value ] : $value;
+               }
+
+               return $ret;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/BooleanDef.php b/includes/libs/ParamValidator/TypeDef/BooleanDef.php
new file mode 100644 (file)
index 0000000..f77c930
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for boolean types
+ *
+ * This type accepts certain defined strings to mean 'true' or 'false'.
+ * The result from validate() is a PHP boolean.
+ *
+ * ValidationException codes:
+ *  - 'badbool': The value is not a recognized boolean. Data:
+ *     - 'truevals': List of recognized values for "true".
+ *     - 'falsevals': List of recognized values for "false".
+ *
+ * @since 1.34
+ */
+class BooleanDef extends TypeDef {
+
+       public static $TRUEVALS = [ 'true', 't', 'yes', 'y', 'on', '1' ];
+       public static $FALSEVALS = [ 'false', 'f', 'no', 'n', 'off', '0' ];
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               $value = strtolower( $value );
+               if ( in_array( $value, self::$TRUEVALS, true ) ) {
+                       return true;
+               }
+               if ( $value === '' || in_array( $value, self::$FALSEVALS, true ) ) {
+                       return false;
+               }
+
+               throw new ValidationException( $name, $value, $settings, 'badbool', [
+                       'truevals' => self::$TRUEVALS,
+                       'falsevals' => array_merge( self::$FALSEVALS, [ 'the empty string' ] ),
+               ] );
+       }
+
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               return $value ? self::$TRUEVALS[0] : self::$FALSEVALS[0];
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/EnumDef.php b/includes/libs/ParamValidator/TypeDef/EnumDef.php
new file mode 100644 (file)
index 0000000..0f4f690
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for enumeration types.
+ *
+ * This class expects that PARAM_TYPE is an array of allowed values. Subclasses
+ * may override getEnumValues() to determine the allowed values differently.
+ *
+ * The result from validate() is one of the defined values.
+ *
+ * ValidationException codes:
+ *  - 'badvalue': The value is not a recognized value. No data.
+ *  - 'notmulti': PARAM_ISMULTI is not set and the unrecognized value seems to
+ *     be an attempt at using multiple values. No data.
+ *
+ * Additional codes may be generated when using certain PARAM constants. See
+ * the constants' documentation for details.
+ *
+ * @since 1.34
+ */
+class EnumDef extends TypeDef {
+
+       /**
+        * (array) Associative array of deprecated values.
+        *
+        * Keys are the deprecated parameter values, values are included in
+        * the ValidationException. If value is null, the parameter is considered
+        * not actually deprecated.
+        *
+        * Note that this does not add any values to the enumeration, it only
+        * documents existing values as being deprecated.
+        *
+        * ValidationException codes: (non-fatal)
+        *  - 'deprecated-value': A deprecated value was encountered. Data:
+        *     - 'flag': The value from the associative array.
+        */
+       const PARAM_DEPRECATED_VALUES = 'param-deprecated-values';
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               $values = $this->getEnumValues( $name, $settings, $options );
+
+               if ( in_array( $value, $values, true ) ) {
+                       // Set a warning if a deprecated parameter value has been passed
+                       if ( isset( $settings[self::PARAM_DEPRECATED_VALUES][$value] ) ) {
+                               $this->callbacks->recordCondition(
+                                       new ValidationException( $name, $value, $settings, 'deprecated-value', [
+                                               'flag' => $settings[self::PARAM_DEPRECATED_VALUES][$value],
+                                       ] ),
+                                       $options
+                               );
+                       }
+
+                       return $value;
+               }
+
+               if ( !isset( $options['values-list'] ) &&
+                       count( ParamValidator::explodeMultiValue( $value, 2 ) ) > 1
+               ) {
+                       throw new ValidationException( $name, $value, $settings, 'notmulti', [] );
+               } else {
+                       throw new ValidationException( $name, $value, $settings, 'badvalue', [] );
+               }
+       }
+
+       public function getEnumValues( $name, array $settings, array $options ) {
+               return $settings[ParamValidator::PARAM_TYPE];
+       }
+
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               if ( !is_array( $value ) ) {
+                       return parent::stringifyValue( $name, $value, $settings, $options );
+               }
+
+               foreach ( $value as $v ) {
+                       if ( strpos( $v, '|' ) !== false ) {
+                               return "\x1f" . implode( "\x1f", $value );
+                       }
+               }
+               return implode( '|', $value );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/FloatDef.php b/includes/libs/ParamValidator/TypeDef/FloatDef.php
new file mode 100644 (file)
index 0000000..0a204b3
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for a floating-point type
+ *
+ * A valid representation consists of:
+ *  - an optional sign (`+` or `-`)
+ *  - a decimal number, using `.` as the decimal separator and no grouping
+ *  - an optional E-notation suffix: the letter 'e' or 'E', an optional
+ *    sign, and an integer
+ *
+ * Thus, for example, "12", "-.4", "6.022e23", or "+1.7e-10".
+ *
+ * The result from validate() is a PHP float.
+ *
+ * ValidationException codes:
+ *  - 'badfloat': The value was invalid. No data.
+ *  - 'notfinite': The value was in a valid format, but conversion resulted in
+ *    infinity or NAN.
+ *
+ * @since 1.34
+ */
+class FloatDef extends TypeDef {
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               // Use a regex so as to avoid any potential oddness PHP's default conversion might allow.
+               if ( !preg_match( '/^[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?$/D', $value ) ) {
+                       throw new ValidationException( $name, $value, $settings, 'badfloat', [] );
+               }
+
+               $ret = (float)$value;
+               if ( !is_finite( $ret ) ) {
+                       throw new ValidationException( $name, $value, $settings, 'notfinite', [] );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Attempt to fix locale weirdness
+        *
+        * We don't have any usable number formatting function that's not locale-aware,
+        * and `setlocale()` isn't safe in multithreaded environments. Sigh.
+        *
+        * @param string $value Value to fix
+        * @return string
+        */
+       private function fixLocaleWeirdness( $value ) {
+               $localeData = localeconv();
+               if ( $localeData['decimal_point'] !== '.' ) {
+                       $value = strtr( $value, [
+                               $localeData['decimal_point'] => '.',
+                               // PHP's number formatting currently uses only the first byte from 'decimal_point'.
+                               // See upstream bug https://bugs.php.net/bug.php?id=78113
+                               $localeData['decimal_point'][0] => '.',
+                       ] );
+               }
+               return $value;
+       }
+
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               // Ensure sufficient precision for round-tripping. PHP_FLOAT_DIG was added in PHP 7.2.
+               $digits = defined( 'PHP_FLOAT_DIG' ) ? PHP_FLOAT_DIG : 15;
+               return $this->fixLocaleWeirdness( sprintf( "%.{$digits}g", $value ) );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/IntegerDef.php b/includes/libs/ParamValidator/TypeDef/IntegerDef.php
new file mode 100644 (file)
index 0000000..556301b
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for integer types
+ *
+ * A valid representation consists of an optional sign (`+` or `-`) followed by
+ * one or more decimal digits.
+ *
+ * The result from validate() is a PHP integer.
+ *
+ * * ValidationException codes:
+ *  - 'badinteger': The value was invalid or could not be represented as a PHP
+ *    integer. No data.
+ *
+ * Additional codes may be generated when using certain PARAM constants. See
+ * the constants' documentation for details.
+ *
+ * @since 1.34
+ */
+class IntegerDef extends TypeDef {
+
+       /**
+        * (bool) Whether to enforce the specified range.
+        *
+        * If set and truthy, ValidationExceptions from PARAM_MIN, PARAM_MAX, and
+        * PARAM_MAX2 are non-fatal.
+        */
+       const PARAM_IGNORE_RANGE = 'param-ignore-range';
+
+       /**
+        * (int) Minimum allowed value.
+        *
+        * ValidationException codes:
+        *  - 'belowminimum': The value was below the allowed minimum. Data:
+        *     - 'min': Allowed minimum, or empty string if there is none.
+        *     - 'max': Allowed (normal) maximum, or empty string if there is none.
+        *     - 'max2': Allowed (high limits) maximum, or empty string if there is none.
+        */
+       const PARAM_MIN = 'param-min';
+
+       /**
+        * (int) Maximum allowed value (normal limits)
+        *
+        * ValidationException codes:
+        *  - 'abovemaximum': The value was above the allowed maximum. Data:
+        *     - 'min': Allowed minimum, or empty string if there is none.
+        *     - 'max': Allowed (normal) maximum, or empty string if there is none.
+        *     - 'max2': Allowed (high limits) maximum, or empty string if there is none.
+        */
+       const PARAM_MAX = 'param-max';
+
+       /**
+        * (int) Maximum allowed value (high limits)
+        *
+        * If not specified, PARAM_MAX will be enforced for all users. Ignored if
+        * PARAM_MAX is not set.
+        *
+        * ValidationException codes:
+        *  - 'abovehighmaximum': The value was above the allowed maximum. Data:
+        *     - 'min': Allowed minimum, or empty string if there is none.
+        *     - 'max': Allowed (normal) maximum, or empty string if there is none.
+        *     - 'max2': Allowed (high limits) maximum, or empty string if there is none.
+        */
+       const PARAM_MAX2 = 'param-max2';
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               if ( !preg_match( '/^[+-]?\d+$/D', $value ) ) {
+                       throw new ValidationException( $name, $value, $settings, 'badinteger', [] );
+               }
+               $ret = intval( $value, 10 );
+
+               // intval() returns min/max on overflow, so check that
+               if ( $ret === PHP_INT_MAX || $ret === PHP_INT_MIN ) {
+                       $tmp = ( $ret < 0 ? '-' : '' ) . ltrim( $value, '-0' );
+                       if ( $tmp !== (string)$ret ) {
+                               throw new ValidationException( $name, $value, $settings, 'badinteger', [] );
+                       }
+               }
+
+               $min = $settings[self::PARAM_MIN] ?? null;
+               $max = $settings[self::PARAM_MAX] ?? null;
+               $max2 = $settings[self::PARAM_MAX2] ?? null;
+               $err = null;
+
+               if ( $min !== null && $ret < $min ) {
+                       $err = 'belowminimum';
+                       $ret = $min;
+               } elseif ( $max !== null && $ret > $max ) {
+                       if ( $max2 !== null && $this->callbacks->useHighLimits( $options ) ) {
+                               if ( $ret > $max2 ) {
+                                       $err = 'abovehighmaximum';
+                                       $ret = $max2;
+                               }
+                       } else {
+                               $err = 'abovemaximum';
+                               $ret = $max;
+                       }
+               }
+               if ( $err !== null ) {
+                       $ex = new ValidationException( $name, $value, $settings, $err, [
+                               'min' => $min === null ? '' : $min,
+                               'max' => $max === null ? '' : $max,
+                               'max2' => $max2 === null ? '' : $max2,
+                       ] );
+                       if ( empty( $settings[self::PARAM_IGNORE_RANGE] ) ) {
+                               throw $ex;
+                       }
+                       $this->callbacks->recordCondition( $ex, $options );
+               }
+
+               return $ret;
+       }
+
+       public function normalizeSettings( array $settings ) {
+               if ( !isset( $settings[self::PARAM_MAX] ) ) {
+                       unset( $settings[self::PARAM_MAX2] );
+               }
+
+               if ( isset( $settings[self::PARAM_MAX2] ) && isset( $settings[self::PARAM_MAX] ) &&
+                       $settings[self::PARAM_MAX2] < $settings[self::PARAM_MAX]
+               ) {
+                       $settings[self::PARAM_MAX2] = $settings[self::PARAM_MAX];
+               }
+
+               return parent::normalizeSettings( $settings );
+       }
+
+       public function describeSettings( $name, array $settings, array $options ) {
+               $info = parent::describeSettings( $name, $settings, $options );
+
+               $min = $settings[self::PARAM_MIN] ?? '';
+               $max = $settings[self::PARAM_MAX] ?? '';
+               $max2 = $settings[self::PARAM_MAX2] ?? '';
+               if ( $max === '' || $max2 !== '' && $max2 <= $max ) {
+                       $max2 = '';
+               }
+
+               if ( empty( $options['compact'] ) ) {
+                       if ( $min !== '' ) {
+                               $info['min'] = $min;
+                       }
+                       if ( $max !== '' ) {
+                               $info['max'] = $max;
+                       }
+                       if ( $max2 !== '' ) {
+                               $info['max2'] = $max2;
+                       }
+               } else {
+                       $key = '';
+                       if ( $min !== '' ) {
+                               $key = 'min';
+                       }
+                       if ( $max2 !== '' ) {
+                               $key .= 'max2';
+                       } elseif ( $max !== '' ) {
+                               $key .= 'max';
+                       }
+                       if ( $key !== '' ) {
+                               $info[$key] = [ 'min' => $min, 'max' => $max, 'max2' => $max2 ];
+                       }
+               }
+
+               return $info;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/LimitDef.php b/includes/libs/ParamValidator/TypeDef/LimitDef.php
new file mode 100644 (file)
index 0000000..99780c4
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+/**
+ * Type definition for "limit" types
+ *
+ * A limit type is an integer type that also accepts the magic value "max".
+ * IntegerDef::PARAM_MIN defaults to 0 for this type.
+ *
+ * @see IntegerDef
+ * @since 1.34
+ */
+class LimitDef extends IntegerDef {
+
+       /**
+        * @inheritDoc
+        *
+        * Additional `$options` accepted:
+        *  - 'parse-limit': (bool) Default true, set false to return 'max' rather
+        *    than determining the effective value.
+        */
+       public function validate( $name, $value, array $settings, array $options ) {
+               if ( $value === 'max' ) {
+                       if ( !isset( $options['parse-limit'] ) || $options['parse-limit'] ) {
+                               $value = $this->callbacks->useHighLimits( $options )
+                                       ? $settings[self::PARAM_MAX2] ?? $settings[self::PARAM_MAX] ?? PHP_INT_MAX
+                                       : $settings[self::PARAM_MAX] ?? PHP_INT_MAX;
+                       }
+                       return $value;
+               }
+
+               return parent::validate( $name, $value, $settings, $options );
+       }
+
+       public function normalizeSettings( array $settings ) {
+               $settings += [
+                       self::PARAM_MIN => 0,
+               ];
+
+               return parent::normalizeSettings( $settings );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/PasswordDef.php b/includes/libs/ParamValidator/TypeDef/PasswordDef.php
new file mode 100644 (file)
index 0000000..289db54
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+
+/**
+ * Type definition for "password" types
+ *
+ * This is a string type that forces PARAM_SENSITIVE = true.
+ *
+ * @see StringDef
+ * @since 1.34
+ */
+class PasswordDef extends StringDef {
+
+       public function normalizeSettings( array $settings ) {
+               $settings[ParamValidator::PARAM_SENSITIVE] = true;
+               return parent::normalizeSettings( $settings );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/PresenceBooleanDef.php b/includes/libs/ParamValidator/TypeDef/PresenceBooleanDef.php
new file mode 100644 (file)
index 0000000..2e1c8f5
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\TypeDef;
+
+/**
+ * Type definition for checkbox-like boolean types
+ *
+ * This boolean is considered true if the parameter is present in the request,
+ * regardless of value. The only way for it to be false is for the parameter to
+ * be omitted entirely.
+ *
+ * The result from validate() is a PHP boolean.
+ *
+ * @since 1.34
+ */
+class PresenceBooleanDef extends TypeDef {
+
+       public function getValue( $name, array $settings, array $options ) {
+               return $this->callbacks->hasParam( $name, $options );
+       }
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               return (bool)$value;
+       }
+
+       public function describeSettings( $name, array $settings, array $options ) {
+               $info = parent::describeSettings( $name, $settings, $options );
+               unset( $info['default'] );
+               return $info;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/StringDef.php b/includes/libs/ParamValidator/TypeDef/StringDef.php
new file mode 100644 (file)
index 0000000..0ed310b
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\Callbacks;
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for string types
+ *
+ * The result from validate() is a PHP string.
+ *
+ * ValidationException codes:
+ *  - 'missingparam': The parameter is the empty string (and that's not allowed). No data.
+ *
+ * Additional codes may be generated when using certain PARAM constants. See
+ * the constants' documentation for details.
+ *
+ * @since 1.34
+ */
+class StringDef extends TypeDef {
+
+       /**
+        * (integer) Maximum length of a string in bytes.
+        *
+        * ValidationException codes:
+        *  - 'maxbytes': The string is too long. Data:
+        *     - 'maxbytes': The maximum number of bytes allowed
+        *     - 'maxchars': The maximum number of characters allowed
+        */
+       const PARAM_MAX_BYTES = 'param-max-bytes';
+
+       /**
+        * (integer) Maximum length of a string in characters (Unicode codepoints).
+        *
+        * The string is assumed to be encoded as UTF-8.
+        *
+        * ValidationException codes:
+        *  - 'maxchars': The string is too long. Data:
+        *     - 'maxbytes': The maximum number of bytes allowed
+        *     - 'maxchars': The maximum number of characters allowed
+        */
+       const PARAM_MAX_CHARS = 'param-max-chars';
+
+       protected $allowEmptyWhenRequired = false;
+
+       /**
+        * @param Callbacks $callbacks
+        * @param array $options Options:
+        *  - allowEmptyWhenRequired: (bool) Whether to reject the empty string when PARAM_REQUIRED.
+        *    Defaults to false.
+        */
+       public function __construct( Callbacks $callbacks, array $options = [] ) {
+               parent::__construct( $callbacks );
+
+               $this->allowEmptyWhenRequired = !empty( $options['allowEmptyWhenRequired'] );
+       }
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               if ( !$this->allowEmptyWhenRequired && $value === '' &&
+                       !empty( $settings[ParamValidator::PARAM_REQUIRED] )
+               ) {
+                       throw new ValidationException( $name, $value, $settings, 'missingparam', [] );
+               }
+
+               if ( isset( $settings[self::PARAM_MAX_BYTES] )
+                       && strlen( $value ) > $settings[self::PARAM_MAX_BYTES]
+               ) {
+                       throw new ValidationException( $name, $value, $settings, 'maxbytes', [
+                               'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? '',
+                               'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? '',
+                       ] );
+               }
+               if ( isset( $settings[self::PARAM_MAX_CHARS] )
+                       && mb_strlen( $value, 'UTF-8' ) > $settings[self::PARAM_MAX_CHARS]
+               ) {
+                       throw new ValidationException( $name, $value, $settings, 'maxchars', [
+                               'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? '',
+                               'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? '',
+                       ] );
+               }
+
+               return $value;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/TimestampDef.php b/includes/libs/ParamValidator/TypeDef/TimestampDef.php
new file mode 100644 (file)
index 0000000..5d0bf4e
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\Callbacks;
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+use Wikimedia\Timestamp\TimestampException;
+
+/**
+ * Type definition for timestamp types
+ *
+ * This uses the wikimedia/timestamp library for parsing and formatting the
+ * timestamps.
+ *
+ * The result from validate() is a ConvertibleTimestamp by default, but this
+ * may be changed by both a constructor option and a PARAM constant.
+ *
+ * ValidationException codes:
+ *  - 'badtimestamp': The timestamp is not valid. No data, but the
+ *    TimestampException is available via Exception::getPrevious().
+ *  - 'unclearnowtimestamp': Non-fatal. The value is the empty string or "0".
+ *    Use 'now' instead if you really want the current timestamp. No data.
+ *
+ * @since 1.34
+ */
+class TimestampDef extends TypeDef {
+
+       /**
+        * (string|int) Timestamp format to return from validate()
+        *
+        * Values include:
+        *  - 'ConvertibleTimestamp': A ConvertibleTimestamp object.
+        *  - 'DateTime': A PHP DateTime object
+        *  - One of ConvertibleTimestamp's TS_* constants.
+        *
+        * This does not affect the format returned by stringifyValue().
+        */
+       const PARAM_TIMESTAMP_FORMAT = 'param-timestamp-format';
+
+       /** @var string|int */
+       protected $defaultFormat;
+
+       /** @var int */
+       protected $stringifyFormat;
+
+       /**
+        * @param Callbacks $callbacks
+        * @param array $options Options:
+        *  - defaultFormat: (string|int) Default for PARAM_TIMESTAMP_FORMAT.
+        *    Default if not specified is 'ConvertibleTimestamp'.
+        *  - stringifyFormat: (int) Format to use for stringifyValue().
+        *    Default is TS_ISO_8601.
+        */
+       public function __construct( Callbacks $callbacks, array $options = [] ) {
+               parent::__construct( $callbacks );
+
+               $this->defaultFormat = $options['defaultFormat'] ?? 'ConvertibleTimestamp';
+               $this->stringifyFormat = $options['stringifyFormat'] ?? TS_ISO_8601;
+       }
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               // Confusing synonyms for the current time accepted by ConvertibleTimestamp
+               if ( !$value ) {
+                       $this->callbacks->recordCondition(
+                               new ValidationException( $name, $value, $settings, 'unclearnowtimestamp', [] ),
+                               $options
+                       );
+                       $value = 'now';
+               }
+
+               try {
+                       $timestamp = new ConvertibleTimestamp( $value === 'now' ? false : $value );
+               } catch ( TimestampException $ex ) {
+                       throw new ValidationException( $name, $value, $settings, 'badtimestamp', [], $ex );
+               }
+
+               $format = $settings[self::PARAM_TIMESTAMP_FORMAT] ?? $this->defaultFormat;
+               switch ( $format ) {
+                       case 'ConvertibleTimestamp':
+                               return $timestamp;
+
+                       case 'DateTime':
+                               // Eew, no getter.
+                               return $timestamp->timestamp;
+
+                       default:
+                               return $timestamp->getTimestamp( $format );
+               }
+       }
+
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               if ( !$value instanceof ConvertibleTimestamp ) {
+                       $value = new ConvertibleTimestamp( $value );
+               }
+               return $value->getTimestamp( $this->stringifyFormat );
+       }
+
+}
diff --git a/includes/libs/ParamValidator/TypeDef/UploadDef.php b/includes/libs/ParamValidator/TypeDef/UploadDef.php
new file mode 100644 (file)
index 0000000..b436a6d
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Psr\Http\Message\UploadedFileInterface;
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\Util\UploadedFile;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Type definition for upload types
+ *
+ * The result from validate() is an object implementing UploadedFileInterface.
+ *
+ * ValidationException codes:
+ *  - 'badupload': The upload is not valid. No data.
+ *  - 'badupload-inisize': The upload exceeded the maximum in php.ini. Data:
+ *     - 'size': The configured size (in bytes).
+ *  - 'badupload-formsize': The upload exceeded the maximum in the form post. No data.
+ *  - 'badupload-partial': The file was only partially uploaded. No data.
+ *  - 'badupload-nofile': There was no file. No data.
+ *  - 'badupload-notmpdir': PHP has no temporary directory to store the upload. No data.
+ *  - 'badupload-cantwrite': PHP could not store the upload. No data.
+ *  - 'badupload-phpext': A PHP extension rejected the upload. No data.
+ *  - 'badupload-notupload': The field was present in the submission but was not encoded as
+ *    an upload. No data.
+ *  - 'badupload-unknown': Some unknown PHP upload error code. Data:
+ *     - 'code': The code.
+ *
+ * @since 1.34
+ */
+class UploadDef extends TypeDef {
+
+       public function getValue( $name, array $settings, array $options ) {
+               $ret = $this->callbacks->getUploadedFile( $name, $options );
+
+               if ( $ret && $ret->getError() === UPLOAD_ERR_NO_FILE &&
+                       !$this->callbacks->hasParam( $name, $options )
+               ) {
+                       // This seems to be that the client explicitly specified "no file" for the field
+                       // instead of just omitting the field completely. DWTM.
+                       $ret = null;
+               } elseif ( !$ret && $this->callbacks->hasParam( $name, $options ) ) {
+                       // The client didn't format their upload properly so it came in as an ordinary
+                       // field. Convert it to an error.
+                       $ret = new UploadedFile( [
+                               'name' => '',
+                               'type' => '',
+                               'tmp_name' => '',
+                               'error' => -42, // PHP's UPLOAD_ERR_* are all positive numbers.
+                               'size' => 0,
+                       ] );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Fetch the value of PHP's upload_max_filesize ini setting
+        *
+        * This method exists so it can be mocked by unit tests that can't
+        * affect ini_get() directly.
+        *
+        * @codeCoverageIgnore
+        * @return string|false
+        */
+       protected function getIniSize() {
+               return ini_get( 'upload_max_filesize' );
+       }
+
+       public function validate( $name, $value, array $settings, array $options ) {
+               static $codemap = [
+                       -42 => 'notupload', // Local from getValue()
+                       UPLOAD_ERR_FORM_SIZE => 'formsize',
+                       UPLOAD_ERR_PARTIAL => 'partial',
+                       UPLOAD_ERR_NO_FILE => 'nofile',
+                       UPLOAD_ERR_NO_TMP_DIR => 'notmpdir',
+                       UPLOAD_ERR_CANT_WRITE => 'cantwrite',
+                       UPLOAD_ERR_EXTENSION => 'phpext',
+               ];
+
+               if ( !$value instanceof UploadedFileInterface ) {
+                       // Err?
+                       throw new ValidationException( $name, $value, $settings, 'badupload', [] );
+               }
+
+               $err = $value->getError();
+               if ( $err === UPLOAD_ERR_OK ) {
+                       return $value;
+               } elseif ( $err === UPLOAD_ERR_INI_SIZE ) {
+                       static $prefixes = [
+                               'g' => 1024 ** 3,
+                               'm' => 1024 ** 2,
+                               'k' => 1024 ** 1,
+                       ];
+                       $size = $this->getIniSize();
+                       $last = strtolower( substr( $size, -1 ) );
+                       $size = intval( $size, 10 ) * ( $prefixes[$last] ?? 1 );
+                       throw new ValidationException( $name, $value, $settings, 'badupload-inisize', [
+                               'size' => $size,
+                       ] );
+               } elseif ( isset( $codemap[$err] ) ) {
+                       throw new ValidationException( $name, $value, $settings, 'badupload-' . $codemap[$err], [] );
+               } else {
+                       throw new ValidationException( $name, $value, $settings, 'badupload-unknown', [
+                               'code' => $err,
+                       ] );
+               }
+       }
+
+       public function stringifyValue( $name, $value, array $settings, array $options ) {
+               // Not going to happen.
+               return null;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/Util/UploadedFile.php b/includes/libs/ParamValidator/Util/UploadedFile.php
new file mode 100644 (file)
index 0000000..2be9119
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+
+namespace Wikimedia\ParamValidator\Util;
+
+use Psr\Http\Message\UploadedFileInterface;
+use RuntimeException;
+use Wikimedia\AtEase\AtEase;
+
+/**
+ * A simple implementation of UploadedFileInterface
+ *
+ * This exists so ParamValidator needn't depend on any specific PSR-7
+ * implementation for a class implementing UploadedFileInterface. It shouldn't
+ * be used directly by other code, other than perhaps when implementing
+ * Callbacks::getUploadedFile() when another PSR-7 library is not already in use.
+ *
+ * @since 1.34
+ */
+class UploadedFile implements UploadedFileInterface {
+
+       /** @var array File data */
+       private $data;
+
+       /** @var bool */
+       private $fromUpload;
+
+       /** @var UploadedFileStream|null */
+       private $stream = null;
+
+       /** @var bool Whether moveTo() was called */
+       private $moved = false;
+
+       /**
+        * @param array $data Data from $_FILES
+        * @param bool $fromUpload Set false if using this task with data not from
+        *  $_FILES. Intended for unit testing.
+        */
+       public function __construct( array $data, $fromUpload = true ) {
+               $this->data = $data;
+               $this->fromUpload = $fromUpload;
+       }
+
+       /**
+        * Throw if there was an error
+        * @throws RuntimeException
+        */
+       private function checkError() {
+               switch ( $this->data['error'] ) {
+                       case UPLOAD_ERR_OK:
+                               break;
+
+                       case UPLOAD_ERR_INI_SIZE:
+                               throw new RuntimeException( 'Upload exceeded maximum size' );
+
+                       case UPLOAD_ERR_FORM_SIZE:
+                               throw new RuntimeException( 'Upload exceeded form-specified maximum size' );
+
+                       case UPLOAD_ERR_PARTIAL:
+                               throw new RuntimeException( 'File was only partially uploaded' );
+
+                       case UPLOAD_ERR_NO_FILE:
+                               throw new RuntimeException( 'No file was uploaded' );
+
+                       case UPLOAD_ERR_NO_TMP_DIR:
+                               throw new RuntimeException( 'PHP has no temporary folder for storing uploaded files' );
+
+                       case UPLOAD_ERR_CANT_WRITE:
+                               throw new RuntimeException( 'PHP was unable to save the uploaded file' );
+
+                       case UPLOAD_ERR_EXTENSION:
+                               throw new RuntimeException( 'A PHP extension stopped the file upload' );
+
+                       default:
+                               throw new RuntimeException( 'Unknown upload error code ' . $this->data['error'] );
+               }
+
+               if ( $this->moved ) {
+                       throw new RuntimeException( 'File has already been moved' );
+               }
+               if ( !isset( $this->data['tmp_name'] ) || !file_exists( $this->data['tmp_name'] ) ) {
+                       throw new RuntimeException( 'Uploaded file is missing' );
+               }
+       }
+
+       public function getStream() {
+               if ( $this->stream ) {
+                       return $this->stream;
+               }
+
+               $this->checkError();
+               $this->stream = new UploadedFileStream( $this->data['tmp_name'] );
+               return $this->stream;
+       }
+
+       public function moveTo( $targetPath ) {
+               $this->checkError();
+
+               if ( $this->fromUpload && !is_uploaded_file( $this->data['tmp_name'] ) ) {
+                       throw new RuntimeException( 'Specified file is not an uploaded file' );
+               }
+
+               // TODO remove the function_exists check once we drop HHVM support
+               if ( function_exists( 'error_clear_last' ) ) {
+                       error_clear_last();
+               }
+               $ret = AtEase::quietCall(
+                       $this->fromUpload ? 'move_uploaded_file' : 'rename',
+                       $this->data['tmp_name'],
+                       $targetPath
+               );
+               if ( $ret === false ) {
+                       $err = error_get_last();
+                       throw new RuntimeException( "Move failed: " . ( $err['message'] ?? 'Unknown error' ) );
+               }
+
+               $this->moved = true;
+               if ( $this->stream ) {
+                       $this->stream->close();
+                       $this->stream = null;
+               }
+       }
+
+       public function getSize() {
+               return $this->data['size'] ?? null;
+       }
+
+       public function getError() {
+               return $this->data['error'] ?? UPLOAD_ERR_NO_FILE;
+       }
+
+       public function getClientFilename() {
+               $ret = $this->data['name'] ?? null;
+               return $ret === '' ? null : $ret;
+       }
+
+       public function getClientMediaType() {
+               $ret = $this->data['type'] ?? null;
+               return $ret === '' ? null : $ret;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/Util/UploadedFileStream.php b/includes/libs/ParamValidator/Util/UploadedFileStream.php
new file mode 100644 (file)
index 0000000..17eaaf4
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+
+namespace Wikimedia\ParamValidator\Util;
+
+use Exception;
+use Psr\Http\Message\StreamInterface;
+use RuntimeException;
+use Throwable;
+use Wikimedia\AtEase\AtEase;
+
+/**
+ * Implementation of StreamInterface for a file in $_FILES
+ *
+ * This exists so ParamValidator needn't depend on any specific PSR-7
+ * implementation for a class implementing UploadedFileInterface. It shouldn't
+ * be used directly by other code.
+ *
+ * @internal
+ * @since 1.34
+ */
+class UploadedFileStream implements StreamInterface {
+
+       /** @var resource File handle */
+       private $fp;
+
+       /** @var int|false|null File size. False if not set yet. */
+       private $size = false;
+
+       /**
+        * Call, throwing on error
+        * @param callable $func Callable to call
+        * @param array $args Arguments
+        * @param mixed $fail Failure return value
+        * @param string $msg Message prefix
+        * @return mixed
+        * @throws RuntimeException if $func returns $fail
+        */
+       private static function quietCall( callable $func, array $args, $fail, $msg ) {
+               // TODO remove the function_exists check once we drop HHVM support
+               if ( function_exists( 'error_clear_last' ) ) {
+                       error_clear_last();
+               }
+               $ret = AtEase::quietCall( $func, ...$args );
+               if ( $ret === $fail ) {
+                       $err = error_get_last();
+                       throw new RuntimeException( "$msg: " . ( $err['message'] ?? 'Unknown error' ) );
+               }
+               return $ret;
+       }
+
+       /**
+        * @param string $filename
+        */
+       public function __construct( $filename ) {
+               $this->fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
+       }
+
+       /**
+        * Check if the stream is open
+        * @throws RuntimeException if closed
+        */
+       private function checkOpen() {
+               if ( !$this->fp ) {
+                       throw new RuntimeException( 'Stream is not open' );
+               }
+       }
+
+       public function __destruct() {
+               $this->close();
+       }
+
+       public function __toString() {
+               try {
+                       $this->seek( 0 );
+                       return $this->getContents();
+               } catch ( Exception $ex ) {
+                       // Not allowed to throw
+                       return '';
+               } catch ( Throwable $ex ) {
+                       // Not allowed to throw
+                       return '';
+               }
+       }
+
+       public function close() {
+               if ( $this->fp ) {
+                       // Spec doesn't care about close errors.
+                       AtEase::quietCall( 'fclose', $this->fp );
+                       $this->fp = null;
+               }
+       }
+
+       public function detach() {
+               $ret = $this->fp;
+               $this->fp = null;
+               return $ret;
+       }
+
+       public function getSize() {
+               if ( $this->size === false ) {
+                       $this->size = null;
+
+                       if ( $this->fp ) {
+                               // Spec doesn't care about errors here.
+                               $stat = AtEase::quietCall( 'fstat', $this->fp );
+                               $this->size = $stat['size'] ?? null;
+                       }
+               }
+
+               return $this->size;
+       }
+
+       public function tell() {
+               $this->checkOpen();
+               return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' );
+       }
+
+       public function eof() {
+               // Spec doesn't care about errors here.
+               return !$this->fp || AtEase::quietCall( 'feof', $this->fp );
+       }
+
+       public function isSeekable() {
+               return (bool)$this->fp;
+       }
+
+       public function seek( $offset, $whence = SEEK_SET ) {
+               $this->checkOpen();
+               self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' );
+       }
+
+       public function rewind() {
+               $this->seek( 0 );
+       }
+
+       public function isWritable() {
+               return false;
+       }
+
+       public function write( $string ) {
+               $this->checkOpen();
+               throw new RuntimeException( 'Stream is read-only' );
+       }
+
+       public function isReadable() {
+               return (bool)$this->fp;
+       }
+
+       public function read( $length ) {
+               $this->checkOpen();
+               return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' );
+       }
+
+       public function getContents() {
+               $this->checkOpen();
+               return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' );
+       }
+
+       public function getMetadata( $key = null ) {
+               $this->checkOpen();
+               $ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' );
+               if ( $key !== null ) {
+                       $ret = $ret[$key] ?? null;
+               }
+               return $ret;
+       }
+
+}
diff --git a/includes/libs/ParamValidator/ValidationException.php b/includes/libs/ParamValidator/ValidationException.php
new file mode 100644 (file)
index 0000000..c8d995e
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use Exception;
+use Throwable;
+use UnexpectedValueException;
+
+/**
+ * Error reporting for ParamValidator
+ *
+ * @since 1.34
+ */
+class ValidationException extends UnexpectedValueException {
+
+       /** @var string */
+       protected $paramName;
+
+       /** @var mixed */
+       protected $paramValue;
+
+       /** @var array */
+       protected $settings;
+
+       /** @var string */
+       protected $failureCode;
+
+       /** @var (string|int|string[])[] */
+       protected $failureData;
+
+       /**
+        * @param string $name Parameter name being validated
+        * @param mixed $value Value of the parameter
+        * @param array $settings Settings array being used for validation
+        * @param string $code Failure code. See getFailureCode() for requirements.
+        * @param (string|int|string[])[] $data Data for the failure code.
+        *  See getFailureData() for requirements.
+        * @param Throwable|Exception|null $previous Previous exception causing this failure
+        */
+       public function __construct( $name, $value, $settings, $code, $data, $previous = null ) {
+               parent::__construct( self::formatMessage( $name, $code, $data ), 0, $previous );
+
+               $this->paramName = $name;
+               $this->paramValue = $value;
+               $this->settings = $settings;
+               $this->failureCode = $code;
+               $this->failureData = $data;
+       }
+
+       /**
+        * Make a simple English message for the exception
+        * @param string $name
+        * @param string $code
+        * @param array $data
+        * @return string
+        */
+       private static function formatMessage( $name, $code, $data ) {
+               $ret = "Validation of `$name` failed: $code";
+               foreach ( $data as $k => $v ) {
+                       if ( is_array( $v ) ) {
+                               $v = implode( ', ', $v );
+                       }
+                       $ret .= "; $k => $v";
+               }
+               return $ret;
+       }
+
+       /**
+        * Fetch the parameter name that failed validation
+        * @return string
+        */
+       public function getParamName() {
+               return $this->paramName;
+       }
+
+       /**
+        * Fetch the parameter value that failed validation
+        * @return mixed
+        */
+       public function getParamValue() {
+               return $this->paramValue;
+       }
+
+       /**
+        * Fetch the settings array that failed validation
+        * @return array
+        */
+       public function getSettings() {
+               return $this->settings;
+       }
+
+       /**
+        * Fetch the validation failure code
+        *
+        * A validation failure code is a reasonably short string matching the regex
+        * `/^[a-z][a-z0-9-]*$/`.
+        *
+        * Users are encouraged to use this with a suitable i18n mechanism rather
+        * than relying on the limited English text returned by getMessage().
+        *
+        * @return string
+        */
+       public function getFailureCode() {
+               return $this->failureCode;
+       }
+
+       /**
+        * Fetch the validation failure data
+        *
+        * This returns additional data relevant to the particular failure code.
+        *
+        * Keys in the array are short ASCII strings. Values are strings or
+        * integers, or arrays of strings intended to be displayed as a
+        * comma-separated list. For any particular code the same keys are always
+        * returned in the same order, making it safe to use array_values() and
+        * access them positionally if that is desired.
+        *
+        * For example, the data for a hypothetical "integer-out-of-range" code
+        * might have data `[ 'min' => 0, 'max' => 100 ]` indicating the range of
+        * allowed values.
+        *
+        * @return (string|int|string[])[]
+        */
+       public function getFailureData() {
+               return $this->failureData;
+       }
+
+}
index 16cb1ed..71a0e34 100644 (file)
@@ -107,7 +107,7 @@ class StatusValue {
                        } else {
                                $errorsOnlyStatusValue->errors[] = $item;
                        }
-               };
+               }
 
                return [ $errorsOnlyStatusValue, $warningsOnlyStatusValue ];
        }
index a326df2..e7dc926 100644 (file)
@@ -849,7 +849,7 @@ EOT;
                $callback = $this->guessCallback;
                if ( $callback ) {
                        $callback( $this, $head, $tail, $file, $mime /* by reference */ );
-               };
+               }
 
                return $mime;
        }
index 5a36c65..465fe82 100644 (file)
@@ -45,6 +45,7 @@ class APCBagOStuff extends BagOStuff {
        const KEY_SUFFIX = ':4';
 
        public function __construct( array $params = [] ) {
+               $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
                parent::__construct( $params );
                // The extension serializer is still buggy, unlike "php" and "igbinary"
                $this->nativeSerialize = ( ini_get( 'apc.serializer' ) !== 'default' );
@@ -62,7 +63,7 @@ class APCBagOStuff extends BagOStuff {
                return $value;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                apc_store(
                        $key . self::KEY_SUFFIX,
                        $this->nativeSerialize ? $value : $this->serialize( $value ),
@@ -80,7 +81,7 @@ class APCBagOStuff extends BagOStuff {
                );
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                apc_delete( $key . self::KEY_SUFFIX );
 
                return true;
@@ -93,12 +94,4 @@ class APCBagOStuff extends BagOStuff {
        public function decr( $key, $value = 1 ) {
                return apc_dec( $key . self::KEY_SUFFIX, $value );
        }
-
-       protected function serialize( $value ) {
-               return $this->isInteger( $value ) ? (int)$value : serialize( $value );
-       }
-
-       protected function unserialize( $value ) {
-               return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
-       }
 }
index 0d9822a..b14ac7c 100644 (file)
@@ -45,6 +45,7 @@ class APCUBagOStuff extends BagOStuff {
        const KEY_SUFFIX = ':4';
 
        public function __construct( array $params = [] ) {
+               $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
                parent::__construct( $params );
                // The extension serializer is still buggy, unlike "php" and "igbinary"
                $this->nativeSerialize = ( ini_get( 'apc.serializer' ) !== 'default' );
@@ -54,7 +55,7 @@ class APCUBagOStuff extends BagOStuff {
                $casToken = null;
 
                $blob = apcu_fetch( $key . self::KEY_SUFFIX );
-               $value = $this->unserialize( $blob );
+               $value = $this->nativeSerialize ? $blob : $this->unserialize( $blob );
                if ( $value !== false ) {
                        $casToken = $blob; // don't bother hashing this
                }
@@ -62,10 +63,10 @@ class APCUBagOStuff extends BagOStuff {
                return $value;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                return apcu_store(
                        $key . self::KEY_SUFFIX,
-                       $this->serialize( $value ),
+                       $this->nativeSerialize ? $value : $this->serialize( $value ),
                        $exptime
                );
        }
@@ -73,12 +74,12 @@ class APCUBagOStuff extends BagOStuff {
        public function add( $key, $value, $exptime = 0, $flags = 0 ) {
                return apcu_add(
                        $key . self::KEY_SUFFIX,
-                       $this->serialize( $value ),
+                       $this->nativeSerialize ? $value : $this->serialize( $value ),
                        $exptime
                );
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                apcu_delete( $key . self::KEY_SUFFIX );
 
                return true;
@@ -101,20 +102,4 @@ class APCUBagOStuff extends BagOStuff {
                        return false;
                }
        }
-
-       protected function serialize( $value ) {
-               if ( $this->nativeSerialize ) {
-                       return $value;
-               }
-
-               return $this->isInteger( $value ) ? (int)$value : serialize( $value );
-       }
-
-       protected function unserialize( $value ) {
-               if ( $this->nativeSerialize ) {
-                       return $value;
-               }
-
-               return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
-       }
 }
index 321476b..50441c5 100644 (file)
@@ -50,8 +50,14 @@ use Wikimedia\WaitConditionLoop;
  * For any given instance, methods like lock(), unlock(), merge(), and set() with WRITE_SYNC
  * should semantically operate over its entire access scope; any nodes/threads in that scope
  * should serialize appropriately when using them. Likewise, a call to get() with READ_LATEST
- * from one node in its access scope should reflect the prior changes of any other node its access
- * scope. Any get() should reflect the changes of any prior set() with WRITE_SYNC.
+ * from one node in its access scope should reflect the prior changes of any other node its
+ * access scope. Any get() should reflect the changes of any prior set() with WRITE_SYNC.
+ *
+ * Subclasses should override the default "segmentationSize" field with an appropriate value.
+ * The value should not be larger than what the storage backend (by default) supports. It also
+ * should be roughly informed by common performance bottlenecks (e.g. values over a certain size
+ * having poor scalability). The same goes for the "segmentedValueMaxSize" member, which limits
+ * the maximum size and chunk count (indirectly) of values.
  *
  * @ingroup Cache
  */
@@ -68,6 +74,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        protected $asyncHandler;
        /** @var int Seconds */
        protected $syncTimeout;
+       /** @var int Bytes; chunk size of segmented cache values */
+       protected $segmentationSize;
+       /** @var int Bytes; maximum total size of a segmented cache value */
+       protected $segmentedValueMaxSize;
 
        /** @var bool */
        private $debugMode = false;
@@ -93,6 +103,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        /** Bitfield constants for set()/merge() */
        const WRITE_SYNC = 4; // synchronously write to all locations for replicated stores
        const WRITE_CACHE_ONLY = 8; // Only change state of the in-memory cache
+       const WRITE_ALLOW_SEGMENTS = 16; // Allow partitioning of the value if it is large
+       const WRITE_PRUNE_SEGMENTS = 32; // Delete all partition segments of the value
+
+       /** @var string Component to use for key construction of blob segment keys */
+       const SEGMENT_COMPONENT = 'segment';
 
        /**
         * $params include:
@@ -103,6 +118,12 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         *   - reportDupes: Whether to emit warning log messages for all keys that were
         *      requested more than once (requires an asyncHandler).
         *   - syncTimeout: How long to wait with WRITE_SYNC in seconds.
+        *   - segmentationSize: The chunk size, in bytes, of segmented values. The value should
+        *      not exceed the maximum size of values in the storage backend, as configured by
+        *      the site administrator.
+        *   - segmentedValueMaxSize: The maximum total size, in bytes, of segmented values.
+        *      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
         */
        public function __construct( array $params = [] ) {
@@ -119,6 +140,8 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                }
 
                $this->syncTimeout = $params['syncTimeout'] ?? 3;
+               $this->segmentationSize = $params['segmentationSize'] ?? 8388608; // 8MiB
+               $this->segmentedValueMaxSize = $params['segmentedValueMaxSize'] ?? 67108864; // 64MiB
        }
 
        /**
@@ -180,7 +203,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        public function get( $key, $flags = 0 ) {
                $this->trackDuplicateKeys( $key );
 
-               return $this->doGet( $key, $flags );
+               return $this->resolveSegments( $key, $this->doGet( $key, $flags ) );
        }
 
        /**
@@ -233,16 +256,112 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
         */
-       abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               if (
+                       ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS ||
+                       is_infinite( $this->segmentationSize )
+               ) {
+                       return $this->doSet( $key, $value, $exptime, $flags );
+               }
+
+               $serialized = $this->serialize( $value );
+               $segmentSize = $this->getSegmentationSize();
+               $maxTotalSize = $this->getSegmentedValueMaxSize();
+
+               $size = strlen( $serialized );
+               if ( $size <= $segmentSize ) {
+                       // Since the work of serializing it was already done, just use it inline
+                       return $this->doSet(
+                               $key,
+                               SerializedValueContainer::newUnified( $serialized ),
+                               $exptime,
+                               $flags
+                       );
+               } elseif ( $size > $maxTotalSize ) {
+                       $this->setLastError( "Key $key exceeded $maxTotalSize bytes." );
+
+                       return false;
+               }
+
+               $chunksByKey = [];
+               $segmentHashes = [];
+               $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 );
+               for ( $i = 0; $i < $count; ++$i ) {
+                       $segment = substr( $serialized, $i * $segmentSize, $segmentSize );
+                       $hash = sha1( $segment );
+                       $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash );
+                       $chunksByKey[$chunkKey] = $segment;
+                       $segmentHashes[] = $hash;
+               }
+
+               $ok = $this->setMulti( $chunksByKey, $exptime, $flags );
+               if ( $ok ) {
+                       // Only when all segments are stored should the main key be changed
+                       $ok = $this->doSet(
+                               $key,
+                               SerializedValueContainer::newSegmented( $segmentHashes ),
+                               $exptime,
+                               $flags
+                       );
+               }
+
+               return $ok;
+       }
+
+       /**
+        * Set an item
+        *
+        * @param string $key
+        * @param mixed $value
+        * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+        * @return bool Success
+        */
+       abstract protected function doSet( $key, $value, $exptime = 0, $flags = 0 );
 
        /**
         * Delete an item
         *
+        * For large values written using WRITE_ALLOW_SEGMENTS, this only deletes the main
+        * segment list key unless WRITE_PRUNE_SEGMENTS is in the flags. While deleting the segment
+        * list key has the effect of functionally deleting the key, it leaves unused blobs in cache.
+        *
         * @param string $key
         * @return bool True if the item was deleted or not found, false on failure
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         */
-       abstract public function delete( $key, $flags = 0 );
+       public function delete( $key, $flags = 0 ) {
+               if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) {
+                       return $this->doDelete( $key, $flags );
+               }
+
+               $mainValue = $this->doGet( $key, self::READ_LATEST );
+               if ( !$this->doDelete( $key, $flags ) ) {
+                       return false;
+               }
+
+               if ( !SerializedValueContainer::isSegmented( $mainValue ) ) {
+                       return true; // no segments to delete
+               }
+
+               $orderedKeys = array_map(
+                       function ( $segmentHash ) use ( $key ) {
+                               return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
+                       },
+                       $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
+               );
+
+               return $this->deleteMulti( $orderedKeys, $flags );
+       }
+
+       /**
+        * Delete an item
+        *
+        * @param string $key
+        * @return bool True if the item was deleted or not found, false on failure
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+        */
+       abstract protected function doDelete( $key, $flags = 0 );
 
        /**
         * Insert an item if it does not already exist
@@ -291,7 +410,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                        $casToken = null; // passed by reference
                        // Get the old value and CAS token from cache
                        $this->clearLastError();
-                       $currentValue = $this->doGet( $key, self::READ_LATEST, $casToken );
+                       $currentValue = $this->resolveSegments(
+                               $key,
+                               $this->doGet( $key, self::READ_LATEST, $casToken )
+                       );
                        if ( $this->getLastError() ) {
                                $this->logger->warning(
                                        __METHOD__ . ' failed due to I/O error on get() for {key}.',
@@ -324,6 +446,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
 
                                return false; // IO error; don't spam retries
                        }
+
                } while ( !$success && --$attempts );
 
                return $success;
@@ -338,7 +461,6 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
-        * @throws Exception
         */
        protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
                if ( !$this->lock( $key, 0 ) ) {
@@ -368,28 +490,40 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         *
         * If an expiry in the past is given then the key will immediately be expired
         *
+        * For large values written using WRITE_ALLOW_SEGMENTS, this only changes the TTL of the
+        * main segment list key. While lowering the TTL of the segment list key has the effect of
+        * functionally lowering the TTL of the key, it might leave unused blobs in cache for longer.
+        * Raising the TTL of such keys is not effective, since the expiration of a single segment
+        * key effectively expires the entire value.
+        *
         * @param string $key
-        * @param int $expiry TTL or UNIX timestamp
+        * @param int $exptime TTL or UNIX timestamp
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
         * @return bool Success Returns false on failure or if the item does not exist
         * @since 1.28
         */
-       public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
-               $found = false;
+       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+               $expiry = $this->convertToExpiry( $exptime );
+               $delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
 
-               $ok = $this->merge(
-                       $key,
-                       function ( $cache, $ttl, $currentValue ) use ( &$found ) {
-                               $found = ( $currentValue !== false );
+               if ( !$this->lock( $key, 0 ) ) {
+                       return false;
+               }
+               // Use doGet() to avoid having to trigger resolveSegments()
+               $blob = $this->doGet( $key, self::READ_LATEST );
+               if ( $blob ) {
+                       if ( $delete ) {
+                               $ok = $this->doDelete( $key, $flags );
+                       } else {
+                               $ok = $this->doSet( $key, $blob, $exptime, $flags );
+                       }
+               } else {
+                       $ok = false;
+               }
 
-                               return $currentValue; // nothing is written if this is false
-                       },
-                       $expiry,
-                       1, // 1 attempt
-                       $flags
-               );
+               $this->unlock( $key );
 
-               return ( $ok && $found );
+               return $ok;
        }
 
        /**
@@ -459,7 +593,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) {
                        unset( $this->locks[$key] );
 
-                       $ok = $this->delete( "{$key}:lock" );
+                       $ok = $this->doDelete( "{$key}:lock" );
                        if ( !$ok ) {
                                $this->logger->warning(
                                        __METHOD__ . ' failed to release lock for {key}.',
@@ -533,9 +667,25 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @return array
         */
        public function getMulti( array $keys, $flags = 0 ) {
+               $valuesBykey = $this->doGetMulti( $keys, $flags );
+               foreach ( $valuesBykey as $key => $value ) {
+                       // Resolve one blob at a time (avoids too much I/O at once)
+                       $valuesBykey[$key] = $this->resolveSegments( $key, $value );
+               }
+
+               return $valuesBykey;
+       }
+
+       /**
+        * Get an associative array containing the item for each of the keys that have items.
+        * @param string[] $keys List of keys
+        * @param int $flags Bitfield; supports READ_LATEST [optional]
+        * @return array
+        */
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                $res = [];
                foreach ( $keys as $key ) {
-                       $val = $this->get( $key, $flags );
+                       $val = $this->doGet( $key, $flags );
                        if ( $val !== false ) {
                                $res[$key] = $val;
                        }
@@ -546,6 +696,9 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
 
        /**
         * Batch insertion/replace
+        *
+        * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
+        *
         * @param mixed[] $data Map of (key => value)
         * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
@@ -553,11 +706,13 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @since 1.24
         */
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+               if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+                       throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+               }
+
                $res = true;
                foreach ( $data as $key => $value ) {
-                       if ( !$this->set( $key, $value, $exptime, $flags ) ) {
-                               $res = false;
-                       }
+                       $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
                }
 
                return $res;
@@ -565,6 +720,9 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
 
        /**
         * Batch deletion
+        *
+        * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
+        *
         * @param string[] $keys List of keys
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
@@ -573,7 +731,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        public function deleteMulti( array $keys, $flags = 0 ) {
                $res = true;
                foreach ( $keys as $key ) {
-                       $res = $this->delete( $key, $flags ) && $res;
+                       $res = $this->doDelete( $key, $flags ) && $res;
                }
 
                return $res;
@@ -624,6 +782,43 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                return $newValue;
        }
 
+       /**
+        * Get and reassemble the chunks of blob at the given key
+        *
+        * @param string $key
+        * @param mixed $mainValue
+        * @return string|null|bool The combined string, false if missing, null on error
+        */
+       protected function resolveSegments( $key, $mainValue ) {
+               if ( SerializedValueContainer::isUnified( $mainValue ) ) {
+                       return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
+               }
+
+               if ( SerializedValueContainer::isSegmented( $mainValue ) ) {
+                       $orderedKeys = array_map(
+                               function ( $segmentHash ) use ( $key ) {
+                                       return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
+                               },
+                               $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
+                       );
+
+                       $segmentsByKey = $this->doGetMulti( $orderedKeys );
+
+                       $parts = [];
+                       foreach ( $orderedKeys as $segmentKey ) {
+                               if ( isset( $segmentsByKey[$segmentKey] ) ) {
+                                       $parts[] = $segmentsByKey[$segmentKey];
+                               } else {
+                                       return false; // missing segment
+                               }
+                       }
+
+                       return $this->unserialize( implode( '', $parts ) );
+               }
+
+               return $mainValue;
+       }
+
        /**
         * Get the "last error" registered; clearLastError() should be called manually
         * @return int ERR_* constant for the "last error" registry
@@ -732,7 +927,15 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @return bool
         */
        protected function isInteger( $value ) {
-               return ( is_int( $value ) || ctype_digit( $value ) );
+               if ( is_int( $value ) ) {
+                       return true;
+               } elseif ( !is_string( $value ) ) {
+                       return false;
+               }
+
+               $integer = (int)$value;
+
+               return ( $value === (string)$integer );
        }
 
        /**
@@ -784,6 +987,22 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
        }
 
+       /**
+        * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit)
+        * @since 1.34
+        */
+       public function getSegmentationSize() {
+               return $this->segmentationSize;
+       }
+
+       /**
+        * @return int|float Maximum total segmented object size in bytes (INF for no limit)
+        * @since 1.34
+        */
+       public function getSegmentedValueMaxSize() {
+               return $this->segmentedValueMaxSize;
+       }
+
        /**
         * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
         *
@@ -822,4 +1041,22 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        public function setMockTime( &$time ) {
                $this->wallClockOverride =& $time;
        }
+
+       /**
+        * @param mixed $value
+        * @return string|int String/integer representation
+        * @note Special handling is usually needed for integers so incr()/decr() work
+        */
+       protected function serialize( $value ) {
+               return is_int( $value ) ? $value : serialize( $value );
+       }
+
+       /**
+        * @param string|int $value
+        * @return mixed Original value or false on error
+        * @note Special handling is usually needed for integers so incr()/decr() work
+        */
+       protected function unserialize( $value ) {
+               return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
+       }
 }
index ffe3a4c..575bc58 100644 (file)
@@ -33,15 +33,15 @@ class EmptyBagOStuff extends BagOStuff {
                return false;
        }
 
-       public function add( $key, $value, $exp = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exp = 0, $flags = 0 ) {
                return true;
        }
 
-       public function set( $key, $value, $exp = 0, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                return true;
        }
 
-       public function delete( $key, $flags = 0 ) {
+       public function add( $key, $value, $exptime = 0, $flags = 0 ) {
                return true;
        }
 
@@ -49,6 +49,10 @@ class EmptyBagOStuff extends BagOStuff {
                return false;
        }
 
+       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+               return false; // faster
+       }
+
        public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                return true; // faster
        }
index d24f408..016bdfe 100644 (file)
@@ -49,6 +49,7 @@ class HashBagOStuff extends BagOStuff {
         *   - maxKeys : only allow this many keys (using oldest-first eviction)
         */
        function __construct( $params = [] ) {
+               $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
                parent::__construct( $params );
 
                $this->token = microtime( true ) . ':' . mt_rand();
@@ -75,7 +76,7 @@ class HashBagOStuff extends BagOStuff {
                return $this->bag[$key][self::KEY_VAL];
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                // Refresh key position for maxCacheKeys eviction
                unset( $this->bag[$key] );
                $this->bag[$key] = [
@@ -94,14 +95,14 @@ class HashBagOStuff extends BagOStuff {
        }
 
        public function add( $key, $value, $exptime = 0, $flags = 0 ) {
-               if ( $this->get( $key ) === false ) {
-                       return $this->set( $key, $value, $exptime, $flags );
+               if ( $this->hasKey( $key ) && !$this->expire( $key ) ) {
+                       return false; // key already set
                }
 
-               return false; // key already set
+               return $this->doSet( $key, $value, $exptime, $flags );
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                unset( $this->bag[$key] );
 
                return true;
@@ -136,7 +137,7 @@ class HashBagOStuff extends BagOStuff {
                        return false;
                }
 
-               $this->delete( $key );
+               $this->doDelete( $key );
 
                return true;
        }
index 3d6bd16..cfbf2b3 100644 (file)
  *
  * @ingroup Cache
  */
-class MemcachedBagOStuff extends BagOStuff {
-       /** @var MemcachedClient|Memcached */
-       protected $client;
-
+abstract class MemcachedBagOStuff extends BagOStuff {
        function __construct( array $params ) {
                parent::__construct( $params );
 
                $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE; // unreliable
+               $this->segmentationSize = $params['maxPreferedKeySize'] ?? 917504; // < 1MiB
        }
 
        /**
@@ -50,55 +48,6 @@ class MemcachedBagOStuff extends BagOStuff {
                ];
        }
 
-       protected function doGet( $key, $flags = 0, &$casToken = null ) {
-               return $this->client->get( $this->validateKeyEncoding( $key ), $casToken );
-       }
-
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
-               return $this->client->set( $this->validateKeyEncoding( $key ), $value,
-                       $this->fixExpiry( $exptime ) );
-       }
-
-       protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
-               return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ),
-                       $value, $this->fixExpiry( $exptime ) );
-       }
-
-       public function delete( $key, $flags = 0 ) {
-               return $this->client->delete( $this->validateKeyEncoding( $key ) );
-       }
-
-       public function add( $key, $value, $exptime = 0, $flags = 0 ) {
-               return $this->client->add( $this->validateKeyEncoding( $key ), $value,
-                       $this->fixExpiry( $exptime ) );
-       }
-
-       public function incr( $key, $value = 1 ) {
-               $n = $this->client->incr( $this->validateKeyEncoding( $key ), $value );
-
-               return ( $n !== false && $n !== null ) ? $n : false;
-       }
-
-       public function decr( $key, $value = 1 ) {
-               $n = $this->client->decr( $this->validateKeyEncoding( $key ), $value );
-
-               return ( $n !== false && $n !== null ) ? $n : false;
-       }
-
-       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
-               return $this->client->touch( $this->validateKeyEncoding( $key ),
-                       $this->fixExpiry( $exptime ) );
-       }
-
-       /**
-        * Get the underlying client object. This is provided for debugging
-        * purposes.
-        * @return MemcachedClient|Memcached
-        */
-       public function getClient() {
-               return $this->client;
-       }
-
        /**
         * Construct a cache key.
         *
index 937ca55..eecf7ec 100644 (file)
@@ -278,6 +278,23 @@ class MemcachedClient {
        }
 
        // }}}
+
+       /**
+        * @param mixed $value
+        * @return string|integer
+        */
+       public function serialize( $value ) {
+               return serialize( $value );
+       }
+
+       /**
+        * @param string $value
+        * @return mixed
+        */
+       public function unserialize( $value ) {
+               return unserialize( $value );
+       }
+
        // {{{ add()
 
        /**
@@ -503,7 +520,8 @@ class MemcachedClient {
 
                if ( $this->_debug ) {
                        foreach ( $val as $k => $v ) {
-                               $this->_debugprint( sprintf( "MemCache: sock %s got %s", serialize( $sock ), $k ) );
+                               $this->_debugprint(
+                                       sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
                        }
                }
 
@@ -1018,7 +1036,7 @@ class MemcachedClient {
                                         * yet read "END"), these 2 calls would collide.
                                         */
                                        if ( $flags & self::SERIALIZED ) {
-                                               $ret[$rkey] = unserialize( $ret[$rkey] );
+                                               $ret[$rkey] = $this->unserialize( $ret[$rkey] );
                                        } elseif ( $flags & self::INTVAL ) {
                                                $ret[$rkey] = intval( $ret[$rkey] );
                                        }
@@ -1072,7 +1090,7 @@ class MemcachedClient {
                if ( is_int( $val ) ) {
                        $flags |= self::INTVAL;
                } elseif ( !is_scalar( $val ) ) {
-                       $val = serialize( $val );
+                       $val = $this->serialize( $val );
                        $flags |= self::SERIALIZED;
                        if ( $this->_debug ) {
                                $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
index db94503..43cebd3 100644 (file)
@@ -27,6 +27,8 @@
  * @ingroup Cache
  */
 class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
+       /** @var Memcached */
+       protected $client;
 
        /**
         * Available parameters are:
@@ -93,24 +95,22 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
 
                // Set the serializer
-               switch ( $params['serializer'] ) {
-                       case 'php':
-                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
-                               break;
-                       case 'igbinary':
-                               if ( !Memcached::HAVE_IGBINARY ) {
-                                       throw new InvalidArgumentException(
-                                               __CLASS__ . ': the igbinary extension is not available ' .
-                                               'but igbinary serialization was requested.'
-                                       );
-                               }
-                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
-                               break;
-                       default:
+               $ok = false;
+               if ( $params['serializer'] === 'php' ) {
+                       $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+               } elseif ( $params['serializer'] === 'igbinary' ) {
+                       if ( !Memcached::HAVE_IGBINARY ) {
                                throw new InvalidArgumentException(
-                                       __CLASS__ . ': invalid value for serializer parameter'
+                                       __CLASS__ . ': the igbinary extension is not available ' .
+                                       'but igbinary serialization was requested.'
                                );
+                       }
+                       $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
+               }
+               if ( !$ok ) {
+                       throw new InvalidArgumentException( __CLASS__ . ': invalid serializer parameter' );
                }
+
                $servers = [];
                foreach ( $params['servers'] as $host ) {
                        if ( preg_match( '/^\[(.+)\]:(\d+)$/', $host, $m ) ) {
@@ -138,9 +138,6 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $params;
        }
 
-       /**
-        * @suppress PhanTypeNonVarPassByRef
-        */
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                $this->debug( "get($key)" );
                if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
@@ -160,9 +157,13 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $result;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->debug( "set($key)" );
-               $result = parent::set( $key, $value, $exptime, $flags = 0 );
+               $result = $this->client->set(
+                       $this->validateKeyEncoding( $key ),
+                       $value,
+                       $this->fixExpiry( $exptime )
+               );
                if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) {
                        // "Not stored" is always used as the mcrouter response with AllAsyncRoute
                        return true;
@@ -172,12 +173,14 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
        protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
                $this->debug( "cas($key)" );
-               return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime, $flags ) );
+               $result = $this->client->cas( $casToken, $this->validateKeyEncoding( $key ),
+                       $value, $this->fixExpiry( $exptime ) );
+               return $this->checkResult( $key, $result );
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                $this->debug( "delete($key)" );
-               $result = parent::delete( $key );
+               $result = $this->client->delete( $this->validateKeyEncoding( $key ) );
                if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
                        // "Not found" is counted as success in our interface
                        return true;
@@ -187,7 +190,12 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
        public function add( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->debug( "add($key)" );
-               return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
+               $result = $this->client->add(
+                       $this->validateKeyEncoding( $key ),
+                       $value,
+                       $this->fixExpiry( $exptime )
+               );
+               return $this->checkResult( $key, $result );
        }
 
        public function incr( $key, $value = 1 ) {
@@ -242,7 +250,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $result;
        }
 
-       public function getMulti( array $keys, $flags = 0 ) {
+       public function doGetMulti( array $keys, $flags = 0 ) {
                $this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
@@ -260,9 +268,55 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( false, $result );
        }
 
-       public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
+       public function deleteMulti( array $keys, $flags = 0 ) {
+               $this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
+               foreach ( $keys as $key ) {
+                       $this->validateKeyEncoding( $key );
+               }
+               $result = $this->client->deleteMulti( $keys ) ?: [];
+               $ok = true;
+               foreach ( $result as $code ) {
+                       if ( !in_array( $code, [ true, Memcached::RES_NOTFOUND ], true ) ) {
+                               // "Not found" is counted as success in our interface
+                               $ok = false;
+                       }
+               }
+               return $this->checkResult( false, $ok );
+       }
+
+       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
                $this->debug( "touch($key)" );
-               $result = $this->client->touch( $key, $expiry );
+               $result = $this->client->touch( $key, $exptime );
                return $this->checkResult( $key, $result );
        }
+
+       protected function serialize( $value ) {
+               if ( is_int( $value ) ) {
+                       return $value;
+               }
+
+               $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+               if ( $serializer === Memcached::SERIALIZER_PHP ) {
+                       return serialize( $value );
+               } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
+                       return igbinary_serialize( $value );
+               }
+
+               throw new UnexpectedValueException( __METHOD__ . ": got serializer '$serializer'." );
+       }
+
+       protected function unserialize( $value ) {
+               if ( $this->isInteger( $value ) ) {
+                       return (int)$value;
+               }
+
+               $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+               if ( $serializer === Memcached::SERIALIZER_PHP ) {
+                       return unserialize( $value );
+               } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
+                       return igbinary_unserialize( $value );
+               }
+
+               throw new UnexpectedValueException( __METHOD__ . ": got serializer '$serializer'." );
+       }
 }
index 8f190c3..ea73cba 100644 (file)
@@ -27,6 +27,9 @@
  * @ingroup Cache
  */
 class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
+       /** @var MemcachedClient */
+       protected $client;
+
        /**
         * Available parameters are:
         *   - servers:             The list of IP:port combinations holding the memcached servers.
@@ -51,11 +54,73 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
                $this->client->set_debug( $debug );
        }
 
-       public function getMulti( array $keys, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0, &$casToken = null ) {
+               $casToken = null;
+
+               return $this->client->get( $this->validateKeyEncoding( $key ), $casToken );
+       }
+
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               return $this->client->set(
+                       $this->validateKeyEncoding( $key ),
+                       $value,
+                       $this->fixExpiry( $exptime )
+               );
+       }
+
+       protected function doDelete( $key, $flags = 0 ) {
+               return $this->client->delete( $this->validateKeyEncoding( $key ) );
+       }
+
+       public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+               return $this->client->add(
+                       $this->validateKeyEncoding( $key ),
+                       $value,
+                       $this->fixExpiry( $exptime )
+               );
+       }
+
+       protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
+               return $this->client->cas(
+                       $casToken,
+                       $this->validateKeyEncoding( $key ),
+                       $value,
+                       $this->fixExpiry( $exptime )
+               );
+       }
+
+       public function incr( $key, $value = 1 ) {
+               $n = $this->client->incr( $this->validateKeyEncoding( $key ), $value );
+
+               return ( $n !== false && $n !== null ) ? $n : false;
+       }
+
+       public function decr( $key, $value = 1 ) {
+               $n = $this->client->decr( $this->validateKeyEncoding( $key ), $value );
+
+               return ( $n !== false && $n !== null ) ? $n : false;
+       }
+
+       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+               return $this->client->touch(
+                       $this->validateKeyEncoding( $key ),
+                       $this->fixExpiry( $exptime )
+               );
+       }
+
+       public function doGetMulti( array $keys, $flags = 0 ) {
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
                }
 
                return $this->client->get_multi( $keys );
        }
+
+       protected function serialize( $value ) {
+               return is_int( $value ) ? $value : $this->client->serialize( $value );
+       }
+
+       protected function unserialize( $value ) {
+               return $this->isInteger( $value ) ? (int)$value : $this->client->unserialize( $value );
+       }
 }
index 1ed91ea..0503382 100644 (file)
@@ -130,6 +130,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                                $missIndexes,
                                $this->asyncWrites,
                                'set',
+                               // @TODO: consider using self::WRITE_ALLOW_SEGMENTS here?
                                [ $key, $value, self::UPGRADE_TTL ]
                        );
                }
@@ -356,4 +357,24 @@ class MultiWriteBagOStuff extends BagOStuff {
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
+
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doDelete( $key, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doGetMulti( array $keys, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function serialize( $value ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function unserialize( $value ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
 }
index a10d1a4..2a12689 100644 (file)
@@ -79,6 +79,7 @@ class RESTBagOStuff extends BagOStuff {
        private $extendedErrorBodyFields;
 
        public function __construct( $params ) {
+               $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
                if ( empty( $params['url'] ) ) {
                        throw new InvalidArgumentException( 'URL parameter is required' );
                }
@@ -146,7 +147,7 @@ class RESTBagOStuff extends BagOStuff {
                return false;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                // @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
                // @TODO: respect $exptime
                $req = [
@@ -172,7 +173,7 @@ class RESTBagOStuff extends BagOStuff {
                return false; // key already set
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                // @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
                $req = [
                        'method' => 'DELETE',
index 2c74d45..0ba9c3f 100644 (file)
@@ -106,7 +106,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function set( $key, $value, $expiry = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $expiry = 0, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
@@ -128,7 +128,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
@@ -146,7 +146,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function getMulti( array $keys, $flags = 0 ) {
+       public function doGetMulti( array $keys, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $keys as $key ) {
@@ -351,25 +351,6 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       /**
-        * @param mixed $data
-        * @return string
-        */
-       protected function serialize( $data ) {
-               // Serialize anything but integers so INCR/DECR work
-               // Do not store integer-like strings as integers to avoid type confusion (T62563)
-               return is_int( $data ) ? $data : serialize( $data );
-       }
-
-       /**
-        * @param string $data
-        * @return mixed
-        */
-       protected function unserialize( $data ) {
-               $int = intval( $data );
-               return $data === (string)$int ? $int : unserialize( $data );
-       }
-
        /**
         * Get a Redis object with a connection suitable for fetching the specified key
         * @param string $key
index 70f9096..f79c1ff 100644 (file)
@@ -75,7 +75,7 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               return ( $flags & self::READ_LATEST )
+               return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
                        ? $this->writeStore->get( $key, $flags )
                        : $this->readStore->get( $key, $flags );
        }
@@ -164,4 +164,24 @@ class ReplicatedBagOStuff extends BagOStuff {
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
+
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doDelete( $key, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doGetMulti( array $keys, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function serialize( $value ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function unserialize( $blob ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
 }
index dac3421..1d8662a 100644 (file)
@@ -1464,7 +1464,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param string $kClass
         * @param float $elapsed Seconds spent regenerating the value
         * @param float $lockTSE
-        * @param $hasLock bool
+        * @param bool $hasLock
         * @return bool Whether it is OK to proceed with a key set operation
         */
        private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
index 8c419b2..9d7e143 100644 (file)
@@ -36,7 +36,7 @@ class WinCacheBagOStuff extends BagOStuff {
                        return false;
                }
 
-               $value = unserialize( $blob );
+               $value = $this->unserialize( $blob );
                if ( $value !== false ) {
                        $casToken = (string)$blob; // don't bother hashing this
                }
@@ -67,8 +67,8 @@ class WinCacheBagOStuff extends BagOStuff {
                return $success;
        }
 
-       public function set( $key, $value, $expire = 0, $flags = 0 ) {
-               $result = wincache_ucache_set( $key, serialize( $value ), $expire );
+       protected function doSet( $key, $value, $expire = 0, $flags = 0 ) {
+               $result = wincache_ucache_set( $key, $this->serialize( $value ), $expire );
 
                // false positive, wincache_ucache_set returns an empty array
                // in some circumstances.
@@ -77,7 +77,11 @@ class WinCacheBagOStuff extends BagOStuff {
        }
 
        public function add( $key, $value, $exptime = 0, $flags = 0 ) {
-               $result = wincache_ucache_add( $key, serialize( $value ), $exptime );
+               if ( wincache_ucache_exists( $key ) ) {
+                       return false; // avoid warnings
+               }
+
+               $result = wincache_ucache_add( $key, $this->serialize( $value ), $exptime );
 
                // false positive, wincache_ucache_add returns an empty array
                // in some circumstances.
@@ -85,7 +89,7 @@ class WinCacheBagOStuff extends BagOStuff {
                return ( $result === [] || $result === true );
        }
 
-       public function delete( $key, $flags = 0 ) {
+       protected function doDelete( $key, $flags = 0 ) {
                wincache_ucache_delete( $key );
 
                return true;
diff --git a/includes/libs/objectcache/serialized/SerializedValueContainer.php b/includes/libs/objectcache/serialized/SerializedValueContainer.php
new file mode 100644 (file)
index 0000000..7c7d8aa
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * Helper class for segmenting large cache values without relying on serializing classes
+ *
+ * @since 1.34
+ */
+class SerializedValueContainer {
+       const SCHEMA = '__svc_schema__';
+       const SCHEMA_UNIFIED = 'DAAIDgoKAQw'; // 64 bit UID
+       const SCHEMA_SEGMENTED = 'CAYCDAgCDw4'; // 64 bit UID
+
+       const UNIFIED_DATA = '__data__';
+       const SEGMENTED_HASHES = '__hashes__';
+
+       /**
+        * @param string $serialized
+        * @return stdClass
+        */
+       public static function newUnified( $serialized ) {
+               return (object)[
+                       self::SCHEMA => self::SCHEMA_UNIFIED,
+                       self::UNIFIED_DATA => $serialized
+               ];
+       }
+
+       /**
+        * @param string[] $segmentHashList Ordered list of hashes for each segment
+        * @return stdClass
+        */
+       public static function newSegmented( array $segmentHashList ) {
+               return (object)[
+                       self::SCHEMA => self::SCHEMA_SEGMENTED,
+                       self::SEGMENTED_HASHES => $segmentHashList
+               ];
+       }
+
+       /**
+        * @param mixed $value
+        * @return bool
+        */
+       public static function isUnified( $value ) {
+               return self::instanceOf( $value, self::SCHEMA_UNIFIED );
+       }
+
+       /**
+        * @param mixed $value
+        * @return bool
+        */
+       public static function isSegmented( $value ) {
+               return self::instanceOf( $value, self::SCHEMA_SEGMENTED );
+       }
+
+       /**
+        * @param mixed $value
+        * @param string $schema SCHEMA_* class constant
+        * @return bool
+        */
+       private static function instanceOf( $value, $schema ) {
+               return (
+                       $value instanceof stdClass &&
+                       property_exists( $value, self::SCHEMA ) &&
+                       $value->{self::SCHEMA} === $schema
+               );
+       }
+}
index 88bc049..24b5402 100644 (file)
@@ -176,7 +176,7 @@ class ChronologyProtector implements LoggerAwareInterface {
                }
 
                $masterName = $lb->getServerName( $lb->getWriterIndex() );
-               if ( $lb->getServerCount() > 1 ) {
+               if ( $lb->hasStreamingReplicaServers() ) {
                        $pos = $lb->getMasterPos();
                        if ( $pos ) {
                                $this->logger->debug( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
index b216892..8af6bb3 100644 (file)
@@ -35,7 +35,7 @@ class DBConnRef implements IDatabase {
        public function __construct( ILoadBalancer $lb, $conn, $role ) {
                $this->lb = $lb;
                $this->role = $role;
-               if ( $conn instanceof Database ) {
+               if ( $conn instanceof IDatabase && !( $conn instanceof DBConnRef ) ) {
                        $this->conn = $conn; // live handle
                } elseif ( is_array( $conn ) && count( $conn ) >= 4 && $conn[self::FLD_DOMAIN] !== false ) {
                        $this->params = $conn;
@@ -461,7 +461,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function buildLike() {
+       public function buildLike( $param ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
@@ -740,6 +740,19 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function __toString() {
+               if ( $this->conn === null ) {
+                       // spl_object_id is PHP >= 7.2
+                       $id = function_exists( 'spl_object_id' )
+                               ? spl_object_id( $this )
+                               : spl_object_hash( $this );
+
+                       return $this->getType() . ' object #' . $id;
+               }
+
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        /**
         * Error out if the role is not DB_MASTER
         *
index fe23a38..bc8883c 100644 (file)
@@ -46,38 +46,6 @@ use RuntimeException;
  * @since 1.28
  */
 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
-       /** Number of times to re-try an operation in case of deadlock */
-       const DEADLOCK_TRIES = 4;
-       /** Minimum time to wait before retry, in microseconds */
-       const DEADLOCK_DELAY_MIN = 500000;
-       /** Maximum time to wait before retry */
-       const DEADLOCK_DELAY_MAX = 1500000;
-
-       /** How long before it is worth doing a dummy query to test the connection */
-       const PING_TTL = 1.0;
-       const PING_QUERY = 'SELECT 1 AS ping';
-
-       const TINY_WRITE_SEC = 0.010;
-       const SLOW_WRITE_SEC = 0.500;
-       const SMALL_WRITE_ROWS = 100;
-
-       /** @var string Lock granularity is on the level of the entire database */
-       const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
-       /** @var string The SCHEMA keyword refers to a grouping of tables in a database */
-       const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
-
-       /** @var int New Database instance will not be connected yet when returned */
-       const NEW_UNCONNECTED = 0;
-       /** @var int New Database instance will already be connected when returned */
-       const NEW_CONNECTED = 1;
-
-       /** @var string The last SQL query attempted */
-       private $lastQuery = '';
-       /** @var float|bool UNIX timestamp of last write query */
-       private $lastWriteTime = false;
-       /** @var string|bool */
-       private $lastPhpError = false;
-
        /** @var string Server that this instance is currently connected to */
        protected $server;
        /** @var string User that this instance is currently connected under the name of */
@@ -92,8 +60,23 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $cliMode;
        /** @var string Agent name for query profiling */
        protected $agent;
+       /** @var int Bitfield of class DBO_* constants */
+       protected $flags;
+       /** @var array LoadBalancer tracking information */
+       protected $lbInfo = [];
+       /** @var array|bool Variables use for schema element placeholders */
+       protected $schemaVars = false;
        /** @var array Parameters used by initConnection() to establish a connection */
        protected $connectionParams = [];
+       /** @var array SQL variables values to use for all new connections */
+       protected $connectionVariables = [];
+       /** @var string Current SQL query delimiter */
+       protected $delimiter = ';';
+       /** @var string|bool|null Stashed value of html_errors INI setting */
+       protected $htmlErrors;
+       /** @var int */
+       protected $nonNativeInsertSelectBatchSize = 10000;
+
        /** @var BagOStuff APC cache */
        protected $srvCache;
        /** @var LoggerInterface */
@@ -104,177 +87,100 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $errorLogger;
        /** @var callable Deprecation logging callback */
        protected $deprecationLogger;
+       /** @var callable|null */
+       protected $profiler;
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
+       /** @var DatabaseDomain */
+       protected $currentDomain;
+       /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
+       private $lazyMasterHandle;
 
        /** @var object|resource|null Database connection */
        protected $conn = null;
-       /** @var bool */
+       /** @var bool Whether a connection handle is open (connection itself might be dead) */
        protected $opened = false;
 
-       /** @var array[] List of (callable, method name, atomic section id) */
-       protected $trxIdleCallbacks = [];
-       /** @var array[] List of (callable, method name, atomic section id) */
-       protected $trxPreCommitCallbacks = [];
-       /** @var array[] List of (callable, method name, atomic section id) */
-       protected $trxEndCallbacks = [];
-       /** @var callable[] Map of (name => callable) */
-       protected $trxRecurringCallbacks = [];
-       /** @var bool Whether to suppress triggering of transaction end callbacks */
-       protected $trxEndCallbacksSuppressed = false;
-
-       /** @var int */
-       protected $flags;
-       /** @var array */
-       protected $lbInfo = [];
-       /** @var array|bool */
-       protected $schemaVars = false;
-       /** @var array */
-       protected $sessionVars = [];
-       /** @var array|null */
-       protected $preparedArgs;
-       /** @var string|bool|null Stashed value of html_errors INI setting */
-       protected $htmlErrors;
-       /** @var string */
-       protected $delimiter = ';';
-       /** @var DatabaseDomain */
-       protected $currentDomain;
-       /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
-       protected $affectedRowCount;
+       /** @var array Map of (name => 1) for locks obtained via lock() */
+       protected $sessionNamedLocks = [];
+       /** @var array Map of (table name => 1) for TEMPORARY tables */
+       protected $sessionTempTables = [];
 
-       /**
-        * @var int Transaction status
-        */
-       protected $trxStatus = self::STATUS_TRX_NONE;
-       /**
-        * @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR
-        */
-       protected $trxStatusCause;
-       /**
-        * @var array|null If wasKnownStatementRollbackError() prevented trxStatus from being set,
-        *  the relevant details are stored here.
-        */
-       protected $trxStatusIgnoredCause;
-       /**
-        * Either 1 if a transaction is active or 0 otherwise.
-        * The other Trx fields may not be meaningfull if this is 0.
-        *
-        * @var int
-        */
+       /** @var int Whether there is an active transaction (1 or 0) */
        protected $trxLevel = 0;
-       /**
-        * Either a short hexidecimal string if a transaction is active or ""
-        *
-        * @var string
-        * @see Database::trxLevel
-        */
+       /** @var string Hexidecimal string if a transaction is active or empty string otherwise */
        protected $trxShortId = '';
-       /**
-        * The UNIX time that the transaction started. Callers can assume that if
-        * snapshot isolation is used, then the data is *at least* up to date to that
-        * point (possibly more up-to-date since the first SELECT defines the snapshot).
-        *
-        * @var float|null
-        * @see Database::trxLevel
-        */
+       /** @var int Transaction status */
+       protected $trxStatus = self::STATUS_TRX_NONE;
+       /** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */
+       protected $trxStatusCause;
+       /** @var array|null Error details of the last statement-only rollback */
+       private $trxStatusIgnoredCause;
+       /** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */
        private $trxTimestamp = null;
-       /** @var float Lag estimate at the time of BEGIN */
+       /** @var float Replication lag estimate at the time of BEGIN for the last transaction */
        private $trxReplicaLag = null;
-       /**
-        * Remembers the function name given for starting the most recent transaction via begin().
-        * Used to provide additional context for error reporting.
-        *
-        * @var string
-        * @see Database::trxLevel
-        */
+       /** @var string Name of the function that start the last transaction */
        private $trxFname = null;
-       /**
-        * Record if possible write queries were done in the last transaction started
-        *
-        * @var bool
-        * @see Database::trxLevel
-        */
+       /** @var bool Whether possible write queries were done in the last transaction started */
        private $trxDoneWrites = false;
-       /**
-        * Record if the current transaction was started implicitly due to DBO_TRX being set.
-        *
-        * @var bool
-        * @see Database::trxLevel
-        */
+       /** @var bool Whether the current transaction was started implicitly due to DBO_TRX */
        private $trxAutomatic = false;
-       /**
-        * Counter for atomic savepoint identifiers. Reset when a new transaction begins.
-        *
-        * @var int
-        */
+       /** @var int Counter for atomic savepoint identifiers (reset with each transaction) */
        private $trxAtomicCounter = 0;
-       /**
-        * Array of levels of atomicity within transactions
-        *
-        * @var array List of (name, unique ID, savepoint ID)
-        */
+       /** @var array List of (name, unique ID, savepoint ID) for each active atomic section level */
        private $trxAtomicLevels = [];
-       /**
-        * Record if the current transaction was started implicitly by Database::startAtomic
-        *
-        * @var bool
-        */
+       /** @var bool Whether the current transaction was started implicitly by startAtomic() */
        private $trxAutomaticAtomic = false;
-       /**
-        * Track the write query callers of the current transaction
-        *
-        * @var string[]
-        */
+       /** @var string[] Write query callers of the current transaction */
        private $trxWriteCallers = [];
-       /**
-        * @var float Seconds spent in write queries for the current transaction
-        */
+       /** @var float Seconds spent in write queries for the current transaction */
        private $trxWriteDuration = 0.0;
-       /**
-        * @var int Number of write queries for the current transaction
-        */
+       /** @var int Number of write queries for the current transaction */
        private $trxWriteQueryCount = 0;
-       /**
-        * @var int Number of rows affected by write queries for the current transaction
-        */
+       /** @var int Number of rows affected by write queries for the current transaction */
        private $trxWriteAffectedRows = 0;
-       /**
-        * @var float Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries
-        */
+       /** @var float Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries */
        private $trxWriteAdjDuration = 0.0;
-       /**
-        * @var int Number of write queries counted in trxWriteAdjDuration
-        */
+       /** @var int Number of write queries counted in trxWriteAdjDuration */
        private $trxWriteAdjQueryCount = 0;
-       /**
-        * @var float RTT time estimate
-        */
-       private $rttEstimate = 0.0;
-
-       /** @var array Map of (name => 1) for locks obtained via lock() */
-       private $namedLocksHeld = [];
-       /** @var array Map of (table name => 1) for TEMPORARY tables */
-       protected $sessionTempTables = [];
-
-       /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
-       private $lazyMasterHandle;
-
-       /** @var float UNIX timestamp */
-       protected $lastPing = 0.0;
+       /** @var array[] List of (callable, method name, atomic section id) */
+       private $trxIdleCallbacks = [];
+       /** @var array[] List of (callable, method name, atomic section id) */
+       private $trxPreCommitCallbacks = [];
+       /** @var array[] List of (callable, method name, atomic section id) */
+       private $trxEndCallbacks = [];
+       /** @var callable[] Map of (name => callable) */
+       private $trxRecurringCallbacks = [];
+       /** @var bool Whether to suppress triggering of transaction end callbacks */
+       private $trxEndCallbacksSuppressed = false;
 
        /** @var int[] Prior flags member variable values */
        private $priorFlags = [];
 
-       /** @var callable|null */
-       protected $profiler;
-       /** @var TransactionProfiler */
-       protected $trxProfiler;
+       /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
+       protected $affectedRowCount;
 
-       /** @var int */
-       protected $nonNativeInsertSelectBatchSize = 10000;
+       /** @var float UNIX timestamp */
+       private $lastPing = 0.0;
+       /** @var string The last SQL query attempted */
+       private $lastQuery = '';
+       /** @var float|bool UNIX timestamp of last write query */
+       private $lastWriteTime = false;
+       /** @var string|bool */
+       private $lastPhpError = false;
+       /** @var float Query rount trip time estimate */
+       private $lastRoundTripEstimate = 0.0;
 
-       /** @var string Idiom used when a cancelable atomic section started the transaction */
-       private static $NOT_APPLICABLE = 'n/a';
-       /** @var string Prefix to the atomic section counter used to make savepoint IDs */
-       private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
+       /** @var string Lock granularity is on the level of the entire database */
+       const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
+       /** @var string The SCHEMA keyword refers to a grouping of tables in a database */
+       const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
+
+       /** @var int New Database instance will not be connected yet when returned */
+       const NEW_UNCONNECTED = 0;
+       /** @var int New Database instance will already be connected when returned */
+       const NEW_CONNECTED = 1;
 
        /** @var int Transaction is in a error state requiring a full or savepoint rollback */
        const STATUS_TRX_ERROR = 1;
@@ -283,10 +189,30 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int No transaction is active */
        const STATUS_TRX_NONE = 3;
 
+       /** @var string Idiom used when a cancelable atomic section started the transaction */
+       private static $NOT_APPLICABLE = 'n/a';
+       /** @var string Prefix to the atomic section counter used to make savepoint IDs */
+       private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
+
        /** @var int Writes to this temporary table do not affect lastDoneWrites() */
-       const TEMP_NORMAL = 1;
+       private static $TEMP_NORMAL = 1;
        /** @var int Writes to this temporary table effect lastDoneWrites() */
-       const TEMP_PSEUDO_PERMANENT = 2;
+       private static $TEMP_PSEUDO_PERMANENT = 2;
+
+       /** Number of times to re-try an operation in case of deadlock */
+       private static $DEADLOCK_TRIES = 4;
+       /** Minimum time to wait before retry, in microseconds */
+       private static $DEADLOCK_DELAY_MIN = 500000;
+       /** Maximum time to wait before retry */
+       private static $DEADLOCK_DELAY_MAX = 1500000;
+
+       /** How long before it is worth doing a dummy query to test the connection */
+       private static $PING_TTL = 1.0;
+       private static $PING_QUERY = 'SELECT 1 AS ping';
+
+       private static $TINY_WRITE_SEC = 0.010;
+       private static $SLOW_WRITE_SEC = 0.500;
+       private static $SMALL_WRITE_ROWS = 100;
 
        /**
         * @note exceptions for missing libraries/drivers should be thrown in initConnection()
@@ -312,7 +238,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Disregard deprecated DBO_IGNORE flag (T189999)
                $this->flags &= ~self::DBO_IGNORE;
 
-               $this->sessionVars = $params['variables'];
+               $this->connectionVariables = $params['variables'];
 
                $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
 
@@ -749,7 +675,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
                // For omitted queries, make them count as something at least
                $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
-               $applyTime += self::TINY_WRITE_SEC * $omitted;
+               $applyTime += self::$TINY_WRITE_SEC * $omitted;
 
                return $applyTime;
        }
@@ -1020,7 +946,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * @throws DBUnexpectedError
         */
-       protected function assertHasConnectionHandle() {
+       final protected function assertHasConnectionHandle() {
                if ( !$this->isOpen() ) {
                        throw new DBUnexpectedError( $this, "DB connection was already closed." );
                }
@@ -1029,7 +955,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /**
         * Make sure that this server is not marked as a replica nor read-only as a sanity check
         *
-        * @throws DBUnexpectedError
+        * @throws DBReadOnlyRoleError
+        * @throws DBReadOnlyError
         */
        protected function assertIsWritableMaster() {
                if ( $this->getLBInfo( 'replica' ) === true ) {
@@ -1064,6 +991,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /**
         * Run a query and return a DBMS-dependent wrapper or boolean
         *
+        * This is meant to handle the basic command of actually sending a query to the
+        * server via the driver. No implicit transaction, reconnection, nor retry logic
+        * should happen here. The higher level query() method is designed to handle those
+        * sorts of concerns. This method should not trigger such higher level methods.
+        *
+        * The lastError() and lastErrno() methods should meaningfully reflect what error,
+        * if any, occured during the last call to this method. Methods like executeQuery(),
+        * query(), select(), insert(), update(), delete(), and upsert() implement their calls
+        * to doQuery() such that an immediately subsequent call to lastError()/lastErrno()
+        * meaningfully reflects any error that occured during that public query method call.
+        *
         * For SELECT queries, this returns either:
         *   - a) A driver-specific value/resource, only on success. This can be iterated
         *        over by calling fetchObject()/fetchRow() until there are no more rows.
@@ -1108,11 +1046,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // for all queries within a request. Use cases:
                // - Treating these as writes would trigger ChronologyProtector (see method doc).
                // - We use this method to reject writes to replicas, but we need to allow
-               //   use of transactions on replicas for read snapshots. This fine given
+               //   use of transactions on replicas for read snapshots. This is fine given
                //   that transactions by themselves don't make changes, only actual writes
                //   within the transaction matter, which we still detect.
                return !preg_match(
-                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\(SELECT)\b/i',
+                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
                        $sql
                );
        }
@@ -1141,7 +1079,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected function isTransactableQuery( $sql ) {
                return !in_array(
                        $this->getQueryVerb( $sql ),
-                       [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ],
+                       [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE' ],
                        true
                );
        }
@@ -1159,7 +1097,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $sql,
                        $matches
                ) ) {
-                       $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
+                       $type = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
                        $this->sessionTempTables[$matches[1]] = $type;
 
                        return $type;
@@ -1190,108 +1128,132 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
-               $this->assertTransactionStatus( $sql, $fname );
-               $this->assertHasConnectionHandle();
-
                $flags = (int)$flags; // b/c; this field used to be a bool
-               $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+               // Sanity check that the SQL query is appropriate in the current context and is
+               // allowed for an outside caller (e.g. does not break transaction/session tracking).
+               $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
+
+               // Send the query to the server and fetch any corresponding errors
+               list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
+               if ( $ret === false ) {
+                       $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+                       // Throw an error unless both the ignore flag was set and a rollback is not needed
+                       $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
+               }
+
+               return $this->resultObject( $ret );
+       }
+
+       /**
+        * Execute a query, retrying it if there is a recoverable connection loss
+        *
+        * This is similar to query() except:
+        *   - It does not prevent all non-ROLLBACK queries if there is a corrupted transaction
+        *   - It does not disallow raw queries that are supposed to use dedicated IDatabase methods
+        *   - It does not throw exceptions for common error cases
+        *
+        * This is meant for internal use with Database subclasses.
+        *
+        * @param string $sql Original SQL query
+        * @param string $fname Name of the calling function
+        * @param int $flags Bitfield of class QUERY_* constants
+        * @return array An n-tuple of:
+        *   - mixed|bool: An object, resource, or true on success; false on failure
+        *   - string: The result of calling lastError()
+        *   - int: The result of calling lastErrno()
+        *   - bool: Whether a rollback is needed to allow future non-rollback queries
+        * @throws DBUnexpectedError
+        */
+       final protected function executeQuery( $sql, $fname, $flags ) {
+               $this->assertHasConnectionHandle();
 
                $priorTransaction = $this->trxLevel;
-               $priorWritesPending = $this->writesOrCallbacksPending();
 
                if ( $this->isWriteQuery( $sql ) ) {
                        # In theory, non-persistent writes are allowed in read-only mode, but due to things
                        # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
                        $this->assertIsWritableMaster();
-                       # Do not treat temporary table writes as "meaningful writes" that need committing.
-                       # Profile them as reads. Integration tests can override this behavior via $flags.
+                       # Do not treat temporary table writes as "meaningful writes" since they are only
+                       # visible to one session and are not permanent. Profile them as reads. Integration
+                       # tests can override this behavior via $flags.
                        $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
                        $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
-                       $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
+                       $isPermWrite = ( $tableType !== self::$TEMP_NORMAL );
                        # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
-                       if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+                       if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
                                throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
                        }
                } else {
-                       $isEffectiveWrite = false;
+                       $isPermWrite = false;
                }
 
-               # Add trace comment to the begin of the sql string, right after the operator.
-               # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
+               // Add trace comment to the begin of the sql string, right after the operator.
+               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
                $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
 
-               # Send the query to the server and fetch any corresponding errors
-               $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
-               $lastError = $this->lastError();
-               $lastErrno = $this->lastErrno();
-
-               $recoverableSR = false; // recoverable statement rollback?
-               $recoverableCL = false; // recoverable connection loss?
-
-               if ( $ret === false && $this->wasConnectionLoss() ) {
-                       # Check if no meaningful session state was lost
-                       $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
-                       # Update session state tracking and try to restore the connection
-                       $reconnected = $this->replaceLostConnection( __METHOD__ );
-                       # Silently resend the query to the server if it is safe and possible
-                       if ( $recoverableCL && $reconnected ) {
-                               $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
-                               $lastError = $this->lastError();
-                               $lastErrno = $this->lastErrno();
-
-                               if ( $ret === false && $this->wasConnectionLoss() ) {
-                                       # Query probably causes disconnects; reconnect and do not re-run it
-                                       $this->replaceLostConnection( __METHOD__ );
-                               } else {
-                                       $recoverableCL = false; // connection does not need recovering
-                                       $recoverableSR = $this->wasKnownStatementRollbackError();
-                               }
-                       }
-               } else {
-                       $recoverableSR = $this->wasKnownStatementRollbackError();
+               // Send the query to the server and fetch any corresponding errors
+               list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
+                       $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
+               // Check if the query failed due to a recoverable connection loss
+               if ( $ret === false && $recoverableCL && $reconnected ) {
+                       // Silently resend the query to the server since it is safe and possible
+                       list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
+                               $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
                }
 
+               $corruptedTrx = false;
+
                if ( $ret === false ) {
                        if ( $priorTransaction ) {
                                if ( $recoverableSR ) {
                                        # We're ignoring an error that caused just the current query to be aborted.
                                        # But log the cause so we can log a deprecation notice if a caller actually
                                        # does ignore it.
-                                       $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
+                                       $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
                                } elseif ( !$recoverableCL ) {
                                        # Either the query was aborted or all queries after BEGIN where aborted.
                                        # In the first case, the only options going forward are (a) ROLLBACK, or
                                        # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
                                        # option is ROLLBACK, since the snapshots would have been released.
+                                       $corruptedTrx = true; // cannot recover
                                        $this->trxStatus = self::STATUS_TRX_ERROR;
                                        $this->trxStatusCause =
-                                               $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
-                                       $ignoreErrors = false; // cannot recover
+                                               $this->getQueryExceptionAndLog( $err, $errno, $sql, $fname );
                                        $this->trxStatusIgnoredCause = null;
                                }
                        }
-
-                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
                }
 
-               return $this->resultObject( $ret );
+               return [ $ret, $err, $errno, $corruptedTrx ];
        }
 
        /**
-        * Wrapper for query() that also handles profiling, logging, and affected row count updates
+        * Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count
+        * tracking, and reconnects (without retry) on query failure due to connection loss
         *
         * @param string $sql Original SQL query
         * @param string $commentedSql SQL query with debugging/trace comment
-        * @param bool $isEffectiveWrite Whether the query is a (non-temporary table) write
+        * @param bool $isPermWrite Whether the query is a (non-temporary table) write
         * @param string $fname Name of the calling function
-        * @return bool|IResultWrapper True for a successful write query, ResultWrapper
-        *     object for a successful read query, or false on failure
+        * @param int $flags Bitfield of class QUERY_* constants
+        * @return array An n-tuple of:
+        *   - mixed|bool: An object, resource, or true on success; false on failure
+        *   - string: The result of calling lastError()
+        *   - int: The result of calling lastErrno()
+        *       - bool: Whether a statement rollback error occured
+        *   - bool: Whether a disconnect *both* happened *and* was recoverable
+        *   - bool: Whether a reconnection attempt was *both* made *and* succeeded
+        * @throws DBUnexpectedError
         */
-       private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) {
-               $this->beginIfImplied( $sql, $fname );
+       private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
+               $priorWritesPending = $this->writesOrCallbacksPending();
+
+               if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
+                       $this->beginIfImplied( $sql, $fname );
+               }
 
                // Keep track of whether the transaction has write queries pending
-               if ( $isEffectiveWrite ) {
+               if ( $isPermWrite ) {
                        $this->lastWriteTime = microtime( true );
                        if ( $this->trxLevel && !$this->trxDoneWrites ) {
                                $this->trxDoneWrites = true;
@@ -1310,27 +1272,42 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->affectedRowCount = null;
                $this->lastQuery = $sql;
                $ret = $this->doQuery( $commentedSql );
+               $lastError = $this->lastError();
+               $lastErrno = $this->lastErrno();
+
                $this->affectedRowCount = $this->affectedRows();
                unset( $ps ); // profile out (if set)
                $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
 
+               $recoverableSR = false; // recoverable statement rollback?
+               $recoverableCL = false; // recoverable connection loss?
+               $reconnected = false; // reconnection both attempted and succeeded?
+
                if ( $ret !== false ) {
                        $this->lastPing = $startTime;
-                       if ( $isEffectiveWrite && $this->trxLevel ) {
+                       if ( $isPermWrite && $this->trxLevel ) {
                                $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
                                $this->trxWriteCallers[] = $fname;
                        }
+               } elseif ( $this->wasConnectionError( $lastErrno ) ) {
+                       # Check if no meaningful session state was lost
+                       $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
+                       # Update session state tracking and try to restore the connection
+                       $reconnected = $this->replaceLostConnection( __METHOD__ );
+               } else {
+                       # Check if only the last query was rolled back
+                       $recoverableSR = $this->wasKnownStatementRollbackError();
                }
 
-               if ( $sql === self::PING_QUERY ) {
-                       $this->rttEstimate = $queryRuntime;
+               if ( $sql === self::$PING_QUERY ) {
+                       $this->lastRoundTripEstimate = $queryRuntime;
                }
 
                $this->trxProfiler->recordQueryCompletion(
                        $generalizedSql,
                        $startTime,
-                       $isEffectiveWrite,
-                       $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret )
+                       $isPermWrite,
+                       $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
                );
 
                // Avoid the overhead of logging calls unless debug mode is enabled
@@ -1346,7 +1323,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        );
                }
 
-               return $ret;
+               return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
        }
 
        /**
@@ -1381,13 +1358,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
                // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
                $indicativeOfReplicaRuntime = true;
-               if ( $runtime > self::SLOW_WRITE_SEC ) {
+               if ( $runtime > self::$SLOW_WRITE_SEC ) {
                        $verb = $this->getQueryVerb( $sql );
                        // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
                        if ( $verb === 'INSERT' ) {
-                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
                        } elseif ( $verb === 'REPLACE' ) {
-                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
                        }
                }
 
@@ -1407,7 +1384,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param string $fname
         * @throws DBTransactionStateError
         */
-       private function assertTransactionStatus( $sql, $fname ) {
+       private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
                $verb = $this->getQueryVerb( $sql );
                if ( $verb === 'USE' ) {
                        throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
@@ -1458,7 +1435,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                # Dropped connections also mean that named locks are automatically released.
                # Only allow error suppression in autocommit mode or when the lost transaction
                # didn't matter anyway (aside from DBO_TRX snapshot loss).
-               if ( $this->namedLocksHeld ) {
+               if ( $this->sessionNamedLocks ) {
                        return false; // possible critical section violation
                } elseif ( $this->sessionTempTables ) {
                        return false; // tables might be queried latter
@@ -1485,7 +1462,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->sessionTempTables = [];
                // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
                // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
-               $this->namedLocksHeld = [];
+               $this->sessionNamedLocks = [];
                // Session loss implies transaction loss
                $this->trxLevel = 0;
                $this->trxAtomicCounter = 0;
@@ -2741,11 +2718,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $s );
        }
 
-       public function buildLike() {
-               $params = func_get_args();
-
-               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
-                       $params = $params[0];
+       public function buildLike( $param, ...$params ) {
+               if ( is_array( $param ) ) {
+                       $params = $param;
+               } else {
+                       $params = func_get_args();
                }
 
                $s = '';
@@ -2963,7 +2940,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function textFieldSize( $table, $field ) {
                $table = $this->tableName( $table );
-               $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+               $sql = "SHOW COLUMNS FROM $table LIKE \"$field\"";
                $res = $this->query( $sql, __METHOD__ );
                $row = $this->fetchObject( $res );
 
@@ -3313,7 +3290,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        public function deadlockLoop() {
                $args = func_get_args();
                $function = array_shift( $args );
-               $tries = self::DEADLOCK_TRIES;
+               $tries = self::$DEADLOCK_TRIES;
 
                $this->begin( __METHOD__ );
 
@@ -3327,7 +3304,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        } catch ( DBQueryError $e ) {
                                if ( $this->wasDeadlock() ) {
                                        // Retry after a randomized delay
-                                       usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
+                                       usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
                                } else {
                                        // Throw the error back up
                                        throw $e;
@@ -3976,7 +3953,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
-       final public function rollback( $fname = __METHOD__, $flush = '' ) {
+       final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
                $trxActive = $this->trxLevel;
 
                if ( $flush !== self::FLUSHING_INTERNAL
@@ -4130,20 +4107,20 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function ping( &$rtt = null ) {
                // Avoid hitting the server if it was hit recently
-               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
-                       if ( !func_num_args() || $this->rttEstimate > 0 ) {
-                               $rtt = $this->rttEstimate;
+               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::$PING_TTL ) {
+                       if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
+                               $rtt = $this->lastRoundTripEstimate;
                                return true; // don't care about $rtt
                        }
                }
 
                // This will reconnect if possible or return false if not
                $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
-               $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
+               $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
                $this->restoreFlags( self::RESTORE_PRIOR );
 
                if ( $ok ) {
-                       $rtt = $this->rttEstimate;
+                       $rtt = $this->lastRoundTripEstimate;
                }
 
                return $ok;
@@ -4268,6 +4245,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function getLag() {
+               if ( $this->getLBInfo( 'master' ) ) {
+                       return 0; // this is the master
+               } elseif ( $this->getLBInfo( 'is static' ) ) {
+                       return 0; // static dataset
+               }
+
+               return $this->doGetLag();
+       }
+
+       protected function doGetLag() {
                return 0;
        }
 
@@ -4497,17 +4484,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // RDBMs methods for checking named locks may or may not count this thread itself.
                // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
                // the behavior choosen by the interface for this method.
-               return !isset( $this->namedLocksHeld[$lockName] );
+               return !isset( $this->sessionNamedLocks[$lockName] );
        }
 
        public function lock( $lockName, $method, $timeout = 5 ) {
-               $this->namedLocksHeld[$lockName] = 1;
+               $this->sessionNamedLocks[$lockName] = 1;
 
                return true;
        }
 
        public function unlock( $lockName, $method ) {
-               unset( $this->namedLocksHeld[$lockName] );
+               unset( $this->sessionNamedLocks[$lockName] );
 
                return true;
        }
@@ -4689,12 +4676,24 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $this->conn;
        }
 
-       /**
-        * @since 1.19
-        * @return string
-        */
        public function __toString() {
-               return (string)$this->conn;
+               // spl_object_id is PHP >= 7.2
+               $id = function_exists( 'spl_object_id' )
+                       ? spl_object_id( $this )
+                       : spl_object_hash( $this );
+
+               $description = $this->getType() . ' object #' . $id;
+               if ( is_resource( $this->conn ) ) {
+                       $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
+               } elseif ( is_object( $this->conn ) ) {
+                       // spl_object_id is PHP >= 7.2
+                       $handleId = function_exists( 'spl_object_id' )
+                               ? spl_object_id( $this->conn )
+                               : spl_object_hash( $this->conn );
+                       $description .= " (handle id #$handleId)";
+               }
+
+               return $description;
        }
 
        /**
index a532ec2..5632027 100644 (file)
@@ -1167,10 +1167,13 @@ class DatabaseMssql extends Database {
 
                $database = $domain->getDatabase();
                if ( $database !== $this->getDBname() ) {
-                       $encDatabase = $this->addIdentifierQuotes( $database );
-                       $res = $this->doQuery( "USE $encDatabase" );
-                       if ( !$res ) {
-                               throw new DBExpectedError( $this, "Could not select database '$database'." );
+                       $sql = 'USE ' . $this->addIdentifierQuotes( $database );
+                       list( $res, $err, $errno ) =
+                               $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
+
+                       if ( $res === false ) {
+                               $this->reportQueryError( $err, $errno, $sql, __METHOD__ );
+                               return false; // unreachable
                        }
                }
                // Update that domain fields on success (no exception thrown)
index 6d28717..ef28f33 100644 (file)
@@ -182,7 +182,7 @@ abstract class DatabaseMysqlBase extends Database {
                }
                // Set any custom settings defined by site config
                // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
-               foreach ( $this->sessionVars as $var => $val ) {
+               foreach ( $this->connectionVariables as $var => $val ) {
                        // Escape strings but not numbers to avoid MySQL complaining
                        if ( !is_int( $val ) && !is_float( $val ) ) {
                                $val = $this->addQuotes( $val );
@@ -244,11 +244,12 @@ abstract class DatabaseMysqlBase extends Database {
 
                if ( $database !== $this->getDBname() ) {
                        $sql = 'USE ' . $this->addIdentifierQuotes( $database );
-                       $ret = $this->doQuery( $sql );
-                       if ( $ret === false ) {
-                               $error = $this->lastError();
-                               $errno = $this->lastErrno();
-                               $this->reportQueryError( $error, $errno, $sql, __METHOD__ );
+                       list( $res, $err, $errno ) =
+                               $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
+
+                       if ( $res === false ) {
+                               $this->reportQueryError( $err, $errno, $sql, __METHOD__ );
+                               return false; // unreachable
                        }
                }
 
@@ -368,7 +369,7 @@ abstract class DatabaseMysqlBase extends Database {
         * Fetch a result row as an associative and numeric array
         *
         * @param resource $res Raw result
-        * @return array
+        * @return array|false
         */
        abstract protected function mysqlFetchArray( $res );
 
@@ -743,7 +744,7 @@ abstract class DatabaseMysqlBase extends Database {
                return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
        }
 
-       public function getLag() {
+       protected function doGetLag() {
                if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
                        return $this->getLagFromPtHeartbeat();
                } else {
@@ -952,21 +953,22 @@ abstract class DatabaseMysqlBase extends Database {
                        $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
                        if ( strpos( $gtidArg, ':' ) !== false ) {
                                // MySQL GTIDs, e.g "source_id:transaction_id"
-                               $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
+                               $sql = "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)";
                        } else {
                                // MariaDB GTIDs, e.g."domain:server:sequence"
-                               $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+                               $sql = "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)";
                        }
                } else {
                        // Wait on the binlog coordinates
                        $encFile = $this->addQuotes( $pos->getLogFile() );
                        $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
-                       $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
+                       $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
                }
 
+               list( $res, $err ) = $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
                $row = $res ? $this->fetchRow( $res ) : false;
                if ( !$row ) {
-                       throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
+                       throw new DBExpectedError( $this, "Replication wait failed: {$err}" );
                }
 
                // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
index 1a5cdab..703c64d 100644 (file)
@@ -203,7 +203,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
 
        /**
         * @param mysqli_result $res
-        * @return bool
+        * @return array|false
         */
        protected function mysqlFetchArray( $res ) {
                $array = $res->fetch_array();
@@ -307,21 +307,6 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                return $conn->real_escape_string( (string)$s );
        }
 
-       /**
-        * Give an id for the connection
-        *
-        * mysql driver used resource id, but mysqli objects cannot be cast to string.
-        * @return string
-        */
-       public function __toString() {
-               if ( $this->conn instanceof mysqli ) {
-                       return (string)$this->conn->thread_id;
-               } else {
-                       // mConn might be false or something.
-                       return (string)$this->conn;
-               }
-       }
-
        /**
         * @return mysqli
         */
index 8e1b06d..aff3774 100644 (file)
@@ -216,8 +216,8 @@ class DatabaseSqlite extends Database {
                        # Enforce LIKE to be case sensitive, just like MySQL
                        $this->query( 'PRAGMA case_sensitive_like = 1' );
 
-                       $sync = $this->sessionVars['synchronous'] ?? null;
-                       if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL' ], true ) ) {
+                       $sync = $this->connectionVariables['synchronous'] ?? null;
+                       if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
                                $this->query( "PRAGMA synchronous = $sync" );
                        }
 
@@ -1119,15 +1119,6 @@ class DatabaseSqlite extends Database {
                return true;
        }
 
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return is_object( $this->conn )
-                       ? 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION )
-                       : '(not connected)';
-       }
-
        /**
         * @return PDO
         */
index 90e30fa..037ae99 100644 (file)
@@ -115,6 +115,8 @@ interface IDatabase {
        const QUERY_PSEUDO_PERMANENT = 2;
        /** @var int Enforce that a query does not make effective writes */
        const QUERY_REPLICA_ROLE = 4;
+       /** @var int Ignore the current presence of any DBO_TRX flag */
+       const QUERY_IGNORE_DBO_TRX = 8;
 
        /** @var bool Parameter to unionQueries() for UNION ALL */
        const UNION_ALL = true;
@@ -165,8 +167,10 @@ interface IDatabase {
        /**
         * Get the UNIX timestamp of the time that the transaction was established
         *
-        * This can be used to reason about the staleness of SELECT data
-        * in REPEATABLE-READ transaction isolation level.
+        * This can be used to reason about the staleness of SELECT data in REPEATABLE-READ
+        * transaction isolation level. Callers can assume that if a view-snapshot isolation
+        * is used, then the data read by SQL queries is *at least* up to date to that point
+        * (possibly more up-to-date since the first SELECT defines the snapshot).
         *
         * @return float|null Returns null if there is not active transaction
         * @since 1.25
@@ -1216,9 +1220,12 @@ interface IDatabase {
         *   $query .= $dbr->buildLike( $pattern );
         *
         * @since 1.16
+        * @param array[]|string|LikeMatch $param
         * @return string Fully built LIKE statement
+        * @phan-suppress-next-line PhanMismatchVariadicComment
+        * @phan-param array|string|LikeMatch ...$param T226223
         */
-       public function buildLike();
+       public function buildLike( $param );
 
        /**
         * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
@@ -2195,6 +2202,15 @@ interface IDatabase {
         * @since 1.31
         */
        public function setIndexAliases( array $aliases );
+
+       /**
+        * Get a debugging string that mentions the database type, the ID of this instance,
+        * and the ID of any underlying connection resource or driver object if one is present
+        *
+        * @return string "<db type> object #<X>" or "<db type> object #<X> (resource/handle id #<Y>)"
+        * @since 1.34
+        */
+       public function __toString();
 }
 
 /**
index 8608a7d..e20f6de 100644 (file)
@@ -85,7 +85,9 @@ abstract class LBFactory implements ILBFactory {
        /** @var callable[] */
        private $replicationWaitCallbacks = [];
 
-       /** @var mixed */
+       /** var int An identifier for this class instance */
+       private $id;
+       /** @var int|null Ticket used to delegate transaction ownership */
        private $ticket;
        /** @var string|bool String if a requested DBO_TRX transaction round is active */
        private $trxRoundId = false;
@@ -153,6 +155,7 @@ abstract class LBFactory implements ILBFactory {
                $this->defaultGroup = $conf['defaultGroup'] ?? null;
                $this->secret = $conf['secret'] ?? '';
 
+               $this->id = mt_rand();
                $this->ticket = mt_rand();
        }
 
@@ -251,7 +254,7 @@ abstract class LBFactory implements ILBFactory {
                }
                $this->trxRoundId = $fname;
                // Set DBO_TRX flags on all appropriate DBs
-               $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
+               $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname, $this->id ] );
                $this->trxRoundStage = self::ROUND_CURSORY;
        }
 
@@ -269,17 +272,17 @@ abstract class LBFactory implements ILBFactory {
                // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
                do {
                        $count = 0; // number of callbacks executed this iteration
-                       $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$count ) {
-                               $count += $lb->finalizeMasterChanges();
+                       $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$count, $fname ) {
+                               $count += $lb->finalizeMasterChanges( $fname, $this->id );
                        } );
                } while ( $count > 0 );
                $this->trxRoundId = false;
                // Perform pre-commit checks, aborting on failure
-               $this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
+               $this->forEachLBCallMethod( 'approveMasterChanges', [ $options, $fname, $this->id ] );
                // Log the DBs and methods involved in multi-DB transactions
                $this->logIfMultiDbTransaction();
                // Actually perform the commit on all master DB connections and revert DBO_TRX
-               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
+               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname, $this->id ] );
                // Run all post-commit callbacks in a separate step
                $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
                $e = $this->executePostTransactionCallbacks();
@@ -294,7 +297,7 @@ abstract class LBFactory implements ILBFactory {
                $this->trxRoundStage = self::ROUND_ROLLING_BACK;
                $this->trxRoundId = false;
                // Actually perform the rollback on all master DB connections and revert DBO_TRX
-               $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
+               $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname, $this->id ] );
                // Run all post-commit callbacks in a separate step
                $this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
                $this->executePostTransactionCallbacks();
@@ -305,17 +308,18 @@ abstract class LBFactory implements ILBFactory {
         * @return Exception|null
         */
        private function executePostTransactionCallbacks() {
+               $fname = __METHOD__;
                // Run all post-commit callbacks until new ones stop getting added
                $e = null; // first callback exception
                do {
-                       $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
-                               $ex = $lb->runMasterTransactionIdleCallbacks();
+                       $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e, $fname ) {
+                               $ex = $lb->runMasterTransactionIdleCallbacks( $fname, $this->id );
                                $e = $e ?: $ex;
                        } );
                } while ( $this->hasMasterChanges() );
                // Run all listener callbacks once
-               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
-                       $ex = $lb->runMasterTransactionListenerCallbacks();
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e, $fname ) {
+                       $ex = $lb->runMasterTransactionListenerCallbacks( $fname, $this->id );
                        $e = $e ?: $ex;
                } );
 
@@ -412,12 +416,11 @@ abstract class LBFactory implements ILBFactory {
                // time needed to wait on the next clusters.
                $masterPositions = array_fill( 0, count( $lbs ), false );
                foreach ( $lbs as $i => $lb ) {
-                       if ( $lb->getServerCount() <= 1 ) {
-                               // T29975 - Don't try to wait for replica DBs if there are none
-                               // Prevents permission error when getting master position
-                               continue;
-                       } elseif ( $opts['ifWritesSince']
-                               && $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
+                       if ( !$lb->hasStreamingReplicaServers() ) {
+                               continue; // T29975: no replication; avoid getMasterPos() permissions errors
+                       } elseif (
+                               $opts['ifWritesSince'] &&
+                               $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
                        ) {
                                continue; // no writes since the last wait
                        }
@@ -605,7 +608,8 @@ abstract class LBFactory implements ILBFactory {
                                // being called later (but before the first connection attempt) (T192611)
                                $this->getChronologyProtector()->applySessionReplicationPosition( $lb );
                        },
-                       'roundStage' => $initStage
+                       'roundStage' => $initStage,
+                       'ownerId' => $this->id
                ];
        }
 
@@ -614,7 +618,7 @@ abstract class LBFactory implements ILBFactory {
         */
        protected function initLoadBalancer( ILoadBalancer $lb ) {
                if ( $this->trxRoundId !== false ) {
-                       $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
+                       $lb->beginMasterChanges( $this->trxRoundId, $this->id ); // set DBO_TRX
                }
 
                $lb->setTableAliases( $this->tableAliases );
@@ -670,7 +674,7 @@ abstract class LBFactory implements ILBFactory {
        public function appendShutdownCPIndexAsQuery( $url, $index ) {
                $usedCluster = 0;
                $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
-                       $usedCluster |= ( $lb->getServerCount() > 1 );
+                       $usedCluster |= $lb->hasStreamingReplicaServers();
                } );
 
                if ( !$usedCluster ) {
index faa9654..4d148b4 100644 (file)
@@ -62,6 +62,8 @@ use InvalidArgumentException;
  *      Note that lag is still possible depending on how wsrep-sync-wait is set server-side.
  *   - Read-only archive clones: set 'is static' in the server configuration maps. This will
  *      treat all such DBs as having 0 lag.
+ *   - Externally updated dataset clones: set 'is static' in the server configuration maps.
+ *      This will treat all such DBs as having 0 lag.
  *   - SQL load balancing proxy: any proxy should handle lag checks on its own, so the 'max lag'
  *      parameter should probably be set to INF in the server configuration maps. This will make
  *      the load balancer ignore whatever it detects as the lag of the logical replica is (which
@@ -86,6 +88,8 @@ interface ILoadBalancer {
 
        /** @var int DB handle should have DBO_TRX disabled and the caller will leave it as such */
        const CONN_TRX_AUTOCOMMIT = 1;
+       /** @var int Return null on connection failure instead of throwing an exception */
+       const CONN_SILENCE_ERRORS = 2;
 
        /** @var string Manager of ILoadBalancer instances is running post-commit callbacks */
        const STAGE_POSTCOMMIT_CALLBACKS = 'stage-postcommit-callbacks';
@@ -116,6 +120,7 @@ interface ILoadBalancer {
         *  - errorLogger : Callback that takes an Exception and logs it. [optional]
         *  - deprecationLogger: Callback to log a deprecation warning. [optional]
         *  - roundStage: STAGE_POSTCOMMIT_* class constant; for internal use [optional]
+        *  - ownerId: integer ID of an LBFactory instance that manages this instance [optional]
         * @throws InvalidArgumentException
         */
        public function __construct( array $params );
@@ -145,12 +150,15 @@ interface ILoadBalancer {
        public function redefineLocalDomain( $domain );
 
        /**
-        * Get the index of the reader connection, which may be a replica DB
+        * Get the server index of the reader connection for a given group
         *
-        * This takes into account load ratios and lag times. It should
-        * always return a consistent index during a given invocation.
+        * This takes into account load ratios and lag times. It should return a consistent
+        * index during the life time of the load balancer. This initially checks replica DBs
+        * for connectivity to avoid returning an unusable server. This means that connections
+        * might be attempted by calling this method (usally one at the most but possibly more).
+        * Subsequent calls with the same $group will not need to make new connection attempts
+        * since the acquired connection for each group is preserved.
         *
-        * Side effect: opens connections to databases
         * @param string|bool $group Query group, or false for the generic group
         * @param string|bool $domain Domain ID, or false for the current domain
         * @throws DBError
@@ -223,8 +231,8 @@ interface ILoadBalancer {
         *
         * @note This method throws DBAccessError if ILoadBalancer::disable() was called
         *
-        * @throws DBError
-        * @return Database
+        * @throws DBError If any error occurs that prevents the yielding of a (live) IDatabase
+        * @return IDatabase|bool This returns false on failure if CONN_SILENCE_ERRORS is set
         */
        public function getConnection( $i, $groups = [], $domain = false, $flags = 0 );
 
@@ -300,31 +308,8 @@ interface ILoadBalancer {
        public function getMaintenanceConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
 
        /**
-        * Open a connection to the server given by the specified index
-        *
-        * The index must be an actual index into the array. If a connection to the server is
-        * already open and not considered an "in use" foreign connection, this simply returns it.
-        *
-        * Avoid using CONN_TRX_AUTOCOMMIT for databases with ATTR_DB_LEVEL_LOCKING (e.g. sqlite)
-        * in order to avoid deadlocks. ILoadBalancer::getServerAttributes() can be used to check
-        * such flags beforehand.
-        *
-        * If the caller uses $domain or sets CONN_TRX_AUTOCOMMIT in $flags, then it must
-        * also call ILoadBalancer::reuseConnection() on the handle when finished using it.
-        * In all other cases, this is not necessary, though not harmful either.
-        * Avoid the use of begin() or startAtomic() on any such connections.
-        *
-        * @note This method throws DBAccessError if ILoadBalancer::disable() was called
+        * Get the server index of the master server
         *
-        * @param int $i Server index (does not support DB_MASTER/DB_REPLICA)
-        * @param string|bool $domain Domain ID, or false for the current domain
-        * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
-        * @return Database|bool Returns false on errors
-        * @throws DBAccessError
-        */
-       public function openConnection( $i, $domain = false, $flags = 0 );
-
-       /**
         * @return int
         */
        public function getWriterIndex();
@@ -346,12 +331,44 @@ interface ILoadBalancer {
        public function isNonZeroLoad( $i );
 
        /**
-        * Get the number of defined servers (not the number of open connections)
+        * Get the number of servers defined in configuration
         *
         * @return int
         */
        public function getServerCount();
 
+       /**
+        * Whether there are any replica servers configured
+        *
+        * This counts both servers using streaming replication from the master server and
+        * servers that just have a clone of the static dataset found on the master server
+        *
+        * @return int
+        * @since 1.34
+        */
+       public function hasReplicaServers();
+
+       /**
+        * Whether any replica servers use streaming replication from the master server
+        *
+        * Generally this is one less than getServerCount(), though it might otherwise
+        * return a lower number if some of the servers are configured with "is static".
+        * That flag is used when both the server has no active replication setup and the
+        * dataset is either read-only or occasionally updated out-of-band. For example,
+        * a script might import a new geographic information dataset each week by writing
+        * it to each server and later directing the application to use the new version.
+        *
+        * It is possible for some replicas to be configured with "is static" but not
+        * others, though it generally should either be set for all or none of the replicas.
+        *
+        * If this returns zero, this means that there is generally no reason to execute
+        * replication wait logic for session consistency and lag reduction.
+        *
+        * @return int
+        * @since 1.34
+        */
+       public function hasStreamingReplicaServers();
+
        /**
         * Get the host name or IP address of the server with the specified index
         *
@@ -414,18 +431,21 @@ interface ILoadBalancer {
        /**
         * Commit transactions on all open connections
         * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @throws DBExpectedError
         */
-       public function commitAll( $fname = __METHOD__ );
+       public function commitAll( $fname = __METHOD__, $owner = null );
 
        /**
         * Run pre-commit callbacks and defer execution of post-commit callbacks
         *
         * Use this only for mutli-database commits
         *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @return int Number of pre-commit callbacks run (since 1.32)
         */
-       public function finalizeMasterChanges();
+       public function finalizeMasterChanges( $fname = __METHOD__, $owner = null );
 
        /**
         * Perform all pre-commit checks for things like replication safety
@@ -434,9 +454,11 @@ interface ILoadBalancer {
         *
         * @param array $options Includes:
         *   - maxWriteDuration : max write query duration time in seconds
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @throws DBTransactionError
         */
-       public function approveMasterChanges( array $options );
+       public function approveMasterChanges( array $options, $fname, $owner = null );
 
        /**
         * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
@@ -447,38 +469,45 @@ interface ILoadBalancer {
         *   - commitAll()
         * This allows for custom transaction rounds from any outer transaction scope.
         *
-        * @param string $fname
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @throws DBExpectedError
         */
-       public function beginMasterChanges( $fname = __METHOD__ );
+       public function beginMasterChanges( $fname = __METHOD__, $owner = null );
 
        /**
         * Issue COMMIT on all open master connections to flush changes and view snapshots
         * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @throws DBExpectedError
         */
-       public function commitMasterChanges( $fname = __METHOD__ );
+       public function commitMasterChanges( $fname = __METHOD__, $owner = null );
 
        /**
         * Consume and run all pending post-COMMIT/ROLLBACK callbacks and commit dangling transactions
         *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @return Exception|null The first exception or null if there were none
         */
-       public function runMasterTransactionIdleCallbacks();
+       public function runMasterTransactionIdleCallbacks( $fname = __METHOD__, $owner = null );
 
        /**
         * Run all recurring post-COMMIT/ROLLBACK listener callbacks
         *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @return Exception|null The first exception or null if there were none
         */
-       public function runMasterTransactionListenerCallbacks();
+       public function runMasterTransactionListenerCallbacks( $fname = __METHOD__, $owner = null );
 
        /**
         * Issue ROLLBACK only on master, only if queries were done on connection
         * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @throws DBExpectedError
         */
-       public function rollbackMasterChanges( $fname = __METHOD__ );
+       public function rollbackMasterChanges( $fname = __METHOD__, $owner = null );
 
        /**
         * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshots
@@ -588,7 +617,7 @@ interface ILoadBalancer {
        public function forEachOpenReplicaConnection( $callback, array $params = [] );
 
        /**
-        * Get the hostname and lag time of the most-lagged replica DB
+        * Get the hostname and lag time of the most-lagged replica server
         *
         * This is useful for maintenance scripts that need to throttle their updates.
         * May attempt to open connections to replica DBs on the default DB. If there is
@@ -611,22 +640,6 @@ interface ILoadBalancer {
         */
        public function getLagTimes( $domain = false );
 
-       /**
-        * Get the lag in seconds for a given connection, or zero if this load
-        * balancer does not have replication enabled.
-        *
-        * This should be used in preference to Database::getLag() in cases where
-        * replication may not be in use, since there is no way to determine if
-        * replication is in use at the connection level without running
-        * potentially restricted queries such as SHOW SLAVE STATUS. Using this
-        * function instead of Database::getLag() avoids a fatal error in this
-        * case on many installations.
-        *
-        * @param IDatabase $conn
-        * @return int|bool Returns false on error
-        */
-       public function safeGetLag( IDatabase $conn );
-
        /**
         * Wait for a replica DB to reach a specified master position
         *
@@ -636,8 +649,9 @@ interface ILoadBalancer {
         * @param DBMasterPos|bool $pos Master position; default: current position
         * @param int $timeout Timeout in seconds [optional]
         * @return bool Success
+        * @since 1.34
         */
-       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 );
+       public function waitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 );
 
        /**
         * Set a callback via IDatabase::setTransactionListener() on
index 51fda52..6617ab1 100644 (file)
@@ -105,7 +105,9 @@ class LoadBalancer implements ILoadBalancer {
        private $errorConnection;
        /** @var int The generic (not query grouped) replica DB index */
        private $genericReadIndex = -1;
-       /** @var bool|DBMasterPos False if not set */
+       /** @var int[] The group replica DB indexes keyed by group */
+       private $readIndexByGroup = [];
+       /** @var bool|DBMasterPos Replication sync position or false if not set */
        private $waitForPos;
        /** @var bool Whether the generic reader fell back to a lagged replica DB */
        private $laggedReplicaMode = false;
@@ -122,6 +124,8 @@ class LoadBalancer implements ILoadBalancer {
        /** @var bool Whether any connection has been attempted yet */
        private $connectionAttempted = false;
 
+       /** @var int|null An integer ID of the managing LBFactory instance or null */
+       private $ownerId;
        /** @var string|bool String if a requested DBO_TRX transaction round is active */
        private $trxRoundId = false;
        /** @var string Stage of the current transaction round in the transaction round life-cycle */
@@ -162,15 +166,29 @@ class LoadBalancer implements ILoadBalancer {
        const ROUND_ERROR = 'error';
 
        public function __construct( array $params ) {
-               if ( !isset( $params['servers'] ) ) {
-                       throw new InvalidArgumentException( __CLASS__ . ': missing "servers" parameter' );
+               if ( !isset( $params['servers'] ) || !count( $params['servers'] ) ) {
+                       throw new InvalidArgumentException( 'Missing or empty "servers" parameter' );
                }
-               $this->servers = $params['servers'];
-               foreach ( $this->servers as $i => $server ) {
+
+               $listKey = -1;
+               $this->servers = [];
+               $this->genericLoads = [];
+               foreach ( $params['servers'] as $i => $server ) {
+                       if ( ++$listKey !== $i ) {
+                               throw new UnexpectedValueException( 'List expected for "servers" parameter' );
+                       }
                        if ( $i == 0 ) {
-                               $this->servers[$i]['master'] = true;
+                               $server['master'] = true;
                        } else {
-                               $this->servers[$i]['replica'] = true;
+                               $server['replica'] = true;
+                       }
+                       $this->servers[$i] = $server;
+
+                       $this->genericLoads[$i] = $server['load'];
+                       if ( isset( $server['groupLoads'] ) ) {
+                               foreach ( $server['groupLoads'] as $group => $ratio ) {
+                                       $this->groupLoads[$group][$i] = $ratio;
+                               }
                        }
                }
 
@@ -181,17 +199,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT;
 
-               $this->conns = [
-                       // Connection were transaction rounds may be applied
-                       self::KEY_LOCAL => [],
-                       self::KEY_FOREIGN_INUSE => [],
-                       self::KEY_FOREIGN_FREE => [],
-                       // Auto-committing counterpart connections that ignore transaction rounds
-                       self::KEY_LOCAL_NOROUND => [],
-                       self::KEY_FOREIGN_INUSE_NOROUND => [],
-                       self::KEY_FOREIGN_FREE_NOROUND => []
-               ];
-               $this->genericLoads = [];
+               $this->conns = self::newConnsArray();
                $this->waitForPos = false;
                $this->allowLagged = false;
 
@@ -204,18 +212,6 @@ class LoadBalancer implements ILoadBalancer {
                $this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
                $this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
 
-               foreach ( $params['servers'] as $i => $server ) {
-                       $this->genericLoads[$i] = $server['load'];
-                       if ( isset( $server['groupLoads'] ) ) {
-                               foreach ( $server['groupLoads'] as $group => $ratio ) {
-                                       if ( !isset( $this->groupLoads[$group] ) ) {
-                                               $this->groupLoads[$group] = [];
-                                       }
-                                       $this->groupLoads[$group][$i] = $ratio;
-                               }
-                       }
-               }
-
                $this->srvCache = $params['srvCache'] ?? new EmptyBagOStuff();
                $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
                $this->profiler = $params['profiler'] ?? null;
@@ -249,6 +245,20 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                $this->defaultGroup = $params['defaultGroup'] ?? null;
+               $this->ownerId = $params['ownerId'] ?? null;
+       }
+
+       private static function newConnsArray() {
+               return [
+                       // Connection were transaction rounds may be applied
+                       self::KEY_LOCAL => [],
+                       self::KEY_FOREIGN_INUSE => [],
+                       self::KEY_FOREIGN_FREE => [],
+                       // Auto-committing counterpart connections that ignore transaction rounds
+                       self::KEY_LOCAL_NOROUND => [],
+                       self::KEY_FOREIGN_INUSE_NOROUND => [],
+                       self::KEY_FOREIGN_FREE_NOROUND => []
+               ];
        }
 
        public function getLocalDomainID() {
@@ -265,6 +275,49 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
+        * @param int $flags
+        * @return bool
+        */
+       private function sanitizeConnectionFlags( $flags ) {
+               if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) === self::CONN_TRX_AUTOCOMMIT ) {
+                       // Assuming all servers are of the same type (or similar), which is overwhelmingly
+                       // the case, use the master server information to get the attributes. The information
+                       // for $i cannot be used since it might be DB_REPLICA, which might require connection
+                       // attempts in order to be resolved into a real server index.
+                       $attributes = $this->getServerAttributes( $this->getWriterIndex() );
+                       if ( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ) {
+                               // Callers sometimes want to (a) escape REPEATABLE-READ stateness without locking
+                               // rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
+                               // to reduce lock contention. None of these apply for sqlite and using separate
+                               // connections just causes self-deadlocks.
+                               $flags &= ~self::CONN_TRX_AUTOCOMMIT;
+                               $this->connLogger->info( __METHOD__ .
+                                       ': ignoring CONN_TRX_AUTOCOMMIT to avoid deadlocks.' );
+                       }
+               }
+
+               return $flags;
+       }
+
+       /**
+        * @param IDatabase $conn
+        * @param int $flags
+        * @throws DBUnexpectedError
+        */
+       private function enforceConnectionFlags( IDatabase $conn, $flags ) {
+               if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT ) {
+                       if ( $conn->trxLevel() ) { // sanity
+                               throw new DBUnexpectedError(
+                                       $conn,
+                                       'Handle requested with CONN_TRX_AUTOCOMMIT yet it has a transaction'
+                               );
+                       }
+
+                       $conn->clearFlag( $conn::DBO_TRX ); // auto-commit mode
+               }
+       }
+
+               /**
         * Get a LoadMonitor instance
         *
         * @return ILoadMonitor
@@ -301,7 +354,7 @@ class LoadBalancer implements ILoadBalancer {
 
                # Unset excessively lagged servers
                foreach ( $lags as $i => $lag ) {
-                       if ( $i != 0 ) {
+                       if ( $i !== $this->getWriterIndex() ) {
                                # How much lag this server nominally is allowed to have
                                $maxServerLag = $this->servers[$i]['max lag'] ?? $this->maxLag; // default
                                # Constrain that futher by $maxLag argument
@@ -349,7 +402,7 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $i
         * @param array $groups
         * @param string|bool $domain
-        * @return int
+        * @return int The index of a specific server (replica DBs are checked for connectivity)
         */
        private function getConnectionIndex( $i, $groups, $domain ) {
                // Check one "group" per default: the generic pool
@@ -359,9 +412,9 @@ class LoadBalancer implements ILoadBalancer {
                        ? $defaultGroups
                        : (array)$groups;
 
-               if ( $i == self::DB_MASTER ) {
+               if ( $i === self::DB_MASTER ) {
                        $i = $this->getWriterIndex();
-               } elseif ( $i == self::DB_REPLICA ) {
+               } elseif ( $i === self::DB_REPLICA ) {
                        # Try to find an available server in any the query groups (in order)
                        foreach ( $groups as $group ) {
                                $groupIndex = $this->getReaderIndex( $group, $domain );
@@ -373,7 +426,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                # Operation-based index
-               if ( $i == self::DB_REPLICA ) {
+               if ( $i === self::DB_REPLICA ) {
                        $this->lastError = 'Unknown error'; // reset error string
                        # Try the general server pool if $groups are unavailable.
                        $i = ( $groups === [ false ] )
@@ -384,7 +437,7 @@ class LoadBalancer implements ILoadBalancer {
                                $this->lastError = 'No working replica DB server: ' . $this->lastError;
                                // Throw an exception
                                $this->reportConnectionError();
-                               return null; // not reached
+                               return null; // unreachable due to exception
                        }
                }
 
@@ -392,12 +445,15 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getReaderIndex( $group = false, $domain = false ) {
-               if ( count( $this->servers ) == 1 ) {
+               if ( $this->getServerCount() == 1 ) {
                        // Skip the load balancing if there's only one server
                        return $this->getWriterIndex();
-               } elseif ( $group === false && $this->genericReadIndex >= 0 ) {
-                       // A generic reader index was already selected and "waitForPos" was handled
-                       return $this->genericReadIndex;
+               }
+
+               $index = $this->getExistingReaderIndex( $group );
+               if ( $index >= 0 ) {
+                       // A reader index was already selected and "waitForPos" was handled
+                       return $index;
                }
 
                if ( $group !== false ) {
@@ -419,6 +475,7 @@ class LoadBalancer implements ILoadBalancer {
                $this->getLoadMonitor()->scaleLoads( $loads, $domain );
 
                // Pick a server to use, accounting for weights, load, lag, and "waitForPos"
+               $this->lazyLoadReplicationPositions(); // optimizes server candidate selection
                list( $i, $laggedReplicaMode ) = $this->pickReaderIndex( $loads, $domain );
                if ( $i === false ) {
                        // DB connection unsuccessful
@@ -427,16 +484,16 @@ class LoadBalancer implements ILoadBalancer {
 
                // If data seen by queries is expected to reflect the transactions committed as of
                // or after a given replication position then wait for the DB to apply those changes
-               if ( $this->waitForPos && $i != $this->getWriterIndex() && !$this->doWait( $i ) ) {
+               if ( $this->waitForPos && $i !== $this->getWriterIndex() && !$this->doWait( $i ) ) {
                        // Data will be outdated compared to what was expected
                        $laggedReplicaMode = true;
                }
 
-               if ( $this->genericReadIndex < 0 && $this->genericLoads[$i] > 0 && $group === false ) {
-                       // Cache the generic (ungrouped) reader index for future DB_REPLICA handles
-                       $this->genericReadIndex = $i;
-                       // Record if the generic reader index is in "lagged replica DB" mode
-                       $this->laggedReplicaMode = ( $laggedReplicaMode || $this->laggedReplicaMode );
+               // Cache the reader index for future DB_REPLICA handles
+               $this->setExistingReaderIndex( $group, $i );
+               // Record whether the generic reader index is in "lagged replica DB" mode
+               if ( $group === false && $laggedReplicaMode ) {
+                       $this->laggedReplicaMode = true;
                }
 
                $serverName = $this->getServerName( $i );
@@ -445,6 +502,40 @@ class LoadBalancer implements ILoadBalancer {
                return $i;
        }
 
+       /**
+        * Get the server index chosen by the load balancer for use with the given query group
+        *
+        * @param string|bool $group Query group; use false for the generic group
+        * @return int Server index or -1 if none was chosen
+        */
+       protected function getExistingReaderIndex( $group ) {
+               if ( $group === false ) {
+                       $index = $this->genericReadIndex;
+               } else {
+                       $index = $this->readIndexByGroup[$group] ?? -1;
+               }
+
+               return $index;
+       }
+
+       /**
+        * Set the server index chosen by the load balancer for use with the given query group
+        *
+        * @param string|bool $group Query group; use false for the generic group
+        * @param int $index The index of a specific server
+        */
+       private function setExistingReaderIndex( $group, $index ) {
+               if ( $index < 0 ) {
+                       throw new UnexpectedValueException( "Cannot set a negative read server index" );
+               }
+
+               if ( $group === false ) {
+                       $this->genericReadIndex = $index;
+               } else {
+                       $this->readIndexByGroup[$group] = $index;
+               }
+       }
+
        /**
         * @param array $loads List of server weights
         * @param string|bool $domain
@@ -468,6 +559,7 @@ class LoadBalancer implements ILoadBalancer {
                        } else {
                                $i = false;
                                if ( $this->waitForPos && $this->waitForPos->asOfTime() ) {
+                                       $this->replLogger->debug( __METHOD__ . ": replication positions detected" );
                                        // "chronologyCallback" sets "waitForPos" for session consistency.
                                        // This triggers doWait() after connect, so it's especially good to
                                        // avoid lagged servers so as to avoid excessive delay in that method.
@@ -479,7 +571,7 @@ class LoadBalancer implements ILoadBalancer {
                                        // Any server with less lag than it's 'max lag' param is preferable
                                        $i = $this->getRandomNonLagged( $currentLoads, $domain );
                                }
-                               if ( $i === false && count( $currentLoads ) != 0 ) {
+                               if ( $i === false && count( $currentLoads ) ) {
                                        // All replica DBs lagged. Switch to read-only mode
                                        $this->replLogger->error(
                                                __METHOD__ . ": all replica DBs lagged. Switch to read-only mode" );
@@ -500,7 +592,7 @@ class LoadBalancer implements ILoadBalancer {
                        $serverName = $this->getServerName( $i );
                        $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." );
 
-                       $conn = $this->openConnection( $i, $domain );
+                       $conn = $this->getConnection( $i, [], $domain, self::CONN_SILENCE_ERRORS );
                        if ( !$conn ) {
                                $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$domain" );
                                unset( $currentLoads[$i] ); // avoid this server next iteration
@@ -574,7 +666,7 @@ class LoadBalancer implements ILoadBalancer {
                $oldPos = $this->waitForPos;
                try {
                        $this->waitForPos = $pos;
-                       $serverCount = count( $this->servers );
+                       $serverCount = $this->getServerCount();
 
                        $ok = true;
                        for ( $i = 1; $i < $serverCount; $i++ ) {
@@ -635,10 +727,10 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
-        * Wait for a given replica DB to catch up to the master pos stored in $this
+        * Wait for a given replica DB to catch up to the master pos stored in "waitForPos"
         * @param int $index Server index
         * @param bool $open Check the server even if a new connection has to be made
-        * @param int|null $timeout Max seconds to wait; default is "waitTimeout" given to __construct()
+        * @param int|null $timeout Max seconds to wait; default is "waitTimeout"
         * @return bool
         */
        protected function doWait( $index, $open = false, $timeout = null ) {
@@ -672,20 +764,20 @@ class LoadBalancer implements ILoadBalancer {
                                );
 
                                return false;
-                       } else {
-                               $conn = $this->openConnection( $index, self::DOMAIN_ANY );
-                               if ( !$conn ) {
-                                       $this->replLogger->warning(
-                                               __METHOD__ . ': failed to connect to {dbserver}',
-                                               [ 'dbserver' => $server ]
-                                       );
+                       }
+                       // Open a temporary new connection in order to wait for replication
+                       $conn = $this->getConnection( $index, [], self::DOMAIN_ANY, self::CONN_SILENCE_ERRORS );
+                       if ( !$conn ) {
+                               $this->replLogger->warning(
+                                       __METHOD__ . ': failed to connect to {dbserver}',
+                                       [ 'dbserver' => $server ]
+                               );
 
-                                       return false;
-                               }
-                               // Avoid connection spam in waitForAll() when connections
-                               // are made just for the sake of doing this lag check.
-                               $close = true;
+                               return false;
                        }
+                       // Avoid connection spam in waitForAll() when connections
+                       // are made just for the sake of doing this lag check.
+                       $close = true;
                }
 
                $this->replLogger->info(
@@ -731,39 +823,32 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getConnection( $i, $groups = [], $domain = false, $flags = 0 ) {
-               if ( $i === null || $i === false ) {
+               if ( !is_int( $i ) ) {
                        throw new InvalidArgumentException( "Cannot connect without a server index" );
+               } elseif ( $groups && $i > 0 ) {
+                       throw new InvalidArgumentException( "Got query groups with server index #$i" );
                }
 
                $domain = $this->resolveDomainID( $domain );
-               $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() );
-
-               if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) === self::CONN_TRX_AUTOCOMMIT ) {
-                       // Assuming all servers are of the same type (or similar), which is overwhelmingly
-                       // the case, use the master server information to get the attributes. The information
-                       // for $i cannot be used since it might be DB_REPLICA, which might require connection
-                       // attempts in order to be resolved into a real server index.
-                       $attributes = $this->getServerAttributes( $this->getWriterIndex() );
-                       if ( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ) {
-                               // Callers sometimes want to (a) escape REPEATABLE-READ stateness without locking
-                               // rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
-                               // to reduce lock contention. None of these apply for sqlite and using separate
-                               // connections just causes self-deadlocks.
-                               $flags &= ~self::CONN_TRX_AUTOCOMMIT;
-                               $this->connLogger->info( __METHOD__ .
-                                       ': ignoring CONN_TRX_AUTOCOMMIT to avoid deadlocks.' );
-                       }
-               }
+               $flags = $this->sanitizeConnectionFlags( $flags );
+               $masterOnly = ( $i === self::DB_MASTER || $i === $this->getWriterIndex() );
 
                // Number of connections made before getting the server index and handle
                $priorConnectionsMade = $this->connsOpened;
-               // Decide which server to use (might trigger new connections)
+
+               // Choose a server if $i is DB_MASTER/DB_REPLICA (might trigger new connections)
                $serverIndex = $this->getConnectionIndex( $i, $groups, $domain );
                // Get an open connection to that server (might trigger a new connection)
-               $conn = $this->openConnection( $serverIndex, $domain, $flags );
-               if ( !$conn ) {
-                       $this->reportConnectionError();
-                       return null; // unreachable due to exception
+               $conn = $this->localDomain->equals( $domain )
+                       ? $this->getLocalConnection( $serverIndex, $flags )
+                       : $this->getForeignConnection( $serverIndex, $domain, $flags );
+               // Throw an error or bail out if the connection attempt failed
+               if ( !( $conn instanceof IDatabase ) ) {
+                       if ( ( $flags & self::CONN_SILENCE_ERRORS ) != self::CONN_SILENCE_ERRORS ) {
+                               $this->reportConnectionError();
+                       }
+
+                       return false;
                }
 
                // Profile any new connections caused by this method
@@ -773,7 +858,16 @@ class LoadBalancer implements ILoadBalancer {
                        $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly );
                }
 
-               if ( $serverIndex == $this->getWriterIndex() ) {
+               if ( !$conn->isOpen() ) {
+                       // Connection was made but later unrecoverably lost for some reason.
+                       // Do not return a handle that will just throw exceptions on use,
+                       // but let the calling code (e.g. getReaderIndex) try another server.
+                       $this->errorConnection = $conn;
+                       return false;
+               }
+
+               $this->enforceConnectionFlags( $conn, $flags );
+               if ( $serverIndex === $this->getWriterIndex() ) {
                        // If the load balancer is read-only, perhaps due to replication lag, then master
                        // DB handles will reflect that. Note that Database::assertIsWritableMaster() takes
                        // care of replica DB handles whereas getReadOnlyReason() would cause infinite loops.
@@ -875,43 +969,15 @@ class LoadBalancer implements ILoadBalancer {
                        : self::DB_REPLICA;
        }
 
+       /**
+        * @param int $i
+        * @param bool $domain
+        * @param int $flags
+        * @return Database|bool Live database handle or false on failure
+        * @deprecated Since 1.34 Use getConnection() instead
+        */
        public function openConnection( $i, $domain = false, $flags = 0 ) {
-               $domain = $this->resolveDomainID( $domain );
-
-               if ( !$this->connectionAttempted && $this->chronologyCallback ) {
-                       // Load any "waitFor" positions before connecting so that doWait() is triggered
-                       $this->connLogger->debug( __METHOD__ . ': calling initLB() before first connection.' );
-                       $this->connectionAttempted = true;
-                       ( $this->chronologyCallback )( $this );
-               }
-
-               $conn = $this->localDomain->equals( $domain )
-                       ? $this->openLocalConnection( $i, $flags )
-                       : $this->openForeignConnection( $i, $domain, $flags );
-
-               if ( $conn instanceof IDatabase && !$conn->isOpen() ) {
-                       // Connection was made but later unrecoverably lost for some reason.
-                       // Do not return a handle that will just throw exceptions on use,
-                       // but let the calling code (e.g. getReaderIndex) try another server.
-                       $this->errorConnection = $conn;
-                       $conn = false;
-               }
-
-               if (
-                       $conn instanceof IDatabase &&
-                       ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT )
-               ) {
-                       if ( $conn->trxLevel() ) { // sanity
-                               throw new DBUnexpectedError(
-                                       $conn,
-                                       'Handle requested with CONN_TRX_AUTOCOMMIT yet it has a transaction'
-                               );
-                       }
-
-                       $conn->clearFlag( $conn::DBO_TRX ); // auto-commit mode
-               }
-
-               return $conn;
+               return $this->getConnection( $i, [], $domain, $flags | self::CONN_SILENCE_ERRORS );
        }
 
        /**
@@ -926,7 +992,7 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $flags Class CONN_* constant bitfield
         * @return Database
         */
-       private function openLocalConnection( $i, $flags = 0 ) {
+       private function getLocalConnection( $i, $flags = 0 ) {
                // Connection handles required to be in auto-commit mode use a separate connection
                // pool since the main pool is effected by implicit and explicit transaction rounds
                $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
@@ -935,11 +1001,8 @@ class LoadBalancer implements ILoadBalancer {
                if ( isset( $this->conns[$connKey][$i][0] ) ) {
                        $conn = $this->conns[$connKey][$i][0];
                } else {
-                       if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
-                               throw new InvalidArgumentException( "No server with index '$i'" );
-                       }
                        // Open a new connection
-                       $server = $this->servers[$i];
+                       $server = $this->getServerInfoStrict( $i );
                        $server['serverIndex'] = $i;
                        $server['autoCommitOnly'] = $autoCommit;
                        $conn = $this->reallyOpenConnection( $server, $this->localDomain );
@@ -991,7 +1054,7 @@ class LoadBalancer implements ILoadBalancer {
         * @return Database|bool Returns false on connection error
         * @throws DBError When database selection fails
         */
-       private function openForeignConnection( $i, $domain, $flags = 0 ) {
+       private function getForeignConnection( $i, $domain, $flags = 0 ) {
                $domainInstance = DatabaseDomain::newFromId( $domain );
                // Connection handles required to be in auto-commit mode use a separate connection
                // pool since the main pool is effected by implicit and explicit transaction rounds
@@ -1049,11 +1112,8 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( !$conn ) {
-                       if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
-                               throw new InvalidArgumentException( "No server with index '$i'" );
-                       }
                        // Open a new connection
-                       $server = $this->servers[$i];
+                       $server = $this->getServerInfoStrict( $i );
                        $server['serverIndex'] = $i;
                        $server['foreignPoolRefCount'] = 0;
                        $server['foreign'] = true;
@@ -1193,9 +1253,22 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
+               $this->lazyLoadReplicationPositions(); // session consistency
+
                return $db;
        }
 
+       /**
+        * Make sure that any "waitForPos" positions are loaded and available to doWait()
+        */
+       private function lazyLoadReplicationPositions() {
+               if ( !$this->connectionAttempted && $this->chronologyCallback ) {
+                       $this->connectionAttempted = true;
+                       ( $this->chronologyCallback )( $this ); // generally calls waitFor()
+                       $this->connLogger->debug( __METHOD__ . ': executed chronology callback.' );
+               }
+       }
+
        /**
         * @throws DBConnectionError
         */
@@ -1243,8 +1316,22 @@ class LoadBalancer implements ILoadBalancer {
                return count( $this->servers );
        }
 
+       public function hasReplicaServers() {
+               return ( $this->getServerCount() > 1 );
+       }
+
+       public function hasStreamingReplicaServers() {
+               foreach ( $this->servers as $i => $server ) {
+                       if ( $i !== $this->getWriterIndex() && empty( $server['is static'] ) ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        public function getServerName( $i ) {
-               $name = $this->servers[$i]['hostName'] ?? $this->servers[$i]['host'] ?? '';
+               $name = $this->servers[$i]['hostName'] ?? ( $this->servers[$i]['host'] ?? '' );
 
                return ( $name != '' ) ? $name : 'localhost';
        }
@@ -1262,7 +1349,7 @@ class LoadBalancer implements ILoadBalancer {
                # master (however unlikely that may be), then we can fetch the position from the replica DB.
                $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
                if ( !$masterConn ) {
-                       $serverCount = count( $this->servers );
+                       $serverCount = $this->getServerCount();
                        for ( $i = 1; $i < $serverCount; $i++ ) {
                                $conn = $this->getAnyOpenConnection( $i );
                                if ( $conn ) {
@@ -1290,14 +1377,7 @@ class LoadBalancer implements ILoadBalancer {
                        $conn->close();
                } );
 
-               $this->conns = [
-                       self::KEY_LOCAL => [],
-                       self::KEY_FOREIGN_INUSE => [],
-                       self::KEY_FOREIGN_FREE => [],
-                       self::KEY_LOCAL_NOROUND => [],
-                       self::KEY_FOREIGN_INUSE_NOROUND => [],
-                       self::KEY_FOREIGN_FREE_NOROUND => []
-               ];
+               $this->conns = self::newConnsArray();
                $this->connsOpened = 0;
        }
 
@@ -1328,13 +1408,14 @@ class LoadBalancer implements ILoadBalancer {
                $conn->close();
        }
 
-       public function commitAll( $fname = __METHOD__ ) {
-               $this->commitMasterChanges( $fname );
+       public function commitAll( $fname = __METHOD__, $owner = null ) {
+               $this->commitMasterChanges( $fname, $owner );
                $this->flushMasterSnapshots( $fname );
                $this->flushReplicaSnapshots( $fname );
        }
 
-       public function finalizeMasterChanges() {
+       public function finalizeMasterChanges( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                $this->assertTransactionRoundStage( [ self::ROUND_CURSORY, self::ROUND_FINALIZED ] );
 
                $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
@@ -1358,7 +1439,8 @@ class LoadBalancer implements ILoadBalancer {
                return $total;
        }
 
-       public function approveMasterChanges( array $options ) {
+       public function approveMasterChanges( array $options, $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                $this->assertTransactionRoundStage( self::ROUND_FINALIZED );
 
                $limit = $options['maxWriteDuration'] ?? 0;
@@ -1391,7 +1473,8 @@ class LoadBalancer implements ILoadBalancer {
                $this->trxRoundStage = self::ROUND_APPROVED;
        }
 
-       public function beginMasterChanges( $fname = __METHOD__ ) {
+       public function beginMasterChanges( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                if ( $this->trxRoundId !== false ) {
                        throw new DBTransactionError(
                                null,
@@ -1415,7 +1498,8 @@ class LoadBalancer implements ILoadBalancer {
                $this->trxRoundStage = self::ROUND_CURSORY;
        }
 
-       public function commitMasterChanges( $fname = __METHOD__ ) {
+       public function commitMasterChanges( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                $this->assertTransactionRoundStage( self::ROUND_APPROVED );
 
                $failures = [];
@@ -1453,7 +1537,8 @@ class LoadBalancer implements ILoadBalancer {
                $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
        }
 
-       public function runMasterTransactionIdleCallbacks() {
+       public function runMasterTransactionIdleCallbacks( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) {
                        $type = IDatabase::TRIGGER_COMMIT;
                } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) {
@@ -1522,7 +1607,8 @@ class LoadBalancer implements ILoadBalancer {
                return $e;
        }
 
-       public function runMasterTransactionListenerCallbacks() {
+       public function runMasterTransactionListenerCallbacks( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) {
                        $type = IDatabase::TRIGGER_COMMIT;
                } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) {
@@ -1549,7 +1635,9 @@ class LoadBalancer implements ILoadBalancer {
                return $e;
        }
 
-       public function rollbackMasterChanges( $fname = __METHOD__ ) {
+       public function rollbackMasterChanges( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
+
                $restore = ( $this->trxRoundId !== false );
                $this->trxRoundId = false;
                $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
@@ -1567,6 +1655,7 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * @param string|string[] $stage
+        * @throws DBTransactionError
         */
        private function assertTransactionRoundStage( $stage ) {
                $stages = (array)$stage;
@@ -1585,6 +1674,20 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
+       /**
+        * @param string $fname
+        * @param int|null $owner Owner ID of the caller
+        * @throws DBTransactionError
+        */
+       private function assertOwnership( $fname, $owner ) {
+               if ( $this->ownerId !== null && $owner !== $this->ownerId ) {
+                       throw new DBTransactionError(
+                               null,
+                               "$fname: LoadBalancer is owned by LBFactory #{$this->ownerId} (got '$owner')."
+                       );
+               }
+       }
+
        /**
         * Make all DB servers with DBO_DEFAULT/DBO_TRX set join the transaction round
         *
@@ -1688,7 +1791,7 @@ class LoadBalancer implements ILoadBalancer {
        public function getLaggedReplicaMode( $domain = false ) {
                if (
                        // Avoid recursion if there is only one DB
-                       $this->getServerCount() > 1 &&
+                       $this->hasStreamingReplicaServers() &&
                        // Avoid recursion if the (non-zero load) master DB was picked for generic reads
                        $this->genericReadIndex !== $this->getWriterIndex() &&
                        // Stay in lagged replica mode during the load balancer instance lifetime
@@ -1824,20 +1927,18 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getMaxLag( $domain = false ) {
-               $maxLag = -1;
                $host = '';
+               $maxLag = -1;
                $maxIndex = 0;
 
-               if ( $this->getServerCount() <= 1 ) {
-                       return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
-               }
-
-               $lagTimes = $this->getLagTimes( $domain );
-               foreach ( $lagTimes as $i => $lag ) {
-                       if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
-                               $maxLag = $lag;
-                               $host = $this->servers[$i]['host'];
-                               $maxIndex = $i;
+               if ( $this->hasReplicaServers() ) {
+                       $lagTimes = $this->getLagTimes( $domain );
+                       foreach ( $lagTimes as $i => $lag ) {
+                               if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
+                                       $maxLag = $lag;
+                                       $host = $this->getServerInfoStrict( $i, 'host' );
+                                       $maxIndex = $i;
+                               }
                        }
                }
 
@@ -1845,7 +1946,7 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getLagTimes( $domain = false ) {
-               if ( $this->getServerCount() <= 1 ) {
+               if ( !$this->hasReplicaServers() ) {
                        return [ $this->getWriterIndex() => 0 ]; // no replication = no lag
                }
 
@@ -1862,15 +1963,32 @@ class LoadBalancer implements ILoadBalancer {
                return $this->getLoadMonitor()->getLagTimes( $indexesWithLag, $domain ) + $knownLagTimes;
        }
 
+       /**
+        * Get the lag in seconds for a given connection, or zero if this load
+        * balancer does not have replication enabled.
+        *
+        * This should be used in preference to Database::getLag() in cases where
+        * replication may not be in use, since there is no way to determine if
+        * replication is in use at the connection level without running
+        * potentially restricted queries such as SHOW SLAVE STATUS. Using this
+        * function instead of Database::getLag() avoids a fatal error in this
+        * case on many installations.
+        *
+        * @param IDatabase $conn
+        * @return int|bool Returns false on error
+        * @deprecated Since 1.34 Use IDatabase::getLag() instead
+        */
        public function safeGetLag( IDatabase $conn ) {
-               if ( $this->getServerCount() <= 1 ) {
-                       return 0;
-               } else {
-                       return $conn->getLag();
+               if ( $conn->getLBInfo( 'is static' ) ) {
+                       return 0; // static dataset
+               } elseif ( $conn->getLBInfo( 'serverIndex' ) == $this->getWriterIndex() ) {
+                       return 0; // this is the master
                }
+
+               return $conn->getLag();
        }
 
-       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
+       public function waitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
                $timeout = max( 1, $timeout ?: $this->waitTimeout );
 
                if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) {
@@ -1879,11 +1997,13 @@ class LoadBalancer implements ILoadBalancer {
 
                if ( !$pos ) {
                        // Get the current master position, opening a connection if needed
-                       $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
+                       $index = $this->getWriterIndex();
+                       $masterConn = $this->getAnyOpenConnection( $index );
                        if ( $masterConn ) {
                                $pos = $masterConn->getMasterPos();
                        } else {
-                               $masterConn = $this->openConnection( $this->getWriterIndex(), self::DOMAIN_ANY );
+                               $flags = self::CONN_SILENCE_ERRORS;
+                               $masterConn = $this->getConnection( $index, [], self::DOMAIN_ANY, $flags );
                                if ( !$masterConn ) {
                                        throw new DBReplicationWaitError(
                                                null,
@@ -1896,12 +2016,15 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( $pos instanceof DBMasterPos ) {
+                       $start = microtime( true );
                        $result = $conn->masterPosWait( $pos, $timeout );
+                       $seconds = max( microtime( true ) - $start, 0 );
                        if ( $result == -1 || is_null( $result ) ) {
-                               $msg = __METHOD__ . ': timed out waiting on {host} pos {pos}';
+                               $msg = __METHOD__ . ': timed out waiting on {host} pos {pos} [{seconds}s]';
                                $this->replLogger->warning( $msg, [
                                        'host' => $conn->getServer(),
                                        'pos' => $pos,
+                                       'seconds' => round( $seconds, 6 ),
                                        'trace' => ( new RuntimeException() )->getTraceAsString()
                                ] );
                                $ok = false;
@@ -1923,6 +2046,22 @@ class LoadBalancer implements ILoadBalancer {
                return $ok;
        }
 
+       /**
+        * Wait for a replica DB to reach a specified master position
+        *
+        * This will connect to the master to get an accurate position if $pos is not given
+        *
+        * @param IDatabase $conn Replica DB
+        * @param DBMasterPos|bool $pos Master position; default: current position
+        * @param int $timeout Timeout in seconds [optional]
+        * @return bool Success
+        * @since 1.28
+        * @deprecated Since 1.34 Use waitForMasterPos() instead
+        */
+       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
+               return $this->waitForMasterPos( $conn, $pos, $timeout );
+       }
+
        public function setTransactionListener( $name, callable $callback = null ) {
                if ( $callback ) {
                        $this->trxRecurringCallbacks[$name] = $callback;
@@ -2005,6 +2144,28 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
+       /**
+        * @param int $i Server index
+        * @param string|null $field Server index field [optional]
+        * @return array|mixed
+        * @throws InvalidArgumentException
+        */
+       private function getServerInfoStrict( $i, $field = null ) {
+               if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
+                       throw new InvalidArgumentException( "No server with index '$i'" );
+               }
+
+               if ( $field !== null ) {
+                       if ( !array_key_exists( $field, $this->servers[$i] ) ) {
+                               throw new InvalidArgumentException( "No field '$field' in server index '$i'" );
+                       }
+
+                       return $this->servers[$i][$field];
+               }
+
+               return $this->servers[$i];
+       }
+
        function __destruct() {
                // Avoid connection leaks for sanity
                $this->disable();
index fcddfcf..4c68833 100644 (file)
@@ -86,6 +86,10 @@ class LoadBalancerSingle extends LoadBalancer {
        protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
                return $this->db;
        }
+
+       public function __destruct() {
+               // do nothing since the connection was injected
+       }
 }
 
 /**
index 180baed..1666c27 100644 (file)
@@ -154,12 +154,12 @@ class LoadMonitor implements ILoadMonitor {
 
                        # Handles with open transactions are avoided since they might be subject
                        # to REPEATABLE-READ snapshots, which could affect the lag estimate query.
-                       $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+                       $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT | ILoadBalancer::CONN_SILENCE_ERRORS;
                        $conn = $this->parent->getAnyOpenConnection( $i, $flags );
                        if ( $conn ) {
                                $close = false; // already open
                        } else {
-                               $conn = $this->parent->openConnection( $i, ILoadBalancer::DOMAIN_ANY, $flags );
+                               $conn = $this->parent->getConnection( $i, [], ILoadBalancer::DOMAIN_ANY, $flags );
                                $close = true; // new connection
                        }
 
@@ -181,25 +181,21 @@ class LoadMonitor implements ILoadMonitor {
                                continue;
                        }
 
-                       if ( $conn->getLBInfo( 'is static' ) ) {
-                               $lagTimes[$i] = 0;
-                       } else {
-                               $lagTimes[$i] = $conn->getLag();
-                               if ( $lagTimes[$i] === false ) {
-                                       $this->replLogger->error(
-                                               __METHOD__ . ": host {db_server} is not replicating?",
-                                               [ 'db_server' => $host ]
-                                       );
-                               } elseif ( $lagTimes[$i] > $this->lagWarnThreshold ) {
-                                       $this->replLogger->warning(
-                                               "Server {host} has {lag} seconds of lag (>= {maxlag})",
-                                               [
-                                                       'host' => $host,
-                                                       'lag' => $lagTimes[$i],
-                                                       'maxlag' => $this->lagWarnThreshold
-                                               ]
-                                       );
-                               }
+                       $lagTimes[$i] = $conn->getLag();
+                       if ( $lagTimes[$i] === false ) {
+                               $this->replLogger->error(
+                                       __METHOD__ . ": host {db_server} is not replicating?",
+                                       [ 'db_server' => $host ]
+                               );
+                       } elseif ( $lagTimes[$i] > $this->lagWarnThreshold ) {
+                               $this->replLogger->warning(
+                                       "Server {host} has {lag} seconds of lag (>= {maxlag})",
+                                       [
+                                               'host' => $host,
+                                               'lag' => $lagTimes[$i],
+                                               'maxlag' => $this->lagWarnThreshold
+                                       ]
+                               );
                        }
 
                        if ( $close ) {
diff --git a/includes/libs/replacers/DoubleReplacer.php b/includes/libs/replacers/DoubleReplacer.php
deleted file mode 100644 (file)
index 9d05e06..0000000
+++ /dev/null
@@ -1,46 +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
- */
-
-/**
- * Class to perform secondary replacement within each replacement string
- *
- * @deprecated since 1.32, use a Closure instead
- */
-class DoubleReplacer extends Replacer {
-       /**
-        * @param mixed $from
-        * @param mixed $to
-        * @param int $index
-        */
-       public function __construct( $from, $to, $index = 0 ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->from = $from;
-               $this->to = $to;
-               $this->index = $index;
-       }
-
-       /**
-        * @param array $matches
-        * @return mixed
-        */
-       public function replace( array $matches ) {
-               return str_replace( $this->from, $this->to, $matches[$this->index] );
-       }
-}
diff --git a/includes/libs/replacers/HashtableReplacer.php b/includes/libs/replacers/HashtableReplacer.php
deleted file mode 100644 (file)
index 8247694..0000000
+++ /dev/null
@@ -1,46 +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
- */
-
-/**
- * Class to perform replacement based on a simple hashtable lookup
- *
- * @deprecated since 1.32, use a Closure instead
- */
-class HashtableReplacer extends Replacer {
-       private $table, $index;
-
-       /**
-        * @param array $table
-        * @param int $index
-        */
-       public function __construct( $table, $index = 0 ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->table = $table;
-               $this->index = $index;
-       }
-
-       /**
-        * @param array $matches
-        * @return mixed
-        */
-       public function replace( array $matches ) {
-               return $this->table[$matches[$this->index]];
-       }
-}
diff --git a/includes/libs/replacers/RegexlikeReplacer.php b/includes/libs/replacers/RegexlikeReplacer.php
deleted file mode 100644 (file)
index bdc4dc0..0000000
+++ /dev/null
@@ -1,49 +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
- */
-
-/**
- * Class to replace regex matches with a string similar to that used in preg_replace()
- *
- * @deprecated since 1.32, use a Closure instead
- */
-class RegexlikeReplacer extends Replacer {
-       private $r;
-
-       /**
-        * @param string $r
-        */
-       public function __construct( $r ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               $this->r = $r;
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        */
-       public function replace( array $matches ) {
-               $pairs = [];
-               foreach ( $matches as $i => $match ) {
-                       $pairs["\$$i"] = $match;
-               }
-
-               return strtr( $this->r, $pairs );
-       }
-}
diff --git a/includes/libs/replacers/Replacer.php b/includes/libs/replacers/Replacer.php
deleted file mode 100644 (file)
index 5425eed..0000000
+++ /dev/null
@@ -1,41 +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
- */
-
-/**
- * Base class for "replacers", objects used in preg_replace_callback() and
- * StringUtils::delimiterReplaceCallback()
- *
- * @deprecated since 1.32, use a Closure instead
- */
-abstract class Replacer {
-       /**
-        * @return array
-        */
-       public function cb() {
-               wfDeprecated( __METHOD__, '1.32' );
-               return [ $this, 'replace' ];
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        */
-       abstract public function replace( array $matches );
-}
index 5ef0135..679b1c3 100644 (file)
@@ -91,15 +91,6 @@ class BufferingStatsdDataFactory extends StatsdDataFactory implements IBuffering
                return $entity;
        }
 
-       /**
-        * @deprecated since 1.30 Use getData() instead
-        * @return StatsdData[]
-        */
-       public function getBuffer() {
-               wfDeprecated( __METHOD__, '1.30' );
-               return $this->buffer;
-       }
-
        public function hasData() {
                return !empty( $this->buffer );
        }
index ddecf9e..ead290f 100644 (file)
@@ -127,7 +127,7 @@ class BlockLogFormatter extends LogFormatter {
        public function getPreloadTitles() {
                $title = $this->entry->getTarget();
                // Preload user page for non-autoblocks
-               if ( substr( $title->getText(), 0, 1 ) !== '#' ) {
+               if ( substr( $title->getText(), 0, 1 ) !== '#' && $title->isValid() ) {
                        return [ $title->getTalkPage() ];
                }
                return [];
index 8078e2e..048b567 100644 (file)
@@ -270,8 +270,6 @@ class DeleteLogFormatter extends LogFormatter {
                                }
                        }
 
-                       $old = $this->parseBitField( $rawParams['6::ofield'] );
-                       $new = $this->parseBitField( $rawParams['7::nfield'] );
                        if ( !is_array( $rawParams['5::ids'] ) ) {
                                $rawParams['5::ids'] = explode( ',', $rawParams['5::ids'] );
                        }
@@ -279,8 +277,6 @@ class DeleteLogFormatter extends LogFormatter {
                        $params = [
                                '::type' => $rawParams['4::type'],
                                ':array:ids' => $rawParams['5::ids'],
-                               ':assoc:old' => [ 'bitmask' => $old ],
-                               ':assoc:new' => [ 'bitmask' => $new ],
                        ];
 
                        static $fields = [
@@ -289,9 +285,20 @@ class DeleteLogFormatter extends LogFormatter {
                                Revision::DELETED_USER => 'user',
                                Revision::DELETED_RESTRICTED => 'restricted',
                        ];
-                       foreach ( $fields as $bit => $key ) {
-                               $params[':assoc:old'][$key] = (bool)( $old & $bit );
-                               $params[':assoc:new'][$key] = (bool)( $new & $bit );
+
+                       if ( isset( $rawParams['6::ofield'] ) ) {
+                               $old = $this->parseBitField( $rawParams['6::ofield'] );
+                               $params[':assoc:old'] = [ 'bitmask' => $old ];
+                               foreach ( $fields as $bit => $key ) {
+                                       $params[':assoc:old'][$key] = (bool)( $old & $bit );
+                               }
+                       }
+                       if ( isset( $rawParams['7::nfield'] ) ) {
+                               $new = $this->parseBitField( $rawParams['7::nfield'] );
+                               $params[':assoc:new'] = [ 'bitmask' => $new ];
+                               foreach ( $fields as $bit => $key ) {
+                                       $params[':assoc:new'][$key] = (bool)( $new & $bit );
+                               }
                        }
                } elseif ( $subtype === 'restore' ) {
                        $rawParams = $entry->getParameters();
index dbeca0b..95053cf 100644 (file)
@@ -231,7 +231,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
                }
 
                // $scaler will return a MediaTransformError on failure, or false on success.
-               // If the scaler is succesful, it will have created a thumbnail at the destination
+               // If the scaler is successful, it will have created a thumbnail at the destination
                // path.
                if ( is_array( $scaler ) && is_callable( $scaler ) ) {
                        // Allow subclasses to specify their own rendering methods.
index 5313ca4..8a5f591 100644 (file)
@@ -250,7 +250,7 @@ class SqlBagOStuff extends BagOStuff {
                return false;
        }
 
-       public function getMulti( array $keys, $flags = 0 ) {
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                $values = [];
 
                $blobs = $this->fetchBlobMulti( $keys );
@@ -261,7 +261,7 @@ class SqlBagOStuff extends BagOStuff {
                return $values;
        }
 
-       public function fetchBlobMulti( array $keys, $flags = 0 ) {
+       protected function fetchBlobMulti( array $keys, $flags = 0 ) {
                $values = []; // array of (key => value)
 
                $keysByTable = [];
@@ -391,8 +391,8 @@ class SqlBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
-               $ok = $this->setMulti( [ $key => $value ], $exptime );
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               $ok = $this->insertMulti( [ $key => $value ], $exptime, $flags, true );
 
                return $ok;
        }
@@ -446,6 +446,10 @@ class SqlBagOStuff extends BagOStuff {
        }
 
        public function deleteMulti( array $keys, $flags = 0 ) {
+               return $this->purgeMulti( $keys, $flags );
+       }
+
+       public function purgeMulti( array $keys, $flags = 0 ) {
                $keysByTable = [];
                foreach ( $keys as $key ) {
                        list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
@@ -482,8 +486,8 @@ class SqlBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function delete( $key, $flags = 0 ) {
-               $ok = $this->deleteMulti( [ $key ], $flags );
+       protected function doDelete( $key, $flags = 0 ) {
+               $ok = $this->purgeMulti( [ $key ], $flags );
 
                return $ok;
        }
@@ -730,10 +734,10 @@ class SqlBagOStuff extends BagOStuff {
         * On typical message and page data, this can provide a 3X decrease
         * in storage requirements.
         *
-        * @param mixed &$data
+        * @param mixed $data
         * @return string
         */
-       protected function serialize( &$data ) {
+       protected function serialize( $data ) {
                $serial = serialize( $data );
 
                if ( function_exists( 'gzdeflate' ) ) {
index e929ed8..12cfe83 100644 (file)
@@ -935,7 +935,7 @@ EOT
                                ) . "\n"
                        );
 
-               };
+               }
                $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
                $res->free();
 
index cdaf062..d69a433 100644 (file)
@@ -406,8 +406,8 @@ class PageArchive {
         * @param User|null $user User performing the action, or null to use $wgUser
         * @param string|string[]|null $tags Change tags to add to log entry
         *   ($user should be able to add the specified tags before this is called)
-        * @return array|bool array(number of file revisions restored, number of image revisions
-        *   restored, log message) on success, false on failure.
+        * @return array|bool number of file revisions restored, number of image revisions
+        *   restored, log message ] on success, false on failure.
         */
        public function undelete( $timestamps, $comment = '', $fileVersions = [],
                $unsuppress = false, User $user = null, $tags = null
index 8df9ab2..013dd75 100644 (file)
@@ -152,7 +152,7 @@ class WikiFilePage extends WikiPage {
                $size = $this->mFile->getSize();
 
                /**
-                * @var $file File
+                * @var File $file
                 */
                foreach ( $dupes as $index => $file ) {
                        $key = $file->getRepoName() . ':' . $file->getName();
index 332b1ee..9e80cf4 100644 (file)
@@ -1697,6 +1697,7 @@ class WikiPage implements Page, IDBAccessObject {
                        MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
                );
 
+               $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
                $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
                $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
 
@@ -1904,7 +1905,11 @@ class WikiPage implements Page, IDBAccessObject {
                // TODO: this logic should not be in the storage layer, it's here for compatibility
                // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
                // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
-               if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+               if ( $needsPatrol && $permissionManager->userCan(
+                       'autopatrol', $user, $this->getTitle()
+               ) ) {
                        $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
                }
 
@@ -3073,7 +3078,7 @@ class WikiPage implements Page, IDBAccessObject {
         * (with ChangeTags::canAddTagsAccompanyingChange)
         *
         * @return array Array of errors, each error formatted as
-        *   array(messagekey, param1, param2, ...).
+        *   [ messagekey, param1, param2, ... ].
         * On success, the array is empty.  This array can also be passed to
         * OutputPage::showPermissionsErrorPage().
         */
@@ -3267,7 +3272,11 @@ class WikiPage implements Page, IDBAccessObject {
                // TODO: this logic should not be in the storage layer, it's here for compatibility
                // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
                // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
-               if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+               if ( $wgUseRCPatrol && $permissionManager->userCan(
+                       'autopatrol', $guser, $this->getTitle()
+               ) ) {
                        $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
                }
 
index 70663a0..d274558 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * Expansion frame with custom arguments
+ * @deprecated since 1.34, use PPCustomFrame_Hash
  * @ingroup Parser
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
index a7fea00..03ee6d9 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * An expansion frame, used as a context to expand the result of preprocessToObj()
+ * @deprecated since 1.34, use PPFrame_Hash
  * @ingroup Parser
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
index 8a435ba..26a4791 100644 (file)
@@ -20,6 +20,7 @@
  */
 
 /**
+ * @deprecated since 1.34, use PPNode_Hash_{Tree,Text,Array,Attr}
  * @ingroup Parser
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
index 52cb9cb..b4c8743 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * Expansion frame with template arguments
+ * @deprecated since 1.34, use PPTemplateFrame_Hash
  * @ingroup Parser
  */
 // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
index 486fdf4..4808caf 100644 (file)
@@ -421,21 +421,10 @@ class Parser {
         * Which class should we use for the preprocessor if not otherwise specified?
         *
         * @since 1.34
+        * @deprecated since 1.34, removing configurability of preprocessor
         * @return string
         */
        public static function getDefaultPreprocessorClass() {
-               if ( wfIsHHVM() ) {
-                       # Under HHVM Preprocessor_Hash is much faster than Preprocessor_DOM
-                       return Preprocessor_Hash::class;
-               }
-               if ( extension_loaded( 'domxml' ) ) {
-                       # PECL extension that conflicts with the core DOM extension (T15770)
-                       wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
-                       return Preprocessor_Hash::class;
-               }
-               if ( extension_loaded( 'dom' ) ) {
-                       return Preprocessor_DOM::class;
-               }
                return Preprocessor_Hash::class;
        }
 
@@ -2786,7 +2775,7 @@ class Parser {
                                        # The vary-revision flag must be set, because the magic word
                                        # will have a different value once the page is saved.
                                        $this->mOutput->setFlag( 'vary-revision' );
-                                       wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
+                                       wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision" );
                                }
                                $value = $pageid ?: null;
                                break;
@@ -2803,13 +2792,14 @@ class Parser {
                                                $value = '-';
                                        } else {
                                                $this->mOutput->setFlag( 'vary-revision-exists' );
+                                               wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-exists" );
                                                $value = '';
                                        }
                                } else {
                                        # Inform the edit saving system that getting the canonical output after
                                        # revision insertion requires another parse using the actual revision ID
                                        $this->mOutput->setFlag( 'vary-revision-id' );
-                                       wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
+                                       wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id" );
                                        $value = $this->getRevisionId();
                                        if ( $value === 0 ) {
                                                $rev = $this->getRevisionObject();
@@ -2839,17 +2829,13 @@ class Parser {
                                $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
                                break;
                        case 'revisiontimestamp':
-                               # Let the edit saving system know we should parse the page
-                               # *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
-                               $value = $this->getRevisionTimestamp();
+                               $value = $this->getRevisionTimestampSubstring( 0, 14, self::MAX_TTS, $index );
                                break;
                        case 'revisionuser':
-                               # Let the edit saving system know we should parse the page
-                               # *after* a revision ID has been assigned for null edits.
+                               # Inform the edit saving system that getting the canonical output after
+                               # revision insertion requires a parse that used the actual user ID
                                $this->mOutput->setFlag( 'vary-user' );
-                               wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
+                               wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user" );
                                $value = $this->getRevisionUser();
                                break;
                        case 'revisionsize':
@@ -2997,7 +2983,7 @@ class Parser {
        /**
         * @param int $start
         * @param int $len
-        * @param int $mtts Max time-till-save; sets vary-revision if result might change by then
+        * @param int $mtts Max time-till-save; sets vary-revision-timestamp if result changes by then
         * @param string $variable Parser variable name
         * @return string
         */
@@ -3006,7 +2992,10 @@ class Parser {
                $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
                # Possibly set vary-revision if there is not yet an associated revision
                if ( !$this->getRevisionObject() ) {
-                       # Get the timezone-adjusted timestamp $mtts seconds in the future
+                       # Get the timezone-adjusted timestamp $mtts seconds in the future.
+                       # This future is relative to the current time and not that of the
+                       # parser options. The rendered timestamp can be compared to that
+                       # of the timestamp specified by the parser options.
                        $resThen = substr(
                                $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
                                $start,
@@ -3014,10 +3003,10 @@ class Parser {
                        );
 
                        if ( $resNow !== $resThen ) {
-                               # Let the edit saving system know we should parse the page
-                               # *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" );
+                               # Inform the edit saving system that getting the canonical output after
+                               # revision insertion requires a parse that used an actual revision timestamp
+                               $this->mOutput->setFlag( 'vary-revision-timestamp' );
+                               wfDebug( __METHOD__ . ": $variable used, setting vary-revision-timestamp" );
                        }
                }
 
@@ -3739,6 +3728,7 @@ class Parser {
                                        // If we transclude ourselves, the final result
                                        // will change based on the new version of the page
                                        $this->mOutput->setFlag( 'vary-revision' );
+                                       wfDebug( __METHOD__ . ": self transclusion, setting vary-revision" );
                                }
                        }
                }
@@ -3847,19 +3837,6 @@ class Parser {
                        'deps' => $deps ];
        }
 
-       /**
-        * Fetch a file and its title and register a reference to it.
-        * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
-        * @param Title $title
-        * @param array $options Array of options to RepoGroup::findFile
-        * @return File|bool
-        * @deprecated since 1.32, use fetchFileAndTitle instead
-        */
-       public function fetchFile( $title, $options = [] ) {
-               wfDeprecated( __METHOD__, '1.32' );
-               return $this->fetchFileAndTitle( $title, $options )[0];
-       }
-
        /**
         * Fetch a file and its title and register a reference to it.
         * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
@@ -5916,7 +5893,7 @@ class Parser {
         *
         * The return value will be either:
         *   - a) Positive, indicating a specific revision ID (current or old)
-        *   - b) Zero, meaning the revision ID specified by getCurrentRevisionCallback()
+        *   - b) Zero, meaning the revision ID is specified by getCurrentRevisionCallback()
         *   - c) Null, meaning the parse is for preview mode and there is no revision
         *
         * @return int|null
@@ -5971,20 +5948,25 @@ class Parser {
        /**
         * Get the timestamp associated with the current revision, adjusted for
         * the default server-local timestamp
-        * @return string
+        * @return string TS_MW timestamp
         */
        public function getRevisionTimestamp() {
-               if ( is_null( $this->mRevisionTimestamp ) ) {
-                       $revObject = $this->getRevisionObject();
-                       $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
-
-                       # The cryptic '' timezone parameter tells to use the site-default
-                       # timezone offset instead of the user settings.
-                       # Since this value will be saved into the parser cache, served
-                       # to other users, and potentially even used inside links and such,
-                       # it needs to be consistent for all visitors.
-                       $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
+               if ( $this->mRevisionTimestamp !== null ) {
+                       return $this->mRevisionTimestamp;
                }
+
+               # Use specified revision timestamp, falling back to the current timestamp
+               $revObject = $this->getRevisionObject();
+               $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
+               $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
+
+               # The cryptic '' timezone parameter tells to use the site-default
+               # timezone offset instead of the user settings.
+               # Since this value will be saved into the parser cache, served
+               # to other users, and potentially even used inside links and such,
+               # it needs to be consistent for all visitors.
+               $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
+
                return $this->mRevisionTimestamp;
        }
 
index 66b1612..709f159 100644 (file)
@@ -895,7 +895,7 @@ class ParserOptions {
 
        /**
         * Timestamp used for {{CURRENTDAY}} etc.
-        * @return string
+        * @return string TS_MW timestamp
         */
        public function getTimestamp() {
                if ( !isset( $this->mTimestamp ) ) {
@@ -913,27 +913,6 @@ class ParserOptions {
                return wfSetVar( $this->mTimestamp, $x );
        }
 
-       /**
-        * Create "edit section" links?
-        * @deprecated since 1.31, use ParserOutput::getText() options instead.
-        * @return bool
-        */
-       public function getEditSection() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return true;
-       }
-
-       /**
-        * Create "edit section" links?
-        * @deprecated since 1.31, use ParserOutput::getText() options instead.
-        * @param bool|null $x New value (null is no change)
-        * @return bool Old value
-        */
-       public function setEditSection( $x ) {
-               wfDeprecated( __METHOD__, '1.31' );
-               return true;
-       }
-
        /**
         * Set the redirect target.
         *
index ab7348f..c8113f3 100644 (file)
@@ -26,8 +26,14 @@ class ParserOutput extends CacheTime {
        /**
         * Feature flags to indicate to extensions that MediaWiki core supports and
         * uses getText() stateless transforms.
+        *
+        * @since 1.31
         */
        const SUPPORTS_STATELESS_TRANSFORMS = 1;
+
+       /**
+        * @since 1.31
+        */
        const SUPPORTS_UNWRAP_TRANSFORM = 1;
 
        /**
@@ -207,6 +213,9 @@ class ParserOutput extends CacheTime {
        /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
        private $mSpeculativeRevId;
 
+       /** @var int|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */
+       private $revisionTimestampUsed;
+
        /** string CSS classes to use for the wrapping div, stored in the array keys.
         * If no class is given, no wrapper is added.
         */
@@ -439,6 +448,22 @@ class ParserOutput extends CacheTime {
                return $this->mSpeculativeRevId;
        }
 
+       /**
+        * @param string $timestamp TS_MW timestamp
+        * @since 1.34
+        */
+       public function setRevisionTimestampUsed( $timestamp ) {
+               $this->revisionTimestampUsed = $timestamp;
+       }
+
+       /**
+        * @return string|null TS_MW timestamp or null if not used
+        * @since 1.34
+        */
+       public function getRevisionTimestampUsed() {
+               return $this->revisionTimestampUsed;
+       }
+
        public function &getLanguageLinks() {
                return $this->mLanguageLinks;
        }
index 0f0496b..9e510d2 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  * @ingroup Parser
+ * @deprecated since 1.34, use Preprocessor_Hash
  */
 
 /**
@@ -37,6 +38,7 @@ class Preprocessor_DOM extends Preprocessor {
        const CACHE_PREFIX = 'preprocess-xml';
 
        public function __construct( $parser ) {
+               wfDeprecated( __METHOD__, '1.34' ); // T204945
                $this->parser = $parser;
                $mem = ini_get( 'memory_limit' );
                $this->memoryLimit = false;
index f76e3a9..8e0cf5c 100644 (file)
@@ -790,7 +790,7 @@ class Sanitizer {
         */
        static function validateTagAttributes( $attribs, $element ) {
                return self::validateAttributes( $attribs,
-                       self::attributeWhitelist( $element ) );
+                       self::attributeWhitelistInternal( $element ) );
        }
 
        /**
@@ -802,14 +802,21 @@ class Sanitizer {
         * - Invalid id attributes are re-encoded
         *
         * @param array $attribs
-        * @param array $whitelist List of allowed attribute names
+        * @param array $whitelist List of allowed attribute names,
+        *   either as a sequential array of valid attribute names or
+        *   as an associative array where keys give valid attribute names
         * @return array
         *
         * @todo Check for legal values where the DTD limits things.
         * @todo Check for unique id attribute :P
         */
        static function validateAttributes( $attribs, $whitelist ) {
-               $whitelist = array_flip( $whitelist );
+               if ( isset( $whitelist[0] ) ) {
+                       // We would like to eventually deprecate calling this
+                       // function with a sequential array, but for now just
+                       // convert it.
+                       $whitelist = array_flip( $whitelist );
+               }
                $hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
 
                $out = [];
@@ -828,10 +835,10 @@ class Sanitizer {
                        # * Disallow data attributes used by MediaWiki code
                        # * Ensure that the attribute is not namespaced by banning
                        #   colons.
-                       if ( !preg_match( '/^data-[^:]*$/i', $attribute )
-                               && !isset( $whitelist[$attribute] )
-                               || self::isReservedDataAttribute( $attribute )
-                       ) {
+                       if ( (
+                               !preg_match( '/^data-[^:]*$/i', $attribute ) &&
+                               !array_key_exists( $attribute, $whitelist )
+                       ) || self::isReservedDataAttribute( $attribute ) ) {
                                continue;
                        }
 
@@ -1245,7 +1252,7 @@ class Sanitizer {
         *   HTML5 definition of id attribute
         *
         * @param string $id Id to escape
-        * @param string|array $options String or array of strings (default is array()):
+        * @param string|array $options String or array of strings (default is []):
         *   'noninitial': This is a non-initial fragment of an id, not a full id,
         *       so don't pay attention if the first character isn't valid at the
         *       beginning of an id.
@@ -1746,26 +1753,63 @@ class Sanitizer {
         * Fetch the whitelist of acceptable attributes for a given element name.
         *
         * @param string $element
-        * @return array
+        * @return array A sequential array of acceptable attribute names
+        * @deprecated since 1.34; should be private
         */
        static function attributeWhitelist( $element ) {
+               wfDeprecated( __METHOD__, '1.34' );
                $list = self::setupAttributeWhitelist();
                return $list[$element] ?? [];
        }
 
+       /**
+        * Fetch the whitelist of acceptable attributes for a given element name.
+        *
+        * @param string $element
+        * @return array An associative array where keys are acceptable attribute
+        *   names
+        */
+       private static function attributeWhitelistInternal( $element ) {
+               $list = self::setupAttributeWhitelistInternal();
+               return $list[$element] ?? [];
+       }
+
        /**
         * Foreach array key (an allowed HTML element), return an array
         * of allowed attributes
         * @return array
+        * @deprecated since 1.34; should be private
         */
        static function setupAttributeWhitelist() {
+               wfDeprecated( __METHOD__, '1.34' );
+               $wlist = self::setupAttributeWhitelistInternal();
+               // This method is expected to return a sequential array as the
+               // value for each HTML element key.
+               return array_map( function ( $v ) {
+                       return array_keys( $v );
+               }, $wlist );
+       }
+
+       /**
+        * Foreach array key (an allowed HTML element), return an array
+        * of allowed attributes
+        * @return array An associative array: keys are HTML element names;
+        *   values are associative arrays where the keys are allowed attribute
+        *   names.
+        */
+       private static function setupAttributeWhitelistInternal() {
                static $whitelist;
 
                if ( $whitelist !== null ) {
                        return $whitelist;
                }
 
-               $common = [
+               // For lookup efficiency flip each attributes array so the keys are
+               // the valid attributes.
+               $merge = function ( $a, $b, $c = [] ) {
+                       return array_merge( $a, array_flip( $b ), array_flip( $c ) );
+               };
+               $common = $merge( [], [
                        # HTML
                        'id',
                        'class',
@@ -1798,9 +1842,10 @@ class Sanitizer {
                        'itemref',
                        'itemscope',
                        'itemtype',
-               ];
+               ] );
+
+               $block = $merge( $common, [ 'align' ] );
 
-               $block = array_merge( $common, [ 'align' ] );
                $tablealign = [ 'align', 'valign' ];
                $tablecell = [
                        'abbr',
@@ -1850,8 +1895,8 @@ class Sanitizer {
                        # acronym
 
                        # 9.2.2
-                       'blockquote' => array_merge( $common, [ 'cite' ] ),
-                       'q'          => array_merge( $common, [ 'cite' ] ),
+                       'blockquote' => $merge( $common, [ 'cite' ] ),
+                       'q'          => $merge( $common, [ 'cite' ] ),
 
                        # 9.2.3
                        'sub'        => $common,
@@ -1861,22 +1906,22 @@ class Sanitizer {
                        'p'          => $block,
 
                        # 9.3.2
-                       'br'         => array_merge( $common, [ 'clear' ] ),
+                       'br'         => $merge( $common, [ 'clear' ] ),
 
                        # https://www.w3.org/TR/html5/text-level-semantics.html#the-wbr-element
                        'wbr'        => $common,
 
                        # 9.3.4
-                       'pre'        => array_merge( $common, [ 'width' ] ),
+                       'pre'        => $merge( $common, [ 'width' ] ),
 
                        # 9.4
-                       'ins'        => array_merge( $common, [ 'cite', 'datetime' ] ),
-                       'del'        => array_merge( $common, [ 'cite', 'datetime' ] ),
+                       'ins'        => $merge( $common, [ 'cite', 'datetime' ] ),
+                       'del'        => $merge( $common, [ 'cite', 'datetime' ] ),
 
                        # 10.2
-                       'ul'         => array_merge( $common, [ 'type' ] ),
-                       'ol'         => array_merge( $common, [ 'type', 'start', 'reversed' ] ),
-                       'li'         => array_merge( $common, [ 'type', 'value' ] ),
+                       'ul'         => $merge( $common, [ 'type' ] ),
+                       'ol'         => $merge( $common, [ 'type', 'start', 'reversed' ] ),
+                       'li'         => $merge( $common, [ 'type', 'value' ] ),
 
                        # 10.3
                        'dl'         => $common,
@@ -1884,7 +1929,7 @@ class Sanitizer {
                        'dt'         => $common,
 
                        # 11.2.1
-                       'table'      => array_merge( $common,
+                       'table'      => $merge( $common,
                                                                [ 'summary', 'width', 'border', 'frame',
                                                                                'rules', 'cellspacing', 'cellpadding',
                                                                                'align', 'bgcolor',
@@ -1899,31 +1944,31 @@ class Sanitizer {
                        'tbody'      => $common,
 
                        # 11.2.4
-                       'colgroup'   => array_merge( $common, [ 'span' ] ),
-                       'col'        => array_merge( $common, [ 'span' ] ),
+                       'colgroup'   => $merge( $common, [ 'span' ] ),
+                       'col'        => $merge( $common, [ 'span' ] ),
 
                        # 11.2.5
-                       'tr'         => array_merge( $common, [ 'bgcolor' ], $tablealign ),
+                       'tr'         => $merge( $common, [ 'bgcolor' ], $tablealign ),
 
                        # 11.2.6
-                       'td'         => array_merge( $common, $tablecell, $tablealign ),
-                       'th'         => array_merge( $common, $tablecell, $tablealign ),
+                       'td'         => $merge( $common, $tablecell, $tablealign ),
+                       'th'         => $merge( $common, $tablecell, $tablealign ),
 
                        # 12.2
                        # NOTE: <a> is not allowed directly, but the attrib
                        # whitelist is used from the Parser object
-                       'a'          => array_merge( $common, [ 'href', 'rel', 'rev' ] ), # rel/rev esp. for RDFa
+                       'a'          => $merge( $common, [ 'href', 'rel', 'rev' ] ), # rel/rev esp. for RDFa
 
                        # 13.2
                        # Not usually allowed, but may be used for extension-style hooks
                        # such as <math> when it is rasterized, or if $wgAllowImageTag is
                        # true
-                       'img'        => array_merge( $common, [ 'alt', 'src', 'width', 'height', 'srcset' ] ),
+                       'img'        => $merge( $common, [ 'alt', 'src', 'width', 'height', 'srcset' ] ),
                        # Attributes for A/V tags added in T163583 / T133673
-                       'audio'      => array_merge( $common, [ 'controls', 'preload', 'width', 'height' ] ),
-                       'video'      => array_merge( $common, [ 'poster', 'controls', 'preload', 'width', 'height' ] ),
-                       'source'     => array_merge( $common, [ 'type', 'src' ] ),
-                       'track'      => array_merge( $common, [ 'type', 'src', 'srclang', 'kind', 'label' ] ),
+                       'audio'      => $merge( $common, [ 'controls', 'preload', 'width', 'height' ] ),
+                       'video'      => $merge( $common, [ 'poster', 'controls', 'preload', 'width', 'height' ] ),
+                       'source'     => $merge( $common, [ 'type', 'src' ] ),
+                       'track'      => $merge( $common, [ 'type', 'src', 'srclang', 'kind', 'label' ] ),
 
                        # 15.2.1
                        'tt'         => $common,
@@ -1936,11 +1981,11 @@ class Sanitizer {
                        'u'          => $common,
 
                        # 15.2.2
-                       'font'       => array_merge( $common, [ 'size', 'color', 'face' ] ),
+                       'font'       => $merge( $common, [ 'size', 'color', 'face' ] ),
                        # basefont
 
                        # 15.3
-                       'hr'         => array_merge( $common, [ 'width' ] ),
+                       'hr'         => $merge( $common, [ 'width' ] ),
 
                        # HTML Ruby annotation text module, simple ruby only.
                        # https://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
@@ -1948,13 +1993,13 @@ class Sanitizer {
                        # rbc
                        'rb'         => $common,
                        'rp'         => $common,
-                       'rt'         => $common, # array_merge( $common, array( 'rbspan' ) ),
+                       'rt'         => $common, # $merge( $common, [ 'rbspan' ] ),
                        'rtc'        => $common,
 
                        # MathML root element, where used for extensions
                        # 'title' may not be 100% valid here; it's XHTML
                        # https://www.w3.org/TR/REC-MathML/
-                       'math'       => [ 'class', 'style', 'id', 'title' ],
+                       'math'       => $merge( [], [ 'class', 'style', 'id', 'title' ] ),
 
                        // HTML 5 section 4.5
                        'figure'     => $common,
@@ -1966,8 +2011,8 @@ class Sanitizer {
 
                        # HTML5 elements, defined by:
                        # https://html.spec.whatwg.org/multipage/semantics.html#the-data-element
-                       'data' => array_merge( $common, [ 'value' ] ),
-                       'time' => array_merge( $common, [ 'datetime' ] ),
+                       'data' => $merge( $common, [ 'value' ] ),
+                       'time' => $merge( $common, [ 'datetime' ] ),
                        'mark' => $common,
 
                        // meta and link are only permitted by removeHTMLtags when Microdata
@@ -1975,8 +2020,8 @@ class Sanitizer {
                        // Also meta and link are only valid in WikiText as Microdata elements
                        // (ie: validateTag rejects tags missing the attributes needed for Microdata)
                        // So we don't bother including $common attributes that have no purpose.
-                       'meta' => [ 'itemprop', 'content' ],
-                       'link' => [ 'itemprop', 'href', 'title' ],
+                       'meta' => $merge( [], [ 'itemprop', 'content' ] ),
+                       'link' => $merge( [], [ 'itemprop', 'href', 'title' ] ),
                ];
 
                return $whitelist;
index 8413054..f3d8d03 100644 (file)
@@ -109,7 +109,7 @@ class LayeredParameterizedPassword extends ParameterizedPassword {
                foreach ( $this->config['types'] as $i => $type ) {
                        if ( $i == 0 ) {
                                continue;
-                       };
+                       }
 
                        // Construct pseudo-hash based on params and arguments
                        /** @var ParameterizedPassword $passObj */
index c954df1..66b1529 100644 (file)
  *
  * @par Example:
  * @code
- * $wgRCFeeds['redis'] = array(
+ * $wgRCFeeds['redis'] = [
  *      'formatter' => 'JSONRCFeedFormatter',
  *      'uri'       => "redis://127.0.0.1:6379/rc.$wgDBname",
- * );
+ * ];
  * @endcode
  *
  * @since 1.22
index faaaece..e71de84 100644 (file)
@@ -65,6 +65,7 @@ class ExtensionProcessor implements Processor {
        protected static $coreAttributes = [
                'SkinOOUIThemes',
                'TrackingCategories',
+               'RestRoutes',
        ];
 
        /**
index 515f287..387e344 100644 (file)
@@ -118,11 +118,10 @@ class ResourceLoader implements LoggerAwareInterface {
                        return;
                }
                $dbr = wfGetDB( DB_REPLICA );
-               $skin = $context->getSkin();
                $lang = $context->getLanguage();
 
                // Batched version of ResourceLoaderModule::getFileDependencies
-               $vary = "$skin|$lang";
+               $vary = ResourceLoaderModule::getVary( $context );
                $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
                                'md_module' => $moduleNames,
                                'md_skin' => $vary,
@@ -196,8 +195,7 @@ class ResourceLoader implements LoggerAwareInterface {
                $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
 
                $key = $cache->makeGlobalKey(
-                       'resourceloader',
-                       'filter',
+                       'resourceloader-filter',
                        $filter,
                        self::CACHE_VERSION,
                        md5( $data )
@@ -1709,7 +1707,6 @@ MESSAGE;
         * @param bool $printable
         * @param bool $handheld
         * @param array $extraQuery
-        *
         * @return array
         */
        public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
@@ -1718,9 +1715,17 @@ MESSAGE;
        ) {
                $query = [
                        'modules' => self::makePackedModulesString( $modules ),
-                       'lang' => $lang,
-                       'skin' => $skin,
                ];
+               // Keep urls short by omitting query parameters that
+               // match the defaults assumed by ResourceLoaderContext.
+               // Note: This relies on the defaults either being insignificant or forever constant,
+               // as otherwise cached urls could change in meaning when the defaults change.
+               if ( $lang !== ResourceLoaderContext::DEFAULT_LANG ) {
+                       $query['lang'] = $lang;
+               }
+               if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
+                       $query['skin'] = $skin;
+               }
                if ( $debug === true ) {
                        $query['debug'] = 'true';
                }
diff --git a/includes/resourceloader/ResourceLoaderCircularDependencyError.php b/includes/resourceloader/ResourceLoaderCircularDependencyError.php
new file mode 100644 (file)
index 0000000..7cd53fe
--- /dev/null
@@ -0,0 +1,26 @@
+<?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
+ */
+
+/**
+ * @internal For use by ResourceLoaderStartUpModule only
+ */
+class ResourceLoaderCircularDependencyError extends Exception {
+}
index 6061fb5..7f2f85f 100644 (file)
@@ -303,7 +303,7 @@ JAVASCRIPT;
 
                // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
                // Pass-through a custom 'target' from OutputPage (T143066).
-               $startupQuery = [];
+               $startupQuery = [ 'raw' => '1' ];
                foreach ( [ 'target', 'safemode' ] as $param ) {
                        if ( $this->options[$param] !== null ) {
                                $startupQuery[$param] = (string)$this->options[$param];
index 58152ea..95a81e6 100644 (file)
@@ -30,6 +30,9 @@ use MediaWiki\MediaWikiServices;
  * of a specific loader request.
  */
 class ResourceLoaderContext implements MessageLocalizer {
+       const DEFAULT_LANG = 'qqx';
+       const DEFAULT_SKIN = 'fallback';
+
        protected $resourceLoader;
        protected $request;
        protected $logger;
@@ -88,7 +91,7 @@ class ResourceLoaderContext implements MessageLocalizer {
                        // The 'skin' parameter is required. (Not yet enforced.)
                        // For requests without a known skin specified,
                        // use MediaWiki's 'fallback' skin for skin-specific decisions.
-                       $this->skin = 'fallback';
+                       $this->skin = self::DEFAULT_SKIN;
                }
        }
 
@@ -134,6 +137,8 @@ class ResourceLoaderContext implements MessageLocalizer {
        }
 
        /**
+        * @deprecated since 1.34 Use ResourceLoaderModule::getConfig instead
+        * inside module methods. Use ResourceLoader::getConfig elsewhere.
         * @return Config
         */
        public function getConfig() {
@@ -148,6 +153,8 @@ class ResourceLoaderContext implements MessageLocalizer {
        }
 
        /**
+        * @deprecated since 1.34 Use ResourceLoaderModule::getLogger instead
+        * inside module methods. Use ResourceLoader::getLogger elsewhere.
         * @since 1.27
         * @return \Psr\Log\LoggerInterface
         */
@@ -174,7 +181,7 @@ class ResourceLoaderContext implements MessageLocalizer {
                        if ( !Language::isValidBuiltInCode( $lang ) ) {
                                // The 'lang' parameter is required. (Not yet enforced.)
                                // If omitted, localise with the dummy language code.
-                               $lang = 'qqx';
+                               $lang = self::DEFAULT_LANG;
                        }
                        $this->language = $lang;
                }
@@ -186,8 +193,10 @@ class ResourceLoaderContext implements MessageLocalizer {
         */
        public function getDirection() {
                if ( $this->direction === null ) {
-                       $this->direction = $this->getRequest()->getRawVal( 'dir' );
-                       if ( !$this->direction ) {
+                       $direction = $this->getRequest()->getRawVal( 'dir' );
+                       if ( $direction === 'ltr' || $direction === 'rtl' ) {
+                               $this->direction = $direction;
+                       } else {
                                // Determine directionality based on user language (T8100)
                                $this->direction = Language::factory( $this->getLanguage() )->getDir();
                        }
index 031541b..7093ab1 100644 (file)
 
 /**
  * ResourceLoader module based on local JavaScript/CSS files.
+ *
+ * The following public methods can query the database:
+ *
+ * - getDefinitionSummary / … / ResourceLoaderModule::getFileDependencies.
+ * - getVersionHash / getDefinitionSummary / … / ResourceLoaderModule::getFileDependencies.
+ * - getStyles / ResourceLoaderModule::saveFileDependencies.
  */
 class ResourceLoaderFileModule extends ResourceLoaderModule {
 
@@ -334,7 +340,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         *     to $IP
         * @param string|null $remoteBasePath Path to use if not provided in module definition. Defaults
         *     to $wgResourceBasePath
-        * @return array Array( localBasePath, remoteBasePath )
+        * @return array [ localBasePath, remoteBasePath ]
         */
        public static function extractBasePaths(
                $options = [],
@@ -617,11 +623,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                        'raw',
                ] as $member ) {
                        $options[$member] = $this->{$member};
-               };
+               }
+
+               $packageFiles = $this->expandPackageFiles( $context );
+               if ( $packageFiles ) {
+                       // Extract the minimum needed:
+                       // - The 'main' pointer (included as-is).
+                       // - The 'files' array, simplied to only which files exist (the keys of
+                       //   this array), and something that represents their non-file content.
+                       //   For packaged files that reflect files directly from disk, the
+                       //   'getFileHashes' method tracks this already.
+                       //   It is important that the keys of the 'files' array are preserved,
+                       //   as they affect the module output.
+                       $packageFiles['files'] = array_map( function ( $fileInfo ) {
+                               return $fileInfo['definitionSummary'] ?? ( $fileInfo['content'] ?? null );
+                       }, $packageFiles['files'] );
+               }
 
                $summary[] = [
                        'options' => $options,
-                       'packageFiles' => $this->expandPackageFiles( $context ),
+                       'packageFiles' => $packageFiles,
                        'fileHashes' => $this->getFileHashes( $context ),
                        'messageBlob' => $this->getMessageBlob( $context ),
                ];
@@ -1068,16 +1089,22 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
        }
 
        /**
-        * Expand the packageFiles definition into something that's (almost) the right format for
-        * getPackageFiles() to return. This expands shorthands, resolves config vars and callbacks,
-        * but does not expand file paths or read the actual contents of files. Those things are done
-        * by getPackageFiles().
+        * Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary().
+        *
+        * This expands the 'packageFiles' definition into something that's (almost) the right format
+        * for getPackageFiles() to return. It expands shorthands, resolves config vars, and handles
+        * summarising any non-file data for getVersionHash(). For file-based data, getFileHashes()
+        * handles it instead, which also ends up in getDefinitionSummary().
         *
-        * This is split up in this way so that getFileHashes() can get a list of file names, and
-        * getDefinitionSummary() can get config vars and callback results in their expanded form.
+        * What it does not do is reading the actual contents of any specified files, nor invoking
+        * the computation callbacks. Those things are done by getPackageFiles() instead to improve
+        * backend performance by only doing this work when the module response is needed, and not
+        * when merely computing the version hash for StartupModule, or when checking
+        * If-None-Match headers for a HTTP 304 response.
         *
         * @param ResourceLoaderContext $context
         * @return array|null
+        * @throws MWException If the 'packageFiles' definition is invalid.
         */
        private function expandPackageFiles( ResourceLoaderContext $context ) {
                $hash = $context->getHash();
@@ -1113,19 +1140,32 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                }
                        }
 
+                       // Perform expansions (except 'file' and 'callback'), creating one of these keys:
+                       // - 'content': literal value.
+                       // - 'filePath': content to be read from a file.
+                       // - 'callback': content computed by a callable.
                        if ( isset( $fileInfo['content'] ) ) {
                                $expanded['content'] = $fileInfo['content'];
                        } elseif ( isset( $fileInfo['file'] ) ) {
                                $expanded['filePath'] = $fileInfo['file'];
                        } elseif ( isset( $fileInfo['callback'] ) ) {
-                               if ( is_callable( $fileInfo['callback'] ) ) {
-                                       $expanded['content'] = $fileInfo['callback']( $context );
-                               } else {
+                               if ( !is_callable( $fileInfo['callback'] ) ) {
                                        $msg = __METHOD__ . ": invalid callback for package file \"{$fileInfo['name']}\"" .
                                                " in module \"{$this->getName()}\"";
                                        wfDebugLog( 'resourceloader', $msg );
                                        throw new MWException( $msg );
                                }
+                               if ( isset( $fileInfo['versionCallback'] ) ) {
+                                       if ( !is_callable( $fileInfo['versionCallback'] ) ) {
+                                               throw new MWException( __METHOD__ . ": invalid versionCallback for file" .
+                                                       " \"{$fileInfo['name']}\" in module \"{$this->getName()}\"" );
+                                       }
+                                       $expanded['definitionSummary'] = ( $fileInfo['versionCallback'] )( $context );
+                                       // Don't invoke 'callback' here as it may be expensive (T223260).
+                                       $expanded['callback'] = $fileInfo['callback'];
+                               } else {
+                                       $expanded['content'] = ( $fileInfo['callback'] )( $context );
+                               }
                        } elseif ( isset( $fileInfo['config'] ) ) {
                                if ( $type !== 'data' ) {
                                        $msg = __METHOD__ . ": invalid use of \"config\" for package file \"{$fileInfo['name']}\" " .
@@ -1184,6 +1224,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 
                // Expand file contents
                foreach ( $expandedPackageFiles['files'] as &$fileInfo ) {
+                       // Turn any 'filePath' or 'callback' key into actual 'content',
+                       // and remove the key after that.
                        if ( isset( $fileInfo['filePath'] ) ) {
                                $localPath = $this->getLocalPath( $fileInfo['filePath'] );
                                if ( !file_exists( $localPath ) ) {
@@ -1198,7 +1240,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                }
                                $fileInfo['content'] = $content;
                                unset( $fileInfo['filePath'] );
+                       } elseif ( isset( $fileInfo['callback'] ) ) {
+                               $fileInfo['content'] = ( $fileInfo['callback'] )( $context );
+                               unset( $fileInfo['callback'] );
                        }
+
+                       // Not needed for client response, exists for getDefinitionSummary().
+                       unset( $fileInfo['definitionSummary'] );
                }
 
                return $expandedPackageFiles;
index 2e2da70..c1b3dc3 100644 (file)
@@ -146,6 +146,7 @@ class ResourceLoaderImage {
         *
         * @param ResourceLoaderContext $context Any context
         * @return string
+        * @throws MWException If no matching path is found
         */
        public function getPath( ResourceLoaderContext $context ) {
                $desc = $this->descriptor;
@@ -167,7 +168,11 @@ class ResourceLoaderImage {
                if ( isset( $desc[$context->getDirection()] ) ) {
                        return $this->basePath . '/' . $desc[$context->getDirection()];
                }
-               return $this->basePath . '/' . $desc['default'];
+               if ( isset( $desc['default'] ) ) {
+                       return $this->basePath . '/' . $desc['default'];
+               } else {
+                       throw new MWException( 'No matching path found' );
+               }
        }
 
        /**
index 9b50d80..90b18eb 100644 (file)
@@ -39,7 +39,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
 
        protected $origin = self::ORIGIN_CORE_SITEWIDE;
 
-       /** @var ResourceLoaderImage[]|null */
+       /** @var ResourceLoaderImage[][]|null */
        protected $imageObjects = null;
        /** @var array */
        protected $images = [];
@@ -113,7 +113,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         * @throws InvalidArgumentException
         */
        public function __construct( $options = [], $localBasePath = null ) {
-               $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
+               $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
 
                $this->definition = $options;
        }
index c4e517a..0269ec3 100644 (file)
@@ -33,7 +33,7 @@ class ResourceLoaderLessVarFileModule extends ResourceLoaderFileModule {
         *
         * @param string $blob
         * @param array $exclusions
-        * @return array $blob
+        * @return object $blob
         */
        protected function excludeMessagesFromBlob( $blob, $exclusions ) {
                $data = json_decode( $blob, true );
index 66a4edf..a7fee85 100644 (file)
@@ -146,10 +146,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                        if ( is_string( $deprecationInfo ) ) {
                                $warning .= "\n" . $deprecationInfo;
                        }
-                       return Xml::encodeJsCall(
-                               'mw.log.warn',
-                               [ $warning ]
-                       );
+                       return 'mw.log.warn(' . ResourceLoader::encodeJsonForScript( $warning ) . ');';
                } else {
                        return '';
                }
@@ -412,7 +409,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
         * @return array List of files
         */
        protected function getFileDependencies( ResourceLoaderContext $context ) {
-               $vary = $context->getSkin() . '|' . $context->getLanguage();
+               $vary = self::getVary( $context );
 
                // Try in-object cache first
                if ( !isset( $this->fileDeps[$vary] ) ) {
@@ -447,7 +444,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
         * @param string[] $files Array of file names
         */
        public function setFileDependencies( ResourceLoaderContext $context, $files ) {
-               $vary = $context->getSkin() . '|' . $context->getLanguage();
+               $vary = self::getVary( $context );
                $this->fileDeps[$vary] = $files;
        }
 
@@ -484,7 +481,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                        }
 
                        // The file deps list has changed, we want to update it.
-                       $vary = $context->getSkin() . '|' . $context->getLanguage();
+                       $vary = self::getVary( $context );
                        $cache = ObjectCache::getLocalClusterInstance();
                        $key = $cache->makeKey( __METHOD__, $this->getName(), $vary );
                        $scopeLock = $cache->getScopedLock( $key, 0 );
@@ -954,8 +951,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                return $cache->getWithSetCallback(
                        $cache->makeGlobalKey(
-                               'resourceloader',
-                               'jsparse',
+                               'resourceloader-jsparse',
                                self::$parseCacheVersion,
                                md5( $contents ),
                                $fileName
@@ -1021,4 +1017,18 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
        protected static function safeFileHash( $filePath ) {
                return FileContentsHasher::getFileContentsHash( $filePath );
        }
+
+       /**
+        * Get vary string.
+        *
+        * @internal For internal use only.
+        * @param ResourceLoaderContext $context
+        * @return string Vary string
+        */
+       public static function getVary( ResourceLoaderContext $context ) {
+               return implode( '|', [
+                       $context->getSkin(),
+                       $context->getLanguage(),
+               ] );
+       }
 }
index 0c70ee1..c860362 100644 (file)
@@ -69,4 +69,13 @@ class ResourceLoaderOOUIIconPackModule extends ResourceLoaderOOUIImageModule {
 
                return $definition;
        }
+
+       public static function extractLocalBasePath( $options, $localBasePath = null ) {
+               global $IP;
+               if ( $localBasePath === null ) {
+                       $localBasePath = $IP;
+               }
+               // Ignore any 'localBasePath' present in $options, this always refers to files in MediaWiki core
+               return $localBasePath;
+       }
 }
index d6cc646..b90b618 100644 (file)
@@ -124,29 +124,50 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
         *
         * @param array $registryData
         * @param string $moduleName
+        * @param string[] $handled Internal parameter for recursion. (Optional)
         * @return array
+        * @throws ResourceLoaderCircularDependencyError
         */
-       protected static function getImplicitDependencies( array $registryData, $moduleName ) {
+       protected static function getImplicitDependencies(
+               array $registryData,
+               $moduleName,
+               array $handled = []
+       ) {
                static $dependencyCache = [];
 
-               // The list of implicit dependencies won't be altered, so we can
-               // cache them without having to worry.
+               // No modules will be added or changed server-side after this point,
+               // so we can safely cache parts of the tree for re-use.
                if ( !isset( $dependencyCache[$moduleName] ) ) {
                        if ( !isset( $registryData[$moduleName] ) ) {
-                               // Dependencies may not exist
-                               $dependencyCache[$moduleName] = [];
+                               // Unknown module names are allowed here, this is only an optimisation.
+                               // Checks for illegal and unknown dependencies happen as PHPUnit structure tests,
+                               // and also client-side at run-time.
+                               $flat = [];
                        } else {
                                $data = $registryData[$moduleName];
-                               $dependencyCache[$moduleName] = $data['dependencies'];
+                               $flat = $data['dependencies'];
 
+                               // Prevent recursion
+                               $handled[] = $moduleName;
                                foreach ( $data['dependencies'] as $dependency ) {
-                                       // Recursively get the dependencies of the dependencies
-                                       $dependencyCache[$moduleName] = array_merge(
-                                               $dependencyCache[$moduleName],
-                                               self::getImplicitDependencies( $registryData, $dependency )
-                                       );
+                                       if ( in_array( $dependency, $handled, true ) ) {
+                                               // If we encounter a circular dependency, then stop the optimiser and leave the
+                                               // original dependencies array unmodified. Circular dependencies are not
+                                               // supported in ResourceLoader. Awareness of them exists here so that we can
+                                               // optimise the registry when it isn't broken, and otherwise transport the
+                                               // registry unchanged. The client will handle this further.
+                                               throw new ResourceLoaderCircularDependencyError();
+                                       } else {
+                                               // Recursively add the dependencies of the dependencies
+                                               $flat = array_merge(
+                                                       $flat,
+                                                       self::getImplicitDependencies( $registryData, $dependency, $handled )
+                                               );
+                                       }
                                }
                        }
+
+                       $dependencyCache[$moduleName] = $flat;
                }
 
                return $dependencyCache[$moduleName];
@@ -173,10 +194,16 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
        public static function compileUnresolvedDependencies( array &$registryData ) {
                foreach ( $registryData as $name => &$data ) {
                        $dependencies = $data['dependencies'];
-                       foreach ( $data['dependencies'] as $dependency ) {
-                               $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
-                               $dependencies = array_diff( $dependencies, $implicitDependencies );
+                       try {
+                               foreach ( $data['dependencies'] as $dependency ) {
+                                       $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
+                                       $dependencies = array_diff( $dependencies, $implicitDependencies );
+                               }
+                       } catch ( ResourceLoaderCircularDependencyError $err ) {
+                               // Leave unchanged
+                               $dependencies = $data['dependencies'];
                        }
+
                        // Rebuild keys
                        $data['dependencies'] = array_values( $dependencies );
                }
@@ -258,7 +285,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        }
 
                        if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
-                               $context->getLogger()->warning(
+                               $this->getLogger()->warning(
                                        "Module '{module}' produced an invalid version hash: '{version}'.",
                                        [
                                                'module' => $name,
@@ -325,6 +352,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
         * @private For internal use by SpecialJavaScriptTest
         * @since 1.32
         * @return array
+        * @codeCoverageIgnore
         */
        public function getBaseModulesInternal() {
                return $this->getBaseModules();
@@ -425,11 +453,4 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                // and hash it to determine the version (as used by E-Tag HTTP response header).
                return true;
        }
-
-       /**
-        * @return string
-        */
-       public function getGroup() {
-               return 'startup';
-       }
 }
index 4c11fce..d37c31b 100644 (file)
@@ -484,7 +484,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
 
                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $allInfo = $cache->getWithSetCallback(
-                       $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getDomainID(), $hash ),
+                       $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID(), $hash ),
                        $cache::TTL_HOUR,
                        function ( $curVal, &$ttl, array &$setOpts ) use ( $func, $pageNames, $db, $fname ) {
                                $setOpts += Database::getCacheSetOptions( $db );
@@ -493,7 +493,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                        },
                        [
                                'checkKeys' => [
-                                       $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getDomainID() ) ]
+                                       $cache->makeGlobalKey( 'resourceloader-titleinfo', $db->getDomainID() ) ]
                        ]
                );
 
@@ -550,7 +550,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
 
                if ( $purge ) {
                        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-                       $key = $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $domain );
+                       $key = $cache->makeGlobalKey( 'resourceloader-titleinfo', $domain );
                        $cache->touchCheckKey( $key );
                }
        }
index 3b7a0a9..3fff6c1 100644 (file)
@@ -30,22 +30,6 @@ use MediaWiki\MediaWikiServices;
  * @ingroup Search
  */
 abstract class PrefixSearch {
-       /**
-        * Do a prefix search of titles and return a list of matching page names.
-        * @deprecated Since 1.23, use TitlePrefixSearch or StringPrefixSearch classes
-        *
-        * @param string $search
-        * @param int $limit
-        * @param array $namespaces Used if query is not explicitly prefixed
-        * @param int $offset How many results to offset from the beginning
-        * @return array Array of strings
-        */
-       public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) {
-               wfDeprecated( __METHOD__, '1.23' );
-               $prefixSearch = new StringPrefixSearch;
-               return $prefixSearch->search( $search, $limit, $namespaces, $offset );
-       }
-
        /**
         * Do a prefix search of titles and return a list of matching page names.
         *
index 9771e88..fa6e7fd 100644 (file)
@@ -46,7 +46,7 @@ abstract class SearchEngine {
        /** @var int */
        protected $offset = 0;
 
-       /** @var array|string */
+       /** @var string[] */
        protected $searchTerms = [];
 
        /** @var bool */
@@ -106,7 +106,7 @@ abstract class SearchEngine {
         * be converted to final in 1.34. Override self::doSearchArchiveTitle().
         *
         * @param string $term Raw search term
-        * @return Status<Title[]>
+        * @return Status
         * @since 1.29
         */
        public function searchArchiveTitle( $term ) {
@@ -117,7 +117,7 @@ abstract class SearchEngine {
         * Perform a title search in the article archive.
         *
         * @param string $term Raw search term
-        * @return Status<Title[]>
+        * @return Status
         * @since 1.32
         */
        protected function doSearchArchiveTitle( $term ) {
index 469502f..6c01f79 100644 (file)
@@ -44,7 +44,7 @@ class SearchHighlighter {
         * Wikitext highlighting when $wgAdvancedSearchHighlighting = true
         *
         * @param string $text
-        * @param array $terms Terms to highlight (not html escaped but
+        * @param string[] $terms Terms to highlight (not html escaped but
         *   regex escaped via SearchDatabase::regexTerm())
         * @param int $contextlines
         * @param int $contextchars
@@ -502,7 +502,7 @@ class SearchHighlighter {
         * Used when $wgAdvancedSearchHighlighting is false.
         *
         * @param string $text
-        * @param array $terms Escaped for regex by SearchDatabase::regexTerm()
+        * @param string[] $terms Escaped for regex by SearchDatabase::regexTerm()
         * @param int $contextlines
         * @param int $contextchars
         * @return string
index 7e51432..a27d719 100644 (file)
@@ -147,7 +147,7 @@ class SearchResult {
        }
 
        /**
-        * @param array $terms Terms to highlight
+        * @param string[] $terms Terms to highlight
         * @return string Highlighted text snippet, null (and not '') if not supported
         */
        function getTextSnippet( $terms ) {
index 3d3b446..92e2a17 100644 (file)
@@ -95,7 +95,7 @@ class SearchResultSet implements Countable, IteratorAggregate {
         * the search terms as parsed by this engine in a text extract.
         * STUB
         *
-        * @return array
+        * @return string[]
         */
        function termMatches() {
                return [];
index 022dc0a..f4e4a23 100644 (file)
@@ -1,20 +1,22 @@
 <?php
 
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 
 /**
  * This class is used for different SQL-based search engines shipped with MediaWiki
  * @ingroup Search
  */
 class SqlSearchResultSet extends SearchResultSet {
-       /** @var ResultWrapper Result object from database */
+       /** @noinspection PhpMissingParentConstructorInspection */
+
+       /** @var IResultWrapper Result object from database */
        protected $resultSet;
-       /** @var string Requested search query */
+       /** @var string[] Requested search query */
        protected $terms;
        /** @var int|null Total number of hits for $terms */
        protected $totalHits;
 
-       function __construct( ResultWrapper $resultSet, $terms, $total = null ) {
+       function __construct( IResultWrapper $resultSet, $terms, $total = null ) {
                $this->resultSet = $resultSet;
                $this->terms = $terms;
                $this->totalHits = $total;
@@ -51,7 +53,7 @@ class SqlSearchResultSet extends SearchResultSet {
 
        function free() {
                if ( $this->resultSet === false ) {
-                       return false;
+                       return;
                }
 
                $this->resultSet->free();
index def3bc3..80a400b 100644 (file)
@@ -402,6 +402,9 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         * }
         * @endcode
         *
+        * Note that the $options parameter to addVaryHeader has been deprecated
+        * since 1.34, and should be `null` or an empty array.
+        *
         * @protected For use by \MediaWiki\Session\SessionManager only
         * @return array
         */
index 20b9445..7742075 100644 (file)
@@ -73,14 +73,14 @@ class Command {
        private $cgroup = false;
 
        /**
-        * bitfield with restrictions
+        * Bitfield with restrictions
         *
         * @var int
         */
        protected $restrictions = 0;
 
        /**
-        * Constructor. Don't call directly, instead use Shell::command()
+        * Don't call directly, instead use Shell::command()
         *
         * @throws ShellDisabledError
         */
@@ -93,7 +93,7 @@ class Command {
        }
 
        /**
-        * Destructor. Makes sure programmer didn't forget to execute the command after all
+        * Makes sure the programmer didn't forget to execute the command after all
         */
        public function __destruct() {
                if ( !$this->everExecuted ) {
index f45596f..05b6297 100644 (file)
@@ -389,9 +389,8 @@ abstract class Skin extends ContextSource {
 
        /**
         * Outputs the HTML generated by other functions.
-        * @param OutputPage|null $out
         */
-       abstract function outputPage( OutputPage $out = null );
+       abstract function outputPage();
 
        /**
         * @param array $data
index eb71fe6..98d3456 100644 (file)
@@ -21,8 +21,6 @@
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Factory class to create Skin objects
  *
@@ -43,15 +41,6 @@ class SkinFactory {
         */
        private $displayNames = [];
 
-       /**
-        * @deprecated in 1.27
-        * @return SkinFactory
-        */
-       public static function getDefaultInstance() {
-               wfDeprecated( __METHOD__, '1.27' );
-               return MediaWikiServices::getInstance()->getSkinFactory();
-       }
-
        /**
         * Register a new Skin factory function.
         *
index a7b7569..8b46ee9 100644 (file)
@@ -207,21 +207,10 @@ class SkinTemplate extends Skin {
        }
 
        /**
-        * initialize various variables and generate the template
-        *
-        * @param OutputPage|null $out
+        * Initialize various variables and generate the template
         */
-       function outputPage( OutputPage $out = null ) {
+       function outputPage() {
                Profiler::instance()->setTemplated( true );
-
-               $oldContext = null;
-               if ( $out !== null ) {
-                       // Deprecated since 1.20, note added in 1.25
-                       wfDeprecated( __METHOD__, '1.25' );
-                       $oldContext = $this->getContext();
-                       $this->setContext( $out->getContext() );
-               }
-
                $out = $this->getOutput();
 
                $this->initPage( $out );
@@ -231,10 +220,6 @@ class SkinTemplate extends Skin {
 
                // result may be an error
                $this->printOrError( $res );
-
-               if ( $oldContext ) {
-                       $this->setContext( $oldContext );
-               }
        }
 
        /**
index 700672f..eb179bf 100644 (file)
@@ -660,7 +660,7 @@ abstract class QueryPage extends SpecialPage {
                # an OutputPage, and let them get on with it
                $this->outputResults( $out,
                        $this->getSkin(),
-                       $dbr, # Should use ResultWrapper for this
+                       $dbr, # Should use IResultWrapper for this
                        $res,
                        min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
                        $this->offset );
@@ -738,13 +738,13 @@ abstract class QueryPage extends SpecialPage {
        }
 
        /**
-        * Creates a new LinkBatch object, adds all pages from the passed ResultWrapper (MUST include
+        * Creates a new LinkBatch object, adds all pages from the passed result wrapper (MUST include
         * title and optional the namespace field) and executes the batch. This operation will pre-cache
         * LinkCache information like page existence and information for stub color and redirect hints.
         *
-        * @param IResultWrapper $res The ResultWrapper object to process. Needs to include the title
+        * @param IResultWrapper $res The result wrapper to process. Needs to include the title
         *  field and namespace field, if the $ns parameter isn't set.
-        * @param null $ns Use this namespace for the given titles in the ResultWrapper object,
+        * @param null $ns Use this namespace for the given titles in the result wrapper,
         *  instead of the namespace value of $res.
         */
        protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) {
index eba406e..d7e39d5 100644 (file)
@@ -456,10 +456,10 @@ class SpecialPage implements MessageLocalizer {
         * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo,
         * etc.):
         *
-        *   - `prefixSearchSubpages( "ba" )` should return `array( "bar", "baz" )`
-        *   - `prefixSearchSubpages( "f" )` should return `array( "foo" )`
-        *   - `prefixSearchSubpages( "z" )` should return `array()`
-        *   - `prefixSearchSubpages( "" )` should return `array( foo", "bar", "baz" )`
+        *   - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]`
+        *   - `prefixSearchSubpages( "f" )` should return `[ "foo" ]`
+        *   - `prefixSearchSubpages( "z" )` should return `[]`
+        *   - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]`
         *
         * @param string $search Prefix to search for
         * @param int $limit Maximum number of results to return (usually 10)
index 94900d4..40172ab 100644 (file)
@@ -367,7 +367,7 @@ class SpecialPageFactory {
         * subpage.
         *
         * @param string $alias
-        * @return array Array( String, String|null ), or array( null, null ) if the page is invalid
+        * @return array [ String, String|null ], or [ null, null ] if the page is invalid
         */
        public function resolveAlias( $alias ) {
                $bits = explode( '/', $alias, 2 );
index 5b07f22..122fa9b 100644 (file)
@@ -166,14 +166,10 @@ class SpecialEmailUser extends UnlistedSpecialPage {
         * Validate target User
         *
         * @param string $target Target user name
-        * @param User|null $sender User sending the email
+        * @param User $sender User sending the email
         * @return User|string User object on success or a string on error
         */
-       public static function getTarget( $target, User $sender = null ) {
-               if ( $sender === null ) {
-                       wfDeprecated( __METHOD__ . ' without specifying the sending user', '1.30' );
-               }
-
+       public static function getTarget( $target, User $sender ) {
                if ( $target == '' ) {
                        wfDebug( "Target is empty.\n" );
 
@@ -190,15 +186,11 @@ class SpecialEmailUser extends UnlistedSpecialPage {
         * Validate target User
         *
         * @param User $target Target user
-        * @param User|null $sender User sending the email
+        * @param User $sender User sending the email
         * @return string Error message or empty string if valid.
         * @since 1.30
         */
-       public static function validateTarget( $target, User $sender = null ) {
-               if ( $sender === null ) {
-                       wfDeprecated( __METHOD__ . ' without specifying the sending user', '1.30' );
-               }
-
+       public static function validateTarget( $target, User $sender ) {
                if ( !$target instanceof User || !$target->getId() ) {
                        wfDebug( "Target is invalid user.\n" );
 
@@ -217,25 +209,21 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                        return 'nowikiemail';
                }
 
-               if ( $sender !== null && !$target->getOption( 'email-allow-new-users' ) &&
-                       $sender->isNewbie()
-               ) {
+               if ( !$target->getOption( 'email-allow-new-users' ) && $sender->isNewbie() ) {
                        wfDebug( "User does not allow user emails from new users.\n" );
 
                        return 'nowikiemail';
                }
 
-               if ( $sender !== null ) {
-                       $blacklist = $target->getOption( 'email-blacklist', '' );
-                       if ( $blacklist ) {
-                               $blacklist = MultiUsernameFilter::splitIds( $blacklist );
-                               $lookup = CentralIdLookup::factory();
-                               $senderId = $lookup->centralIdFromLocalUser( $sender );
-                               if ( $senderId !== 0 && in_array( $senderId, $blacklist ) ) {
-                                       wfDebug( "User does not allow user emails from this user.\n" );
+               $blacklist = $target->getOption( 'email-blacklist', '' );
+               if ( $blacklist ) {
+                       $blacklist = MultiUsernameFilter::splitIds( $blacklist );
+                       $lookup = CentralIdLookup::factory();
+                       $senderId = $lookup->centralIdFromLocalUser( $sender );
+                       if ( $senderId !== 0 && in_array( $senderId, $blacklist ) ) {
+                               wfDebug( "User does not allow user emails from this user.\n" );
 
-                                       return 'nowikiemail';
-                               }
+                               return 'nowikiemail';
                        }
                }
 
index ef61ac5..5a63581 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * A special page that allows users to export pages in a XML file
@@ -387,6 +388,8 @@ class SpecialExport extends SpecialPage {
                if ( $exportall ) {
                        $exporter->allPages();
                } else {
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                        foreach ( $pages as $page ) {
                                # T10824: Only export pages the user can read
                                $title = Title::newFromText( $page );
@@ -395,7 +398,7 @@ class SpecialExport extends SpecialPage {
                                        continue;
                                }
 
-                               if ( !$title->userCan( 'read', $this->getUser() ) ) {
+                               if ( !$permissionManager->userCan( 'read', $this->getUser(), $title ) ) {
                                        // @todo Perhaps output an <error> tag or something.
                                        continue;
                                }
index c984af8..3e9676c 100644 (file)
@@ -174,7 +174,7 @@ JAVASCRIPT
                // load before qunit/export.
                $scripts = $out->makeResourceLoaderLink( 'jquery.qunit',
                        ResourceLoaderModule::TYPE_SCRIPTS,
-                       [ 'raw' => true, 'sync' => true ]
+                       [ 'raw' => '1', 'sync' => '1' ]
                );
 
                $head = implode( "\n", [ $styles, $scripts ] );
index 15b7c63..252df5b 100644 (file)
@@ -597,7 +597,15 @@ class MovePageForm extends UnlistedSpecialPage {
                # Do the actual move.
                $mp = new MovePage( $ot, $nt );
 
+               # check whether the requested actions are permitted / possible
                $userPermitted = $mp->checkPermissions( $user, $this->reason )->isOK();
+               if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
+                       $this->moveTalk = false;
+               }
+               if ( $this->moveSubpages ) {
+                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+                       $this->moveSubpages = $permissionManager->userCan( 'move-subpages', $user, $ot );
+               }
 
                $status = $mp->moveIfAllowed( $user, $this->reason, $createRedirect );
                if ( !$status->isOK() ) {
@@ -646,19 +654,11 @@ class MovePageForm extends UnlistedSpecialPage {
                $movePage = $this;
                Hooks::run( 'SpecialMovepageAfterMove', [ &$movePage, &$ot, &$nt ] );
 
-               # Now we move extra pages we've been asked to move: subpages and talk
-               # pages.  First, if the old page or the new page is a talk page, we
-               # can't move any talk pages: cancel that.
-               if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
-                       $this->moveTalk = false;
-               }
-
-               if ( count( $ot->getUserPermissionsErrors( 'move-subpages', $user ) ) ) {
-                       $this->moveSubpages = false;
-               }
-
-               /**
-                * Next make a list of id's.  This might be marginally less efficient
+               /*
+                * Now we move extra pages we've been asked to move: subpages and talk
+                * pages.
+                *
+                * First, make a list of id's.  This might be marginally less efficient
                 * than a more direct method, but this is not a highly performance-cri-
                 * tical code path and readable code is more important here.
                 *
index 1b8ba85..04db704 100644 (file)
@@ -160,6 +160,8 @@ class SpecialNewpages extends IncludableSpecialPage {
                                $navigation = $pager->getNavigationBar();
                        }
                        $out->addHTML( $navigation . $pager->getBody() . $navigation );
+                       // Add styles for change tags
+                       $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
                } else {
                        $out->addWikiMsg( 'specialpage-empty' );
                }
index bedd2c5..c7e2a37 100644 (file)
@@ -162,7 +162,7 @@ class SpecialUnblock extends SpecialPage {
         * Submit callback for an HTMLForm object
         * @param array $data
         * @param HTMLForm $form
-        * @return array|bool Array(message key, parameters)
+        * @return array|bool [ message key, parameters ]
         */
        public static function processUIUnblock( array $data, HTMLForm $form ) {
                return self::processUnblock( $data, $form->getContext() );
@@ -177,7 +177,7 @@ class SpecialUnblock extends SpecialPage {
         * @param array $data
         * @param IContextSource $context
         * @throws ErrorPageError
-        * @return array|bool Array( Array( message key, parameters ) ) on failure, True on success
+        * @return array|bool [ [ message key, parameters ] ] on failure, True on success
         */
        public static function processUnblock( array $data, IContextSource $context ) {
                $performer = $context->getUser();
index 456face..05c622a 100644 (file)
@@ -138,8 +138,10 @@ class SpecialUndelete extends SpecialPage {
         */
        protected function isAllowed( $permission, User $user = null ) {
                $user = $user ?: $this->getUser();
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                if ( $this->mTargetObj !== null ) {
-                       return $this->mTargetObj->userCan( $permission, $user );
+                       return $permissionManager->userCan( $permission, $user, $this->mTargetObj );
                } else {
                        return $user->isAllowed( $permission );
                }
index 87bc259..1c87f7a 100644 (file)
@@ -405,8 +405,6 @@ class UserrightsPage extends SpecialPage {
                wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" );
                wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) . "\n" );
                wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) . "\n" );
-               // Deprecated in favor of UserGroupsChanged hook
-               Hooks::run( 'UserRights', [ &$user, $add, $remove ], '1.26' );
 
                // Only add a log entry if something actually changed
                if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
@@ -1001,12 +999,12 @@ class UserrightsPage extends SpecialPage {
        /**
         * Returns $this->getUser()->changeableGroups()
         *
-        * @return array Array(
-        *   'add' => array( addablegroups ),
-        *   'remove' => array( removablegroups ),
-        *   'add-self' => array( addablegroups to self ),
-        *   'remove-self' => array( removable groups from self )
-        *  )
+        * @return array [
+        *   'add' => [ addablegroups ],
+        *   'remove' => [ removablegroups ],
+        *   'add-self' => [ addablegroups to self ],
+        *   'remove-self' => [ removable groups from self ]
+        *  ]
         */
        function changeableGroups() {
                return $this->getUser()->changeableGroups();
index 0c4959a..5456ce7 100644 (file)
@@ -812,7 +812,7 @@ class SpecialVersion extends SpecialPage {
                }
 
                // ... and generate the description; which can be a parameterized l10n message
-               // in the form array( <msgname>, <parameter>, <parameter>... ) or just a straight
+               // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
                // up string
                if ( isset( $extension['descriptionmsg'] ) ) {
                        // Localized description of extension
index 1d29efb..2d3b6b2 100644 (file)
@@ -471,14 +471,16 @@ class ImageListPager extends TablePager {
                                        );
                                        $download = Xml::element(
                                                'a',
-                                               [ 'href' => $services->getRepoGroup()->findFile( $filePage )->getUrl() ],
+                                               [ 'href' => $services->getRepoGroup()->getLocalRepo()->newFile( $filePage )->getUrl() ],
                                                $imgfile
                                        );
                                        $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
 
                                        // Add delete links if allowed
                                        // From https://github.com/Wikia/app/pull/3859
-                                       if ( $filePage->userCan( 'delete', $this->getUser() ) ) {
+                                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+                                       if ( $permissionManager->userCan( 'delete', $this->getUser(), $filePage ) ) {
                                                $deleteMsg = $this->msg( 'listfiles-delete' )->text();
 
                                                $delete = $linkRenderer->makeKnownLink(
index d39975d..215bd20 100644 (file)
@@ -433,7 +433,7 @@ class UploadStash {
         * List all files in the stash.
         *
         * @throws UploadStashNotLoggedInException
-        * @return array
+        * @return array|false
         */
        public function listFiles() {
                if ( !$this->isLoggedIn ) {
index 6db219d..df5edef 100644 (file)
@@ -21,7 +21,7 @@
 use MediaWiki\Auth\AuthenticationResponse;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\BotPasswordSessionProvider;
-use Wikimedia\Rdbms\IMaintainableDatabase;
+use Wikimedia\Rdbms\IDatabase;
 
 /**
  * Utility class for bot passwords
@@ -71,7 +71,7 @@ class BotPassword implements IDBAccessObject {
        /**
         * Get a database connection for the bot passwords database
         * @param int $db Index of the connection to get, e.g. DB_MASTER or DB_REPLICA.
-        * @return IMaintainableDatabase
+        * @return IDatabase
         */
        public static function getDB( $db ) {
                global $wgBotPasswordsCluster, $wgBotPasswordsDatabase;
index bdcb17b..6025d3c 100644 (file)
@@ -950,12 +950,12 @@ class User implements IDBAccessObject, UserIdentity {
                        $result = (int)$s->user_id;
                }
 
-               self::$idCacheByName[$name] = $result;
-
-               if ( count( self::$idCacheByName ) > 1000 ) {
+               if ( count( self::$idCacheByName ) >= 1000 ) {
                        self::$idCacheByName = [];
                }
 
+               self::$idCacheByName[$name] = $result;
+
                return $result;
        }
 
@@ -1346,13 +1346,6 @@ class User implements IDBAccessObject, UserIdentity {
         * @return bool True if the user is logged in, false otherwise.
         */
        private function loadFromSession() {
-               // Deprecated hook
-               $result = null;
-               Hooks::run( 'UserLoadFromSession', [ $this, &$result ], '1.27' );
-               if ( $result !== null ) {
-                       return $result;
-               }
-
                // MediaWiki\Session\Session already did the necessary authentication of the user
                // returned here, so just use it if applicable.
                $session = $this->getRequest()->getSession();
@@ -1822,8 +1815,7 @@ class User implements IDBAccessObject, UserIdentity {
                        $fromReplica
                );
 
-               if ( $block instanceof AbstractBlock ) {
-                       wfDebug( __METHOD__ . ": Found block.\n" );
+               if ( $block ) {
                        $this->mBlock = $block;
                        $this->mBlockedby = $block->getByName();
                        $this->mBlockreason = $block->getReason();
@@ -3298,7 +3290,7 @@ class User implements IDBAccessObject, UserIdentity {
         * and 'all', which forces a reset of *all* preferences and overrides everything else.
         *
         * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
-        *  array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
+        *  [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ]
         *  for backwards-compatibility.
         * @param IContextSource|null $context Context source used when $resetKinds
         *  does not contain 'all', passed to getOptionKinds().
@@ -5045,10 +5037,10 @@ class User implements IDBAccessObject, UserIdentity {
         * Returns an array of the groups that a particular group can add/remove.
         *
         * @param string $group The group to check for whether it can add/remove
-        * @return array Array( 'add' => array( addablegroups ),
-        *     'remove' => array( removablegroups ),
-        *     'add-self' => array( addablegroups to self),
-        *     'remove-self' => array( removable groups from self) )
+        * @return array [ 'add' => [ addablegroups ],
+        *     'remove' => [ removablegroups ],
+        *     'add-self' => [ addablegroups to self ],
+        *     'remove-self' => [ removable groups from self ] ]
         */
        public static function changeableByGroup( $group ) {
                global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
@@ -5118,10 +5110,10 @@ class User implements IDBAccessObject, UserIdentity {
 
        /**
         * Returns an array of groups that this user can add and remove
-        * @return array Array( 'add' => array( addablegroups ),
-        *  'remove' => array( removablegroups ),
-        *  'add-self' => array( addablegroups to self),
-        *  'remove-self' => array( removable groups from self) )
+        * @return array [ 'add' => [ addablegroups ],
+        *  'remove' => [ removablegroups ],
+        *  'add-self' => [ addablegroups to self ],
+        *  'remove-self' => [ removable groups from self ] ]
         */
        public function changeableGroups() {
                if ( $this->isAllowed( 'userrights' ) ) {
index c987354..12b8a70 100644 (file)
@@ -59,6 +59,31 @@ class ClassCollector {
                $this->alias = null;
                $this->tokens = [];
 
+               // HACK: The PHP tokenizer is slow (T225730).
+               // Speed it up by reducing the input to the three kinds of statement we care about:
+               // - namespace X;
+               // - [final] [abstract] class X … {}
+               // - class_alias( … );
+               $lines = [];
+               $matches = null;
+               preg_match_all(
+                       // phpcs:ignore Generic.Files.LineLength.TooLong
+                       '#^\t*(?:namespace |(final )?(abstract )?(class|interface|trait) |class_alias\()[^;{]+[;{]\s*\}?#m',
+                       $code,
+                       $matches
+               );
+               if ( isset( $matches[0][0] ) ) {
+                       foreach ( $matches[0] as $match ) {
+                               $match = trim( $match );
+                               if ( substr( $match, -1 ) === '{' ) {
+                                       // Keep it balanced
+                                       $match .= '}';
+                               }
+                               $lines[] = $match;
+                       }
+               }
+               $code = '<?php ' . implode( "\n", $lines ) . "\n";
+
                foreach ( token_get_all( $code ) as $token ) {
                        if ( $this->startToken === null ) {
                                $this->tryBeginExpect( $token );
index d700570..f648535 100644 (file)
@@ -31,7 +31,7 @@ class FullSearchResultWidget implements SearchResultWidget {
 
        /**
         * @param SearchResult $result The result to render
-        * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+        * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
         * @param int $position The result position, including offset
         * @return string HTML
         */
@@ -48,7 +48,10 @@ class FullSearchResultWidget implements SearchResultWidget {
                // This is not quite safe, but better than showing excerpts from
                // non-readable pages. Note that hiding the entry entirely would
                // screw up paging (really?).
-               if ( !$result->getTitle()->userCan( 'read', $this->specialPage->getUser() ) ) {
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+               if ( !$permissionManager->userCan(
+                       'read', $this->specialPage->getUser(), $result->getTitle()
+               ) ) {
                        return "<li>{$link}</li>";
                }
 
@@ -118,7 +121,7 @@ class FullSearchResultWidget implements SearchResultWidget {
         * title with highlighted words).
         *
         * @param SearchResult $result
-        * @param string $terms
+        * @param string[] $terms
         * @param int $position
         * @return string HTML
         */
index 095c30a..745bc12 100644 (file)
@@ -24,7 +24,7 @@ class InterwikiSearchResultWidget implements SearchResultWidget {
 
        /**
         * @param SearchResult $result The result to render
-        * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+        * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
         * @param int $position The result position, including offset
         * @return string HTML
         */
index 3fbdbef..4f0a271 100644 (file)
@@ -10,7 +10,7 @@ use SearchResult;
 interface SearchResultWidget {
        /**
         * @param SearchResult $result The result to render
-        * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+        * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
         * @param int $position The zero indexed result position, including offset
         * @return string HTML
         */
index 552cbaf..86a04b1 100644 (file)
@@ -26,7 +26,7 @@ class SimpleSearchResultWidget implements SearchResultWidget {
 
        /**
         * @param SearchResult $result The result to render
-        * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
+        * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
         * @param int $position The result position, including offset
         * @return string HTML
         */
index 4e663c2..fd8aedf 100644 (file)
@@ -2967,8 +2967,8 @@ class Language {
        }
 
        /**
-        * @param array $termsArray
-        * @return array
+        * @param string[] $termsArray
+        * @return string[]
         */
        function convertForSearchResult( $termsArray ) {
                # some languages, e.g. Chinese, need to do a conversion
@@ -4537,7 +4537,7 @@ class Language {
         *
         * @since 1.22
         * @param string $code Language code
-        * @return array Array( fallbacks, site fallbacks )
+        * @return array [ fallbacks, site fallbacks ]
         */
        public static function getFallbacksIncludingSiteLanguage( $code ) {
                global $wgLanguageCode;
diff --git a/languages/LanguageCode.php b/languages/LanguageCode.php
deleted file mode 100644 (file)
index 7d954d3..0000000
+++ /dev/null
@@ -1,204 +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 Language
- */
-
-/**
- * Methods for dealing with language codes.
- * @todo Move some of the code-related static methods out of Language into this class
- *
- * @since 1.29
- * @ingroup Language
- */
-class LanguageCode {
-       /**
-        * Mapping of deprecated language codes that were used in previous
-        * versions of MediaWiki to up-to-date, current language codes.
-        * These may or may not be valid BCP 47 codes; they are included here
-        * because MediaWiki renamed these particular codes at some point.
-        *
-        * @var array Mapping from deprecated MediaWiki-internal language code
-        *   to replacement MediaWiki-internal language code.
-        *
-        * @since 1.30
-        * @see https://meta.wikimedia.org/wiki/Special_language_codes
-        */
-       private static $deprecatedLanguageCodeMapping = [
-               // Note that als is actually a valid ISO 639 code (Tosk Albanian), but it
-               // was previously used in MediaWiki for Alsatian, which comes under gsw
-               'als' => 'gsw', // T25215
-               'bat-smg' => 'sgs', // T27522
-               'be-x-old' => 'be-tarask', // T11823
-               'fiu-vro' => 'vro', // T31186
-               'roa-rup' => 'rup', // T17988
-               'zh-classical' => 'lzh', // T30443
-               'zh-min-nan' => 'nan', // T30442
-               'zh-yue' => 'yue', // T30441
-       ];
-
-       /**
-        * Mapping of non-standard language codes used in MediaWiki to
-        * standardized BCP 47 codes.  These are not deprecated (yet?):
-        * IANA may eventually recognize the subtag, in which case the `-x-`
-        * infix could be removed, or else we could rename the code in
-        * MediaWiki, in which case they'd move up to the above mapping
-        * of deprecated codes.
-        *
-        * As a rule, we preserve all distinctions made by MediaWiki
-        * internally.  For example, `de-formal` becomes `de-x-formal`
-        * instead of just `de` because MediaWiki distinguishes `de-formal`
-        * from `de` (for example, for interface translations).  Similarly,
-        * BCP 47 indicates that `kk-Cyrl` SHOULD not be used because it
-        * "typically does not add information", but in our case MediaWiki
-        * LanguageConverter distinguishes `kk` (render content in a mix of
-        * Kurdish variants) from `kk-Cyrl` (convert content to be uniformly
-        * Cyrillic).  As the BCP 47 requirement is a SHOULD not a MUST,
-        * `kk-Cyrl` is a valid code, although some validators may emit
-        * a warning note.
-        *
-        * @var array Mapping from nonstandard MediaWiki-internal codes to
-        *   BCP 47 codes
-        *
-        * @since 1.32
-        * @see https://meta.wikimedia.org/wiki/Special_language_codes
-        * @see https://phabricator.wikimedia.org/T125073
-        */
-       private static $nonstandardLanguageCodeMapping = [
-               // All codes returned by Language::fetchLanguageNames() validated
-               // against IANA registry at
-               //   https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
-               // with help of validator at
-               //   http://schneegans.de/lv/
-               'cbk-zam' => 'cbk', // T124657
-               'de-formal' => 'de-x-formal',
-               'eml' => 'egl', // T36217
-               'en-rtl' => 'en-x-rtl',
-               'es-formal' => 'es-x-formal',
-               'hu-formal' => 'hu-x-formal',
-               'map-bms' => 'jv-x-bms', // [[en:Banyumasan_dialect]] T125073
-               'mo' => 'ro-Cyrl-MD', // T125073
-               'nrm' => 'nrf', // [[en:Norman_language]] T25216
-               'nl-informal' => 'nl-x-informal',
-               'roa-tara' => 'nap-x-tara', // [[en:Tarantino_dialect]]
-               'simple' => 'en-simple',
-               'sr-ec' => 'sr-Cyrl', // T117845
-               'sr-el' => 'sr-Latn', // T117845
-
-               // Although these next codes aren't *wrong* per se, including
-               // both the script and the country code helps compatibility with
-               // other BCP 47 users. Note that MW also uses `zh-Hans`/`zh-Hant`,
-               // without a country code, and those should be left alone.
-               // (See $variantfallbacks in LanguageZh.php for Hans/Hant id.)
-               'zh-cn' => 'zh-Hans-CN',
-               'zh-sg' => 'zh-Hans-SG',
-               'zh-my' => 'zh-Hans-MY',
-               'zh-tw' => 'zh-Hant-TW',
-               'zh-hk' => 'zh-Hant-HK',
-               'zh-mo' => 'zh-Hant-MO',
-       ];
-
-       /**
-        * Returns a mapping of deprecated language codes that were used in previous
-        * versions of MediaWiki to up-to-date, current language codes.
-        *
-        * This array is merged into $wgDummyLanguageCodes in Setup.php, along with
-        * the fake language codes 'qqq' and 'qqx', which are used internally by
-        * MediaWiki's localisation system.
-        *
-        * @return string[]
-        *
-        * @since 1.29
-        */
-       public static function getDeprecatedCodeMapping() {
-               return self::$deprecatedLanguageCodeMapping;
-       }
-
-       /**
-        * Returns a mapping of non-standard language codes used by
-        * (current and previous version of) MediaWiki, mapped to standard
-        * BCP 47 names.
-        *
-        * This array is exported to JavaScript to ensure
-        * mediawiki.language.bcp47 stays in sync with LanguageCode::bcp47().
-        *
-        * @return string[]
-        *
-        * @since 1.32
-        */
-       public static function getNonstandardLanguageCodeMapping() {
-               $result = [];
-               foreach ( self::$deprecatedLanguageCodeMapping as $code => $ignore ) {
-                       $result[$code] = self::bcp47( $code );
-               }
-               foreach ( self::$nonstandardLanguageCodeMapping as $code => $ignore ) {
-                       $result[$code] = self::bcp47( $code );
-               }
-               return $result;
-       }
-
-       /**
-        * Replace deprecated language codes that were used in previous
-        * versions of MediaWiki to up-to-date, current language codes.
-        * Other values will returned unchanged.
-        *
-        * @param string $code Old language code
-        * @return string New language code
-        *
-        * @since 1.30
-        */
-       public static function replaceDeprecatedCodes( $code ) {
-               return self::$deprecatedLanguageCodeMapping[$code] ?? $code;
-       }
-
-       /**
-        * Get the normalised IETF language tag
-        * See unit test for examples.
-        * See mediawiki.language.bcp47 for the JavaScript implementation.
-        *
-        * @param string $code The language code.
-        * @return string A language code complying with BCP 47 standards.
-        *
-        * @since 1.31
-        */
-       public static function bcp47( $code ) {
-               $code = self::replaceDeprecatedCodes( strtolower( $code ) );
-               if ( isset( self::$nonstandardLanguageCodeMapping[$code] ) ) {
-                       $code = self::$nonstandardLanguageCodeMapping[$code];
-               }
-               $codeSegment = explode( '-', $code );
-               $codeBCP = [];
-               foreach ( $codeSegment as $segNo => $seg ) {
-                       // when previous segment is x, it is a private segment and should be lc
-                       if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) {
-                               $codeBCP[$segNo] = strtolower( $seg );
-                       // ISO 3166 country code
-                       } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) {
-                               $codeBCP[$segNo] = strtoupper( $seg );
-                       // ISO 15924 script code
-                       } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) {
-                               $codeBCP[$segNo] = ucfirst( strtolower( $seg ) );
-                       // Use lowercase for other cases
-                       } else {
-                               $codeBCP[$segNo] = strtolower( $seg );
-                       }
-               }
-               $langCode = implode( '-', $codeBCP );
-               return $langCode;
-       }
-}
diff --git a/languages/MessageLocalizer.php b/languages/MessageLocalizer.php
deleted file mode 100644 (file)
index 9a1796b..0000000
+++ /dev/null
@@ -1,43 +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 Language
- */
-
-/**
- * Interface for localizing messages in MediaWiki
- *
- * @since 1.30
- * @ingroup Language
- */
-interface MessageLocalizer {
-
-       /**
-        * This is the method for getting translated interface messages.
-        *
-        * @see https://www.mediawiki.org/wiki/Manual:Messages_API
-        * @see Message::__construct
-        *
-        * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
-        *   or a MessageSpecifier.
-        * @param mixed $params,... Normal message parameters
-        * @return Message
-        */
-       public function msg( $key /*...*/ );
-
-}
index 455678d..9b9720d 100644 (file)
@@ -188,8 +188,8 @@ class LanguageZh extends LanguageZh_hans {
        }
 
        /**
-        * @param array $termsArray
-        * @return array
+        * @param string[] $termsArray
+        * @return string[]
         */
        function convertForSearchResult( $termsArray ) {
                $terms = implode( '|', $termsArray );
index ad6680d..cc70f69 100644 (file)
        "actionthrottled": "لا يمكن عمل المزيد من هذا الفعل",
        "actionthrottledtext": "كإجراء ضد السبام، أنت ممنوع من إجراء هذا الفعل عدد كبير من المرات في فترة زمنية قصيرة، ولقد تجاوزت هذا الحد.\nمن فضلك حاول مرة ثانية خلال عدة دقائق.",
        "protectedpagetext": "هذه الصفحة تمت حمايتها لمنع التعديل.",
-       "viewsourcetext": "يمكنك رؤية ونسخ مصدر هذه الصفحة:",
+       "viewsourcetext": "يمكنك رؤية ونسخ مصدر هذه الصفحة.",
        "viewyourtext": "يمكنك رؤية ونسخ مصدر <strong>تعديلاتك</strong> في هذه الصفحة.",
-       "protectedinterface": "هذه الصفحة توفر نص الواجهة للبرنامج، وهي مقفلة لمنع التخريب.",
+       "protectedinterface": "توفر هذه الصفحة نص الواجهة للبرنامج في هذا الويكي، وهي محمية لمنع سوء استخدامها.\nلإضافة أو تغيير الترجمات لكل الويكيات، رجاء استخدم [https://translatewiki.net/ translatewiki.net]، مشروع الترجمة الخاص بميدياويكي.",
        "editinginterface": "<strong>تحذير:</strong> أنت تقوم بتحرير صفحة تستخدم في الواجهة النصية للبرنامج.\nسوف تؤثر التغييرات على هذه الصفحة على مظهر واجهة المستخدم للمستخدمين الآخرين.",
        "cascadeprotected": "تمت حماية هذه الصفحة من التعديل لأنها مدمجة في {{PLURAL:$1||الصفحة التالية، والتي|الصفحتين التاليتين، واللتين|الصفحات التالية، والتي}} تم استعمال خاصية \"حماية الصفحات المدمجة\" {{PLURAL:$1||بها|بهما|بها}}:\n$2",
        "namespaceprotected": "لا تمتلك الصلاحية لتعديل الصفحات في نطاق <strong>$1</strong>.",
        "pt-userlogout": "أخرج",
        "php-mail-error-unknown": "خطأ غير معروف في وظيفة البريد PHP's mail()",
        "user-mail-no-addy": "لقد حاولت إرسال بريد إلكتروني دون عنوان بريد إلكتروني.",
-       "resetpass_announce": "تم تسجيل دخولك بكلمة سر مؤقتة.\nللدخول بشكل نهائي، يجب عليك ضبط كلمة سر جديدة هنا:",
+       "resetpass_announce": "لإنهاء عملية تسجيل الدخول، يجب تعيين كلمة سر جديدة.",
        "resetpass_header": "غير كلمة سر الحساب",
        "oldpassword": "كلمة السر القديمة:",
        "newpassword": "كلمة السر الجديدة:",
        "mergelog": "سجل الدمج",
        "revertmerge": "إلغاء الدمج",
        "mergelogpagetext": "بالأسفل قائمة بأحدث عمليات الدمج لتاريخ صفحة ما إلى أخرى.",
-       "history-title": " «$1»: تاريخ المراجعة",
-       "difference-title": "«$1»: الفرق بينات المراجعتين",
-       "difference-title-multipage": "«$1» و«$2»: الفرق بين الصفحتين",
+       "history-title": "تاريخ \"$1\"",
+       "difference-title": "الفرق بينات المراجعتين ل«$1»",
+       "difference-title-multipage": "الفرق بين الصفحتين «$1» و«$2»",
        "difference-multipage": "(الفرق بين الصفحتين)",
        "lineno": "سطر $1:",
        "compareselectedversions": "قارن بين النسختين المختارتين",
        "recentchangeslinked-to": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضاً عن ذلك",
        "upload": "صبّ فشياي",
        "uploadlogpage": "سجل الرفع",
-       "filedesc": "ملخص:",
+       "filedesc": "ملخص",
        "license": "ترخيص:",
        "file-anchor-link": "فيشياي",
        "filehist": "تاريخ الپاج",
        "restriction-edit": "تبديل",
        "undeletelink": "اعرض/استعد",
        "undeleteviewlink": "اعرض",
-       "namespace": "النطاق",
+       "namespace": "النطاق:",
        "invert": "اعكس الاختيار",
        "blanknamespace": "(رئيسي)",
        "contributions": "مساهمات {{GENDER:$1|المستعمل|المستعملة}}",
        "tooltip-n-randompage": "خرّج پاج بالزهر",
        "tooltip-feed-atom": "تلقيم أتوم لهذه الصفحة",
        "tooltip-t-contributions": "ليستة مساهمات ها {{GENDER:$1|المستعمل|المستعملة}}",
-       "tooltip-t-emailuser": "أرسÙ\84 Ø±Ø³Ø§Ù\84Ø© Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8aة {{GENDER:$1|لهذا المستخدم|لهذه المستخدمة}}",
+       "tooltip-t-emailuser": "إرساÙ\84 Ø±Ø³Ø§Ù\84ة {{GENDER:$1|لهذا المستخدم|لهذه المستخدمة}}",
        "tooltip-t-upload": "صبّ فيشيايات",
        "tooltip-ca-nstab-user": "اعرض صفحة المستخدم",
        "tooltip-ca-nstab-special": "هذي پاج سپاسيال، و ما تنجّمش تبدّل فيها شي",
index f6435e7..bff03ea 100644 (file)
        "undelete-revision": "Versión borrata de $1 (editada por $3, o $4 a las $5):",
        "undeleterevision-missing": "Versión no conforme u no trobata. Regular que o vinclo sía incorrecto u que ixa versión s'haiga restaurato u borrato d'o fichero.",
        "undelete-nodiff": "No s'ha trobato garra versión anterior.",
-       "undeletebtn": "Restaurar!",
+       "undeletebtn": "Restaurar",
        "undeletelink": "amostrar/restaurar",
        "undeleteviewlink": "veyer",
        "undeleteinvert": "Contornar selección",
index a0c8293..81980f9 100644 (file)
@@ -17,7 +17,8 @@
                        "WhatamIdoing",
                        "Hogweard",
                        "Amire80",
-                       "Pyscowicz"
+                       "Pyscowicz",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "Mearc under hlencan:",
        "restriction-level-sysop": "full borgen",
        "restriction-level-autoconfirmed": "sāmborgen",
        "restriction-level-all": "ǣnig emnet",
-       "undeletebtn": "Edstaðola!",
+       "undeletebtn": "Edstaðola",
        "undeletelink": "sēon/nīwian",
        "undeleteviewlink": "sēon",
        "undelete-search-submit": "Sēcan",
index 39495af..189c3a2 100644 (file)
        "history": "تاريخ الصفحة",
        "history_short": "التاريخ",
        "history_small": "تاريخ",
-       "updatedmarker": "عÙ\8fدÙ\84ت Ù\85Ù\86Ø° Ø²Ù\8aارتÙ\8a الأخيرة",
+       "updatedmarker": "عÙ\8fدÙ\90Ù\91Ù\84ت Ù\85Ù\86Ø° Ø²Ù\8aارتÙ\83 الأخيرة",
        "printableversion": "نسخة للطباعة",
        "permalink": "وصلة دائمة",
        "print": "اطبع",
        "otherlanguages": "بلغات أخرى",
        "redirectedfrom": "(بالتحويل من $1)",
        "redirectpagesub": "صفحة تحويل",
-       "redirectto": "تحويل إلى",
+       "redirectto": "تحويل إلى:",
        "lastmodifiedat": "آخر تعديل لهذه الصفحة كان يوم $1، الساعة $2.",
        "viewcount": "{{PLURAL:$1|لم تعرض هذه الصفحة أبدا|تم عرض هذه الصفحة مرة واحدة|تم عرض هذه الصفحة مرتين|تم عرض هذه الصفحة $1 مرات|تم عرض هذه الصفحة $1 مرة}}.",
        "protectedpage": "صفحة محمية",
        "selfredirect": "<strong>تحذير:</strong> أنت تقوم بتحويل الصفحة إلى نفسها.\nربما حددت الهدف الخطأ للتحويلة أو أنك تقوم بتحرير الصفحة الخطأ.\n\nإذا نقرت على «$1» مرة أخرى، سيتم إنشاء التحويلة رغم الخطأ.",
        "missingcommenttext": "من فضلك أدخل تعليقا.",
        "missingcommentheader": "<strong>تنبيه:</strong>  لم تقم بوضع موضوع/عنوان لهذا التعليق.\nإذا قمت بالضغط على \"$1\" مجددا، سيتم حفظ تعليقك بدون عنوان.",
-       "summary-preview": "معاينة ملخص تحرير",
+       "summary-preview": "معاينة ملخص تحرير:",
        "subject-preview": "معاينة الموضوع:",
        "previewerrortext": "حدث خطأ أثناء محاولة معاينة تغييراتك.",
        "blockedtitle": "المستخدم ممنوع",
        "autoblockedtext": "مُنِع عنوان آيبيك تلقائيا لأن مستخدما آخرا منعه $1 استخدمه.\nالسبب المعطى هو التالي:\n\n:<em>$2</em>\n\n* بداية المنع: $8\n* انتهاء المنع: $6\n* الممنوع المقصود: $7\n\nيمكنك أن تتصل ب $1 أو أحد [[{{MediaWiki:Grouppage-sysop}}|الإداريين]] الآخرين لمناقشة المنع.\n\nلاحظ أنه لا يمكنك استخدام خاصية \"{{int:emailuser}}\" إلا لو كان لديك عنوان بريد إلكتروني صحيح مسجل في [[Special:Preferences|تفضيلاتك]] ولم يتم منعك من استخدامه.\n\nعنوان آيبيك الحالي $3، ورقم المنع #$5.\nمن فضلك اذكر كل التفاصيل بالأعلى في أي استعلامات تقوم بها.",
        "systemblockedtext": "اسم المستخدم أو عنوان الأيبي الخاص بك تم منعه تلقائيا بواسطة ميدياويكي.\nالسبب المعطى هو:\n\n:<em>$2</em>\n\n* بداية المنع: $8\n* نهاية المنع: $6\n* المقصود بالمنع: $7\n\nعنوان الأيبي الحالي الخاص بك هو $3.\nمن فضلك ضمن كل التفاصيل بالأعلى في أي استعلام تقوم به.",
        "blockednoreason": "لا سبب معطى",
+       "blockedtext-composite": "<strong>تم منع اسم المستخدم أو عنوان الآيبي الخاص بك.</strong>\n\nالسبب المعطى هو:\n\n:<em>$2</em>.\n\n* بداية المنع: $8\n*  نهاية صلاحية أطول منع: $6\n\nعنوان الآيبي الحالي الخاص بك هو $3.\nيُرجَى تضمين جميع التفاصيل أعلاه في أية استفسارات تقوم بها.",
+       "blockedtext-composite-reason": "هناك عدة عمليات منع ضد حسابك و/أو عنوان الآيبي الخاص بك",
        "whitelistedittext": "يجب عليك $1 لتتمكن من تعديل الصفحات.",
        "confirmedittext": "يجب عليك تأكيد بريدك الإلكتروني قبل تعديل الصفحات.\nمن فضلك اكتب وأكد بريدك الإلكتروني من خلال [[Special:Preferences|تفضيلاتك]].",
        "nosuchsectiontitle": "تعذر إيجاد القسم",
        "mergelogpagetext": "بالأسفل قائمة بأحدث عمليات الدمج لتاريخ صفحة ما إلى أخرى.",
        "history-title": "تاريخ \"$1\"",
        "difference-title": "الفرق بين المراجعتين ل\"$1\"",
-       "difference-title-multipage": "«$1» و«$2»: الفرق بين الصفحتين",
+       "difference-title-multipage": "الفرق بين الصفحتين «$1» و«$2»",
        "difference-multipage": "(الفرق بين الصفحتين)",
        "lineno": "سطر $1:",
        "compareselectedversions": "قارن بين النسختين المختارتين",
        "protect-cascadeon": "هذه الصفحة محمية حاليا لكونها مضمنة في {{PLURAL:$1||الصفحة التالية|الصفحتين التاليتين|الصفحات التالية}}، والتي بها خيار حماية الصفحات المدمجة فعال.\nلن يؤثر تغيير مستوى حماية هذه الصفحة على حماية الصفحات المدمجة.",
        "protect-default": "اسمح لكل المستخدمين",
        "protect-fallback": "السماح فقط للمستخدمين ذوي الصلاحية \"$1\"",
-       "protect-level-autoconfirmed": "اÙ\84سÙ\85اح Ù\81Ù\82Ø· Ù\84Ù\84Ù\85ستخدÙ\85Ù\8aÙ\86 Ø§Ù\84Ù\85ؤÙ\83دÙ\8aÙ\86 ØªÙ\84Ù\82ائÙ\8aا",
+       "protect-level-autoconfirmed": "اÙ\84سÙ\85اح Ù\84Ù\84Ù\85ستخدÙ\85Ù\8aÙ\86 Ø§Ù\84Ù\85ؤÙ\83دÙ\8aÙ\86 ØªÙ\84Ù\82ائÙ\8aا Ù\81Ù\82Ø·",
        "protect-level-sysop": "السماح للإداريين فقط",
        "protect-summary-cascade": "مضمنة",
        "protect-expiring": "تنتهي في $1 (UTC)",
        "uctop": "حالية",
        "month": "من شهر (وأقدم):",
        "year": "من سنة (وأقدم):",
-       "date": "من تاريخ (وأقدم).",
+       "date": "من تاريخ (وأقدم):",
        "sp-contributions-newbies": "اعرض مساهمات الحسابات الجديدة فقط",
        "sp-contributions-newbies-sub": "للحسابات الجديدة",
        "sp-contributions-newbies-title": "مساهمات المستخدم للحسابات الجديدة",
        "tooltip-feed-rss": "تلقيم أر إس إس لهذه الصفحة",
        "tooltip-feed-atom": "تلقيم أتوم لهذه الصفحة",
        "tooltip-t-contributions": "رؤية قائمة مساهمات {{GENDER:$1|هذا المستخدم|هذه المستخدمة}}",
-       "tooltip-t-emailuser": "أرسÙ\84 Ø±Ø³Ø§Ù\84Ø© Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8aة {{GENDER:$1|لهذا المستخدم|لهذه المستخدمة}}",
+       "tooltip-t-emailuser": "إرساÙ\84 Ø±Ø³Ø§Ù\84ة {{GENDER:$1|لهذا المستخدم|لهذه المستخدمة}}",
        "tooltip-t-info": "المزيد من المعلومات عن هذه الصفحة",
        "tooltip-t-upload": "ارفع ملفات",
        "tooltip-t-specialpages": "قائمة بكل الصفحات الخاصة",
        "previousdiff": "→ التعديل السابق",
        "nextdiff": "التعديل اللاحق ←",
        "mediawarning": "<strong>تحذير:</strong> قد يحتوي نوع هذا الملف على كود خبيث.\nيمكن عند تشغيله السيطرة على نظامك.",
-       "imagemaxsize": "حد حجم الصورة في صفحات وصف الملفات",
+       "imagemaxsize": "حد حجم الصورة في صفحات وصف الملفات:",
        "thumbsize": "حجم العرض المصغر:",
        "widthheightpage": "$1×$2، {{PLURAL:$3|لا صفحات|صفحة واحدة|صفحتان|$3 صفحات|$3 صفحة}}",
        "file-info": "حجم الملف: $1، نوع MIME: $2",
        "version-poweredby-others": "آخرون",
        "version-poweredby-translators": "مترجمو ترانسليت ويكي دوت نت",
        "version-credits-summary": "نود أن نعرف بالأشخاص التالية أسماؤهم لمساهمتهم في [[Special:Version|ميدياويكي]].",
-       "version-license-info": "Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a Ø¨Ø±Ù\86اÙ\85ج Ø­Ø±Ø\8c Ù\8aØ­Ù\82 Ù\84Ù\83 ØªÙ\88زÙ\8aعÙ\87 Ù\88/Ø£Ù\88 ØªØ¹Ø¯Ù\8aÙ\84Ù\87 Ù\88Ù\81Ù\82اÙ\8b Ù\84بÙ\86Ù\88د Ø±Ø®ØµØ© Ø¬Ù\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ© Ù\83Ù\85ا Ù\86شرتÙ\87ا Ù\85ؤسسة Ø§Ù\84برÙ\85جÙ\8aات Ø§Ù\84حرةØ\8c Ø§Ù\84إصدار Ø§Ù\84ثاÙ\86Ù\8a Ø£Ù\88 (Ù\88Ù\81Ù\82ا Ù\84اختÙ\8aارÙ\83 Ø£Ù\86ت) Ø£Ù\8a Ø¥ØµØ¯Ø§Ø± Ù\84احÙ\82.\n\nÙ\87ذا Ø§Ù\84برÙ\86اÙ\85ج Ù\8aÙ\88زع Ø¹Ù\84Ù\89 Ø£Ù\85Ù\84 Ø£Ù\86 Ù\8aÙ\83Ù\88Ù\86 Ù\85Ù\81Ù\8aداÙ\8bØ\8c Ù\88Ù\84Ù\83Ù\86 <em>دÙ\88Ù\86 Ø£Ù\8aØ© Ø¶Ù\85اÙ\86ات</em>Ø\8c Ø¨Ù\85ا Ù\81Ù\8a Ø°Ù\84Ù\83 Ø¶Ù\85اÙ\86ات <strong>اÙ\84تسÙ\88Ù\8aÙ\82</strong> Ø£Ù\88 <strong>اÙ\84Ù\85Ù\84اءÙ\85Ø© Ù\84غرض Ù\85عÙ\8aÙ\86</strong>. Ø§Ù\86ظر Ø±Ø®ØµØ© ØºÙ\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ© Ù\84Ù\85زÙ\8aد Ù\85Ù\86 Ø§Ù\84تÙ\81اصÙ\8aÙ\84.\n\nÙ\8aÙ\86بغÙ\8a Ø£Ù\86 ØªÙ\83Ù\88Ù\86 Ù\82د ØªÙ\84Ù\82Ù\8aت Ù\86سخة Ù\85Ù\86 Ø±Ø®ØµØ© Ø¬Ù\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ© Ø¥Ø°Ø§ Ù\84Ù\85 Ù\8aتÙ\85 Ø°Ù\84Ù\83Ø\8c Ø§Ù\83تب Ø¥Ù\84Ù\89: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Ø£Ù\88 [//www.gnu.org/licenses/old-licenses/gpl-2.0.html Ø§Ù\82رأ على الإنترنت].",
+       "version-license-info": "Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a Ø¨Ø±Ù\86اÙ\85ج Ø­Ø±Ø\8c Ù\8aØ­Ù\82 Ù\84Ù\83 ØªÙ\88زÙ\8aعÙ\87 Ù\88/Ø£Ù\88 ØªØ¹Ø¯Ù\8aÙ\84Ù\87 Ù\88Ù\81Ù\82اÙ\8b Ù\84بÙ\86Ù\88د Ø±Ø®ØµØ© Ø¬Ù\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ© Ù\83Ù\85ا Ù\86شرتÙ\87ا Ù\85ؤسسة Ø§Ù\84برÙ\85جÙ\8aات Ø§Ù\84حرةØ\8c Ø§Ù\84إصدار Ø§Ù\84ثاÙ\86Ù\8a Ø£Ù\88 (Ù\88Ù\81Ù\82ا Ù\84اختÙ\8aارÙ\83 Ø£Ù\86ت) Ø£Ù\8a Ø¥ØµØ¯Ø§Ø± Ù\84احÙ\82.\n\nÙ\87ذا Ø§Ù\84برÙ\86اÙ\85ج Ù\8aÙ\88زع Ø¹Ù\84Ù\89 Ø£Ù\85Ù\84 Ø£Ù\86 Ù\8aÙ\83Ù\88Ù\86 Ù\85Ù\81Ù\8aداÙ\8bØ\8c Ù\88Ù\84Ù\83Ù\86 <em>دÙ\88Ù\86 Ø£Ù\8aØ© Ø¶Ù\85اÙ\86ات</em>Ø\8c Ø¨Ù\85ا Ù\81Ù\8a Ø°Ù\84Ù\83 Ø¶Ù\85اÙ\86ات <strong>اÙ\84تسÙ\88Ù\8aÙ\82</strong> Ø£Ù\88 <strong>اÙ\84Ù\85Ù\84اءÙ\85Ø© Ù\84غرض Ù\85عÙ\8aÙ\86</strong>. Ø§Ù\86ظر Ø±Ø®ØµØ© Ø¬Ù\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ© Ù\84Ù\85زÙ\8aد Ù\85Ù\86 Ø§Ù\84تÙ\81اصÙ\8aÙ\84.\n\nÙ\8aÙ\86بغÙ\8a Ø£Ù\86 ØªÙ\83Ù\88Ù\86 Ù\82د ØªÙ\84Ù\82Ù\8aت [{{SERVER}}{{SCRIPTPATH}}/COPYING Ù\86سخة Ù\85Ù\86 Ø±Ø®ØµØ© Ø¬Ù\86Ù\88 Ø§Ù\84عÙ\85Ù\88Ù\85Ù\8aØ©] Ø¥Ø°Ø§ Ù\84Ù\85 Ù\8aتÙ\85 Ø°Ù\84Ù\83Ø\8c Ø§Ù\83تب Ø¥Ù\84Ù\89 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Ø£Ù\88 [//www.gnu.org/licenses/old-licenses/gpl-2.0.html Ø§Ù\82رأÙ\87ا على الإنترنت].",
        "version-software": "البرنامج المثبت",
        "version-software-product": "المنتج",
        "version-software-version": "النسخة",
        "redirect-summary": "هذه الصفحة الخاصة تحوّل إلى ملف (باسمه) أو صفحة (برقم إحدى مراجعاتها) أو إلى صفحة مستخدم (برقمه التعريفي) أو إلى مدخلة سجل (برقم السجل). الاستخدام [[{{#Special:Redirect}}/file/Example.jpg]] أو [[{{#Special:Redirect}}/revision/328429]] أو [[{{#Special:Redirect}}/user/101]] أو [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "اذهب",
        "redirect-lookup": "ابحث في:",
-       "redirect-value": "الوجهة",
+       "redirect-value": "الوجهة:",
        "redirect-user": "رقم مستخدم",
        "redirect-page": "معرف الصفحة",
        "redirect-revision": "مراجعة صفحة",
        "tags-activate-submit": "تفعيل",
        "tags-deactivate-title": "عطل الوسم",
        "tags-deactivate-question": "أنت على وشك تعطيل الوسم \"$1\".",
-       "tags-deactivate-reason": "سبب",
+       "tags-deactivate-reason": "اÙ\84سبب:",
        "tags-deactivate-not-allowed": "من غير الممكن تعطيل الوسم \"$1\".",
        "tags-deactivate-submit": "عطل",
        "tags-apply-no-permission": "ليس لديك إذن لتطبيق علامات التغيير جنبا إلى جنب مع التغييرات.",
        "duration-centuries": "{{PLURAL:$1||قرن واحد|قرنان|$1 قرون|$1 قرنًا|$1 قرن}}",
        "duration-millennia": "{{PLURAL:$1||ألفية واحدة|ألفيتان|$1 ألفيات|$1 ألفية}}",
        "rotate-comment": "تدوير الصورة  {{PLURAL:$1||درجة واحدة|درجتان|$1 درجات|$1 درجة}} باتجاه عقارب الساعة",
-       "limitreport-title": "بيانات تحليلية",
+       "limitreport-title": "بيانات تحليلية:",
        "limitreport-cputime": "زمن المعالجة المستغرق",
        "limitreport-cputime-value": "{{PLURAL:$1|أقل من ثانية|ثانية واحدة|ثانيتان|$1 ثوان|$1 ثانية}}",
        "limitreport-walltime": "الزمن الحقيقي المستغرق",
index c28c1b6..c53c1e4 100644 (file)
        "otherlanguages": "بلغات تانيه",
        "redirectedfrom": "(تحويل من $1)",
        "redirectpagesub": "صفحة تحويل",
-       "redirectto": "تحويل ل",
+       "redirectto": "تحويل ل:",
        "lastmodifiedat": "الصفحه دى اتعدلت اخر مره فى $1,‏ $2.",
        "viewcount": "الصفحة دى اتدخل عليها{{PLURAL:$1|مرة واحدة|مرتين|$1 مرات|$1 مرة}}.",
        "protectedpage": "صفحه محميه",
        "protectedpagetext": "الصفحة دى اتحمت من التعديل.",
        "viewsourcetext": "ممكن تشوف وتنسخ مصدر الصفحه دى",
        "protectedinterface": "الصفحة دى هى اللى بتوفر نص الواجهة بتاعة البرنامج،وهى مقفولة لمنع التخريب.\nعلشان إضافة أو تغيير الترجمات لجميع مشاريع الويكي،  لو سمحت روح على [https://translatewiki.net/ translatewiki.net]، مشروع ترجمة ميدياويكى",
-       "editinginterface": "<strong>تحذير</strong> : أنت بتعدل صفحة بتستخدم فى الواجهة النصية  بتاعة البرنامج. \nالتغييرات فى الصفحة دى ها تأثر على مظهر واجهة اليوزر لليوزرز التانيين. \nعلشان إضافة أو تغيير الترجمات لجميع مشاريع الويكي،  لو سمحت روح على [https://translatewiki.net/ translatewiki.net]، مشروع ترجمة ميدياويكى",
+       "editinginterface": "<strong>تحذير:</strong> أنت بتعدل صفحة بتستخدم فى الواجهة النصية  بتاعة البرنامج. \nالتغييرات فى الصفحة دى ها تأثر على مظهر واجهة اليوزر لليوزرز التانيين.",
        "cascadeprotected": "الصفحة دى محمية من التعديل، بسبب انها مدمجة فى {{PLURAL:$1|الصفحة|الصفحتين|الصفحات}} دي، اللى مستعمل فيها خاصية \"حماية الصفحات المدمجة\" :\n$2",
        "namespaceprotected": "ما عندكش صلاحية تعديل الصفحات  اللى فى نطاق <strong>$1</strong>.",
        "ns-specialprotected": "الصفحات المخصوصة مش ممكن تعديلها.",
        "userlogin-yourname-ph": "اكتب اسم اليوزر بتاعك",
        "createacct-another-username-ph": "اكتب اسم يوزر",
        "yourpassword": "الباسوورد:",
-       "userlogin-yourpassword": "الباسورد:",
+       "userlogin-yourpassword": "الباسورد",
        "yourpasswordagain": "اكتب الباسورد تاني:",
        "createacct-yourpasswordagain": "أكد كلمه السر",
        "yourdomainname": "النطاق بتاعك:",
        "userlogin-helplink2": "مساعده ف الدخول",
        "createacct-email-ph": "اكتب عنوان الإيميل بتاعك",
        "createaccountmail": "استخدم باسورد مؤقته و إبعتها ع الايميل المحدد ده",
-       "createacct-reason": "سبب:",
+       "createacct-reason": "اÙ\84سبب",
        "createacct-submit": "افتح حسابك",
        "createacct-benefit-body1": "$1 {{PLURAL:$1|تعديل|تعديلات}}",
        "createacct-benefit-body2": "{{PLURAL:$1|صفحه|صفحات}}",
        "pt-createaccount": "افتح حساب",
        "pt-userlogout": "خروج",
        "changepassword": "غير الباسورد",
-       "resetpass_announce": " علشان تخلص عملية  تسجيل الدخول ،لازم تعملك باسورد جديده:",
+       "resetpass_announce": "علشان تخلص عملية  تسجيل الدخول، لازم تعملك باسورد جديده.",
        "resetpass_text": "<!-- أضف نصا هنا -->",
        "resetpass_header": "غيّر الباسورد بتاعة الحساب",
        "oldpassword": "الباسورد القديمة:",
        "missingcommenttext": "لو سمحت اكتب تعليق تحت.",
        "missingcommentheader": "<strong>خد بالك:</strong> انت ما كتبتش عنوان\\موضوع للتعليق دا\nلو دوست على $1 مرة تانيه، تعليقك ح يتحفظ من غير عنوان.",
        "summary-preview": "بروفه للملخص:",
-       "subject-preview": "بروفة للعنوان/للموضوع",
+       "subject-preview": "بروفة للعنوان/للموضوع:",
        "blockedtitle": "اليوزر ممنوع",
        "blockedtext": "<strong>تم منع اسم اليوزر أو عنوان الاى بى بتاعك .</strong>\n\nاللى عمل المنع $1.\nسبب المنع هو: <em>$2</em>.\n\n* بداية المنع: $8\n* انتهاء المنع: $6\n* الممنوع المقصود: $7\n\nممكن التواصل مع $1 لمناقشة المنع، أو مع واحد من [[{{MediaWiki:Grouppage-sysop}}|الاداريين]] عن المنع.\nافتكر انه مش ممكن تستخدم الخاصيه \"{{int:emailuser}}\" الا اذا كنت سجلت عنوان ايميل صحيح فى صفحة [[Special:Preferences|التفضيلات]] بتاعتك\nو ما تكونش اتمنعت من استعمالها.\nعنوان الاى بى بتاعك حاليا هو $3 وكود المنع هو #$5.\nمن فضلك ضيف كل التفاصيل اللى فوق فى اى رساله للتساؤل عن المنع.",
        "autoblockedtext": "عنوان الأيبى بتاعك اتمنع اتوماتيكى  علشان فى يوزر تانى استخدمه واللى هو كمان ممنوع بــ $1.\nالسبب هو:\n\n:<em>$2</em>\n\n* بداية المنع: $8\n* انهاية المنع: $6\n* الممنوع المقصود: $7\n\nممكن تتصل  ب $1 أو واحد من [[{{MediaWiki:Grouppage-sysop}}|الإداريين]] االتانيين لمناقشة المنع.\n\nلاحظ أنه مش ممكن استخدام خاصية \"{{int:emailuser}}\" إلا اذا كان عندك ايميل صحيح متسجل فى [[Special:Preferences|تفضيلاتك]].\n\nعنوان الأيبى الحالى الخاص بك هو $3، رقم المنع هو #$5.\nلو سمحت تذكر الرقم دا فى اى استفسار.",
        "content-model-wikitext": "ويكى تكست",
        "content-model-text": "كلام عادى",
        "content-model-javascript": "جاڤاسكربت",
-       "expensive-parserfunction-warning": "<strong>تحذير:</strong> الصفحه دى فيهااستدعاءات دالة محلل كثيرة مكلفة.\n\nلازم تكون أقل من $2 {{PLURAL:$2|استدعاء|استدعاء}}، يوجد {{PLURAL:$1|الآن $1 استدعاء|الآن $1 استدعاء}}.",
+       "expensive-parserfunction-warning": "<strong>تحذير:</strong> الصفحه دى فيهااستدعاءات دالة محلل كثيرة مكلفة.\n\nلازم تكون أقل من $2 {{PLURAL:$2|استدعاء}}، يوجد {{PLURAL:$1|الآن $1 استدعاء}}.",
        "expensive-parserfunction-category": "صفحات فيها استدعاءات دوال محلل كثيرة ومكلفة",
        "post-expand-template-inclusion-warning": "<strong>تحذير:</strong> حجم تضمين القالب كبير قوي.\nبعض القوالب مش ح تتضمن.",
        "post-expand-template-inclusion-category": "الصفحات اللى تم تجاوز حجم تضمين القالب فيها",
        "mergelog": "سجل الدمج",
        "revertmerge": "استرجاع الدمج",
        "mergelogpagetext": "فى تحت لستة بأحدث عمليات الدمج لتاريخ صفحة فى التانية.",
-       "history-title": " «$1»: تاريخ التعديل",
-       "difference-title": "«$1»: الفرق بين النسختين",
+       "history-title": "تاريخ التعديل بتاع «$1»",
+       "difference-title": "الفرق بين النسختين بتاع «$1»",
        "difference-multipage": "(الفرق بين الصفحتين)",
        "lineno": "سطر $1:",
        "compareselectedversions": "قارن بين النسختين المختارتين",
        "recentchangesdays": "عدد الأيام المعروضة فى اخرالتغييرات:",
        "recentchangesdays-max": "(الحد الاقصى $1 {{PLURAL:$1|يوم|ايام}})",
        "recentchangescount": "عدد التعديلات اللى بتظهر اوتوماتيكى فى اخر التغييرات, تواريخ الصفحه, و فى السجلات, :",
-       "prefs-help-recentchangescount": "بÙ\8aحتÙ\88Ù\89 Ø¹Ù\84Ù\89 Ø§Ø­Ø¯Ø« Ø§Ù\84تغÙ\8aÙ\8aرات Ø\8c ØªÙ\88ارÙ\8aØ® Ø§Ù\84صÙ\81حات Ù\88 Ø§Ù\84سجÙ\84ات.",
+       "prefs-help-recentchangescount": "اÙ\82صÙ\89 Ø±Ù\82Ù\85: 1000",
        "savedprefs": "التفضيلات بتاعتك اتحفظت.",
-       "timezonelegend": "منطقة التوقيت",
-       "localtime": "التوقيت المحلى",
+       "timezonelegend": "منطقة التوقيت:",
+       "localtime": "التوقيت المحلى:",
        "timezoneuseserverdefault": "استخدم الويكى الافتراضى ($1)",
        "timezoneuseoffset": "تانى (حدد الفرق)",
-       "servertime": "وقت السيرفر",
+       "servertime": "وقت السيرفر:",
        "guesstimezone": "دخل التوقيت من البراوزر",
        "timezoneregion-africa": "افريقيا",
        "timezoneregion-america": "امريكا",
        "prefs-help-signature": "التعليقات فى صفحات النقاش لازم تتوقع ب\"<nowiki>~~~~</nowiki>\" واللى حتتحول لتوقيعك وتاريخ.",
        "badsig": "الامضا الخام بتاعتك مش صح.\nاتإكد من التاجز بتاعة الHTML.",
        "badsiglength": "الامضا بتاعتك اطول م اللازم.\nلازم تكون اصغر من$1 {{PLURAL:$1|حرف|حرف}}.",
-       "yourgender": "النوع:",
+       "yourgender": "ازاى بتفضل ان البرنامج يخاطبك؟",
        "gender-unknown": "مش متحدد",
        "gender-male": "ذكر",
        "gender-female": "انثى",
-       "prefs-help-gender": "اختÙ\8aارÙ\8a: Ø¨Ù\8aستعÙ\85Ù\84Ù\88Ù\87 Ù\81Ù\89  Ø§Ù\84Ù\85خاطبة Ø§Ù\84Ù\85عتÙ\85دة Ø¹Ù\84Ù\89 Ø§Ù\84Ù\86Ù\88ع Ø¨Ø§Ù\84سÙ\88Ù\81تÙ\88Ù\8aر. المعلومه دى ح تكون علنيه.",
+       "prefs-help-gender": "عÙ\85Ù\84 Ø§Ù\84تÙ\81ضÙ\8aÙ\84 Ø¯Ù\87 Ø§Ø®ØªÙ\8aارÙ\89.\nبÙ\8aستعÙ\85Ù\84Ù\88Ù\87 Ù\81Ù\89  Ø§Ù\84Ù\85خاطبة Ø§Ù\84Ù\85عتÙ\85دة Ø¹Ù\84Ù\89 Ø§Ù\84Ù\86Ù\88ع Ø¨Ø§Ù\84سÙ\88Ù\81تÙ\88Ù\8aر.\nالمعلومه دى ح تكون علنيه.",
        "email": "الإيميل",
        "prefs-help-realname": "الاسم الحقيقى اختيارى.\nلو إخترت تكتبه, حيستعمل بس علشان شغلك يتنسب لإسمك.",
        "prefs-help-email": "عنوان اللإيميل اختيارى ، بس لازم علشان لو نسيت الپاسوورد..",
        "saveusergroups": "حفظ مجموعات {{GENDER:$1|اليوزر}}",
        "userrights-groupsmember": "عضو في:",
        "userrights-groupsmember-auto": "عضو ضمنى فى :",
-       "userrights-groups-help": "إنت ممكن تغير المجموعات اللى اليوزر دا عضو فيها .\n* صندوق متعلم يعنى اليوزر دا عضو فى المجموعة دي.\n* صندوق مش متعلم يعنى  اليوزر دا مش عضو فى المجموعة دي.\n* علامة * يعنى انك مش ممكن تشيل المجموعات بعد ما تضيفها و العكس بالعكس.",
+       "userrights-groups-help": "إنت ممكن تغير المجموعات اللى اليوزر دا عضو فيها:\n* صندوق متعلم يعنى اليوزر دا عضو فى المجموعة دى.\n* صندوق مش متعلم يعنى  اليوزر دا مش عضو فى المجموعة دى.\n* علامة * يعنى انك مش ممكن تشيل المجموعات بعد ما تضيفها و العكس بالعكس.",
        "userrights-reason": "السبب:",
        "userrights-no-interwiki": "أنت  مش من حقك تعدل صلاحيات اليوزرز على الويكيات التانية.",
        "userrights-nodatabase": "قاعدة البيانات $1  مش موجودة أو مش محلية.",
        "recentchanges-label-minor": "ده تعديل صغير",
        "recentchanges-label-bot": "التعديل ده عمله بوت",
        "recentchanges-label-unpatrolled": "التعديل ده مإتراجعش لسه",
-       "recentchanges-legend-heading": "<strong>شرح</strong>",
+       "recentchanges-legend-heading": "<strong>شرح:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (بص كمان على [[Special:NewPages|قايمه الصفحات الجديده]])",
        "rcnotefrom": "{{PLURAL:$5|ده التعديل|دى التعديلات}} من اول <strong>$3, $4</strong> (لغايه<strong>$1</strong> معروضه).",
        "rclistfrom": "اظهر التعديلات بدايه من $3 $2",
        "upload_directory_missing": "مجلد التحميل($1) ضايع السيرفير وماقدرش يعمل واحد تاني.",
        "upload_directory_read_only": "مجلد التحميل ($1) مش ممكن الكتابة عليه بواسطة سيرڨر الويب.",
        "uploaderror": "غلطه فى التحميل",
-       "uploadtext": "استخدم الاستمارة علشان تحميل الملفات.\nلعرض أو البحث ف الملفات المتحملة سابقا، راجع عمليات [[Special:Log/delete|المسح]]، عمليات التحميل  موجودة فى [[Special:Log/upload|سجل التحميل]].\n\nعلشان تحط صورة فى صفحة، استخدم الوصلات فى الصيغ التالية:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code></strong> علشان استخدام النسخة الكاملة لملف\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|نص بديل]]</nowiki></code></strong> لاستخدام صورة عرضها 200 بكسل فى صندوق فى الجانب الأيسر مع \"نص بديل\" كوصف\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code></strong> للوصل للملف مباشرة بدون عرض الملف",
+       "uploadtext": "استخدم الاستمارة علشان تحميل الملفات.\nعلشان تشوف او تدور فى الفايلات اللى اتحملت قبل كده روح على [[Special:FileList|ليسته الفايلات اللى اتحملت]]، عمليات التحميل  موجودة فى [[Special:Log/upload|سجل التحميل]]، والحذف فى [[Special:Log/delete|سجل المسح]].\n\nعلشان تحط صورة فى صفحة، استخدم الوصلات فى الصيغ التالية:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code></strong> علشان استخدام النسخة الكاملة لملف\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|نص بديل]]</nowiki></code></strong> لاستخدام صورة عرضها 200 بكسل فى صندوق فى الجانب الأيسر مع \"نص بديل\" كوصف\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code></strong> للوصل للملف مباشرة بدون عرض الملف",
        "upload-permitted": "{{PLURAL:$2|نوع|انواع}} الملفات اللى مسموح بيها: $1.",
        "upload-preferred": "{{PLURAL:$2|نوع|انواع}} الملفات المفضله: $1.",
        "upload-prohibited": "{{PLURAL:$2|نوع|انواع}} الملفات الممنوعه: $1.",
        "upload-file-error": "غلط داخلي",
        "upload-file-error-text": "حصل غلط داخلى واحنا بنحاول نعمل ملف مؤقت على السيرفر.\nلو سمحت اتصل [[Special:ListUsers/sysop|بسيسوب]].",
        "upload-misc-error": "غلط مش معروف فى التحميل",
-       "upload-misc-error-text": "حصل غلط مش معروف وإنت بتحمل.\nلو سمحت تتاكد أن اليوأرإل صح و ممكن تدخل عليه و بعدين حاول تاني.\nإذا المشكلة تنتها موجودة،اتصل بإدارى نظام.",
+       "upload-misc-error-text": "حصل غلط مش معروف وإنت بتحمل.\nلو سمحت تتاكد أن اليو أر إل صح و ممكن تدخل عليه و بعدين حاول تانى.\nإذا المشكلة تنتها موجودة، اتصل [[Special:ListUsers/sysop|بإدارى نظام]].",
        "upload-too-many-redirects": "الـ URL فيه تحويلات اكتر من اللازم",
        "upload-http-error": "حصل غلط فى الـHTTB :$1",
        "img-auth-accessdenied": "الوصول مش مسموح بيه",
        "brokenredirects-edit": "تحرير",
        "brokenredirects-delete": "مسح",
        "withoutinterwiki": "صفحات من غير وصلات للغات تانيه",
-       "withoutinterwiki-summary": "الصفحات دى  مالهاش لينكات لنسخ بلغات تانية:",
+       "withoutinterwiki-summary": "الصفحات دى  مالهاش لينكات لنسخ بلغات تانية.",
        "withoutinterwiki-legend": "بريفيكس",
        "withoutinterwiki-submit": "عرض",
        "fewestrevisions": "اقل المقالات فى عدد التعديلات",
        "activeusers-noresult": "مالقيناش اى يوزر",
        "listgrouprights": "حقوق مجموعات اليوزرز",
        "listgrouprights-summary": "دى لستة بمجموعات اليوزرز المتعرفة فى الويكى دا، بالحقوق اللى معاهم.\nممكن تلاقى معلومات زيادة عن الحقوق بتاعة كل واحد  [[{{MediaWiki:Listgrouprights-helppage}}|هنا]].",
-       "listgrouprights-key": "* <span class=\"listgrouprights-granted\">حق ممنوح</span>\n* <span class=\"listgrouprights-revoked\">حق متصادر</span>",
+       "listgrouprights-key": "شرح:\n* <span class=\"listgrouprights-granted\">حق ممنوح</span>\n* <span class=\"listgrouprights-revoked\">حق متصادر</span>",
        "listgrouprights-group": "المجموعة",
        "listgrouprights-rights": "الحقوق",
        "listgrouprights-helppage": "Help: حقوق المجموعات",
        "move-page": "انقل $1",
        "move-page-legend": "انقل الصفحة",
        "movepagetext": "لو استعملت النموذج ده ممكن تغير اسم الصفحه، و تنقل تاريخها للاسم الجديد.\nهاتبتدى تحويله من العنوان القديم للصفحه بالعنوان الجديد.\nلكن،  الوصلات فى الصفحات اللى بتتوصل بالصفحه دى مش ها تتغيير.\nاتأكد من ان مافيش [[Special:DoubleRedirects|وصلات متتاليه]] او [[Special:BrokenRedirects|وصلات مقطوعه]]، للتأكد من أن المقالات تتصل مع بعضها بشكل مناسب.\n\nلاحظ ان الصفحه <strong>مش</strong> هاتتنقل لو كان فيه صفحه بالاسم الجديد، إلا إذا كانت صفحة فاضيه، أو صفحة تحويل، ومالهاش تاريخ.\nو ده معناه أنك مش ها تقدر تحط صفحه مكان صفحه، كمان ممكن ارجاع الصفحه لمكانها فى حال تم النقل بشكل غلط.\n\n<strong>تحذير!</strong>\nنقل الصفحه ممكن يكون له اثار كبيرة، وتغييرات مش متوقعه بالنسبة للصفحات المشهوره.\nمن فضلك  اتأكد من فهم عواقب نقل الصفحات قبل ما تقوم بنقل الصفحه.",
-       "movepagetalktext": "صفحة المناقشه بتاعة المقاله هاتتنقل برضه، لو كانت موجوده. لكن صفحة المناقشه '''مش''' هاتتنقل فى الحالات دى:\n* نقل الصفحة عبر نطاقات  مختلفه.\n*فيه  صفحة مناقشه موجوده تحت العنوان الجديد للمقاله.\n* لو انت شلت اختيار نقل صفحة المناقشه .\n\nوفى الحالات  دى، لو عايز  تنقل صفحة المناقشه  لازم تنقل أو تدمج محتوياتها  يدويا.",
+       "movepagetalktext": "لو علمت على الاختيار ده، فصفحه المناقشه حتتنقل اوتوماتيك للعنوان الجديد، الا لو كان فيه صفحة مناقشه مش فاضيه هناك.\n\nوفى الحاله  دى، لو عايز  تنقل صفحة المناقشه لازم تنقل أو تدمج محتوياتها  يدويا.",
        "moveuserpage-warning": "<strong>خد بالك:</strong> انت ح تعمل نقل لصفحه بتاعة يوزر. لو سمحت تعمل حسابك ان الصفحه هى بس اللى ح تتنقل و اسم اليوزر <em>مش</em> ح يتغير.",
        "movenologintext": "لازم تكون يوزر متسجل و تعمل [[Special:UserLogin|دخول]] علشان تنقل الصفحة.",
        "movenotallowed": "ماعندكش الصلاحية لنقل الصفحات.",
        "confirmemail_sendfailed": "{{SITENAME}} ماقدرش يبعت ايميل التأكيد.\nلو سمحت تتأكد من الايميل بتاعك.\n\nالغلط اللى حصل: $1",
        "confirmemail_invalid": "كود تفعيل غلط.\nيمكن صلاحيته تكون انتهت.",
        "confirmemail_needlogin": "لازم $1 علشان تأكد الايميل بتاعك.",
-       "confirmemail_success": "الايميل بتاعك اتأكد خلاص.\nممكن دلوقتى تسجل دخولك و تستمتع بالويكي.",
+       "confirmemail_success": "الايميل بتاعك اتأكد خلاص.\nممكن دلوقتى [[Special:UserLogin|تسجل دخولك]] و تستمتع بالويكى.",
        "confirmemail_loggedin": "الايميل بتاعك اتأكد خلاص.",
        "confirmemail_subject": "تأكيد الايميل من {{SITENAME}}",
        "confirmemail_body": "فى واحد، ممكن يكون إنتا، من عنوان الأيبى $1،\nفتح حساب \"$2\" بعنوان الايميل دا فى {{SITENAME}}.\n\nعلشان نتأكد أن  الحساب دا بتاعك فعلا و علشان كمان تفعيل خواص الايميل فى {{SITENAME}}، افتح اللينك دى فى البراوزر بتاعك :\n\n$3\n\nإذا *ماكنتش* إنتا اللى فتحت الحساب ، دوس على اللينك دى علشان تلغى تأكيد الايميل\n:\n\n$5\n\nكود التفعيل دا ح ينتهى $4.",
index db8b3ef..0a3d39e 100644 (file)
        "pager-older-n": "{{PLURAL:$1|পুৰণতৰ ১|পুৰণতৰ $1}}",
        "suppress": "অমনোযোগ",
        "querypage-disabled": "কাৰ্য্যগত কাৰণত এই বিশেষ পৃষ্ঠাটো নিষ্ক্ৰিয় কৰা হৈছে।",
+       "apihelp-no-such-module": "\"$1\" মডিউল পোৱা নগ'ল।",
        "apisandbox-results": "ফলাফল",
        "apisandbox-continue": "অব্যাহত ৰাখক",
        "booksources": "গ্ৰন্থৰ উৎস সমূহ",
index 5552739..d8319f0 100644 (file)
        "category-article-count": "{{PLURAL:$2|Esta categoría contien namái la páxina siguiente.|{{PLURAL:$1|La páxina siguiente ta|Les $1 páxines siguientes tán}} nesta categoría, d'un total de $2.}}",
        "category-article-count-limited": "{{PLURAL:$1|La páxina siguiente ta|Les $1 páxines siguientes tán}} na categoría actual.",
        "category-file-count": "{{PLURAL:$2|Esta categoría contien namái el ficheru siguiente.|{{PLURAL:$1|El ficheru siguiente ta|Los $1 ficheros siguientes tán}} nesta categoría, d'un total de $2.}}",
-       "category-file-count-limited": "{{PLURAL:$1|El ficheru siguiente ta|Los $1 ficheeros siguientes tán}} na categoría actual.",
+       "category-file-count-limited": "{{PLURAL:$1|El ficheru siguiente ta|Los $1 ficheros siguientes tán}} na categoría actual.",
        "listingcontinuesabbrev": "cont.",
        "index-category": "Páxines indexaes",
        "noindex-category": "Páxines sin indexar",
        "session_fail_preview_html": "¡Sentímoslo! Nun pudo procesase la to edición por aciu d'una perda de datos de la sesión.\n\n<em>Como {{SITENAME}} tien el HTML puru activáu, la vista previa ta tapecida como precaución escontra ataques en JavaScript.</em>\n\n<strong>Si esti ye un intentu llexítimu d'edición, por favor vuelvi a intentalo.</strong>\nSi inda nun funciona, intenta [[Special:UserLogout|colar]] y volver a aniciar sesión, y comprueba que'l to restolador permite les cookies d'esti sitiu.",
        "token_suffix_mismatch": "'''La to edición nun s'aceutó porque'l to navegador mutiló los caráuteres de puntuación nel editor.'''\nLa edición nun foi aceutada pa prevenir corrupciones na páxina de testu.\nDacuando esto pasa por usar un serviciu proxy anónimu basáu en web que tenga fallos.",
        "edit_form_incomplete": "'''Delles partes del formulariu d'edición nun llegaron al sirvidor; comprueba que les ediciones tean intactes y vuelvi a tentalo.'''",
-       "editing": "Editando $1",
-       "creating": "Creando $1",
+       "editing": "Edición de «$1»",
+       "creating": "Creación de «$1»",
        "editingsection": "Editando $1 (seición)",
        "editingcomment": "Editando $1 (seición nueva)",
        "editconflict": "Conflictu d'edición: $1",
        "rcfilters-activefilters-show-tooltip": "Amosar l'área de Filtros activos",
        "rcfilters-advancedfilters": "Filtros avanzaos",
        "rcfilters-limit-title": "Resultancies qu'amosar",
-       "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|cambiu|$1 cambios}}, $2",
+       "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|cambéu|cambeos}}, $2",
        "rcfilters-date-popup-title": "Periodu de tiempu a buscar",
        "rcfilters-days-title": "Últimos díes",
        "rcfilters-hours-title": "Últimes hores",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suxerir cambiu al aniciar sesión",
        "easydeflate-invaliddeflate": "El conteníu dau nun ta comprimíu correutamente",
        "unprotected-js": "Por razones de seguridá, JavaScript nun puede cargase dende páxines ensin protexer. Crea javascript sólo nel espaciu de nomes MediaWiki: o como subpáxina d'usuariu",
-       "userlogout-continue": "Si desees zarrar la sesión [$1 sigui na páxina de finar sesión]."
+       "userlogout-continue": "¿Desees zarrar la sesión?"
 }
index e6122fc..07496b9 100644 (file)
        "perfcached": "Aşağıdakı məlumatlar keş yaddaşdan götürülmüşdür və bu səbəbdən aktual olmaya bilər. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.",
        "perfcachedts": "Aşağıdakı məlumatlar keş yaddaşdan götürülmüşdür və sonuncu dəfə $1 tarixində yenilənmişdir. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.",
        "querypage-no-updates": "Bu an üçün güncəlləmələr sıradan çıxdı. Buradakı məlumat dərhal yenilənməyəcək.",
-       "viewsource": "Mənbə göstər",
+       "viewsource": "Kodu göstər",
        "viewsource-title": "$1 üçün mənbəyə bax",
        "actionthrottled": "Sürət məhdudiyyəti",
        "actionthrottledtext": "Spamla mübarizə məqsədilə qısa vaxt kəsiyi ərzində bu hərəkətlərin təkrarlanma sayı məhdudlaşdırılıb və siz qoyulan həddi aşmısınız.\nLütfən bir neçə dəqiqə sonra yenidən yoxlayın.",
        "feedback-subject": "Mövzu:",
        "feedback-submit": "Təsdiq et",
        "feedback-thanks-title": "Təşəkkür!",
-       "searchsuggest-search": "{{grammar:prepositional|{{SITENAME}}}} axtar",
+       "searchsuggest-search": "{{grammar:prepositional|{{SITENAME}}}} saytında axtar",
        "api-error-unknown-warning": "Naməlum xəbərdarlıq: \"$1\".",
        "api-error-unknownerror": "Naməlum xəta: \"$1\".",
        "duration-seconds": "$1 {{PLURAL:$1|saniyə|saniyə}}",
index 0f8d534..d47aaec 100644 (file)
@@ -15,7 +15,8 @@
                        "Macofe",
                        "Matěj Suchánek",
                        "Rachitrali",
-                       "Sultanselim baloch"
+                       "Sultanselim baloch",
+                       "FarsiNevis"
                ]
        },
        "tog-underline": ":لینکاں کِشک کن",
        "searcharticle": "برا",
        "history": "دیمی تاریخ",
        "history_short": "دپتر",
-       "history_small": "تاریخچگ",
+       "history_small": "وھدگ",
        "updatedmarker": "په روچ بیتگین چه منی اهری  اهری  چارگ",
        "printableversion": "چاپی بھر",
        "permalink": "دایمی لینک",
        "redirectedfrom": "(غیر مستقیم بوتگ چه $1)",
        "redirectpagesub": "صفحه غیر مستقیم",
        "redirectto": "مسیری ٹگل داتین بی:",
-       "lastmodifiedat": "  $2, $1.ای صفحه اهری تغییر دهگ بیته",
+       "lastmodifiedat": "اے تاک گُڈی برا $1 $2 ئا ٹگل دیگ بیتہ",
        "viewcount": "ای صفحه دسترسی بیتگ {{PLURAL:$1|بار|$1رند}}.",
        "protectedpage": "صفحه محافظتی",
        "jumpto": "کپ به:",
        "pool-queuefull": "مهزنء صف پر انت",
        "pool-errorunknown": "ناپجارین ارور",
        "pool-servererror": "سرویسء پول سینٹر ودی نبیت ($1).",
-       "aboutsite": "باره {{SITENAME}}",
+       "aboutsite": "{{SITENAME}}ءِ بارہ‌ئا",
        "aboutpage": "Project:باره",
        "copyright": "محتوا مان اجازت نامهٔ $1 انت مگان ایشی که آئی هلاپء آرگ ببیت انت.",
        "copyrightpage": "{{ns:project}}:حق کپی",
        "currentevents": "هنوکین رویداد",
        "currentevents-url": "Project:هنوکین رویداد",
-       "disclaimers": "بÛ\8c Ù\85Û\8cارÛ\8c Ú¯Û\8cاÙ\86",
+       "disclaimers": "بÛ\92 Ù\85Û\8cارÛ\8c",
        "disclaimerpage": "Project:عمومی بی میاریگان",
        "edithelp": "کمک اصلاح",
        "helppage-top-gethelp": "کومک",
        "policy-url": "Project:سیاست",
        "portal": "دیوانءِ درگت",
        "portal-url": "Project:پرتال انجمن",
-       "privacy": "سÛ\8cاست Ø­Ù\81ظ Ø§Ø³Ø±Ø§Ø±",
+       "privacy": "رازدارÛ\8cØ¡Ù\90 Ù¾Ø¦Û\8cÙ\85",
        "privacypage": "Project:سیاست حفظ اسرار",
        "badaccess": "حطا اجازت",
        "badaccess-group0": "شما مجاز نهیت عملی که درخواست کت اجرا کنیت",
        "difference-multipage": "(پرک مان تاک ان)",
        "lineno": "خط$1:",
        "compareselectedversions": "مقایسه انتخاب بوتگین نسخه یان",
-       "showhideselectedversions": "نمایش/پنهان کتن نسخ انتخابی",
+       "showhideselectedversions": "سۏج/جوان کنگ اے ورژنئی",
        "editundo": "خنثی کتن",
        "diff-empty": "(بئ پرک)",
        "diff-multi-sameuser": "({{PLURAL:$1|یک میانجیگین نسخگ|$1 میانجیگین نسخگ}} گون همجندیء کاربر که پیش دارگ نه بوتگ انت)",
        "newuserlogpagetext": ".شی یک ورودی چه شرکتن کاربر",
        "rightslog": "ورودان حقوق کاربر",
        "rightslogtext": "شی یک آماری چه تغییرات په حقوق کاربری انت.",
-       "action-read": "وانگ این صفحه",
+       "action-read": "اے تاکءِ وانگ",
        "action-edit": "اصلاح ای صفحه",
        "action-createpage": "شرکتن ای صفحه",
        "action-createtalk": "شرکتن صفحات بحث",
        "action-editmyprivateinfo": "وتی پرایویت اینفارمیشنء ادیت بکن",
        "nchanges": "$1 {{PLURAL:$1|تغییر|تغییرات}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|چه آهریگین چارگ}}",
-       "enhancedrc-history": "تاریخچگ",
+       "enhancedrc-history": "وھدگ",
        "recentchanges": "نوکین تغییرات",
        "recentchanges-legend": "گزینه ی نوکین تغییرات",
        "recentchanges-summary": "رندگر نوکترین تغییرات ته ویکی تی ای صفحه.",
        "rc-enhanced-hide": "پناه کتن جزییات",
        "rc-old-title": "اڈ بیتگ گون «$1»",
        "recentchangeslinked": "مربوطین تغییرات",
-       "recentchangeslinked-feed": "مربوطین تغییرات",
-       "recentchangeslinked-toolbox": "مربوطین تغییرات",
+       "recentchangeslinked-feed": "امبندݔں ٹگلاں",
+       "recentchangeslinked-toolbox": "امبندݔں ٹگلاں",
        "recentchangeslinked-title": "تغییراتی مربوط په \"$1\"",
        "recentchangeslinked-summary": "شی یک لیستی چه تغییراتی هستنت که نوکی اعمال بوتگنت په صفحاتی که چه یک صفحه خاصی لینک بوته( یا په اعضای یک خاصین دسته).\nصفحات ته [[Special:Watchlist| شمی لیست چارگ]] '''' پررنگنت''''",
        "recentchangeslinked-page": "تاکدیمِ نام:",
        "backend-fail-batchsize": "دسته‌ای مشتمل بر $1 {{PLURAL:$1|عملکرد|عملکرد}} پرونده به پشتیبان ذخیره داده شد؛ حداکثر مجاز $2 {{PLURAL:$2|عملکرد|عملکرد}} است.",
        "backend-fail-usable": "امکان خواندن یا نوشتن پروندهٔ $1 وجود نداشت چرا که سطح دسترسی کافی نیست یا شاخه/محفظهٔ مورد نظر وجود ندارد.",
        "filejournal-fail-dbconnect": "امکان وصل شدن به پایگاه داده دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
-       "filejournal-fail-dbquery": "اÙ\85کاÙ\86 Ø¨Ù\87 Ø±Ù\88ز Ú©Ø±Ø¯Ù\86 Ù¾Ø§Û\8cگاÙ\87 Ø¯Ø§Ø¯Ù\87 دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
+       "filejournal-fail-dbquery": "اÙ\85کاÙ\86 Ø±Ù\88زآÙ\85دسازÛ\8c Ø¯Ø§Ø¯Ú¯Ø§Ù\86 دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
        "lockmanager-notlocked": "نمی‌توان قفل «$1» را گشود؛ چون قفل نشده‌است.",
        "lockmanager-fail-closelock": "امکان بستن پرونده قفل شده \"$1\" وجود ندارد.",
        "lockmanager-fail-deletelock": "امکان حذف پرونده قفل شده \"$1\" وجود ندارد.",
        "filehist-datetime": "تاریح/زمان",
        "filehist-thumb": "بند انگشت",
        "filehist-thumbtext": "بندانگشتی از نسخهٔ مورخ $1",
-       "filehist-nothumb": "فاقد بندانگشتی",
+       "filehist-nothumb": "بندلنکُتکی نے",
        "filehist-user": "کاربر",
        "filehist-dimensions": "جنبه یان",
        "filehist-filesize": "اندازه فایل",
        "watchnologin": "وارد نه بی تگیت",
        "addedwatchtext": "صفحه  \"[[:$1]]\"  په شمی [[Special:Watchlist|watchlist]] هور بیت.\nدیمگی تغییرات په ای صفحه و آیاء صفحه گپ ادان لیست بنت، و صفحه پررنگ جاه کیت ته [[Special:RecentChanges|لیست نوکیت تغییرات]] په راحتر کتن شی که آی زورگ بیت.",
        "removedwatchtext": "صفحه\"[[:$1]]\"  چه [[Special:Watchlist|شمی لیست چارگ]]. دربیت.",
-       "watch": "به چار",
+       "watch": "چار",
        "watchthispage": "ای تاکدیما بگیند",
        "unwatch": "نه چارگ",
        "unwatchthispage": "چارگ بند کن",
        "watchlistedit-raw-done": "شمی لیست چارگ په روچ بیتگت",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 عنوان انت|$1 عناوین ات}} اضافه بوت:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 عنوان|$1 عناوین}} دور بوت:",
-       "watchlisttools-view": "مربوطین تغییرات بچار",
-       "watchlisttools-edit": "به چار و اصلاح کن لیست چارگ آ",
-       "watchlisttools-raw": "Ù\87اÙ\85Û\8cÙ\86 Ù\84Û\8cست Ú\86ارگ Ø¢ Ø§ØµÙ\84اح Ú©ن",
+       "watchlisttools-view": "امبندݔں ٹگلاں چار",
+       "watchlisttools-edit": "چارگءِ لیست‌ئا چار ءُ ٹگلݔنی",
+       "watchlisttools-raw": "Ù\84Û\8cست Ú\86ارگâ\80\8cئا Ù¹Ú¯Ù\84Ý\94ن",
        "iranian-calendar-m1": "فروردین",
        "iranian-calendar-m2": "اردیبهشت",
        "iranian-calendar-m3": "خرداد",
index 63611ff..c347ae2 100644 (file)
        "history": "Гісторыя старонкі",
        "history_short": "Гісторыя",
        "history_small": "гісторыя",
-       "updatedmarker": "абноÑ\9eлена Ð· Ñ\87аÑ\81Ñ\83 Ð¼Ð°Ð¹Ð³Ð¾ апошняга наведваньня",
+       "updatedmarker": "абноÑ\9eлена Ð· Ñ\87аÑ\81Ñ\83 Ð²Ð°Ñ\88ага апошняга наведваньня",
        "printableversion": "Вэрсія для друку",
        "permalink": "Сталая спасылка",
        "print": "Друкаваць",
        "autoblockedtext": "Ваш IP-адрас быў аўтаматычна заблякаваны, таму што ён ужываўся іншым удзельнікам, які быў заблякаваны $1.\nПрычына гэтага:\n\n:<em>$2</em>\n\n* Блякаваньне пачалося: $8\n* Блякаваньне скончыцца: $6\n* Быў заблякаваны: $7\n\nВы можаце скантактавацца з $1 ці з адным зь іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб абмеркаваць блякаваньне.\n\nЗаўважце, што вы ня зможаце ўжываць магчымасьць «{{int:emailuser}}», пакуль ня будзе пазначаны дзейны адрас электроннай пошты ў вашых [[Special:Preferences|наладах удзельніка]], і калі гэта вам не было забаронена.\n\nВаш цяперашні IP-адрас — $3, ідэнтыфікатар блякаваньня — #$5.\nКалі ласка, улучайце ўсю вышэйпададзеную інфармацыю ва ўсе запыты, што вы будзеце рабіць.",
        "systemblockedtext": "Вашае імя ўдзельніка ці IP-адрас былі аўтаматычна заблякаваныя MediaWiki.\nЗ наступнай прычыны:\n\n:<em>$2</em>\n\n* Пачатак блякаваньня: $8\n* Сканчэньне блякаваньня: $6\n* Мэта блякаваньня: $7\n\nВаш цяперашні IP-адрас — $3.\nКалі ласка, уключайце ўсе пададзеныя вышэй дэталі ва ўсе запыты, што вы робіце.",
        "blockednoreason": "прычына не пазначана",
+       "blockedtext-composite": "<strong>Вашае імя ўдзельніка ці IP-адрас былі заблякаваныя.</strong>\n\nПададзеная прычына:\n\n:<em>$2</em>.\n\n* Пачатак блякаваньня: $8\n* Сканчэньне найдаўжэйшага з блякаваньняў: $6\n\nВаш цяперашні IP-адрас — $3.\nКалі ласка, дадайце ўсе падрабязнасьці, прыведзеныя вышэй, у запыты, што вы будзеце рабіць.",
+       "blockedtext-composite-reason": "Маецца некалькі блякаваньняў вашага рахунку і/ці IP-адрасу",
        "whitelistedittext": "Вам трэба $1, каб рэдагаваць старонкі.",
        "confirmedittext": "Вы мусіце пацьвердзіць Ваш адрас электроннай пошты перад рэдагаваньнем старонак. Калі ласка, пазначце і пацьвердзіце адрас электроннай пошты праз Вашы [[Special:Preferences|налады]].",
        "nosuchsectiontitle": "Немагчыма знайсьці сэкцыю",
        "unwatch": "Не назіраць",
        "unwatchthispage": "Перастаць назіраць",
        "notanarticle": "Не старонка зьместу",
-       "notvisiblerev": "Ð\92Ñ\8dÑ\80Ñ\81Ñ\96Ñ\8f была выдаленая",
+       "notvisiblerev": "Ð\90поÑ\88нÑ\8fÑ\8f Ð²Ñ\8dÑ\80Ñ\81Ñ\96Ñ\8f Ð°Ñ\9eÑ\82аÑ\80Ñ\81Ñ\82ва Ñ\96нÑ\88ага Ñ\9eдзелÑ\8cнÑ\96ка была выдаленая",
        "watchlist-details": "У вашым сьпісе назіраньня $1 {{PLURAL:$1|старонка|старонкі|старонак}} (плюс старонкі размоваў).",
-       "wlheader-enotif": "Апавяшчэньне па e-mail уключанае.",
+       "wlheader-enotif": "Апавяшчэньне праз электронную пошту ўключанае.",
        "wlheader-showupdated": "Старонкі, зьмененыя з часу вашага апошняга візыту, вылучаныя <strong>тоўстым</strong> шрыфтам.",
        "wlnote": "Ніжэй {{PLURAL:$1|паказаная <strong>$1</strong> апошняя зьмена|паказаныя <strong>$1</strong> апошнія зьмены|паказаныя <strong>$1</strong> апошніх зьменаў}} за <strong>$2</strong> {{PLURAL:$2|гадзіну|гадзіны|гадзінаў}}, па стане на $4 $3.",
        "wlshowlast": "Паказаць за апошнія $1 гадзінаў, $2 дзён",
        "watchlist-options": "Налады сьпісу назіраньня",
        "watching": "Дадаецца ў сьпіс назіраньня…",
        "unwatching": "Выдаляецца са сьпісу назіраньня…",
-       "watcherrortext": "УзÑ\8cнÑ\96кла Ð¿Ð°Ð¼Ñ\8bлка Ð¿Ð°Ð´Ñ\87аÑ\81 Ð·Ñ\8cменÑ\8b Ð\92ашага сьпісу назіраньня для «$1».",
+       "watcherrortext": "УзÑ\8cнÑ\96кла Ð¿Ð°Ð¼Ñ\8bлка Ð¿Ð°Ð´Ñ\87аÑ\81 Ð·Ñ\8cменÑ\8b Ð½Ð°Ð»Ð°Ð´Ð°Ñ\9e Ð²ашага сьпісу назіраньня для «$1».",
        "enotif_reset": "Пазначыць усе старонкі як прагледжаныя",
        "enotif_impersonal_salutation": "Удзельнік {{GRAMMAR:родны|{{SITENAME}}}}",
-       "enotif_subject_deleted": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð²Ñ\8bдаленаÑ\8f {{GENDER:$2|Ñ\83дзелÑ\8cнÑ\96кам|Ñ\83дзельніцай}} $2",
-       "enotif_subject_created": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ñ\81Ñ\82воÑ\80анаÑ\8f {{GENDER:$2|Ñ\83дзелÑ\8cнÑ\96кам|Ñ\83дзельніцай}} $2",
-       "enotif_subject_moved": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð¿ÐµÑ\80анеÑ\81енаÑ\8f {{GENDER:$2|Ñ\83дзелÑ\8cнÑ\96кам|Ñ\83дзельніцай}} $2",
-       "enotif_subject_restored": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð°Ð´Ð½Ð¾Ñ\9eленаÑ\8f {{GENDER:$2|Ñ\83дзелÑ\8cнÑ\96кам|Ñ\83дзельніцай}} $2",
-       "enotif_subject_changed": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð·Ñ\8cмененаÑ\8f {{GENDER:$2|Ñ\83дзелÑ\8cнÑ\96кам|Ñ\83дзельніцай}} $2",
+       "enotif_subject_deleted": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð²Ñ\8bдаленаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
+       "enotif_subject_created": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ñ\81Ñ\82воÑ\80анаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
+       "enotif_subject_moved": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð¿ÐµÑ\80анеÑ\81енаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
+       "enotif_subject_restored": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð°Ð´Ð½Ð¾Ñ\9eленаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
+       "enotif_subject_changed": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð·Ñ\8cмененаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
        "enotif_body_intro_deleted": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была выдаленая $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, глядзіце $3.",
        "enotif_body_intro_created": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была створаная $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, па цяперашнюю вэрсію глядзіце $3.",
        "enotif_body_intro_moved": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была перанесеная $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, па цяперашнюю вэрсію глядзіце $3.",
index 042613f..f1bbf9b 100644 (file)
        "action-changetags": "добавяне и премахване на произволни етикети на индивидуални редакции и записи в дневниците",
        "action-deletechangetags": "изтриване на етикети от базата от данни",
        "action-purge": "почисти кеша на тази страница",
+       "action-ipblock-exempt": "пренебрегване на IP блокирания, автоматични блокирания и блокирани диапазони",
        "nchanges": "$1 {{PLURAL:$1|промяна|промени}}",
        "ntimes": "$1×",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|от последното посещение}}",
index fe1c4d6..4ba7b23 100644 (file)
        "history": "Historial de canvis",
        "history_short": "Historial",
        "history_small": "historial",
-       "updatedmarker": "actualitzat des de la darrera visita",
+       "updatedmarker": "actualitzat des de la vostra darrera visita",
        "printableversion": "Versió per a impressora",
        "permalink": "Enllaç permanent",
        "print": "Imprimir",
        "undeleterevision-missing": "La revisió no és vàlida o no hi és. Podeu tenir-hi un enllaç incorrecte, o bé pot haver-se restaurat o eliminat de l'arxiu.",
        "undeleterevision-duplicate-revid": "No s'ha pogut restaurar {{PLURAL:$1|una revisió|$1 revisions}}, perquè {{PLURAL:$1|el seu|els seus}} <code>rev_id</code> ja s'estaven fent servir.",
        "undelete-nodiff": "No s'ha trobat cap revisió anterior.",
-       "undeletebtn": "Restaura!",
+       "undeletebtn": "Restaura",
        "undeletelink": "mira/restaura",
        "undeleteviewlink": "veure",
        "undeleteinvert": "Invertir selecció",
        "passwordpolicies-policyflag-forcechange": "cal canviar a l'inici de sessió",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suggereix canvi a l'inici de sessió",
        "easydeflate-invaliddeflate": "El contingut proporcionat no està deflactat adequadament",
-       "unprotected-js": "Per motius de seguretat, el JavaScript no es pot carregar de les pàgines desprotegides. Creeu javascript en l'espai de noms MediaWiki o en una subpàgina d'usuari"
+       "unprotected-js": "Per motius de seguretat, el JavaScript no es pot carregar de les pàgines desprotegides. Creeu javascript en l'espai de noms MediaWiki o en una subpàgina d'usuari",
+       "userlogout-continue": "Voleu finalitzar la sessió?"
 }
index 359d6ed..9da77d9 100644 (file)
@@ -61,6 +61,7 @@
        "tog-norollbackdiff": "Cék-hèng huòi-gūng ī-hâiu ng-sāi hiēng-sê chă-biék",
        "tog-useeditwarning": "我編輯頁面其時候離開,起動警告我蜀下",
        "tog-prefershttps": "Láuk-diē ī-hâiu tié-lāu sāi ăng-ciòng lièng-giék",
+       "tog-showrollbackconfirmation": "Dók huòi-tó̤i liêng-ciék gì sì-hâiu hiēng-sê káuk-nêng tì-sê",
        "underline-always": "直頭",
        "underline-never": "頭𡅏無",
        "underline-default": "皮膚或者瀏覽器默認其",
        "returnto": "轉去$1。",
        "tagline": "Chók-cê̤ṳ {{SITENAME}}",
        "help": "Bŏng-cô",
+       "help-mediawiki": "MediaWiki gì siók-mìng",
        "search": "Sìng-tō̤",
        "searchbutton": "Sìng-tō̤",
        "go": "去",
index 60a6acd..99edfcb 100644 (file)
@@ -22,7 +22,8 @@
                        "Lost Whispers",
                        "Épine",
                        "Fitoschido",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "ئارام بکر"
                ]
        },
        "tog-underline": "ھێڵھێنان بەژێر بەستەرەکان:",
        "yourpassword": "تێپەڕوشە:",
        "userlogin-yourpassword": "تێپەڕوشە",
        "userlogin-yourpassword-ph": "تێپەڕوشەکەت بنووسە",
-       "createacct-yourpassword-ph": "تێپەروشەیەک بنووسە",
+       "createacct-yourpassword-ph": "تێپەڕوشەیەک بنووسە",
        "yourpasswordagain": "دیسان تێپەڕوشەکە بنووسەوە:",
-       "createacct-yourpasswordagain": "تێپەروشە پشتڕاست بکەرەوە",
-       "createacct-yourpasswordagain-ph": "تێپەروشە دیسان بنووسەوە",
+       "createacct-yourpasswordagain": "تێپەڕوشە پشتڕاست بکەرەوە",
+       "createacct-yourpasswordagain-ph": "تێپەڕوشە دیسان بنووسەوە",
        "userlogin-remembermypassword": "لەژوورەوە بمھێڵەرەوە",
        "userlogin-signwithsecure": "پەیوەندیی دڵنیا بەکاربھێنە",
        "cannotlogin-title": "ناتوانیت بچیتە ژوورەوە",
        "createaccountmail-help": "دەتوانرێت بەکار بھێندرێت بۆ دروستکردنی ھەژمار بۆ کەسێکی تر بەبێ زانینی تێپەڕ وشەکەی.",
        "createacct-realname": "ناوی ڕاستی (دڵخوازانە)",
        "createacct-reason": "ھۆکار",
-       "createacct-reason-ph": "بۆ ھەژمارێکی تر دروست دەکەی",
+       "createacct-reason-ph": "بۆچی ھەژمارێکی تر دروست دەکەیت",
        "createacct-submit": "ھەژمارەکەت دروست بکە",
        "createacct-another-submit": "ھەژمار دروست بکە",
        "createacct-continue-submit": "بەردەوامبوون لە دروستکردنی ھەژمار",
        "badretype": "تێپەڕوشەکان لەیەک ناچن.",
        "usernameinprogress": "دروستکردنی ھەژمارێک بۆ ئەم ناوی بەکارھێنەرە لە پڕۆسەی بەرھەمھێناندایە. تکایە چاوەڕوان بە.",
        "userexists": "ئەو ناوەی تۆ داوتە پێشتر بەکارھێنراوە.\nناوێکی دیکە ھەڵبژێرە.",
+       "createacct-normalization": "بەھۆی بەستنەوە تەکنیکییەکان ناوە بەکارھێنەرییەکەت دەگۆڕدرێت بۆ \"$2\".",
        "loginerror": "ھەڵەی چوونەژوورەوە",
        "createacct-error": "ھەڵە لە دروستکردنی ھەژمار",
        "createaccounterror": "ناتوانیت هەژماری بەکارهێنەر دروست بکەیت: $1",
        "nocookiesnew": "ھەژماری بەکارھێنەری دروست کرا، بەڵام نەچوویتەوە ژوورەوە.\n{{SITENAME}} بۆ چوونەوە ژوورەوەی بەکارھێنەر کوکی بەکاردەھێنێت.\nتۆ کوکییەکەکەت لەکارخستووە.\nتکایە کوکییەکە کارا بکە، پاشان بە ناوی بەکارھێنەری و تێپەڕوشەکەت بچۆ ژوورەوە.",
        "nocookieslogin": "{{SITENAME}} بۆ چوونەژوورەوە لە کووکی‌یەکان کەڵک وەرئەگرێت.\nڕێگەت نەداوە بە کووکی‌یەکان.\nڕێگەیان پێ بدەو و دیسان تێبکۆشە.",
        "nocookiesfornew": "ھەژماری بەکارھێنەری دروست نەکرا، چون ناتوانین سەرچاوەکەی پشتڕاست بکەینەوە.\nدڵنیا بە کوکییەکانت چالاک کردووە، پەڕەکە بار بکەوە و دیسان ھەوڵ بدە.",
-       "createacct-loginerror": "ھەژمارەکە بە سەرکەوتوانە دروست کرا، بەڵام ناتوانرێت بە شێوەیەکی ئۆتۆماتیکی بکرێیتە ژوورەوە. تکایە سەردانی [[Special:UserLogin|ڕێنماییەکانی چوونەژوورەوە]] بکە.",
+       "createacct-loginerror": "ھەژمارەکە بە سەرکەوتووانە دروست کرا، بەڵام ناتوانرێت بە شێوەیەکی خۆکارانە بکرێیتە ژوورەوە. تکایە سەردانی [[Special:UserLogin|ڕێنماییەکانی چوونەژوورەوە]] بکە.",
        "noname": "ناوی بەکارهێنەرییەکی گۆنجاوت دیاری نەکردووه.",
        "loginsuccesstitle": "چوویە ناوەوە",
        "loginsuccess": "'''ئێستا بە ناوی «$1»ەوە لە {{SITENAME}} چوویتەتەژوورەوە.'''",
        "tooltip-t-contributions": "پێڕستی بەشدارییەکانی {{GENDER:$1|ئەم بەکارھێنەرە}}",
        "tooltip-t-emailuser": "ئیمەیڵێک بنێرە بۆ {{GENDER:$1|ئەم بەکارھێنەرە}}",
        "tooltip-t-info": "زانیاری زیاتر لەبارەی ئەم پەڕەیەوە",
-       "tooltip-t-upload": "پەڕگە بار بکە",
+       "tooltip-t-upload": "پەڕگەکان بار بکە",
        "tooltip-t-specialpages": "پێڕستی ھەموو پەڕە تایبەتەکان",
        "tooltip-t-print": "وەشانی چاپی ئەم پەڕەیە",
        "tooltip-t-permalink": "گرێدەری ھەمیشەیی بۆ ئەم وەشانەی ئەم پەڕەیە",
index 48797ce..dd1630d 100644 (file)
@@ -10,7 +10,8 @@
                        "Умар",
                        "Macofe",
                        "Danvintius Bookix",
-                       "Stephanecbisson"
+                       "Stephanecbisson",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "Багълантыларнынъ тюбюни сызув:",
        "undelete": "Ёкъ этильген саифелерни косьтер",
        "undeletepage": "Саифенинъ ёкъ этильген версияларына козь ат ве кери кетир.",
        "viewdeletedpage": "Ёкъ этильген саифелерге бакъ",
-       "undeletebtn": "Кери кетир!",
+       "undeletebtn": "Кери кетир",
        "undeletelink": "косьтер/кери кетир",
        "undeletecomment": "Себеп:",
        "undelete-header": "Кеченлерде ёкъ этильген саифелерни корьмек ичюн [[Special:Log/delete|ёкъ этюв журналына]] бакъынъыз.",
index 18a354f..0ba17ed 100644 (file)
@@ -6,7 +6,8 @@
                        "Urhixidur",
                        "아라",
                        "Macofe",
-                       "Stephanecbisson"
+                       "Stephanecbisson",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "Bağlantılarnıñ tübüni sızuv:",
        "undelete": "Yoq etilgen saifelerni köster",
        "undeletepage": "Saifeniñ yoq etilgen versiyalarına köz at ve keri ketir.",
        "viewdeletedpage": "Yoq etilgen saifelerge baq",
-       "undeletebtn": "Keri ketir!",
+       "undeletebtn": "Keri ketir",
        "undeletelink": "köster/keri ketir",
        "undeletecomment": "Sebep:",
        "undelete-header": "Keçenlerde yoq etilgen saifelerni körmek içün [[Special:Log/delete|yoq etüv jurnalına]] baqıñız.",
index b5c6e88..a4b4b79 100644 (file)
@@ -43,7 +43,8 @@
                        "Radana",
                        "Jan Růžička",
                        "Jaroslav Cerny",
-                       "Slepi"
+                       "Slepi",
+                       "Tchoř"
                ]
        },
        "tog-underline": "Podtrhávat odkazy:",
        "history": "Historie stránky",
        "history_short": "Historie",
        "history_small": "historie",
-       "updatedmarker": "změněno od poslední návštěvy",
+       "updatedmarker": "změněno od vaší poslední návštěvy",
        "printableversion": "Verze k tisku",
        "permalink": "Trvalý odkaz",
        "print": "Vytisknout",
        "autoblockedtext": "Vaše IP adresa byla automaticky zablokována, protože ji používal jiný uživatel, kterého zablokoval $1.\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec blokování: $6\n* Původně blokovaný uživatel: $7\n\nZablokování můžete prodiskutovat se správcem $1 nebo některým z dalších [[{{MediaWiki:Grouppage-sysop}}|správců]].\n\nUvědomte si však, že funkci „{{int:emailuser}}“ nemůžete použít, pokud nemáte ve svém [[Special:Preferences|uživatelském nastavení]] zadaný platný e-mail a nebylo vám zablokováno jeho užívání.\n\nVaše současná IP adresa je $3, číslo vašeho zablokování je #$5.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
        "systemblockedtext": "Vaše IP adresa byla automaticky zablokována softwarem MediaWiki.\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec blokování: $6\n* Původně blokovaný uživatel: $7\n\nVaše současná IP adresa je $3.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
        "blockednoreason": "důvod nebyl zadán",
+       "blockedtext-composite": "<strong>Vaše uživatelské jméno nebo IP adresa byla zablokována.</strong>\n\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec nejdelšího blokování: $6\n\nVaše současná IP adresa je $3.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
        "whitelistedittext": "Pro editaci se musíte $1.",
        "confirmedittext": "Pro editaci stránek je vyžadováno potvrzení vaší e-mailové adresy.\nNa stránce [[Special:Preferences|nastavení]] zadejte a nechte potvrdit svou e-mailovou adresu.",
        "nosuchsectiontitle": "Sekce nenalezena",
        "lockmanager-fail-closelock": "Soubor se zámkem pro „$1“ nelze zavřít.",
        "lockmanager-fail-deletelock": "Soubor se zámkem pro „$1“ nelze smazat.",
        "lockmanager-fail-acquirelock": "Zámek pro „$1“ nelze získat.",
-       "lockmanager-fail-openlock": "Soubor zámku „$1“ nelze otevřít. Ujistěte se, že váš adresář nahraných souborů je správně nakonfigurován a že váš webový server má povolení k zápisu do tohoto adresáře. Pro další informace viz https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUploadDirectory.",
+       "lockmanager-fail-openlock": "Soubor zámku „$1“ nelze otevřít. Ujistěte se, že váš adresář nahraných souborů je správně nakonfigurován a že váš webový server má povolení k zápisu do tohoto adresáře. Pro další informace vizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUploadDirectory.",
        "lockmanager-fail-releaselock": "Zámek pro „$1“ nelze uvolnit.",
        "lockmanager-fail-db-bucket": "Nelze navázat spojení s dostatečným počtem databází zámků v bloku $1.",
        "lockmanager-fail-db-release": "Uzamčení databáze $1 nelze uvolnit.",
        "uploadstash-zero-length": "Soubor má nulovou délku.",
        "invalid-chunk-offset": "Neplatný posun bloku",
        "img-auth-accessdenied": "Přístup odepřen",
-       "img-auth-nopathinfo": "Chybí informace o cestě.\nVáš server musí být nastaven tak, aby předával proměnné REQUEST_URI nebo PATH_INFO.\nPokud je, zkuste zapnout $wgUsePathInfo.\nViz https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
+       "img-auth-nopathinfo": "Chybí informace o cestě.\nVáš server musí být nastaven tak, aby předával proměnné REQUEST_URI nebo PATH_INFO.\nPokud je, zkuste zapnout $wgUsePathInfo.\nVizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "img-auth-notindir": "Požadovaná cesta nespadá pod nakonfigurovaný adresář s načtenými soubory.",
        "img-auth-badtitle": "Z „$1“ nelze vytvořit platný název stránky.",
        "img-auth-nofile": "Soubor „$1“ neexistuje.",
        "unusedimages": "Nepoužívané soubory",
        "wantedcategories": "Chybějící kategorie",
        "wantedpages": "Chybějící stránky",
-       "wantedpages-summary": "Seznam neexistujících stránek, na které vede nejvíce odkazů, kromě stránek, na které odkazují jen přesměrování. Pro seznam neexistujících stránek, na které odkazují přesměrování, viz [[{{#special:BrokenRedirects}}|seznam přerušených přesměrování]].",
+       "wantedpages-summary": "Seznam neexistujících stránek, na které vede nejvíce odkazů, kromě stránek, na které odkazují jen přesměrování. Pro seznam neexistujících stránek, na které odkazují přesměrování, vizte [[{{#special:BrokenRedirects}}|seznam přerušených přesměrování]].",
        "wantedpages-badtitle": "Výsledky obsahují neplatný název: $1",
        "wantedfiles": "Chybějící soubory",
        "wantedfiletext-cat": "Následující soubory se používají, ale neexistují. Soubory ze vzdálených úložišť zde mohou být uvedeny, přestože existují. Taková falešná pozitiva budou zobrazena <del>přeškrtnutě</del>. Stránky, které vkládají neexistující soubory, jsou navíc uvedeny v [[:$1]].",
        "index-category-desc": "Stránka obsahuje kouzelné slovo <code><nowiki>__INDEX__</nowiki></code> (a je ve jmenném prostoru, ve kterém je tento příznak dovolen), takže je indexována roboty, přestože by normálně nebyla.",
        "post-expand-template-inclusion-category-desc": "Stránka je po rozbalení všech šablon větší než <code>$wgMaxArticleSize</code>, takže některé šablony rozbaleny nebyly.",
        "post-expand-template-argument-category-desc": "Stránka je po rozbalení argumentu šablony (něco v trojitých závorkách, např. <code>{{{Foo}}}</code>) větší než <code>$wgMaxArticleSize</code>.",
-       "expensive-parserfunction-category-desc": "Stránka používá příliš mnoho náročných funkcí syntaktického analyzátoru (jako <code>#ifexist</code>). Viz [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgExpensiveParserFunctionLimit Manual:$wgExpensiveParserFunctionLimit].",
+       "expensive-parserfunction-category-desc": "Stránka používá příliš mnoho náročných funkcí syntaktického analyzátoru (jako <code>#ifexist</code>). Vizte [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgExpensiveParserFunctionLimit Manual:$wgExpensiveParserFunctionLimit].",
        "broken-file-category-desc": "Stránka obsahuje nefunkční odkaz na soubor (odkaz pro vložení souboru, který neexistuje).",
        "hidden-category-category-desc": "Kategorie ve svém textu obsahuje <code><nowiki>__HIDDENCAT__</nowiki></code>, což způsobuje, že se na stránkách implicitně nezobrazuje v rámečku odkazů na kategorie.",
        "trackingcategories-nodesc": "Popis není k dispozici.",
        "blocklog-showsuppresslog": "{{GENDER:$1|Tento uživatel byl zablokován a skryt|Tato uživatelka byla zablokována a skryta}}. Zde je pro přehled zobrazen výpis záznamu utajení:",
        "blocklogentry": "blokuje „[[$1]]“ s časem vypršení $2 $3",
        "reblock-logentry": "mění nastavení bloku „[[$1]]“ s časem vypršení $2 $3",
-       "blocklogtext": "Toto je kniha úkonů blokování a odblokování uživatelů.\nAutomaticky blokované IP adresy nejsou vypsány.\nViz též [[Special:BlockList|seznam všech probíhajících bloků]].",
+       "blocklogtext": "Toto je kniha úkonů blokování a odblokování uživatelů.\nAutomaticky blokované IP adresy nejsou vypsány.\nVizte též [[Special:BlockList|seznam všech probíhajících bloků]].",
        "unblocklogentry": "odblokovává „$1“",
        "block-log-flags-anononly": "pouze anonymní uživatelé",
        "block-log-flags-nocreate": "vytváření účtů zablokováno",
        "edit-error-long": "Chyby:\n\n$1",
        "revid": "revize $1",
        "pageid": "Stránka s ID $1",
-       "interfaceadmin-info": "$1\n\nOprávnění editovat celoprojektové soubory s CSS/JS/JSON bylo nedávno odděleno z oprávnění <code>editinterface</code>. Pokud nerozumíte, proč se vám zobrazuje tato chyba, viz [[mw:MediaWiki_1.32/interface-admin]].",
+       "interfaceadmin-info": "$1\n\nOprávnění editovat celoprojektové soubory s CSS/JS/JSON bylo nedávno odděleno z oprávnění <code>editinterface</code>. Pokud nerozumíte, proč se vám zobrazuje tato chyba, vizte [[mw:MediaWiki_1.32/interface-admin]].",
        "rawhtml-notallowed": "Značky &lt;html&gt; nelze používat mimo běžné stránky.",
        "gotointerwiki": "Opustit {{GRAMMAR:4sg|{{SITENAME}}}}",
        "gotointerwiki-invalid": "Zadaný název je neplatný.",
index 51f10b8..70eb328 100644 (file)
@@ -12,7 +12,8 @@
                        "Chuvash2014",
                        "Macofe",
                        "Chuvash",
-                       "Marat-avgust"
+                       "Marat-avgust",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "Ссылкăсене аялтан туртса палармалла:",
        "undelete": "Кăларса пăрахнă страницăсене пăх",
        "viewdeletedpage": "Кăларса пăрахнă страницăсене пăх",
        "undeleterevisions": "$1 {{PLURAL:$1|верси|версисене}} пăса утнă",
-       "undeletebtn": "Каялла тавăр!",
+       "undeletebtn": "Каялла тавăр",
        "undeleteviewlink": "пăх",
        "undelete-search-box": "Кăларса пăрахнă страницăсен хушшинчи шырав",
        "undelete-search-submit": "Шыра",
index 838980e..17e2516 100644 (file)
        "history": "Tarixê perrer",
        "history_short": "Veror",
        "history_small": "tarix",
-       "updatedmarker": "cı kewtena mına peyêne ra dıme biyo rocane",
+       "updatedmarker": "ziyaretê peyêni dıma biyo rocane",
        "printableversion": "Versiyonê çapkerdışi",
        "permalink": "Gıreyo daimi",
        "print": "Bınuşne",
        "redirectedfrom": "($1 ra kırışı yê)",
        "redirectpagesub": "Perra kırıştışi",
        "redirectto": "Kırışêno:",
-       "lastmodifiedat": "Ena perre roca $1 de, saete $2 de vırriye.",
+       "lastmodifiedat": "Ena pela roca $1 de, sehate $2 de vıriyaya",
        "viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.",
        "protectedpage": "Pera pawıyayi",
        "jumpto": "Şo be:",
        "nstab-category": "Kategoriye",
        "mainpage-nstab": "Pela seri",
        "nosuchaction": "Fealiyeto wınasi çıniyo",
-       "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
+       "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta aşkera bıkero.",
        "nosuchspecialpage": "Pela hısusiya wınasiyên çıniya.",
        "nospecialpagetext": "<strong>To yew pela xasa nêvêrdiye waşte.</strong>\n\nSeba lista pelanê xasanê vêrdeyan reca kena: [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Xeta",
        "mycustomjsprotected": "Desturê şıma çıniyo ke na pela JavaScripti bıvurnê.",
        "myprivateinfoprotected": "Ğısusi malumatana ğo timar kerdışire icazeta şıma çıniya.",
        "mypreferencesprotected": "Terciha timar kerdışire icazeta şıam çıniya.",
-       "ns-specialprotected": "Pelê xısusiyi nêşenê bıvurriyê.",
-       "titleprotected": "No sername terefê [[User:$1|$1]] ra, afernayene ra şevekiyayo.\nSebebê xo <em>$2</em> dero.",
+       "ns-specialprotected": "Pelanê bağseya şıma nêşenê bıvurnê.",
+       "titleprotected": "No sername terefê [[User:$1|$1]] ra, afernayene ra şevekiyayo.\nSebebê cı <em>$2</em> de deya yo.",
        "filereadonlyerror": "Dosyay vurnayışê \"$1\" nê abêno lakin depoy dosya da \"$2\" mod dê  salt wendi de yo.\n\nXızmetkarê  kılit kerdışi wa bewniro enay wa çım ra ravyarn o: \"$3\".",
        "invalidtitle": "Sernuşteyo nêravêrde",
        "invalidtitle-knownnamespace": "Canemey \"$2\" u metnê \"$3\" xırabo",
        "publishchanges": "Vurnayışan qeyd ke",
        "savearticle-start": "Pele qeyd ke...",
        "savechanges-start": "Vurnayışan qeyd ke...",
-       "publishpage-start": "Pele weşane...",
-       "publishchanges-start": "Vurnayışan weşane...",
+       "publishpage-start": "Riperri aşkera ke...",
+       "publishchanges-start": "Vırnayışan aşkera ke...",
        "preview": "Verqayt",
        "showpreview": "Verasayışi bımocne",
        "showdiff": "Vurnayışan bımocne",
        "viewpagelogs": "Qeydanê na pele bımocne",
        "nohistory": "Verorê vurnayışanê na perer çıni yo.",
        "currentrev": "Çımraviyarnayışo rocane",
-       "currentrev-asof": "Çımraviyarnayışê $1iyo peyên",
+       "currentrev-asof": "$1 ra tepiya weziyeta pela",
        "revisionasof": "Çımraviyarnayışê $1",
        "revision-info": "Vurnayışo ke $1 de terefê {{GENDER:$6|$2}}$7 ra biyo",
        "previousrevision": "← Çımraviyarnayışo kıhanêr",
        "difference-title": "Pela \"$1\" ferqê çım ra viyarnayışan",
        "difference-title-multipage": "Ferkê pelan dê \"$1\" u \"$2\"",
        "difference-multipage": "(Ferqê pelan)",
-       "lineno": "Xeta $1:",
+       "lineno": "Satır $1:",
        "compareselectedversions": "Rewizyonanê weçineyan pêver ke",
        "showhideselectedversions": "weçinaye revizyona bımotne/bınımne",
        "editundo": "peyser bıgê",
        "saveusergroups": "Grubanê {{GENDER:$1|karberi}} qeyd bıke",
        "userrights-groupsmember": "Ezayê:",
        "userrights-groupsmember-auto": "Ezao daxıl/ezaa daxıle ê:",
-       "userrights-groups-help": "şıma şenê grubanê nê karberi/na karbere, oyo/aya ke tede, bıvurnê:\n* qutiya ke nışankerdiya, mocnena ke karber/e na grube dero/dera.\n* qutiya ke nışankerdiye niya, mocnena ke karber/ na grube de niyo/niya.\n* Yew estare * mocneno ke, gruba ke şıma kerda ra ser (daxıl kerda), şıma nêşenê wedarê/hewa dê ya ki dêmlaşta/tersê cı.",
+       "userrights-groups-help": "şıma şenê grubanê nê karberi/na karbere, oyo/aya ke tede, bıvurnê:\n* qutiya ke nışankerdiya, mocnena ke karber/e na grube de yo/de ya.\n* qutiya ke nışankerdiye niya, mocnena ke karber/ na grube de niyo/niya.\n* Yew estare * mocneno ke, gruba ke şıma kerda ra ser (daxıl kerda), şıma nêşenê wedarê/hewa dê ya ki dêmlaşta/tersê cı.",
        "userrights-reason": "Sebeb:",
        "userrights-no-interwiki": "Heqa şıma çıniya ke heqanê karberanê Wikipediyanê binan sero bıgureyê.",
        "userrights-nodatabase": "Database $1 çıniyo ya zi mehelli niyo.",
        "right-reupload-own": "Dosyeyê ke to bar kerdi, inan sero bınuse",
        "right-reupload-shared": "Dosyeyê ke ambarê medyao barekerde de, inan mehelli wedare",
        "right-upload_by_url": "Yew URL ra dosyeyan bar ke",
-       "right-purge": "Virê sita seba yew pele bêdestur bestere.",
+       "right-purge": "Qandê yew pela vervirê site bıesterne",
        "right-autoconfirmed": "Perê ke nême kılit biyê, inan bıvurne",
        "right-bot": "Zey yew karê otomatiki kar bıvêne",
        "right-nominornewtalk": "Pelanê werênayışan rê vurnayışê qıckeki çıniyê, qutiya mesacanê newiyan bıgurene",
        "right-sendemail": "Karberanê binî ra e-mail bişirav",
        "right-managechangetags": "[[Special:Tags|Etiketi]] vıraz u aktiv (me)ke",
        "right-applychangetags": "[[Special:Tags|Etiketa]]  vurnayışana piya dezge fi.",
+       "right-deletechangetags": "Database ra [[Special:Tags|etiketa]] bıesternê",
        "grant-generic": "\"$1\" paketa heqan",
        "grant-group-page-interaction": "Peran na tesiri",
        "grant-group-file-interaction": "Medya na tesiri",
        "action-applychangetags": "Vurnayışana piya etiket kerdışi zi dezge fi",
        "action-deletechangetags": "etitikan danegeh ra bestere",
        "action-purge": "Ane perer newe ke",
+       "action-editprotected": "\"{{int:protect-level-sysop}}\" şeveknaye pêlan de vırnayış bıkerê",
+       "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" deyne şeveknaye pelan dê vurnayış bıkerê",
        "action-editinterface": "miyanriyê karberi bıvurne",
        "action-editusercss": "dosyeyanê CSSyê karberanê binan bıvurne",
        "action-edituserjson": "dosyeyanê JSONiyê karberanê binan bıvurne",
        "action-editmyusercss": "dosyeyanê CSSyê karberiya xo bıvurne",
        "action-editmyuserjson": "dosyeyanê JSONiyê karberiya xo bıvurne",
        "action-editmyuserjs": "dosyeyanê JavaScriptiyê karberiya xo bıvurne",
+       "action-viewsuppressed": "Karberan ra nımneyayen revizyona bıvênê",
+       "action-hideuser": "Yew nameyê karberi şari ra miyanki bloke bıkerê",
+       "action-ipblock-exempt": "Blokanê IPi, oto-blokan u blokanê menzıli ra ravêre",
+       "action-unblockself": "Blpqey ho wedarne",
        "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}",
        "enhancedrc-history": "tarix",
        "rcfilters-hours-title": "Seatê peyêni",
        "rcfilters-days-show-days": "($1 {{PLURAL:$1|roce|roci}})",
        "rcfilters-days-show-hours": "($1 {{PLURAL:$1|saete|saeti}})",
+       "rcfilters-highlighted-filters-list": "Wesıbneyayeni:$1",
        "rcfilters-quickfilters": "Parzûnê qeydbiyayeyi",
        "rcfilters-quickfilters-placeholder-title": "Qet yew parzûn qeyd nêbiyo",
        "rcfilters-quickfilters-placeholder-description": "Eyaranê parzûni qeydkerdış u bahdo zi seba gurenayışi rê, cêr de simgeyanê cayanê parzûnanê aktifan bıtıknê.",
        "rcfilters-empty-filter": "Parzûnê aktifi çıniyê. İştırakê cı pêro mocniyenê.",
        "rcfilters-filterlist-title": "Parzûni",
        "rcfilters-filterlist-whatsthis": "Nê çıtewri guriyenê?",
+       "rcfilters-highlightbutton-title": "Neticeyê wesıbneyayeni",
        "rcfilters-highlightmenu-title": "Yew reng weçine",
        "rcfilters-filterlist-noresults": "Parzûni nêvêniyayi",
        "rcfilters-filtergroup-authorship": "Wayiriya iştırakan",
        "rcfilters-filter-user-experience-level-newcomer-label": "Ameyayeyê neweyi",
        "rcfilters-filter-user-experience-level-newcomer-description": "Karberê qeydınê ke 10 ra kemi vurnayışi ya zi 4 rocan ra fealiyetê xo estê.",
        "rcfilters-filter-user-experience-level-learner-label": "Musayoği",
-       "rcfilters-filter-user-experience-level-learner-description": "Vurnayoğê qeydınê ke cerrebnayışê cı \"Neweameyoği\" û \"Karberê westay\"an miyan dero.",
+       "rcfilters-filter-user-experience-level-learner-description": "Vurnayoğê qeydınê ke cerrebnayışê cı \"Neweameyoği\" u \"Karberê westay\"an miyan de yo.",
        "rcfilters-filter-user-experience-level-experienced-label": "Karberê mısayeyi",
        "rcfilters-filter-user-experience-level-experienced-description": "Vurnayoğê qeydınê ke 30 roce ra zêdêr fealiyet û wayirê 500 ra zêdêr vurnayışanê.",
        "rcfilters-filtergroup-automated": "İştırakê otomatiki",
        "emptyfile": "dosya ya ke şıma bar kerda veng asena, nameyê dosyayi şaş nusyaya belka.",
        "windows-nonascii-filename": "Na wiki namen de dosyayan de xısusi karaxtera karkerdışa peşti nêdana.",
        "fileexists": "Nê namey ra yew dosya xora esta. Kerem kerên, <strong>[[:$1]]</strong> kontrol kerê {{GENDER:|şıma}} ke emin niyê naye bıvurnê.   \n[[$1|thumb]]",
-       "filepageexists": "qey na dosya pelê eşkera kerdışi <strong>[[:$1]]</strong> na adresi de ca ra vıraziyayo labele no name de yew dosya nêasena.\nkılmnuşteyê şıma nêasena eke şıma qayili bıvini gani şıma pê dest bıvurni\n[[$1|resimo qıc]]",
+       "filepageexists": "Seba na dosyay riperrê aşkera kerdışi <strong>[[:$1]]</strong> nê adresi de ca ra vıraziyao, labelê no name de jû dosya nêasena.\nKılmnuştey şıma nêaseno. Eke şıma qailê bıvênê, gani şıma pê dest bıvırnê\n[[$1|resimo qıc]]",
        "fileexists-extension": "zey no nameyê dosyayi yewna nameyê dosyayi esta: [[$2|thumb]]\n* dosyaya ke bar biya: <strong>[[:$1]]</strong>\n* dosyaya ke ca ra esta: <strong>[[:$2]]</strong>\nkerem kere yewna name bıvıcinê",
        "fileexists-thumbnail-yes": "na dosya wina asena ke versiyona yew resmê qıc biyayeya ''(thumbnail)''. [[$1|thumb]]\nkerem kerê <strong>[[:$1]]</strong> na dosya konrol bıkerê .",
        "file-thumbnail-no": "nameyê na dosyayi pê ney <strong>$1</strong> dest keno pê.\nna manena ke versiyona yew resmê qıc biyaye ya ''(thumbnail)''",
        "upload-options": "Tercihanê bar kerdişî",
        "watchthisupload": "Ena dosya seyr bike",
        "filewasdeleted": "no name de yew dosya yew wexto nızdi de bar biya u dıma zi serkaran hewn a kerdo. wexya ke şıma dosya bar keni bıewnê no pel $1.",
-       "filename-bad-prefix": "name yo ke şıma bar keni zey nameyê kamerayê dijital î, pê ney '''\"$1\"''' destpêkeno .\nkerem kere yewna nameyo eşkera bıvicinê.",
+       "filename-bad-prefix": "nameo ke şıma bar kenê, zey namey kameraya dicitalo, pê '''\"$1\"''' sıfte keno.\nKerem kerên, nameyê do eşkera'o bin weçinên.",
        "filename-prefix-blacklist": " #<!-- leave this line exactly as it is --> <pre>\n# Syntax is as follows:\n#   * Everything from a \"#\" character to the end of the line is a comment\n#   * Every non-blank line is a prefix for typical file names assigned automatically by digital cameras\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # some mobile phones\nIMG # generic\nJD # Jenoptik\nMGP # Pentax\nPICT # misc.\n #</pre> <!-- leave this line exactly as it is -->",
        "upload-proto-error": "Porotokol raşt ni yo.",
        "upload-proto-error-text": "Bar kerdişê durî gani  URLî estbiye ke pe <code>http://</code> ya zi <code>ftp://</code> başli beno.",
        "linkstoimage-redirect": "$1 (Dosya raçarnayış) $2",
        "duplicatesoffile": "a {{PLURAL:$1|dosya|$1 dosya}}, kopyayê na dosyayi ([[Special:FileDuplicateSearch/$2|teferruati]]):",
        "sharedupload": "Ena dosya $1 ra u belki projeyê binan dı hewitiyeno.",
-       "sharedupload-desc-there": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [şınasiya dosyay pela $2] mocniyeno.",
-       "sharedupload-desc-here": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [şınasiya dosyay pela $2] mocniyeno.",
+       "sharedupload-desc-there": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [$2 şınasiya dosyay pela] mocniyeno.",
+       "sharedupload-desc-here": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [$2 şınasiya dosyay pela] mocniyeno.",
        "sharedupload-desc-edit": "Na dosya $1 proceyan dê binandı ke şeno bıgurweyno.\nŞıma qayılê ke malumatê cı bıvurnê se şıre [pela da $2 ].",
        "sharedupload-desc-create": "Na dosya $1 proceyan dê binandı ke şeno bıgurweyno.\nŞıma qayılê ke malumatê cı bıvurnê se şıre [pela da $2 ].",
        "filepage-nofile": "Ena name de dosya çin o.",
        "suppress": "Fetesnayene",
        "querypage-disabled": "Na pelaya xısusi,sebeb de performansi ra qefılneyê.",
        "apihelp": "Peştiya APIyi",
-       "apihelp-no-such-module": "Modulê \"$1\" çıniyo.",
+       "apihelp-no-such-module": "Modulê \"$1\" nêvineya.",
        "apisandbox": "API qumdor",
        "apisandbox-api-disabled": "API na site de dewre ra veciyayo.",
        "apisandbox-submit": "Bıwazê",
        "allpagesfrom": "Herfa kı pa liste bo:",
        "allpagesto": "Perranê ke ena herfe qediyenê bımotne:",
        "allarticles": "Peli pêro",
-       "allinnamespace": "Peli pênro ( $1 cayênameyî)",
+       "allinnamespace": "Peli pêro (Caynamey: $1)",
        "allpagessubmit": "Şo",
        "allpagesprefix": "herfê ke şıma tiya de nuşti, pê ney herfan pelê ke destpêkenê liste ker:",
        "allpagesbadtitle": "pel o ke şıma kewenî cı, nameyê no peli de gıreyê zıwanan u wikiyi re elaqa esto, ê ra cıkewtış qebul niyo. ya zi sernameyan de karakterê qedexeyi tede esto.",
        "delete-warning-toobig": "no pel wayirê tarixê vurnayiş ê derg o, $1 {{PLURAL:$1|revizyonê|revizyonê}} seri de.\nhewn a kerdışê ıney {{SITENAME}} şuxul bıne gırano;\nbı diqqet dewam kerê.",
        "deleteprotected": "Şıma nêşenê ena perer esternê,  çıkı per starya ya.",
        "rollback": "vurnayişan tepiya bıger",
+       "rollback-confirmation-confirm": "Araşt Kerê :",
        "rollback-confirmation-yes": "Peyser biya",
        "rollback-confirmation-no": "Bıtexelne",
        "rollbacklink": "ageyrayış",
        "mycontris": "İştıraki",
        "anoncontribs": "İştıraki",
        "contribsub2": "Qandê {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "Qandê {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "Hesabê karberi \"$1\" qeyd nêbiyo.",
        "nocontribs": "Ena kriteriya de vurnayîş çini yo.",
        "uctop": "weziyet",
        "sp-contributions-newbies-sub": "Qe hesebê newe",
        "sp-contributions-newbies-title": "Hesabanê neweyan rê iştırakê karberi",
        "sp-contributions-blocklog": "qeydê kılitkerdışi",
+       "sp-contributions-suppresslog": "İştirakê {{GENDER:$1|karberiyê}} degusneyayey",
        "sp-contributions-deleted": "iştırakê {{GENDER:$1|karberi}} esterdi",
        "sp-contributions-uploads": "Barkerdışi",
        "sp-contributions-logs": "qeydi",
        "sp-contributions-talk": "werênayış",
        "sp-contributions-userrights": "idareyê heqanê {{GENDER:$1|karberan}}",
-       "sp-contributions-blocked-notice": "verniyê no/na karber/e geriyayo/a\nqê referansi qeydê vernigrewtışi cêr de eşkera biyo:",
+       "sp-contributions-blocked-notice": "Eno karber/ena karbere emanet blokekerdeyo/blokekerdiya.\nCıkewtışo tewr peyêno ke bloke biyo, cêr seba referansi belikerdeyo:",
        "sp-contributions-blocked-notice-anon": "Eno adresê IPi bloke biyo.\nCıkewtışo tewr peyêno ke bloke biyo, cêr seba referansi belikerdeyo:",
        "sp-contributions-search": "Dekerdena cı geyrê",
        "sp-contributions-username": "Adresa IPy ya zi nameyê karberi:",
        "previousdiff": "← Vırnayışê kıhanêr.",
        "nextdiff": "Vurnayışo peyên →",
        "mediawarning": "'''Teme''': Na dosya de belkia kodê xırabıni estê.\nGurênayışê nae de, beno ke sistemê şıma zerar bıvêno.",
-       "imagemaxsize": "Sinorê ebadê resımiyo ke pelanê şınasnayışê dosyeyan dero:",
+       "imagemaxsize": "Sinorê ebadê resımiyo ke pelanê şınasnayışê dosya:",
        "thumbsize": "Ebado werdi:",
        "widthheight": "$1 - $2",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|pele|peli}}",
        "version": "Versiyon",
        "version-extensions": "Ekstensiyonî ke ronaye",
        "version-skins": "Bar kerde bejni",
-       "version-specialpages": "Pelê xısusiyi",
+       "version-specialpages": "Pelê bağsey",
        "version-parserhooks": "Çengelê Parserî",
        "version-variables": "Vurnayeyî",
        "version-editors": "Vurnayoği",
        "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.",
        "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.",
        "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.",
-       "specialpages": "Pelê xısusiyi",
+       "specialpages": "Pelê bağsey",
        "specialpages-note-top": "Kıtabek",
        "specialpages-note-restricted": "* Pelê xasê normali.\n* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
-       "specialpages-group-maintenance": "Rapora pawıtışi",
-       "specialpages-group-other": "Pelê xısusiyê bini",
-       "specialpages-group-login": "Cı kewe / hesab vıraze",
-       "specialpages-group-changes": "Vurnayışê peyêni û qeydi",
-       "specialpages-group-media": "Raporê medya û barkerdışi",
-       "specialpages-group-users": "Karberi u heqê inan",
-       "specialpages-group-highuse": "Pelê ke zêdêr gureniyenê",
-       "specialpages-group-pages": "Listeyê pelan",
+       "specialpages-group-maintenance": "Raporê weynayışi",
+       "specialpages-group-other": "Pelê bağseyê bini",
+       "specialpages-group-login": "Ronıştış akerê / hesab vıraze",
+       "specialpages-group-changes": "Vurnayışê peyêni û Roceki",
+       "specialpages-group-media": "Raporê medyay u barkerdışi",
+       "specialpages-group-users": "Karberi u Heqi",
+       "specialpages-group-highuse": "Pelê zaf karnıyayey",
+       "specialpages-group-pages": "Listey peleyan",
        "specialpages-group-pagetools": "Haletê pelan",
        "specialpages-group-wiki": "Melumat u haceti",
-       "specialpages-group-redirects": "Pelê serşıkıtışiyê xısusiyi",
+       "specialpages-group-redirects": "Pelê bağseyê serşıkıtışini",
        "specialpages-group-spam": "haletê spami",
        "specialpages-group-developer": "Xacetanê raverberdoğî",
        "blankpage": "Pela venge",
        "htmlform-datetime-placeholder": "SSSS-AA-RR SS:DD:SS",
        "logentry-delete-delete": "$1 perra $3 {{GENDER:$2|esterıte}}",
        "logentry-delete-restore": "$1 pela $3 ($4) {{GENDER:$2|peyser arde}}",
+       "logentry-delete-restore-nocount": "$1, pela $3 {{GENDER:$2|timar kerd }}",
        "restore-count-revisions": "{{PLURAL:$1|1 çımraviyarnayış|$1 çımraviyarnayışi}}",
        "restore-count-files": "{{PLURAL:$1|1 dosya|$1 dosyeyi}}",
        "logentry-delete-event": "$1 $3: $4 de asayışê {{PLURAL:$5|cıkerdışi|cıkerdışan}} {{GENDER:$2|vurna}}",
        "revdelete-uname-unhid": "nameyê karberi nênımıteyo",
        "revdelete-restricted": "vergırewtışê ke xızmekaran rê biye",
        "revdelete-unrestricted": "vergırewtışê ke xızmekaran rê dariyê we",
+       "logentry-block-block": "$1, karber {{GENDER:$4|$3}} $5 demi rê {{GENDER:$2|kerd men}} $6",
+       "logentry-block-unblock": "$1, {{GENDER:$4|$3}} {{GENDER:$2|men kerdış wedarna}}",
        "logentry-partialblock-block-page": "{{PLURAL:$1|pele|peli}} $2",
        "logentry-partialblock-block-ns": "{{PLURAL:$1|cayê nameyi|cayê nameyan}} $2",
        "logentry-move-move": "$1, pela $3 ra {{GENDER:$2|kırışt}} pela $4",
        "mw-widgets-titlesmultiselect-placeholder": "Tayêna cı ke...",
        "date-range-from": "Nê tarixi ra:",
        "date-range-to": "Heta nê tarixi:",
+       "sessionprovider-generic": "Ronıştışê $1",
        "randomrootpage": "Pela raştameya rıçıkıne",
        "log-action-filter-block": "Tewrê kılitkerdışi:",
        "log-action-filter-contentmodel": "Tewrê vurnayışê modelê zerreki:",
        "log-action-filter-delete-revision": "Esterıtışê çımraviyarnayışi",
        "log-action-filter-import-interwiki": "Zerrenayışê Transwikiyi",
        "log-action-filter-import-upload": "Ebe barkerdışê XMLi ra zerre ke",
+       "log-action-filter-managetags-create": "Etiket vıraştış",
+       "log-action-filter-managetags-delete": "Etiket esternayış",
+       "log-action-filter-managetags-activate": "Etiket raştkerdış",
+       "log-action-filter-managetags-deactivate": "Etiket hewadayış",
+       "log-action-filter-newusers-autocreate": "Otomatik vıraştış",
+       "log-action-filter-patrol-patrol": "Dewriyeyo menuel",
+       "log-action-filter-patrol-autopatrol": "Dewriyeyo otomatik",
        "log-action-filter-protect-protect": "Şeveknayış",
        "log-action-filter-protect-modify": "Vurnayışê şeveknayışi",
        "log-action-filter-protect-unprotect": "Şeveknayışi wedare",
index af10a59..ffcdf09 100644 (file)
        "cachedspecial-refresh-now": "Προβολή τελευταίας.",
        "categories": "Κατηγορίες",
        "categories-submit": "Εμφάνιση",
-       "categoriespagetext": "{{PLURAL:$1|Η ακόλουθη κατηγορία υπάρχει|Οι ακόλουθες κατηγορίες υπάρχουν}} σε αυτό το wiki, και μπορεί ή μπορεί να μην είναι {{PLURAL:$1|αχρησιμοποίητη|αχρησιμοποίητες}}.\nΔείτε επίσης τις [[Special:WantedCategories|ζητούμενες κατηγορίες]].",
+       "categoriespagetext": "{{PLURAL:$1|Η ακόλουθη κατηγορία υπάρχει|Οι ακόλουθες κατηγορίες υπάρχουν}} σε αυτό το wiki, και μπορεί ή μπορεί να μην είναι {{PLURAL:$1|αχρησιμοποίητη|αχρησιμοποίητες}}. Δείτε επίσης τις [[Special:WantedCategories|ζητούμενες κατηγορίες]].",
        "categoriesfrom": "Εμφάνιση κατηγοριών που αρχίζουν από:",
        "deletedcontributions": "Διαγεγραμμένες συνεισφορές χρήστη",
        "deletedcontributions-title": "Διαγεγραμμένες συνεισφορές χρήστη",
index a9bf065..d0f6ceb 100644 (file)
        "history": "Page history",
        "history_short": "History",
        "history_small": "history",
-       "updatedmarker": "updated since my last visit",
+       "updatedmarker": "updated since your last visit",
        "printableversion": "Printable version",
        "permalink": "Permanent link",
        "print": "Print",
        "autoblockedtext": "Your IP address has been automatically blocked because it was used by another user, who was blocked by $1.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou may contact $1 or one of the other [[{{MediaWiki:Grouppage-sysop}}|administrators]] to discuss the block.\n\nNote that you may not use the \"{{int:emailuser}}\" feature unless you have a valid email address registered in your [[Special:Preferences|user preferences]] and you have not been blocked from using it.\n\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
        "systemblockedtext": "Your username or IP address has been automatically blocked by MediaWiki.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYour current IP address is $3.\nPlease include all above details in any queries you make.",
        "blockednoreason": "no reason given",
+       "blockedtext-composite": "<strong>Your username or IP address has been blocked.</strong>\n\nThe reason given is:\n\n:<em>$2</em>.\n\n* Start of block: $8\n* Expiration of longest block: $6\n\nYour current IP address is $3.\nPlease include all above details in any queries you make.",
+       "blockedtext-composite-reason": "There are multiple blocks against your account and/or IP address",
        "whitelistedittext": "Please $1 to edit pages.",
        "confirmedittext": "You must confirm your email address before editing pages.\nPlease set and validate your email address through your [[Special:Preferences|user preferences]].",
        "nosuchsectiontitle": "Cannot find section",
index fdf7e8c..597dca2 100644 (file)
@@ -57,7 +57,8 @@
                        "YvesNevelsteen",
                        "Vlad5250",
                        "Mirin",
-                       "Etrapani"
+                       "Etrapani",
+                       "Taylor"
                ]
        },
        "tog-underline": "Substrekado de ligiloj:",
        "autoblockedtext": "Via IP-adreso estas aŭtomate forbarita, ĉar uzis ĝin alia uzanto, kiun baris $1.\nLa donita kialo estas jena:\n\n:<em>$2</em>\n\n*Komenco de forbaro: $8\n*Limdato de la blokado: $6\n*Intencis forbari uzanton: $7\n\nVi povas kontakti $1 aŭ iun ajn el la aliaj [[{{MediaWiki:Grouppage-sysop}}|administrantojn]] por diskuti la blokon.\n\nNotu, ke vi ne povas uzi la servon \"{{int:emailuser}}\" krom se vi havas validan retpoŝt-adreson registritan en viaj [[Special:Preferences|preferojn]], kaj vi estas ne blokita kontraŭ ĝia uzado.\n\nVia nuna IP-adreso estas $3, kaj la forbaro-identigo estas $5.\nBonvolu inkluzivi tiujn detalojn en iuj ajn demandoj kiun vi farus.",
        "systemblockedtext": "Via salutnomo aŭ IPa adreso estis aŭtomate forbarita de MediaWiki.\nLa kialo donita estas:\n\n:<em>$2</em>\n\n* Komenco de forbaro: $8\n* Eksvalidiĝo de forbaro: $6\n* Intenca forbarulo: $7\n\nVia nuna IP-adreso estas $3.\nBonvolu inkluzivi ĉiujn suprajn detalojn en ajnaj demandoj kiujn vi faras.",
        "blockednoreason": "neniu kialo estis donita",
+       "blockedtext-composite": "<strong>Oni forbaris vian salutnomon aŭ IP-adreson.</strong>\n\nLa donita kialo estas:\n\n:<em>$2</em>.\n\n* Komenco de forbaro: $8\n* Fino de plej longa forbaro: $6\n\nVia aktuala IP-adreso estas $3.\nPlease include all above details in any queries you make.",
+       "blockedtext-composite-reason": "Estas pluraj forbaroj kontraŭ via konto kaj/aŭ IP-adreso",
        "whitelistedittext": "Vi devas $1 por redakti paĝojn.",
        "confirmedittext": "Vi devas konfirmi vian retpoŝtan adreson antaŭ ol redakti paĝojn. Bonvolu agordi kaj validigi vian retadreson per viaj [[Special:Preferences|preferoj]].",
        "nosuchsectiontitle": "Ne povas trovi sekcion",
        "move-page": "Alinomi $1",
        "move-page-legend": "Alinomi paĝon",
        "movepagetext": "Per la jena formulo vi povas ŝanĝi la nomon de iu paĝo, kunportante ĝian historion de redaktoj al la nova nomo.\nLa antaŭa titolo fariĝos alidirektilo al la nova titolo.\nVi povas ĝisdatigi alidirektilojn kiu indikas la originalan titolon aŭtomate.\nSe vi elektas ĝisdatigi permane, bonvolu kontroli [[Special:DoubleRedirects|duoblajn]] aŭ [[Special:BrokenRedirects|rompitajn alidirektilojn]].\nVi estas responsa por certigi ke ligilojn direktas fidinde.\n\nNotu, ke la paĝo '''ne''' estos movita se jam ekzistas paĝo ĉe la nova titolo, krom se tiu loko estas malplena aŭ alidirektilo al ĉi tiu paĝo, kaj sen antaŭa redaktohistorio.\nPro tio, vi ja povos removi la paĝon je la antaŭa titolo se vi mistajpus, kaj ne povas forviŝi ekzistantan paĝon per movo.\n\n'''Note:'''\nTio povas esti drasta kaj neatendita ŝanĝo por populara paĝo;\nbonvolu certigi vin, ke vi komprenas ties konsekvencojn antaŭ ol vi antaŭeniru.",
-       "movepagetext-noredirectfixer": "Per jena formularo vi povas alinomigi paĝon, kaj movi tutan ĝian redaktohistorion al la nova nomo. \nLa antaŭa titolo alidirektigos onin al la nova titolo.\nKontrolu pri [[Special:DoubleRedirects|duoblajn]] aŭ [[Special:BrokenRedirects|nefunkciantajn alidirektilojn]].\nVi respondecas pri tio ke ligoj restas montrantaj ĝustadirekten.\n\nKonsciu ke la paĝo '''ne'' estas movota se jam ekzistas paĝo havanta la novan titolon, krom se ĝi estas alidirektilo sen antaŭa redaktohistorio.\nTio ĉi signifas ke vi povas alinomigi paĝon reen al antaŭa nomo se vi eraras, kaj vi ke vi ne povas anstataŭigi ekzistantan paĝon.\n\n'''Rimarko:''\nEblas ke tio ĉi estas drasta kaj neatendita ŝanĝo de populara paĝo;\nAntaŭ daŭrigi, bonvolu certiĝi, ke vi komprenas la konsekvencojn de tiuj ĉi ŝanĝo.",
+       "movepagetext-noredirectfixer": "Per jena formularo vi povas alinomigi paĝon, kaj movi tutan ĝian redaktohistorion al la nova nomo. \nLa antaŭa titolo alidirektigos onin al la nova titolo.\nKontrolu pri [[Special:DoubleRedirects|duoblajn]] aŭ [[Special:BrokenRedirects|nefunkciantajn alidirektilojn]].\nVi respondecas pri tio ke ligoj restas montrantaj ĝustadirekten.\n\nKonsciu ke la paĝo <strong>ne</strong> estas movota se jam ekzistas paĝo havanta la novan titolon, krom se ĝi estas alidirektilo sen antaŭa redaktohistorio.\nTio ĉi signifas ke vi povas alinomigi paĝon reen al antaŭa nomo se vi eraras, kaj vi ke vi ne povas anstataŭigi ekzistantan paĝon.\n\n<strong>Rimarko:</strong>\nEblas ke tio ĉi estas drasta kaj neatendita ŝanĝo de populara paĝo;\nAntaŭ daŭrigi, bonvolu certiĝi, ke vi komprenas la konsekvencojn de tiuj ĉi ŝanĝo.",
        "movepagetalktext": "Se vi validas tiun elektobutono, la asociata diskutpaĝo estos aŭtomate alinomita al nova titolo, krom se malplena diskutpaĝo jam ekzistas.\n\nTiujokaze, vi alinomigendos aŭ kunfandendos malaŭtomate la paĝon se vi tion deziras.",
        "moveuserpage-warning": "<strong>Averto:</strong> Vi preskaŭ alinomas paĝon de uzanto. Bonvolu noti ke nur la paĝo estos alinomita kaj la uzanto mem <em>ne</em> estos alinomita.",
        "movecategorypage-warning": "<strong>Averto:</strong> Vi baldaŭ movos kategorian paĝon. Bonvolu noti ke, nur la paĝo estos movita, kaj la paĝoj en la malnova kategorio <em>ne</em> transiros en la novan kategorion.",
        "imagetypemismatch": "La nova dosierfinaĵo ne kongruas ĝian dosiertipon.",
        "imageinvalidfilename": "La cela dosiernomo estas nevalida",
        "fix-double-redirects": "Ĝisdatigi iujn alidirektilojn kiuj direktas al la originala titolo",
-       "move-leave-redirect": "Forlasi alidirektilon",
+       "move-leave-redirect": "Postlasi alidirektilon",
        "protectedpagemovewarning": "'''Averto:''' Ĉi tiu paĝo estis ŝlosita tiel nur uzantoj kun administranto-rajtoj povas movi ĝin.\nJen la lasta protokolero por via referenco:",
        "semiprotectedpagemovewarning": "<strong>Averto:</strong> ĉi tiu paĝo estis ŝlosita tiel, ke ĝin povas movi nur aŭtomate konfirmitaj uzantoj.\nJen por vi la plej nova protokolero:",
        "move-over-sharedrepo": "[[:$1]] ekzistas en komuna dosierujo. Movado de dosiero al ĉi tiu titolo anstataŭigos la komunan dosieron.",
        "tag-mw-new-redirect-description": "Redaktoj kiuj kreas novajn alidirektigilojn aŭ ŝanĝas paĝojn al alidirektigiloj",
        "tag-mw-removed-redirect": "Forigis alidirektilon",
        "tag-mw-removed-redirect-description": "Redaktoj kiuj ŝanĝas ekzistintan alidirektigilon al ne-alidirektigilon",
-       "tag-mw-changed-redirect-target": "Ŝanĝis celon de alidirektilon",
+       "tag-mw-changed-redirect-target": "Ŝanĝis celon de alidirektilo",
        "tag-mw-changed-redirect-target-description": "Redaktoj kiuj ŝanĝas la celon de alidirektigilo",
        "tag-mw-blank": "Vakigo",
        "tag-mw-blank-description": "Redaktoj kiuj vakigis paĝon",
        "passwordpolicies-policyflag-suggestchangeonlogin": "sugesti ŝanĝadon dum ensaluto",
        "easydeflate-invaliddeflate": "Provizita enhavo ne estas ĝuste densigita",
        "unprotected-js": "Pro sekurecaj kialoj, JavaScript ne povas esti ŝargata el neprotektataj paĝoj. Bonvolu nur krei JavaScript en la nomspaco MediaWiki: aŭ kiel subpaĝo de Uzanto.",
-       "userlogout-continue": "Se vi vola elsaluti, bonvolu  [$1 iri al la elsaluta paĝo]."
+       "userlogout-continue": "Ĉu vi volas elsaluti?"
 }
index 018131b..34a3ec0 100644 (file)
        "mw-widgets-abandonedit-title": "¿Seguro?",
        "mw-widgets-copytextlayout-copy": "Copiar",
        "mw-widgets-copytextlayout-copy-fail": "No se pudo copiar en el portapapeles.",
-       "mw-widgets-copytextlayout-copy-success": "Copiado en el portapapeles",
+       "mw-widgets-copytextlayout-copy-success": "Copiado en el portapapeles.",
        "mw-widgets-dateinput-no-date": "Ninguna fecha seleccionada",
        "mw-widgets-dateinput-placeholder-day": "AAAA-MM-DD",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "passwordpolicies-policyflag-suggestchangeonlogin": "sugerir cambio al acceder a la cuenta",
        "easydeflate-invaliddeflate": "El contenido proporcionado no esta comprimido correctamente",
        "unprotected-js": "Por razones de seguridad, JavaScript no se puede cargar desde páginas desprotegidas. Crea javascript solo en MediaWiki: espacio de nombres o como subpágina de usuario",
-       "userlogout-continue": "Si deseas cerrar sesión, [$1 continúa a la página de cierre de sesión]."
+       "userlogout-continue": "¿Quieres finalizar la sesión?"
 }
index 790b005..a23894f 100644 (file)
        "exif-primarychromaticities": "चित्रकणक पहिलुक अधिकार",
        "exif-ycbcrcoefficients": "रंग स्थान परिवर्तन मैट्रिक्स गुणक",
        "exif-referenceblackwhite": "कारी आ उज्जर सन्दर्भ मूल्यक जोड़ा",
-       "exif-datetime": "सà¤\82चिका परिवर्तन तिथि आ समए",
+       "exif-datetime": "सà¤\9eà¥\8dचिका परिवर्तन तिथि आ समए",
        "exif-imagedescription": "चित्र शीर्षक",
        "exif-make": "क्यामरा निर्माता",
        "exif-model": "क्यामरा मोडल",
-       "exif-software": "प्रयोग कएल सफ्टवेयर",
+       "exif-software": "पà¥\8dरयà¥\8bà¤\97 à¤\95à¤\8fल à¤\97à¥\87ल à¤¸à¤«à¥\8dà¤\9fवà¥\87यर",
        "exif-artist": "लिखैबला",
        "exif-copyright": "सर्वाधिकारी",
        "exif-exifversion": "एक्जिफ संस्करण",
@@ -47,7 +47,7 @@
        "exif-usercomment": "सदस्यक टिप्पणी",
        "exif-relatedsoundfile": "संबंधित ध्वनि फ़ाईल",
        "exif-datetimeoriginal": "डाटा बनाबैक तारिख आ समय",
-       "exif-datetimedigitized": "à¤\85à¤\99à¥\8dà¤\95à¥\80à¤\95रणà¤\95 à¤¤à¤¾à¤°à¤¿à¤\96 à¤\86 à¤¸à¤®à¤¯",
+       "exif-datetimedigitized": "à¤\85à¤\99à¥\8dà¤\95à¥\80à¤\95रणà¤\95 à¤¤à¤¾à¤°à¤¿à¤\96 à¤\86 à¤¸à¤®à¤\8f",
        "exif-subsectime": "दिनांकसमयक उपसेकंड",
        "exif-subsectimeoriginal": "मूलदिनांकसमयक उपसेकंड",
        "exif-subsectimedigitized": "मूलदिनांकअंकीकरणक उपसेकंड",
index 6eac350..c9afaa9 100644 (file)
        "exif-copyrighted": "Copyright status. This is a true or false field showing either Copyrighted or Public Domain. It should be noted that Copyrighted includes freely-licensed works.",
        "exif-copyrightowner": "{{exif-qqq}}\n\nCopyright owner. Can have more than one person or entity.",
        "exif-usageterms": "Terms under which you're allowed to use the image/media.",
-       "exif-webstatement": "{{exif-qqq}}\n\nURL detailing the copyright status of the image, and how you're allowed to use the image. Often this is a link to a creative commons license, however the creative commons people recommend using a page that generally contains specific information about the image, and recommend using {{msg-mw|exif-licenseurl}} for linking to the license. See http://wiki.creativecommons.org/XMP",
+       "exif-webstatement": "{{exif-qqq}}\n\nURL detailing the copyright status of the image, and how you're allowed to use the image. Often this is a link to a creative commons license, however the creative commons people recommend using a page that generally contains specific information about the image, and recommend using {{msg-mw|exif-licenseurl}} for linking to the license. See https://wiki.creativecommons.org/wiki/XMP",
        "exif-originaldocumentid": "A unique ID of the original document (image) that this document (image) is based on.",
        "exif-licenseurl": "{{exif-qqq}}\n\nURL for copyright license. This is almost always a creative commons license since this information comes from the creative commons namespace of XMP (but could be a link to any type of license). See also {{msg-mw|exif-webstatement}}",
        "exif-morepermissionsurl": "A URL where you can \"buy\" (or otherwise negotiate) to get more rights for the image.",
index cf2b244..044703b 100644 (file)
@@ -6,7 +6,8 @@
                        "Milicevic01",
                        "Rancher",
                        "Sasa Stefanovic",
-                       "Сербијана"
+                       "Сербијана",
+                       "Zoranzoki21"
                ]
        },
        "exif-imagewidth": "Ширина",
        "exif-photometricinterpretation-2": "RGB",
        "exif-photometricinterpretation-3": "Палета",
        "exif-photometricinterpretation-4": "Маска транспарентности",
+       "exif-photometricinterpretation-5": "Одвојено (вероватно CMYK)",
        "exif-photometricinterpretation-6": "YCbCr",
        "exif-photometricinterpretation-8": "CIE L*a*b*",
+       "exif-photometricinterpretation-9": "CIE L*a*b* (ICC кодирање)",
+       "exif-photometricinterpretation-10": "CIE L*a*b* (ITU кодирање)",
        "exif-unknowndate": "Непознат датум",
        "exif-orientation-1": "Нормално",
        "exif-orientation-2": "Обрнуто по хоризонтали",
index 41ff061..2a07679 100644 (file)
@@ -8,7 +8,8 @@
                        "Liuxinyu970226",
                        "PhiLiP",
                        "Qiyue2001",
-                       "Xiaomingyan"
+                       "Xiaomingyan",
+                       "神樂坂秀吉"
                ]
        },
        "exif-imagewidth": "宽度",
        "exif-copyrighted-false": "版权状态未设定",
        "exif-photometricinterpretation-0": "黑白(白为0)",
        "exif-photometricinterpretation-1": "黑白(黑为0)",
+       "exif-photometricinterpretation-3": "主色调",
        "exif-photometricinterpretation-4": "透明遮罩",
        "exif-photometricinterpretation-5": "分隔(可能是CMYK)",
+       "exif-photometricinterpretation-8": "CIE L*a*b*",
        "exif-photometricinterpretation-9": "CIE L*a*b*(ICC编码)",
        "exif-photometricinterpretation-10": "CIE L*a*b*(ITU编码)",
        "exif-photometricinterpretation-32803": "色彩滤镜矩阵",
index 70c0b67..58edc36 100644 (file)
        "title-invalid-leading-colon": "عنوان صفحهٔ درخواستی دارای دونقطهٔ نامجاز در ابتدایش است.",
        "perfcached": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و ممکن است کاملاً به‌روز نباشند. حداکثر {{PLURAL:$1|یک نتیجه| $1 نتیجه}} در حافظهٔ نهانی قابل دسترس است.",
        "perfcachedts": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و آخرین بار در $1 به‌روزرسانی شدند. حداکثر {{PLURAL:$4|یک نتیجه|$4 نتیجه}} در حافظهٔ نهانی قابل دسترس است.",
-       "querypage-no-updates": "اÙ\85کاÙ\86 Ø¨Ù\87â\80\8cرÙ\88زرساÙ\86Û\8c Ø§Û\8cÙ\86 ØµÙ\81Ø­Ù\87 Ù\81عÙ\84اÙ\8b ØºÛ\8cرÙ\81عاÙ\84 Ø´Ø¯Ù\87â\80\8cاست.\nاطÙ\84اعات Ø§Û\8cÙ\86 ØµÙ\81Ø­Ù\87 Ù\85Ù\85Ú©Ù\86 Ø§Ø³Øª Ø¨Ù\87â\80\8cرÙ\88ز Ù\86باشد.",
+       "querypage-no-updates": "رÙ\88زآÙ\85دسازÛ\8c Ø§Û\8cÙ\86 ØµÙ\81Ø­Ù\87 Ù\87Ù\85â\80\8cاکÙ\86Ù\88Ù\86 ØºÛ\8cر Ù\81عاÙ\84 Ø§Ø³Øª.\nدادÙ\87â\80\8cÙ\87اÛ\8c Ø§Û\8cÙ\86 ØµÙ\81Ø­Ù\87 Ø¯Ø± Ø­Ø§Ù\84 Ø­Ø§Ø¶Ø±Ø\8c Ø¨Ø§Ø²Ø¢Ù\88رÛ\8c Ù\86Ù\85Û\8câ\80\8cØ´Ù\88د.",
        "viewsource": "نمایش مبدأ",
        "viewsource-title": "نمایش مبدأ برای $1",
        "actionthrottled": "جلوی عمل شما گرفته شد",
        "botpasswords-label-needsreset": "(نیاز به تنظیم مجدد گذرواژه)",
        "botpasswords-label-appid": "نام ربات:",
        "botpasswords-label-create": "ایجاد",
-       "botpasswords-label-update": "بÙ\87â\80\8cرÙ\88ز Ø±Ø³Ø§Ù\86ی",
+       "botpasswords-label-update": "رÙ\88زآÙ\85دسازی",
        "botpasswords-label-cancel": "لغو",
        "botpasswords-label-delete": "حذف",
        "botpasswords-label-resetpassword": "بازگردانی گذرواژه",
        "botpasswords-update-failed": "شکست در به‌روزرسانی نام رباتی «$1». حذف شده است؟",
        "botpasswords-created-title": "گذرواژه ربات ایجاد شد",
        "botpasswords-created-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» ایجاد شد.",
-       "botpasswords-updated-title": "گذرÙ\88اÚ\98Ù\87 Ø±Ø¨Ø§Øª Ø¨Ù\87â\80\8cرÙ\88ز شد",
-       "botpasswords-updated-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» به‌روز شد.",
+       "botpasswords-updated-title": "گذرÙ\88اÚ\98Ù\87 Ø±Ø¨Ø§Øª Ø±Ù\88زآÙ\85د شد",
+       "botpasswords-updated-body": "گذرواژهٔ رباتی برای ربات «$1» {{GENDER:$2|کاربر}} «$2» روزآمد شد.",
        "botpasswords-deleted-title": "گذرواژه ربات حذف شد",
        "botpasswords-deleted-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» حذف شد.",
        "botpasswords-newpassword": "<strong>$2</strong> گذرواژهٔ جدید برای ورود با حساب <strong>$1</strong> است. <em>لطفاً آن را برای ارجاع در آینده ذخیره کنید.</em> <br> (برای ربات‌های قدیمی که نیاز به نام کاربری مطابق با حساب کاربری‌شان دارد، شما می‌توانید از <strong>$3</strong> به عنوان نام کاربری و از <strong>$4</strong> به عنوان گذرواژه استفاده کنید.)",
        "sitejsonpreview": "<strong>توجه داشته باشید که شما در حال آزمودن و پیش نمایش گرفتن از تنظیمات JSON هستید و هنوز آن را ذخیره نکردید!</strong>",
        "sitejspreview": "'''به یاد داشته باشید که شما فقط دارید پیش‌نمایش این جاوااسکریپت را می‌بینید.'''\n'''این جاوااسکریپت هنوز ذخیره نشده‌است!'''",
        "userinvalidconfigtitle": "<strong>هشدار:</strong> پوسته‌ای به نام «$1» وجود ندارد.\nبه یاد داشته باشید که صفحه‌های شخصی ‎.css ،.json و ‎.js باید عنوانی با حروف کوچک داشته باشند؛ نمونه: {{ns:user}}:فو/vector.css در مقابل {{ns:user}}:فو/Vector.css.",
-       "updated": "(بÙ\87â\80\8cرÙ\88ز شد)",
+       "updated": "(رÙ\88زآÙ\85د شد)",
        "note": "'''نکته:'''",
        "previewnote": "'''به یاد داشته باشید که این فقط پیش‌نمایش است.'''\nتغییرات شما هنوز ذخیره نشده‌است!",
        "continue-editing": "رفتن به قسمت ویرایش",
        "moveddeleted-notice-recent": "متاسفانه صفحه قبلا حذف شده‌است (در ۲۴ ساعت اخیر) \nدلیل حذف و سیاههٔ انتقال، و حفاظت در پائین موجود است.",
        "log-fulllog": "مشاهدهٔ سیاههٔ کامل",
        "edit-hook-aborted": "ویرایش توسط قلاب لغو شد.\nتوضیحی در این مورد داده نشد.",
-       "edit-gone-missing": "اÙ\85کاÙ\86 Ø¨Ù\87â\80\8cرÙ\88ز Ú©Ø±Ø¯Ù\86 ØµÙ\81Ø­Ù\87 Ù\88جÙ\88د Ù\86دارد.\nبÙ\87 Ù\86ظرÙ\85Û\8câ\80\8cرسد Ú©Ù\87 ØµÙ\81Ø­Ù\87 Ø­Ø°Ù\81 Ø´Ø¯Ù\87 Ø¨Ø§Ø´Ø¯.",
+       "edit-gone-missing": "اÙ\85کاÙ\86 Ø±Ù\88زآÙ\85دسازÛ\8c ØµÙ\81Ø­Ù\87 Ù\88جÙ\88د Ù\86دارد.\nبÙ\87 Ù\86ظر Ù\85Û\8câ\80\8cرسد Ú©Ù\87 ØµÙ\81Ø­Ù\87 Ø­Ø°Ù\81 Ø´Ø¯Ù\87 Ø§Ø³Øª.",
        "edit-conflict": "تعارض ویرایشی.",
        "edit-no-change": "ویرایش شما نادیده گرفته شد، زیرا تغییری در متن داده نشده بود.",
        "edit-slots-cannot-add": "این {{PLURAL:$1|اسلات|اسلات‌ها}} پشتیبانی نمی‌شود: $2.",
        "revdelete-unsuppress": "حذف محدودیت‌ها در بازبینی‌های ترمیم‌شده",
        "revdelete-log": "دلیل:",
        "revdelete-submit": "اعمال بر {{PLURAL:$1|نسخهٔ|نسخه‌های}} انتخاب شده",
-       "revdelete-success": "'''پیدایی نسخه به روز شد.'''",
+       "revdelete-success": "پیدایی بازنگری، روزآمد شد.",
        "revdelete-failure": "'''پیدایی نسخه‌ها قابل به روز کردن نیست:'''\n$1",
        "logdelete-success": "تغییر پیدایی مورد انجام شد.",
        "logdelete-failure": "'''پیدایی سیاهه‌ها قابل تنظیم نیست:'''\n$1",
        "pageswithprop-prophidden-binary": "جزییات مقدار مخفی باینری ($1)",
        "doubleredirects": "تغییرمسیرهای دوتایی",
        "doubleredirectstext": "این صفحه فهرستی از صفحه‌های تغییرمسیری را ارائه می‌کند که به صفحهٔ تغییرمسیر دیگری اشاره می‌کنند.\nهر سطر دربردارندهٔ پیوندهایی به تغییرمسیر اول و دوم و همچنین مقصد تغییرمسیر دوم است، که معمولاً صفحهٔ مقصد واقعی است و نخستین تغییرمسیر باید به آن اشاره کند.\nموارد <del>خط خورده</del> درست شده‌اند.",
-       "double-redirect-fixed-move": "[[$1]] انتقال داده شده‌است.\n\nبه صورت خودکار به‌روز شده‌است و  تغییرمسیری به [[$2]] داده شد.",
+       "double-redirect-fixed-move": "[[$1]] انتقال داده شده است.\nبه‌صورت خودکار روزآمد شده و هم‌اکنون به [[$2]] تغییر مسیر داده شده است.",
        "double-redirect-fixed-maintenance": "رفع خودکار تغییرمسیر دوتایی از [[$1]] به [[$2]] در روند نگهداری",
        "double-redirect-fixer": "تعمیرکار تغییرمسیرها",
        "brokenredirects": "تغییرمسیرهای خراب",
        "nonfile-cannot-move-to-file": "امکان انتقال محتوای غیر پرونده به فضای نام پرونده وجود ندارد",
        "imagetypemismatch": "پسوند پرونده تازه با نوع آن سازگار نیست",
        "imageinvalidfilename": "نام پروندهٔ هدف نامعتبر است",
-       "fix-double-redirects": "بÙ\87 Ø±Ù\88ز Ú©Ø±Ø¯Ù\86 ØªÙ\85اÙ\85Û\8c تغییرمسیرهایی که به مقالهٔ اصلی اشاره می‌کنند",
+       "fix-double-redirects": "رÙ\88زآÙ\85دسازÛ\8c Ù\87Ù\85Ù\87Ù\94 تغییرمسیرهایی که به مقالهٔ اصلی اشاره می‌کنند",
        "move-leave-redirect": "بر جا گذاشتن یک تغییرمسیر",
        "protectedpagemovewarning": "'''هشدار:''' این صفحه قفل شده‌است به طوری که تنها کاربران با دسترسی مدیریت می‌توانند آن را انتقال دهند.\nآخرین موارد سیاهه در زیر آمده است:",
        "semiprotectedpagemovewarning": "'''تذکر:''' این صفحه قفل شده‌است به طوری که تنها کاربران ثبت نام کرده می‌توانند آن را انتقال دهند.\nآخرین موارد سیاهه در زیر آمده است:",
        "watchlistedit-raw-legend": "ویرایش فهرست خام پی‌گیری‌ها",
        "watchlistedit-raw-explain": "عنوان‌های موجود در فهرست پی‌گیری‌های شما در زیر نشان داده شده‌اند، و شما می‌توانید مواردی را حذف یا اضافه کنید؛ هر مورد در یک سطر جداگانه باید قرار بگیرد.\nدر پایان، دکمهٔ «{{int:Watchlistedit-raw-submit}}» را بفشارید.\nتوجه کنید که شما می‌توانید از [[Special:EditWatchlist|ویرایشگر استاندارد فهرست پی‌گیری‌ها]] هم استفاده کنید.",
        "watchlistedit-raw-titles": "عنوان‌ها:",
-       "watchlistedit-raw-submit": "بÙ\87â\80\8cرÙ\88زرساÙ\86ی پی‌گیری‌ها",
-       "watchlistedit-raw-done": "Ù\81Ù\87رست Ù¾Û\8câ\80\8cÚ¯Û\8cرÛ\8câ\80\8cÙ\87اÛ\8c Ø´Ù\85ا Ø¨Ù\87 Ø±Ù\88ز شد.",
+       "watchlistedit-raw-submit": "رÙ\88زآÙ\85دسازی پی‌گیری‌ها",
+       "watchlistedit-raw-done": "Ù\81Ù\87رست Ù¾Û\8câ\80\8cÚ¯Û\8cرÛ\8câ\80\8cÙ\87اÛ\8c Ø´Ù\85ا Ø±Ù\88زآÙ\85د شد.",
        "watchlistedit-raw-added": "$1 عنوان به فهرست پی‌گیری‌ها اضافه {{PLURAL:$1|شد|شدند}}:",
        "watchlistedit-raw-removed": "$1 عنوان حذف {{PLURAL:$1|شد|شدند}}:",
        "watchlistedit-clear-title": "پاک کردن فهرست پی‌گیری‌ها",
        "log-description-pagelang": "این سیاههٔ تغییرات صفحهٔ زبان‌ها است.",
        "logentry-pagelang-pagelang": "$1 زبان $3  از  $4  به  $5 {{GENDER:$2| تغییریافت}}",
        "default-skin-not-found": "اوه! پوسته پیش‌فرض برای ویکی شما تعریف‌شده در <code dir=\"ltr\"<$wgDefaultSkin</code> به عنوان <code>$1</code>، در دسترس نیست.\n\nبه نظر می‌آید نصب شما شامل پوسته‌های زیر می‌شود. [https://www.mediawiki.org/wiki/Manual:Skin_configuration راهنما: تنظیمات پوسته] را برای کسب اطلاعات در باره چگونگی فعال‌ساختن آن‌ها و انتخاب پیش‌فرض ببینید.\n\n$2\n\n; اگر اخیراً مدیاویکی را نصب کرده‌اید:\n: احتمالاً از گیت، یا به طور مستقیم از کد مبدأ که از چند متد دیگر استفاده می‌کند نصب کردید. انتظار می‌رود. چند {{PLURAL:$4|پوسته|پوسته}} از [https://www.mediawiki.org/wiki/Category:All_skins فهرست پوسته mediawiki.org] نصب کنید، که همراه چندین پوسته و افزونه هستند. شما می‌توانید شاخه <code>skins/</code> را از آن نسخه‌برداری کرده و بچسبانید.\n\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins استفاده از گیت برای دریافت پوسته‌ها].\n: انجام این کار با مخزن گیت‌تان تداخل نمی‌کند اگر توسعه‌دهنده مدیاویکی هستید.\n\n; اگر اخیراً مدیاویکی را ارتقاء دادید:\n: مدیاویکی ۱٫۲۴ و تازه‌تر دیگر به طور خودکار پوسته‌های نصب‌شده را فعال نمی‌کند ([https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery راهنما: کشف خودکار پوسته] را ببینید). شما می‌توانید خطوط زیر را به داخل <code>LocalSettings.php</code> بچسبانید تا {{PLURAL:$5|همه|همه}} پوسته‌های نصب‌شده را فعال کنید:\n\n<pre dir=\"ltr\">$3</pre>\n\n; اگر اخیراً <code>LocalSettings.php</code> را تغییر دادید:\n: نام پوسته‌ها را برای غلط املایی دوباره بررسی کنید.",
-       "default-skin-not-found-no-skins": "پوستهٔ پیش‌فرض برای ویکی شما تعریف‌شده در<code>$wgDefaultSkin</code> به عنوان <code>$1</code>، هست موجود نیست.\n\nشما پوسته‌ها را نصب نکرده‌اید.\n\n:اگر مدیاویکی را به‌روز یا نصب کرده‌اید:\n:ممکن است از گیت یا از کد منبع با روش‌های دیگر نصب کرده‌اید. انتظار می‌رود MediaWiki 1.24 یا جدیدتر در پوشهٔ اصلی هیچ پوسته‌ای نداشته باشند.\nسعی کنید تعدادی پوسته از [https://www.mediawiki.org/wiki/Category:All_skins پوشهٔ پوسته‌های مدیاویکی]، با:\n:*دریافت [https://www.mediawiki.org/wiki/Download نصب‌کننده تاربال]، که با چندین پوسته و افزونه هست. شما می توانید پوستهٔ <code>skins/</code> را از آن کپی و پیست کنید.\n:*کلون کردن یکی از <code dir=\"ltr\">mediawiki/skins/*</code> از مخزن در پوشهٔ <code>skins/</code> مدیاویکی‌تان.\n:اگر توسعه‌دهندهٔ مدیاویکی هستید، انجام این کار نباید تعارضی با مخزن گیت شما داشته باشد. برای اطلاعات بیشتر و فعال کردن پوسته‌ها و انتخاب آنها به عنوان پیش‌فرض [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: تنظیمات پوسته] را مشاهده کنید.",
+       "default-skin-not-found-no-skins": "پوستهٔ پیش‌فرض برای ویکی شما تعریف‌شده در<code>$wgDefaultSkin</code> به‌عنوان <code>$1</code>، هست موجود نیست.\n\nشما پوسته‌ها را نصب نکرده‌اید.\n\n:اگر مدیاویکی را روزآمد یا نصب کرده‌اید:\n:ممکن است از گیت یا از کد منبع با روش‌های دیگر نصب کرده‌اید. انتظار می‌رود MediaWiki 1.24 یا جدیدتر در پوشهٔ اصلی هیچ پوسته‌ای نداشته باشند.\nسعی کنید تعدادی پوسته از [https://www.mediawiki.org/wiki/Category:All_skins پوشهٔ پوسته‌های مدیاویکی]، با:\n:*دریافت [https://www.mediawiki.org/wiki/Download نصب‌کننده تاربال]، که با چندین پوسته و افزونه هست. شما می‌توانید پوستهٔ <code>skins/</code> را از آن کپی و پیست کنید.\n:*کلون کردن یکی از <code dir=\"ltr\">mediawiki/skins/*</code> از مخزن در پوشهٔ <code>skins/</code> مدیاویکی‌تان.\n:اگر توسعه‌دهندهٔ مدیاویکی هستید، انجام این کار نباید تعارضی با مخزن گیت شما داشته باشد. برای اطلاعات بیشتر و فعال کردن پوسته‌ها و انتخاب آنها به‌عنوان پیش‌فرض [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: تنظیمات پوسته] را مشاهده کنید.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (فعال)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>غیرفعال</strong>)",
        "mediastatistics": "آمار رسانه‌ها",
index 07f305e..cee8a41 100644 (file)
        "history": "Historique de la page",
        "history_short": "Historique",
        "history_small": "historique",
-       "updatedmarker": "modifié depuis ma dernière visite",
+       "updatedmarker": "modifié depuis votre dernière visite",
        "printableversion": "Version imprimable",
        "permalink": "Lien permanent",
        "print": "Imprimer",
        "autoblockedtext": "Votre adresse IP a été bloquée automatiquement car elle a été utilisée par un autre utilisateur, lui-même bloqué par $1.\nLa raison invoquée est :\n\n: <em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage : $6\n* Compte bloqué : $7\n\nVous pouvez contacter $1 ou l’un des autres [[{{MediaWiki:Grouppage-sysop}}|administrateurs]] pour discuter de ce blocage.\n\nNotez que vous ne pourrez utiliser la fonctionnalité « {{int:emailuser}} » que si vous avez une adresse de courriel validée dans vos [[Special:Preferences|préférences]] et que cette fonctionnalité ne vous a pas été désactivée.\n\nVotre adresse IP actuelle est $3, et le numéro de blocage est $5.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
        "systemblockedtext": "Votre nom d'utilisateur ou votre adresse IP ont été bloqués automatiquement par MediaWiki.\nLa raison donnée est la suivante:\n\n: <em>$2</em>.\n\n* Le début du blocage: $8\n* Expiration du délai de blocage: $6\n* Elément concerné: $7\n\nVotre adresse IP actuelle est $3.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
        "blockednoreason": "aucune raison donnée",
+       "blockedtext-composite": "<strong>Votre nom d'utilisateur ou votre adresse IP ont été bloqués.</strong>\n\nLa raison invoquées est :\n\n:<em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage le plus long : $6\n\nVotre adresse IP actuelle est $3.\nVeuillez inclure tous les détails ci-dessus dans chaque demande que vous ferez.",
+       "blockedtext-composite-reason": "Il existe plusieurs blocages sur votre compte et/ou votre adresse IP",
        "whitelistedittext": "Vous devez vous $1 pour avoir la permission de modifier le contenu.",
        "confirmedittext": "Vous devez confirmer votre adresse de courriel avant de modifier les pages.\nVeuillez entrer et valider votre adresse de courriel dans vos [[Special:Preferences|préférences]].",
        "nosuchsectiontitle": "Impossible de trouver la section",
        "sharedupload-desc-edit": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.\nVous voulez peut-être modifier la description sur sa [$2 page de description].",
        "sharedupload-desc-create": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.\nVous voulez peut-être modifier la description sur sa [$2 page de description].",
        "filepage-nofile": "Aucun fichier de ce nom n’existe.",
-       "filepage-nofile-link": "Aucun fichier de ce nom n’existe, mais vous pouvez [$1 en importer un].",
+       "filepage-nofile-link": "Aucun fichier de ce nom n’existe, mais vous pouvez [$1 en téléverser un].",
        "uploadnewversion-linktext": "Importer une nouvelle version de ce fichier",
        "shared-repo-from": "de : $1",
        "shared-repo": "un dépôt partagé",
index ae90b48..f72f85c 100644 (file)
        "aboutsite": "Oer {{SITENAME}}",
        "aboutpage": "Project:Ynfo",
        "copyright": "Ynhâld is beskikber ûnder de $1.",
-       "copyrightpage": "{{ns:project}}:Auteursrjocht",
+       "copyrightpage": "{{ns:project}}:Auteursrjochten",
        "currentevents": "Rinnende saken",
        "currentevents-url": "Project:Rinnende saken",
        "disclaimers": "Foarbehâld",
        "disclaimerpage": "Project:Algemien foarbehâld",
-       "edithelp": "Bewurk-help",
+       "edithelp": "Bewurkhelp",
        "helppage-top-gethelp": "Help",
        "mainpage": "Haadside",
        "mainpage-description": "Haadside",
        "perfcached": "Dit is bewarre ynformaasje dy't mooglik ferâldere is. In maksimum fan {{PLURAL:$1|ien resultaat is|$1 resultaten binne}} beskikber yn de cache.",
        "perfcachedts": "De neikommende gegevens komme út de bewarre ynformaasje, dizze is it lêst fernijd op $1. In maksimum fan {{PLURAL:$4|ien resultaat is|$4 resultaten binne}} beskikber yn de cache.",
        "querypage-no-updates": "Dizze side kin net bywurke wurde. Dizze gegevens wurde net ferfarske.",
-       "viewsource": "Besjoch de boarne",
+       "viewsource": "Boarne besjen",
        "viewsource-title": "Besjoch de boarne foar $1",
        "actionthrottled": "Hanneling opkeard",
        "actionthrottledtext": "As maatregel tsjin spam is it tal kearen per tiidsienheid beheind dat jo dizze hanneling ferrjochtsje kinne. Jo binne oer de limyt. Besykje it in tal minuten letter wer.",
        "protectedpagetext": "Dizze side is befeilige. Bewurkjen is net mooglik.",
        "viewsourcetext": "Jo kinne de boarnetekst fan dizze side besjen en kopiearje:",
        "protectedinterface": "Dizze side jout systeemteksten fan 'e software en is befeilige tsjin misbrûk. Asto oersettingen foar alle wiki's tafoegje of bewurkje wolst, kinsto [https://translatewiki.net/ translatewiki.net] brûke.",
-       "editinginterface": "<strong>Warskôging:</strong> Jo bewurkje in side dy't brûkt wurdt foar systeemteksten foar de software.\nBewurkings op dizze side beynfloedzje de meidoggersynterface fan elkenien.",
+       "editinginterface": "<strong>Warskôging:</strong> Jo bewurkje in side dy't brûkt wurdt foar systeemteksten fan de programmatuer.\nFeroarings oan dizze side beynfloedzje it oansjoch fan it meidoggersoerflak fan oaren op dizze wiki.",
        "cascadeprotected": "Dizze side is skoattele tsjin wizigjen, om't der in ûnderdiel útmakket fan de neikommende {{PLURAL:$1|side|siden}}, dy't skoattele {{PLURAL:$1|is|binne}} mei de \"ûnderlizzende siden\" opsje ynskeakele: $2",
        "namespaceprotected": "Jo hawwe gjin rjochten om siden yn'e nammeromte '''$1''' te bewurkjen.",
        "ns-specialprotected": "Siden yn'e nammerûmte {{ns:special}} kinne net bewurke wurde.",
        "summary": "Gearfetting:",
        "subject": "Underwerp:",
        "minoredit": "Dit is fan lytse betsjutting",
-       "watchthis": "Folgje dizze side",
+       "watchthis": "Dizze side folgje",
        "savearticle": "Side bewarje",
        "publishpage": "Side fêstlizze",
        "publishchanges": "Feroarings publisearje",
        "editingsection": "Bewurkje $1 (seksje)",
        "editingcomment": "Bewurkjen fan $1 (nij mêd)",
        "editconflict": "Tagelyk bewurke: \"$1\"",
-       "explainconflict": "In oar hat de side feroare sûnt jo begûn binne mei it bewurkjen.\nIt earste bewurkingsfjild is hoe't de tekst wilens wurden is.\nJo feroarings stean yn it twadde fjild.\nDy wurde allinnich tapast safier as jo se yn it earste fjild ynpasse.\n'''Allinnich''' de tekst út it earste fjild kin fêstlein wurde.",
+       "explainconflict": "Immen oars hat dizze side feroare, nei't jo mei bewurkjen derfan begûn binne.\nIt bewurkingsfjild boppe befettet de sidetekst sa as it no is.\nJo wizigings wurde werjûn yn it tekstfjild ûnder.\nJo sille jo wizigings gearfoegje moatte mei de besteande tekst.\n<strong>Allinnich</strong> de tekst yn it bewurkingsfjild boppe wurdt bewarre at jo op \"$1\" drukke.",
        "yourtext": "Jo tekst",
        "storedversion": "Fêstleine ferzje",
        "editingold": "<strong>Warskôging: Jo binne dwaande mei in âldere ferzje fan dizze side.</strong>\nSoene jo dy fêstlizze, dan is alles wei wat sûnt dy tiid feroare is.",
        "currentrevisionlink": "Rinnende ferzje",
        "cur": "no",
        "next": "folgjende",
-       "last": "foarige",
+       "last": "frg.",
        "page_first": "earste",
        "page_last": "lêste",
-       "histlegend": "Ferskil oanjaan: Markearje de rûntsjes fan 'e te ferlykjen ferzjes, en druk op Enter of de knop ûnderoan.<br />\nLeginda: <strong>({{int:cur}})</strong> = ferskil mei de lêste ferzje, <strong>({{int:last}})</strong> = ferskil mei de eardere ferzje, <strong>{{int:minoreditletter}}</strong> = fan lytse betsjutting.",
+       "histlegend": "Ferskil oanjaan: Markearje de rûntsjes fan 'e te ferlykjen ferzjes, en druk op Enter of de knop ûnderoan.<br />\nLeginda: <strong>({{int:cur}})</strong> = ferskil mei de lêste ferzje, <strong>({{int:last}})</strong> = ferskil mei de foargeande ferzje, <strong>{{int:minoreditletter}}</strong> = fan lytse betsjutting.",
        "history-fieldset-title": "Ferzjes filterje",
        "histfirst": "âldste",
        "histlast": "nijste",
        "searchprofile-everything-tooltip": "Alle ynhâld trochsykje (ynklusyf oerlissiden)",
        "searchprofile-advanced-tooltip": "Sykje yn oanjûne nammeromten",
        "search-result-size": "$1 ({{PLURAL:$2|1 wurd|$2 wurden}})",
-       "search-redirect": "(trochferwizing $1)",
+       "search-redirect": "(trochwiisd fan $1)",
        "search-section": "(seksje $1)",
        "search-category": "(kategory $1)",
        "search-suggest": "Bedoele jo: $1",
        "search-nonefound": "Der binne gjin resultaten foar jo sykopdracht.",
        "powersearch-legend": "Utwreidich sykje",
        "powersearch-ns": "Nammeromten trochsykje:",
-       "powersearch-togglelabel": "Oanfinke:",
+       "powersearch-togglelabel": "Oanfinkje:",
        "powersearch-toggleall": "Alles",
        "powersearch-togglenone": "Gjint",
        "powersearch-remember": "Seleksje ûnthâlde foar sykopdrachten yn 'e takomst",
        "newsectionsummary": "/* $1 */ nije seksje",
        "rc-enhanced-expand": "Details werjaan",
        "rc-enhanced-hide": "Details ferskûlje",
-       "recentchangeslinked": "Folgje keppelings",
-       "recentchangeslinked-feed": "Folgje keppelings",
-       "recentchangeslinked-toolbox": "Folgje keppelings",
-       "recentchangeslinked-title": "Feroarings yn ferbân mei \"$1\"",
+       "recentchangeslinked": "Keppelings folgje",
+       "recentchangeslinked-feed": "Keppelings folgje",
+       "recentchangeslinked-toolbox": "Keppelings folgje",
+       "recentchangeslinked-title": "Feroarings besibbe mei \"$1\"",
        "recentchangeslinked-summary": "Jou in sidenamme, en besjoch de feroarings op siden dy't keppele binne fan as nei dy side. (Jou {{ns:category}}:Kategorynamme om de leden fan in kategory te besjen). Wizigings oan siden op [[Special:Watchlist|jo Folchlist]] wurde <strong>fet</strong> werjûn.",
        "recentchangeslinked-page": "Sidenamme:",
        "recentchangeslinked-to": "Feroarings oan siden mei ferwizings nei dizze side besjen",
        "sharedupload-desc-here": "Dit bestân komt fan $1, en kin ek troch oare projekten brûkt wurde.\nDe beskriuwing op syn [$2 bestânsside] dêre wurdt hjirûnder werjûn.",
        "filepage-nofile": "Der bestiet gjin bestân mei sa'n namme.",
        "filepage-nofile-link": "Der bestiet gjin bestân mei sa'n namme [bied $1 oan].",
-       "uploadnewversion-linktext": "Bied in nije ferzje fan dit bestân oan",
+       "uploadnewversion-linktext": "In nije ferzje fan dit bestân oanbiede",
        "shared-repo-from": "fan $1",
+       "upload-disallowed-here": "Jo kinne gjin nije ferzje fan dit bestân oanbiede.",
        "filerevert": "$1 weromsette",
        "filerevert-legend": "Bestân weromsette",
        "filerevert-intro": "Jo binne '''[[Media:$1|$1]]''' oan it weromdraaien ta de [$4 ferzje op $2, $3].",
        "rollbacklinkcount": "$1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie",
        "rollbacklinkcount-morethan": "mear as $1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie",
        "rollbackfailed": "Weromdraaien fan wizigings net slagge.",
-       "cantrollback": "Dizze feroaring kin net werom setten wurde, om't der mar ien skriuwer is.",
+       "cantrollback": "Kin de wiziging net ûngedien meitsje;\nde lêste bydrager is de iennichste bewurker fan dizze side.",
        "alreadyrolled": "Kin de wiziging fan [[:$1]] troch [[User:$2|$2]] ([[User talk:$2|oerlis]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) net weromdraaie;\nin oar hat de wiziging weromdraaid, of oars wat oan de side feroare.\n\nDe lêste wiziging wie fan [[User:$3|$3]] ([[User talk:$3|oerlis]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "De gearfetting wie: <em>$1</em>.",
        "revertpage": "Bewurkings fan [[Special:Contributions/$2|$2]] ([[User talk:$2|oerlis]]) weromset ta de lêste ferzje fan [[User:$1|$1]]",
        "undelete-show-file-submit": "Ja",
        "namespace": "Nammeromte:",
        "invert": "Seleksje útsein",
+       "tooltip-invert": "Oanfinkje om sidewizigings yn de selektearre nammeromte (en oanfinke de byhearrende nammeromte) te ferbergjen",
+       "tooltip-whatlinkshere-invert": "Oanfinkje om ferwizingssiden yn de selektearre nammeromte te ferbergjen.",
        "namespace_association": "Byhearrende nammeromte",
+       "tooltip-namespace_association": "Oanfinkje om by de selektearre nammeromte ek de ferbûne nammeromte foar oerlis of ynhâld te belûken",
        "blanknamespace": "(Haad)",
        "contributions": "Bydragen fan 'e {{GENDER:$1|meidogger|meidochster}}",
        "contributions-title": "Bydragen fan $1",
        "whatlinkshere-hidetrans": "$1 transklúzjes",
        "whatlinkshere-hidelinks": "$1 keppelings",
        "whatlinkshere-filters": "Filters",
-       "blockip": "Slút {{GENDER:$1|meidogger}} út",
+       "blockip": "{{GENDER:$1|Meidogger|Meidochster}} útslute",
        "blockiptext": "Brûk dizze fjilden om in beskaat IP-adres of meidochnamme fan skriuwtagong út te sluten.\nDat soe allinnich dien wurde moatte fanwegen fandalisme of oar ûnakseptabel hâlden en dragen, sa't de\n[[{{MediaWiki:Policy-url}}|útslút-rie]] it oanjout.\nMeld de krekte reden! Neam bygelyks de siden dy't oantaaste waarden.\nJo kinne IP-adresrigen útslute mei de syntaksis fan [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]; de grutst tastiene rige is /$1 foar IPv4 en /$2 foar IPv6.",
        "ipaddressorusername": "IP-adres of meidochnamme:",
        "ipbreason": "Reden:",
        "confirmemail_send": "Stjoer in befêstigingskoade",
        "confirmemail_sent": "Befêstiginskoade tastjoerd.",
        "confirmemail_sendfailed": "De befêstigingskoade koe net stjoerd wurde. Faaks stean der ferkearde tekens yn it e-postadres.\n\nBerjocht: $1",
-       "confirmemail_invalid": "Dizze befêstiginskoade jildt net (mear).\nFaaks is de koade ferrûn.",
+       "confirmemail_invalid": "Unjildige befêstigingskoade.\nDe koade soe ferrûn wêze kinne.",
        "confirmemail_needlogin": "Jo moatte $1 om jo e-mailadres befêstigje te kinnen.",
        "confirmemail_success": "Jo netpostadres is befêstige. Jo kinne jo no oanmelde en de wiki brûke.",
        "confirmemail_loggedin": "Jo e-mailadres is no befêstige.",
        "confirmemail_subject": "Befêstiging e-mailadres foar {{SITENAME}}",
-       "confirmemail_body": "Immen, nei gedachten jo, hat him by {{SITENAME}} oanmelde as \"$2\", mei dit netpostadres ($1).\n\nHjirtroch komme ek de netpostfunksjes fan {{SITENAME}} foar jo beskikber. Iepenje de neikommende keppeling om te befêstigjen dat jo wier josels by {{SITENAME}} mei dit netpostadres oanmelde hawwe:\n\n$3\n\nAt jo dat *net* wienen, brûk dy keppeling dan net, en klik hjir:\n\n$5\n\nDizze befêstigingskoade ferrint dan op $4.",
+       "confirmemail_body": "Immen, nei alle gedachten jo, mei it IP-adres $1,\nhat it akkount \"$2\" op {{SITENAME}} oanmakke mei dit e-mailadres.\n\nOm te befêstigjen dat dat akkount wier jowes is en de e-mailfunksjes\nop {{SITENAME}} beskikber makke wurde kinne, iepenje dan dizze\nferwizing yn jo webblêder:\n\n$3\n\nAt jo it akkount *net* oanmakke hawwe, folgje dan dizze ferwizing\nom it befêstigjen fan it e-mailadres ôf te sizzen:\n\n$5\n\nDe befêstigingskoade ferrint op $4.",
+       "invalidateemail": "Befêstigjen e-mail ôfsizze",
        "scarytranscludetoolong": "[URL-adres is te lang]",
        "confirmrecreate": "Sûnt jo begûn binne dizze side te bewurkjen, hat meidogger [[User:$1|$1]] ([[User talk:$1|oerlis]]) de side wiske. De reden dy't derfoar jûn waard wie:\n: ''$2''\nWolle jo de side wier op 'e nij skriuwe?",
        "unit-pixel": "px",
index 6fd1e82..7919e19 100644 (file)
        "history": "Listorik di paj-a",
        "history_short": "Listorik",
        "history_small": "listorik",
-       "updatedmarker": "modifyé dipi mo dannyé vizit",
+       "updatedmarker": "modifyé dipi zòt dannyé vizit",
        "printableversion": "Vèrsyon enprimab",
        "permalink": "Lyannaj pèrmannan",
        "print": "Enprimé",
        "boteditletter": "b",
        "rc-change-size-new": "$1 {{PLURAL:$1|ògtè}} apré chanjman",
        "rc-old-title": "kréyé inisyalman ké tit « $1 »",
-       "recentchangeslinked": "Swivi dé paj ki lyannen",
-       "recentchangeslinked-feed": "Swivi dé paj ki lyannen",
-       "recentchangeslinked-toolbox": "Swivi dé paj ki lyannen",
+       "recentchangeslinked": "Swivi di paj-ya ki yannen",
+       "recentchangeslinked-feed": "Swivi di paj-ya ki yannen",
+       "recentchangeslinked-toolbox": "Swivi di paj-ya ki yannen",
        "recentchangeslinked-title": "Swivi dé paj asosyé à « $1 »",
        "recentchangeslinked-summary": "Rantré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj ki lyannen dipi oben bò'd sa paj (pou wè manm-yan di roun katégori, rantré {{ns:category}}:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa <strong>an gra</strong>.",
        "recentchangeslinked-page": "Non di paj :",
        "sp-contributions-toponly": "Montré ki kontribisyon-yan ki sa dannyé-ya dé artik",
        "sp-contributions-newonly": "Afiché inikman modifikasyon-yan ki sa dé kréyasyon di paj",
        "sp-contributions-submit": "Sasé",
-       "whatlinkshere": "Paj ki lyannen",
+       "whatlinkshere": "Paj ki yannen",
        "whatlinkshere-title": "Paj ki ka pwenté bò'd « $1 »",
        "whatlinkshere-page": "Paj :",
        "linkshere": "Paj-ya ki anba ka kontni roun lyannaj bò'd <strong>$2</strong> :",
index 0c6121d..86526a1 100644 (file)
        "autoblockedtext": "כתובת ה־IP שלך נחסמה באופן אוטומטי כיוון שמשתמש אחר, שנחסם על־ידי $1, השתמש בה.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nבאפשרותך ליצור קשר עם $1 או עם כל אחד מ[[{{MediaWiki:Grouppage-sysop}}|מפעילי המערכת]] האחרים כדי לדון בחסימה.\n\nכמו־כן, באפשרותך להשתמש בתכונת \"{{int:emailuser}}\", אלא אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\n\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "systemblockedtext": "שם המשתמש או כתובת ה־IP שלך נחסמו באופן אוטומטי על־ידי תוכנת מדיה־ויקי.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nכתובת ה־IP הנוכחית שלך היא $3.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "blockednoreason": "לא ניתנה סיבה",
+       "blockedtext-composite": "<strong>שם המשתמש או כתובת ה־IP שלכם נחסמו מעריכה.</strong>\n\nהסיבה שניתנה היא:\n\n:<em>$2</em>.\n\n* תחילת החסימה: $8\n* פקיעת החסימה הארוכה ביותר: $6\n\nכתובת ה־IP הנוכחית שלך היא $3.\nיש לספק את כל המידע הנ\"ל עבור כל השאילתות שאתם מבצעים.",
+       "blockedtext-composite-reason": "ישנן מספר חסימות על החשבון שלך ו/או כתובת ה־IP שלך",
        "whitelistedittext": "נדרשת $1 כדי לערוך דפים.",
        "confirmedittext": "יש לאמת את כתובת הדוא\"ל לפני עריכת דפים.\nנא להגדיר ולאמת את כתובת הדוא\"ל שלך באמצעות [[Special:Preferences|העדפות המשתמש]] שלך.",
        "nosuchsectiontitle": "הפסקה לא נמצאה",
index d7eb11f..a6d9c48 100644 (file)
        "virus-badscanner": "Loša konfiguracija: nepoznati skener za viruse: ''$1''",
        "virus-scanfailed": "skeniranje neuspješno (kod $1)",
        "virus-unknownscanner": "nepoznati antivirus:",
-       "logouttext": "'''Odjavili ste se.'''\n\nNeke se stranice mogu prikazivati kao da ste još uvijek prijavljeni, sve dok ne očistite međuspremnik svog preglednika.",
+       "logouttext": "<strong>Odjavljeni ste.</strong>\n\nNeke se stranice mogu prikazivati kao da ste još uvijek prijavljeni, sve dok ne očistite međuspremnik svog preglednika.",
        "logging-out-notify": "Odjavljujemo Vas, molimo pričekajte.",
        "cannotlogoutnow-title": "Odjava trenutno nije moguća",
        "cannotlogoutnow-text": "Odjava nije moguća tijekom uporabe $1.",
index 450d6b2..410918c 100644 (file)
@@ -11,7 +11,8 @@
                        "Bfpage",
                        "Macofe",
                        "Lucas",
-                       "LeGuyanaisPure"
+                       "LeGuyanaisPure",
+                       "Schery19"
                ]
        },
        "tog-underline": "Souliyen lyen yo :",
        "history": "Istorik paj la",
        "history_short": "Istorik",
        "history_small": "Istwa",
-       "updatedmarker": "Aktyalize depi dènyè visit mwen",
+       "updatedmarker": "Aktyalize depi dènyè visit ou",
        "printableversion": "Vèsyon ou kapab enprime",
        "permalink": "Lyen pou tout tan",
        "print": "Enprime",
index 1385d75..f4017f4 100644 (file)
        "userlogin-createanother": "Másik felhasználói fiók létrehozása",
        "createacct-emailrequired": "E-mail-cím",
        "createacct-emailoptional": "E-mail-cím (opcionális)",
-       "createacct-email-ph": "Add meg e-mail-címed",
+       "createacct-email-ph": "Add meg az e-mail-címed",
        "createacct-another-email-ph": "Add meg az e-mail-címet",
        "createaccountmail": "Átmeneti, véletlenszerű jelszó beállítása és kiküldése a megadott e-mail-címre",
        "createaccountmail-help": "A jelszó megismerése nélkül készíthető valaki másnak fiók.",
        "autoblockedtext": "Az IP-címed automatikusan blokkolva lett, mert korábban egy olyan szerkesztő használta, akit $1 blokkolt, az alábbi indoklással:\n\n:''$2''\n\n*A blokk kezdete: '''$8'''\n*A blokk lejárata: '''$6'''\n*Blokkolt szerkesztő: '''$7'''\n\nKapcsolatba léphetsz $1 szerkesztőnkkel, vagy egy másik [[{{MediaWiki:Grouppage-sysop}}|adminisztrátorral]], és megbeszélheted vele a blokkolást.\n\nAz „{{int:emailuser}}” funkciót csak akkor használhatod, ha érvényes e-mail címet adtál meg\n[[Special:Preferences|fiókbeállításaidban]], és nem blokkolták a használatát.\n\nJelenlegi IP-címed: $3, a blokkolás azonosítószáma: #$5.\nKérjük, hogy érdeklődés esetén mindkettőt add meg.",
        "systemblockedtext": "A felhasználónevedet vagy IP-címedet automatikusan blokkolta a MediaWiki.\nA blokkolás indoka:\n\n:<em>$2</em>\n\n* A blokk kezdete: $8\n* A blokk lejárata: $6\n* Blokkolt szerkesztő: $7\n\nA jelenlegi IP-címed: $3.\nKérjük, hogy érdeklődés esetén minden fenti részletet adj meg.",
        "blockednoreason": "nem adott meg okot",
+       "blockedtext-composite": "<strong>A felhasználónevedet vagy IP-címedet blokkolták.</strong>\nA blokkolás indoka:\n\n:<em>$2</em>\n\n* A blokk kezdete: $8\n* A leghoszabb blokk lejárata: $6\n\nA jelenlegi IP-címed: $3.\nKérjük, hogy érdeklődés esetén minden fenti részletet adj meg.",
+       "blockedtext-composite-reason": "Fiókoddal és/vagy IP-címeddel szemben több blokk is érvényben van",
        "whitelistedittext": "Lapok szerkesztéséhez $1.",
        "confirmedittext": "Lapok szerkesztése előtt meg kell erősítened az e-mail címedet. Kérjük, hogy a [[Special:Preferences|szerkesztői beállításaidban]] add meg, majd erősítsd meg az e-mail címedet.",
        "nosuchsectiontitle": "A szakasz nem található",
        "passwordpolicies-policyflag-forcechange": "lecserélés követelése bejelentkezéskor",
        "passwordpolicies-policyflag-suggestchangeonlogin": "lecserélés ajánlása bejelentkezéskor",
        "unprotected-js": "Biztonsági okokból JavaScript nem tölthető be védtelen lapokról. Kérlek egyedül a MediaWiki névtérben készíts JavaScriptet, vagy szerkesztői allapként.",
-       "userlogout-continue": "Amennyiben ki szeretnél jelentkezni, [$1 használd a kijelentkezési oldalt]."
+       "userlogout-continue": "Biztos ki szeretnél jelentkezni?"
 }
index af01537..4cb740b 100644 (file)
        "categories-submit": "Ցուցադրել",
        "categoriespagetext": "Հետևյալ կատեգորիաները պարունակում են էջեր կամ մեդիա.\n[[Special:UnusedCategories|Unused categories]] are not shown here.\nAlso see [[Special:WantedCategories|wanted categories]].",
        "deletedcontributions": "Մասնակցի ջնջված ներդրում",
-       "deletedcontributions-title": "Մասնակցի ջնջված ներդրում",
+       "deletedcontributions-title": "Մասնակիցի ջնջուած ներդրում",
        "sp-deletedcontributions-contribs": "ներդրում",
        "linksearch": "Արտաքին հղումներ",
        "linksearch-ns": "Անվանատարածք.",
index 85d9246..eaee1b5 100644 (file)
        "navigation-heading": "Նաւարկութեան ցուցակ",
        "errorpagetitle": "Սխալ",
        "returnto": "Վերադարնալ դէպի $1։",
-       "tagline": "{{SITENAME}}էն",
+       "tagline": "",
        "help": "Օգնութիւն",
        "search": "Որոնել",
        "searchbutton": "Որոնել",
        "savearticle": "Էջը պահել",
        "savechanges": "Պահպանել փոփոխութիւնները",
        "publishpage": "Ստեղծել էջը",
-       "publishchanges": "Õ\80Ö\80Õ¡Õ¿արակել փոփոխութիւնները",
+       "publishchanges": "Õ\80Ö\80Õ¡Õºարակել փոփոխութիւնները",
        "savearticle-start": "Էջը պահել...",
        "savechanges-start": "Պահպանել փոփոխութիւնները...",
        "publishpage-start": "Ստեղծել էջը...",
        "prevn": "նախորդ {{PLURAL:$1|$1}}",
        "nextn": "յաջորդ {{PLURAL:$1|$1}}",
        "prev-page": "նախորդ էջ",
-       "next-page": "յաջորդ էջ",
+       "next-page": "յաջորդ էջը",
        "prevn-title": "Նախորդ $1 {{PLURAL:$1|արդիւնքը|արդիւնքները}}",
        "nextn-title": "Յաջորդ $1 {{PLURAL:$1|արդիւնքը|արդիւնքները}}",
        "shown-title": "Իւրաքանչիւր էջի վրայ ցուցնել $1 {{PLURAL:$1|արդիւնք|արդիւնքներ}}",
        "search-external": "Արտաքին որոնում",
        "preferences": "Նախընտրութիւններ",
        "mypreferences": "Նախընտրութիւններ",
-       "skin-preview": "Նախադիտել",
+       "skin-preview": "Կանխաստուգել",
        "prefs-watchlist": "Հսկողութեան ցանկ",
        "prefs-editwatchlist-clear": "Մաքրել հսկողութեան ցանկը",
        "saveprefs": "Յիշել",
        "prefs-info": "Հիմնական տուեալներ",
        "prefs-signature": "Ստորագրութիւն",
        "prefs-editor": "Խմբագրող",
-       "prefs-preview": "Նախադիտել",
+       "prefs-preview": "Կանխաստուգել",
        "group": "Խումբ.",
        "group-bot": "Մեքենայիկներ",
        "group-sysop": "Վարիչներ",
        "all-logs-page": "Բոլոր հանրային տեղեկատետրերը",
        "alllogstext": "{{SITENAME}} կայքի տեղեկատետրերու միացեալ ցանկ։\nԿրնաք արդիւնքները սահմանափակել ըստ տեղեկատետրի տեսակին, մասնակիցի անունին կամ համապատասխան էջին։",
        "logempty": "Համապատասխան տարրեր չկան տեղեկատետերին մէջ։",
+       "checkbox-none": "Ոչ մէկ",
        "allpages": "Բոլոր էջերը",
        "allarticles": "Բոլոր էջերը",
        "allpagessubmit": "‎Յառաջանալ",
        "allpages-hide-redirects": "Թաքցնել վերայղումները",
        "categories": "Ստորոգութիւններ",
+       "deletedcontributions": "Մասնակիցի ջնջուած ներդրում",
        "activeusers": "Աշխոյժ մասնակիցներու ցանկ",
        "activeusers-submit": "Ցոյց տալ աշխոյժ մասնակիցները",
        "listgrouprights-members": "(անդամներու ցանկ)",
        "logentry-newusers-autocreate": "$1 մասնակցային հաշիւը {{GENDER:$2|ստեղծուած է}} ինքնաբերաբար",
        "logentry-upload-upload": "$1 {{GENDER:$2|ներբեռնուած է}} $3",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|վերբեռնեց}} $3ի նոր տարբերակ",
+       "rightsnone": "(ոչ մէկ)",
        "feedback-cancel": "Չեղարկել",
        "searchsuggest-search": "Որոնել {{SITENAME}} կայքին մէջ",
        "duration-days": "$1 {{PLURAL:$1|օր}}",
+       "expand_templates_preview": "Կանխաստուգել",
        "special-characters-group-latin": "Լատիներէն",
        "special-characters-group-arabic": "Արաբերէն",
        "randomrootpage": "Պատահական արմատ էջ"
index fba1d63..63969ab 100644 (file)
        "autoblockedtext": "Tu adresse IP ha essite automaticamente blocate perque un altere usator lo usava qui esseva blocate per $1.\nLe motivo presentate es:\n\n:<em>$2</em>\n\n* Initio del blocada: $8\n* Expiration del blocada: $6\n* Blocato intendite: $7\n\nTu pote contactar $1 o un del altere [[{{MediaWiki:Grouppage-sysop}}|administratores]] pro discuter le blocada.\n\nNota que tu pote solmente utilisar le function \"{{int:emailuser}}\" si tu ha registrate un adresse de e-mail valide in tu [[Special:Preferences|preferentias de usator]] e tu non ha essite blocate de usar lo.\n\nTu adresse IP actual es $3, e le ID del blocada es #$5.\nPer favor include tote le detalios supra specificate in omne correspondentia.",
        "systemblockedtext": "Tu nomine de usator o adresse IP ha essite blocate automaticamente per MediaWiki.\nLe motivo presentate es:\n\n:<em>$2</em>\n\n* Initio del blocada: $8\n* Expiration del blocada: $6\n* Blocato intendite: $7\n\nTu adresse IP actual es $3.\nPer favor, include tote le detalios enumerate hic supra in omne questiones que tu pone.",
        "blockednoreason": "nulle motivo specificate",
+       "blockedtext-composite": "<strong>Tu nomine de usator o adresse IP ha essite blocate.</strong>\n\nLe motivo presentate es:\n\n:<em>$2</em>.\n\n* Initio del blocada: $8\n* Expiration del blocada le plus longe: $6\n\nTu adresse IP actual es $3.\nPer favor, include tote le detalios enumerate hic supra in omne questiones que tu pone.",
+       "blockedtext-composite-reason": "Il ha plure blocadas contra tu conto e/o adresse IP",
        "whitelistedittext": "Tu debe $1 pro poter modificar paginas.",
        "confirmedittext": "Tu debe confirmar tu adresse de e-mail pro poter modificar paginas.\nPer favor entra e valida tu adresse de e-mail per medio de tu [[Special:Preferences|preferentias de usator]].",
        "nosuchsectiontitle": "Section non trovate",
index d695f7e..a965592 100644 (file)
        "virus-scanfailed": "skano ne sucesis (kodexo $1)",
        "virus-unknownscanner": "antiviruso nekonocata:",
        "logouttext": "<strong>Vu ekirabas.</strong>\n\nAtencez ke kelka pagini posible duras montresar quaze vu ne ekiris, til ke vu vakuigos la tempala-magazino di la navigilo.",
+       "logout-failed": "Ne povas ekirar nun: $1",
        "cannotlogoutnow-title": "Ne povas ekirar nun",
        "cannotlogoutnow-text": "Ekirar ne esas posibla kande vu uzas $1.",
        "welcomeuser": "Esez bonvenanta, $1!",
        "botpasswords-newpassword": "La nova pasovorto por enirar <strong>$1</strong> esas <strong>$2</strong>.\n<em>Voluntez memorigar to por futura refero.</em> <br> (Por anciena ''bot-''i, qui bezonas la nomo di 'login' esar la sama kam l'eventuala nomo dil uzero, vu anke povas uzar <strong>$3</strong> kom uzero-nomo, e <strong>$4</strong> kom pasovorto.)",
        "botpasswords-no-provider": "\"BotPasswordsSessionProvider\" ne esas disponebla.",
        "botpasswords-restriction-failed": "Restrikti pri pasovorti koncerne ''bot''-i impedas vua 'log in'.",
+       "botpasswords-invalid-name": "L'uzero-nomo informata ne kontenas separilo di 'bot'-pasovorto (\"$1\")",
        "botpasswords-not-exist": "L'uzero \"$1\" ne havas pasovorto nomizita \"$2\" por lua 'bot'.",
        "botpasswords-needs-reset": "La pasovorto por la 'bot' nomizita \"$1\" dal {{GENDER:$2|uzero}} \"$2\" mustas rikreesar.",
        "botpasswords-locked": "Vu ne povas facar 'login' per robotala pasovorto (bot password), pro ke vua konto blokusesis.",
        "subject-preview": "Previdado di la temo:",
        "previewerrortext": "Eventis eroro kande on probis krear previdado pri vua modifikuri.",
        "blockedtitle": "La uzero esas blokusita",
+       "blocked-email-user": "<strong>Vu blokusesis pri sendar e-posto. Vu ankore povas redaktar altra pagini en ca wiki.</strong> Vu povas konocar omna detali pri la blokuso en la [[Special:MyContributions|pagino pri vua kontributadi]].\n\nLa blokuso facesis da $1.\n\nLa motivo esis <em>$2</em>.\n\n* Komenco di la blokuso: $8\n* La blokuso finos ye: $6\n* Motivo por blokuso: $7\n* Nombro dil blokuso #$5",
        "blockedtext-partial": "<strong>Vua uzero-nomo od IP-adreso blokusesis koncerne modifikuri en ca pagino. Vu ankore povas redaktar altra pagini en ca Wiki.</strong> Vu povas vidar omna detali pri la blokuso en [[Special:MyContributions|account contributions]].\n\n$1 blokusis vu. La motivo esis <em>$2</em>.\n\n* Komenco dil blokuso: $8\n* Fino dil blokuso: $6\n* Motivo dil blokuso: $7\n* Blokuso #$5",
        "blockedtext": "<strong>Vua uzantonomo od IP-adreso blokusesis.</strong>\n\n$1 blokusis vu.\nLa motivo esis <em>$2</em>.\n\n* Komenco di la blokuso: $8\n* Fino di la blokuso: $6\n* Motivo dil blokuso: $7\n\nVu povas kontaktar $1 od altra [[{{MediaWiki:Grouppage-sysop}}|administrero]] por diskutar la blokuso.\nVu ne povas uzar \"email this user\" por sendar e-posto ecepte se valida email indikesis en tua [[Special:Preferences|preferaji dil uzanto]], e se vu ne blokusesis por uzar ol.\nVua nuna IP-adreso esas $3, e la ID dil blokuso esas #$5.\nVoluntez inkluzor omna detali adsupre en omna demandi quin vu facos.",
        "autoblockedtext": "<strong>Vua uzantonomo od IP-adreso blokusesis.</strong>\n\n$1 blokusis vu.\nLa motivo esis <em>$2</em>.\n\n* Komenco di la blokuso: $8\n* Fino di la blokuso: $6\n* Persono blokusata: $7\n\nVu povas kontaktar $1 od altra [[{{MediaWiki:Grouppage-sysop}}|administrero]] por diskutar pri la blokuso.\nVu ne povas uzar \"email this user\" por sendar e-posto, ecepte se valida email indikesis en tua [[Special:Preferences|preferaji dil uzero]], e se vu ne blokusesis por uzar ol.\nVua nuna IP-adreso esas $3, e la ID dil blokuso esas #$5.\nVoluntez inkluzor omna detali adsupre en omna demandi quin vu facos.",
        "systemblockedtext": "Vua uzero-nomo od IP-adreso blokusabis automatale da MediaWiki.\nLa motivo esas:\n\n:<em>$2</em>\n\n* Komenco di la blokuso: $8\n* Fino di la blokuso: $6\n* Persono blokuzata: $7\n\nVua nuna IP-adreso esas $3.\nVoluntez inkluzar omna detalii furnisita adsupre, en irga demandi quin vu facos.",
-       "blockednoreason": "nula motivo donesis",
+       "blockednoreason": "nula motivo informesis",
        "whitelistedittext": "Vu mustas $1 por redaktar pagini.",
        "confirmedittext": "Vu mustas konfirmar vua adreso di e-posto ante ke vu povas redaktar pagini. Voluntez informar e validigar vua e-posto adreso tra vua [[Special:Preferences|preferaji di uzero]].",
        "nosuchsectiontitle": "On ne povis trovar la seciono",
        "nolinkstoimage": "Nula pagino ligesas ad ica pagino.",
        "morelinkstoimage": "Videz [[Special:WhatLinksHere/$1|plusa ligili]] ad ica arkivo.",
        "linkstoimage-redirect": "$1 (arkivo ridirektita) $2",
-       "sharedupload": "Ca arkivo esas de $1 e posible esas uzata da altra projekti.",
+       "sharedupload": "Ca arkivo originis de $1 e posible esas uzata da altra projeti.",
        "sharedupload-desc-here": "Ca arkivo jacas en $1, e povas uzesar en altra projeti.\nLa deskriptado en lua [$2 pagino di deskriptado] montresas adinfre.",
        "sharedupload-desc-edit": "Ca arkivo venas de $1 e povas uzesar en altra projeti.\nPosible vu deziros redaktar ibe lua deskripto en [$2 lua deskripto-pagino].",
        "sharedupload-desc-create": "Ca arkivo venas de $1 e povas uzesar en altra projeti.\nPosible vu deziros redaktar ibe lua deskripto en [$2 lua deskripto-pagino].",
        "ipb-blocklist-contribs": "Kontributadi dil uzero {{GENDER:$1|$1}}",
        "block-actions": "Agadi blokusota:",
        "block-expiry": "Expiro:",
+       "block-options": "Plusa agadi:",
+       "block-reason": "Motivo:",
        "unblockip": "Desblokusar uzero",
        "unblockiptext": "Uzez la sequanta formularo por restaurar la skribo-aceso ad IP-adreso qua blokusesis antee.",
        "ipusubmit": "Desblokusar",
        "ipblocklist": "Blokusita uzanti",
+       "blocklist-reason": "Motivo",
        "ipblocklist-submit": "Serchar",
        "ipblocklist-otherblocks": "Altra {{PLURAL:$1|blokuso|blokusi}}",
        "infiniteblock": "nefinita",
        "mw-widgets-dateinput-no-date": "Nula dato selektita",
        "mw-widgets-dateinput-placeholder-day": "YYYY-MM-DD",
        "mw-widgets-titleinput-description-redirect": "Ridirektar ad $1",
+       "mw-widgets-usersmultiselect-placeholder": "Adjuntez pluse...",
+       "mw-widgets-titlesmultiselect-placeholder": "Adjuntez pluse...",
        "date-range-to": "Til (dato):",
        "sessionprovider-nocookies": "''Bisquiti'' forsan esas desacendita. Certigez ke vu acendar ''bisquiti'' e riprobez.",
        "randomrootpage": "Hazarda radikopagino",
index 5e3bc65..2948200 100644 (file)
        "history": "Cronologia della pagina",
        "history_short": "Cronologia",
        "history_small": "cronologia",
-       "updatedmarker": "modificata dalla mia ultima visita",
+       "updatedmarker": "modificata dalla tua ultima visita",
        "printableversion": "Versione stampabile",
        "permalink": "Link permanente",
        "print": "Stampa",
        "createaccountblock": "registrazione bloccata",
        "emailblock": "e-mail bloccate",
        "blocklist-nousertalk": "non può modificare la propria pagina di discussione",
+       "blocklist-editing": "modifica",
+       "blocklist-editing-sitewide": "modifica (sito intero)",
        "blocklist-editing-page": "pagine",
        "blocklist-editing-ns": "namespace",
        "ipblocklist-empty": "L'elenco dei blocchi è vuoto.",
index 94f5ab0..e27a850 100644 (file)
        "action-deletechangetags": "データベースからタグの削除",
        "action-purge": "このページのキャッシュ破棄",
        "action-apihighlimits": "API要求でのより高い制限値の使用",
+       "action-autoconfirmed": "IPベースの速度制限を受けない",
        "action-bigdelete": "大きな履歴があるページの削除",
        "action-blockemail": "利用者のメール送信のブロック",
+       "action-bot": "自動処理と認識させる",
        "action-editprotected": "「{{int:protect-level-sysop}}」の保護を設定されたページの編集",
        "action-editsemiprotected": "「{{int:protect-level-autoconfirmed}}」の保護を設定されたページの編集",
        "action-editinterface": "ユーザーインターフェースの編集",
        "action-editmyuserjson": "自分のJSONファイルの編集",
        "action-editmyuserjs": "自分のJavaScriptファイルの編集",
        "action-viewsuppressed": "すべての利用者から隠された版の閲覧",
+       "action-hideuser": "利用者名をブロックして公開記録から隠す",
        "action-ipblock-exempt": "IPブロック、自動ブロック、広域ブロックの回避",
        "action-unblockself": "自分に対するブロックの解除",
+       "action-noratelimit": "速度制限を受けない",
        "action-reupload-own": "自分がアップロードした既存のファイルへの上書き",
+       "action-nominornewtalk": "議論ページの細部の編集をした際に、新着メッセージとして通知しない",
        "action-markbotedits": "巻き戻しをボットの編集として扱う",
        "action-patrolmarks": "最近の更新での巡回済み印の閲覧",
        "action-override-export-depth": "リンク先ページの5階層まで含めた書き出し",
        "passwordpolicies-policyflag-suggestchangeonlogin": "ログイン時に変更を提案",
        "easydeflate-invaliddeflate": "提供されたコンテンツが適切に圧縮されていません",
        "unprotected-js": "セキュリティ上の理由から、JavaScriptは保護されていないページからは読み込みできません。MediaWiki: 名前空間内、利用者下位ページのいずれかでのみjavascriptを作成してください。",
-       "userlogout-continue": "ã\83­ã\82°ã\82¢ã\82¦ã\83\88ã\82\92è¡\8cã\81\84ã\81\9fã\81\84å ´å\90\88ã\80\81[$1 ã\83­ã\82°ã\82¢ã\82¦ã\83\88ã\83\9aã\83¼ã\82¸ã\81\8bã\82\89å®\9fæ\96½]ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82"
+       "userlogout-continue": "ã\83­ã\82°ã\82¢ã\82¦ã\83\88ã\81\97ã\81¾ã\81\99ã\81\8bï¼\9f"
 }
index 759c803..9c0eaf4 100644 (file)
        "email": "E-poste",
        "prefs-help-realname": "Namo rastıkên serbesto.\nSıma ke ney bıgurenê, karê sıma de no namdarêni dano.",
        "prefs-help-email": "Dayışê adresa e-postey keyfiyo, labelê seba eyarê parola lazıma, wexto ke şıma naye xo vira kerê.",
-       "prefs-help-email-others": "Şıma şenê weçinê ke ê bini be yew gırey pela şımaya karberi ya zi pela werênayışi sera şıma de ebe e-poste irtıbat kewê.\nKaberê bini ke şıma de kewti irtıbat, adresa e-postey şıma eşkera nêbena.",
+       "prefs-help-email-others": "Şıma şenê weçinê ke ê bini be yew gırey pela şımaya karberi ya zi pela werênayışi sera şıma de ebe e-poste irtıbat kewê.\nKaberê bini ke şıma de kewti irtıbat, adresa e-postey şıma aşkera nêbena.",
        "prefs-help-email-required": "Adresa emaili lazıma.",
        "prefs-signature": "İmza",
        "prefs-diffs": "Ferqi",
index af8afe7..ae0377a 100644 (file)
                        "Ryuch",
                        "Delim",
                        "Comjun04",
-                       "Son77391"
+                       "Son77391",
+                       "Jango"
                ]
        },
        "tog-underline": "링크에 밑줄 긋기:",
-       "tog-hideminor": "ìµ\9cê·¼ ë°\94ë\80\9cì\97\90ì\84\9c ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91ì\9d\84 숨기기",
+       "tog-hideminor": "ìµ\9cê·¼ ë³\80ê²½í\95\9c ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91 숨기기",
        "tog-hidepatrolled": "최근 바뀜에서 점검한 편집을 숨기기",
        "tog-newpageshidepatrolled": "새 문서 목록에서 검토한 문서를 숨기기",
        "tog-hidecategorization": "페이지 분류 숨기기",
        "autoblockedtext": "당신의 IP 주소는 $1님이 차단한 사용자가 사용했던 IP이기 때문에 자동으로 차단되었습니다.\n차단된 이유는 다음과 같습니다:\n\n:<em>$2</em>\n\n* 차단이 시작된 시간: $8\n* 차단이 끝나는 시간: $6\n* 차단된 사용자: $7\n\n$1 또는 [[{{MediaWiki:Grouppage-sysop}}|다른 관리자]]에게 차단에 대해 문의할 수 있습니다.\n\n[[Special:Preferences|사용자 환경 설정]]에 올바른 이메일 주소가 있어야만 \"이메일 보내기\" 기능을 사용할 수 있습니다. 또한 이메일 보내기 기능이 차단되어 있으면 이메일을 보낼 수 없습니다.\n\n현재 IP 주소는 $3이고, 차단 ID는 #$5입니다.\n문의할 때에 이 정보를 같이 알려주세요.",
        "systemblockedtext": "당신의 사용자 이름 또는 IP 주소가 자동으로 미디어위키에 의해 차단되었습니다.\n이유는 다음과 같습니다:\n\n:<em>$2</em>\n\n* 차단 시작: $8\n* 차단 만료: $6\n* 차단 대상: $7\n\n당신의 현재 IP 주소는 $3입니다.\n문의에 대해 상기의 상세 설명을 모두 포함해 주십시오.",
        "blockednoreason": "이유를 입력하지 않음",
+       "blockedtext-composite": "<strong>당신의 사용자 이름 또는 IP 주소가 미디어위키에 의해 차단되었습니다.\n\n이유는 다음과 같습니다:\n\n:<em>$2</em>\n\n* 차단 시작: $8\n* 차단 만료: $6\n\n당신의 현재 IP 주소는 $3입니다.\n문의에 대해 상기의 상세 설명을 모두 포함해 주십시오.",
        "whitelistedittext": "문서를 편집하기 전에 $1해야 합니다.",
        "confirmedittext": "문서를 고치려면 이메일 인증 절차가 필요합니다.\n[[Special:Preferences|사용자 환경 설정]]에서 이메일 주소를 입력하고 이메일 주소 인증을 해주시기 바랍니다.",
        "nosuchsectiontitle": "문단을 찾을 수 없음",
        "confirm-unwatch-top": "이 문서를 주시문서 목록에서 뺄까요?",
        "confirm-rollback-button": "확인",
        "confirm-rollback-top": "이 문서의 편집을 되돌리시겠습니까?",
+       "confirm-rollback-bottom": "이 작업은 이 문서의 선택된 변경 사항을 즉시 되돌립니다.",
        "confirm-mcrrestore-title": "판 복구",
        "confirm-mcrundo-title": "변경사항 취소",
        "mcrundofailed": "실행 취소를 실패했습니다",
        "passwordpolicies-policyflag-suggestchangeonlogin": "로그인할 때 변경 제안",
        "easydeflate-invaliddeflate": "주어진 컨텐츠가 적절히 압축되지 않았습니다",
        "unprotected-js": "보안 상의 이유로 자바스크립트는 보호되지 않은 문서로부터 불러올 수 없습니다. 미디어위키: 이름공간이나 사용자의 하위 문서에서만 자바스크립트를 만들어 주십시오.",
-       "userlogout-continue": "로그아웃하려면 [$1 페이지 로그아웃 문서로 이동하십시오]."
+       "userlogout-continue": "로그아웃하시겠습니까?"
 }
index 853cd14..0ae2c67 100644 (file)
        "history": "Historique vun der Säit",
        "history_short": "Versiounen",
        "history_small": "Versiounen",
-       "updatedmarker": "geännert zanter ech d'Säit fir d'lescht gekuckt hunn",
+       "updatedmarker": "geännert zanter Ärem leschte Besuch",
        "printableversion": "Drockversioun",
        "permalink": "Zitéierfäege Link",
        "print": "Drécken",
        "mw-widgets-abandonedit-keep": "Virufuere mat Änneren",
        "mw-widgets-abandonedit-title": "Sidd Dir sécher?",
        "mw-widgets-copytextlayout-copy": "Kopéieren",
+       "mw-widgets-copytextlayout-copy-success": "An den Tëschespäicher kopéiert",
        "mw-widgets-dateinput-no-date": "Keen Datum erausgesicht",
        "mw-widgets-dateinput-placeholder-day": "JJJJ-MM-DD",
        "mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
index fb8d8d7..4afee0e 100644 (file)
@@ -13,7 +13,8 @@
                        "Alirezaaa",
                        "Fitoschido",
                        "Matěj Suchánek",
-                       "Physicsch"
+                       "Physicsch",
+                       "FarsiNevis"
                ]
        },
        "tog-underline": "خط کیشائن ژێر پیوندەل:",
        "moveddeleted-notice-recent": "متاسفانه صفحه قبلا حذف شده‌است (در ۲۴ ساعت اخیر) \nدلیل حذف و سیاههٔ انتقال در پائین موجود است.",
        "log-fulllog": "مشاهدهٔ سیاههٔ کامل",
        "edit-hook-aborted": "ویرایش توسط قلاب لغو شد.\nتوضیحی در این مورد داده نشد.",
-       "edit-gone-missing": "اÙ\85کاÙ\86 Ø¨Ù\87â\80\8cرÙ\88ز Ú©Ø±Ø¯Ù\86 ØµÙ\81Ø­Ù\87 Ù\88جÙ\88د Ù\86دارد.\nبÙ\87 Ù\86ظرÙ\85Û\8câ\80\8cرسد Ú©Ù\87 ØµÙ\81Ø­Ù\87 Ø­Ø°Ù\81 Ø´Ø¯Ù\87 Ø¨Ø§Ø´Ø¯.",
+       "edit-gone-missing": "اÙ\85کاÙ\86 Ø±Ù\88زآÙ\85دسازÛ\8c ØµÙ\81Ø­Ù\87 Ù\88جÙ\88د Ù\86دارد.\nبÙ\87 Ù\86ظر Ù\85Û\8câ\80\8cرسد Ú©Ù\87 ØµÙ\81Ø­Ù\87 Ø­Ø°Ù\81 Ø´Ø¯Ù\87 Ø§Ø³Øª.",
        "edit-conflict": "تعارض ویرایشی.",
        "edit-no-change": "ویرایش شما نادیده گرفته شد، زیرا تغییری در متن داده نشده بود.",
        "postedit-confirmation-created": "وةڵگة دؤرس بیة",
        "revdelete-log": ":دةلیل",
        "revdelete-submit": "اعمال بر {{PLURAL:$1|نسخهٔ|نسخه‌های}} انتخاب شده",
        "revdelete-success": "نمایش رویزیون به‌روژ بوو",
-       "revdelete-failure": "'''Ù¾Û\8cداÛ\8cÛ\8c Ù\88رÚ\98Ù\86 Ù\87ا Ù\82ابÙ\84 Ø¨Ù\87 Ø±Ù\88ز Ú©Ø±Ø¯Ù\86 نیست:'''\n$1",
+       "revdelete-failure": "'''Ù¾Û\8cداÛ\8cÛ\8c Ù\86سخÙ\87â\80\8cÙ\87ا Ù\82ابÙ\84 Ø±Ù\88زآÙ\85دسازÛ\8c نیست:'''\n$1",
        "logdelete-success": "ورود نمایش ست",
        "logdelete-failure": "'''پیدایی سیاهه‌ها قابل تنظیم نیست:'''\n$1",
        "revdel-restore": "گؤەڕانن/تغییر پیدایی",
        "backend-fail-batchsize": "دسته‌ای مشتمل بر $1 {{PLURAL:$1|عملکرد|عملکردها}} پرونده به پشتیبان ذخیره داده شد؛ حداکثر مجاز $2 {{PLURAL:$2|عملکرد|عملکردها}} است.",
        "backend-fail-usable": "امکان خواندن یا نوشتن پروندهٔ $1 وجود نداشت چرا که سطح دسترسی کافی نیست یا شاخه/محفظهٔ مورد نظر وجود ندارد.",
        "filejournal-fail-dbconnect": "امکان وصل شدن به پایگاه داده دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
-       "filejournal-fail-dbquery": "اÙ\85کاÙ\86 Ø¨Ù\87 Ø±Ù\88ز Ú©Ø±Ø¯Ù\86 Ù¾Ø§Û\8cگاÙ\87 Ø¯Ø§Ø¯Ù\87 دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
+       "filejournal-fail-dbquery": "اÙ\85کاÙ\86 Ø±Ù\88زآÙ\85دسازÛ\8c Ø¯Ø§Ø¯Ú¯Ø§Ù\86 دفترخانه برای پشتیبان ذخیره‌سازی «$1» وجود نداشت.",
        "lockmanager-notlocked": "نمی‌توان قفل «$1» را گشود؛ چون قفل نشده‌است.",
        "lockmanager-fail-closelock": "امکان بستن پرونده قفل شده \"$1\" وجود ندارد.",
        "lockmanager-fail-deletelock": "امکان حذف پرونده قفل شده \"$1\" وجود ندارد.",
        "nonfile-cannot-move-to-file": "امکان انتقال محتوای غیر پرونده به فضای نام پرونده وجود ندارد",
        "imagetypemismatch": "پسوند پرونده تازه با نوع آن سازگار نیست",
        "imageinvalidfilename": "نام پروندهٔ هدف نامجاز است",
-       "fix-double-redirects": "بÙ\87 Ø±Ù\88ز Ú©Ø±Ø¯Ù\86 ØªÙ\85اÙ\85Û\8c تغییرمسیرهایی که به مقالهٔ اصلی اشاره می‌کنند",
+       "fix-double-redirects": "رÙ\88زآÙ\85دسازÛ\8c Ù\87Ù\85Ù\87Ù\94 تغییرمسیرهایی که به مقالهٔ اصلی اشاره می‌کنند",
        "move-leave-redirect": "بر جا گذاشتن یک تغییرمسیر",
        "protectedpagemovewarning": "'''هشدار:''' این صفحه قفل شده‌است به طوری که تنها کاربران با دسترسی مدیریت می‌توانند آن را انتقال دهند.\nآخرین موارد سیاهه در زیر آمده است:",
        "semiprotectedpagemovewarning": "'''تذکر:''' این صفحه قفل شده‌است به طوری که تنها کاربران ثبت نام کرده می‌توانند آن را انتقال دهند.\nآخرین موارد سیاهه در زیر آمده است:",
        "watchlistedit-raw-legend": "ویرایش فهرست خام پی‌گیری‌ها",
        "watchlistedit-raw-explain": "عنوان‌های موجود در فهرست پی‌گیری‌های شما در زیر نشان داده شده‌اند، و شما می‌توانید مواردی را حذف یا اضافه کنید؛ هر مورد در یک سطر جداگانه باید قرار بگیرد.\nدر پایان، دکمهٔ «{{int:Watchlistedit-raw-submit}}» را بفشارید.\nتوجه کنید که شما می‌توانید از [[Special:EditWatchlist|ویرایشگر استاندارد فهرست پی‌گیری‌ها]] هم استفاده کنید.",
        "watchlistedit-raw-titles": "عنوانةل:",
-       "watchlistedit-raw-submit": "بÙ\87â\80\8cرÙ\88زرساÙ\86ی پی‌گیری‌ها",
+       "watchlistedit-raw-submit": "رÙ\88زآÙ\85دسازی پی‌گیری‌ها",
        "watchlistedit-raw-done": "فهرست پی‌گیری‌های شما به روز شد.",
        "watchlistedit-raw-added": "$1 عنوان به فهرست پی‌گیری‌ها اضافه {{PLURAL:$1|شد|شدند}}:",
        "watchlistedit-raw-removed": "$1 عنوان حذف {{PLURAL:$1|شد|شدند}}:",
        "log-description-pagelang": "ای پهرستنومه در بلگه زونا آلشت گرته.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2| تغییریافت}} زبان صفحه برای  $3  از  $4  به  $5 .",
        "default-skin-not-found": "اوه! پوسته پیش‌فرض برای ویکی شما تعریف‌شده در <code dir=\"ltr\"<$wgDefaultSkin</code> به عنوان <code>$1</code>، در دسترس نیست.\n\nبه نظر می‌آید نصب شما شامل پوسته‌های زیر می‌شود. [https://www.mediawiki.org/wiki/Manual:Skin_configuration راهنما: تنظیمات پوسته] را برای کسب اطلاعات در باره چگونگی فعال‌ساختن آن‌ها و انتخاب پیش‌فرض ببینید.\n\n$2\n\n; اگر اخیراً مدیاویکی را نصب کرده‌اید:\n: احتمالاً از گیت، یا به طور مستقیم از کد مبدأ که از چند متد دیگر استفاده می‌کند نصب کردید. انتظار می‌رود. چند {{PLURAL:$4|پوسته|پوسته}} از [https://www.mediawiki.org/wiki/Category:All_skins فهرست پوسته mediawiki.org] نصب کنید، که همراه چندین پوسته و افزونه هستند. شما می‌توانید شاخه <code>skins/</code> را از آن نسخه‌برداری کرده و بچسبانید.\n\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins استفاده از گیت برای دریافت پوسته‌ها].\n: انجام این کار با مخزن گیت‌تان تداخل نمی‌کند اگر توسعه‌دهنده مدیاویکی هستید.\n\n; اگر اخیراً مدیاویکی را ارتقاء دادید:\n: مدیاویکی ۱٫۲۴ و تازه‌تر دیگر به طور خودکار پوسته‌های نصب‌شده را فعال نمی‌کند ([https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery راهنما: کشف خودکار پوسته] را ببینید). شما می‌توانید خطوط زیر را به داخل <code>LocalSettings.php</code> بچسبانید تا {{PLURAL:$5|همه|همه}} پوسته‌های نصب‌شده را فعال کنید:\n\n<pre dir=\"ltr\">$3</pre>\n\n; اگر اخیراً <code>LocalSettings.php</code> را تغییر دادید:\n: نام پوسته‌ها را برای غلط املایی دوباره بررسی کنید.",
-       "default-skin-not-found-no-skins": "پوستهٔ پیش‌فرض برای ویکی شما تعریف‌شده در<code>$wgDefaultSkin</code> به عنوان <code>$1</code>، هست موجود نیست.\n\nشما پوسته‌ها را نصب نکرده‌اید.\n\n:اگر مدیاویکی را به‌روز یا نصب کرده‌اید:\n:ممکن است از گیت یا از کد منبع با روش‌های دیگر نصب کرده‌اید. انتظار می‌رود MediaWiki 1.24 یا جدیدتر در پوشهٔ اصلی هیچ پوسته‌ای نداشته باشند.\nسعی کنید تعدادی پوسته از [https://www.mediawiki.org/wiki/Category:All_skins پوشهٔ پوسته‌های مدیاویکی]، با:\n:*دریافت [https://www.mediawiki.org/wiki/Download نصب‌کننده تاربال]، که با چندین پوسته و افزونه هست. شما می توانید پوستهٔ <code>skins/</code> را از آن کپی و پیست کنید.\n:*کلون کردن یکی از <code dir=\"ltr\">mediawiki/skins/*</code> از مخزن در پوشهٔ <code>skins/</code> مدیاویکی‌تان.\n:اگر توسعه‌دهندهٔ مدیاویکی هستید، انجام این کار نباید تعارضی با مخزن گیت شما داشته باشد. برای اطلاعات بیشتر و فعال کردن پوسته‌ها و انتخاب آنها به عنوان پیش‌فرض [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: تنظیمات پوسته] را مشاهده کنید.",
+       "default-skin-not-found-no-skins": "پوستهٔ پیش‌فرض برای ویکی شما تعریف‌شده در<code>$wgDefaultSkin</code> به‌عنوان <code>$1</code>، هست موجود نیست.\n\nشما پوسته‌ها را نصب نکرده‌اید.\n\n:اگر مدیاویکی را روزآمد یا نصب کرده‌اید:\n:ممکن است از گیت یا از کد منبع با روش‌های دیگر نصب کرده‌اید. انتظار می‌رود MediaWiki 1.24 یا جدیدتر در پوشهٔ اصلی هیچ پوسته‌ای نداشته باشند.\nسعی کنید تعدادی پوسته از [https://www.mediawiki.org/wiki/Category:All_skins پوشهٔ پوسته‌های مدیاویکی]، با:\n:*دریافت [https://www.mediawiki.org/wiki/Download نصب‌کننده تاربال]، که با چندین پوسته و افزونه هست. شما می توانید پوستهٔ <code>skins/</code> را از آن کپی و پیست کنید.\n:*کلون کردن یکی از <code dir=\"ltr\">mediawiki/skins/*</code> از مخزن در پوشهٔ <code>skins/</code> مدیاویکی‌تان.\n:اگر توسعه‌دهندهٔ مدیاویکی هستید، انجام این کار نباید تعارضی با مخزن گیت شما داشته باشد. برای اطلاعات بیشتر و فعال کردن پوسته‌ها و انتخاب آنها به‌عنوان پیش‌فرض [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: تنظیمات پوسته] را مشاهده کنید.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (فعال)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 ('''غیر فعال''')",
        "mediastatistics": "آمار رسانه‌ها",
index 57f0c4b..dee85ef 100644 (file)
@@ -36,7 +36,7 @@
        "tog-enotifwatchlistpages": "ٱر یاٛ بٱلگٱ یا جانؽا د ساٛلٛ بٱرگ ماْ آلشت بۊئٱ ماْ ناْ ڤا ٱنجومانامٱ خڤٱر کو",
        "tog-enotifusertalkpages": "ڤٱختؽ کاْ بٱلگٱ سالفٱ کاریاریم آلشت کاری بی ماْ ناْ ڤا ٱنجومانامٱ خڤٱر کو",
        "tog-enotifminoredits": "ڤٱختؽ کاْ ڤیرایشؽا کوچکؽ د بٱلگٱیایا جانؽایا ٱنجوم بۊئٱ ماْ ناْ ڤارٱسیاری کو",
-       "tog-enotifrevealaddr": "تیر نشوݩ ٱنجومانامٱ ماْ ناْ د ٱنجومانامٱ دؽارکو دؽاری کو",
+       "tog-enotifrevealaddr": "تیرنشوݩ ٱنجومانامٱ ماْ ناْ د ٱنجومانامٱ دؽارکو دؽاری کو",
        "tog-shownumberswatching": "ٱندازٱ کاریارؽایی کاْ د هال ۉ بال دیئن هؽسن دؽاری کو",
        "tog-oldsig": "اْمزا ایسنی شما:",
        "tog-fancysig": "ڤا اْمزا چی یاٛ ڤیکی نیسسٱ رٱفتار کو",
        "filecopyerror": "نمۊئٱ جانؽا $1 د $2 ڤرداشتٱ بۊئٱ",
        "filerenameerror": "نمۊئٱ نوم جانؽا $1 د $2 آلشت کاری بۊئٱ.",
        "filedeleteerror": "نمۊئٱ جانؽا $1 پاکسا بۊئٱ.",
-       "directorycreateerror": "نمۊئٱ تیرنشونگٱاٛ$1 دۏرس بۊئٱ.",
+       "directorycreateerror": "نمۊئٱ تیرنشونگٱ$1 دۏرس بۊئٱ.",
        "directoryreadonlyerror": "فقٱت مۊئٱ تیرنشونگٱ \"$1\" ناْ بونی.",
        "directorynotreadableerror": "تیرنشونگٱ \"$1\" ڤٱننی نؽ.",
        "filenotfound": "نمؽ تونؽت جانؽا $1 ناْ بٱجۊرؽت.",
        "createaccount": "هساو دۏرس بٱکؽت",
        "userlogin-resetpassword-link": "رازینٱ گوئارسن تو د ڤیرتو رٱتٱ؟",
        "userlogin-helplink2": "هومیاری کردن د تٱریق ڤامؽن اوماین",
-       "userlogin-loggedin": "شما ایساْ چی یاٛ {{GENDER:$1|$1}} اومایتٱ ڤامؽن.نوم بٱلگٱ هاری ناْ سی ڤامؽن اوماین چی یاٛ کاریار هنی بٱلگٱ هاری سی ڤا مؽن اومابن چی یاٛ کاریار هنی ڤ کار باٛیرؽت.",
+       "userlogin-loggedin": "شما ایساْ چی یاٛ {{GENDER:$1|$1}} اومایتٱ ڤامؽن.نوم بٱلگٱ هاری ناْ سی ڤامؽن اوماین چی یاٛ کاریار هنی بٱلگٱ هاری سی ڤا مؽن اوماین چی یاٛ کاریار هنی ڤ کار باٛیرؽت.",
        "userlogin-createanother": "یاٛ هساو هنی دۏرس بٱکؽت",
        "createacct-emailrequired": "تیرنشوݩ ٱنجومانامٱ",
        "createacct-emailoptional": "تیرنشوݩ ٱنجومانامٱ",
        "permissionserrorstext-withaction": "شما سی $2 سلا \nنهاگیری نارؽت {{PLURAL:$1|دلٛیلٛ|دلٛیلٛؽا}}:",
        "recreate-moveddeleted-warn": "'''ڤ ڤیرتو با:شما بٱلگاٛیی کاْ ھا ڤادما ۉ پاکسا بیٱ د نۊ دۏرس کردؽتٱ.'''\nبایٱد د ڤیرتو با کاْ آیا ھنی نوئاگیری ڤیرایش اؽ بٱلگٱ خۊئٱ.\nپاکسا کاری ۉ جا ڤ جا کاری اؽ بٱلگٱ سی هال ۉ بار پٱلٛٱمار شما آمادٱ بیٱ:",
        "moveddeleted-notice": "اؽ بٱلگٱ پاکسا بیٱ.\nپاکسا کاری ۉ جا ڤ جا کاری اؽ بٱلگٱ سی هال ۉ بار پٱلٛٱمار شما آمادٱ بیٱ.",
-       "log-fulllog": "دیئن هأمە پئهئرستنوٙمە یا",
-       "edit-hook-aborted": "Ú¤Û\8cراÛ\8cئشت Ú¤Ø§ Ù\82Ù\88Ù\84اڤ Ù\86ئھاگئرÛ\8c Ø¨Û\8cÛ\8cÛ\95.\nÚ¾Û\8cÚ\86 ØªÙ\88ضÛ\8cÛ\8c Ø³Û\8cØ´ Ù\86Û\8c.",
-       "edit-gone-missing": "نأبوٙە ئی بألگە نە ڤئ ھئنگوم بأکیت.\nچئنی ڤئ نأظأر میا کئ ڤئ پاکسا بییە.",
-       "edit-conflict": "ری ڤئ ری کاری د ڤیرایئشت.",
-       "edit-no-change": "سی یە کئ ھیچ آلئشتکاری د نیسئسە أنجوم نأگئرئتە د ڤیرایئشتکای شوم تیە پوٙشی بییە.",
-       "postedit-confirmation-created": "بألگە دوروس بییە.",
-       "postedit-confirmation-restored": "بألگە د نۊ ئمایە بییە.",
-       "postedit-confirmation-saved": "Ú¤Û\8cراÛ\8cئشتئتÙ\88Ù\99 Ø¦Ù\85اÛ\8cÛ\95 بی.",
-       "edit-already-exists": "نأبوٙە یئ گئل بألگە تازە بأکیت .\nڤئ ھیش.",
-       "defaultmessagetext": "Ù\86Û\8cسئسÛ\95 Ù¾Ø¦Û\8cغÙ\88Ù\85 Ù¾Û\8cØ´ Ù\81أرض",
-       "content-failed-to-parse": "د یأک تیچئسئن چیا مین $2 د مودئل $1:$3",
-       "invalid-content-data": "دÙ\88Ù\86ئسÙ\85Ø£Ù\86Û\8c Ù\85Û\8cÙ\86Ù\88Ù\99Ù\86Û\95 Ù\86ادÛ\8cار",
+       "log-fulllog": "دیئن هٱمٱ پهرستنومٱیا",
+       "edit-hook-aborted": "Ú¤Û\8cراÛ\8cØ´ Ú¤Ø§ Ù\82Ù\88Ù\84اڤ Ù\86ھاگÛ\8cرÛ\8c Ø¨Û\8cÙ±.\nÚ¾Û\8cÚ\98 ØªÛ\89زÛ\8cÙ\87Û\8c Ø³Û\8cØ´ Ù\86ؽ.",
+       "edit-gone-missing": "نمۊئٱ اؽ بٱلگٱ ناْ ڤ ھٱنگوم بٱکؽت.\nچنی ڤ نٱزٱر مؽا کاْ ڤٱ پاکسا بیٱ.",
+       "edit-conflict": "ری ڤ ری کاری د ڤیرایش.",
+       "edit-no-change": "سی یٱ کاْ ھیژ آلشتکاری د نیسسٱ ٱنجوم نٱگرتٱ د ڤیرایشتکاری شوم تیٱ پۊشی بیٱ.",
+       "postedit-confirmation-created": "بٱلگٱ دۏرس بیٱ.",
+       "postedit-confirmation-restored": "بٱلگٱ د نۊ آمادٱ بیٱ.",
+       "postedit-confirmation-saved": "Ú¤Û\8cراÛ\8cØ´ ØªÙ\88 Ø¢Ù\85ادٱ بی.",
+       "edit-already-exists": "نمۊئٱ یاٛ بٱلگٱ تازٱ بٱکؽت .\nڤٱ ھؽسش.",
+       "defaultmessagetext": "Ù\86Û\8cسسٱ Ù¾Ø§Ù\9bغÙ\88Ù\85 Ù¾Û\8cØ´ Ù\81ٱرز",
+       "content-failed-to-parse": "د یٱک تیچسن چیا مؽن $2 د مودل $1:$3",
+       "invalid-content-data": "دÙ\88Ù\86سÙ\85Ù±Ù\86Û\8c Ù\85Û\8cÙ\86Ù\88Ù\86Ù± Ù\86ادؽار",
        "content-not-allowed-here": " مینوٙنە \"$1\" سی بألگە [[:$2]] صئلا دأ نأبیە",
-       "editwarning-warning": "أر ئی بألگە نئ ڤئل بأکیت ھأر آلئشتی کئ أنجوم دأئیتە پاک بوٙە.\nأر شوما ھائیت ڤامین، شوما می توٙنیت ب زئنار نە د \"{{int:prefs-editing}}\" کئ ھا د بأرجا چیا نازار شوما ناکونئشتگأر بأکیت.",
-       "editpage-notsupportedcontentformat-title": "شئکل مینوٙنە حامینداری نأبییە",
-       "editpage-notsupportedcontentformat-text": "حال و بال مینوٙنە $1 د مودئل مینوٙنە $2 حامینداری نأبوٙە.",
+       "editwarning-warning": "ٱر اؽ بٱلگٱ ناْ ڤلٛ بٱکؽت ھٱر آلشتی کاْ ٱنجوم داٛئؽتٱ پاک مۊئٱ.\nٱر شما ھایؽت ڤامؽن، شما مؽ تونؽت ب زٱنڳیار ناْ د \"{{int:prefs-editing}}\" کاْ ھا د بٱئرجا چیا نازار شما ناکونشگٱر بٱکؽت.",
+       "editpage-notsupportedcontentformat-title": "شکل مینونٱ هامینداری ناٛییٱ",
+       "editpage-notsupportedcontentformat-text": "هال ۉ بال مینونٱ $1 د مودل مینونٱ $2 هامینداری نمۊئٱ.",
        "content-model-wikitext": "ڤیکی نیسسٱ",
-       "content-model-text": "Ù\86Û\8cسئسÛ\95 Ø³Ø§Ø¯Û\95",
-       "content-model-javascript": "جاڤا Ø¦Ø³Ú©Ø¦ریپت",
+       "content-model-text": "Ù\86Û\8cسسٱ Ø³Ø§Ø¯Ù±",
+       "content-model-javascript": "جاڤا Ø§Ù\92سکریپت",
        "content-model-css": "سی اس اس",
-       "content-json-empty-object": "داسÙ\88Ù\99Ù\86 Ø­Ø§Ù\84ی",
-       "content-json-empty-array": "آرایە حالی",
-       "duplicate-args-category": "بÙ\84Ú¯Ù\87 Û\8cا Û\8cÛ\8c Ú©Ù\87 Ú\86Ú© Ú\86Ù\86Ù\87 Ú©Ø§Ø±Û\8cا Ø¯Ù\88 Ú©Ù\88Ù\86Ù\87 Ù\86Ù\87 Ø¯ Ú\86Ù\88ئÙ\87 Û\8cا Ù\88احÙ\88Ù\86Û\8cØ´Ù\88 Ù\88Ù\87 Ú©Ø§Ø± Ù\85Û\8cئرÙ\86",
-       "duplicate-args-category-desc": "بÙ\84Ú¯Ù\87 Û\8cÛ\8c Ú©Ù\87 Ø¢Ø±Ú¯Ù\88Ù\85اÙ\86 Ø¯Ù\88Ú©Ù\88Ù\86Ù\87 Ø¯Ø§Ø±Ù\87 چی، <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> یا <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
-       "expensive-parserfunction-warning": "<strong>زئنار:</strong>ای بلگه مینونه دار واحونی دستوریا مئن اشکافت فره ای هئ.\n\nانازه و باید د کمتر با$2 {{PLURAL:$2|واحونی|واحونیا}}، ایسه {{PLURAL:$1|$1 واحونی|$1 واحونیا}}ئه.",
-       "expensive-parserfunction-category": "بÙ\84Ú¯Ù\87 Û\8cاÛ\8cÛ\8c Ú©Ù\87 Ù\88احÙ\88Ù\86Û\8c Ù¾Û\8cÙ\88Ù\86دگر Ø®Ø·Ø§ Ú¯Ø±Ù\88Ù\86 Ù\81رÙ\87 Ø§Û\8c ها دشو",
-       "post-expand-template-inclusion-warning": "زÙ\86ئار Ú\86Ù\88ئÙ\87 Ø¯ Ù\88ر Ú¯Ø±ØªÙ\87 Ø§Ù\86ازÙ\87 Ø§Û\8c Û\8cÙ\87 Ú©Ù\87 Ù\81رÙ\87 Ú¯Ù¾Ù\87.پارÙ\87 Ø§Û\8c Ø¯ Ú\86Ù\88ئÙ\87 Û\8cا Ù\86Ù\87 Ø¯ Ù\88ر Ù\86Ù\85Û\8cئرÙ\87.",
-       "post-expand-template-inclusion-category": "بÙ\84Ú¯Û\8cا Ø¯ Ù\88ر Ú¯Ø±ØªÙ\87 Ú\86Ù\88ئÙ\87 Û\8cÙ\86 Ú©Ù\87 Ø§Ù\86ازش Ø¯ Ø­Ø¯ Ø§Ù\88Ù\85ائÙ\87 Ù\88Ù\87 Ø¯ر",
-       "post-expand-template-argument-warning": "زÙ\86Ù\87ار Ø§Û\8c Ø¨Ù\84Ú¯Ù\87 Ø¯ Ù\88ر Ú¯Ø±ØªÙ\87 Ø­Ø¯Ø§Ù\82Ù\84 Û\8cÙ\87 Ú\86Ù\88ئÙ\87 Ø³Û\8c Ú\86Ú© Ú\86Ù\86Ù\87 Û\8cÙ\87 Ú©Ù\87 Ø§Ù\86ازÙ\87 Ù\81رÙ\87 Ú¯Ù¾Ù\87.\nگپسÙ\86Û\8cا Ù¾Ø§Ú© Ø¨Û\8cÙ\86Ù\87.",
+       "content-json-empty-object": "داسÙ\88Ù\86 Ù\87اÙ\84Ù\9bی",
+       "content-json-empty-array": "آرایٱ هالٛی",
+       "duplicate-args-category": "بٱÙ\84Ú¯Ù±Û\8cاÛ\8cؽ Ú©Ø§Ù\92 Ú\86Ù±Ú© Ú\86Ù\86Ù± Ú©Ø§Ø±Û\8cا Ø¯Û\8f Ú¯Û\8aÙ\86Ù± Ù\86اÙ\92 Ø¯ Ú\86Û\8aئٱ Û\8cا Ú¤Ø§Ù\87Ù\88Ù\86Û\8cØ´Ù\88 Ú¤ Ú©Ø§Ø± Ù\85اÙ\9bÛ\8cرٱÙ\86",
+       "duplicate-args-category-desc": "بٱÙ\84گاÙ\9bÛ\8cؽ Ú©Ø§Ù\92 Ø¢Ø±Ú¯Ù\88Ù\85اÙ\86 Ø¯Û\8fÚ¯Û\8aÙ\86Ù± Ø¯Ø§Ø±Ù± چی، <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> یا <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
+       "expensive-parserfunction-warning": "<strong>زٱنڳیار:</strong>اؽ بٱلگٱ مینونٱ دار ڤاهونی دٱستۊرؽا مؽن اْشکافت فراٛیؽ هؽ.\n ٱندازٱ بایٱد د کٱمتر با$2 {{PLURAL:$2|ڤاهونی|ڤاهونؽا}}، ایساْ {{PLURAL:$1|$1ڤاهونی|$1 ڤاهونؽا}} ئٱ.",
+       "expensive-parserfunction-category": "بٱÙ\84Ú¯Ù±Û\8cاÛ\8cؽ Ú©Ø§Ù\92 Ú¤Ø§Ù\87Ù\88Ù\86Û\8c Ù¾Ø§Ù\9bÚ¤Ù±Ù\86گر Ø®Ù±ØªØ§ Ú¯Ø±Ù\88Ù\86 Ù\81راÙ\9bÛ\8cؽ ها دشو",
+       "post-expand-template-inclusion-warning": "زٱÙ\86Ú³Û\8cار Ú\86Û\8aئٱ Ø¯ Ú¤Ù±Ø± Ú¯Ø±ØªÙ± Ù±Ù\86دازاÙ\9b Û\8cÙ± Ú©Ø§Ù\92 Ù\81رٱ Ú¯Ù±Ù¾Ù±.پاراÙ\9bÛ\8cؽ Ø¯ Ú\86Û\8aئٱÛ\8cا Ù\86اÙ\92 Ø¯ Ú¤Ù±Ø± Ù\86Ù\85اÙ\9bÛ\8cرٱ.",
+       "post-expand-template-inclusion-category": "بٱÙ\84Ú¯Ù±Û\8cا Ø¯ Ú¤Ù±Ø± Ú¯Ø±ØªÙ± Ú\86Û\8aئٱ Ù\87ؽسÙ\86 Ú©Ø§Ù\92 Ù±Ù\86دازٱش Ø¯ Ù\87ٱد Ø§Ù\88Ù\85اÛ\8cÙ± Ú¤ Ø¯Ù±ر",
+       "post-expand-template-argument-warning": "زٱÙ\86Ú³Û\8cار Ø§Ø½ Ø¨Ù±Ù\84Ú¯Ù± Ø¯ Ú¤Ù±Ø± Ú¯Ø±ØªÙ± Ù\87ٱدٱÙ\82Ù±Ù\84 Û\8cاÙ\9b Ú\86Û\8aئٱ Ø³Û\8c Ú\86Ù±Ú© Ú\86Ù\86Ù± Û\8cÙ± Ú©Ø§Ù\92 Ù±Ù\86دازٱ Ù\81رٱ Ú¯Ù±Ù¾Ù±.\nگٱپسÙ\86ؽا Ù¾Ø§Ú© Ø¨Û\8cÙ\86Ù±.",
        "post-expand-template-argument-category": "بلگه د ور گرته چوئه چک چنیا د بین رئته",
        "parser-template-loop-warning": "حلقه چوئه دیاری کرده:[[$1]]",
        "parser-template-recursion-depth-warning": "محدودیت پی یا ورئشتن چوئه رد بی($1)",
        "search-relatedarticle": "مرتوط",
        "searchrelated": "مرتوط",
        "searchall": "همٱ",
-       "showingresults": "نمائشت بیشترونه {{PLURAL:$1|'''۱''' نتیجه|'''$1''' نتیجه}} د هار، شرو د شماره'''$2'''.",
-       "showingresultsinrange": "نمائشت بیشترونه {{PLURAL:$1|'''۱''' نتیجه|'''$1''' نتیجه}} د هار، شرو د شماره'''$2''' تا شماره '''$3'''.",
+       "showingresults": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2'''.",
+       "showingresultsinrange": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2''' تا شمارٱ '''$3'''.",
        "search-showingresults": "{{PLURAL:$4|نٱتیجٱیا<strong>$1</strong> د <strong>$3</strong>|نٱتیجٱیا<strong>$1 - $2</strong د <strong>$3</strong>}}",
        "search-nonefound": "هیچ نتیجاٛیؽ ڤا پاٛجۊری تو یٱکؽ نؽ.",
        "powersearch-legend": "پی جوری پیشکرده",
        "prefs-registration-date-time": "$1",
        "yourrealname": "نوم راستكی:",
        "yourlanguage": "زوٙن:",
-       "yourvariant": "Ù\85Û\8cÙ\86Ù\88Ù\86Ù\87 Ø¢Ù\84شتگر Ø²Ù\88Ù\86:",
-       "prefs-help-variant": "Ù\82سÙ\87 Ù\88رÛ\8c Ø§Ù\86تخاÙ\88Û\8c Ø´Ù\85ا Ø³Û\8c Ù\86Ù\85ائشت Ù\85Û\8cÙ\86Ù\88Ù\86Ù\87 Ø¨Ù\84Ú¯Ù\87 Û\8cا Ø¯ Ø§Û\8c Ù\88یکی.",
+       "yourvariant": "Ù\85Û\8cÙ\86Ù\88Ù\86Ù± Ø¢Ù\84شتگٱر Ø²Ú¤Ù\88Ý©:",
+       "prefs-help-variant": "Ù\82سٱ Ú¤Ø±Û\8c Ø§Ù\92Ù\86تخاÙ\88Û\8cÛ\8c Ø´Ù\85ا Ø³Û\8c Ù\86Ù\85اÛ\8cØ´ Ù\85Û\8cÙ\86Ù\88Ù\86Ù± Ø¨Ù±Ù\84Ú¯Ù±Û\8cا Ø¯ Ø§Ø½ Ú¤یکی.",
        "yournick": "امضا تازه:",
        "prefs-help-signature": "ویر و باوریا نیسسه بیه د بلگه چک چنه باید وا«<nowiki>~~~~</nowiki>» امضا بان؛ ای نشون وه شکل خودانجومی وه امضا شما و مؤر ویرگار تبدیل بوئه.",
        "badsig": "ئمضا خوم بی ئتئڤار.\nسأردیسیا ئچ تی ئم ئل نە ڤارئسی بأکیت.",
        "right-createtalk": "بلگه یا چک چنه نه راس بکید",
        "right-createaccount": "یه گل حساو کاروری تازه راس بکیت",
        "right-minoredit": "نشودار کردن همه ویرایشتیا چی حیرده",
-       "right-move": "بÙ\84Ú¯Ù\87 Û\8cا Ø¬Ø§ Ù\88Ù\87 جا کو",
-       "right-move-subpages": "بÙ\84Ú¯Ù\87 Û\8cا Ù\88 Ø²Û\8cر Ø¨Ù\84Ú¯Ù\87 Û\8cا Ø´Ù\88Ù\86Ù\87 Ø¬Ø§ Ù\88Ù\87 جا کو",
-       "right-move-rootuserpages": "بÙ\84Ú¯Ù\87 Û\8cا Ø±Û\8cØ´Ù\87 Ø§Û\8c Ú©Ø§Ø±Ù\88ر Ù\86Ù\87 Ø¬Ø§ Ù\88Ù\87 جا کو",
+       "right-move": "بٱÙ\84Ú¯Ù±Û\8cا Ù\86اÙ\92 Ø¬Ø§ Ú¤ جا کو",
+       "right-move-subpages": "بٱÙ\84Ú¯Ù±Û\8cا Û\89 Ø²Ø½Ø± Ø¨Ù±Ù\84Ú¯Ù±Û\8cا Ø´Ù\88Ù\86اÙ\92 Ø¬Ø§ Ú¤ جا کو",
+       "right-move-rootuserpages": "بٱÙ\84Ú¯Ù±Û\8cا Ø±Û\8cشاÙ\9bÛ\8cÛ\8c Ú©Ø§Ø±Û\8cار Ù\86اÙ\92 Ø¬Ø§ Ú¤ جا کو",
        "right-move-categorypages": "دسه بلگه یا نه جا وه جا بکیت",
-       "right-movefile": "جانیایا نه جا وه جا کو",
+       "right-movefile": "جانؽایا ناْ جا ڤ جا کو",
        "right-suppressredirect": "اوسه که بلگه یا د بین رئتنه هیچ واگردونی سی بلگه یا سرچشمه دروس نبیه",
        "right-upload": "سوار کردن جانیایا",
        "right-reupload": "سوارکرد هنی جانیایی که دماتر بئیشه",
        "action-createaccount": "هساو اؽ کاریار ناْ دۏرس بٱکؽت",
        "action-history": "ویرگار ای بلگه نه بوینیت",
        "action-minoredit": "ای ویرایشت نه چی یه حیرده ویرایشت نشو بیئت",
-       "action-move": "لی بلگه جا وه جا کو",
+       "action-move": "اؽ بٱلگٱ ناْ جا ڤ جا کو",
        "action-move-subpages": "ای بلگه و زیر بلگه یاشه جا وه جا بکید",
        "action-move-rootuserpages": "بلگه یا ریشه ای کاریار نه جا وه جا بکید",
        "action-move-categorypages": "جا وه جا کردن دسه بلگه یا",
        "upload-preferred": "جوٙرا حاستئنی جانیا {{PLURAL:$2|جوٙر|جوٙرا}}:$1 .",
        "upload-prohibited": "جورا جانیا صلادار:$1{{PLURAL:$2|.}}",
        "uploadlogpage": "سڤارکرد",
-       "uploadlogpagetext": "Ù\86Ù\88Ù\85Ú¯Ù\87 Ù\87ارÛ\8c Û\8cÙ\87 Ú¯Ù\84 Ù\86Ù\88Ù\85Ú¯Ù\87 Ø¯ Ø¢Ø®Ø±Û\8c Ø³Ù\88ارکرد Ø¬Ø§Ù\86Û\8cاÛ\8cا Ù\87ئ.\nسÛ\8c Ø¯ Ù\86Ù\88 Ø³Û\8cÙ\84 Ú©Ø±Ø¯Ù\86[[Special:NewFiles|عسگدÙ\88Ù\86Û\8c Ø¬Ø§Ù\86Û\8cاÛ\8cا ØªØ§Ø²Ù\87 Ù\86Ù\87]] Ø¨Ù\87 Ù\88Ù\86Û\8cت.",
+       "uploadlogpagetext": "Ù\86Ù\88Ù\85Ú¯Ù± Ù\87ارÛ\8c Û\8cاÙ\9b Ù\86Ù\88Ù\85Ú¯Ù± Ø¯ Ø¢Ø®Ø±Û\8c Ø³Ú¤Ø§Ø±Ú©Ø±Ø¯ Ø¬Ø§Ù\86ؽاÛ\8cا Ù\87ؽ.\nسÛ\8c Ø¯ Ù\86Û\8a Ø³Ø§Ù\9bÙ\84Ù\9b Ú©Ø±Ø¯Ù\86[[Special:NewFiles|عٱسگدÙ\88Ù\86Û\8c Ø¬Ø§Ù\86ؽاÛ\8cا ØªØ§Ø²Ù± Ù\86اÙ\92]] Ø¨Ú¤Ù±Ù\86ؽت.",
        "filename": "نوم جانیا",
        "filedesc": "چکسٱ",
        "fileuploadsummary": "چکسه",
        "filehist-comment": "ڤیر ۉ باڤٱر",
        "imagelinks": "ڤ کار گرتن جانؽا",
        "linkstoimage": "دۏنبال بيٱ {{PLURAL:$1|ديس ڤنؽا بٱلگٱ|$1 ديس ڤنؽا بٱلگٱيا}} د اؽ فایلٛ:",
-       "linkstoimage-more": "بؽشتر Ø¯ $1 Ø¨Ù±Ù\84Ú¯Ù± Ø¯ Ø§Ø½ Ø¬Ø§Ù\86ؽا Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 {{PLURAL:$1|بٱ|بÛ\8cÙ\86Ù±}}.\nÙ\86Ù\88Ù\85Ú¯Ù± Ù\87ارÛ\8c ØªÙ±Ù\86ڳؽا{{PLURAL:$1|Ù±Ú¤Ù\84Û\8c Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86|Ù±Ú¤Ù\84Û\8c $1 Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86}} Ø¯ Ø½ Ø¨Ù±Ù\84Ú¯Ù± Ù\86اÙ\92 Ù\86Ø´Ù\88Ý© Ù\85ؽ یٱ.\n[[Special:WhatLinksHere/$2|نومگٱ کامل]] ٱم هؽسش.",
+       "linkstoimage-more": "بؽشتر Ø¯ $1 Ø¨Ù±Ù\84Ú¯Ù± Ø¯ Ø§Ø½ Ø¬Ø§Ù\86ؽا Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 {{PLURAL:$1|بٱ|بÛ\8cÙ\86Ù±}}.\nÙ\86Ù\88Ù\85Ú¯Ù± Ù\87ارÛ\8c ØªÙ±Ù\86ڳؽا{{PLURAL:$1|Ù±Ú¤Ù\84Û\8c Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86|Ù±Ú¤Ù\84Û\8c $1 Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86}} Ø¯ Ø½ Ø¨Ù±Ù\84Ú¯Ù± Ù\86اÙ\92 Ù\86Ø´Ù\88Ý© Ù\85اÙ\9bیٱ.\n[[Special:WhatLinksHere/$2|نومگٱ کامل]] ٱم هؽسش.",
        "nolinkstoimage": "ایچاْ هیچ بٱلگاٛیی سی هوم پیاٛڤٱن بیئن ڤا اؽ جانؽا نؽ",
        "morelinkstoimage": " [[ویجه:چه هوم پیوندی ها ایچه/$1|هوم پیوندیا هنی]]سی ای جانیا نه بونیت.",
        "linkstoimage-redirect": "$1 (ڤاگٱردونی جانؽا) $2",
        "brokenredirectstext": "واگردونیا نهاتر د بلگه یایی که وجود نارن هوم پیوند بینه.",
        "brokenredirects-edit": "ڤیرایئشت",
        "brokenredirects-delete": "پاكسا كردن",
-       "withoutinterwiki": "بÙ\84Ú¯Ù\87 Û\8cاÛ\8cÛ\8c Ú©Ù\87 Ù\87Ù\88Ù\85 Ù¾Û\8cÙ\88Ù\86د Ø²Ù\88Ù\86 Ù\86ارÙ\86",
-       "withoutinterwiki-summary": "بÙ\84Ú¯Ù\87 Û\8cا Ù\87ارÛ\8c Ù\88Ù\87 Ø²Ù\88Ù\86 Ù\86سÙ\82Ù\87 Û\8cا Ø²Ù\88Ù\86ا ØªØ± Ù\87Ù\88Ù\85 Ù¾Û\8cÙ\88Ù\86د Ù\86بÛ\8cÙ\87.",
+       "withoutinterwiki": "بٱÙ\84Ú¯Ù±Û\8cاÛ\8cؽ Ú©Ø§Ù\92 Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ø²Ú¤Ù\88Ý© Ù\86ارٱÙ\86",
+       "withoutinterwiki-summary": "بٱÙ\84Ú¯Ù±Û\8cا Ù\87ارÛ\8c Ú¤ Ø²Ú¤Ù\88Ý© Ù\86Û\8fسخٱÛ\8cا Ø²Ú¤Ù\88Ù\86ؽا ØªØ± Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ù\86اÙ\9bئÛ\8cÙ\86Ù±.",
        "withoutinterwiki-legend": "پیشون",
        "withoutinterwiki-submit": "نشون دائن",
        "fewestrevisions": "بلگه یایی که کمتری وانئری نه دارن",
        "ntransclusions": "$1 {{PLURAL:$1|بلگه|بلگيا}} استفاده بیه",
        "specialpage-empty": "نتیجه ای د ای گزارشت نئ.",
        "lonelypages": "بلگه یا تک منه",
-       "lonelypagestext": "د Ø¨Ù\84Ú¯Ù\87 Û\8cا Ù\87ارÛ\8c Ù\87Û\8cÚ\86 Ø¨Ù\84Ú¯Ù\87 Ù\87Ù\86Û\8c Ø¯ {{SITENAME}} Ù\87Ù\88Ù\85 Ù¾Û\8cÙ\88Ù\86د Ù\86بÛ\8cÙ\87 Ù\88 Ø¯ Ù\87Û\8cÚ\86 Ø¨Ù\84Ú¯Ù\87 Ù\87Ù\86Û\8c Ù\85Û\8cÙ\86 Ú\86Û\8cÙ\86 Ù\86بÛ\8cÙ\87.",
+       "lonelypagestext": "د Ø¨Ù±Ù\84Ú¯Ù±Û\8cا Ù\87ارÛ\8c Ù\87Û\8cÚ\98 Ø¨Ù±Ù\84گاÙ\9b Ù\87Ù\86Û\8c Ø¯ {{SITENAME}} Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ù\86اÙ\9bئÛ\8cÙ±Û\89 Ø¯ Ù\87Û\8cÚ\98 Ø¨Ù±Ù\84گاÙ\9b Ù\87Ù\86Û\8c Ù\85ؽÙ\86 Ú\86Û\8cÙ\86 Ù\86اÙ\9bئÛ\8cÙ±.",
        "uncategorizedpages": "بلگه یا دسه بنی نبیه",
        "uncategorizedcategories": "دسه یا دسه بنی نبیه",
        "uncategorizedimages": "فایلیا دسه بنی نبیه",
        "shortpages": "بلگه یا کؤچک",
        "longpages": "بلگه یا گپ",
        "deadendpages": "بلگه یا نابود بیئنی",
-       "deadendpagestext": "بÙ\84Ú¯Ù\87 Û\8cا Ù\87ارÛ\8c Ù\88Ù\87 Ù\87Û\8cÚ\86 Ø¨Ù\84Ú¯Ù\87 Ù\87Ù\86Û\8c Ø¯ {{SITENAME}} Ù\87Ù\88Ù\85 Ù¾Û\8cÙ\88Ù\86د Ù\86بÛ\8cÙ\86Ù\87.",
+       "deadendpagestext": "بٱÙ\84Ú¯Ù±Û\8cا Ù\87ارÛ\8c Ú¤ Ù\87Û\8cÚ\98 Ø¨Ù±Ù\84گاÙ\9b Ù\87Ù\86Û\8c Ø¯ {{SITENAME}} Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ù\86اÙ\9bئÛ\8cÙ\86Ù±.",
        "protectedpages": "بلگه یا حفاظت بيه",
        "protectedpages-indef": "فقط پر و پیم بیین یا بی زمون",
        "protectedpages-summary": "د ای بلگه نومگه بلگه یایی هیئن که د ایسنی پر و پیم بینه. سی نومگه سرونیا که نبوئه دروس بان، سیل[[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]]  بکیت.",
        "undeleterevisions": "$1 نسقه مال دیاری{{PLURAL:$1|بیه|بینه}}",
        "undeletehistory": "ار ای بلگه نه د نو زنه بکیت، همه نسقه یا وه د ویرگارچه ش د نو زنه بوئن.\nار بلگه تازه یی وا نوم هومبراوری د گات پاکسا بیین دروس بیه با، نسقه یا د نو زنه بیه د ویرگارچه ره وندیاری می کن.",
        "undeleterevdel": "ناپاکسا کردن بلگه یا د حال و باری که باعث پاکسا بیین بهرجایی د آخری نسقه بلگه یا جانیا با امکانش نئ.\nد ای حال و بار شما واس تازه تری نسقه پاکساگری بینه ئم د نو زنه بکیت.",
-       "undeletehistorynoadmin": "ای بلگه پاکسا بیه.\nدلیل پاکسا بیین ای بلگه واگرد مشخصات کاریاریایی که دما د پاکسا کردن ای بلگه نه ویرایشت دئنه ها د چکسته هاری.\nنیسسه راستیکی ای ویرایشت پاکسا بیه و فقط ها د دسرس دیوونداریا.",
+       "undeletehistorynoadmin": "اؽ بٱلگٱ پاکسا بیٱ.\nدلٛیلٛ پاکسا بیئن اؽ بٱلگٱ ڤاگرد موشٱخٱسؽا کاریارؽایؽ کاْ دما د پاکسا کردن اؽ بٱلگٱ ناْ ڤیرایش کردنٱ ها د چکسٱ هاری.\nنیسسٱ راسٱکی اؽ ڤیرایش پاکسا بیٱ ۉ فقٱت ها د دٱسرس دیڤوندارؽا.",
        "undelete-revision": "نسقه پاکسا بیه $1 (د ویرگار$4 ساعت $5) وه دس $3:",
        "undeleterevision-missing": "وانئری یا گم بیه یا نامعتوره.\nشایت هوم پیوند شما دروس نبوئه یا یه که ای وانئری د اماییه جا پاکسا بیه یا بازجست بیه.",
        "undelete-nodiff": "وانئری دماتری پیدا نبیه.",
        "lockfilenotwritable": "نبوئه قلف رسینه جا نه بنیسیت. سی یه بتونیت رسینه جا قلف بکیت یا قلفش وا بکیت، واس ای جانیا نیسسه یی بوئه.",
        "databasenotlocked": "رسینه گا وازه.",
        "lockedbyandtime": "(وا{{GENDER:$1|$1}} د $2 د$3)",
-       "move-page": "$1 جا وه جا کو",
-       "move-page-legend": "بÙ\84Ú¯Ù\87 Ù\86Ù\87 Ø¬Ø§ Ù\88Ù\87 جا کو",
+       "move-page": "$1 جا ڤ جا کو",
+       "move-page-legend": "بٱÙ\84Ú¯Ù± Ù\86اÙ\92 Ø¬Ø§ Ú¤ جا کو",
        "movepagetext": "وا وه کار گرتن نوم بلگه های نوم بلگه آلشت موئه، و همه ویرگارچه وه روئه وه نوم تازه ش.\nشما می تونیت آلشتکاری مسیریایی که وه داسون اصلی خوشو اشاره می کن نه وه هنگوم سازی بکیت.\nهوم پیوندیایی که چی بلگه دماترین، آلشتکاری نموئن؛ حتمن آلشت کاری مسیریا [[Special:DoubleRedirects|دوتایی]] یا [[Special:BrokenRedirects|خروا]] نه وارسی بکیت.\n'''شما''' مسئول یه دل بیین ده یه نیت که هوم پیوندیا هنی هان د هموچه که قراره روئن.\n\nد ویر داشوئیت که ار د دما یه گل بلگه د داسون تازه با بلگه\nجا وه جا '''نبوئه'''،\nمر یه آخری ویرایشت آلشتکاری مسیر با و د ویرگارچه ویرایشتی نبوئه.\nوه یئنی که ار اشتوا کردیته می تونیت بلگه نه د هموچه که جا وه جا بیه ورگردونیت و یه که نمی تونیت ری بلگه یا ایسنی بنیسیت. \n\n'''هشدار!'''\nجاوه جا کاری بلگه د نوم تازه شایت یه گل آلشتکاری پایه یی و ناحاستنی سی بلگه یا حاستنی با؛\nلطف بکیت یه دل بوئیت که دما د جا وه جا کاری بلگه، عاقوت ای کار نه دونیت.",
        "movepagetext-noredirectfixer": "وا وه کار گرتن نوم بلگه های نوم بلگه آلشت موئه، و همه ویرگارچه وه روئه وه نوم تازه ش.\nشما می تونیت آلشتکاری مسیریایی که وه داسون اصلی خوشو اشاره می کن نه وه هنگوم سازی بکیت.\nهوم پیوندیایی که چی بلگه دماترین، آلشتکاری نموئن؛ حتمن آلشت کاری مسیریا [[Special:DoubleRedirects|دوتایی]] یا [[Special:BrokenRedirects|خروا]] نه وارسی بکیت.\n'''شما''' مسئول یه دل بیین ده یه نیت که هوم پیوندیا هنی هان د هموچه که قراره روئن.\n\nد ویر داشوئیت که ار د دما یه گل بلگه د داسون تازه با بلگه\nجا وه جا '''نبوئه'''،\nمر یه آخری ویرایشت آلشتکاری مسیر با و د ویرگارچه ویرایشتی نبوئه.\nوه یئنی که ار اشتوا کردیته می تونیت بلگه نه د هموچه که جا وه جا بیه ورگردونیت و یه که نمی تونیت ری بلگه یا ایسنی بنیسیت. \n\n'''هشدار!'''\nجاوه جا کاری بلگه د نوم تازه شایت یه گل آلشتکاری پایه یی و ناحاستنی سی بلگه یا حاستنی با؛\nلطف بکیت یه دل بوئیت که دما د جا وه جا کاری بلگه، عاقوت ای کار نه دونیت.",
        "movepagetalktext": "بلگه چک چنه مربوطه، ار با، وه حال و بار خودانجوم واگرد گوتار اصلی جا وه جا کاری بوئه<strong>مر یه که:</strong>\n* شما د حال و بار جا وه جاکاری بلگه د ای نوم جا وه یه گل نوم جا هنی بوئیت.\n* یه گل بلگه چک چنه حال نبیه نه وا ای نوم با، یا \n* جعوه هاری نه نشودار نکردیته.\n\nد ای حال و باریا، واس بلگه نه دسی جا وه جاکاری بکیت یا مینونه یا دو بلگه نه وا ویرایشت یکی بکیت.",
        "cant-move-to-category-page": "شما صلا ینه که یه بلگه نه بوریت وه بلگه دسه ناریت.",
        "newtitle": "سی سرون هنی:",
        "move-watch": "دیئن بلگه سرچشمه و بلگه حاستنی",
-       "movepagebtn": "بÙ\84Ú¯Ù\87 Ø¬Ø§ Ù\88Ù\87 جا کو",
+       "movepagebtn": "بٱÙ\84Ú¯Ù± Ø¬Ø§ Ú¤ جا کو",
        "pagemovedsub": "د خوئی جا وه جا بیه",
        "movepage-moved": "<strong>\"$1\" جا وه جا بیه سی \"$2\"</strong>",
        "movepage-moved-redirect": "یه گل واگردونی دروس بیه.",
index 5f14b5b..9b65424 100644 (file)
@@ -5,7 +5,8 @@
                        "علی ساکی لرستانی",
                        "Mjbmr",
                        "Hosseinblue",
-                       "MtDu"
+                       "MtDu",
+                       "Shahriar dehghani"
                ]
        },
        "tog-underline": "لینکیا خط وه دومن",
        "logentry-move-move": "$1 {{GENDER:$2|انتقال دادھ بیه}} بلگه $3 ۉھ $4",
        "logentry-newusers-create": "حسآۉ کارڤأر $1 ڤابیە {{GENDER:$2|راس ڤیدھ }}",
        "logentry-upload-upload": "$1 {{GENDER:$2|بلم گیر کردھ ۉابی}} $3",
-       "searchsuggest-search": "جۉستأن"
+       "searchsuggest-search": "جۉستأن",
+       "userlogout-continue": "ایخیت برِیِتو وَدَر"
 }
index 263abd6..9c42c2a 100644 (file)
        "accmailtext": "Nejauši ģenerēta parole lietotājam [[User talk:$1|$1]] tika nosūtīta uz $2.\n\nŠī konta paroli pēc ielogošanās varēs nomainīt ''[[Special:ChangePassword|šeit]]''.",
        "newarticle": "(Jauns raksts)",
        "newarticletext": "Šajā projektā vēl nav lapas ar šādu nosaukumu.\nLai izveidotu lapu, sāc rakstīt teksta logā apakšā (par teksta formatēšanu un sīkākai informācija skatīt [$1 palīdzības lapu]).\nJa tu šeit nonāci kļūdas pēc, vienkārši uzspied <strong>back</strong> pogu pārlūkprogrammā.",
-       "anontalkpagetext": "----\n<em>Šī ir anonīma dalībnieka, kurš vēl nav izveidojis lietotāja kontu vai to nelieto, diskusiju lapa.</em>\nTādēļ mums ir jāizmanto IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt vairākiem dalībniekiem.\nJa tu esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:CreateAccount|izveido kontu]] vai [[Special:UserLogin|pieslēdzies]], lai izvairītos no turpmākām neskaidrībām un tu netiktu sajaukts ar citiem anonīmiem dalībniekiem.",
+       "anontalkpagetext": "----\n<em>Šī ir anonīma dalībnieka, kurš vēl nav izveidojis lietotāja kontu vai to nelieto, diskusiju lapa.</em>\nTādēļ mums ir jāizmanto IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt kopīga vairākiem dalībniekiem.\nJa esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:CreateAccount|izveido kontu]] vai [[Special:UserLogin|pieslēdzies]], lai izvairītos no turpmākām neskaidrībām un netiktu sajaukts ar citiem anonīmiem dalībniekiem.",
        "noarticletext": "Šajā lapā šobrīd nav nekāda teksta.\nTu vari [[Special:Search/{{PAGENAME}}|meklēt citās lapās pēc šīs lapas nosaukuma]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītos reģistru ierakstos]\nvai arī [{{fullurl:{{FULLPAGENAME}}|action=edit}} izveidot šo lapu]</span>.",
        "noarticletext-nopermission": "Šajā lapā pašlaik nav nekāda teksta.\nTu vari [[Special:Search/{{PAGENAME}}|meklēt šīs lapas nosaukumu]] citās lapās,\nvai <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītus reģistru ierakstus]</span>, bet jums nav atļauja izveidot šo lapu.",
        "userpage-userdoesnotexist": "Lietotājs \"<nowiki>$1</nowiki>\" nav reģistrēts.\nLūdzu, pārliecinies vai vēlies izveidot/izmainīt šo lapu.",
        "action-sendemail": "sūtīt e-pastus",
        "action-editmyoptions": "labot savas izvēles",
        "action-deletechangetags": "dzēst iezīmes no datubāzes",
+       "action-unblockself": "atbloķēt sevi",
        "nchanges": "$1 {{PLURAL:$1|izmaiņas|izmaiņa|izmaiņas}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|kopš pēdējā apmeklējuma}}",
        "enhancedrc-history": "vēsture",
        "dellogpage": "Dzēšanas reģistrs",
        "dellogpagetext": "Šajā lapā ir pēdējo dzēsto lapu saraksts.",
        "deletionlog": "dzēšanas reģistrs",
+       "log-name-create": "Lapu izveides žurnāls",
        "reverted": "Atjaunots uz iepriekšējo versiju",
        "deletecomment": "Iemesls:",
        "deleteotherreason": "Cits/papildu iemesls:",
index f23a157..e4b3eb3 100644 (file)
        "group-autoconfirmed-member": "自證其簿",
        "group-bot-member": "僕",
        "group-sysop-member": "有秩",
-       "group-interface-admin-member": "司空",
+       "group-interface-admin-member": "{{GENDER:$1|司空}}",
        "group-bureaucrat-member": "門下",
        "group-suppress-member": "監",
        "grouppage-user": "{{ns:project}}:簿",
index 9ce3707..640d2ae 100644 (file)
        "navigation-heading": "दिक्चालन सूची",
        "errorpagetitle": "त्रुटि",
        "returnto": "$1 पर आबी।",
-       "tagline": "मैथिली {{SITENAME}}सँ",
+       "tagline": "मैथिली {{SITENAME}} सँ",
        "help": "मदति",
        "search": "ताकी",
        "searchbutton": "ताकी",
        "userlogout": "फेर आयब",
        "notloggedin": "सम्प्रवेशित नै छी",
        "userlogin-noaccount": "खाता नै अछि?",
-       "userlogin-joinproject": "{{SITENAME}}सँ जुडी",
+       "userlogin-joinproject": "{{SITENAME}} सँ जुड़ी",
        "createaccount": "खाता खोली",
        "userlogin-resetpassword-link": "अपन कूटशब्द बिसरि गेलौ?",
        "userlogin-helplink2": "सम्प्रवेशित करवाक लेल मदति",
        "resetpass-expired": "अहाँके कूटशब्दक वैधता अवधि खत्तम भऽ गेल अछि । कृपया सम्प्रवेशित करवाक लेल नयाँ कूटशब्द राखु।",
        "resetpass-expired-soft": "अहाँक कूटशब्द कऽ वैधता अवधि समाप्त भऽ गेल आर कूटशब्द परिवार्तन करवाक आवश्यकता अछि। कृपया एगो नव कूटशब्द राखी, वा पाछा रिसेट करवाक लेल \"{{int:authprovider-resetpass-skip-label}}\" क्लिक करी।",
        "resetpass-validity-soft": "अहाँके कूटशब्द मान्य नै अछि: $1 \n\nकृपया आब एगो नव कूटशब्द चुनी, वा पाछ पुनर्स्थापित करएक लेल \"{{int:authprovider-resetpass-skip-label}}\" क्लिक करी।",
-       "passwordreset": "कूटशब्द फेरसँ बनाबी",
+       "passwordreset": "कूटशब्द फेर सँ बनाबी",
        "passwordreset-text-one": "अपन कूटशब्द रीसेट करवाक लेल इ फारम भरी।",
        "passwordreset-text-many": "{{PLURAL:$1|ई-पत्रके माध्यमसऽ एकटा अस्थायी कूटशब्द पावैलेल कोनो एकटा डिब्बा भरी।}}",
        "passwordreset-disabled": "कूटशब्द फेरसँ बनाएब ऐ विकीपर अक्षम कएल अछि।",
        "showpreview": "पूर्वप्रदर्शन",
        "showdiff": "परिवर्तन देखाबी",
        "blankarticle": "<strong>चेतावनी:</strong> अहाँ एक रिक्त पन्ना के निर्माण करि रहल छी।\nयदि अहाँ \"$1\" क पुनः दाबबै त पन्नाक बिना कोनो सामग्रीक निर्मित भ जाएत।",
-       "anoneditwarning": "<strong>चेतौनी:</strong> अहाँ सम्प्रवेश नै केनए छी । यदि अहाँ सम्पादन करबै तहन ई पृष्ठक सम्पादन इतिहासमे अहाँक आइपी ठेगान दर्ज कएल जाएत। यदि अहाँ <strong>[$1 सम्प्रवेश]</strong> करैत छी अथवा <strong>[$2 खाता बनाबैत छी]</strong> तहन अन्य सुविधासभ संगे अहाँक सम्पादनसभक श्रेय अहाँक प्रयोगकर्तानाम पर दएल जाएत।",
+       "anoneditwarning": "<strong>चेतौनी:</strong> अहाँ सम्प्रवेश नै केनए छी । यदि अहाँ सम्पादन करब तहन ई पृष्ठक सम्पादन इतिहासमे अहाँक आइपी ठेगान दर्ज कएल जाएत। यदि अहाँ <strong>[$1 सम्प्रवेश]</strong> करैत छी अथवा <strong>[$2 खाता बनबैत छी]</strong> तहन अन्य सुविधासभ सङ्गे अहाँक सम्पादनसभक श्रेय अहाँक प्रयोगकर्तानाम पर देल जाएत।",
        "anonpreviewwarning": "<em>अहाँ सम्प्रवेशित नै छी। अखन रक्षण केलासँ अहाँक अनिकेत पता ई पन्नाक सम्पादन इतिहासमे दर्ज भऽ जाएत।</em>",
        "missingsummary": "<strong>स्मारक:</strong> अहाँ सम्पादन सार नै देने छी।\nजँ अहाँ फेरसँ क्लिक करब \"$1\", अहाँक सम्पादन बिना एकर संरक्षित भऽ जाएत।",
        "selfredirect": "<strong>चेतावनी:</strong> आहाँ स्वेम के ई पन्ना पुनः निर्देशीत कएर रहल छी।\nआहाँ अनुप्रेषित के लेल गलत लक्ष्य निर्दिष्ट भ्या सकएत अछि, या आहाँ गलत पन्ना कें संपादन कैर सकएत छी।\nआहाँ फेरो से \"$1\" क्लिक करएत छी, रीडायरेक्ट ओनाहो भी बनाबल जेल अछि।",
        "permissionserrors": "आज्ञा गल्ती",
        "permissionserrorstext": "अहाँके ऐ लेल अनुमति नै अछि, ऐ ले {{PLURAL:$1|कारण|कारणसभ}}:",
        "permissionserrorstext-withaction": "अहाँक अनुमति नै अछि $2 लेल, एकर लेल {{PLURAL:$1|कारण|कारणसभ}}सँ:",
-       "recreate-moveddeleted-warn": "'''चेतौनी''': अहाँ फेरसँ ओ पन्ना बना रहल छी जे पहिने मेटा देल गेल छै।'''\n\nअहाँ विचारू जे की ई सम्पादन केनाइ उचित अछि।\nऐ पन्नाक मेटाएल बला आ हटाएल वृत्तलेख एतए सुविधा लेल देल जा रहल अछि:",
+       "recreate-moveddeleted-warn": "<strong>चेतौनी: अहाँ फेर सँ ओ पन्ना बना रहल छी जे पहिने मेटा देल गेल छै।<strong>\n\nअहाँ विचारू जे की ई सम्पादन केनाए उचित अछि।\nई पन्नाक मेटाएल आ हटाएल वृत्तलेख एतय सुविधाक लेल देल जा रहल अछि:",
        "moveddeleted-notice": "ई पन्ना मेटाएल गेल अछि।\nई पन्ना लेल मेटाएल आ स्थानान्तरणक लग सन्दर्भ लेल नीचाँ देल गेल अछि।",
        "log-fulllog": "सम्पूर्ण लौग देखी",
        "edit-hook-aborted": "सम्पादन नोकसीसँ खतम भेल।\nई कोनो कारण नै देलक।",
        "viewpagelogs": "ई पन्नाक लग देखी",
        "nohistory": "ऐ पन्ना लेल कोनो सम्पादन इतिहास नै अछि।",
        "currentrev": "नूतन संशोधन",
-       "currentrev-asof": "$1 क समकालिक तखुनका संशोधन",
-       "revisionasof": "à¤\85नà¥\8dतिम à¤ªà¤°à¤¿à¤µà¤°à¥\8dतà¥\8dतन  $1",
+       "currentrev-asof": "$1 कऽ समकालिक अवतरण",
+       "revisionasof": "अन्तिम परिवर्तन  $1",
        "revision-info": "$2 द्वारा कएल संशोधन अछि $1",
        "previousrevision": "←पुरान परिवर्तन",
        "nextrevision": "नूतन संशोधन →",
        "page_first": "पहिल",
        "page_last": "अन्तिम",
        "histlegend": "फाइल तुलना तंत्रांशक चयन: संशोधन तुलनाक रेडियो बक्शाकेँ चिन्हित करू आ एन्टर बटन क्लिक करू वा सभसँ नीचाँक बटन क्लिक करू। <br />\nकहबी: '''({{int:cur}})''' = अद्यतन संशोधनसँ अन्तर, '''({{int:last}})''' = अद्यतनसँ पहिलुका संशोधनसँ अन्तर, '''{{int:minoreditletter}}''' = मामूली सम्पादन।",
-       "history-fieldset-title": "à¤\87तिहास à¤µà¤¿à¤\9aरण à¤\95री",
+       "history-fieldset-title": "à¤\85वतरण à¤\96à¥\8bà¤\9cी",
        "history-show-deleted": "खाली मेटाएल",
        "histfirst": "सभसँ पुरान",
        "histlast": "आइ-काल्हिक",
        "difference-title": "\"$1\" के अवतरणसभमे अन्तर",
        "difference-title-multipage": "\"$1\" आर \"$2\" पृष्ठसभ मे अंतर",
        "difference-multipage": "(पन्ना सभक बीचमे अन्तर)",
-       "lineno": "पà¤\82क्त्ति $1:",
+       "lineno": "पà¤\99à¥\8dक्त्ति $1:",
        "compareselectedversions": "चयन कएल संशोधन सभक तुलना करी",
        "showhideselectedversions": "चयनित अवतरण देखाबी/नुकाबी",
        "editundo": "असम्पादन",
        "diff-empty": "(कोनो अंतर नै)",
        "diff-multi-sameuser": "(इ प्रयोक्ताद्वारा {{PLURAL:$1|कएल गेल बीचके एक अवतरण नै देखाओल गेल |कएल गेल बीचके $1 अवतरण नै देखाओल गेल}})",
-       "diff-multi-otherusers": "({{PLURAL:$1|एकटा मध्यस्थ संशोधन|$1 मध्यस्थ संशोधन सभ}} $2 सँ बेसी {{PLURAL:$2|प्रयोक्ता|प्रयोक्ता सभ}} नै देखाएल)",
+       "diff-multi-otherusers": "({{PLURAL:$1|एकटा मध्यस्थ संशोधन|$1 मध्यस्थ संशोधन सभ}} $2 सँ बेसी {{PLURAL:$2|प्रयोक्ता|प्रयोक्तासभ}} नै देखाएल)",
        "diff-multi-manyusers": "({{PLURAL:$1|एकटा मध्यस्थ संशोधन|$1 मध्यस्थ संशोधन सभ}} $2 सँ बेसी {{PLURAL:$2|प्रयोक्ता|प्रयोक्ता सभ}} नै देखाएल)",
        "difference-missing-revision": "इ अंतर {{PLURAL:$2|के एकटा अवतरण|के $2 अवतरण}} ($1) नै {{PLURAL:$2|पाओल गेल|पाओल गेल}}।\n\nइ सामन्य ढंगमे हटाओल गेल पृष्ठके अवतरसभ मे अंतर खोजला स होएत अछि । आर जानकारी [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटाओल लग] मे भेट सकैत अछि।",
        "searchresults": "तकबाक फलाफल",
        "uploadlogpage": "उपारोपण लौग",
        "uploadlogpagetext": "नीचाँ अद्यतन सञ्चिका उपारोपणक वर्णन अछि।\nदेखी [[Special:NewFiles|नव सञ्चिकाक बखारी]] बेसी स्पष्ट समुच्चा दृश्य लेल।",
        "filename": "सञ्चिका नाम",
-       "filedesc": "सà¤\82क्षेप",
+       "filedesc": "सà¤\99à¥\8dक्षेप",
        "fileuploadsummary": "संक्षेप:",
        "filereuploadsummary": "सञ्चिका परिवर्तन:",
        "filestatus": "सर्वाधिकारक स्थिति:",
        "listfiles-latestversion-no": "नै",
        "file-anchor-link": "सञ्चिका",
        "filehist": "फाइल इतिहास",
-       "filehist-help": "तà¤\96à¥\81नà¤\95ा à¤¤à¤¿à¤¥à¤¿/ à¤¸à¤®à¤\8f à¤ªà¤° à¤\95à¥\8dलिà¤\95 à¤\95रà¥\80 à¤\9cà¤\96à¥\81नका फाइल देखबाक अछि",
+       "filehist-help": "तà¤\96नà¤\95ा à¤¤à¤¿à¤¥à¤¿/ à¤¸à¤®à¤\8f à¤ªà¤° à¤\95à¥\8dलिà¤\95 à¤\95रà¥\80 à¤\9cà¤\96नका फाइल देखबाक अछि",
        "filehist-deleteall": "सभटाकेँ मेटाउ",
        "filehist-deleteone": "मेटाउ",
        "filehist-revert": "फेरसँ वएह",
-       "filehist-current": "à¤\85à¤\96à¥\81नà¤\95ा",
+       "filehist-current": "अखनका",
        "filehist-datetime": "तिथि/ समए",
        "filehist-thumb": "लघुचित्र",
        "filehist-thumbtext": "तखुनका लघुचित्र $1",
        "imagelinks": "फाइलक उपयोग",
        "linkstoimage": "ई {{PLURAL:$1|पृष्ठ|$1 पन्नासभ}}मे ई फाइलक लिङ्क अछि:",
        "linkstoimage-more": "$1 सँ बेसी {{PLURAL:$1|page links|पन्ना सभक लागि}} ऐ संचिकाक।\nई सूची देखबैए {{PLURAL:$1|first page link|first $1 page links}} मात्र ऐ संचिकाक।\nएकटा [[Special:WhatLinksHere/$2|पूर्ण सूची]] उपलब्ध अछि।",
-       "nolinkstoimage": "एकोटा पन्ना नै अछि जे ई सञ्चिका सँ जुडल होए।",
+       "nolinkstoimage": "à¤\8fà¤\95à¥\8bà¤\9fा à¤ªà¤¨à¥\8dना à¤¨à¥\88 à¤\85à¤\9bि à¤\9cà¥\87 à¤\88 à¤¸à¤\9eà¥\8dà¤\9aिà¤\95ा à¤¸à¤\81 à¤\9cà¥\81ड़ल à¤¹à¥\8bà¤\8f।",
        "morelinkstoimage": "देखू [[Special:WhatLinksHere/$1|आर लागि]] ऐ संचिकाक।",
        "linkstoimage-redirect": "$1 (संचिका घुमौआ) $2",
        "duplicatesoffile": "ऐ संचिकाक {{PLURAL:$1|file is a duplicate|$1 संचिका सभ द्वितीयक अछि}} अछि ([[Special:FileDuplicateSearch/$2|आर वर्णन]]):",
        "sharedupload-desc-here": "ई सञ्चिका $1सँ अछि आ ई दोसर परियोजनाद्वारा प्रयोग कएल जा सकैए।\nएतए रहल विवरण [$2 सञ्चिका विवरण पन्ना] ओइपर नीचाँ देखाएल अछि।",
        "sharedupload-desc-edit": "ई फ़ाइल $1 से छी आर अन्य परियोजना द्वारा सेहो प्रयोग भ्या रहल अछि\nशायद आहाँ [$2 पे एकर फ़ाइल विवरण पन्ना] के सम्पादन करइल चाहए छी।",
        "sharedupload-desc-create": "ई फ़ाइल $1 से अछि आर अन्य परियोजनासभ द्वारा से प्रयोग भऽ रहल अछि\nशायद आहाँ [$2 पे एकर फ़ाइल विवरण पन्ना] के सम्पादन करइल चाहए छी ।",
-       "filepage-nofile": "à¤\90 à¤¨à¤¾à¤®à¤\95 à¤\95à¥\8bनà¥\8b à¤¸à¤\82चिका उपलब्ध नै अछि।",
+       "filepage-nofile": "à¤\88 à¤¨à¤¾à¤®à¤\95 à¤\95à¥\8bनà¥\8b à¤¸à¤\9eà¥\8dचिका उपलब्ध नै अछि।",
        "filepage-nofile-link": "ऐ नामक कोनो संचिका उपलब्ध नै अछि मुदा अहाँ [$1 एकरा उपारोपित करू]।",
        "uploadnewversion-linktext": "ऐ फाइलक नव संस्करणक उपारोपण",
        "shared-repo-from": "$1 सँ",
        "mostimages": "सभसँ बेसी लागिबला सञ्चिकासभ",
        "mostinterwikis": "सर्वाधिक अन्तरविकी जडीभेल पृष्ठसभ",
        "mostrevisions": "सभसँ बेसी संशोधनबला पन्ना",
-       "prefixindex": "à¤\89पसरà¥\8dà¤\97à¤\95 à¤¸à¤\82ग सभटा पृष्ठ",
+       "prefixindex": "à¤\89पसरà¥\8dà¤\97à¤\95 à¤¸à¤\99à¥\8dग सभटा पृष्ठ",
        "prefixindex-namespace": "उपसर्ग भएल सभ पृष्ठ ($1 नामस्थान)",
        "prefixindex-submit": "देखाबी",
        "prefixindex-strip": "नतिजामे उपसर्ग नुकाबी",
        "removedwatchtext-short": "इ पृष्ठ \"$1\" अहाँ के साकांक्ष सूची मे राखल गेल अछि।",
        "watch": "ध्यान राखी",
        "watchthispage": "ऐ पृष्ठपर ध्यान राखू",
-       "unwatch": "à¤\9bà¥\8bडी",
+       "unwatch": "धà¥\8dयान à¤¹à¤\9fाबी",
        "unwatchthispage": "देखनाइ छोडी",
        "notanarticle": "कोनो विषय सूची नै",
        "notvisiblerev": "कोनो दोसर प्रयोक्ता द्वारा कएल अन्तिम परिवर्तन मेटा देल गेल",
        "minimum-size": "न्यून आकार",
        "maximum-size": "अधिक आकार:",
        "pagesize": "(अष्टक)",
-       "restriction-edit": "सà¤\82पादन",
+       "restriction-edit": "समà¥\8dपादन",
        "restriction-move": "स्थानान्तरण",
        "restriction-create": "बनाउ",
        "restriction-upload": "उपारोपण",
        "contribsub2": "{{GENDER:$3|$1}} ($2)क लेल",
        "contributions-userdoesnotexist": "प्रयोक्ता खाता \"$1\" पंजीकृत नै अछि।",
        "nocontribs": "कोनो परिवर्तन ऐ सँ मेल नै खाइए।",
-       "uctop": "शिà¤\96र",
-       "month": "माससँ (आ पहिने)",
+       "uctop": "वरà¥\8dतमान",
+       "month": "मास सँ (आ पहिने)",
        "year": "ई साल (आ पहिने)",
        "date": "माससँ (आ पहिने)",
        "sp-contributions-newbies": "मात्र नव खाताक योगदान देखाबी",
        "sp-contributions-deleted": "{{GENDER:$1|प्रयोगकर्ता}}क मेटाएल योगदान",
        "sp-contributions-uploads": "उपारोपण",
        "sp-contributions-logs": "लौग",
-       "sp-contributions-talk": "वारà¥\8dतà¥\8dता",
+       "sp-contributions-talk": "वार्ता",
        "sp-contributions-userrights": "{{GENDER:$1|user}}प्रयोक्ता अधिकारकऽ प्रबन्धन",
        "sp-contributions-blocked-notice": "ई प्रयोक्ता अखन प्रतिबन्धित अछि।\nनव प्रतिबन्धित वृत्तलेख लेख सन्दर्भ नीचाँ देल अछि:",
        "sp-contributions-blocked-notice-anon": "ई अनिकेत अखन प्रतिबन्धित अछि।\nअद्यतन प्रतिबन्धित  वृत्तलेख लेखा सन्दर्भ नीचाँ देल अछि:",
-       "sp-contributions-search": "à¤\85वदानà¤\95 à¤²à¥\87ल à¤¤à¤¾à¤\95à¥\82",
-       "sp-contributions-username": "à¤\85निà¤\95à¥\87त à¤¸à¤\82केत वा प्रयोक्तानाम:",
+       "sp-contributions-search": "यà¥\8bà¤\97दानà¤\95 à¤²à¥\87ल à¤¤à¤¾à¤\95à¥\80",
+       "sp-contributions-username": "à¤\85निà¤\95à¥\87त à¤¸à¤\99à¥\8dकेत वा प्रयोक्तानाम:",
        "sp-contributions-toponly": "मात्र ओ सम्पादन देखाबी जे नवीनतम संशोधन छी।",
-       "sp-contributions-newonly": "मात्र ओ सम्पादन देखाबी जहिसँ पृष्ठ निर्मित भेल अछि",
+       "sp-contributions-newonly": "मात्र ओ सम्पादन देखाबी जहि सँ पृष्ठ निर्मित भेल अछि",
        "sp-contributions-hideminor": "अल्प सम्पादन नुकाबी",
        "sp-contributions-submit": "ताकू",
        "whatlinkshere": "एतय कोन लिङ्क अछि",
        "tooltip-pt-watchlist": "पन्नासभ जेकर परिवर्तन पर अहाँक नजरि अछि",
        "tooltip-pt-mycontris": "{{GENDER:|अहाँक}} योगदानक सूची",
        "tooltip-pt-anoncontribs": "ई आइपी पता सँ सम्पादनक सूची",
-       "tooltip-pt-login": "à¤\85हाà¤\81à¤\95 à¤\96ाता à¤\96à¥\8bलà¤\95 à¤²à¥\87ल à¤ªà¥\8dरà¥\8bतà¥\8dसाहित à¤\95à¤\8fल à¤\9cाà¤\87त अछि; मुदा ई अनिवार्य नै अछि",
+       "tooltip-pt-login": "à¤\85हाà¤\81à¤\95 à¤\96ाता à¤\96à¥\8bलà¤\95 à¤²à¥\87ल à¤ªà¥\8dरà¥\8bतà¥\8dसाहित à¤\95à¤\8fल à¤\9cाà¤\8fत अछि; मुदा ई अनिवार्य नै अछि",
        "tooltip-pt-logout": "फेर आयब",
-       "tooltip-pt-createaccount": "à¤\85हाà¤\81à¤\95 à¤\96ाता à¤\96à¥\8bलà¤\95 à¤²à¥\87ल à¤ªà¥\8dरà¥\8bतà¥\8dसाहित à¤\95à¤\8fल à¤\9cाà¤\87त अछि; मुदा ई अनिवार्य नै अछि",
-       "tooltip-ca-talk": "विषयसà¥\82à¤\9aà¥\80à¤\95 à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¤®à¥\8dबनà¥\8dधमà¥\87 à¤µà¤°à¥\8dत्तालाप",
+       "tooltip-pt-createaccount": "à¤\85हाà¤\81à¤\95 à¤\96ाता à¤\96à¥\8bलà¤\95 à¤²à¥\87ल à¤ªà¥\8dरà¥\8bतà¥\8dसाहित à¤\95à¤\8fल à¤\9cाà¤\8fत अछि; मुदा ई अनिवार्य नै अछि",
+       "tooltip-ca-talk": "विषयसà¥\82à¤\9aà¥\80à¤\95 à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¤®à¥\8dबनà¥\8dधमà¥\87 à¤µà¤¾à¤°्तालाप",
        "tooltip-ca-edit": "ई पन्नाक सम्पादित करी",
        "tooltip-ca-addsection": "नव खण्ड शुरू करी",
        "tooltip-ca-viewsource": "ई पन्ना संरक्षित अछि ।\nअहाँ एकर स्रोत देख सकै छी ।",
        "tooltip-ca-delete": "ऐ पन्नाकेँ मेटाउ",
        "tooltip-ca-undelete": "ई पन्ना मेटेबासँ पहिने भेल सम्पादन वापस करू",
        "tooltip-ca-move": "ई पृष्ठ स्थानानतरित करी",
-       "tooltip-ca-watch": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 अपन साकांक्षसूचीमे राखी",
+       "tooltip-ca-watch": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\82 अपन साकांक्षसूचीमे राखी",
        "tooltip-ca-unwatch": "ऐ पन्नाकेँ हमर साकांक्ष सूचीसँ हटाउ",
        "tooltip-search": "{{SITENAME}}मे ताकी",
        "tooltip-search-go": "पृष्ठपर पहुँची जौं एनमेन पृष्ठ रहए",
        "tooltip-p-logo": "सम्मुख पन्ना देखी",
        "tooltip-n-mainpage": "मुख्य पृष्ठ देखी",
        "tooltip-n-mainpage-description": "मुख्य पन्नापर जाए",
-       "tooltip-n-portal": "परियोजनाक विषयमे,अहाँ की कए सकैत छी, वस्तु प्राप्ति स्थल",
+       "tooltip-n-portal": "परियोजनाक विषयमे, अहाँ की कए सकैत छी, वस्तु प्राप्ति स्थल",
        "tooltip-n-currentevents": "लगक घटनाक विषयमे आधार सूचना प्राप्त करी।",
        "tooltip-n-recentchanges": "विकिमे लगक परिवर्तनक सूची",
        "tooltip-n-randompage": "कोनो अनिर्धारित पन्ना लोड करी",
-       "tooltip-n-help": "पता à¤²à¤\97ावà¥\88वाला à¤¸à¥\8dथान",
+       "tooltip-n-help": "पता लगवैवाला स्थान",
        "tooltip-t-whatlinkshere": "सभ विकी-पन्नाक सूची जकर एतय लिङ्क अछि",
        "tooltip-t-recentchangeslinked": "ई पृष्ठक लगक पन्नामे भेल नव परिवर्तनसभ",
        "tooltip-feed-rss": "ऐ पन्ना लेल आर.एस.एस. सूचना",
        "tooltip-ca-nstab-template": "नमूना देखी",
        "tooltip-ca-nstab-help": "सहायता पृष्ठ देखी",
        "tooltip-ca-nstab-category": "श्रेणी पन्ना देखी",
-       "tooltip-minoredit": "à¤\8fà¤\95रा à¤®à¤¾à¤®à¤²à¥\80 à¤¸à¤®à¥\8dपादन à¤\9aिनà¥\8dहित à¤\95रà¥\82",
+       "tooltip-minoredit": "à¤\8fà¤\95रा à¤\9bà¥\8bà¤\9f à¤¸à¤®à¥\8dपादन à¤\9aिनà¥\8dहित à¤\95रà¥\80",
        "tooltip-save": "अपन परिवर्तन सुरक्षित करी",
        "tooltip-publish": "परिवर्तन प्रकाशित करी",
        "tooltip-preview": "परिवर्तनक प्रदर्शन, संरक्षण सँ पहिने एकर प्रयोग करी!",
        "tooltip-diff": "ई पाठमे अहाँद्वारा कएल परिवर्तन देखी।",
        "tooltip-compareselectedversions": "ऐ पन्नाक दू टा चयन कएल संशोधनक बीचक अन्तर देखू",
-       "tooltip-watch": "à¤\90 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 à¤\85पन à¤¸à¤¾à¤\95ाà¤\82à¤\95à¥\8dष à¤¸à¥\82à¤\9aà¥\80मà¥\87 à¤\9cà¥\8bड़à¥\82",
+       "tooltip-watch": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\82 à¤\85पन à¤¸à¤¾à¤\95ाà¤\82à¤\95à¥\8dष à¤¸à¥\82à¤\9aà¥\80मà¥\87 à¤\9cà¥\8bड़à¥\80",
        "tooltip-watchlistedit-normal-submit": "शीर्षक सभकेँ हटाउ",
        "tooltip-watchlistedit-raw-submit": "साकांक्षसूची उद्दतन करू",
        "tooltip-recreate": "पन्ना फेरसँ बनाउ तखनो जँ ई मेटा देल गेल हुअए",
        "tooltip-rollback": "\"प्रत्यावर्तन\" ई पन्नाक अन्तिम योगदान करैबलाक सम्पादन (सम्पादनसभ)क एक क्लिकमे पुरान जगहपर लऽ जाए।",
        "tooltip-undo": "\"फेरसँ वएह\" सम्पादनकेँ पूर्वस्थितिमे लऽ जाइए आ पूर्वावलोकन अवस्थामे सम्पादन फॉर्म खोलैए। ई सारांशमे कारण जोड़बाक विकल्प दैत अछि।",
        "tooltip-preferences-save": "मोनपसंद के सुरक्षित करू",
-       "tooltip-summary": "à¤\9bà¥\8bà¤\9f à¤¸à¤\82à¤\95à¥\8dषà¥\87प à¤¦à¤¿à¤\85",
+       "tooltip-summary": "à¤\9bà¥\8bà¤\9f à¤¸à¤\99à¥\8dà¤\95à¥\8dषà¥\87प à¤¦à¤°à¥\8dà¤\9c à¤\95रà¥\80",
        "anonymous": "{{SITENAME}}क अज्ञात {{PLURAL:$1|प्रयोक्ता|प्रयोक्तासभ}}",
        "siteuser": "{{SITENAME}} प्रयोक्ता $1",
        "anonuser": "{{SITENAME}} नुकायल प्रयोक्ता $1",
        "pageinfo-title": "\"$1\"पृष्ठक लेल नब गप",
        "pageinfo-not-current": "माफ करु, पुरान संशोधन के लेल ई जानकारी प्रदान करनाए संभव नै अछि ।",
        "pageinfo-header-basic": "न्यूनतम जानकारी",
-       "pageinfo-header-edits": "सà¤\82पादन",
+       "pageinfo-header-edits": "समà¥\8dपादन à¤\87तिहास",
        "pageinfo-header-restrictions": "पन्ना संरक्षण",
        "pageinfo-header-properties": "पन्ना जानकारी",
        "pageinfo-display-title": "प्रदर्शन शिर्षक",
-       "pageinfo-default-sort": "डिफलà¥\8dà¤\9f à¤¸à¤°à¥\8dà¤\9f à¤\95à¥\81à¤\82जी",
+       "pageinfo-default-sort": "डिफलà¥\8dà¤\9f à¤¸à¤°à¥\8dà¤\9f à¤\95à¥\81à¤\9eà¥\8dजी",
        "pageinfo-length": "पन्ना आकार (बाइट्स में)",
        "pageinfo-namespace": "नामस्थान",
-       "pageinfo-article-id": "पनà¥\8dना à¤\86à¤\88॰डà¥\80॰",
+       "pageinfo-article-id": "पà¥\83षà¥\8dठ à¤\86à¤\87डà¥\80",
        "pageinfo-language": "पन्ना सामग्री भाषा",
        "pageinfo-language-change": "परिवर्तन",
-       "pageinfo-content-model": "पन्ना सामग्री के नमूना",
+       "pageinfo-content-model": "पन्ना सामग्रीकें नमूना",
        "pageinfo-content-model-change": "परिवर्तन",
        "pageinfo-robot-policy": "बोटद्वारा अनुक्रमण",
        "pageinfo-robot-index": "मान्य",
        "pageinfo-robot-noindex": "अमान्य",
        "pageinfo-watchers": "जानकारक संख्या",
        "pageinfo-visiting-watchers": "पृष्ठ देखनिहारक सङ्ख्या जे हालक सम्पादनमे आबए।",
-       "pageinfo-few-watchers": "$1 स कम ध्यान दीए {{PLURAL:$1|वाला}}",
+       "pageinfo-few-watchers": "$1 सँ कम ध्यान देबऽ  {{PLURAL:$1|वाला|वालासभ}}",
        "pageinfo-few-visiting-watchers": "भ सकैत अछि या नै भी कि कियो ई हाल क सम्पादनद्वारा कोनो प्रयोक्ता आएल होए।",
        "pageinfo-redirects-name": "ई पन्नाक पुनर्निर्देशसभ सङ्ख्या",
        "pageinfo-subpages-name": "इ पन्ना के उप-पन्ना",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|पुनर्निर्देश}}; $3 {{PLURAL:$3|ग़ैर-पुनर्निर्देश}})",
-       "pageinfo-firstuser": "पनà¥\8dना à¤¸à¤°à¥\8dà¤\9cà¤\95",
+       "pageinfo-firstuser": "पà¥\83षà¥\8dठ à¤¨à¤¿à¤°à¥\8dमाता",
        "pageinfo-firsttime": "पृष्ठ निर्माण तिथि",
        "pageinfo-lastuser": "अन्तिम सम्पादक",
        "pageinfo-lasttime": "नवीनतम सम्पादन तिथि",
-       "pageinfo-edits": "समà¥\8dपादनà¤\95 à¤¸à¤\82ख्या",
-       "pageinfo-authors": "भिनà¥\8dन à¤²à¥\87à¤\96à¤\95 à¤¸à¤\82ख्या",
-       "pageinfo-recent-edits": "लगक सम्पादन सभ के संख्या (पिछुल्का $1 में)",
-       "pageinfo-recent-authors": "लग में लेखक सभ के संख्या",
+       "pageinfo-edits": "समà¥\8dपादनà¤\95 à¤\95à¥\82ल à¤¸à¤\99à¥\8dख्या",
+       "pageinfo-authors": "भिनà¥\8dन à¤²à¥\87à¤\96à¤\95 à¤¸à¤\99à¥\8dख्या",
+       "pageinfo-recent-edits": "लगक सम्पादन सभकें सङ्ख्या (पिछुल्का $1 मे)",
+       "pageinfo-recent-authors": "लगमे लेखकसभक सङ्ख्या",
        "pageinfo-magic-words": "जादु {{PLURAL:$1|शब्द|शब्द सभ}} ($1)",
        "pageinfo-hidden-categories": "नुकाएल {{PLURAL:$1|संवर्ग|संवर्ग सभ}} ($1)",
-       "pageinfo-templates": "प्रयुक्त {{PLURAL:$1|आकृति|आकृति सभ}} ($1)",
+       "pageinfo-templates": "प्रयुक्त {{PLURAL:$1|आकृति|आकृतिसभ}} ($1)",
        "pageinfo-transclusions": "$1 {{PLURAL:$1|पन्ना|पन्ना}} पर ट्रान्सक्ल्युडेड",
-       "pageinfo-toolboxlink": "पनà¥\8dना जानकारी",
+       "pageinfo-toolboxlink": "à¤\88 à¤ªà¤¨à¥\8dना à¤ªà¤° जानकारी",
        "pageinfo-redirectsto": "मे पुनर्निर्देश:",
        "pageinfo-redirectsto-info": "जानकारी",
-       "pageinfo-contentpage": "सामग्री पृष्ठ सभ में गिनल जाएत अछि",
+       "pageinfo-contentpage": "सामग्री पृष्ठ सभमे गिनल जाएत अछि",
        "pageinfo-contentpage-yes": "हँ",
        "pageinfo-protect-cascading": "सुरक्षा-विकल्प यहाँ से व्यापक भऽ रहल अछि",
        "pageinfo-protect-cascading-yes": "हँ",
        "redirect": "फाइल, सदस्य, पृष्ठ, अवतरण या लग आइडीद्वारा अनुप्रेषित",
        "redirect-summary": "ई विशेष पन्ना फाइलनाम प्रदान करै पर फाइल नामके, पन्न आइडी अथवा अवतरण आइडी दुनु पर पन्नाके, आर साथी सदस्य आइडी दुनु पर सदस्य पन्नाके पुनर्प्रेषित करैत अछि । उदाहरण: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], या [[{{#Special:Redirect}}/user/101]]।",
        "redirect-submit": "जाए",
-       "redirect-lookup": "ताà¤\95à¥\82:",
+       "redirect-lookup": "ताà¤\95à¥\80:",
        "redirect-value": "मूल्य:",
-       "redirect-user": "पà¥\8dरयà¥\8bà¤\95à¥\8dता à¤\86à¤\88॰डà¥\80॰",
-       "redirect-page": "पनà¥\8dना à¤\86à¤\88॰डà¥\80॰",
+       "redirect-user": "पà¥\8dरयà¥\8bà¤\95à¥\8dता à¤\86à¤\88डà¥\80",
+       "redirect-page": "पनà¥\8dना à¤\86à¤\88डà¥\80",
        "redirect-revision": "पन्ना अवतरण संख्या",
        "redirect-file": "फाइल नाम",
        "redirect-logid": "प्रवेश आइडी",
        "logentry-newusers-create": "प्रयोगकर्ता खाता $1 {{GENDER:$2|बनाएल}} गेल",
        "logentry-newusers-create2": "$1 {{GENDER:$2|बनाएल}} {{GENDER:$4|एकटा प्रयोक्ता खाता}} $3",
        "logentry-newusers-byemail": "$1 द्वारा प्रयोक्ता खाता $3 {{GENDER:$2|बनाओल}} गेल आ कूटशब्द ई-पत्र द्वारा भेजल गेल",
-       "logentry-newusers-autocreate": "à¤\96ाता $1 à¤\9bल {{GENDER:$2|बनाà¤\8fल}} à¤¸à¥\8dवतà¤\83",
+       "logentry-newusers-autocreate": "à¤\96ाता $1 à¤¸à¥\8dवà¤\9aालित à¤°à¥\82प à¤¸à¤\81 {{GENDER:$2|बनाà¤\8fल}} à¤\97à¥\87ल à¤\9bल",
        "logentry-protect-protect": "$1 ने $3 $4 {{GENDER:$2|सुरक्षित}} किरल।",
        "logentry-upload-upload": "$1 {{GENDER:$2|ए}} $3 अपलोड केलक",
+       "logentry-upload-overwrite": "$1 {{GENDER:$2|अपलोड कएल गेल}} $3 कऽ एक नव अवतरण",
        "log-name-managetags": "समय प्रबंधन लॉग",
        "logentry-managetags-create": "$1 {{GENDER:$2 बनाएल}} टैग $4",
        "log-name-tag": "ट्याग लौग",
index 8441adf..961f242 100644 (file)
        "tog-usenewrc": "Промени во групи по страници во списокот на скорешни промени",
        "tog-numberheadings": "Наброј ги заглавијата",
        "tog-editondblclick": "Уредување на страници при двоен стисок",
-       "tog-editsectiononrightclick": "Уредување на заглавија со десно копче од глушецот на нивниот наслов",
+       "tog-editsectiononrightclick": "Ð\9eвозможи Ñ\83редување на заглавија со десно копче од глушецот на нивниот наслов",
        "tog-watchcreations": "Додавај ги страниците што ги создавам и податотеките што ги подигам во набљудуваните",
        "tog-watchdefault": "Додавај ги страниците и податотеките што ги уредувам во набљудуваните",
        "tog-watchmoves": "Додавај ги страниците и податотеките што ги преместувам во набљудуваните",
        "tog-watchdeletion": "Додавај ги страниците и податотеките што ги бришам во набљудуваните",
        "tog-watchuploads": "Ставај ги податотеките што ги подигам во набљудуваните",
-       "tog-watchrollback": "Додај ги страниците сум ги отповикал во набљудувани",
+       "tog-watchrollback": "Додавај ги страниците сум ги отповикал во набљудуваните",
        "tog-minordefault": "Обележувај ги сите уредувања како ситни по основно",
        "tog-previewontop": "Прикажи преглед пред кутијата за уредување",
        "tog-previewonfirst": "Прикажи преглед при првото уредување",
        "blockedtext-partial": "<strong>На вашето корисничко име или IP-адреса му е забрането да прави измени на страницава. Можете сепак да уредувате други страници на ова вики.</strong> Сите поединости за забраната ќе ги најдете во [[Special:MyContributions|придонесите на сметката]].\n\nЗабраната ја дал $1.\n\nНаведената причина гласи <em>$2</em>.\n\n* Почеток на забраната: $8\n* Истек на забраната: $6\n* Предвиден забраненик: $7\n* Назнака на забраната #$5",
        "blockedtext": "<strong>Вашето корисничко име или IP-адреса е блокирано.</strong>\n\nБлокирањето е направено од страна на $1.\nДаденото образложение е <em>$2</em>.\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Корисникот што требало да биде блокиран: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со блокирањето.\nМожете да ја искористите можноста „{{int:emailuser}}“ ако е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и не ви е забрането да ја користите.\nВашата сегашна IP-адреса е $3, а назнака на блокирањето гласи #$5.\nВе молиме наведете ги сите подробности прикажани погоре, во вашата евентуална реакција.",
        "autoblockedtext": "Вашата IP-адреса е автоматски блокирана бидејќи била користена од страна на друг корисник, кој бил блокиран од $1.\nДаденото образложение е следново:\n\n:<em>$2</em>\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Со намера да се блокира: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со ова блокирање.\n\nИмајте предвид дека можеби нема да можете да ја искористите можноста „{{int:emailuser}}“ доколку не е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и ви е забрането користење на истата.\n\nВашата IP-адреса е $3, a назнака на блокирањетo е $5.\nВе молиме наведете ги овие подробности доколку реагирате на блокирањето.",
-       "systemblockedtext": "Ð\92аÑ\88еÑ\82о ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP-адÑ\80еÑ\81а Ðµ Ð°Ð²Ñ\82омаÑ\82Ñ\81ки Ð±Ð»Ð¾ÐºÐ¸Ñ\80ано Ð¾Ð´ Ð\9cедиÑ\98аÐ\92ики.\nÐ\9fонÑ\83дена Ð¿Ñ\80иÑ\87ина:\n\n:<em>$2</em>\n\n* Почеток на блокот: $8\n* Истек на блокот: $6\n* Блокот е наменет за: $7\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.",
+       "systemblockedtext": "Ð\92аÑ\88еÑ\82о ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP-адÑ\80еÑ\81а Ðµ Ð°Ð²Ñ\82омаÑ\82Ñ\81ки Ð±Ð»Ð¾ÐºÐ¸Ñ\80ано Ð¾Ð´ Ð\9cедиÑ\98аÐ\92ики.\nÐ\9dаведенаÑ\82а Ð¿Ñ\80иÑ\87ина Ð³Ð»Ð°Ñ\81и:\n\n:<em>$2</em>\n\n* Почеток на блокот: $8\n* Истек на блокот: $6\n* Блокот е наменет за: $7\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.",
        "blockednoreason": "не е наведена причина",
+       "blockedtext-composite": "<strong>Вашето корисничко име или IP-адреса е блокирано.</strong>\n\nНаведената причина гласи:\n\n:<em>$2</em>.\n\n* Почеток на блокот: $8\n* Истек на најдолгиот блок: $6\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.",
+       "blockedtext-composite-reason": "Вашата сметка и/или IP-адреса има неколку блокови",
        "whitelistedittext": "Мора да сте $1 за да уредувате страници.",
        "confirmedittext": "Морате да ја потврдите вашата е-поштенска адреса пред да уредувате страници.\nПоставете ја и валидирајте ја вашата е-поштенска адреса преку вашите [[Special:Preferences|нагодувања]].",
        "nosuchsectiontitle": "Не можам да го пронајдам заглавието",
        "accmailtitle": "Лозинката е испратена.",
        "accmailtext": "На $2 е спратена е случајно создадена лозинка за [[User talk:$1|$1]] е испратена. Истата може да се смени на страницата ''[[Special:ChangePassword|Менување на лозинка]]'' откако ќе се најавите.",
        "newarticle": "(нова)",
-       "newarticletext": "Дојдовте на врска до страница што не постои.\nЗа да ја создадете страницата, напишете текст во полето подолу ([$1 помош]). Ако сте овде по грешка, само систнете на копчето '''назад''' во вашиот прелистувач.",
-       "anontalkpagetext": "----\n<em>Ова е разговорна страница со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.<em>\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:CreateAccount|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.''",
+       "newarticletext": "Дојдовте на врска до страница која сѐ уште не постои.\nЗа да ја создадете страницата, напишете текст во полето подолу ([$1 помош]). Ако сте овде по грешка, само систнете на копчето '''назад''' во вашиот прелистувач.",
+       "anontalkpagetext": "----\n<em>Ова е разговорна страница со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.<em>\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:CreateAccount|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.",
        "noarticletext": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии,\nда ги <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате дневниците],\nили да [{{fullurl:{{FULLPAGENAME}}|action=edit}} ја создадете]</span>.",
        "noarticletext-nopermission": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии или пак да <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате поврзаните дневници]</span>, но немате дозвола да ја создадете страницата.",
        "missing-revision": "Не ја пронајдов преработката бр. $1 на страницата со наслов „{{FULLPAGENAME}}“.\n\nОва обично се должи на застарена врска за разлики што води кон избришана страница.\nПовеќе подробности ќе најдете во [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневникот на бришења].",
index ac5d598..565a91d 100644 (file)
@@ -36,7 +36,7 @@
                ]
        },
        "tog-underline": "കണ്ണികൾക്ക് അടിവരയിടുക:",
-       "tog-hideminor": "à´ªàµ\81തിയ à´®à´¾à´±àµ\8dà´±à´\99àµ\8dà´\99à´³àµ\81à´\9fàµ\86 à´ªà´\9fàµ\8dà´\9fà´¿à´\95യിൽ à´\9aàµ\86റിയ തിരുത്തുകൾ മറയ്ക്കുക",
+       "tog-hideminor": "സമàµ\80à´ªà´\95ാല à´®à´¾à´±àµ\8dà´±à´\99àµ\8dà´\99à´³àµ\81à´\9fàµ\86 à´ªà´\9fàµ\8dà´\9fà´¿à´\95യിൽ à´\9aàµ\86à´±àµ\81തിരുത്തുകൾ മറയ്ക്കുക",
        "tog-hidepatrolled": "റോന്തുചുറ്റിയ തിരുത്തുകൾ പുതിയമാറ്റങ്ങളിൽ മറയ്ക്കുക",
        "tog-newpageshidepatrolled": "റോന്തുചുറ്റപ്പെട്ട താളുകൾ പുതിയതാളുകളുടെ പട്ടികയിൽ മറയ്ക്കുക",
        "tog-hidecategorization": "താളുകളുടെ വർഗ്ഗീകരണം മറയ്ക്കുക",
        "history": "നാൾവഴി",
        "history_short": "നാൾവഴി",
        "history_small": "നാൾവഴി",
-       "updatedmarker": "à´\95à´´à´¿à´\9eàµ\8dà´\9e à´¸à´¨àµ\8dദർശനതàµ\8dതിനàµ\8d à´¶àµ\87à´·à´\82 à´®à´¾à´±àµ\8dà´±à´\82 à´µà´¨àµ\8dà´¨ത്",
+       "updatedmarker": "à´\95à´´à´¿à´\9eàµ\8dà´\9e à´¸à´¨àµ\8dദർശനതàµ\8dതിനàµ\8d à´¶àµ\87à´·à´\82 à´ªàµ\81à´¤àµ\81à´\95àµ\8dà´\95à´ªàµ\8dà´ªàµ\86à´\9fàµ\8dà´\9fത്",
        "printableversion": "അച്ചടിരൂപം",
        "permalink": "സ്ഥിരംകണ്ണി",
        "print": "അച്ചടിയ്ക്കുക",
        "title-invalid-magic-tilde": "ആവശ്യപ്പെട്ട താൾ തലക്കെട്ടിൽ അസാധുവായ മാന്ത്രിക ടിൽഡേ പരമ്പര ഉൾപ്പെടുന്നു (<nowiki>~~~</nowiki>).",
        "title-invalid-too-long": "ഈ തലക്കെട്ടിന്റെ നീളം കൂടുതലാണു്. UTF-8 എൻകോഡിങ്ങിൽ തലക്കെട്ടുകൾക്ക് $1 {{PLURAL:$1|ബൈറ്റിലധികം|ബൈറ്റുകളിലധികം}} നീളമുണ്ടാകാൻ പാടില്ല.",
        "title-invalid-leading-colon": "ആവശ്യപ്പെട്ട താൾ തലക്കെട്ടിന്റെയാദ്യം അസാധുവായ അപൂർണ്ണവിരാമം ഉൾപ്പെടുന്നു.",
-       "perfcached": "താഴെ കൊടുത്തിരിക്കുന്ന വിവരം ശേഖരിച്ചു വെച്ചിരിക്കുന്നതാണ്, അതുകൊണ്ട് ചിലപ്പോൾ പുതിയതായിരിക്കണമെന്നില്ല. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$4|ഒരു ഫലം|$4 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.",
+       "perfcached": "താഴെ കൊടുത്തിരിക്കുന്ന വിവരം ശേഖരിച്ചു വെച്ചിരിക്കുന്നതാണ്, അതുകൊണ്ട് ചിലപ്പോൾ പുതിയതായിരിക്കണമെന്നില്ല. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$1|ഒരു ഫലം|$1 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.",
        "perfcachedts": "താഴെയുള്ള വിവരങ്ങൾ ശേഖരിച്ചുവെച്ചവയിൽ പെടുന്നു, അവസാനം പുതുക്കിയത് $1-നു ആണ്‌. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$4|ഒരു ഫലം|$4 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.",
        "querypage-no-updates": "ഈ താളിന്റെ പുതുക്കൽ തൽക്കാലം നടക്കുന്നില്ല. ഇവിടുള്ള വിവരങ്ങൾ ഏറ്റവും പുതിയതാവണമെന്നില്ല.",
        "viewsource": "മൂലരൂപം കാണുക",
        "passwordpolicies-policyflag-forcechange": "ലോഗിൻ മാറ്റിയിരിക്കണം",
        "passwordpolicies-policyflag-suggestchangeonlogin": "ലോഗിൻ മാറ്റാൻ നിർദ്ദേശിക്കുന്നു",
        "unprotected-js": "സുരക്ഷാകാരണങ്ങളാൽ സംരക്ഷണമില്ലാത്ത താളുകളിൽ നിന്നും ജാവാസ്ക്രിപ്റ്റ് എടുത്തുപയോഗിക്കാൻ കഴിയില്ല. ജാവാസ്ക്രിപ്റ്റ് താളുകൾ മീഡിയവിക്കി: നാമമേഖലയിലോ ഉപയോക്തൃ ഉപതാളായോ മാത്രം സൃഷ്ടിക്കുക",
-       "userlogout-continue": "താà´\99àµ\8dà´\95ൾ à´ªàµ\81റതàµ\8dà´¤àµ\8d à´\95à´\9fà´\95àµ\8dà´\95ാൻ à´\86à´\97àµ\8dരഹിà´\95àµ\8dà´\95àµ\81à´¨àµ\8dà´¨àµ\81à´µàµ\86à´\99àµ\8dà´\95ിൽ [$1 à´²àµ\8bà´\97àµ\8d à´\94à´\9fàµ\8dà´\9fàµ\8d à´¤à´¾à´³à´¿à´²àµ\87à´\95àµ\8dà´\95àµ\8d à´¤àµ\81à´\9fà´°àµ\81à´\95]."
+       "userlogout-continue": "à´ªàµ\81റതàµ\8dà´¤àµ\81à´\95à´\9fà´\95àµ\8dà´\95à´£àµ\8b?"
 }
index 5f668ab..2878062 100644 (file)
        "history": "စာမျက်နှာ ရာဇဝင်",
        "history_short": "ရာဇဝင်",
        "history_small": "ရာဇဝင်",
-       "updatedmarker": "နောက်ဆုံးကြည့်ပြီးသည့်နောက်ပိုင်း တည်းဖြတ်ထားသည်",
+       "updatedmarker": "နောက်ဆုံးကြည့်ပြီးသည့်နောက်ပိုင်း တည်းဖြတ်ထားသည်",
        "printableversion": "ပရင့်ထုတ်နိုင်သော ဗားရှင်း",
        "permalink": "ပုံ​သေ​လိပ်​စာ​",
        "print": "ပရင့်ထုတ်",
        "filerenameerror": "ဖိုင် \"$1\" ကို \"$2\" သို့ အမည်ပြောင်းမရပါ။",
        "filedeleteerror": "ဖိုင် \"$1\" ကို ဖျက်မရပါ။",
        "directorycreateerror": "လမ်းညွှန် \"$1\" ကို ဖန်တီးမရနိုင်ပါ။",
+       "directoryreadonlyerror": "လမ်းညွှန် \"$1\" သည် ဖတ်ရှုရန်သာဖြစ်သည်။",
+       "directorynotreadableerror": "လမ်းညွှန် \"$1\" သည် ဖတ်ရှု၍မရနိုင်ပါ။",
        "filenotfound": "ဖိုင် \"$1\" ကို ရှာမတွေ့ပါ။",
+       "unexpected": "မမျော်လင့်ထားသောတန်ဖိုး: \"$1\"=\"$2\"",
        "formerror": "အမှား - ဖောင်သွင်းနိုင်ခြင်းမရှိပါ",
        "badarticleerror": "ဤလုပ်ဆောင်မှုအား ဤစာမျက်နှာတွင် လုပ်ဆောင်၍ မရနိုင်ပါ။",
        "cannotdelete": "\"$1\" စာမျက်နှာ သို့မဟုတ် ဖိုင်ကို ဖျက်၍ မရပါ။\nတစ်စုံတစ်ဦးမှ ဖျက်နှင့်ပြီး ဖြစ်နိုင်ပါသည်။",
        "cannotdelete-title": "\"$1\" စာမျက်နှာကို ဖျက်၍ မရပါ",
+       "delete-scheduled": "စာမျက်နှာ \"$1\" ကို ဖျက်ပစ်ရန် ရက်သတ်မှတ်ထားသည်။ ကျေးဇူးပြု၍ စိတ်ရှည်ပါ။",
        "delete-hook-aborted": "ရှင်းလင်းပြချက် မပေးထားပါ။",
        "badtitle": "ညံ့ဖျင်းသော ခေါင်းစဉ်",
        "badtitletext": "တောင်းဆိုထားသော စာမျက်နှာ ခေါင်းစဉ်သည် တရားမဝင်ပါ (သို့) ဗလာဖြစ်နေသည် (သို့) အခြားဘာသာများ(inter-language or inter-wiki title)သို့ မှားယွင်းစွာ လင့်ချိတ်ထားသည်။",
        "page_first": "ပထမဆုံး",
        "page_last": "နောက်ဆုံး",
        "histlegend": "တည်းဖြတ်မူများကို နှိုင်းယှဉ်ရန် radio boxes လေးများကို မှတ်သားပြီးနောက် Enter ရိုက်ချပါ သို့ အောက်ခြေမှ ခလုတ်ကို နှိပ်ပါ။<br />\nLegend: <strong>({{int:cur}})</strong> = နောက်ဆုံးမူနှင့် ကွဲပြားချက် <strong>({{int:last}})</strong> = ယင်းရှေ့မူနှင့် ကွဲပြားချက်, <strong>{{int:minoreditletter}}</strong> = အရေးမကြီးသော ပြုပြင်မှု.",
-       "history-fieldset-title": "á\80\9aá\80\81á\80\84á\80ºá\80\99á\80°á\80\99á\80»á\80¬á\80¸ á\80\9bá\80¾á\80¬á\80\96á\80½á\80±ရန်",
+       "history-fieldset-title": "á\80\9aá\80\81á\80\84á\80ºá\80\99á\80°á\80\99á\80»á\80¬á\80¸ á\80\85á\80­á\80\85á\80\85á\80ºရန်",
        "history-show-deleted": "ဖျက်ထားသော မူများသာ",
        "histfirst": "အဟောင်းဆုံး",
        "histlast": "အသစ်ဆုံး",
        "recentchangescount": "လတ်တလော အပြောင်းအလဲများ၊ စာမျက်နှာ ရာဇဝင်များနှင့် မှတ်တမ်းများတွင် ပုံသေအားဖြင့် ပြသရန် တည်းဖြတ်မှုအရေအတွက် -",
        "prefs-help-recentchangescount": "အများဆုံးအရေအတွက် - ၁ဝဝဝ",
        "prefs-help-watchlist-token2": "ဤသည် သင့်စောင့်ကြည့်စာရင်း၏ web feed ရှိ လျို့ဝှက်သော့ ဖြစ်ပါသည်။ သင်၏စောင့်ကြည့်စာရင်းကို ဖတ်ရှုနိုင်သော မည်သူ့ကိုမဆို ယင်းအားမမျှဝေပါနှင့်။ သင်လိုအပ်ပါက [[Special:ResetTokens|ယင်းအား ပြန်ချိန်နိုင်ပါသည်]]။",
+       "prefs-help-tokenmanagement": "သင့်စောင့်ကြည့်စာရင်း၏ web feed ကို ဝင်ရောက်နိုင်သော သင့်အကောင့်လုံခြုံရေး သော့ခလုတ်ကို တွေ့မြင်၊ ပြန်လည်ချိန်ညှိနိုင်ပါသည်။ သော့ခလုတ်ကိုသိသည့် မည်သူမဆို သင့်စောင့်ကြည့်စာရင်းကို ဖတ်ရှုနိုင်သည်၊ ထို့ကြောင့် ယင်းအား မမျှဝေပါနှင့်။",
        "savedprefs": "သင့်ရွေးချယ်မှုတို့ကို သိမ်းပြီးပါပြီ။",
        "savedrights": "{{GENDER:$1|$1}}၏ အသုံးပြု အခွင့်အရေးများကို သိမ်းပြီးပါပြီ။",
        "timezonelegend": "အချိန်ဇုန် -",
        "grant-blockusers": "အသုံးပြုသူများအား ပိတ်ပင်ခြင်းနှင့် ပိတ်ပင်မှု ဖယ်ရှားခြင်း",
        "grant-createaccount": "အကောင့်များ ဖန်တီးရန်",
        "grant-createeditmovepage": "စာမျက်နှာများကို ဖန်တီး၊ တည်းဖြတ်၊ ရွေ့ပြောင်းရန်",
-       "grant-editmyoptions": "á\80\9eá\80\84á\80ºá\81\8fá\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80° á\80¡á\80\95á\80¼á\80\84á\80ºá\80¡á\80\86á\80\84á\80ºá\80\99á\80»á\80¬á\80¸ကို ပြင်ရန်",
+       "grant-editmyoptions": "á\80\9eá\80\84á\80ºá\81\8fá\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80° á\80\9bá\80½á\80±á\80¸á\80\81á\80»á\80\9aá\80ºá\80\85á\80\9bá\80¬á\80\99á\80»á\80¬á\80¸á\80\94á\80¾á\80\84á\80·á\80º JSON á\80¡á\80\95á\80¼á\80\84á\80ºá\80¡á\80\86á\80\84á\80ºကို ပြင်ရန်",
        "grant-editmywatchlist": "သင့် စောင့်ကြည့်စာရင်းကို တည်းဖြတ်ရန်",
        "grant-editpage": "ရှိပြီးသား စာမျက်နှာများကို တည်းဖြတ်ရန်",
        "grant-editprotected": "ကာကွယ်ထားသော စာမျက်နှာများကို တည်းဖြတ်ရန်",
        "rcfilters-savedqueries-add-new-title": "လက်ရှိ စိစစ်မှုအပြင်အဆင်များကို သိမ်းရန်",
        "rcfilters-restore-default-filters": "မူလပုံသေ စိစစ်မှုများအတိုင်း ပြန်ထားရန်",
        "rcfilters-clear-all-filters": "စိစစ်မှုများအားလုံး ရှင်းလင်းရန်",
-       "rcfilters-show-new-changes": "နောက်ဆုံး ပြောင်းလဲမှုများကို ကြည့်ရန်",
+       "rcfilters-show-new-changes": "$1 ကတည်းက ပြောင်းလဲမှုအသစ်များကို ကြည့်ရန်",
        "rcfilters-search-placeholder": "စိစစ်မှုစနစ် အပြောင်းအလဲများ (စိစစ်စနစ်အမည်အတွက် menu သို့မဟုတ် ရှာဖွေခလုတ်ကို အသုံးပြုပါ)",
        "rcfilters-invalid-filter": "မရေရာသော စိစစ်မှု",
        "rcfilters-empty-filter": "သက်ဝင်နေသော စိစစ်မှုစနစ်များ မရှိပါ။ ပံ့ပိုးမှုအားလုံးကို ပြသထားသည်။",
        "rcfilters-watchlist-showupdated": "သင်နောက်ဆုံးကြည့်ရှုခဲ့ပြီးနောက် ပြောင်းလဲမှုရှိခဲ့သော စာမျက်နှာများကို <strong>စာလုံးမဲ</strong> ဖြင့် ပြသထားသည်။",
        "rcfilters-preference-label": "လတ်တလောအပြောင်းအလဲများ၏ မွမ်းမံထားသောဗားရှင်းကို ဝှက်ရန်",
        "rcfilters-watchlist-preference-label": "စောင့်ကြည့်စာရင်း၏ မွမ်းမံထားသောဗားရှင်းကို ဝှက်ရန်",
+       "rcfilters-watchlist-preference-help": "စစ်ထုတ်ရှာဖွေခြင်း သို့မဟုတ် မီးမောင်းထိုးပြခြင်း လုပ်ဆောင်ချက်မပါဘဲ စောင့်ကြည့်စာရင်းကို ခေါ်ယူမည်။",
        "rcfilters-target-page-placeholder": "စာမျက်နှာနာမည် (သို့မဟုတ် ကဏ္ဍ) ရိုက်ထည့်ပါ",
        "rcnotefrom": "အောက်ပါတို့မှာ <strong>$3၊ $4</strong> မှစ၍ {{PLURAL:$5|ပြောင်းလဲမှု|ပြောင်းလဲမှုများ}} ဖြစ်သည်  (<strong>$1</strong> အထိ ပြထား)။",
        "rclistfromreset": "ရက်စွဲရွေးချယ်မှုအား ပြန်စရန်",
        "upload-description": "ဖိုင်ဖော်ပြချက်",
        "upload-options": "ဖိုင်တင်သည့် ရွေးချယ်မှုများ",
        "watchthisupload": "ဤဖိုင်အား စောင့်ကြည့်ရန်",
+       "upload-proto-error": "မမှန်ကန်သော လုပ်နည်းလုပ်ထုံး",
        "upload-file-error": "အတွင်းပိုင်းအမှား",
        "upload-misc-error": "upload တင်ရာတွင် အမည်မသိ အမှား",
        "upload-dialog-title": "ဖိုင်​တင်​ရန်​",
        "emailccme": "ကျွန်ုပ်ပို့လိုက်သော အီးမေးကော်ပီကို ကျွန်ုပ်ထံ ပြန်ပို့ပါ။",
        "emailsent": "အီးမေးပို့လိုက်ပြီ",
        "emailsenttext": "သင့်အီးမေးမက်ဆေ့ကို ပို့လိုက်ပြီးပြီ ဖြစ်သည်။",
+       "usermessage-summary": "စနစ်စာတို ချန်ထားခြင်း။",
        "usermessage-editor": "စနစ်မက်ဆင်ဂျာ",
        "watchlist": "စောင့်ကြည့်စာရင်း",
        "mywatchlist": "စောင့်ကြည့်စာရင်း",
        "delete-confirm": "\"$1\"ကို ဖျက်ပါ",
        "delete-legend": "ဖျက်",
        "historywarning": "<strong>သတိပေးချက်။</strong> သင်ဖျက်ပစ်တော့မည့် စာမျက်နှာတွင် {{PLURAL:$1|တည်းဖြတ်မူ|တည်းဖြတ်မူများ}} $1 ခု ရှိနေသည်-",
-       "historyaction-submit": "ပြသရန်",
+       "historyaction-submit": "á\80\95á\80¼á\80\94á\80ºá\80\9cá\80\8aá\80ºá\80\95á\80¼á\80\84á\80ºá\80\86á\80\84á\80ºá\80\99á\80¾á\80¯á\80\99á\80»á\80¬á\80¸á\80\80á\80­á\80¯ á\80\95á\80¼á\80\9eá\80\9bá\80\94á\80º",
        "confirmdeletetext": "သင်သည် စာမျက်နှာတစ်ခုကို ယင်း၏ မှတ်တမ်းများနှင့်တကွ ဖျက်ပစ်တော့မည် ဖြစ်သည်။\nဤသို့ ဖျက်ပစ်ရန် သင် အမှန်တကယ် ရည်ရွယ်လျက်  နောက်ဆက်တွဲ အကျိုးဆက်များကို သိရှိနားလည်ပြီး [[{{MediaWiki:Policy-url}}|မူဝါဒ]] အတိုင်းလုပ်ဆောင်နေခြင်းဖြစ်ကြောင်းကို အတည်ပြုပေးပါ။",
        "actioncomplete": "လုပ်ဆောင်ချက် ပြီးပြီ",
        "actionfailed": "ဆောင်ရွက်မှုမအောင်မြင်ပါ",
        "deleting-backlinks-warning": "<strong>သတိပေးချက်။</strong> သင်ဖျက်ပစ်တော့မည့် စာမျက်နှာအား [[Special:WhatLinksHere/{{FULLPAGENAME}}|အခြားစာမျက်နှာများမှ]] ချိတ်ဆက်ထားခြင်း သို့မဟုတ် ထည့်သွင်းထားခြင်း ရှိနေသည်။",
        "deleting-subpages-warning": "<strong>သတိပေးချက်။</strong> သင်ဖျက်တော့မည့် စာမျက်နှာတွင် [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|စာမျက်နှာခွဲ တစ်ခု|စာမျက်နှာခွဲ $1 ခု|51=စာမျက်နှာခွဲ ၅၀ ကျော်}}]] ရှိနေသည်။",
        "rollback": "နောက်ပြန်ပြင် တည်းဖြတ်မှုများ",
+       "rollback-confirmation-confirm": "ကျေးဇူးပြု၍ အတည်ပြုပါ-",
        "rollback-confirmation-yes": "နောက်ပြန် ပြန်သွားရန်",
        "rollback-confirmation-no": "မလုပ်တော့ပါ",
        "rollbacklink": "နောက်ပြန် ပြန်သွားရန်",
        "ipbreason-dropdown": "*ယေဘုယျ ပိတ်ပင်တားဆီးရခြင်း အကြောင်းပြချက်များ\n** မှားယွင်းအချက်အလက်များကို ထည့်သွင်းမှု\n** စာမျက်နှာများမှ အကြောင်းအရာကို ဖယ်ရှားမှု\n** ပြင်ပဆိုဒ်များသို့လင့်ခ်ချိတ်၍ ဖွမှု\n** စာမျက်နှာများတွင် ပေါက်တတ်ကရများ ထည့်သွင်းမှု\n** ခြိမ်းခြောက်ခြင်း အပြုအမူ/အနှောက်အယှက်ပေးခြင်း\n** အကောင့်များစွာကို အလွဲသုံးစားလုပ်မှု\n** လက်ခံနိုင်ဖွယ်မရှိသော အသုံးပြုသူအမည်",
        "ipb-hardblock": "ဤအိုင်ပီလိပ်စာမှ လော့ဂ်အင်ဝင်ထားသော အသုံးပြုသူများကို တည်းဖြတ်ခြင်းမှ တားမြစ်ရန်",
        "ipbcreateaccount": "အကောင့်ဖန်တီးခြင်း",
-       "ipbemailban": "á\80¡á\80®á\80¸á\80\99á\80±á\80¸á\80\95á\80­á\80¯á\80·á\80\81á\80¼á\80\84á\80ºá\80¸á\80\99á\80¾ á\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80°á\80\80á\80­á\80¯ á\80\90á\80¬á\80¸á\80\86á\80®á\80¸á\80\9bá\80\94်",
+       "ipbemailban": "á\80¡á\80®á\80¸á\80\99á\80±á\80¸á\80\9cá\80ºá\80\95á\80­á\80¯á\80·á\80\94á\80±á\80\9eá\80\8a်",
        "ipbenableautoblock": "ဤအသုံးပြုသူ အသုံးပြုသော အိုင်ပီလိပ်စာနှင့် သူတို့ ပြင်ဆင်ရန် ကြိုးစားသည့် နောက်ဆက်တွဲ အိုင်ပီလိပ်စာများကိုပါ အလိုအလျောက်ပိတ်ပင်ရန်",
        "ipbsubmit": "ဤအသုံးပြုသူကို ပိတ်ပင်ရန်",
        "ipbother": "အခြားအချိန်:",
        "ipboptions": "၂ နာရီ:2 hours,၁ ရက်:1 day,၃ ရက်:3 days,၁ ပတ်:1 week,၂ ပတ်:2 weeks,၁ လ:1 month,၃ လ:3 months,၆ လ:6 months,၁ နှစ်:1 year,အနန္တ:infinite",
        "ipbhidename": "အသုံးပြုသူအမည်ကို တည်းဖြတ်မှုများနှင့် စာရင်းမှထဲတွင် ဝှက်ထားရန်",
        "ipbwatchuser": "ဤအသုံးပြုသူ၏ စာမျက်နှာနှင့် ဆွေးနွေးချက်တို့ကို စောင့်ကြည့်ရန်",
-       "ipb-disableusertalk": "á\80\95á\80­á\80\90á\80ºá\80\95á\80\84á\80ºá\80\91á\80¬á\80¸á\80\85á\80\89á\80ºá\80¡á\80\90á\80½á\80\84á\80ºá\80¸ á\80¤á\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80°á\80¡á\80¬á\80¸ á\80\9eá\80°á\80\90á\80­á\80¯á\80·á\81\8f á\80\80á\80­á\80¯á\80\9aá\80ºá\80\95á\80­á\80¯á\80\84á\80ºá\80\86á\80½á\80±á\80¸á\80\94á\80½á\80±á\80¸á\80\81á\80»á\80\80á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯ á\80\95á\80¼á\80\84á\80ºá\80\86á\80\84á\80ºá\80\81á\80¼á\80\84á\80ºá\80¸á\80\99á\80¾ á\80\95á\80­á\80\90á\80ºá\80\95á\80\84á\80ºá\80\9bá\80\94်",
+       "ipb-disableusertalk": "á\80\9eá\80°á\80\90á\80­á\80¯á\80·á\81\8f á\80\80á\80­á\80¯á\80\9aá\80ºá\80\95á\80­á\80¯á\80\84á\80ºá\80\86á\80½á\80±á\80¸á\80\94á\80½á\80±á\80¸á\80\81á\80»á\80\80á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80­á\80¯ á\80\95á\80¼á\80\84á\80ºá\80\86á\80\84á\80ºá\80\81á\80¼á\80\84á\80ºá\80¸á\80\94á\80±á\80\9eá\80\8a်",
        "ipb-change-block": "အသုံးပြုသူအား ဤအပြင်အဆင်များဖြင့် ထပ်မံပိတ်ပင်ရန်",
        "ipb-confirm": "ပိတ်ပင်မှုကို အတည်ပြု",
        "ipb-partial": "တစ်စိတ်တစ်ပိုင်း",
        "blocklist-userblocks": "အကောင့်ပိတ်ပင်မှုများ ဝှက်",
        "blocklist-tempblocks": "ယာယီပိတ်ပင်မှုများ ဝှက်",
        "blocklist-addressblocks": "အိုင်ပီတစ်ခုတည်းပိတ်ပင်မှု ဝှက်",
+       "blocklist-type": "အမျိုးအစား:",
        "blocklist-type-opt-all": "အားလုံး",
        "blocklist-type-opt-partial": "တစ်စိတ်တစ်ပိုင်း",
        "blocklist-rangeblocks": "အကွာအဝေးလိုက် ပိတ်ပင်မှုများ ဝှက်",
        "blocklist-editing-page": "စာမျက်နှာများ",
        "blocklist-editing-ns": "အမည်ညွှန်းများ",
        "ipblocklist-empty": "ပိတ်ပင်ထားမှုစာရင်းသည် ဗလာဖြစ်နေသည်။",
-       "ipblocklist-no-results": "á\80\90á\80±á\80¬á\80\84á\80ºá\80¸á\80\86á\80­á\80¯á\80\9cá\80­á\80¯á\80\80á\80ºá\80\9eá\80±á\80¬ á\80¡á\80­á\80¯á\80\84á\80ºá\80\95á\80®á\80\9cá\80­á\80\95á\80ºá\80\85á\80¬ á\80\9eá\80­á\80¯á\80·á\80\99á\80\9fá\80¯á\80\90á\80º á\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80°á\80¡á\80\99á\80\8aá\80ºá\80\80á\80­á\80¯ á\80\99á\80\95á\80­á\80\90á\80ºá\80\95á\80\84á\80ºá\80\91á\80¬á\80¸ပါ။",
+       "ipblocklist-no-results": "á\80\90á\80±á\80¬á\80\84á\80ºá\80¸á\80\86á\80­á\80¯á\80\9cá\80­á\80¯á\80\80á\80ºá\80\9eá\80±á\80¬ á\80¡á\80­á\80¯á\80\84á\80ºá\80\95á\80®á\80\9cá\80­á\80\95á\80ºá\80\85á\80¬ á\80\9eá\80­á\80¯á\80·á\80\99á\80\9fá\80¯á\80\90á\80º á\80¡á\80\9eá\80¯á\80¶á\80¸á\80\95á\80¼á\80¯á\80\9eá\80°á\80¡á\80\99á\80\8aá\80ºá\80\90á\80½á\80\84á\80º á\80\80á\80­á\80¯á\80\80á\80ºá\80\8aá\80®á\80\9eá\80±á\80¬á\80\95á\80­á\80\90á\80ºá\80\95á\80\84á\80ºá\80\91á\80¬á\80¸á\80\86á\80®á\80¸á\80\99á\80¾á\80¯á\80\80á\80­á\80¯ á\80\99á\80\90á\80½á\80±á\80·á\80\9bá\80¾á\80­ပါ။",
        "blocklink": "ပိတ်ပင်",
        "unblocklink": "မပိတ်ပင်တော့ရန်",
        "change-blocklink": "စာကြောင်းအမည် ပြောင်းရန်",
        "move-leave-redirect": "ပြန်ညွှန်းတစ်ခု ချန်ထားရန်",
        "protectedpagemovewarning": "<strong>သတိပေးချက်။</strong> ဤစာမျက်နှာအား စီမံခန့်ခွဲသူအဆင့်ရှိသူများသာ ရွှေ့ပြောင်းနိုင်ရန် ကာကွယ်ထားသည်။\nနောက်ဆုံးမှတ်တမ်းအား ကိုးကားနိုင်ရန် အောက်တွင် ဖော်ပြထားသည်။",
        "semiprotectedpagemovewarning": "<strong>မှတ်ချက်။</strong> ဤစာမျက်နှာအား အလိုအလျောက် အတည်ပြုထားသော အသုံးပြုသူအဆင့်ရှိသူများသာ ရွှေ့ပြောင်းနိုင်ရန် ကာကွယ်ထားသည်။\nနောက်ဆုံးမှတ်တမ်းအား ကိုးကားနိုင်ရန် အောက်တွင် ဖော်ပြထားသည်။",
-       "export": "စာမျက်နှာများကို Export ထုတ်ရန်",
+       "export": "စာမျက်နှာများကို တင်ပို့ရန်",
+       "exportall": "စာမျက်နှာများအားလုံးကို တင်ပို့ရန်",
        "export-submit": "တင်ပို့ရန်",
        "export-addcattext": "ကဏ္ဍမှ စာမျက်နှာများကို ပေါင်းထည့်ရန် -",
        "export-addcat": "ပေါင်းထည့်ရန်",
        "export-addnstext": "အမည်ညွှန်းမှ စာမျက်နှာများကို ပေါင်းထည့်ရန်",
        "export-addns": "ပေါင်းထည့်ရန်",
        "export-download": "ဖိုင်အဖြစ် သိမ်းရန်",
+       "export-templates": "တမ်းပလိတ်များ ပါဝင်မည်",
        "allmessages": "စနစ်၏ သတင်းများ",
        "allmessagesname": "အမည်",
        "allmessagesdefault": "ပုံမှန် အသိပေးချက် စာသား",
        "importinterwiki": "အခြားဝီကီမှ တင်သွင်းရန်",
        "import-interwiki-sourcewiki": "ရင်းမြစ် ဝီကီ:",
        "import-interwiki-sourcepage": "ရင်းမြစ် စာမျက်နှာ:",
+       "import-interwiki-templates": "တမ်းပလိတ်များအားလုံး ပါဝင်မည်",
        "import-interwiki-submit": "တင်သွင်းရန်",
        "import-upload-filename": "ဖိုင်အမည် -",
        "import-comment": "မှတ်ချက် -",
        "import-token-mismatch": "session data ဆုံးရှုံးမှု ဖြစ်ပါသည်။\n\nသင်သည် အကောင့်မှ ထွက်လိုက်တာဖြစ်နိုင်သည်။  <strong>အကောင့်ထဲသို့ ဝင်ထားနေခြင်းဖြစ်အောင် အတည်ပြုပြီး ထပ်မံကြိုးစားကြည့်ပါ</strong>။\nအကယ်၍ အလုပ်မဖြစ်သေးပါက [[Special:UserLogout|အကောင့်မှထွက်]]ပြီးနောက် ထပ်မံလော့ဂ်အင်ဝင်ရောက်ပါ။ သင်၏ဘရောက်ဆာက ဤဝဘ်ဆိုဒ်မှ cookie ကို ခွင့်ပြုထားကြောင့် စစ်ဆေးပေးပါ။",
        "importlogpage": "ထည့်သွင်းသည့် မှတ်တမ်း",
        "importlogpagetext": "အခြားဝီကီများမှ အက်ဒမင်ဆိုင်ရာ တည်းဖြတ်မှုရာဇဝင်နှင့် စာမျက်နှာ တင်သွင်းမှုများ",
+       "javascripttest-pagetext-unknownaction": "အမည်မသိ လုပ်ဆောင်ချက် \"$1\"။",
+       "javascripttest-qunit-intro": "[$1 စမ်းသပ်မှုစာရွက်စာတမ်း] ကို mediawiki.org ပေါ်တွင်ကြည့်ပါ။",
        "tooltip-pt-userpage": "{{GENDER:|သင်၏ အသုံးပြုသူ}} စာမျက်နှာ",
        "tooltip-pt-mytalk": "{{GENDER:|သင်၏}} ဆွေးနွေးချက်စာမျက်နှာ",
        "tooltip-pt-anontalk": "ဤအိုင်ပီလိပ်စာမှ တည်းဖြတ်မှုများအကြောင်း ဆွေးနွေးချက်",
        "logentry-block-block": "$1 က {{GENDER:$4|$3}} ကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပိတ်ပင်ခဲ့သည်}}",
        "logentry-block-unblock": "$1 က {{GENDER:$4|$3}} ကို {{GENDER:$2|ပိတ်ပင်မှုမှ ပြန်ဖြေခဲ့သည်}}",
        "logentry-block-reblock": "$1 က {{GENDER:$4|$3}} အတွက် ပိတ်ပင်မှုအပြင်အဆင်များကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပြောင်းလဲခဲ့သည်}}",
+       "logentry-partialblock-block-page": "{{PLURAL:$1|စာမျက်နှာ|စာမျက်နှာများ}} $2",
        "logentry-suppress-block": "{{GENDER:$4|$3}} အား $5 ကြာအောင် $1 က {{GENDER:$2|ပိတ်ပင်ခဲ့သည်}} $6",
        "logentry-suppress-reblock": "$1 က {{GENDER:$4|$3}} အတွက် ပိတ်ပင်မှုအပြင်အဆင်များကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပြောင်းလဲခဲ့သည်}}",
        "logentry-move-move": "$3 စာမျက်နှာကို $4 သို့ $1က {{GENDER:$2|ရွှေ့ခဲ့သည်}}",
        "feedback-cancel": "မလုပ်တော့ပါ",
        "feedback-close": "ပြီးပြီ",
        "feedback-dialog-title": "အကြံပေး ပေါင်းထည့်ရန်",
+       "feedback-error2": "အမှား- တည်းဖြတ်မှု မအောင်မြင်ပါ",
        "feedback-message": "မက်ဆေ့:",
        "feedback-subject": "အကြောင်းအရာ:",
        "feedback-submit": "ထည့်သွင်းရန်",
        "special-characters-group-hebrew": "ဟီးဘရူး",
        "special-characters-group-bangla": "ဘင်္ဂလား",
        "special-characters-group-tamil": "တမီးလ်",
+       "special-characters-group-telugu": "တီလူဂု",
+       "special-characters-group-sinhala": "ရှင်ဟာလာ",
+       "special-characters-group-gujarati": "ဂူဂျာရတီ",
+       "special-characters-group-devanagari": "ဒီဗနာဂရီ",
        "special-characters-group-thai": "ထိုင်း",
        "special-characters-group-lao": "လာအို",
        "special-characters-group-khmer": "ခမာ",
        "log-action-filter-patrol-patrol": "လူဖြင့် စောင့်ကြပ်စစ်ဆေး",
        "log-action-filter-patrol-autopatrol": "အလိုအလျောက် စောင့်ကြပ်စစ်ဆေး",
        "log-action-filter-protect-protect": "ကာကွယ်မှု",
+       "log-action-filter-protect-unprotect": "မကာကွယ်တော့ခြင်း",
        "log-action-filter-rights-rights": "လူဖြင့် ပြောင်းလဲမှု",
        "log-action-filter-rights-autopromote": "အလိုအလျောက် ပြောင်းလဲမှု",
        "log-action-filter-upload-revert": "ပြန်ပြောင်းရန်",
        "specialpage-securitylevel-not-allowed-title": "ခွင့်မပြုပါ",
        "cannotauth-not-allowed-title": "ခွင့်ပြုချက် ငြင်းပယ်လိုက်သည်",
        "cannotauth-not-allowed": "သင်သည် ဤစာမျက်နှာကို အသုံးပြုခွင့်မရှိပါ",
+       "credentialsform-account": "အကောင့်နာမည်-",
        "userjsispublic": "ကျေးဇူးပြု၍ မှတ်သားပါ- JavaScript စာမျက်နှာခွဲများတွင် အခြားအသုံးပြုသူများ ကြည့်ရှုနိုင်သော လျို့ဝှက်အပ်သည့်အချက်အလက် မပါဝင်သင့်ပါ။",
        "edit-error-short": "အမှား - $1",
        "edit-error-long": "အမှားများ:\n\n$1",
index dead74a..2900229 100644 (file)
        "prot_1movedto2": "[[$1]] sóa khì tī [[$2]]",
        "protect-legend": "Khak-tēng beh pó-hō·",
        "protectcomment": "Lí-iû:",
+       "protect-default": "Ún-chún só͘-ū iōng-chiá",
        "protect-level-autoconfirmed": "Ta ín-chún chū-tōng khak-jīn iōng-chiá",
        "protect-level-sysop": "Ta ín-chún koán-lí jîn-oân",
        "protect-expiring": "chì $1 (UTC) kòe-kî",
        "logentry-move-move": "$1 {{GENDER:$2|sóa}} $3 chit ia̍h khì $4",
        "logentry-move-move_redir": "$1 iōng choán-ia̍h {{GENDER:$2|sóa}} ia̍h-bīn $3 kòe $4",
        "logentry-newusers-create": "已經{{GENDER:$2|開好}}用者口座 $1",
+       "logentry-protect-protect": "$1 {{GENDER:$2|pó-hō͘ liáu}} $3 $4",
        "searchsuggest-search": "Chhoē {{SITENAME}}",
        "expandtemplates": "Khok-chhiong pang-bô͘",
        "expand_templates_input": "Su-ji̍p bûn-jī:",
index 741d7f8..de39a2e 100644 (file)
        "about": "Informasie",
        "article": "Artikel",
        "newwindow": "(niej vienster)",
-       "cancel": "Aofbreken",
+       "cancel": "Afbreaken",
        "moredotdotdot": "Meer...",
        "morenotlisted": "Disse lieste is niet kompleet...",
        "mypage": "Gebrukerszied",
        "externaldberror": "Der gung iets fout bie de externe authentisering, of je maggen je gebrukersprofiel niet bewarken.",
        "login": "Anmelden",
        "nav-login-createaccount": "Anmelden",
-       "logout": "Ofmelden",
+       "logout": "Afmelden",
        "userlogout": "Aofmelden",
        "notloggedin": "Neet an-emelded",
        "userlogin-noaccount": "Heb jy noch geen gebrukersname?",
        "publishpage": "Zied uutbrengen",
        "publishchanges": "Wiezigingen uutbrengen",
        "preview": "Naokieken",
-       "showpreview": "Bewarking naokieken",
-       "showdiff": "Verschil bekieken",
+       "showpreview": "Bewarking nåkyken",
+       "showdiff": "Verskil bekyken",
        "blankarticle": "<strong>Waorschuwing:</strong> de zied die'j anmaken willen is leeg.\nA'j noen weer op \"$1\" klikken, dan wördt de zied an-emaakt zonder enige inhoud.",
        "anoneditwarning": "<strong>Waorschuwing:</strong> je bin niet an-emeld.\nJoew IP-adres zal op-esleugen wörden a'j wiezigingen op disse zied anbrengen. A'j je eigen <strong>[$1 anmelden]</strong> of <strong>[$2 inschrieven]</strong> dan koemen joew bewarkingen onder joew gebrukersnaam te staon, samen mit aandere veurdelen.",
        "anonpreviewwarning": "''Je bin niet an-emeld.''\n''Deur de bewarking op te slaon wörden joew IP-adres op-esleugen in de ziedgeschiedenisse.''",
index 4adfca2..40e2855 100644 (file)
        "history": "Geschiedenis",
        "history_short": "Geschiedenis",
        "history_small": "geschiedenis",
-       "updatedmarker": "bewerkt sinds mijn laatste bezoek",
+       "updatedmarker": "bewerkt sinds uw laatste bezoek",
        "printableversion": "Printvriendelijke versie",
        "permalink": "Permanente koppeling",
        "print": "Afdrukken",
        "autoblockedtext": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt is door een andere gebruiker, die geblokkeerd is door $1.\nDe opgegeven reden is:\n\n:''$2''\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\n\nU kunt geen gebruik maken van de functie \"{{int:emailuser}}\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]], en het gebruik van deze functie niet is geblokkeerd.\n\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
        "systemblockedtext": "Uw gebruikersaccount of IP-adres is automatisch geblokkeerd door MediaWiki.\nDe opgegeven reden is:\n\n:<em>$2</em>\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nUw huidige IP-adres is $3.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
        "blockednoreason": "geen reden opgegeven",
+       "blockedtext-composite": "Uw gebruikersaccount of IP-adres is geblokkeerd.\n\nDe opgegeven reden is:\n\n:<em>$2</em>\n\n* Aanvang blokkade: $8\n* Einde van de langste blokkade: $6\n\nUw huidige IP-adres is $3.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
+       "blockedtext-composite-reason": "Er zijn meerdere blokkades tegen uw account en/of IP-adres",
        "whitelistedittext": "U moet $1 om pagina's te bewerken.",
        "confirmedittext": "U moet uw e-mailadres bevestigen voor u kunt bewerken.\nVoer uw e-mailadres in en bevestig het via uw [[Special:Preferences|voorkeuren]].",
        "nosuchsectiontitle": "Deze subkop bestaat niet",
index 40405e6..3eb72a5 100644 (file)
@@ -7,7 +7,8 @@
                        "Youssoufkadialy",
                        "Amire80",
                        "Nafadji Mory Diané",
-                       "Babamamadidiane"
+                       "Babamamadidiane",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "ߛߘߌ߬ߜߋ߲߬ ߞߘߐߞߍ߬ߙߍ߲߬ߘߍ߬ߣߍ߲",
        "history": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ",
        "history_short": "ߘߐ߬ߝߐ",
        "history_small": "ߕߊ߬ߡߌ߲߬ߣߍ߲",
-       "updatedmarker": "ß\8a߬ ß\9fß\8fß²ß\98ß\90ߦß\8a ß\9eß\8a߬ߦß\8c߯ ß\92 ß ߊ߫ ߞߐߟߊ߫ ߓߐߒߡߊߟߌ ߟߎ߬ ߡߊ߬",
+       "updatedmarker": "ß\8a߬ ß\9fß\8fß²ß\98ß\90ߦß\8a ß\9eß\8a߬ߦß\8c߯ ß\8c ß\9fߊ߫ ߞߐߟߊ߫ ߓߐߒߡߊߟߌ ߟߎ߬ ߡߊ߬",
        "printableversion": "ߓߐߞߏߣߊ߲߫ ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ",
        "permalink": "ߛߘߌ߬ߜߋ߲߬ ߓߟߏߕߍ߰ߓߊߟߌ",
        "print": "ߜߌ߬ߙߌ߲߬ߘߌ߬ߟߌ",
-       "view": "ß\8a߬ ß\98ß\90ß\9eß\8a߬ß\99ß\8a߲߬",
+       "view": "ߦß\8c߬ß\98ß\8a߬ß\9fß\8c",
        "view-foreign": "ߊ߬ ߦߋ߫ ߦߊ߲߬ $1",
        "edit": "ߊ߬ ߢߟߊߞߎߘߦߊ߫",
        "edit-local": "ߕߌ߲߬ߞߎߘߎ߲ ߞߊ߲߬ߛߓߍߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
        "currentevents-url": "Project:ߛߋ߲߬ߠߊ߬ ߞߍߞߎߘߊ ߟߎ߬",
        "disclaimers": "ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ",
        "disclaimerpage": "Project:ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ߫ ߝߘߏ߬ߓߊ߬ߡߊ",
-       "edithelp": "ß¡ß\8a߬ߦß\9fß\8d߬ߢߊ߲߬ߠߌ߲ ߘߍ߬ߡߍ߲߬ߠߌ߲",
+       "edithelp": "ß¡ß\8a߬ߦß\9fß\8d߬ߡߊ߲߬ߠߌ߲ ߘߍ߬ߡߍ߲߬ߠߌ߲",
        "helppage-top-gethelp": "ߘߍ߬ߡߍ߲߬ߠߌ",
        "mainpage": "ߓߏ߬ߟߏ߲߬ߘߊ",
        "mainpage-description": "ߓߏ߬ߟߏ߲߬ߘߊ",
        "viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫",
        "viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫",
        "viewsourcetext": "ߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߛߎ߲ ߦߋ߫ ߟߊ߫߸ ߞߵߊ߬ ߓߊߓߌ߬ߟߊ߬",
+       "customcssprotected": "CSS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.",
+       "customjsonprotected": "JSON ߞߐߜߍ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.",
+       "customjsprotected": "JavaScript ߞߐߜߍ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.",
+       "sitecssprotected": "CSS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߕߙߐ߫ ߟߊ߫.",
+       "sitejsonprotected": "JSON ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߕߙߐ߫ ߟߊ߫.",
+       "sitejsprotected": "JavaScript ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߘߋ߬ߦߌ߬ ߟߊ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߡߊ߬.",
        "mycustomcssprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ CCS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.",
        "mycustomjsonprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ JSON ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.",
        "mycustomjsprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ JavaScript ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.",
        "titleprotected": "ߞߎ߲߬ߕߐ߮ ߣߌ߲߬ ߓߘߊ߫ ߟߊߞߊ߲ߘߊ߫ ߛߌ߲ߘߟߌ ߡߊ߬  [[User:$1|$1]] ߓߟߏ߫.\nߞߎ߲߭ ߡߍ߲ ߦߴߏ߬ ߟߊ߫߸ ߏ߬ ߦߋ߫ <em>$2</em>.",
        "filereadonlyerror": "ߞߐߕߐ߮ \"$1\" ߕߍ߫ ߛߐ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫߸ ߞߵߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߞߐߕߐ߮ ߟߊߡߙߊ߬ ߦߙߐ  \"$2\" ߦߋ߫ ߞߊ߬ߙߊ߲ ߘߐߙߐ߲߫ ߝߊ߬ߘߌ ߟߋ߬ ߘߐ߫.\n\nߞߊ߲ߞߋ ߟߊߓߊ߯ߙߟߊ ߡߍ߲ ߣߵߊ߬ ߛߐ߰ ߟߊ߫߸ ߏ߬ ߓߘߊ߫ ߘߊ߲߬ߕߍ߰ߟߌ ߘߏ߫ ߞߍ߫: \"$3\".",
        "invalidtitle": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ",
+       "invalidtitle-knownnamespace": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ ߕߐ߮ ߛߓߍ ߞߣߍ ߡߊ߬ \"$2\" ߊ߬ ߣߌ߫ ߛߓߍߟߌ  \"$3\"",
+       "invalidtitle-unknownnamespace": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ ߞߊ߬ ߓߍ߲߬ ߕߐ߯ߛߓߍ ߞߣߍ߫ ߡߊߟߐ߲ߓߊߟߌ ߝߙߍߕߍ ߡߊ߬ $1 ߊ߬ ߣߌ߫ ߛߓߍߟߌ  \"$2\"",
        "exception-nologin": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫",
+       "exception-nologin-text": "ߌ ߜߊ߲߬ߞߎ߲߫ ߖߊ߰ߣߌ߲߬߸ ߛߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫ ߥߟߊ߫ ߝߏ߲߬ߝߏ߲.",
        "virus-unknownscanner": "ߢߐߛߌߙߋ߲ߞߟߊ߬ ߡߊߟߐ߲ߓߊߟߌ",
+       "logouttext": "<strong>ߌ ߜߊ߲߬ߞߎ߲߬ߓߐ߬ߣߍ߲߬ ߕߍ߫.</strong>\n\nߞߐߜߍ ߘߏ߫ ߟߎ߫ ߕߘߍ߬ ߘߌ߫ ߞߍ߫ ߓߊ߯ߙߊ߫ ߟߊ߫ ߞߵߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߕߏ߫߸ ߝߏ߫ ߣߴߌ ߞߵߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬ ߖߏ߬ߛߌ߬.",
        "logging-out-notify": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ ߦߴߌ ߘߐ߫߸ ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬.",
        "logout-failed": "ߌ ߕߍߣߊ߬ ߛߋ߫ ߟߴߌ ߜߊ߲߬ߞߎ߬ߣߍ߲ ߓߐ߫ ߟߊ߫ ߕߊ߲߫ $1",
        "cannotlogoutnow-title": "ߌ ߕߍ߫ ߣߊ߬ ߛߋ߫ ߟߴߌ ߜߊ߲߬ߞߎ߬ߣߍ߲ ߓߐ߫ ߟߊ߫ ߕߊ߲߫",
        "createaccounterror": "ߖߊ߬ߕߋ߬ߘߊ߬ ߕߍ߫ ߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߛߌ߲ߘߌ߫ ߟߊ߫: $1",
        "nocookiesnew": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ ߓߘߊ߫ ߛߌ߲ߘߌ߫߸ ߞߏ߬ߣߌ߲߬ ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫.\n{{SITENAME}} ߦߋ߫ ߞߎߞߌߦߋ ߟߊ߫ ߞߊ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߜߊ߲߬ߞߎ߲߬.\nߌ ߓߘߊ߫ ߞߎߞߌߦߋ ߓߴߊ߬ ߟߊ߫.\nߊ߬ߟߎ߫ ߓߌ߬ߟߵߊ߬ ߟߊ߫ ߖߊ߰ߣߌ߲߫߸ ߏ߬ ߓߊ߰ ߞߍ߫ ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߫ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߣߌ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߞߎߘߊ߫ ߘߌ߫.",
        "nocookieslogin": "\n{{SITENAME}} ߦߋ߫ ߞߎߞߌߦߋ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫ ߞߊ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߜߊ߲߬ߞߎ߲߬.\nߌ ߓߘߊ߫ ߞߎߞߌߦߋ ߓߴߊ߬ ߟߊ߫.\nߊ߬ ߓߌ߬ߟߵߊ߬ ߟߊ߫ ߖߊ߰ߣߌ߲߫ ߞߵߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.",
+       "nocookiesfornew": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ ߣߌ߲߬ ߡߊ߫ ߛߌ߲ߘߌ߫ ߡߎߣߎ߲߬߸ ߓߊ ߊ߲ ߕߍ߫ ߛߋ߫ ߊ߬ ߓߐߛߎ߲ ߠߊߘߤߊ߬ ߟߊ߫. ߌ ߦߋ߫ ߘߍ߲߬ߞߣߍ߬ߦߴߊ߬ ߡߊ߬ ߞߏ߫ ߌ ߓߘߊ߫ ߞߎߞߌߦߋ ߓߌ߬ߟߵߊ߬ ߟߊ߫߸ ߞߐߜߍ ߣߌ߲߬ ߠߊߢߎ߲߫ ߞߊ߬ ߓߊ߲߫ ߞߵߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.",
+       "createacct-loginerror": "ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߓߊ߲߫ ߛߌ߲ߘߌ߫ ߟߊ߫ ߝߛߊߦߌ߫ ߞߏ߬ߣߌ߲߬ ߌ ߕߍߣߊ߬ ߛߋ߫ ߟߴߌ ߜߊ߲߬ߞߎ߲߬ ߠߊ߫ ߞߍߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬.ߖߊ߰ߣߌ߲߬ ߌ ߦߴߊ߬ ߡߌ߬ߘߵߊ߬ ߛߎ߲ ߖߊ߰ߣߌ߲߬ ߦߊ߲߬ [[Special:UserLogin|manual login]].",
        "noname": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߯ ߖߐ߲ߖߐ߲߫ ߟߊߘߊ߲߫ ߣߍ߲߫ ߕߴߌ ߓߟߏ߫.",
        "loginsuccesstitle": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "loginsuccess": "<strong>ߌ ߓߘߴߌ ߜߊ߲߬ߞߎ߲߬ {{SITENAME}} ߟߊ߫ ߕߊ߲߬ $1</strong>",
        "nosuchuser": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߌ߫ ߕߍ߫ ߕߐ߮ ߟߊ߫  \"$1\".\nߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߓߐߣߍ߲߫ ߦߋ߫ ߘߏ߫ ߟߊ߫. \nߌ ߟߊ߫ ߌ ߟߊ߫ ߛߓߍߟߌ ߝߛߍ߬ߝߛߍ߬߸ ߥߟߊ߫ [[Special:CreateAccount|create a new account]].",
+       "nosuchusershort": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߌ߫ ߕߍ߫ ߕߐ߮  \"$1\" ߟߊ߫.\nߌ ߟߊ߫ ߛߓߍߟߌ ߞߎ߬ߙߎ߲߬ߘߎ ߝߛߍ߬ߝߛߍ߬.",
        "nouserspecified": "ߌ ߞߊߞߊ߲߫ ߞߊ߬ ߕߐ߯ ߟߊߓߊ߯ߙߕߊ߫ ߞߋߟߋ߲߫ ߡߊߕߍ߰",
        "login-userblocked": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߣߌ߲߬ ߓߊ߬ߟߊ߲߬ߣߍ߲߫ ߠߋ߫. ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߊߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫.",
        "wrongpassword": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߓߍ߲߬ ߣߍ߲߬ ߕߍ߫ ߥߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߠߊߘߏ߲߬ߣߍ߲.\nߖߊ߰ߣߌ߲߬ ߌ ߦߴߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.",
        "passwordtoolong": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊ߲߫ ߞߊ߲߫ ߞߊ߬ ߖߊ߲߰ߧߊ߬ {{PLURAL:$1|ߛߓߍߘߋ߲ ߁|$1 ߛߓߍߘߋ߲ ߠߎ߬}}.",
        "passwordtoopopular": " ߝߘߏ߬ߓߊ߬ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߡߊߟߐ߲ߣߍ߲ ߠߎ߬ ߕߍ߫ ߣߊ߬ ߛߋ߫ ߟߊ߫ ߟߊߓߊ߯ߙߊߊ߫ ߟߊ߫. ߌ ߦߋ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߜߘߍ߫ ߛߎߥߊ߲ߘߌ߫ ߖߊ߰ߣߌ߲߬ ߡߍ߲ ߡߊߟߐ߲߫ ߜߏߡߊ߲߫.",
        "passwordinlargeblacklist": "ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߠߊߘߏ߲߬ߣߍ߲ ߦߋ߫ ߝߘߏ߬ߓߊ߬ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߡߊߟߐ߲ߣߍ߲ߓߊ ߟߎ߬ ߛߙߍߘߍ ߟߋ߬ ߘߐ߫.\nߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߦߙߋߞߋ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫.",
+       "password-name-match": "ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߦߋ߫ ߝߘߏ߬ ߌ ߕߐ߯ ߟߊߓߊ߯ߙߕߊ ߡߊ߬.",
+       "password-login-forbidden": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߣߌ߲߬ ߣߌ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ ߓߘߊ߫ ߟߊߕߐ߲߫.",
        "mailmypassword": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߦߟߍ߬ߡߊ߲߬",
        "passwordremindertitle": "{{SITENAME}} ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߎ߬ߡߊ߬ߞߎ߲߬ߡߊ ߞߎߘߊ",
        "noemail": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߛߌ߫ ߕߍ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ \"$1\" ߟߊ߫",
        "botpasswords-no-central-id": "ߖߐ߲߬ߛߊ߫ ߌ ߘߌ߫ ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊߓߊ߯ߙߊ߫߸ ߌ ߞߊ߫ ߞߊ߲߫ ߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲߬ ߕߊ߲ߓߊ߲ߓߐߣߍ߲ ߞߍ߫.",
        "botpasswords-existing": "ߕߋ߲߭ߕߋ߲߭ ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲",
        "botpasswords-createnew": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫",
+       "botpasswords-editexisting": "ߓߏߕ ߕߋ߲߬ߕߋ߲߬ ߕߊߡߌ߲ߞߊ߲ ߡߊߦߟߍ߬ߡߊ߲߬",
        "botpasswords-label-needsreset": "(ߕߊ߬ߡߌ߲߬ߞߊ߲ ߤߊ߬ߕߊ߬ߦߋ߬ߣߍ߲߫ ߡߝߊ߬ߟߋ߲߬ߠߌ߲ ߠߊ߫)",
        "botpasswords-label-appid": "ߓߏߕ ߕߐ߮:",
        "botpasswords-label-create": "ߊ߬ ߛߌ߲ߘߌ߫",
        "botpasswords-label-cancel": "ߊ߬ ߘߐߛߊ߬",
        "botpasswords-label-delete": "ߊ߬ ߖߏ߬ߛߌ߬",
        "botpasswords-label-resetpassword": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߦߟߍ߬ߡߊ߲߬",
+       "botpasswords-label-grants-column": "ߘߌ߬ߢߍ߬ ߓߘߊ߫ ߞߍ߫",
        "botpasswords-bad-appid": "ߓߏߕ ߕߐ߮  \"$1\" ߓߍ߲߬ ߣߍ߲߬ ߕߍ߫.",
        "botpasswords-insert-failed": "ߓߏߕ ߕߐ߮ ߟߊߘߏ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫  \"$1\" ߊ߬ ߕߎ߲߬ ߓߘߊ߫ ߟߊߘߏ߲߭ ߠߋ߬ ߓߊ߬؟",
        "botpasswords-update-failed": "ߓߏߕ ߕߐ߮ ߟߏ߲ߘߐߦߊߟߌ ߓߘߊ߫ ߗߌߙߏ߲߫  \"$1\" ߊ߬ ߓߘߊ߫ ߖߏ߬ߛߌ߫ ߟߋ߬ ߓߊ߬؟",
        "botpasswords-updated-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫.",
        "botpasswords-deleted-title": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߘߊ߫ ߖߏ߬ߛߌ߬",
        "botpasswords-deleted-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.",
+       "botpasswords-not-exist": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߐ߯ߟߊߣߍ߲߫ \"$2\" ߕߍ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ  \"$1\" ߓߟߏ߫",
+       "botpasswords-needs-reset": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲  \"$1\" ߓߏߕ ߕߐ߯ \"$2\" ߦߋ߫ {{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}  \"$1\" ߦߋ߫ ߡߊߦߟߍ߬ߡߊ߲߫.",
+       "botpasswords-locked": "ߌ ߕߍߣߊ߬ ߛߋߟߴߌ ߜߊ߲߬ߞߎ߲߬ ߠߊ߫ ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߘߌ߫ ߓߊ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߛߐ߰ߣߍ߲߫ ߠߋ߫.",
        "resetpass_forbidden": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߘߋ߲߬ ߠߊ߫.",
        "resetpass_forbidden-reason": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߟߋ߲߬ ߠߊ߫: $1",
        "resetpass-no-info": "ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߬ ߡߎߣߎ߲߬ ߞߣߊ߬ ߕߏ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫.",
        "template-protected": "(ߊ߬ ߡߊߞߊ߲ߞߊ߲ߣߍ߲߫ ߠߋ߬)",
        "template-semiprotected": "(ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ-ߝߊ߲߬ߞߋ߬ߟߋ߲߬ߡߊ)",
        "hiddencategories": "ߞߐߜߍ ߣߌ߲߬ ߦߋ߫ ߢߌ߲߬ ߠߎ߫ ߛߌ߲߬ߝߏ߲ ߠߋ߬ ߘߌ߫{{PLURAL:$1|}}",
+       "sectioneditnotsupported-text": "ߛߌ߰ߘߊ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߕߊ߲߬.",
        "permissionserrors": "ߝߌ߬ߟߌ߫ ߘߌ߬ߢߍ߬ߒߧߋ",
+       "permissionserrorstext": "ߌ ߟߊߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫ ߞߵߏ߬ ߞߍ߫߸ ߣߌ߲߬ ߠߊ߫ {{PLURAL:$1|ߛߊߓߎ|ߛߊߓߎ ߟߎ߬}}:",
        "permissionserrorstext-withaction": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ߬ ߛߌ߫ ߕߴߌ ߦߋ߫ ߞߊ߬ $2߸ {{PLURAL:$1|ߞߏߛߐ߲߬|ߟߎ߬ ߞߏߛߐ߲߬}}",
+       "contentmodelediterror": "ߌ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߛߌ߰ߘߊ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫߸ ߓߊߏ߬ ߞߣߐߘߐ ߛߎ߯ߦߊ ߦߋ߫ <code>$1</code> ߟߋ߬ ߘߌ߫߸ ߡߍ߲ ߦߋ߫ ߕߋ߲߬ߕߋ߲߬ ߞߣߐߘߐ ߛߎ߯ߦߊ ߝߘߏ߬ ߟߊ߫ ߞߐߜߍ <code>$2</code> ߘߐ߫.",
        "recreate-moveddeleted-warn": "<strong>ߌ ߖߊ߲߬ߕߏ߫: ߌ ߦߋ߫ ߞߐߜߍ ߘߏ߫ ߟߋ߬ ߟߊߘߊ߲߫ ߞߏ ߘߐ߫ ߣߌ߲߬߸ ߡߍ߲ ߖߏ߬ߛߌ߬ߣߍ߲߬ ߡߎߣߎ߲߬.</strong> \nߌ ߓߛߌ߬ߞߌ߬ ߕߐ߫ ߟߋ߬ ߛߍ߲߸ ߣߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߓߊ߲߫ ߠߊ߫. \nߞߐߜߍ ߣߌ߲߬ ߦߟߌߣߐ ߖߏ߬ߛߌ߬ߣߍ߲ ߣߴߊ߬ ߛߋ߲߬ߓߐ߬ߣߍ߲ ߠߎ߬ ߡߊߘߊ߲ߣߍ߲߫ ߦߊ߲߬ ߠߋ ߟߊ߬ߣߐ߰ߦߊ߬ߟߌ ߘߌ߫:",
        "moveddeleted-notice": "ߞߐߜߍ ߣߌ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.\nߖߏ߬ߛߌ߬ߟߌ߸ ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ߸ ߊ߬ ߣߌ߫ ߞߐߜߍ ߛߓߍߟߌ ߟߎ߬ ߛߋ߲߬ߓߐ߸ ߏ߬ ߟߎ߫ ߓߍ߯ ߡߊߛߐߣߍ߲߫ ߦߋ߫ ߘߎ߰ߟߊ ߘߐ߫.",
        "log-fulllog": "ߘߎ߲ߛߓߍ ߘߝߊߣߍ߲ ߦߋ߫",
        "postedit-confirmation-restored": "ߞߐߜߍ ߓߘߊ߫ ߓߊ߲߫ ߘߐߓߍ߲߬ ߠߊ߫.",
        "postedit-confirmation-saved": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߞߎ߲߬ߘߎ߬.",
        "postedit-confirmation-published": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߥߊ߲߬ߞߊ߫.",
+       "edit-already-exists": "ߌ ߕߴߛߋ߫ ߞߐߜߍ߫ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫ ߟߊ߫.\nߊ߬ ߦߋ߫ ߦߋ߲߬ ߞߘߐ߬ߡߊ߲߬.",
+       "defaultmessagetext": "ߓߐߛߎ߲ ߗߋߛߓߍ ߛߓߍߟߌ",
        "invalid-content-data": "ߞߣߐߘߐ ߓߟߏߡߟߊ ߓߍ߲߬ߓߊߟߌ",
        "content-not-allowed-here": "\"$1\" ߞߣߐߘߐ ߟߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ ߞߐߜߍ ߘߐ߫ [[:$2]] ߛߍ߲ߞߍߘߊ ߘߐ߫  \"$3\"",
        "editwarning-warning": "ߣߴߌ ߓߐ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߌ ߘߌ߫ ߓߣߐ߬ ߌ ߟߊ߫ ߡߊ߬ߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߞߍߣߍ߲ ߠߎ߬ ߓߍ߯ ߘߐ߫.\nߣߴߌ ߘߏ߲߬ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߞߍ߫ ߘߊ߫߸ ߌ ߘߌ߫ ߛߋ߫ ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ ߣߌ߲߬ ߓߐ߫ ߟߴߊ߬ ߟߊ߫  \"{{int:prefs-editing}}\" ߘߐ߫߸ ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߥߟߊ߬ߘߊ ߘߐ߫.",
        "revertmerge": "ߊ߬ ߓߐߢߐ߲߮ߞߊ߲߬",
        "history-title": "$1 ߡߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ",
        "difference-title": "ߘߊ߲߬ߝߘߊ߬ߓߐ ߡߍ߲ ߦߋ߫ ߡߛߊ߬ߦߌ߲߬ߠߌ߲ $1 ߕߍ߫",
+       "difference-title-multipage": "ߘߊ߲߬ߝߘߊ߬ߓߐ ߡߍ߲ ߦߋ߫ ߞߐߜߍ ߟߎ߬ ߕߍ߫ \"$1\" ߣߌ߫  \"$2\"",
+       "difference-multipage": "(ߘߊ߲߬ߝߘߊ߬ߓߐ ߡߍ߲ ߦߋ߫ ߞߐߜߍ ߟߎ߬ ߕߍ߫)",
        "lineno": "$1 ߛߌ߬ߕߊߙߌ:",
        "compareselectedversions": "ߘߟߊߡߌߘߊ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߟߊߢߐ߲߯ߡߊ߫",
+       "showhideselectedversions": "ߟߢߊ߬ߟߌ߬ ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߦߋߢߊ ߡߊߝߊ߬ߟߋ߲߬",
        "editundo": "ߊ߬ ߘߐߛߊ߬",
        "diff-empty": "(ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߕߴߊ߬ߟߎ߬ ߕߍ߫)",
        "diff-multi-sameuser": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} ߟߊ߬ߓߊ߰ߙߊ߬ ߞߋߟߋ߲ ߓߟߏ߫߸ ߏ߬ ߡߊ߫ ߦߌ߬ߘߊ߬)",
        "diff-multi-otherusers": "({{PLURAL:$1|ߕߍߟߐ ߡߊߛߊߦߌ߲߬ߞߏ߬ ߞߋߟߋ߲߫|ߕߍߟߐ ߡߊߛߊ߬ߦߌ߲}} {{PLURAL:$2|ߟߊߓߊ߯ߙߟߊ߫ ߘߏ߫ ߜߘߍ߫|ߟߊߓߊ߯ߙߟߊ ߟߎ߬}} ߏ߬ ߡߊ߫ ߟߊ߲ߞߣߍߡߊ߫)",
+       "diff-multi-manyusers": "({{PLURAL:$1|ߕߍߟߊߘߐ߫ ߟߢߊߟߌ߫ ߞߋߟߋ߲߫|$1 ߕߍߟߊߘߐ߫ ߟߢߊߟߌ ߟߎ߬}} ߞߊ߬ ߝߘߊ߫ {{PLURAL:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬}} ߦߙߌߞߊ ߟߎ߬ $2 ߡߊ߫ ߦߌ߬ߘߊ߬)",
+       "diff-paragraph-moved-tonew": "ߛߌ߬ߘߊ߰ߙߋ߲ ߠߎ߬ ߡߊ߫ ߛߋ߲߬ߓߐ߫. ߛߐ߲߬ߞߌ߲߬ߠߌ߲ ߞߍ߫ ߞߵߌ ߕߐ߬ߡߐ߲߬ ߞߊ߬ ߥߊ߫ ߘߌ߲߬ߞߌ߬ߙߊ߬ ߜߘߍ߫ ߘߐ߫.",
+       "diff-paragraph-moved-toold": "ߛߌ߬ߘߊ߰ߙߋ߲ ߓߘߊ߫ ߛߋ߲߬ߓߐ߫. ߛߐ߲߬ߞߌ߲߬ߠߌ߲ ߞߍ߫ ߞߵߌ ߕߐ߬ߡߐ߲߬ ߞߊ߬ ߥߊ߫ ߘߌ߲߬ߞߌ߬ߙߊ߬ ߞߘߐ ߘߐ߫.",
        "searchresults": "ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ ߟߎ߬",
+       "search-filter-title-prefix": "ߞߐߜߍ ߡߍ߲ ߠߎ߬ ߞߎ߲߬ߕߐ߮ ߦߋ߫ ߘߊߡߌ߬ߣߊ߬ ߟߊ߫  \"$1\" ߡߊ߬ ߏ߬ ߟߎ߫ ߟߋ߬ ߘߐߙߐ߲߫ ߢߌߣߌ߲ ߦߴߌ ߘߐ߫.",
        "search-filter-title-prefix-reset": "ߞߐߜߍ ߓߍ߯ ߢߌߣߌ߲߫",
        "searchresults-title": "ߣߌ߲߬ \"$1\" ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ",
        "prevn": "ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ {{PLURAL:$1|$1}}",
        "nextn": "ߟߊߕߎ߲߰ߠߊ {{PLURAL:$1|$1}}",
+       "prev-page": "ߞߐߜߍ ߢߍߕߊ",
+       "next-page": "ߞߐߜߍ߫ ߣߊ߬ߕߐ",
        "prevn-title": "ߢߝߍߕߊ $1 {{PLURAL:$1|result|results}}",
        "nextn-title": "ߢߍߕߊ $1 {{PLURAL:$1|ߞߐߖߋߓߌ}}",
        "shown-title": "ߞߐߜߍ߫ ߞߋ߬ߟߋ߲߬ߞߋ߬ߟߋ߲߬ߠߊ $1{{PLURAL:$1|ߞߐߝߟߌ |ߞߐߝߟߌ ߟߎ߬ }} ߦߌߘߊߞߊ߬",
        "search-category": "(ߦߌߟߡߊ $1)",
        "search-file-match": "(ߞߐߕߐ߮ ߞߣߐߘߐ ߓߘߊ߫ ߟߊߞߊ߬ߝߏ߬)",
        "search-suggest": "ߌ ߞߊ߲߫ ߦߋ߫ ߣߌ߲߬ ߠߋ߬ ߡߊ߬ $1",
+       "search-rewritten": "$1 ߞߐߝߟߌ ߦߌ߬ߘߊ ߦߴߌ ߘߐ߫. $2 ߢߌߣߌ߲߫ ߞߋߟߋ߲ߘߌ߫.",
+       "search-interwiki-caption": "ߖߊ߬ߕߋ߬ߘߐ߬ߛߌ߮ ߡߊ߬ߡߛߏ ߟߎ߬ ߞߐߝߟߌ",
+       "search-interwiki-default": "ߞߐߝߐߟߌ ߞߵߊ߬ ߕߊ߬ $1:",
+       "search-interwiki-more": "(ߡߊ߬ߞߊ߬ߝߏ߬ ߜߘߍ ߟߎ߬)",
        "search-interwiki-more-results": "ߞߐߝߟߌ߫ ߜߘߍ ߟߎ߬",
+       "search-relatedarticle": "ߓߌ߬ߟߊ߬ߢߐ߲߰ߡߊ",
+       "searchrelated": "ߓߌ߬ߟߊ߬ߢߐ߲߰ߡߊ",
        "searchall": "ߊ߬ ߓߍ߯",
        "search-showingresults": "{{PLURAL:$4|Result <strong>$1</strong> of <strong>$3</strong>|Results <strong>$1 – $2</strong> of <strong>$3</strong>}}",
        "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬.",
        "powersearch-legend": "ߢߌߣߌ߲ߠߌ߲ ߖߊ߲߬ߝߊ߬ߣߍ߲",
        "powersearch-ns": "ߊ߬ ߢߌߣߌ߲߫ ߕߐ߯ߛߓߍ ߞߣߍ ߘߐ߫.",
        "powersearch-togglelabel": "ߝߛߍ߬ߝߛߍ߬ߟߌ",
+       "powersearch-toggleall": "ߊ߬ ߓߍ߯",
+       "powersearch-togglenone": "ߝߏߦߌ߬",
+       "search-external": "ߞߐߞߊ߲߫ ߢߌߣߌ߲ߠߌ߲",
+       "search-error": "ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߓߘߴߊ߬ ߞߎ߲߬ߓߐ߫ ߞߵߌ ߕߏ߫ $1 ߢߌߣߌ߲ ߠߊ߫",
+       "search-warning": "ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ ߘߏ߫ ߓߘߴߊ߬ ߞߎ߲߬ߓߐ߫ ߞߵߌ ߕߏ߫ $1 ߢߌߣߌ߲ ߠߊ߫",
        "preferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
        "mypreferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
        "prefs-edits": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬ ߦߙߌߞߊ:",
        "prefs-watchlist-edits-max": "ߝߙߍߕߍ ߞߐߘߊ߲: ߁߀߀߀",
        "prefs-watchlist-token": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߖߐߟߐ߲ߞߐ",
        "prefs-watchlist-managetokens": "ߖߐߟߐ߲ߞߐ ߘߊߘߐߓߍ߲߭",
+       "prefs-misc": "ߜߘߍ ߟߎ߬",
        "prefs-resetpass": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߝߊ߬ߟߋ߲߬",
        "prefs-changeemail": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߡߊߝߊ߬ߟߋ߲߬ ߥߟߊ߫ ߌ ߦߴߊ߬ ߛߋ߲߬ߓߐ߫",
        "prefs-setemail": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߘߏ߫ ߟߊߘߏ߲߬",
        "prefs-email": "ߢߎߡߍߙߋ߲ ߞߏ߲ߘߏ ߛߎߥߊ߲ߘߟߌ",
+       "prefs-rendering": "ߟߊ߲ߞߣߍߡߊ",
        "saveprefs": "ߊ߬ ߟߊߞߎ߲߬ߘߎ߬",
+       "prefs-editing": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߴߌ ߘߐ߫",
+       "searchresultshead": "ߢߌߣߌ߲ߠߌ߲",
+       "stub-threshold-sample-link": "ߣߐ߰ߡߊ߲",
+       "stub-threshold-disabled": "ߊ߬ ߓߘߊ߫ ߓߐ߫ ߊ߬ ߟߊ߫.",
+       "recentchangesdays": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߠߊ߬ߓߊ߲ ߠߎ߬ ߦߌ߬ߘߊ߬ ߕߐ߬ ߟߏ߲ ߡߍ߲ ߠߎ߬ ߘߐ߫.",
+       "recentchangesdays-max": "{{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}} ߞߐߘߊ߲ $1",
+       "recentchangescount": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬ ߦߙߌߞߊ ߡߍ߲ ߦߌ߬ߘߊ߬ߕߊ ߦߋ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߠߊ߬ߓߊ߲ ߘߐ߫߸ ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߘߐ߫߸ ߊ߬ ߘߎ߲ߛߓߍ ߟߎ߬ ߘߐ߫߸ ߓߐߛߎ߲ ߓߟߏ߫.",
+       "prefs-help-recentchangescount": "ߝߙߍߕߍ ߞߐߘߊ߲: ߁߀߀߀",
+       "savedprefs": "ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߓߘߊ߫ ߟߊߞߎ߲߬ߘߎ߬.",
+       "savedrights": "ߌ ߟߊ߫ ߞߙߎ߫ ߟߊߓߊ߯ߙߕߊ {{GENDER:$1|$1}} ߓߘߊ߫ ߟߊߞߎ߲߬ߘߎ߬.",
+       "timezonelegend": "ߕߌ߲߬ߞߎߘߎ߲ ߕߎ߬ߡߊ",
+       "localtime": "ߕߌ߲߬ߞߎߘߎ߲ ߕߎ߬ߡߊ:",
+       "timezoneuseserverdefault": "ߥߞߌ߫ ߓߐߛߎ߲ ($1) ߠߊߓߊ߯ߙߊ߫",
+       "timezone-useoffset-placeholder": "ߞߏߟߊߒߞߏߡߊ ߡߐ߬ߟߐ߲: : ߴߴ߀߇:߀߀ߴߴ ߥߟߊ߫ ߴߴ߀߁:߀߀ߴߴ",
+       "servertime": "ߡߊ߬ߛߐ߬ߟߊ ߕߎ߬ߡߊ߬ߙߋ߲:",
+       "guesstimezone": "ߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߛߏ߲߯ߓߊߟߊ߲ ߝߍ߬",
        "timezoneregion-africa": "ߊߝߙߌߞߌ߬",
+       "timezoneregion-america": "ߊߡߋߙߌߞߌ߬",
+       "timezoneregion-antarctica": "ߜߟߌߟߌߓߊ",
+       "timezoneregion-arctic": "ߞߐ߬ߘߎ߰ߞߊ",
+       "timezoneregion-asia": "ߊߖ߭ߌ߫",
+       "timezoneregion-atlantic": "ߟߌ߲ߓߊ߲߫ ߡߊ߲ߞߊ߲",
+       "timezoneregion-australia": "ߏߛߑߕߙߊߟߌ߫",
+       "timezoneregion-europe": "ߋߙߐߔߎ߬",
+       "timezoneregion-indian": "ߌ߲ߘߌ߫ ߟߌ߲ߓߊ߲",
+       "timezoneregion-pacific": "ߖߐ߮ ߟߌ߲ߓߊ߲",
+       "allowemail": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߕߐ߭ ߟߎ߬ ߟߊߘߌ߬ߢߍ߬ ߢߎߡߍߙߋ߲߫ ߗߋ ߘߐ߫ ߒ ߡߊ߬",
+       "email-allow-new-users-label": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߞߎߘߊ ߟߎ߬ߟߊߘߌ߬ߢߍ߬ ߢߎߡߍߙߋ߲߫ ߗߋ ߘߐ߫ ߒ ߡߊ߬",
+       "prefs-searchoptions": "ߢߌߣߌ߲ߠߌ߲",
+       "prefs-namespaces": "ߕߐ߯ ߛߓߍ ߞߣߍ",
+       "default": "ߓߐߛߎ߲",
+       "prefs-files": "ߞߐߕߐ߮ ߟߎ߬",
+       "prefs-custom-css": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ CSS",
+       "prefs-custom-json": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ JSON",
+       "prefs-custom-js": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ JavaScript",
+       "prefs-emailconfirm-label": "ߢߎߡߍߙߋ߲ ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ:",
+       "youremail": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ:",
+       "username": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮}}:",
+       "prefs-memberingroups": "{{GENDER:$2|ߛߌ߲߬ߝߏ߲}} {{PLURAL:$1|ߞߙߎ|ߞߙߎ ߟߎ߬}} ߘߐ߫:",
+       "group-membership-link-with-expiry": "$1 (ߝߏ߫ $2)",
+       "prefs-registration": "ߕߐ߯ߛߓߍߟߌ ߕߎ߬ߡߊ:",
+       "yourrealname": "ߕߐ߯ ߓߘߍ:",
+       "yourlanguage": "ߞߊ߲:",
+       "yourvariant": "ߞߣߐߘߐ ߞߊ߲ ߠߎ߬ ߓߐߣߍ߲߫ ߢߐ߲߮ ߡߊ߬:",
+       "yournick": "ߞߟߊ߬ߣߐ߰ ߞߎߘߊ:",
+       "badsig": "ߞߟߊ߬ߣߐ߮ ߢߊ߲ߞߌߛߊ߲ ߓߍ߲߬ߓߊߟߌ.\nHTLM ߘߎ߲ߛߓߍ ߝߛߍ߬ߝߛߍ߬.",
+       "badsiglength": "ߌ ߟߊ߫ ߞߟߊ߬ߣߐ߮ ߖߊ߰ߡߊ߲߬ ߞߏߖߎ߰.\nߊ߬ ߡߊ߲ߞߊ߲߫ ߞߊ߬ ߕߊ߬ߡߌ߲߬ $1 {{PLURAL:$1|ߛߓߍߟߌ|ߛߓߍߟߌ ߟߎ߬}} ߖߊ߲߰ߧߊ ߟߊ߫.",
+       "yourgender": "ߌ ߕߐ߮ ߛߓߍ߫ ߢߊ ߢߎ߬ߡߊ߲߬ ߞߊߘߴߌ ߦߋ߫؟",
+       "gender-male": "ߊ߬ (ߗߍ߭) ߓߘߊ߫ ߥߞߌ߫ ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "gender-female": "ߊ߬ (ߡߏ߬ߛߏ) ߓߘߊ߫ ߥߞߌ߫ ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "email": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ",
+       "prefs-help-email-required": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߞߊ߬ߣߌ߲߬ߣߍ߲߫.",
+       "prefs-info": "ߞߎ߲߬ߠߊ߬ߝߎߟߋ߲ ߓߊߖߎ",
+       "prefs-i18n": "ߡߊ߲߬ߕߏ߬ߕߍ߬ߦߊ߬ߟߌ",
+       "prefs-signature": "ߞߟߊ߬ߣߐ߮",
+       "prefs-dateformat": "ߕߎ߬ߡߊ߬ߘߊ ߖߙߎߡߎ߲",
+       "prefs-timeoffset": "ߕߎ߬ߡߊ ߘߐߓߍ߲߬",
+       "prefs-advancedediting": "ߢߣߊߕߊߟߌ ߞߙߎߞߙߍ",
+       "prefs-developertools": "ߟߊ߬ߥߙߎ߬ߞߌ߬ߟߊ ߖߐ߯ߙߊ߲ ߠߎ߬",
+       "prefs-editor": "ߛߓߍߦߟߊ",
+       "prefs-preview": "ߟߊ߬ߕߎ߲߰ߠߊ",
+       "prefs-advancedrc": "ߢߣߊߕߊߟߌ ߖߊ߲߬ߝߊ߬ߣߍ߲",
+       "prefs-advancedrendering": "ߢߣߊߕߊߟߌ ߖߊ߲߬ߝߊ߬ߣߍ߲",
+       "prefs-advancedsearchoptions": "ߢߣߊߕߊߟߌ ߖߊ߲߬ߝߊ߬ߣߍ߲",
+       "prefs-advancedwatchlist": "ߢߣߊߕߊߟߌ ߖߊ߲߬ߝߊ߬ߣߍ߲",
+       "prefs-displayrc": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ ߢߣߊߕߊߟߌ",
+       "prefs-displaywatchlist": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ ߢߣߊߕߊߟߌ",
+       "prefs-changesrc": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߦߌ߬ߘߊ߬",
+       "prefs-changeswatchlist": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߦߌ߬ߘߊ߬",
+       "prefs-pageswatchlist": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬",
+       "prefs-tokenwatchlist": "ߖߐߟߐ߲ߞߐ",
+       "prefs-help-prefershttps": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߣߌ߲߬ ߘߴߊ߬ ߝߏ߲߬ߝߏ߲ ߟߴߌ ߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߣߊ߬ߕߐ ߞߊ߲߬.",
+       "userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ",
+       "userrights-lookup-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫",
+       "userrights-user-editname": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߘߏ߫ ߟߊߘߏ߲߬:",
+       "editusergroup": "ߞߙߎ߫ ߟߊߓߊ߯ߙߕߊ ߟߊߢߎ߲߫",
+       "editinguser": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߡߊߦߟߍߡߊ߲ ߦߴߌ ߘߐ߫ <strong> [[User:$1|$1]]</strong> $2",
+       "viewinguserrights": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߤߊߞߍ ߦߌ߬ߘߊ ߦߴߌ ߘߐ߫ <strong> [[User:$1|$1]]</strong> $2",
+       "userrights-editusergroup": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߙߎ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "userrights-viewusergroup": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߙߎ ߡߊߦߟߍ߬ߡߊ߲ ߦߴߌ ߘߐ߫",
+       "saveusergroups": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߙߎ ߟߊߞߎ߲߬ߘߎ߬",
+       "userrights-groupsmember": "ߛߌ߲߬ߝߏ߲ ߠߎ߬:",
+       "userrights-reason": "ߊ߬ ߛߊߓߎ:",
+       "userrights-nodatabase": "ߓߟߏߡߟߊ ߝߊ߲ $1 ߕߴߦߋ߲߬ ߥߟߴߊ߬ ߕߍ߫ ߕߌ߲߬ߞߎ߬ߘߎ߲߬ߡߊ߬ ߘߌ߫.",
+       "userrights-changeable-col": "ߌ ߘߌ߫ ߛߋ߫ ߞߙߎ ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫",
+       "userrights-unchangeable-col": "ߌ ߕߴߛߋ߫ ߞߙߎ ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫",
+       "userrights-expiry-current": "ߊ߬ ߛߕߊ ߓߘߊ߫ ߝߊ߫ $1",
+       "userrights-expiry-none": "ߊ߬ ߛߕߊ ߡߊ߫ ߝߊ߫ ߡߎߣߎ߲߬",
+       "userrights-expiry": "ߊ߬ ߛߕߊ ߓߘߊ߫ ߝߊ߫:",
+       "userrights-expiry-existing": "ߕߋ߲߭ߕߋ߲߭ ߛߕߊߝߊ߫ ߕߎߡߊ: $3߸ $2",
+       "userrights-expiry-othertime": "ߕߎ߬ߡߊ߬ ߜߘߍ:",
+       "userrights-expiry-options": "ߕߟߋ߬ ߁: ߕߟߋ߬ ߁߸ ߞߎ߲߬ߢߐ߰ ߁: ߞߎ߲߬ߢߐ߰ ߁߸ ߞߊߙߏ߫ ߁: ߞߊߙߏ߫ ߁߸ ߞߊߙߏ߫ ߃: ߞߊߙߏ߫ ߃߸ ߞߊߙߏ߫ ߆: ߞߊߙߏ߫ ߆߸ ߛߊ߲߬ ߁: ߛߊ߲߬ ߁",
+       "group": "ߞߙߎ:",
+       "group-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ",
+       "group-autoconfirmed": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߬ ߞߍߒߖߘߍߦߋ߫ ߟߊߛߙߋߦߊߣߍ߲",
        "group-bot": "ߓߏߕ",
        "group-sysop": "ߞߎ߲߬ߠߊ߬ߛߌ߰ߟߊ",
+       "group-bureaucrat": "ߛߓߍߘߟߊߡߐ߮",
+       "group-all": "(ߊ߬ ߓߍ߯)",
+       "group-user-member": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}",
+       "group-autoconfirmed-member": "{{GENDER:$1|ߞߍߒߖߘߍߦߋ߫ ߟߊߛߙߋߦߊߟߌ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}",
+       "group-bot-member": "{{GENDER:$1|ߓߏߕ}}",
+       "group-bureaucrat-member": "{{GENDER:$1|ߛߓߍߘߟߊߡߐ߮}}",
+       "grouppage-user": "{{ns:project}}: ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ",
        "grouppage-bot": "{{ns:project}}:ߓߏߕ",
        "grouppage-sysop": "{{ns:project}}:ߡߊ߬ߡߙߊ߬ߟߌ߬ߟߊ",
+       "right-read": "ߞߐߜߍ ߘߐߞߊ߬ߙߊ߲߬",
+       "right-edit": "ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-createpage": "ߞߐߜߍ ߘߏ߫ ߛߌ߲ߘߌ߫ (ߡߍ߲ ߕߍ߫ ߓߊ߬ߘߏ߬ߓߊ߬ߘߌ߬ߦߊ߬ ߞߐߜߍ ߝߋ߲߫ ߘߌ߫)",
+       "right-createtalk": "ߓߊ߬ߘߏ߬ߓߊ߬ߘߌ߬ߦߊ߬ ߞߐߜߍ ߛߌ߲ߘߌ߫",
+       "right-createaccount": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ߫ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫",
+       "right-minoredit": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߐ߬ߣߐ߬ ߡߌ߬ߛߍ߬ߡߊ߲ ߘߌ߫",
+       "right-move": "ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
+       "right-move-subpages": "ߞߐߜߍ ߛߋ߲߬ߓߐ߫ ߊ߬ߟߎ߬ ߟߊ߫ ߞߐߜߍߙߋ߲ ߠߎ߬ ߘߐ߫",
+       "right-move-categorypages": "ߦߌߟߡߊ߫ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
+       "right-movefile": "ߞߐߕߐ߮ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
+       "right-upload": "ߞߐߕߐ߮ ߟߎ߬ ߟߊߦߟߍ߬",
        "right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
+       "right-delete": "ߞߐߜߍ ߟߎ߬ ߖߏ߰ߛߌ߬",
+       "right-bigdelete": "ߞߐߜߍ߫ ߘߝߐ߬ ߓߟߋ߬ߓߟߋ߬ߡߊ ߟߎ߬ ߖߏ߰ߛߌ߬",
+       "right-browsearchive": "ߞߐߜߍ߫ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߢߌߣߌ߲߫",
+       "right-undelete": "ߞߐߜߍ ߖߏ߰ߛߌ߬ߣߍ߲ ߓߐ߫",
+       "right-suppressionlog": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߎ߲ߛߓߍ ߟߎ߬ ߦߋ߫",
+       "right-block": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߓߊ߬ߟߌ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߊ߬",
+       "right-blockemail": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߓߊ߬ߟߌ߬ ߢߎߡߍߙߋ߲ ߗߋߟߌ ߡߊ߬",
+       "right-hideuser": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߓߊ߬ߟߌ߬߸ ߊ߬ ߢߡߊߘߏ߲߰ ߖߊ߬ߡߊ ߡߊ߬.",
+       "right-unblockself": "ߴ ߖߍ߬ߘߍ ߓߊ߬ߟߌ߬ߣߍ߲ ߓߐ߫",
+       "right-editcontentmodel": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߡߊߝߊ߬ߟߋ߲߬",
+       "right-editusercss": "CSS ߞߐߕߐ߮ ߘߏ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-edituserjson": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ CSS ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-edituserjs": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editsitecss": "ߞߍߦߙߐ ߞߣߍ CSS ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editsitejson": "ߞߍߦߙߐ ߞߣߍ JSON ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editsitejs": "ߞߍߦߙߐ ߞߣߍ JavaScript ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editmyusercss": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editmyuserjson": "ߌ ߖߍ߬ߘߍ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-viewmywatchlist": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫",
+       "right-editmyoptions": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߟߊߝߌߛߦߊߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫",
+       "right-mergehistory": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
+       "right-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-userrights-interwiki": "ߥߞߌ ߘߏ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-siteadmin": "ߓߟߏߡߟߊ ߝߊ߲ ߣߍ߰ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߟߊߞߊ߬",
+       "right-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߡߊ߬",
+       "grant-group-email": "ߢߎߡߍߙߋ߲ ߗߋ߫",
+       "grant-createaccount": "ߖߊ߬ߕߋ߬ߘߊ ߘߏ߫ ߛߌ߲ߘߌ߫",
+       "grant-createeditmovepage": "ߞߐߜߍ ߛߌ߲ߘߌ߫߸ ߡߊߦߟߍ߬ߡߊ߲߫߸ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߛߋ߲߬ߓߐ߫",
+       "grant-editinterface": "MediaWiki ߕߐ߯ߛߓߍ ߞߣߍ ߡߊߦߟߍ߬ߡߊ߲߫ ߊ߬ ߣߌ߫ ߞߍߦߙߐ ߞߣߍ/ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ JSON",
+       "grant-editmycssjs": "ߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ CSS/JSON/JavaScript ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-editmyoptions": "ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ ߣߌ߫ JSON ߛߏ߯ߙߏߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-editmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-editsiteconfig": "ߞߍߦߙߐ ߞߣߍ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ CSS/JS ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-editpage": "ߞߐߜߍ߫ ߓߍߓߊ߮ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-editprotected": "ߞߐߜߍ߫ ߟߊߞߊ߲ߘߊߣߍ߲ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "grant-highvolume": "ߢߊ߲ߞߊ߲-ߛߊ߲ߘߐߕߊ ߡߊߦߟߍߡߊ߲ ߦߴߌ ߘߐ߫",
+       "grant-privateinfo": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߟߊߛߐ߬ߘߐ߲߬",
+       "grant-protect": "ߞߐߜߍ ߟߎ߬ ߟߊߞߊ߲ߘߊ߫ ߊ߬ ߣߌ߫ ߞߵߊ߬ߟߎ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߓߐ߫",
+       "grant-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߡߊ߬",
+       "grant-uploadeditmovefile": "ߞߐߕߐ߮ ߟߊߦߟߍ߬߸ ߣߐ߬ߘߐߓߌ߬ߟߊ߬߸ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߛߋ߲߬ߓߐ߫",
+       "grant-uploadfile": "ߞߐߕߐ߮ ߞߎߘߊ߫ ߟߊߦߟߍ߬",
+       "grant-basic": "ߤߊߞߍ ߓߊߖߎߟߞߊ",
+       "grant-viewdeleted": "ߞߐߜߍ ߣߌ߫ ߞߐߕߐ߮ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߦߋ߫",
+       "grant-viewmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫",
        "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬",
+       "newuserlogpagetext": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߘߎ߲ߛߓߍ߫ ߛߌ߲ߘߌߣߍ߲ ߘߏ߫ ߟߋ߬ ߦߋ߫ ߣߌ߲߬.",
        "rightslog": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߜߊ߲߬ߞߎ߲߬ ߢߊ߬ ߓߘߍ",
+       "action-read": "ߞߐߜߍ ߣߌ߲߬ ߘߐߞߊ߬ߙߊ߲߬",
        "action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬",
+       "action-createpage": "ߞߐߜߍ ߣߌ߲߬ ߛߌ߲ߘߌ߫",
+       "action-createtalk": "ߘߊߘߐߖߊߥߏ߫ ߞߐߜߍ ߣߌ߲߬ ߛߌ߲ߘߌ߫",
        "action-createaccount": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ ߣߌ߲߬ ߠߊߘߊ߲߫",
+       "action-autocreateaccount": "ߞߐߞߊ߲ߝߊ߲ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߖߊߕߋߘߊ ߣߌ߲߬ ߛߌ߲ߘߌ߫ ߞߍߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬",
+       "action-history": "ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߘߐ߬ߝߐ ߦߋ߫",
+       "action-minoredit": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߣߐ߬ߣߐ߬ ߢߟߋߢߟߋ ߘߌ߫",
+       "action-move": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫",
+       "action-move-subpages": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫߸ ߊ߬ ߣߴߊ߬ ߞߐߜߍߙߋ߲ ߠߎ߬",
+       "action-move-categorypages": "ߦߌߟߡߊ߫ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
+       "action-movefile": "ߞߐߕߐ߮ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫",
+       "action-upload": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬",
+       "action-reupload": "ߞߐߕߐ߯ ߓߍߓߊ߮ ߣߌ߲߬ ߥߦߊ߬",
+       "action-upload_by_url": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬ ߞߊ߬ ߓߐ߫ URL ߘߐ߫",
+       "action-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
+       "action-delete": "ߞߐߜߍ ߣߌ߲߬ ߖߏ߰ߛߌ߬",
+       "action-deleterevision": "ߟߢߊ߬ߟߌ ߟߎ߬ ߖߏ߬ߛߌ߬",
+       "action-deletedhistory": "ߞߐߜߍ ߟߎ߬ ߖߏ߰ߛߌ߬ߟߌ ߘߐ߬ߝߐ ߦߋ߫",
+       "action-browsearchive": "ߞߐߜߍ߬ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߢߌߣߌ߲߫",
+       "action-undelete": "ߞߐߜߍ ߖߏ߰ߛߌ߬ߓߊߟߌ ߟߎ߬",
+       "action-suppressionlog": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߎ߲ߛߓߍ ߣߌ߲߬ ߦߋ߫",
+       "action-block": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߣߌ߲߬ ߓߊ߬ߟߌ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߊ߬",
+       "action-protect": "ߞߐߜߍ ߣߌ߲߬ ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ߬ ߞߛߊߞߊ ߡߊߝߊ߬ߟߋ߲߬",
+       "action-import": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߥߞߌ ߕߐ߭ ߟߎ߬ ߘߐ߫",
+       "action-importupload": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬ ߘߐ߫",
+       "action-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫",
+       "action-mergehistory": "ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
+       "action-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-userrights-interwiki": "ߥߞߌ ߘߏ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-siteadmin": "ߓߟߏߡߟߊ ߝߊ߲ ߣߍ߰ ߥߟߊ߫ ߞߵߊ߬ ߣߍ߰ߣߍ߲ ߓߐ߫",
+       "action-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫",
+       "action-editmyoptions": "ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-viewmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫",
+       "action-viewmyprivateinfo": "ߌ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߟߎ߬ ߦߋ߫",
+       "action-editmyprivateinfo": "ߌ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editcontentmodel": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editusercss": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-edituserjson": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-edituserjs": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editsitecss": "ߞߍߦߙߐ ߞߣߍ CSS ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editsitejson": "ߞߍߦߙߐ ߞߣߍ JSON ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editsitejs": "ߞߍߦߙߐ ߞߣߍ JavaScript ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editmyusercss": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editmyuserjson": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "action-viewsuppressed": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ ߟߢߊ߬ߟߌ߬ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬ ߦߋ߫",
        "enhancedrc-history": "ߕߊ߬ߡߌ߲߬ߣߍ߲",
        "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬",
        "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ",
        "recentchanges-label-plusminus": "ߞߐߜߍ ߢߊ߲ߞߊ߲ ߓߘߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫ ߞߵߊ߬ ߝߌ߬ߘߊ߲ ߦߙߌߞߊ ߣߌ߲߬ ߘߌ߫",
        "recentchanges-legend-heading": "<strong>ߡߊ߬ߛߙߋ:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ߣߌ߲߬ ߝߣߊ߫ ߦߋ߫ \n[[Special:NewPages|list of new pages]])",
+       "recentchanges-submit": "ߊ߬ ߦߌ߬ߘߊ߬",
+       "rcfilters-tag-remove": "$1 ߛߋ߲߬ߓߐ߫",
+       "rcfilters-legend-heading": "<strong>ߟߊ߬ߘߛߏ߬ߟߌ ߛߙߍߘߍ</strong>",
+       "rcfilters-other-review-tools": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߖߐ߯ߙߊ߲ ߘߏ ߟߎ߬",
+       "rcfilters-group-results-by-page": "ߞߙߎ ߞߐߝߟߌ ߞߐߜߍ ߡߊ߬",
+       "rcfilters-activefilters-hide": "ߊ߬ ߢߡߊߘߏ߲߰",
+       "rcfilters-activefilters-show": "ߊ߬ ߦߌ߬ߘߊ߬",
+       "rcfilters-limit-title": "ߞߐߝߟߌ ߡߍ߲ ߠߎ߬ ߦߌ߬ߘߊ߬ߕߊ ߦߋ߫",
+       "rcfilters-limit-and-date-label": "$1{{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬}}߸ $2",
+       "rcfilters-date-popup-title": "ߕߎ߬ߡߊ ߣߌ߫ ߥߎ߬ߛߎ ߡߍ߲ ߠߎ߬ ߢߌߣߌ߲ߕߊ ߦߋ߫",
+       "rcfilters-days-title": "ߟߏ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬",
+       "rcfilters-hours-title": "ߕߎ߬ߡߊ߬ߙߋ߲߫ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬",
+       "rcfilters-days-show-days": "$1 {{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}}",
+       "rcfilters-days-show-hours": "$1 {{PLURAL:$1|ߕߎ߬ߡߊ߬ߙߋ߲|ߕߎ߬ߡߊ߬ߙߋ߲ ߠߎ߬}}",
+       "rcfilters-highlighted-filters-list": "ߡߊߦߋߙߋ߲ߣߍ߲:$1",
+       "rcfilters-quickfilters": "ߞߎ߲߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߠߎ߬",
+       "rcfilters-quickfilters-placeholder-title": "ߛߌ߲ߘߌߣߍ߲ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߬ ߕߍ߫ ߡߎߣߎ߲߬",
+       "rcfilters-savedqueries-defaultlabel": "ߞߎ߲߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߠߎ߬",
+       "rcfilters-filter-user-experience-level-learner-label": "ߞߊ߬ߙߊ߲߬ߠߊ ߟߎ߬",
+       "rcfilters-filtergroup-watchlist": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߞߐߜߍ ߟߎ߬",
+       "rcfilters-filter-watchlist-watched-label": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫",
        "rcnotefrom": "ߘߎ߰ߟߊ ߘߐ߫ {{PLURAL:$5|is the change|are the changes}} ߞߊ߬ߦߌ߯ <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
        "rclistfrom": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߦߌ߬ߘߊ ߘߊߡߌ߬ߣߊ߬ ߣߌ߲߭ ߡߊ߬ $2, $3",
        "rcshowhideminor": "$1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲",
        "recentchangeslinked-to": "ߞߐߜߍ ߛߘߌ߬ߜߋ߲ ߠߎ߬ ߦߌ߬ߘߊ߬߸ ߞߊ߬ ߞߐߜߍ ߣߌ߬ ߞߋߟߋ߲ߘߌ߫",
        "upload": "ߞߐߕߐ߮ ߟߊߦߟߍ",
        "uploadlogpage": "ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߘߏ߫ ߟߊߦߟߍ߬",
+       "filename": "ߞߐߕߐ߮ ߕߐ߮",
        "filedesc": "ߟߊߘߛߏߣߍ߲",
+       "fileuploadsummary": "ߟߊ߬ߘߛߏ߬ߟߌ:",
+       "filereuploadsummary": "ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲:",
+       "filesource": "ߛߎ߲:",
+       "savefile": "ߞߐߕߐ߮ ߟߊߞߎ߲߬ߘߎ߬",
+       "upload-dialog-title": "ߞߐߕߐ߮ ߟߊߦߟߍ߬",
+       "upload-dialog-button-cancel": "ߊ߬ ߘߐߛߊ߬",
+       "upload-dialog-button-back": "ߌ ߞߐߛߊ߬ߦߌ߬",
        "license": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫:",
        "license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫",
        "imgfile": "ߞߐߕߐ߮",
        "filehist-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ",
        "filehist-dimensions": "ߛߎߡߊ߲ߘߐ",
        "filehist-comment": "ߞߊ߲߬ߝߐߟߌ",
-       "imagelinks": "ߞߐߕߐ߮ ߟߊߓߊ߯ߙߊ",
+       "imagelinks": "ߞߐߕߐ߮ ߟߊߓߊ߯ߙߊߟߌ",
        "linkstoimage": "ߞߐߕߐ߮ ߣߌ߲߬ {{PLURAL:$1|ߞߐߜߍ ߟߎ߬|$1 ߞߐߜߍ ߟߎ߬}}:",
        "linkstoimage-more": "ߞߐߕߐ߮ ߣߌ߲߬ $1 {{PLURAL:$1|page uses|pages use}} ߠߊߓߊ߯ߙߊߓߊ߮ ߞߊߛߌߦߊ߫.\nߛߙߍߘߍ ߢߌ߲߬ ߠߎ߬ ߦߋ߫ {{PLURAL:$1|first page|first $1 pages}} ߞߐߕߐ߮ ߣߌ߲߬ ߞߋߟߋ߲߫ ߠߊߓߊ߯ߙߊߓߊ߮ ߟߎ߬ ߛߙߍߘߍ ߟߋ߬ ߦߌ߬ߘߊ߬ ߟߊ߫.\nߛߘߌ߬ߜߋ߲߬ [[Special:WhatLinksHere/$2|full list]] ߓߟߏߡߊߞߊ߬ߣߍ߲ ߦߋ߫ ߦߋ߲߬.",
        "nolinkstoimage": " ߞߐߜߍ߫ ߛߌ߫ ߡߊ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ߫ ߡߎߣߎ߲߬",
        "metadata-fields": "ߟߐ߲ߕߊߞߐ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߣߍ ߡߍ߲ ߦߋ߫ ߗߋߛߓߍ ߣߌ߲߬ ߘߐ߫߸ ߏ߬ ߘߌ߫ ߣߊ߬ ߥߟߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߐߜߍ ߘߐ߫ ߣߌ߫ ߟߐ߲ߕߊߞߐ߫ ߥߟߊ߬ߟߋ߲ ߠߊߘߐ߯ߦߊ߫ ߘߊ߫. ߊ߬ ߕߐ߭ ߟߎ߬ ߢߡߊߘߏ߲߰ߣߍ߲ ߘߌ߫ ߕߏ߫ ߝߍ߭ ߞߏߛߐ߲߬.\n•ߊ߬ ߞߍ߫ \n•ߛߎ߯ߦߊ \n•ߕߎ߬ߡߊ߬ߘߊ ߣߌ߫ ߕߎ߬ߡߊ߬ߙߋ߲߫ ߓߐߛߎ߲ߡߊ \n•ߟߊ߬ߝߏߦߌ ߕߎ߬ߡߊ߬ߘߊ߬ ߖߐ߲ߖߐ߲ \n•ߞ ߝߙߍߕߍ \n•ߡ.ߛ.ߛ ߞߊߟߌߦߊ ߡߐ߬ߟߐ߲߬ߦߊ߬ߟߌ \n•ߕߊߞߎ߲ߡߊ ߥߊ߲߬ߥߊ߲ \n•ߞߎ߬ߛߊ߲ \n•ߓߊߦߟߍߡߊ߲ ߤߊߞߍ  ߘߞߖ \n•ߖߌ߬ߦߊ߬ߓߍ ߞߊ߲߬ߛߓߍ\n•ߘߟߊߕߍ߮ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߘߎ߰ߕߍߟߍ߲ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߞߐߓߋ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)",
        "namespacesall": "ߊ߬ ߓߍ߯",
        "monthsall": "ߡߎ߰ߡߍ",
+       "parentheses-start": "⸜",
+       "parentheses-end": "⸝",
        "imgmultipagenext": "ߞߐߜߍ ߣߊ߬ߕߐ ←",
        "imgmultigo": "ߥߊ߫߹",
        "imgmultigoto": "ߥߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬ $1",
index 8e0b8d4..323980d 100644 (file)
        "session_fail_preview_html": "'''କ୍ଷମା କରିବେ! ଅବଧି ସରି ଯିବାରୁ ଡାଟା ନଷ୍ଟ ହୋଇଥିବା ହେତୁ ଆପଣଙ୍କ ସମ୍ପାଦନା ମିଳିପାରିଲା ନାହିଁ ।'''\n\n''କାରଣ {{SITENAME}} ରେ ଖାଲି HTML ସଚଳ କରାଯାଇଛି, JavaScript ଆକ୍ରମଣରୁ ବଞ୍ଚିବା ପାଇଁ ସାଇତା ଆଗରୁ ଦେଖଣା ଲୁଛାଯାଇଛି''\n\n'''ଯଦି ଏହା ଏକ ବୈଧ ସମ୍ପାଦନା ଚେଷ୍ଟା, ତେବେ ଆଉଥରେ ଚେଷ୍ଟା କରନ୍ତୁ ।'''\nତଥାପି ଯଦି ଏହା କାମ ନକରେ, ତେବେ [[Special:UserLogout|ଲଗଆଉଟ]] କରି ଆଉଥରେ ଲଗ ଇନ କରନ୍ତୁ ।",
        "token_suffix_mismatch": "'''ଆପଣଙ୍କ ସମ୍ପାଦନା ନାକଚ କରିଦିଆଗଲା କାରଣ ଆପଣଙ୍କ ଅପରପକ୍ଷ ସମ୍ପାଦନାରେ ଭୁଲ ବିସ୍ମୟସୂଚକ ଚିହ୍ନ ଦେଇଦେଇଛି ।'''\nପୃଷ୍ଠା ଲେଖାରେ ଭୁଲ ଥିବାରୁ ଆପଣଙ୍କ ସମ୍ପାଦନାକୁ ନାକଚ କରିଦିଆଗଲା ।\nଆପଣ ଏକ ୱେବ-ରେ ଥିବା ଅଜଣା ପ୍ରକ୍ସି ସାଇଟ କରି  ବ୍ୟବହାର କରୁଥିଲେ ଏପରି ହୋଇଥାଏ ।",
        "edit_form_incomplete": "'''ସମ୍ପାଦନାର କେତେକ ଭାଗ ସର୍ଭର ଠେଇଁ ପହଞ୍ଚିଲା ନାହିଁ; ଭଲକରି ପରଖିନିଅନ୍ତୁ ଯେ ନିଜ ସମ୍ପାଦନା ସବୁ ଅକ୍ଷତ କି ନାହିଁ ଓ ଆଉଥରେ ଚେଷ୍ଟା କରନ୍ତୁ ।'''",
-       "editing": "$1 କୁ ବଦଳାଉଛି",
+       "editing": "$1କୁ ବଦଳାଉଛି",
        "creating": "$1କୁ ତିଆରି କରୁଛି",
        "editingsection": "$1 (ଭାଗ)କୁ ବଦଳାଇବେ",
        "editingcomment": "$1 (ନୂଆ ଭାଗ)କୁ ବଦଳାଉଛୁ",
        "mergehistory-invalid-source": "ମୂଳ ପୃଷ୍ଠାଟି ଏକ ଠିକ ନାମ ହୋଇଥିବା ଉଚିତ ।",
        "mergehistory-invalid-destination": "ଅନ୍ତ ପୃଷ୍ଠାର ନାମ ସଠିକ ହୋଇଥିବା ଉଚିତ ।",
        "mergehistory-autocomment": "[[:$2]] ସହିତ [[:$1]]କୁ ଯୋଡ଼ି ଦିଆଗଲା ।",
-       "mergehistory-comment": "[[:$2]] ଭିତରେ [[:$1]]କୁ ଯୋଡ଼ି ଦିଆଗଲା: $3",
+       "mergehistory-comment": "[[:$2]] ଭିତରେ [[:$1]]କୁ ଯୋଡ଼ି ଦିଆଗଲା: $3",
        "mergehistory-same-destination": "ମୂଳାଧାର ଓ ଅନ୍ତ ପୃଷ୍ଠା ସମାନ ହୋଇପାରିବ ନାହିଁ",
        "mergehistory-reason": "କାରଣ:",
        "mergelog": "ମିଶ୍ରଣ ଲଗ୍",
        "sp-contributions-newonly": "କେବଳ ନୂଆ ପୃଷ୍ଠା ତିଆରିର ସମ୍ପାଦନା ଦେଖାନ୍ତୁ",
        "sp-contributions-submit": "ଖୋଜନ୍ତୁ",
        "whatlinkshere": "ଏଠାରେ ଥିବା ଲିଙ୍କ",
-       "whatlinkshere-title": "\"$1\" କୁ ପୃଷ୍ଠା ଲିଙ୍କ",
+       "whatlinkshere-title": "\"$1\"କୁ ପୃଷ୍ଠା ଲିଙ୍କ",
        "whatlinkshere-page": "ପୃଷ୍ଠା:",
        "linkshere": "ଏହି ପୃଷ୍ଠା ସବୁ  <strong>$2</strong> ସହ ଯୋଡ଼ା ଯାଇଅଛି:",
        "nolinkshere": "'''$2''' ସହିତ କୌଣସିଟି ପୃଷ୍ଠା ଯୋଡ଼ାଯାଇନାହିଁ ।",
        "pageinfo-templates": "{{PLURAL:$1|template|templates}} ($1) ଯୋଡିହେଇଥିବା",
        "pageinfo-transclusions": "{{PLURAL:$1|ପୃଷ୍ଠା|ପୃଷ୍ଠାସବୁ}} ($1)ରେ ଯୋଡାଗଲା",
        "pageinfo-toolboxlink": "ପୃଷ୍ଠା ସୂଚନା",
-       "pageinfo-redirectsto": "କୁ ଲେଉଟାଣି",
+       "pageinfo-redirectsto": "କୁ ଲେଉଟାଣି",
        "pageinfo-redirectsto-info": "ସୂଚନା",
        "pageinfo-contentpage": "ବିଷୟବସ୍ତୁ ପୃଷ୍ଠାଭାବେ ଗଣା ହେଲା",
        "pageinfo-contentpage-yes": "ହଁ",
index d13d5d6..6a3e2c0 100644 (file)
        "history": "Historia strony",
        "history_short": "historia",
        "history_small": "historia",
-       "updatedmarker": "zmienione od ostatniej wizyty",
+       "updatedmarker": "zmienione od twojej ostatniej wizyty",
        "printableversion": "Wersja do druku",
        "permalink": "Link do tej wersji",
        "print": "Drukuj",
        "autoblockedtext": "Ten adres IP został zablokowany automatycznie, gdyż korzysta z niego inny użytkownik, zablokowany przez administratora $1.\nPowód blokady:\n\n:<em>$2</em>\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n\nMożesz skontaktować się z $1 lub jednym z pozostałych [[{{MediaWiki:Grouppage-sysop}}|administratorów]] w celu uzyskania informacji o blokadzie.\n\nNie możesz użyć funkcji „{{int:emailuser}}”, jeśli brak jest poprawnego adresu e‐mail w Twoich [[Special:Preferences|preferencjach]] lub jeśli taka możliwość została Ci zablokowana.\n\nTwój obecny adres IP to $3, a numer identyfikacyjny blokady to #$5.\nProsimy o podanie obu tych numerów przy wyjaśnianiu blokady.",
        "systemblockedtext": "Twoja nazwa użytkownika lub adres IP zostały automatycznie zablokowane przez MediaWiki.\nPodany powód to:\n\n:<em>$2</em>\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zamierzano zablokować: $7\n\nTwój obecny adres IP to $3.\nProsimy o dołączenie powyższych szczegółów w jakichkolwiek zadawanych pytaniach.",
        "blockednoreason": "nie podano przyczyny",
+       "blockedtext-composite": "<strong>Twoja nazwa użytkownika lub adres IP zostały zablokowane.</strong>\n\nPodany powód to:\n\n:<em>$2</em>\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n\nTwój obecny adres IP to $3.\nProsimy o dołączenie powyższych szczegółów w jakichkolwiek zadawanych pytaniach.",
+       "blockedtext-composite-reason": "Na twoje konto i/lub adresy IP nałożono wiele blokad.",
        "whitelistedittext": "Musisz $1, by edytować strony.",
        "confirmedittext": "Edytowanie jest możliwe dopiero po zweryfikowaniu adresu e‐mail.\nPodaj adres e‐mail i potwierdź go w swoich [[Special:Preferences|ustawieniach użytkownika]].",
        "nosuchsectiontitle": "Nie można znaleźć sekcji",
index 8128d97..bc6aaa6 100644 (file)
        "history": "Histórico da página",
        "history_short": "Histórico",
        "history_small": "histórico",
-       "updatedmarker": "atualizado desde a minha última visita",
+       "updatedmarker": "atualizado desde sua última visita",
        "printableversion": "Versão para impressão",
        "permalink": "Ligação permanente",
        "print": "Imprimir",
        "autoblockedtext": "O seu endereço IP foi bloqueado de forma automática porque foi utilizado recentemente por outro usuário, o qual foi bloqueado por $1.\nO motivo apresentado foi:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\n\nNote que para utilizar a funcionalidade \"{{int:emailuser}}\" precisa de ter um endereço de e-mail válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\n\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contatos relacionados com este bloqueio, por favor.",
        "systemblockedtext": "O seu nome de usuário ou endereço IP foram bloqueados automaticamente pelo MediaWiki.\nO motivo fornecido é:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nO seu endereço IP atual é $3.\nInclua todos os detalhes acima em quaisquer contatos sobre este assunto, por favor.",
        "blockednoreason": "sem motivo especificado",
+       "blockedtext-composite": "<strong>Seu nome de usuário ou endereço IP foi bloqueado.</strong>\n\nO motivo fornecido é:\n\n:<em>$2</em>.\n\n* Início do bloqueio: $8\n* Expiração do bloqueio mais longo: $6\n\nSeu endereço IP atual é $3.\nPor favor inclua todos os detalhes acima em qualquer questão que você faça.",
+       "blockedtext-composite-reason": "Existem vários bloqueios contra sua conta e/ou endereço IP",
        "whitelistedittext": "Você precisa $1 para poder editar páginas.",
        "confirmedittext": "Você precisa confirmar o seu endereço de e-mail antes de começar a editar páginas.\nPor favor, introduza um e valide-o através das suas [[Special:Preferences|preferências de usuário]].",
        "nosuchsectiontitle": "Não foi possível encontrar a seção",
index 3e6d572..4c7b336 100644 (file)
        "autoblockedtext": "O seu endereço IP foi bloqueado de forma automática porque foi utilizado recentemente por outro utilizador, o qual foi bloqueado por $1.\nO motivo apresentado foi:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\n\nNote que para utilizar a funcionalidade \"{{int:emailuser}}\" precisa de ter um endereço de correio eletrónico válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\n\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contactos relacionados com este bloqueio, por favor.",
        "systemblockedtext": "O seu nome de utilizador ou endereço IP foram bloqueados automaticamente pelo MediaWiki.\nO motivo fornecido é:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nO seu endereço IP atual é $3.\nInclua todos os detalhes acima em quaisquer contactos sobre este assunto, por favor.",
        "blockednoreason": "sem motivo especificado",
+       "blockedtext-composite": "<strong>O seu nome de utilizador ou endereço IP foram bloqueados.</strong>\n\nO motivo fornecido é:\n\n:<em>$2</em>.\n\n* Início do bloqueio: $8\n* Expiração do bloqueio mais longo: $6\n\nO seu endereço IP atual é $3.\nInclua todos os detalhes acima em quaisquer contactos sobre este assunto, por favor.",
+       "blockedtext-composite-reason": "Existem vários bloqueios da sua conta ou endereço IP",
        "whitelistedittext": "Precisa de $1 para poder editar páginas.",
        "confirmedittext": "Precisa de confirmar o seu endereço de correio eletrónico antes de começar a editar páginas.\nIntroduza e valide o endereço através das suas [[Special:Preferences|preferências de utilizador]], por favor.",
        "nosuchsectiontitle": "Não foi possível encontrar a secção",
        "passwordpolicies-policyflag-suggestchangeonlogin": "sugerir alteração ao iniciar sessão",
        "easydeflate-invaliddeflate": "O conteúdo fornecido não está devidamente comprimido",
        "unprotected-js": "Por motivos de segurança o JavaScript de páginas desprotegidas não pode ser carregado. Crie javascript só no espaço nominal/domínio MediaWiki: ou numa subpágina do utilizador",
-       "userlogout-continue": "Se pretende terminar a sessão [$1 prossiga para a página de saída], por favor."
+       "userlogout-continue": "Quer sair?"
 }
index 49c33f2..74482f6 100644 (file)
        "autoblockedtext": "Text displayed to automatically blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block (in case of autoblocks: {{msg-mw|autoblocker}})\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link). Use it for GENDER.\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext|notext=1}}\n* {{msg-mw|Systemblockedtext|notext=1}}",
        "systemblockedtext": "Text displayed to requests blocked by MediaWiki configuration.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - (Unused) A dummy user attributed as the blocker, possibly as a link to a user page.\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the dummy blocking user's username (plain text, without the link).\n* $5 - A short string indicating the type of system block.\n* $6 - the expiry of the block\n* $7 - the intended target of the block\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext|notext=1}}\n* {{msg-mw|Autoblockedtext|notext=1}}",
        "blockednoreason": "Substituted with <code>$2</code> in the following message if the reason is not given:\n* {{msg-mw|cantcreateaccount-text}}.\n{{Identical|No reason given}}",
+       "blockedtext-composite": "Text displayed to requests blocked by more than one block.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - (Unused) A dummy user attributed as the blocker, possibly as a link to a user page.\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the dummy blocking user's username (plain text, without the link).\n* $5 - (Unused) placeholder for the block ID.\n* $6 - the expiry of the block with the longest duration\n* $7 - (Unused) the intended target of the block\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Systemblockedtext|notext=1}}",
+       "blockedtext-composite-reason": "Reason given to blocked users who are affected by more than one block.\n\nSee also:\n* {{msg-mw|blockedtext-composite}}",
        "whitelistedittext": "Used as error message. Parameters:\n* $1 - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description\n* $2 - an URL to the same\n\nSee also:\n* {{msg-mw|Nocreatetext}}\n* {{msg-mw|Uploadnologintext}}\n* {{msg-mw|Loginreqpagetext}}",
        "confirmedittext": "Used as error message.",
        "nosuchsectiontitle": "Used as error message when the user has attempted to edit a nonexistent section.",
index c60aecb..f6c23c6 100644 (file)
        "history": "Storie d'a pàgene",
        "history_short": "Cunde",
        "history_small": "cunde",
-       "updatedmarker": "aggiornate da l'urtema visita meje",
+       "updatedmarker": "aggiornate da l'urtema visita toje",
        "printableversion": "Versione ca se stambe",
        "permalink": "Collegamende ca remane pe sembre",
        "print": "Stambe",
        "virus-scanfailed": "condrolle fallite (codece $1)",
        "virus-unknownscanner": "antivirus scanusciute:",
        "logouttext": "'''Tu tè scollegate.'''\n\nNote Bbuene ca certe pàggene ponne condinuà a essere viste cumme ce tu ste angore collegate, fine a quanne a cache d'u browser no se sdevache.",
+       "logout-failed": "Non ge puè assè mò: $1",
        "cannotlogoutnow-title": "Non ge puè assè mò",
        "cannotlogoutnow-text": "Non ge puè assè quanne ste ause $1.",
        "welcomeuser": "Bovègne, $1!",
        "action-changetags": "Aggiunge e live arbitrariamende tag sus a le revisiune individuale e vôsce de l'archivije",
        "action-deletechangetags": "scangille le tag da 'u database",
        "action-purge": "aggiorne sta pàgene",
+       "action-editinterface": "cange l'inderfacce utende",
+       "action-editusercss": "cange 'u CSS de l'otre utinde",
+       "action-edituserjson": "cange 'u JSON de l'otre utinde",
+       "action-edituserjs": "cange 'u JavaScript de l'otre utinde",
+       "action-editsitecss": "cange 'u CSS d'u site",
+       "action-editsitejson": "cange 'u JSON d'u site",
+       "action-editsitejs": "cange 'u JavaScript d'u site",
+       "action-editmyusercss": "cange le file tune de CSS",
+       "action-editmyuserjson": "cange le file tune de JSON",
+       "action-editmyuserjs": "cange le file tune de JavaScript",
+       "action-viewsuppressed": "'ndruche le revisiune scunnute da tutte le utinde",
+       "action-hideuser": "bluecche 'nu cunde utende, scunnènnele da 'u pubbliche",
+       "action-ipblock-exempt": "zumbe le blocche de l'IP, auto blocche e le blocche a indervalle",
+       "action-unblockself": "sbluecche da sule",
+       "action-noratelimit": "non g'à state tuccate da le limite de le pundegge",
+       "action-reupload-own": "sovrascrive 'nu file esistende carichete da quacchedune",
+       "action-nominornewtalk": "no scè ausanne le cangiaminde stuèdeche jndr'à le pàggene de le 'ngazzaminde quanne lasse messagge nuève",
+       "action-markbotedits": "marche le cangiaminde annullate cumme cangiaminde de bot",
+       "action-patrolmarks": "'ndruche le cangiaminde recende marcate cumme a condrollate",
+       "action-override-export-depth": "l'esportazione de pàggene inglude pàggene collegate 'mbonde a 'na profonnetà de 5",
+       "action-suppressredirect": "no scè ccrejanne 'nu ridirezionamende da 'u nome vecchije quanne spueste 'na pàgene",
        "nchanges": "$1 {{PLURAL:$1|cangiaminde|cangiaminde}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|da l'urtema visite}}",
        "enhancedrc-history": "cunde",
index f870148..fa38c4f 100644 (file)
                        "Romanko Mikhail",
                        "Diralik",
                        "1233qwer1234qwer4",
-                       "Саша Волохов"
+                       "Саша Волохов",
+                       "Serhio Magpie"
                ]
        },
        "tog-underline": "Подчёркивание ссылок:",
        "noindex-category": "Неиндексируемые страницы",
        "broken-file-category": "Страницы с неработающими файловыми ссылками",
        "categoryviewer-pagedlinks": "($1) ($2)",
-       "category-header-numerals": "$1â\80\93$2",
+       "category-header-numerals": "$1â\80\94$2",
        "about": "Описание",
        "article": "Статья",
        "newwindow": "(в новом окне)",
        "autoblockedtext": "Ваш IP-адрес автоматически заблокирован в связи с тем, что он ранее использовался кем-то из участников, заблокированных администратором $1. \nБыла указана следующая причина блокировки:\n\n: «$2».\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВы можете связаться с $1 или любым другим [[{{MediaWiki:Grouppage-sysop}}|администратором]], чтобы обсудить блокировку.\n\nОбратите внимание, что вы не сможете использовать функцию «{{int:emailuser}}», если в своих [[Special:Preferences|персональных настройках]] не задали или не подтвердили корректный адрес электронной почты, или если ваша блокировка включает запрет отправки писем подобным образом.\n\nВаш IP-адрес — $3, идентификатор блокировки — #$5.\nПожалуйста, указывайте эти сведения в любых своих обращениях.",
        "systemblockedtext": "Ваше имя участника или IP-адрес были автоматически заблокированы MediaWiki.\nУказана следующая причина:\n\n:<em>$2</em>\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.",
        "blockednoreason": "причина не указана",
+       "blockedtext-composite": "<strong>Ваше имя участника или IP-адрес были заблокированы.</strong>\n\nУказана следующая причина:\n\n:<em>$2</em>\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.",
+       "blockedtext-composite-reason": "Есть несколько блокировок вашей учётной записи и/или IP-адреса",
        "whitelistedittext": "Вы должны $1 для изменения страниц.",
        "confirmedittext": "Вы должны подтвердить свой адрес электронной почты перед правкой страниц.\nПожалуйста, введите и подтвердите свой адрес электронной почты в своих [[Special:Preferences|персональных настройках]].",
        "nosuchsectiontitle": "Невозможно найти раздел",
index 0171ad6..7cc15a5 100644 (file)
        "protectedinterface": "Тота сторінка є частёв інтрефейсу проґрамового забеспечіня той вікі і єй можуть едітовати лем адміністраторы проєкту.\nЖебы придати або змінити переклады, просиме хоснуйте [https://translatewiki.net/ translatewiki.net], локалізачный проєкт MediaWiki.",
        "editinginterface": "<strong>Позірь:</strong> Едітуєте сторінку, котра є частинов текстового інтерфейсу.\nЗміны той сторінкы выкличуть зміну інтерфейсу про іншых хоснователїв той вікі.",
        "translateinterface": "Додати ці змінити переклады на вшыткых вікі просиме хоснуйте [https://translatewiki.net/ translatewiki.net] — проєкт, што ся занимать локалізаціов MediaWiki.",
-       "cascadeprotected": "СÑ\82оÑ\80Ñ\96нка Ñ\94 Ð·Ð°Ð¼Ð½ÐºÑ\83Ñ\82а, Ð±Ð¾ Ñ\94 Ð²Ð»Ð¾Ð¶ÐµÐ½Ð° Ð´Ð¾  {{PLURAL:$1|наÑ\81лÑ\97дÑ\83Ñ\8eÑ\87ой Ñ\81Ñ\82оÑ\80Ñ\96нкÑ\8b Ð·Ð°Ð¼ÐºÐ½Ñ\83Ñ\82Ñ\8b|наÑ\81лÑ\97дÑ\83Ñ\8eÑ\87Ñ\96Ñ\85 Ñ\81Ñ\82оÑ\80Ñ\96нок Ð·Ð°Ð¼Ð½ÐºÐ½Ñ\83Ñ\82Ñ\8bÑ\85|наÑ\81лÑ\97дÑ\83Ñ\8eÑ\87Ñ\96Ñ\85 Ñ\81Ñ\82оÑ\80Ñ\96нок Ð·Ð°Ð¼Ð½ÐºÐ½Ñ\83Ñ\82Ñ\8bÑ\85}} ÐºÐ°Ñ\81кадовÑ\8bм Ð·Ð°Ð¼ÐºÐ¾м:\n$2",
+       "cascadeprotected": "СÑ\82оÑ\80Ñ\96нка Ñ\94 Ð·Ð°Ð¼ÐºÐ½Ñ\83Ñ\82а, Ð±Ð¾ Ñ\94 Ð²Ð»Ð¾Ð¶ÐµÐ½Ð° Ð´Ð¾ {{PLURAL:$1|наÑ\81лÑ\97дÑ\83Ñ\8eÑ\87ой Ñ\81Ñ\82оÑ\80Ñ\96нкÑ\8b Ð·Ð°Ð¼ÐºÐ½Ñ\83Ñ\82Ñ\8b|наÑ\81лÑ\97дÑ\83Ñ\8eÑ\87Ñ\96Ñ\85 Ñ\81Ñ\82оÑ\80Ñ\96нок Ð·Ð°Ð¼ÐºÐ½Ñ\83Ñ\82Ñ\8bÑ\85}} â\80\9eкаÑ\81кадовÑ\8bмâ\80\9c Ð·Ð°Ð¼ÐºÐ½Ñ\83Ñ\82Ñ\8fм:\n$2",
        "namespaceprotected": "Не маєте права едітовати сторінкы в просторї  назв «$1».",
        "customcssprotected": "Не маєте права едітовати тоту сторінку з CSS, бо обсягує персоналны наставлїна іншого хоснователя.",
        "customjsonprotected": "Не маєте права едітовати тоту сторінку з JSON, бо обсягує персоналны наставлїна іншого хоснователя.",
        "mypreferencesprotected": "Не мате дозволїня мінити свої наставлїня.",
        "ns-specialprotected": "Шпеціалны сторінкы не є можне едітовати.",
        "titleprotected": "Створїня сторінкы з таков назвов было заборонене хоснователём [[User:$1|$1]] з причінов: <em>$2</em>.",
-       "filereadonlyerror": "Ð\9dе Ð³Ð¾Ð´Ð½Ð¾ Ð·Ð¼Ñ\96ниÑ\82и Ñ\84айл â\80\9e$1â\80\9c, Ð±Ð¾ Ð°Ñ\80Ñ\85Ñ\96в Ñ\84айлÑ\96в â\80\9e$2â\80\9c Ñ\94 Ñ\82епеÑ\80Ñ\8c Ð»ÐµÐ¼ Ð½Ð° Ñ\87Ñ\96Ñ\82анÑ\8f.\n\nÐ\90дмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80 Ñ\81еÑ\80веÑ\80а, ÐºÐ¾Ñ\82Ñ\80Ñ\8bй Ð°Ñ\80Ñ\85Ñ\96в Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð², Ð´Ð¾Ð´Ð°Ð² Ñ\82оÑ\82о Ð¿Ð¾Ñ\8fÑ\81нÑ\97нÑ\8f: â\80\9e''$3''“.",
+       "filereadonlyerror": "Ð\9dе Ð±Ñ\8bло Ð¼Ð¾Ð¶Ð½Ð¾ Ð·Ð¼Ñ\96ниÑ\82и Ñ\84айл â\80\9e$1â\80\9c, Ð±Ð¾ Ð°Ñ\80Ñ\85Ñ\96в Ñ\84айлÑ\96в â\80\9e$2â\80\9c Ñ\94 Ñ\82епеÑ\80Ñ\8c Ð»ÐµÐ¼ Ð½Ð° Ñ\87Ñ\96Ñ\82анÑ\8f.\n\nÐ\90дмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80 Ñ\81еÑ\80веÑ\80а, ÐºÐ¾Ñ\82Ñ\80Ñ\8bй Ð°Ñ\80Ñ\85Ñ\96в Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð², Ð´Ð¾Ð´Ð°Ð² Ñ\82оÑ\82о Ð¿Ð¾Ñ\8fÑ\81нÑ\97нÑ\8f: â\80\9e$3“.",
        "invalidtitle": "Неприпустна назва",
        "invalidtitle-knownnamespace": "Непряавилна назва в просторї назв „$2“ і текстом „$3“",
        "invalidtitle-unknownnamespace": "Неправилна назва з незнамым чіслом простору назв $1 і текстом „$2“",
        "nocookieslogin": "{{SITENAME}} хоснує cookies про приголошіня хоснователїв. Вы маєте cookies выпнуты. Просиме Вас, повольте їх і спобуйте знова.",
        "nocookiesfornew": "Конто хоснователя не было створене, бо сьме не были годны потвердити ёго походжіня.\nУтвердите ся, же маєте дозволены cookies, обновте тоту сторінку і спробуйте то знову.",
        "noname": "Мусите увести мено свого конта.",
-       "loginsuccesstitle": "УÑ\81пÑ\96Ñ\88не Ð¿Ñ\80иголоÑ\88Ñ\96нÑ\8f",
+       "loginsuccesstitle": "Ð\9fÑ\80иголоÑ\88енÑ\8bй(а)",
        "loginsuccess": "'''Теперь працуєте {{grammar:locative|{{SITENAME}}}} під меном $1.'''",
        "nosuchuser": "Не екзістує хоснователь з меном «$1».\nУ хосновательскых мен ся розлишують малы/великы писмена.\nСконтролюйте запис, або собі [[Special:CreateAccount|зареґіструйте нове конто]].",
        "nosuchusershort": "Хоснователь з меном $1 не екзістує.\nПеревірте правилность написаня мена.",
index 0c427e6..ab3e9f5 100644 (file)
        "tog-numberheadings": "Numarazioni otomàtigga di li tìturi di sezzioni",
        "tog-editondblclick": "Mudìfigga di li pàgini attrabessu dóppiu clic",
        "tog-editsectiononrightclick": "Mudìfigga di li sezzioni attrabessu lu clic dresthu i' lu tìturu",
-       "tog-watchcreations": "Aggiungi li pàgini criaddi a l'abbaidaddi ippiziari",
-       "tog-watchdefault": "Aggiungi li pàgini mudìfiggaddi a l'abbaidaddi ippiziari",
-       "tog-watchmoves": "Aggiungi li pàgini ippusthaddi a l'abbaidaddi ippiziari",
-       "tog-watchdeletion": "Aggiungi li pàgini canzilladdi a l'abbaidaddi ippiziari",
+       "tog-watchcreations": "Aggiungi li pàgini criaddi e l'archìbii carriggaddi da me a l'abbaiddaddi ippiziari.",
+       "tog-watchdefault": "Aggiungi li pàgini e l'archìbii mudifiggaddi da me a l'abbaiddaddi ippiziari.",
+       "tog-watchmoves": "Aggiungi li pàgini e li schedarii ippusthaddi da me a l'abbaiddaddi ippiziari.",
+       "tog-watchdeletion": "Aggiungi li pàgini e li schedarii chi àggiu canzilladdu a l'abbaiddaddi ippiziari.",
+       "tog-watchuploads": "Aggiugnì nobi archìbii chi carriggu a l'abbaiddaddi ippiziari méi",
        "tog-minordefault": "Indica tutti li mudìfigghi cumenti 'minori' in otomàtiggu",
        "tog-previewontop": "Musthra l'antiprimma sobra la casella di mudìfigga",
        "tog-previewonfirst": "Musthra l'antiprimma pa la primma mudìfigga",
-       "tog-enotifwatchlistpages": "Signàrami pa postha erettrònica li mudìfigghi a li pàgini abbaidaddi",
+       "tog-enotifwatchlistpages": "Signàrami pa postha erettrònica li mudìfigghi a li pàgini o schedarii abbaiddaddi.",
        "tog-enotifusertalkpages": "Signàrami pa postha erettrònica li mudìfigghi a la me' pàgina di dischussioni",
-       "tog-enotifminoredits": "Signàrami pa postha erettrònica puru li mudìfigghi minori",
+       "tog-enotifminoredits": "Signàrami pa postha erettrònica puru li mudìfigghi minori.",
        "tog-enotifrevealaddr": "Rivera lu me' indirizzu di postha erettrònica i' l'imbasciaddi d'avvisu",
        "tog-shownumberswatching": "Musthra lu nùmaru d'utenti ch'àni la pàgina abbaidadda",
-       "tog-oldsig": "Fimma esisthenti",
+       "tog-oldsig": "Fimma esisthenti.",
        "tog-fancysig": "Interpreta i cumandi wiki i' la fimma (chena cullegaumentu otomatiggu)",
-       "tog-uselivepreview": "Attiba la funzioni ''Live preview'' (dumanda JavaScript; ippirimintari)",
+       "tog-uselivepreview": "Attiba la funzioni ''Live preview''. (dumanda JavaScript; ippirimintari)",
        "tog-forceeditsummary": "Dumanda cunfèimma si l'oggettu di la mudìfigga è bioddu",
        "tog-watchlisthideown": "Cua li me' mudìfigghi i' l'abbaidaddi ippiziari",
        "tog-watchlisthidebots": "Cua li mudìfigghi di li bot i' l'abbaidaddi ippiziari",
        "tog-watchlisthideminor": "Cua li mudìfigghi minori i' l'abbaidaddi ippiziari",
+       "tog-watchlisthideliu": "Cuà mudìfigghi da utenti intraddi di la listha di pàgini sottu osseivvazioni",
+       "tog-watchlistreloadautomatically": "Sempri turrà a carriggà la listha di li pàgini sottu osseivvazioni candu un filthru è ciambaddu (dumanda JavaScript)",
        "tog-ccmeonemails": "Inviammi una còpia di l'imbasciaddi ippididdi a l'althri utenti",
        "tog-diffonly": "No visuarizzà lu cuntinuddu di la pàgina daboi lu cunfrontu tra versioni",
        "tog-showhiddencats": "Musthrà li categuri cuaddi",
        "logentry-newusers-create": "La registhrazioni di l'utenti $1 è isthadda {{GENDER:$2|criadda}}",
        "logentry-upload-upload": "$1 {{GENDER:$2|à carriggaddu}} $3",
        "rightsnone": "(nisciunu)",
-       "searchsuggest-search": "Zercha di dentru a {{SITENAME}}"
+       "searchsuggest-search": "Zercha di dentru a {{SITENAME}}",
+       "userlogout-continue": "Bói iscí?"
 }
index 5b15466..fb724c4 100644 (file)
        "databaseerror-query": "Upit: $1",
        "databaseerror-function": "Funkcija: $1",
        "databaseerror-error": "Greška: $1",
-       "transaction-duration-limit-exceeded": "Da se izbjegne veliko zaostajanje replikacije, transakcija je prekinuta zato što je trajanje zapisivanja ($1) prekoračilo ograničenje od {{PLURAL:$2|jedne sekunde|$2 sekunde|$2 sekundi}}.\nAko mijenjate mnogo stavki odjednom, uradite to u više navrata.",
+       "transaction-duration-limit-exceeded": "Da se izbjegne veliko zaostajanje odgovorâ, transakcija je prekinuta zato što prekoračeno je trajanje zapisivanja ($1), ograničeno {{PLURAL:$2|jednom sekundom|$2 sekunde|$2 sekundi}}.\nAko mijenjate više stavki odjednom, uradite ovo na više navrata umjesto zajednički.",
        "laggedslavemode": "'''Upozorenje''': Stranica ne mora sadržavati posljednja ažuriranja.",
        "readonly": "Baza podataka je zaključana",
        "enterlockreason": "Unesite razlog za zaključavanje, uključujući procjenu vremena otključavanja",
        "autoblockedtext": "Vaša IP adresa je automatski blokirana jer je korištena od strane drugog korisnika, a blokirao ju je $1.\nNaveden je slijedeći razlog:\n\n:<em>$2</em>\n\n* Početak blokade: $8\n* Kraj blokade: $6\n* Blokirani korisnik: $7\n\nMožete kontaktirati $1 ili nekog drugog iz grupe [[{{MediaWiki:Grouppage-sysop}}|administratora]] i zahtijevati da Vas deblokira.\n\nZapamtite da ne možete koristiti opciju \"{{int:emailuser}}\" ukoliko nije unesena validna e-mail adresa u [[Special:Preferences|Vašim postavkama]] te Vas ne spriječava ga je koristite.\n\nVaša trenutna IP adresa je $3, a ID blokade je $5.\nMolimo da navedete sve gore navedene detalje u zahtjevu za deblokadu.",
        "systemblockedtext": "MediaWiki je automatski blokirao Vaše korisničko ime ili IP-adresu.\nDat je sljedeći razlog:\n\n:<em>$2</em>\n\n* Početak bloka: $8\n* Istek bloka: $6\n* Blok je namijenjen za: $7\n\nVaša trenutna IP-adresa je $3.\nPrepišite sve gorenavedene pojedinosti ukoliko želite da vlasti pitaju za blok.",
        "blockednoreason": "razlog nije naveden",
+       "blockedtext-composite": "<strong>Vaše korisničko ime ili IP-adresa je blokirano.</strong>\n\nDat je sljedeći razlog:\n\n:<em>$2</em>.\n\n* Početak bloka: $8\n* Istek najdužeg bloka: $6\n\nVaša trenutna IP-adresa je $3.\nPrepišite sve gorenavedene pojedinosti ukoliko želite da vlasti pitaju za blok.",
+       "blockedtext-composite-reason": "Vaš račun i/ili IP adresa ima nekoliko blokova",
        "whitelistedittext": "Da bi ste uređivali stranice, morate se $1.",
        "confirmedittext": "Morate potvrditi Vašu e-mail adresu prije nego počnete mijenjati stranice.\nMolimo da postavite i verifikujete Vašu e-mail adresu putem Vaših [[Special:Preferences|korisničkih opcija]].",
        "nosuchsectiontitle": "Ne mogu pronaći sekciju",
        "accmailtext": "Nasumično odabrana šifra za [[User talk:$1|$1]] je poslata na adresu $2.\n\nŠifra/lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|izmjene šifre]]'' nakon prijave.",
        "newarticle": "(Novi)",
        "newarticletext": "Preko linka ste došli na stranicu koja još uvijek ne postoji.\n* Ako želite stvoriti stranicu, počnite tipkati u okviru dolje (v. [$1 stranicu za pomoć] za više informacija).\n* Ukoliko ste došli greškom, pritisnike dugme '''Nazad''' ('''back''') na vašem pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio račun ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
+       "anontalkpagetext": "----\n<em>Ovo je razgovorna stranica s anonimnim korisnikom koji još nije napravio račun ili ga ne koristi.</em>\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.",
        "noarticletext": "Na ovoj stranici trenutno nema teksta.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] u drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretraživati srodne registre],\nili [{{fullurl:{{FULLPAGENAME}}|action=edit}} napraviti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Trenutno nema teksta na ovoj stranici.\nMožete [[Special:Search/{{PAGENAME}}|tražiti ovaj naslov stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane registre]</span>. alio nemate dozvolu za stvaranje ove stranice.",
        "missing-revision": "Ne mogu da pronađem izmenu br. $1 na stranici pod nazivom „{{FULLPAGENAME}}“.\n\nOvo se obično dešava kada pratite zastarjelu vezu do stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].",
        "copyrightwarning2": "Zapamtite da svaki doprinos na stranici {{SITENAME}} može biti izmijenjen, promijenjen ili uklonjen od strane ostalih korisnika. Ako ne želite da ovo desi sa Vašim tekstom, onda ga nemojte slati ovdje.<br />\nTakođer nam garantujete da ste ovo Vi napisali, ili da ste ga kopirali iz javne domene ili sličnog slobodnog izvora informacija (pogledajte $1 za više detalja).\n'''NE ŠALJITE DJELA ZAŠTIĆENA AUTORSKIM PRAVOM BEZ DOZVOLE!'''",
        "editpage-cannot-use-custom-model": "Model sadržaja ove stranice se ne može promijeniti.",
        "longpageerror": "'''Greška: tekst koji ste uneli je veličine {{PLURAL:$1|jedan kilobajt|$1 kilobajta|$1 kilobajta}}, što je veće od {{PLURAL:$2|dozvoljenog jednog kilobajta|dozvoljena $2 kilobajta|dozvoljenih $2 kilobajta}}.'''\nStranica ne može biti sačuvana.",
-       "readonlywarning": "<strong>Upozorenje: baza podataka je zaključana radi održavanja, tako da trenutno nećete moći da sačuvate izmene.</strong>\nMožda biste želeli sačuvati tekst za kasnije u nekoj tekstualnoj datoteci.\n\nAdministrator koji je zaključao bazu dao je sledeće objašnjenje: $1",
+       "readonlywarning": "<strong>Upozorenje: baza podataka je zaključana radi održavanja, i stoga trenutno nećete moći da sačuvate izmjene.</strong>\n\nPreporučujemo Vam prekopirati tekst na strani i sačuvati ga za kasnije.\n\nAdministrator koji je zaključao bazu dao je sledeće objašnjenje: $1",
        "protectedpagewarning": "'''PAŽNJA: Ova stranica je zaključana tako da samo korisnici sa administratorskim privilegijama mogu da je mijenjaju.'''\nPosljednja stavka u registru je prikazana ispod kao referenca:",
        "semiprotectedpagewarning": "<strong>Pažnja:</strong> Ova stranica je zaključana tako da je samo automatski potvrđeni korisnici mogu uređivati.\nPosljednja stavka registra je prikazana ispod kao referenca:",
        "cascadeprotectedwarning": "<strong>Upozorenje:</strong> Ova stranica je zaključana tako da je samo korisnici sa [[Special:ListGroupRights|određenim pravima]] mogu mijenjati, jer je ona uključena u {{PLURAL:$1|sljedeću, prenosivo zaštićenu stranicu|sljedeće, prenosivo zaštićene stranice}}:",
        "edit-gone-missing": "Stranica se nije mogla osvježiti.\nIzgleda da je obrisana.",
        "edit-conflict": "Sukob izmjena.",
        "edit-no-change": "Vaša izmjena je ignorirana, jer nije bilo promjena teksta stranice.",
+       "edit-slots-cannot-add": "{{PLURAL:$1|Sljedeći slot ovdje nije podržan|Sljedeći slotovi ovdje nisu podržani}}: $2.",
+       "edit-slots-cannot-remove": "{{PLURAL:$1|Sljedeći slot je obavezan i ne može da se ukloni|Sljedeći slotovi su obavezni i ne mogu da se uklone}}: $2.",
+       "edit-slots-missing": "{{PLURAL:$1|Sljedeći slot nedostaje|Sljedeći slotovi nedostaju}}: $2.",
        "postedit-confirmation-created": "Stranica je stvorena.",
        "postedit-confirmation-restored": "Stranica je obnovljena.",
        "postedit-confirmation-saved": "Vaša izmjena je snimljena.",
        "nchanges": "$1 {{PLURAL:$1|izmjena|izmjene|izmjena}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|izmjena od Vaše posljedne posjete}}",
        "enhancedrc-history": "historija",
-       "recentchanges": "Nedavne izmjene / Скорашње измене",
+       "recentchanges": "Nedavne promjene / Недавне промене",
        "recentchanges-legend": "Postavke za Nedavne promjene",
        "recentchanges-summary": "Na ovoj stranici možete pratiti nedavne izmjene.",
        "recentchanges-noresult": "Bez promjena tokom cijelog perioda koji ispunjava ove kriterije.",
        "anonymous": "{{PLURAL:$1|Anonimni korisnik|$1 anonimna korisnika|$1 anonimnih korisnika}} projekta {{SITENAME}}",
        "siteuser": "{{SITENAME}} korisnik $1",
        "anonuser": "{{SITENAME}} anonimni korisnik $1",
-       "lastmodifiedatby": "Ovu stranicu je posljednji put promjenio $3, u $2, $1",
+       "lastmodifiedatby": "Ovu stranicu je posljednji put {{GENDER:$4|uredio|uredila}} $3 u $2 na datum $1.",
        "othercontribs": "Bazirano na radu od strane korisnika $1.",
        "others": "ostali",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|{{GENDER:$1|korisnik}}|korisnika}} $1",
        "version-poweredby-others": "ostali",
        "version-poweredby-translators": "translatewiki.net prevodioci",
        "version-credits-summary": "Htjeli bismo da zahvalimo sljedećim osobama na njihovom doprinosu [[Special:Version|MediaWiki]].",
-       "version-license-info": "Mediawiki je slobodni softver, možete ga redistribuirati i/ili mijenjati pod uslovima GNU opće javne licence kao što je objavljeno od strane Fondacije Slobodnog Softvera, bilo u verziji 2 licence, ili (po vašoj volji) nekoj od kasniji verzija.\n\nMediawiki se distriburia u nadi da će biti korisna, ali BEZ IKAKVIH GARANCIJA, čak i bez ikakvih posrednih garancija o KOMERCIJALNOSTI ili DOSTUPNOSTI ZA ODREĐENU SVRHU. Pogledajte GNU opću javnu licencu za više detalja.\n\nTrebali biste dobiti [{{SERVER}}{{SCRIPTPATH}}/KOPIJU GNU opće javne licence] zajedno s ovim programom, ako niste, pišite Fondaciji Slobodnog Softvera na adresu  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ili je pročitajte [//www.gnu.org/licenses/old-licenses/gpl-2.0.html online].",
+       "version-license-info": "Mediawiki je slobodni softver; možete ga redistribuirati i/ili mijenjati pod uslovima GNU-ove opće javne licence Fondacije slobodnog softvera; ili u verziji 2 Licence, ili nekoj od kasniji verzija (po vašoj volji).\n\nMediaWiki se nudi u nadi da će biti korisna, ali <em>BEZ IKAKVIH GARANCIJA</em>; čak i bez podrazumjevane garancije o <strong>KOMERCIJALNOSTI</strong> ili <strong>PRIKLADNOSTI ZA ODREĐENU SVRHU</strong>. Za više informacija, pogledajte GNU-ovu opću javnu licencu.\n\nZajedno s ovim programom trebali biste dobiti [{{SERVER}}{{SCRIPTPATH}}/COPYING primjerak GNU-ove opće javne licence]; ako niste dobili primjerak, pišite na Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA ili je [//www.gnu.org/licenses/old-licenses/gpl-2.0.html pročitajte ovdje].",
        "version-software": "Instalirani softver",
        "version-software-product": "Proizvod",
        "version-software-version": "Verzija",
index 77ae281..da11bf7 100644 (file)
        "history": "Zgodovina strani",
        "history_short": "Zgodovina",
        "history_small": "zgodovina",
-       "updatedmarker": "Posodobljeno od mojega zadnjega obiska",
+       "updatedmarker": "posodobljeno od vašega zadnjega obiska",
        "printableversion": "Različica za tisk",
        "permalink": "Trajna povezava",
        "print": "Tisk",
        "blockedtext-partial": "<strong>Vaše uporabniško ime ali IP-naslov je bil blokiran pred spreminjanjem te strani. Še vedno lahko urejate druge strani na tem wikiju.</strong> Polne podrobnosti blokade si lahko ogledate na [[Special:MyContributions|prispevkih računa]].\n\nBlokado je opravil(-a) $1.\n\nPodani razlog je <em>$2</em>.\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n* ID blokade #$5",
        "blockedtext": "<strong>Urejanje z vašim uporabniškim imenom oziroma IP-naslovom je onemogočeno.</strong>\n\nBlokiral vas je $1.\nPodani razlog je <em>$2</em>.\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n\nO blokiranju se lahko pogovorite z uporabnikom/-co $1 ali katerim drugim [[{{MediaWiki:Grouppage-sysop}}|administratorjem]].\nVedite, da lahko ukaz »{{int:emailuser}}« uporabite le, če ste v [[Special:Preferences|nastavitvah]] vpisali in potrdili svoj elektronski naslov in ta ni blokiran.\nVaš IP-naslov je $3, številka blokade pa #$5.\nProsimo, vključite ju v vse morebitne poizvedbe.",
        "autoblockedtext": "Vaš IP-naslov je bil samodejno blokiran, saj je bil uporabljen s strani drugega uporabnika, ki ga je blokiral $1.\nRazlog za to je bil naslednji:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Konec blokade: $6\n* Blokirani uporabnik: $7\n\nKontaktirate lahko $1 ali katerega od drugih [[{{MediaWiki:Grouppage-sysop}}|administratorjev]], da razpravljate o blokadi.\n\nVedite, da lahko funkcijo »{{int:emailuser}}« uporabljate le, če ste v svoje [[Special:Preferences|uporabniške nastavitve]] vnesli veljaven e-poštni naslov, in vam njena uporaba ni bila preprečena.\n\nVaš trenutni IP-naslov je $3, ID blokiranja pa #$5. Prosimo, vključite ta ID v vsako zastavljeno vprašanje.",
-       "systemblockedtext": "Vaše uporabniško ime ali IP-naslov je MediaWiki samodejn blokiral.\nPodani razlog je:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n\nVaš trenutni IP-naslov je $3.\nProsimo, da v svoje poizvedbe vključite vse zgornje podatke.",
+       "systemblockedtext": "Vaše uporabniško ime ali IP-naslov je MediaWiki samodejno blokiral.\nPodani razlog je:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n\nVaš trenutni IP-naslov je $3.\nProsimo, da v svoje poizvedbe vključite vse zgornje podatke.",
        "blockednoreason": "razlog ni podan",
+       "blockedtext-composite": "<strong>Vaše uporabniško ime ali IP-naslov je bil blokiran.</strong>\n\nPodani razlog je:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Potek najdaljše blokade: $6\n\nVaš trenutni IP-naslov je $3.\nProsimo, da v svoje poizvedbe vključite vse zgornje podatke.",
+       "blockedtext-composite-reason": "Za vaš račun in/ali IP-naslov je nastavljenih več blokad.",
        "whitelistedittext": "Za urejanje strani se morate $1.",
        "confirmedittext": "Pred urejanjem strani morate potrditi svoj e-poštni naslov.\nProsimo, da ga z uporabo [[Special:Preferences|uporabniških nastavitev]] vpišete in potrdite.",
        "nosuchsectiontitle": "Ne najdem razdelka",
index 3da455d..1b8aa35 100644 (file)
        "databaseerror-query": "Упит: $1",
        "databaseerror-function": "Функција: $1",
        "databaseerror-error": "Грешка: $1",
+       "transaction-duration-limit-exceeded": "Због избегавања великих заостајања репликације, ова трансакција је прекинута због тога што је трајање записивања ($1) премашило $2 секунди ограничења. \nУколико мењате више ставки одједном, правите ово на више мањих операција.",
        "laggedslavemode": "<strong>Упозорење:</strong> страница можда не садржи недавна ажурирања.",
        "readonly": "База података је закључана",
        "enterlockreason": "Унесите разлог за закључавање, укључујући и време откључавања",
        "virus-scanfailed": "скенирање није успело (код $1)",
        "virus-unknownscanner": "непознати антивирус:",
        "logouttext": "<strong>Сада сте одјављени.</strong>\n\nЗапамтите да неке странице могу наставити да се приказују као да сте још увек пријављени, све док не обришете кеш свог прегледача.",
+       "logging-out-notify": "Одјављивање је у току, сачекајте.",
+       "logout-failed": "Тренутно није могуће одјавити се: $1",
        "cannotlogoutnow-title": "Одјава тренутно није могућа",
        "cannotlogoutnow-text": "Одјава није могућа током употребе $1.",
        "welcomeuser": "Добро дошли, $1!",
        "badretype": "Лозинке које сте унели се не поклапају.",
        "usernameinprogress": "Налог за ово корисничко име се већ прави, сачекајте.",
        "userexists": "Унесено корисничко име је већ у употреби.\nОдаберите друго.",
+       "createacct-normalization": "Ваше корисничко име биће прилагођено на „$2” због техничких ограничења.",
        "loginerror": "Грешка при пријављивању",
        "createacct-error": "Дошло је до грешке при отварању налога",
        "createaccounterror": "Није могуће отворити налог: $1",
        "grant-editmycssjs": "Уређивање вашег корисничког Це-Ес-Еса/ЈСОН-а/јаваскрипта",
        "grant-editmyoptions": "Уређивање ваших корисничких подешавања и ЈСОН поставке",
        "grant-editmywatchlist": "Уређивање вашег списка надгледања",
+       "grant-editsiteconfig": "уређивање CSS-а/JS-а корисника и целог сајта",
        "grant-editpage": "Уређивање постојећих страница",
        "grant-editprotected": "Уређивање заштићених страница",
        "grant-highvolume": "Мењање великог обима",
        "action-changetags": "додате и уклоните разне ознаке на појединачним изменама и уносима у дневницима",
        "action-deletechangetags": "бришете ознаке из базе података",
        "action-purge": "освежите ову страницу",
+       "action-bigdelete": "бришете странице с великим историјама измена",
        "action-blockemail": "блокирате кориснику слање е-порука",
+       "action-bot": "будете сматрани аутоматизованим процесом",
+       "action-editprotected": "уређуете странице заштићене нивоом „{{int:protect-level-sysop}}”",
+       "action-editsemiprotected": "уређуете странице заштићене нивоом „{{int:protect-level-autoconfirmed}}”",
+       "action-editinterface": "уређујете кориснички интерфејс",
+       "action-editusercss": "уређујете CSS датотеке других корисника",
+       "action-edituserjson": "уређујете JSON датотеке других корисника",
+       "action-edituserjs": "уређујете JavaScript датотеке других корисника",
        "action-editsitecss": "уређујете CSS на новоу сајта",
        "action-editsitejson": "уређујете JSON на нивоу сајта",
        "action-editsitejs": "уређујете JavaScript на новоу сајта",
+       "action-editmyusercss": "уређујете сопствене CSS датотеке",
+       "action-editmyuserjson": "уређујете сопствене JSON датотеке",
+       "action-editmyuserjs": "уређујете сопствене JavaScript датотеке",
        "action-hideuser": "блокирате корисничко име, сакривајући га од јавности",
        "nchanges": "$1 {{PLURAL:$1|промена|промене|промена}}",
        "ntimes": "$1×",
        "rcfilters-restore-default-filters": "Врати подразумеване филтере",
        "rcfilters-clear-all-filters": "Обришите све филтере",
        "rcfilters-show-new-changes": "Нове промене од $1",
-       "rcfilters-search-placeholder": "ФилÑ\82Ñ\80иÑ\80аÑ\98Ñ\82е Ð¿Ñ\80омене (коÑ\80иÑ\81Ñ\82иÑ\82е Ð¼ÐµÐ½Ð¸ Ð¸Ð»Ð¸ Ð¿Ñ\80еÑ\82Ñ\80агÑ\83 Ð·Ð° име филтера)",
+       "rcfilters-search-placeholder": "ФилÑ\82Ñ\80иÑ\80аÑ\98Ñ\82е Ð¿Ñ\80омене (коÑ\80иÑ\81Ñ\82иÑ\82е Ð¼ÐµÐ½Ð¸ Ð¸Ð»Ð¸ Ð¿Ñ\80еÑ\82Ñ\80ажиÑ\82е име филтера)",
        "rcfilters-invalid-filter": "Неважећи филтер",
        "rcfilters-empty-filter": "Нема активних филтера. Сви доприноси су приказани.",
        "rcfilters-filterlist-title": "Филтери",
        "blocklink": "блокирај",
        "unblocklink": "деблокирај",
        "change-blocklink": "промени блокаду",
+       "empty-username": "(корисничко име није доступно)",
        "contribslink": "доприноси",
        "emaillink": "пошаљи е-поруку",
        "autoblocker": "Аутоматски сте блокирани јер делите IP адресу с корисником/цом [[User:$1|$1]].\nРазлог блокирања корисника/це $1 је „$2“",
        "specialpages-group-developer": "Програмерске алатке",
        "blankpage": "Празна страница",
        "intentionallyblankpage": "Ова страница је намерно остављена празном.",
+       "disabledspecialpage-disabled": "Администратор система је онемогућио ову страницу.",
        "external_image_whitelist": " #Оставите овај ред онаквим какав јесте<pre>\n#Испод додајте одломке регуларних израза (само део који се налази између //)\n#Они ће бити упоређени с адресама спољашњих слика\n#Оне које се поклапају биће приказане као слике, а преостале као везе до слика\n#Редови који почињу с тарабом се сматрају коментарима\n#Сви уноси су осетљиви на мала и велика слова\n\n#Додајте све одломке регуларних израза изнад овог реда. Овај ред не дирајте</pre>",
        "tags": "Важеће ознаке промена",
        "tag-filter": "Филтер [[Special:Tags|ознака]]:",
        "mw-widgets-abandonedit-discard": "Одбаци измене",
        "mw-widgets-abandonedit-keep": "Настави са уређивањем",
        "mw-widgets-abandonedit-title": "Јесте ли сигурни?",
+       "mw-widgets-copytextlayout-copy": "Копирај",
+       "mw-widgets-copytextlayout-copy-fail": "Копирање у оставу није успело.",
+       "mw-widgets-copytextlayout-copy-success": "Копирано у оставу.",
        "mw-widgets-dateinput-no-date": "Датум није изабран",
        "mw-widgets-dateinput-placeholder-day": "ГГГГ-ММ-ДД",
        "mw-widgets-dateinput-placeholder-month": "ГГГГ-ММ",
        "authmanager-autocreate-noperm": "Аутоматско отварање налога није дозвољено.",
        "authmanager-autocreate-exception": "Аутоматско креирање налога је привремено онемогућено због претходних грешака.",
        "authmanager-userdoesnotexist": "Кориснички налог „$1“ није отворен.",
+       "authmanager-userlogin-remembermypassword-help": "Да ли лозинка треба да се памти дуже од дужине сесије.",
        "authmanager-username-help": "Корисничко име за потврду идентитета.",
        "authmanager-password-help": "Лозинка за потврду идентитета.",
        "authmanager-domain-help": "Домен за спољашњу потврду идентитета.",
index 302e8b0..f2532a2 100644 (file)
@@ -81,7 +81,9 @@
                        "Bengtsson96",
                        "Nirmos (Wikimedia)",
                        "Psl85",
-                       "Sturban"
+                       "Sturban",
+                       "Taylor",
+                       "Mjälten"
                ]
        },
        "tog-underline": "Stryk under länkar:",
        "history": "Sidhistorik",
        "history_short": "Historik",
        "history_small": "historik",
-       "updatedmarker": "uppdaterad sedan senaste besöket",
+       "updatedmarker": "uppdaterad sedan ditt senaste besök",
        "printableversion": "Utskriftsvänlig version",
        "permalink": "Permanent länk",
        "print": "Skriv ut",
        "autoblockedtext": "Din IP-adress har blockerats automatiskt eftersom den har använts av en annan användare som blockerats av $1.\nMotiveringen av blockeringen var:\n\n:''$2''\n\n* Blockeringen startade: $8\n* Blockeringen gäller till: $6\n* Blockeringen är avsedd för: $7\n\nDu kan kontakta $1 eller någon annan [[{{MediaWiki:Grouppage-sysop}}|administratör]] för att diskutera blockeringen.\n\nObservera att du inte kan använda dig av funktionen \"{{int:emailuser}}\" om du inte har registrerat en giltig e-postadress i [[Special:Preferences|dina inställningar]] eller om du har blivit blockerad från att skicka e-post.\n\nDin nuvarande IP-adress är $3, och blockerings-ID är #$5.\nVänligen ange informationen ovan i alla förfrågningar som du gör i ärendet.",
        "systemblockedtext": "Ditt användarnamn eller IP-adress h    ar blockerats automatiskt av MediaWiki.\n\nMotiveringen av blockeringen var:\n\n:<em>$2</em>\n\n* Blockeringen startade: $8\n* Blockeringen gäller till: $6\n* Blockeringen är avsedd för: $7\n\nDin nuvarande IP-adress är $3.\nVänligen ange informationen ovan i alla förfrågningar som du gör i ärendet.",
        "blockednoreason": "ingen motivering angavs",
+       "blockedtext-composite": "<strong>Ditt användarnamn eller din IP-adress har blockerats.</strong>\n\nMotiveringen till detta är:\n\n<em>$2</em>.\n\n* Blockeringen startade: $8\n* Den längsta blockeringen gäller till: $6\n\nDin nuvarande IP-adress är $3.\n\nVänligen ange all informationen ovan i förfrågningar som du gör i ärendet.",
+       "blockedtext-composite-reason": "Det föreligger flera blockeringar mot ditt konto eller din IP-adress.",
        "whitelistedittext": "Vänligen $1 för att redigera sidor.",
        "confirmedittext": "Du måste bekräfta din e-postadress innan du kan redigera sidor. Var vänlig ställ in och validera din e-postadress genom dina [[Special:Preferences|användarinställningar]].",
        "nosuchsectiontitle": "Kan inte hitta avsnitt",
        "rcfilters-view-tags-tooltip": "Filtrera resultat med redigeringsmärken",
        "rcfilters-view-return-to-default-tooltip": "Återvänd till huvudfiltreringsmenyn",
        "rcfilters-view-tags-help-icon-tooltip": "Läs mer om taggade redigeringar",
-       "rcfilters-liveupdates-button": "Liveuppdateringar",
-       "rcfilters-liveupdates-button-title-on": "Stäng av liveuppdateringar",
+       "rcfilters-liveupdates-button": "Realtidsuppdateringar",
+       "rcfilters-liveupdates-button-title-on": "Stäng av uppdateringar i realtid",
        "rcfilters-liveupdates-button-title-off": "Visa nya ändringar när de händer",
        "rcfilters-watchlist-markseen-button": "Markera alla ändringar som sedda",
        "rcfilters-watchlist-edit-watchlist-button": "Redigera din lista över bevakade sidor",
        "uploaded-setting-href-svg": "Användning av taggen \"set\" för att lägga till attributen \"href\" till överordnade element blockeras.",
        "uploaded-wrong-setting-svg": "Användning av \"set\"-taggen för att lägga till ett remote-/data-/skriptmål till något attribut är blokerat. Hittade <code>&lt;set to=\"$1\"&gt;</code> i den uppladdade SVG-filen.",
        "uploaded-setting-handler-svg": "SVG som anger \"handler\"-attributet med remote/data/skript är blockerat. Hittade <code>$1=\"$2\"</code> i den uppladdade SVG-filen.",
-       "uploaded-remote-url-svg": "SVG som anger style-attributet med en fjärr-URL är blockerat. Hittade <code>$1=\"$2\"</code> i den uppladdade SVG-filen.",
+       "uploaded-remote-url-svg": "SVG som anger stilattributet med en fjärr-URL är blockerat. Hittade <code>$1=\"$2\"</code> i den uppladdade SVG-filen.",
        "uploaded-image-filter-svg": "Hittade bildfilter med URL: <code>&lt;$1 $2=\"$3\"&gt;</code> i den uppladdade SVG-filen.",
        "uploadscriptednamespace": "Denna SVG-fil innehåller den ogiltiga namnrymden \"<nowiki>$1</nowiki>\".",
        "uploadinvalidxml": "XML-koden i den uppladdade filen kunde inte tolkas.",
index a374adc..1bcca5e 100644 (file)
        "recentchanges-submit": "Onyesha",
        "rcfilters-activefilters-hide": "Ficha",
        "rcfilters-activefilters-show": "Onyesha",
+       "rcfilters-days-show-days": "{{PLURAL:$1|siku}} $1",
        "rcfilters-savedqueries-rename": "Badili jina",
        "rcfilters-savedqueries-remove": "Ondoa",
        "rcfilters-savedqueries-new-name-label": "Jina",
        "minutes": "dakika {{PLURAL:$1|$1}}",
        "hours": "{{PLURAL:$1|saa $1|masaa $1}}",
        "days": "siku {{PLURAL:$1|$1}}",
+       "weeks": "{{PLURAL:$1|wiki}} $1",
        "ago": "$1 zilizopita",
        "hours-ago": "{{PLURAL:$1|saa $1 iliyo|masaa $1 yaliyo}}pita",
        "minutes-ago": "dakika $1 {{PLURAL:$1|iliyo|zilizo}}pita",
index ffce23c..7158abb 100644 (file)
        "autoblockedtext": "IP adresiniz otomatik olarak engellendi, çünkü $1 tarafından engellenmiş başka bir kullanıcı tarafından kullanılmaktaydı.\nBelirtilen sebep şudur:\n\n:<em>$2</em>\n\n* Engellemenin başlangıcı: $8\n* Engellemenin bitişi: $6\n* Bloke edilmesi istenen: $7\n\nEngelleme hakkında tartışmak için $1 ile veya diğer [[{{MediaWiki:Grouppage-sysop}}|hizmetlilerden]] biriyle irtibata geçebilirsiniz.\n\nNot, [[Special:Preferences|kullanıcı tercihlerinize]] geçerli bir e-posta adresi kaydetmediyseniz  \"{{int:emailuser}}\" özelliğinden faydalanamayabilirsiniz ve bu özelliği kullanmaktan engellenmediniz.\n\nŞu anki IP numaranız $3 ve engellenme ID'niz #$5.\nLütfen yapacağınız herhangi bir sorguda yukarıdaki bütün detayları bulundurun.",
        "systemblockedtext": "Kullanıcı adınız veya IP adresiniz MediaWiki tarafından otomatik olarak engellendi.\nSebebi:\n\n:<em>$2</em>\n\n* Engelin başlangıcı: $8\n* Engelin süresi: $6\n* Engellenmesi istenen: $7\n\nMevcut IP adresiniz $3.\nLütfen yukarıdaki tüm ayrıntıları, yaptığınız sorgularda belirtin.",
        "blockednoreason": "sebep verilmedi",
+       "blockedtext-composite": "<strong>Kullanıcı adınız veya IP adresiniz engellendi.</strong>\n\nSebebi:\n\n:<em>$2</em>.\n\n* Engel başlama tarihi: $8\n* Engelin süresi: $6\n\nGeçerli IP adresiniz $3.\nLütfen yukarıdaki tüm detayları yaptığınız tüm sorgulara dahil ediniz.",
+       "blockedtext-composite-reason": "Hesabınızda ve/veya IP adresinizde birden fazla engel mevcut.",
        "whitelistedittext": "Değişiklik yapabilmek için $1.",
        "confirmedittext": "Sayfa değiştirmeden önce e-posta adresinizi onaylamalısınız. Lütfen [[Special:Preferences|tercihler]] kısmından e-postanızı ekleyin ve onaylayın.",
        "nosuchsectiontitle": "Bölüm bulunamadı",
        "page_first": "ilk",
        "page_last": "son",
        "histlegend": "Fark seçimi: Karşılaştırmayı istediğiniz 2 sürümün önündeki daireleri işaretleyip, \"{{int:Compareselectedversions}}\" düğmesine basın.<br />\nTanımlar: '''({{int:cur}})''' = son revizyon ile arasındaki fark, '''({{int:last}})''' = bir önceki revizyon ile arasındaki fark, '''{{int:minoreditletter}}''' = küçük değişiklik.",
-       "history-fieldset-title": "Revizyonları filtrele",
+       "history-fieldset-title": "Sürümleri filtrele",
        "history-show-deleted": "Sadece silinen sürümler",
        "histfirst": "en eski",
        "histlast": "en yeni",
        "mergehistory-fail-no-change": "Geçmiş birleştirme hiçbir sürümü birleştirmedi. Lütfen sayfa ve zaman parametrelerini bir kez daha kontrol edin.",
        "mergehistory-fail-permission": "Geçmiş birleştirmek için gerekli izinler yok.",
        "mergehistory-fail-self-merge": "Kaynak ve hedef sayfa aynı.",
+       "mergehistory-fail-timestamps-overlap": "Kaynak revizyonları çakışıyor veya hedef revizyonlarından sonra geliyor.",
        "mergehistory-fail-toobig": "Limit olarak belirlenen $1 {{PLURAL:$1|sürümden|sürümden}} daha fazlasını taşımak gerekeceği için geçmiş birleştirme gerçekleştirilemiyor.",
        "mergehistory-no-source": "Kaynak sayfa $1 bulunmamaktadır.",
        "mergehistory-no-destination": "Hedef sayfa $1 bulunmamaktadır.",
        "action-changetags": "tekil sürümlere veya günlük kayıtlarına etiket ekleme veya çıkarma",
        "action-deletechangetags": "etiketleri veritabanından sil",
        "action-purge": "bu sayfayı temizle",
+       "action-apihighlimits": "API sorgularında daha yüksek sınır kullan",
+       "action-autoconfirmed": "IP-tabanlı hız limitleri etkilenmez",
+       "action-bigdelete": "uzun tarihli sayfaları sil",
+       "action-blockemail": "bir kullanıcının e-posta göndermesini engelle",
+       "action-bot": "otomatik bir işlem gibi muamele gör",
+       "action-editprotected": "\"{{int:protect-level-sysop}}\" olarak korunan sayfalarda değişiklik yap",
+       "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" olarak korunan sayfalarda değişiklik yap",
        "action-editinterface": "Kullanıcı arayüzünü değiştir",
        "action-editusercss": "Diğer kullanıcıların CSS sayfalarını değiştir",
        "action-edituserjson": "Diğer kullanıcıların JSON sayfalarını değiştir",
        "action-edituserjs": "Diğer kullanıcıların JavaScript sayfalarını değiştir",
+       "action-editsitecss": "sitewide CSS düzenle",
+       "action-editsitejson": "sitewide JSON'u düzenle",
+       "action-editsitejs": "sitewide JavaScript'i düzenle",
+       "action-editmyusercss": "kendi kullanıcı CSS dosyaları düzenle",
+       "action-editmyuserjson": "kendi kullanıcı JSON dosyalarını düzenle",
+       "action-editmyuserjs": "kendi kullanıcı JavaScript dosyalarını düzenle",
+       "action-viewsuppressed": "herhangi bir kullanıcıdan saklanan sürümleri göster",
        "action-hideuser": "Herkesten gizleyerek bir kullanıcı adını engelle",
+       "action-ipblock-exempt": "IP engellemelerini, otomatik engellemelerini ve aralık engellemelerini atla",
+       "action-unblockself": "kendi engellini kaldır",
+       "action-noratelimit": "derecelendirme sınırlamalarından etkilenme",
        "nchanges": "$1 {{PLURAL:$1|değişiklik|değişiklik}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|son ziyaretten bu yana}}",
        "enhancedrc-history": "geçmiş",
        "markedaspatrollednotify": "$1 için bu değişiklik kontrol edildi olarak işaretlendi.",
        "markedaspatrollederrornotify": "Kontrol edildi olarak işaretleme başarısız oldu.",
        "patrol-log-page": "Devriye günlüğü",
-       "patrol-log-header": "Bu gözlenmiş revizyonların günlüğüdür.",
+       "patrol-log-header": "Bu onaylanmış sürümlerin günlüğüdür.",
        "confirm-markpatrolled-button": "TAMAM",
        "deletedrevision": "$1 sayılı eski sürüm silindi.",
        "filedeleteerror-short": "$1 dosyanın silinmesinde hata oldu",
        "mw-widgets-categoryselector-add-category-placeholder": "Bir kategori ekle...",
        "mw-widgets-usersmultiselect-placeholder": "Daha fazla ekle...",
        "date-range-from": "Şu tarihten:",
-       "date-range-to": "Bu güne kadar:",
+       "date-range-to": "Şu güne kadar:",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "çerez tabanlı oturumlar",
        "sessionprovider-nocookies": "Çerezler devre dışı olabilir. Çerkezlerin aktif olduğuna emin olun ve yeniden başlatin.",
        "randomrootpage": "Rasgele kök sayfa",
index 3537faa..dc556f2 100644 (file)
        "history": "Історія сторінки",
        "history_short": "Історія",
        "history_small": "історія",
-       "updatedmarker": "оновлено Ð¿Ñ\96Ñ\81лÑ\8f Ð¼ого останнього перегляду",
+       "updatedmarker": "оновлено Ð¿Ñ\96Ñ\81лÑ\8f Ð\92аÑ\88ого останнього перегляду",
        "printableversion": "Версія до друку",
        "permalink": "Постійне посилання",
        "print": "Друк",
        "autoblockedtext": "Ваша IP-адреса автоматично заблокована у зв'язку з тим, що вона раніше використовувалася кимось із користувачів, якого заблокував $1.\nПричина блокування блокування:\n\n:<em>$2</em>\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Блокування виконав: $7\n\nВи можете надіслати листа користувачеві $1 або будь-якому іншому [[{{MediaWiki:Grouppage-sysop}}|адміністратору]], щоб обговорити блокування.\n\nЗверніть увагу, що ви не зможете скористатися функцією \"{{int:emailuser}}\", так як не маєте дійсної електронної пошти, зареєстрованої в [[Special:Preferences|особистих налаштуваннях]], а також якщо вам було заборонено надсилати листи при блокуванні.\n\nВаша поточна IP-адреса — $3, ідентифікатор блокування — #$5. Будь ласка, зазначайте ці дані у своїх запитах.",
        "systemblockedtext": "Ваше ім'я користувача або IP-адресу було автоматично заблоковано MediaWiki.\nВказана причина:\n\n:<em>$2</em>\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Ціль блокування: $7\n\nВаша поточна IP-адреса — $3.\nБудь ласка, додайте всі вказані подробиці до будь-яких запитів, які Ви будете робити.",
        "blockednoreason": "не вказано причини",
+       "blockedtext-composite": "<strong>Ваше ім'я користувача або IP-адресу було заблоковано.</strong>\n\nВказана причина:\n\n:<em>$2</em>.\n\n* Початок блокування: $8\n* Закінчення найдовшого блокування: $6\n\nВаша поточна IP-адреса — $3.\nБудь ласка, додайте всі вказані подробиці до будь-яких запитів, які Ви будете робити.",
+       "blockedtext-composite-reason": "Встановлено кілька блокувань для Вашого облікового запису та/або IP-адреси",
        "whitelistedittext": "Ви повинні $1, щоб редагувати сторінки.",
        "confirmedittext": "Ви повинні підтвердити вашу адресу електронної пошти перед редагуванням сторінок.\nБудь-ласка вкажіть і підтвердіть вашу електронну адресу на [[Special:Preferences|сторінці налаштувань]].",
        "nosuchsectiontitle": "Не вдається знайти розділ",
index abaa944..ff50d0b 100644 (file)
        "undelete-revision": "Revision scancelà de la pagina $1 (inserìa su $4 el $5) de $3:",
        "undeleterevision-missing": "Revision mìa valida o mancante. O el colegamento no'l xe mìa giusto, opure la revision la xe stà zà ripristinà o eliminà da l'archivio.",
        "undelete-nodiff": "No xe stà catà nissuna revision precedente.",
-       "undeletebtn": "RIPRISTINA!",
+       "undeletebtn": "Ripristina",
        "undeletelink": "varda/ripristina",
        "undeleteviewlink": "varda",
        "undeleteinvert": "Inverti selession",
index 1248a58..d581da9 100644 (file)
        "returnto": "Padà sí $1.",
        "tagline": "Lát'ọwọ́ {{SITENAME}}",
        "help": "Ìrànlọ́wọ́",
+       "help-mediawiki": "Ìrànwọ́ nípa MediaWiki",
        "search": "Àwárí",
+       "search-ignored-headings": "#<!-- fi ìlà yìí sílẹ̀ bó ṣe wà --> <pre>\n# Àwọn àkọlé tí ìwárí kò ní kọbiara sí.\n# Àwọn àtúnṣe tuntun yíò hàn láìpẹ́ lẹ́yìn tí àkọlé bá ti jẹ́ títòjọ.\n# Ẹ ṣe itúntòjọ ojúewé pẹ̀lu àtúnṣe agbòfo.\n# Bí ìlàkọ rẹ̀ yíò ṣe rí nìyí:\n# * Ohun gbogbo láti àmì-lẹ́tà \"#\" títí dé òpin oríìlà jẹ́ àròyé. \n# * Gbogbo oríilà aláìlófo jẹ́ àkọlé gangan tí kò ní kọbiara sí, lẹ́tà gbàngbà àti ohun gbogbo.\nÌtọ́kasí\nÀwọn ìjápọ̀ òde\nẸ tun wo\n#</pre> <!-- fi ìlà yìí sílẹ̀ bó ṣe wà -->",
        "searchbutton": "Àwárí",
        "go": "Rìnsó",
        "searcharticle": "Lọ",
        "laggedslavemode": "'''Ìkìlọ̀:''' Ojúewé náà le mọ́ nìí àwọn àtúnṣe tuntun.",
        "readonly": "Títìpa ibùdó dátà",
        "enterlockreason": "Ẹ ṣàlàyé ìtìpa náà, àti ìgbàtí ẹ rò pé ìtìpa náà yíò kúrò.",
-       "readonlytext": "Ibùdó dátà jẹ́ títìpa sí àwọn ìkówọlé tuntun àti sí àwọn àtúnṣe míràn, bóyá fún ìtọ́jú ibùdó dátà gbogbo ìgbà, lẹ́yìn èyí yíò padà sí ní ṣiṣẹ́.\n\nOlùmójútó tó tìípa ṣe àlàyé yìí: $1",
+       "readonlytext": "Ibùdó dátà tijẹ́ títìpa lásìkò yìí sí àwọn ìkówọlé tuntun àti sí àwọn àtúnṣe míràn, bóyá fún ìṣètọ́jú ibùdó dátà gbogbo ìgbà, lẹ́yìn èyí yíò padà sí ní ṣiṣẹ́.\n\nOlùmójútó tó tìípa ṣe àlàyé yìí: $1",
        "missing-article": "Ibùdó dátà kò rí ìkọ̀wé fún ojúewé kan tóyẹ kí ó rí, pẹ̀lú orúkọ \"$1\" $2.\n\nOhun tó ún fa èyí ní ìtẹ̀lé ìjapọ̀ \"ìyàtọ́\" tótipẹ́ tàbí ìjápọ̀ ìtàn ojúewé tí a ti parẹ́.\n\nTí kì bá ṣe bẹ́ẹ̀, ó lè jẹ́ pé ẹ ti rí àsìṣe nínú atòlànà kọ̀mpútà náà.\nẸjọ̀wọ́ ẹ fi èyí tó [[Special:ListUsers/sysop|alámùójútó]] kan létí, kí ẹ sí mọ́ gbàgbé láti fúun ní URL ọ̀hún.",
        "missingarticle-rev": "(àtúnyẹ̀wò#: $1)",
        "missingarticle-diff": "(Ìyàtọ̀: $1, $2)",
        "badarticleerror": "Ìgbéṣẹ̀ yìí kò ṣe é ṣe lórí ojúewé yìí.",
        "cannotdelete": "Ojúewé tàbí fáìlì \"$1\" kò ṣe é parẹ́.\nOníṣe mìíràn le ti paárẹ́.",
        "cannotdelete-title": "Kò le pa ojúewè \"$1\" rẹ́",
+       "delete-scheduled": "Ojúewé \"$1\" ti jẹ́ pípètò fún ìparẹ́.\nẸ jọ̀wọ́ ẹ mú sùúrù.",
        "delete-hook-aborted": "Hook ti ṣe ìdádúró ìparẹ́.\nKò ṣe àlàyé kankan.",
+       "no-null-revision": "Àtùnyẹ́wò agbòfo fún ojúewé \"$1\" kò ṣe é dásílẹ̀",
        "badtitle": "Àkọ́lé búburú",
        "badtitletext": "Àkọlé ojúewé tí ẹ bèrè fún kò ní ìbáramu, jẹ́ òfo, tàbí áṣìṣe wà nínú ìjápọ̀ àkọlé láàrin èdè tàbí láàrin wiki.\nÓ ṣe é ṣe kó jẹ́pé ó ní ìkan tàbí ọ̀pọ̀ àmi-lẹ́tà tí kò ṣe é lò nínú àkọlé.",
+       "title-invalid-empty": "Àkọlé ojúewé ajẹ́títọrọ ní òfo tàbí ó ní orúkọ fún orúkọàyè nìkàn.",
+       "title-invalid-utf8": "Àkọlé ojúewé ajẹ́títọrọ ní ìtèléùntèlé UTF-8 tí kò yẹ.",
+       "title-invalid-interwiki": "Àkọlé ojúewé ajẹ́títọrọ ní ìjápọ̀ interwiki tí kò ṣe é lò nìnú àkọlé.",
+       "title-invalid-talk-namespace": "Àkọlé ojúewé ajẹ́títọrọ tọ́ka sí ojúewé ọ̀rọ̀ tí kò sí.",
+       "title-invalid-characters": "Àkọlé ojúewé ajẹ́títọrọ ní àwọn àmì-lẹ́tà tí kò yẹ: \"$1\".",
        "perfcached": "Ìwònyí jẹ́ dátà láti inú cache nítoríẹ̀ ó le mọ́ jẹ̀ẹ́ tuntun. Ó pọ̀jùlọ {{PLURAL:$1|èsì kan|èsì $1}} wà nínú cache.",
        "perfcachedts": "Ìwònyí jẹ́ dátà láti inú cache, ọjọ́ tí a ṣe àtúnṣe rẹ̀ gbẹ̀yìn ni $1. Ó pọ̀jùlọ {{PLURAL:$4|èsì kan|èsì $4}} wà nínú cache.",
        "querypage-no-updates": "Àtúnṣe sí ojúewé yìí kò ṣe é ṣe lọ́wọ́lọ́wọ́.\nÀwọn ìpèsè tuntun kò ní hàn báyìí ná.",
        "sig_tip": "Ìtọwọ́bọ̀wé yín pẹ̀lú àsìkò àti déètì",
        "hr_tip": "Ìlà gbọlọjọ (ẹ lọ̀ọ́ pẹ̀lú àkíyèsì)",
        "summary": "Àkótán:",
-       "subject": "Orí ọ̀rọ̀/àkọlé:",
+       "subject": "Ìdálé-ọ̀rọ̀:",
        "minoredit": "Àtúnṣe kékeré nìyí",
        "watchthis": "M'ójútó ojúewé yìí",
        "savearticle": "Ìdásí ojúewé",
+       "savechanges": "Ìfipamọ́ àtúnṣe",
        "publishpage": "Ṣàtẹ̀jáde ojú ewé",
        "publishchanges": "Ṣàtẹ̀jáde àtúnṣe",
+       "savearticle-start": "Ìfipamọ́ ojúewé...",
+       "savechanges-start": "Ìfipamọ́ àtúnṣe...",
+       "publishpage-start": "Ìtẹ̀jáde àtúnṣe...",
+       "publishchanges-start": "Ìtẹ̀jáde àtúnṣe...",
        "preview": "Àyẹ̀wò",
        "showpreview": "Àkọ́yẹ̀wò",
        "showdiff": "Ìfihàn àwọn àtúnṣe",
+       "blankarticle": "<strong>Ìkìlọ̀:</strong> Ojúewé tí ẹ̀ úndá kò ní ùnkankan nínú.\nTí ẹ bá tún tẹ klik \"$1\", ojúewé náà yíò jẹ́dídá sílẹ̀ láì ní ùnkankan nínú.",
        "anoneditwarning": "<strong>Ìkìlọ̀:</strong> Ẹ kò tíì wọlé.\nÀdírẹ́ẹ̀sì IP yín yíò hàn jáde tí ẹ bá ṣe àtùnṣe. Tí ẹ bá <strong>[$1 wọlé]</strong> tàbí <strong>[$2 dá àkópamọ́]</strong>, àwọn àtúnṣe yín yíò hàn pẹ̀lú orúkọ-oníṣe yín, pẹ̀lú àwọn ànfàní míràn.",
        "anonpreviewwarning": "''Ẹ kò tíì wọlé. Àdírẹ́ẹ̀sì IP yín yíò jẹ́ kíkọsílẹ̀ sínú ìwé ìtàn àtúnṣe ojúewé yìí tí ẹ bá ṣàmúpamọ́ rẹ̀.''",
        "missingsummary": "'''Ìránlétí:''' Ẹ kò pèsè àkótán fún àtúnṣe yìí\nTí ẹ bá tẹ Ìmúpamọ́ lẹ́ẹ̀kansi, àtúnṣe yín yíò jẹ̀ mímúpamọ́ láìní kankan.",
+       "selfredirect": "<strong>Ìkìlọ̀:</strong> Ẹ̀ ún ṣàtúnjúwe ojúewé yìí sí ara rẹ̀.\nÓ le jẹ́ pé ọ̀tọ̀ nibi tí ẹ fẹ́ ṣàtúnjúwe rẹ̀ sí, tàbí pé ẹ̀ ún ṣàtúnṣe ojúewé ọ̀tọ̀.\nTí ẹ bá tún tẹ klik \"$1\", àtúnjúwe náà yíò jẹ́ dídá sílẹ̀.",
        "missingcommenttext": "Jọ̀wọ́ fi èrò ọkàn rẹ sílẹ̀.",
        "missingcommentheader": "'''Ìránlétí:''' Ẹ kò pèsè àkọlé/oríọ̀rọ̀ kankan fún àríwí yìí.\nTí ẹ bá tẹ \"$1\" lẹ́ẹ̀kansi, àtúnṣe yín yíò jẹ́ mímúpamọ́ láìní kankan.",
        "summary-preview": "Àkọ́yẹ̀wò àkótán àtúnṣe:",
        "subject-preview": "Àkọ́yẹ̀wò àkọlé ọ̀rọ̀:",
+       "previewerrortext": "Àsìṣe kan ṣẹlẹ̀ nígbà tí à ún gbìyànjú láti ṣàtúngbéyẹ̀wò àwọn àtúnṣe yín.",
        "blockedtitle": "Ìdínà oníṣe",
+       "blocked-email-user": "<strong>Orúkọ oníṣe yín tijẹ́ dídílọ́nà láti fi email ránṣẹ́. Ẹ sì le ṣàtùnṣe àwọn ojúewé míràn lórí wiki yìí.</strong> Ẹ lè wo gbogbo ẹ̀kúnrẹ́rẹ́ ìdínà náà nínú [[Special:MyContributions|àwọn àfikún àdápamọ́]].\n\nÌdínà náà wá látọwọ́ $1.\n\nÌdíẹ̀ tó sọ ni <em>$2</em>.\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Ìparí ìdínà: $6\n* Ẹni tí a fẹ́ dínà: $7\n* ID ìdínà #$5",
+       "blockedtext-partial": "<strong>Orúkọ oníṣe yín tàbí àdírẹ́ẹ̀sì IP yín tijẹ́ dídílọ́nà láti ṣàtúnṣe sí ojúewé yìí. Ẹ sì le ṣàtùnṣe àwọn ojúewé míràn lórí wiki yìí.</strong> Ẹ lè wo gbogbo ẹ̀kúnrẹ́rẹ́ ìdínà náà nínú [[Special:MyContributions|àwọn àfikún àdápamọ́]].\n\nÌdínà náà wá látọwọ́ $1.\n\nÌdíẹ̀ tó sọ ni <em>$2</em>.\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Ìparí ìdínà: $6\n* Ẹni tí a fẹ́ dínà: $7\n* ID ìdínà #$5",
        "blockedtext": "<strong>Orúkọ oníṣe yín tàbí àdírẹ́sì IP yín ti jẹ́ dídílọ́nà.</strong>\n\n$1 ni ó ṣe ìdínà.\nÌdí tó fun ni <em>$2</em>.\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Òpin ìdínà: $6\n* Ẹni tí a fẹ́ dínà: $7\n\nẸ ṣ'èránṣẹ́ sí $1 tàbí [[{{MediaWiki:Grouppage-sysop}}|alámùójútó]] mìíràn láti fọ̀rọ̀wérọ̀ lórí ìdínà ọ̀ún.\nẸ kò le è lo \"{{int:emailuser}}\" àyàfi tí àdírẹ́sì e-mail tó dájú bá wà ní [[Special:Preferences|àwọn ìfẹ́ràn àpamọ́]] yín tí wọn kò sì ti dínà yín láti lò ó.\nÀdírẹ́sì IP yín lọ́wọ́lọ́wọ́ ni $3, bẹ́ ẹ̀ sì ni ID fún ìdínà yín ni #$5.\nẸ jọ̀wọ́ ẹ fi gbogbo ẹ̀kúnrẹ́rẹ́ òkè yìí kún ìbérè tí ẹ bá ṣe.",
-       "autoblockedtext": "Àdírẹ́sì IP yín ti jẹ́ dídílọ́nà ní fúnrararẹ̀ nítorí pé ó jẹ́ lílò látọwọ́ oníṣe míràn tí ó jẹ́ dídílọ́nà látọwọ́ $1.\nÌdíẹ̀ tó ṣe jẹ́ bẹ́ẹ̀ nìyí:\n\n:''$2''\n\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Ìparí ìdínà: $6\n* Ẹni tí a fẹ́ dínà: $7\n\nẸ le ránṣẹ́ sí $1 tàbí ìkan láàrin [[{{MediaWiki:Grouppage-sysop}}|àwọn olùmójútó]] mìíràn láti fọ̀rọ̀wérọ̀ lórí ìdínà ọ̀ún.\n\nÀkíyèsí pé ẹ le mọ́ le lo ìní ''Ẹ fi e-mail ránṣẹ́ sí oníṣe yìí'' tí àdírẹ́sì e-mail tó tọ́ jẹ́ fífilórúkọsílẹ̀ sínú [[Special:Preferences|àwọn ìfẹ́ràn oníṣe]] yín tí wọn kò sì ti dínà yín láti lò ó.\n\nÀdírẹ́sì IP yín lọ́wọ́lọ́wọ́ ni $3, bẹ́ ẹ̀ sì ni ID fún ìdínà yín ni #$5.\nẸ jọ̀wọ́ ẹ fi gbogbo ẹ̀kúnrẹ́rẹ́ òkè yìí pọ̀mọ́ ìbérè tí ẹ bá ṣe.",
+       "autoblockedtext": "Àdírẹ́sì IP yín ti jẹ́ dídílọ́nà ní fúnrararẹ̀ nítorí pé ó jẹ́ lílò látọwọ́ oníṣe míràn tí ó jẹ́ dídílọ́nà látọwọ́ $1.\nÌdíẹ̀ tó ṣe jẹ́ bẹ́ẹ̀ nìyí:\n\n:<em>$2</em>\n\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Ìparí ìdínà: $6\n* Ẹni tí a fẹ́ dínà: $7\n\nẸ le ránṣẹ́ sí $1 tàbí ìkan láàrin [[{{MediaWiki:Grouppage-sysop}}|àwọn olùmójútó]] mìíràn láti fọ̀rọ̀wérọ̀ lórí ìdínà ọ̀ún.\n\nÀkíyèsí pé ẹ le mọ́ le lo ìní \"{{int:emailuser}}\" àyàfi tí ẹ bá ní àdírẹ́sì email tó yẹ nínú [[Special:Preferences|àwọn ìfẹ́ràn oníṣe]] yín tí wọn kò sì ti dínà yín láti lò ó.\n\nÀdírẹ́sì IP yín lọ́wọ́lọ́wọ́ ni $3, bẹ́ ẹ̀ sì ni ID fún ìdínà yín ni #$5.\nẸ jọ̀wọ́ ẹ fi gbogbo ẹ̀kúnrẹ́rẹ́ òkè yìí pọ̀mọ́ ìbérè tí ẹ bá ṣe.",
        "blockednoreason": "kó sí àlàyé kankan",
        "whitelistedittext": "Ẹ gbọ́dọ̀ $1 láti ṣ'àtúnṣe àwọn ojúewé.",
        "confirmedittext": "Ẹ gbọ́dọ̀ ṣe ìmúdájú àdírẹ́ẹ̀sì e-mail yín kí ẹ tó le è mọ ṣ'àtúnṣe àwọn ojúewé.\nẸjọ̀wọ́ ẹ ṣètò bẹ́ sìni ki ẹ fọwọ́sí àdírẹ́ẹ̀sì e-mail nínú [[Special:Preferences|àwọn ìfẹ́ràn ọníṣe]] yín.",
        "histfirst": "pípẹ́jùlọ",
        "histlast": "tuntunjùlọ",
        "historysize": "({{PLURAL:$1|1 byte|$1 bytes}})",
-       "historyempty": "(òfo)",
+       "historyempty": "òfo",
        "history-feed-title": "Ìtàn àtúnyẹ̀wò",
        "history-feed-description": "Ìtàn àtúnyẹ̀wò fún ojúewé yìí ní orí wiki",
        "history-feed-item-nocomment": "$1 ní $2",
        "userrights-expiry-current": "Yíòparí $1",
        "userrights-expiry-none": "Kò ní parí",
        "userrights-expiry": "Ìparí:",
+       "userrights-expiry-options": "ọjọ́ 1:1 day,ọ̀sẹ̀ 1:1 week,oṣù 1:1 month,oṣù 3:3 months,oṣù 6:6 months,ọdún 1:1 year",
        "group": "Ìdìpọ̀:",
        "group-user": "Àwọn oníṣe",
        "group-autoconfirmed": "Àwọn oníṣe aláàmúdájúarawọn",
        "rcfilters-savedqueries-apply-label": "Ìdáálẹ̀ ajọ̀",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Ìdáálẹ̀ ajọ̀ ìbẹ̀rẹ̀",
        "rcfilters-savedqueries-cancel-label": "Fagilé",
+       "rcfilters-filter-humans-label": "Ti ènìyàn (kìí ṣe ti bot)",
+       "rcfilters-filter-pageedits-label": "Àwọn àtúnṣe ojúewé",
+       "rcfilters-filter-pageedits-description": "Àwọn àtúnṣe sí àkóónú wiki, ọ̀rọ̀, àpèjúwe ẹ̀ka...",
+       "rcfilters-filter-newpages-label": "Àwọn ìdá ojúewé",
+       "rcfilters-filter-newpages-description": "Àwọn àtúnṣe tó dá ojúewé tuntun.",
+       "rcfilters-filter-categorization-label": "Àwọn àtúnṣe ẹ̀ka",
+       "rcfilters-liveupdates-button": "Àtúnṣe ìsinsìnyí",
+       "rcfilters-liveupdates-button-title-on": "Pa àtúnṣe ìsinsìnyí dé",
+       "rcfilters-liveupdates-button-title-off": "Ìfihàn àwọn àtúnṣe tuntun bí wọ́n ṣe ún ṣẹlẹ̀",
        "rcnotefrom": "Nísàlẹ̀ ni {{PLURAL:$5|àtúnṣe|àwọn àtúnṣe}} wà láti <strong>$3, $4</strong> (títí dé <strong>$1</strong> ló hàn).",
        "rclistfrom": "Àfihàn àwọn àtúnṣe tuntun nípa bíbẹ̀rẹ̀ láti $3 $2",
        "rcshowhideminor": "$1 àwọn àtúnṣe kékéèké",
        "unusedtemplateswlh": "àwọn ìjápọ̀ míràn",
        "randompage": "Ojúewé àrìnàkò",
        "randompage-nopages": "Kò sí ojúewé kankan nínú {{PLURAL:$2|orúkọàyè|àwọn orúkọàyè}} ìsàlẹ̀ yìí: $1",
+       "randomincategory-nopages": "Kò sí ojúewé kankan nínú ẹ̀ka [[:Category:$1|$1]].",
+       "randomincategory-category": "Ẹ̀ka:",
+       "randomincategory-submit": "Lọ",
        "randomredirect": "Àtúndarí àrìnàkò",
        "randomredirect-nopages": "Kò sí àtúnjúwe kankan nínú orúkọàyè \"$1\".",
        "statistics": "Àwọn statistiki",
        "pager-older-n": "{{PLURAL:$1|pípẹ́jùlọ 1|pípẹ́jùlọ $1}}",
        "suppress": "Alábẹ̀wò",
        "querypage-disabled": "Ojúewé pàtàkì yìí jẹ́ ìdálẹ́kun nítorí ìsiṣẹ́.",
+       "apihelp-no-such-module": "Module \"$1\" kò sí.",
        "booksources": "Àwọn orísun ìwé",
        "booksources-search-legend": "Àwáàrí fún áwọn ìwé ìtọ́ka",
        "booksources-search": "Ṣàwárí",
        "mycontris": "Àwọn àfikún",
        "anoncontribs": "Àwọn àfikún",
        "contribsub2": "Fún {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "Fún {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "Oníṣẹ́ yìí \"$1\" kò forúkọ sílẹ̀",
        "nocontribs": "Kò sí àtúnṣe tuntun tó bá àwárí mu.",
        "uctop": "lówọ́",
        "version-hooks": "Àwọn hook",
        "version-hook-name": "Orúkọ hook",
        "version-version": "($1)",
-       "version-license": "Ìwé àṣẹ",
+       "version-license": "Ìwé-àṣẹ MediaWiki",
+       "version-ext-license": "Ìwé-àṣe",
        "version-poweredby-credits": "Agbára ìṣiṣẹ́ wiki yìí wá látọwọ́ '''[https://www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
        "version-poweredby-others": "àwọn mìíràn",
+       "version-poweredby-translators": "àwọn olùyédèsómíràn translatewiki.net",
        "version-credits-summary": "Ìdùnnú wa ni láti rántí àwọn ẹni wọ̀nyí fún ìdáwọ́lé wọn sí [[Special:Version|MediaWiki]].",
        "version-software": "Atòlànà kọ̀mpútà kíkànsínú",
        "version-software-product": "Èso",
        "htmlform-submit": "Fúnsílẹ̀",
        "htmlform-reset": "Ìdápadà àwọn àtúnṣe",
        "htmlform-selectorother-other": "Òmíràn",
+       "htmlform-date-placeholder": "YYYY-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "YYYY-MM-DD HH:MM:SS",
        "logentry-delete-delete": "$1 pa ojúewé $3 rẹ́",
        "logentry-delete-restore": "$1 ti mú ojúewé $3 ($4) {{GENDER:$2|padàwá}}",
        "logentry-delete-event": "$1 ṣe àyípadà ìhànsí {{PLURAL:$5|ìṣẹ̀lẹ̀ àkọọ́lẹ̀ kan|àwọn ìṣẹ̀lẹ̀ àkọọ́lẹ̀ $5}} lórí $3: $4",
        "special-characters-group-khmer": "Khmer",
        "randomrootpage": "Ojúewé ìtẹ́dìí àrìnàkò",
        "edit-error-short": "Àṣìṣe: $1",
-       "edit-error-long": "Àwọn àsìṣe:\n\n\n$1"
+       "edit-error-long": "Àwọn àsìṣe:\n\n$1"
 }
index 0b3b932..27e1acf 100644 (file)
        "rcfilters-savedqueries-add-new-title": "儲存而家個篩選條件設定",
        "rcfilters-restore-default-filters": "恢復返預設篩選條件",
        "rcfilters-clear-all-filters": "清走全部篩選條件",
-       "rcfilters-show-new-changes": "睇吓最新嘅改動",
+       "rcfilters-show-new-changes": "睇下自$1以來新嘅改動",
        "rcfilters-search-placeholder": "篩選條件最近改動(瀏覽或者開始輸入)",
        "rcfilters-invalid-filter": "無效嘅篩選條件",
        "rcfilters-empty-filter": "無用到篩選條件。顯示晒全部貢獻。",
        "blockip": "封鎖{{GENDER:$1|用戶}}",
        "blockiptext": "使用以下嘅表格嚟去阻止指定嘅IP地址或用戶名嘅寫權限。\n僅當僅當為咗避免有版畀人惡意破壞嘅時候先可以使用,而且唔可以違反[[{{MediaWiki:Policy-url}}|政策]]。\n喺下面填寫阻止嘅確切原因(比如:引用咗某啲已經破壞咗嘅頁面)。\n你可以用[https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]語法格式封鎖 IP 範圍,IPv4最大容許範圍係 /$1,IPv6就係 /$2。",
        "ipaddressorusername": "IP地址或用戶名:",
-       "ipbreason": "原因:",
-       "ipbreason-dropdown": "*å\85±ç\94¨å°\81é\8e\96å\8e\9få\9b \n** æ\8f\92å\85¥é\8c¯å\98\85è³\87æ\96\99\n** é\9f¿é \81é\9d¢åº¦æ\8b\8eèµ°\n** 亂加入外部連結\n** 響頁度加入冇意義嘅嘢\n** 嚇人/騷擾\n** 濫用多個戶口\n** 唔能夠接受嘅用戶名",
+       "ipbreason": "原因",
+       "ipbreason-dropdown": "*常ç\94¨å°\81é\8e\96å\8e\9få\9b \n** å\8a å\85¥é\8c¯å\98\85è³\87æ\96\99\n** é\9f¿é \81é\9d¢åº¦æ\8b\8eèµ°å\86\85容\n** 亂加入外部連結\n** 響頁度加入冇意義嘅嘢\n** 嚇人/騷擾\n** 濫用多個戶口\n** 唔能夠接受嘅用戶名",
        "ipb-hardblock": "唔畀簽到用戶用呢個IP位址去改文",
        "ipbcreateaccount": "防止開新戶口",
        "ipbemailban": "防止用戶傳送電郵",
index a7183f3..c5f1879 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "建议在登录时更改",
        "easydeflate-invaliddeflate": "提供的内容未被适当缩小",
        "unprotected-js": "基于安全原因,JavaScript不能在未保护页面中载入。请在“MediaWiki:”名字空间或者用户子页面中添加JavaScript。",
-       "userlogout-continue": "如果你希望登出请[$1 点这里]。"
+       "userlogout-continue": "您确定要登出吗?"
 }
index c01c98a..39d9be7 100644 (file)
        "autoblockedtext": "因先前的另一位使用者被 $1 封鎖,您的 IP 位址已被自動封鎖。\n原因是:\n\n:<em>$2</em>\n\n* 封鎖開始時間:$8\n* 封鎖結束時間:$6\n* 相關封鎖對象:$7\n\n您可以聯絡 $1 或其他的 [[{{MediaWiki:Grouppage-sysop}}|管理員]] 討論封鎖的相關問題。\n若您已在 [[Special:Preferences|偏好設定]] 中設定了一個有效的電子郵件地址,且尚未被封鎖郵件功能,則您可透過 \"{{int:emailuser}}\" 的功能來聯絡相關管理員。\n您目前的 IP 位址是 $3,此次封鎖的 ID 為 #$5。\n請您在詢問時附註以上詳細資料。",
        "systemblockedtext": "您的使用者名稱或 IP 位址已被 MediaWiki 自動封鎖,原因如下:\n\n:<em>$2</em>\n\n* 封鎖開始時間:$8\n* 封鎖結束時間:$6\n* 被封鎖的使用者:$7\n\n您目前的 IP 位址為 $3。\n請在做詢問時附上以上資訊。",
        "blockednoreason": "未說明原因",
+       "blockedtext-composite": "<strong>您的使用者名稱或 IP 位址已被封鎖。</strong>\n\n原因如下:\n\n:<em>$2</em>\n\n* 封鎖開始時間:$8\n* 最長的封鎖結束時間:$6\n\n您目前的 IP 位址為 $3。\n請在做詢問時附上以上資訊。",
+       "blockedtext-composite-reason": "有多個封鎖目標為您的帳號和/或IP位址",
        "whitelistedittext": "請先 $1 才可編輯頁面。",
        "confirmedittext": "在編輯此頁之前您必須確認您的電子郵件地址。\n請透過 [[Special:Preferences|偏好設定]] 設定並驗證您的電子郵件地址。",
        "nosuchsectiontitle": "找不到章節",
        "passwordpolicies-policyflag-suggestchangeonlogin": "建議在登入時更改",
        "easydeflate-invaliddeflate": "提供的內容未被正常的壓縮",
        "unprotected-js": "基於安全因素,JavaScript 不能從未保護的頁面來載入。請僅在 MediaWiki:命名空間或使用者子頁面中建立 JavaScript。",
-       "userlogout-continue": "若您想要登出請[$1 繼續前至登出頁面]。"
+       "userlogout-continue": "您想要登出嗎?"
 }
index 45afe2a..675d537 100644 (file)
@@ -59,3 +59,4 @@ $magicWords = [
 ];
 
 $separatorTransformTable = [ ',' => '.', '.' => ',' ];
+$linkTrail = '/^([a-zçəğıöşü]+)(.*)$/sDu';
index c1a096f..2cb3770 100644 (file)
@@ -23,7 +23,7 @@
 
 require_once __DIR__ . '/Maintenance.php';
 
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -76,7 +76,7 @@ class ConvertUserOptions extends Maintenance {
        }
 
        /**
-        * @param ResultWrapper $res
+        * @param IResultWrapper $res
         * @param IDatabase $dbw
         * @return null|int
         */
index 2442caa..a1d4e99 100644 (file)
@@ -33,8 +33,13 @@ class DeduplicateArchiveRevId extends LoggedUpdateMaintenance {
 
        protected function doDBUpdates() {
                $this->output( "Deduplicating ar_rev_id...\n" );
-
                $dbw = $this->getDB( DB_MASTER );
+               // Sanity check. If this is a new install, we don't need to do anything here.
+               if ( PopulateArchiveRevId::isNewInstall( $dbw ) ) {
+                       $this->output( "New install, nothing to do here.\n" );
+                       return true;
+               }
+
                PopulateArchiveRevId::checkMysqlAutoIncrementBug( $dbw );
 
                $minId = $dbw->selectField( 'archive', 'MIN(ar_rev_id)', [], __METHOD__ );
index 7d43f21..05dd0d0 100644 (file)
@@ -27,6 +27,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IResultWrapper;
 
 require_once __DIR__ . '/Maintenance.php';
 
@@ -299,7 +300,7 @@ class GenerateSitemap extends Maintenance {
         * Return a database resolution of all the pages in a given namespace
         *
         * @param int $namespace Limit the query to this namespace
-        * @return Resource
+        * @return IResultWrapper
         */
        function getPageRes( $namespace ) {
                return $this->dbr->select( 'page',
index c99aa15..0b5cdf9 100644 (file)
@@ -76,7 +76,7 @@ class ImportTextFiles extends Maintenance {
                                        $this->fatalError( "Fatal error: The file '$arg' does not exist!" );
                                }
                        }
-               };
+               }
 
                $count = count( $files );
                $this->output( "Importing $count pages...\n" );
index b2b14cb..71fff56 100644 (file)
@@ -98,9 +98,6 @@ class MigrateArchiveText extends LoggedUpdateMaintenance {
 
                                                if ( $wgDefaultExternalStore ) {
                                                        $data = ExternalStore::insertToDefault( $data );
-                                                       if ( !$data ) {
-                                                               throw new MWException( "Unable to store text to external storage" );
-                                                       }
                                                        if ( $flags ) {
                                                                $flags .= ',';
                                                        }
index 3c73306..333b8b9 100644 (file)
@@ -28,7 +28,7 @@ require_once __DIR__ . '/Maintenance.php';
 
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IMaintainableDatabase;
 
 /**
@@ -429,7 +429,7 @@ class NamespaceDupes extends Maintenance {
         * @param string $name Prefix that is being made a namespace
         * @param array $options Associative array of validated command-line options
         *
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        private function getTargetList( $ns, $name, $options ) {
                if (
index 96fcebf..c85e194 100644 (file)
@@ -43,6 +43,15 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                $this->setBatchSize( 100 );
        }
 
+       /**
+        * @param IDatabase $dbw
+        * @return bool
+        */
+       public static function isNewInstall( IDatabase $dbw ) {
+               return $dbw->selectRowCount( 'archive' ) === 0 &&
+                       $dbw->selectRowCount( 'revision' ) === 1;
+       }
+
        protected function getUpdateKey() {
                return __CLASS__;
        }
index a264545..f3e373a 100644 (file)
@@ -25,7 +25,7 @@ use MediaWiki\Storage\NameTableStore;
 use MediaWiki\Storage\SqlBlobStore;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 
 require_once __DIR__ . '/Maintenance.php';
 
@@ -239,12 +239,12 @@ class PopulateContentTables extends Maintenance {
        }
 
        /**
-        * @param ResultWrapper $rows
+        * @param IResultWrapper $rows
         * @param int $startId
         * @param string $table
         * @return int|null
         */
-       private function populateContentTablesForRowBatch( ResultWrapper $rows, $startId, $table ) {
+       private function populateContentTablesForRowBatch( IResultWrapper $rows, $startId, $table ) {
                $this->beginTransaction( $this->dbw, __METHOD__ );
 
                if ( $this->contentRowMap === null ) {
index 8f47b16..8ecd810 100644 (file)
@@ -23,7 +23,7 @@
 
 require_once __DIR__ . '/Maintenance.php';
 
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 
 /**
  * Maintenance script that sends purge requests for pages edited in a date
@@ -163,12 +163,12 @@ class PurgeChangedPages extends Maintenance {
         *
         * @todo move this elsewhere
         *
-        * @param ResultWrapper $res Query result sorted by $column (ascending)
+        * @param IResultWrapper $res Query result sorted by $column (ascending)
         * @param string $column
         * @param int $limit
         * @return array (array of rows, string column value)
         */
-       protected function pageableSortedRows( ResultWrapper $res, $column, $limit ) {
+       protected function pageableSortedRows( IResultWrapper $res, $column, $limit ) {
                $rows = iterator_to_array( $res, false );
 
                // Nothing to do
index 6a4cf04..3b0607f 100644 (file)
@@ -25,7 +25,7 @@
 require_once __DIR__ . '/Maintenance.php';
 
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBQueryError;
 
@@ -186,7 +186,7 @@ class MwSql extends Maintenance {
 
        /**
         * Print the results, callback for $db->sourceStream()
-        * @param ResultWrapper|bool $res
+        * @param IResultWrapper|bool $res
         * @param IDatabase $db
         * @return int|null Number of rows selected or updated, or null if the query was unsuccessful.
         */
index 257c6be..f759c13 100644 (file)
@@ -24,6 +24,8 @@
  * @ingroup Maintenance
  */
 
+use Wikimedia\Rdbms\IMaintainableDatabase;
+
 /**
  * Look for duplicate user table entries and optionally prune them.
  *
  * @ingroup Maintenance
  */
 class UserDupes {
+       /**
+        * @var IMaintainableDatabase
+        */
        private $db;
        private $reassigned;
        private $trimmed;
        private $failed;
        private $outputCallback;
 
-       function __construct( &$database, $outputCallback ) {
+       /**
+        * UserDupes constructor.
+        * @param IMaintainableDatabase &$database
+        * @param callback $outputCallback
+        */
+       public function __construct( &$database, $outputCallback ) {
                $this->db = $database;
                $this->outputCallback = $outputCallback;
        }
@@ -57,7 +67,7 @@ class UserDupes {
         * user_name index applied.
         * @return bool
         */
-       function hasUniqueIndex() {
+       public function hasUniqueIndex() {
                $info = $this->db->indexInfo( 'user', 'user_name', __METHOD__ );
                if ( !$info ) {
                        $this->out( "WARNING: doesn't seem to have user_name index at all!\n" );
@@ -82,7 +92,7 @@ class UserDupes {
         *
         * @return bool
         */
-       function clearDupes() {
+       public function clearDupes() {
                return $this->checkDupes( true );
        }
 
@@ -100,7 +110,7 @@ class UserDupes {
         *   from the database; false to just check.
         * @return bool
         */
-       function checkDupes( $doDelete = false ) {
+       private function checkDupes( $doDelete = false ) {
                if ( $this->hasUniqueIndex() ) {
                        echo wfWikiID() . " already has a unique index on its user table.\n";
 
@@ -163,9 +173,8 @@ class UserDupes {
 
        /**
         * We don't want anybody to mess with our stuff...
-        * @private
         */
-       function lock() {
+       private function lock() {
                $set = [ 'user', 'revision' ];
                $names = array_map( [ $this, 'lockTable' ], $set );
                $tables = implode( ',', $names );
@@ -173,23 +182,22 @@ class UserDupes {
                $this->db->query( "LOCK TABLES $tables", __METHOD__ );
        }
 
-       function lockTable( $table ) {
+       private function lockTable( $table ) {
                return $this->db->tableName( $table ) . ' WRITE';
        }
 
        /**
         * @private
         */
-       function unlock() {
+       private function unlock() {
                $this->db->query( "UNLOCK TABLES", __METHOD__ );
        }
 
        /**
         * Grab usernames for which multiple records are present in the database.
         * @return array
-        * @private
         */
-       function getDupes() {
+       private function getDupes() {
                $user = $this->db->tableName( 'user' );
                $result = $this->db->query(
                        "SELECT user_name,COUNT(*) AS n
@@ -211,9 +219,8 @@ class UserDupes {
         * for edits. If the dupes have no edits, we can safely remove them.
         * @param string $name
         * @param bool $doDelete
-        * @private
         */
-       function examine( $name, $doDelete ) {
+       private function examine( $name, $doDelete ) {
                $result = $this->db->select( 'user',
                        [ 'user_id' ],
                        [ 'user_name' => $name ],
@@ -260,9 +267,8 @@ class UserDupes {
         * where it might show up...
         * @param int $userid
         * @return int
-        * @private
         */
-       function editCount( $userid ) {
+       private function editCount( $userid ) {
                return intval( $this->db->selectField(
                        'revision',
                        'COUNT(*)',
@@ -273,9 +279,8 @@ class UserDupes {
        /**
         * @param int $from
         * @param int $to
-        * @private
         */
-       function reassignEdits( $from, $to ) {
+       private function reassignEdits( $from, $to ) {
                $this->out( 'reassigning... ' );
                $this->db->update( 'revision',
                        [ 'rev_user' => $to ],
@@ -287,9 +292,8 @@ class UserDupes {
        /**
         * Remove a user account line.
         * @param int $userid
-        * @private
         */
-       function trimAccount( $userid ) {
+       private function trimAccount( $userid ) {
                $this->out( "deleting..." );
                $this->db->delete( 'user', [ 'user_id' => $userid ], __METHOD__ );
                $this->out( " ok" );
index 8b3b39e..6084c84 100644 (file)
        display: none;
 }
 
+.config-help-field-checkbox {
+       display: none;
+}
+
 /* tooltip styles */
 .config-help-field-hint {
-       display: none;
        margin-left: 2px;
-       margin-bottom: -8px;
        padding: 0 0 0 15px;
        /* @embed */
        background-image: url( images/help-question.gif );
        border: 1px solid #5dc9f4;
        margin-left: 20px;
 }
+
+.config-help-field-checkbox:not( :checked ) ~ .config-help-field-data {
+       display: none;
+}
+
+#p-logo a {
+       background-image: url( images/installer-logo.png );
+}
index 521072e..235ff4a 100644 (file)
                        $label.text( labelText.replace( '$1', value ) );
                }
 
-               // Set up the help system
-               $( '.config-help-field-data' ).hide()
-                       .closest( '.config-help-field-container' ).find( '.config-help-field-hint' )
-                       .show()
-                       .on( 'click', function () {
-                               // FIXME: Use CSS transition
-                               // eslint-disable-next-line no-jquery/no-slide
-                               $( this ).closest( '.config-help-field-container' ).find( '.config-help-field-data' )
-                                       .slideToggle( 'fast' );
-                       } );
-
                // Show/hide code for DB-specific options
                // FIXME: Do we want slow, fast, or even non-animated (instantaneous) showing/hiding here?
                $( '.dbRadio' ).each( function () {
index 09306f6..3e4081a 100644 (file)
@@ -5,7 +5,7 @@
  * familiarise yourself with that CSS before making any changes to this code.
  *
  * Dual licensed:
- * - CC BY 3.0 <http://creativecommons.org/licenses/by/3.0>
+ * - CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>
  * - GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
  *
  * @class jQuery.plugin.makeCollapsible
index 82aa24f..1257f66 100644 (file)
@@ -2,7 +2,7 @@
  * These plugins provide extra functionality for interaction with textareas.
  *
  * - encapsulateSelection: Ported from skins/common/edit.js by Trevor Parscal
- *   © 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
+ *   © 2009 Wikimedia Foundation (GPLv2) - https://www.wikimedia.org
  * - getCaretPosition, scrollToCaretPosition: Ported from Wikia's LinkSuggest extension
  *   https://github.com/Wikia/app/blob/c0cd8b763/extensions/wikia/LinkSuggest/js/jquery.wikia.linksuggest.js
  *   © 2010 Inez Korczyński (korczynski@gmail.com) & Jesús Martínez Novo (martineznovo@gmail.com) (GPLv2)
index c7c061e..4343ecc 100644 (file)
                                q = {};
                                // using replace to iterate over a string
                                if ( uri.query ) {
-                                       uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) {
-                                               var k, v;
-                                               if ( $1 ) {
-                                                       k = Uri.decode( $1 );
-                                                       v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 );
+                                       uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( match, k, eq, v ) {
+                                               if ( k ) {
+                                                       k = Uri.decode( k );
+                                                       v = ( eq === '' || eq === undefined ) ? null : Uri.decode( v );
 
                                                        // If overrideKeys, always (re)set top level value.
                                                        // If not overrideKeys but this key wasn't set before, then we set it as well.
index 198c820..70a8163 100644 (file)
@@ -17,6 +17,7 @@
 
        &-body {
                max-height: 70vh;
+               min-width: 100%;
        }
 
        &-footer {
index 085e22b..ab75653 100644 (file)
@@ -267,11 +267,6 @@ OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
 
 /* Methods */
 
-/**
- * Override parent method to avoid unnecessary resize events.
- */
-FilterTagMultiselectWidget.prototype.updateIfHeightChanged = function () { };
-
 /**
  * Respond to view select widget choose event
  *
index d8b773c..1e9400a 100644 (file)
@@ -43,9 +43,9 @@
 
 /*
  * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
- * Needs an ID so that it's more specific than Vector's div#content h3.
+ * Needs to be more specific than Vector's `.mw-body-content h3`.
  */
-#bodyContent .mw-number-text h3 {
+.mw-body-content .mw-number-text h3 {
        color: #222;
        margin: 0;
        padding: 0;
index c251a86..da048ff 100644 (file)
@@ -74,7 +74,7 @@ function isCompatible( ua ) {
                //
                // Please extend the regex instead of adding new ones!
                // And add a test case to startup.test.js
-               !ua.match( /MSIE 10|webOS\/1\.[0-4]|SymbianOS|Series60|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight|PLAYSTATION|PlayStation/ )
+               !ua.match( /MSIE 10|webOS\/1\.[0-4]|SymbianOS|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight|PLAYSTATION|PlayStation/ )
        );
 }
 
index 861111a..8b6c6d5 100644 (file)
@@ -60,10 +60,11 @@ $wgAutoloadClasses += [
        'MediaWikiPHPUnitResultPrinter' => "$testDir/phpunit/MediaWikiPHPUnitResultPrinter.php",
        'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
        'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
+       'MediaWikiUnitTestCase' => "$testDir/phpunit/MediaWikiUnitTestCase.php",
        'MediaWikiTestResult' => "$testDir/phpunit/MediaWikiTestResult.php",
        'MediaWikiTestRunner' => "$testDir/phpunit/MediaWikiTestRunner.php",
        'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php",
-       'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+       'ResourceLoaderFileModuleTestingSubclass' => "$testDir/phpunit/ResourceLoaderTestCase.php",
        'ResourceLoaderFileTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
        'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php",
        'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
@@ -178,6 +179,7 @@ $wgAutoloadClasses += [
 
        # tests/phpunit/includes/libs
        'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
+       'Wikimedia\ParamValidator\TypeDef\TypeDefTestCase' => "$testDir/phpunit/includes/libs/ParamValidator/TypeDef/TypeDefTestCase.php",
 
        # tests/phpunit/maintenance
        'MediaWiki\Tests\Maintenance\DumpAsserter' => "$testDir/phpunit/maintenance/DumpAsserter.php",
index 3b63c19..7d46e83 100644 (file)
@@ -797,6 +797,13 @@ class ParserTestRunner {
 
                $class = $wgParserConf['class'];
                $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf );
+               if ( $preprocessor ) {
+                       # Suppress deprecation warning for Preprocessor_DOM while testing
+                       Wikimedia\suppressWarnings();
+                       wfDeprecated( 'Preprocessor_DOM::__construct' );
+                       Wikimedia\restoreWarnings();
+                       $parser->getPreprocessor();
+               }
                ParserTestParserHook::setup( $parser );
 
                return $parser;
index f9416be..6c8b51f 100644 (file)
@@ -1596,6 +1596,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * Stub. If a test suite needs to test against a specific database schema, it should
         * override this method and return the appropriate information from it.
         *
+        * 'create', 'drop' and 'alter' in the returned array should list all the tables affected
+        * by the 'scripts', even if the test is only interested in a subset of them, otherwise
+        * the overrides may not be fully cleaned up, leading to errors later.
+        *
         * @param IMaintainableDatabase $db The DB connection to use for the mock schema.
         *        May be used to check the current state of the schema, to determine what
         *        overrides are needed.
diff --git a/tests/phpunit/MediaWikiUnitTestCase.php b/tests/phpunit/MediaWikiUnitTestCase.php
new file mode 100644 (file)
index 0000000..407be20
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Base class for MediaWiki unit tests.
+ *
+ * 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 Testing
+ */
+
+use PHPUnit\Framework\TestCase;
+
+abstract class MediaWikiUnitTestCase extends TestCase {
+       use PHPUnit4And6Compat;
+       use MediaWikiCoversValidator;
+}
index bd6df5f..64693b0 100644 (file)
@@ -157,23 +157,40 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
        }
 }
 
+/**
+ * A more constrained and testable variant of ResourceLoaderFileModule.
+ *
+ * - Implements getLessVars() support.
+ * - Disables database persistance of discovered file dependencies.
+ */
 class ResourceLoaderFileTestModule extends ResourceLoaderFileModule {
        protected $lessVars = [];
 
-       public function __construct( $options = [], $test = [] ) {
-               parent::__construct( $options );
-
-               foreach ( $test as $key => $value ) {
-                       $this->$key = $value;
+       public function __construct( $options = [] ) {
+               if ( isset( $options['lessVars'] ) ) {
+                       $this->lessVars = $options['lessVars'];
+                       unset( $options['lessVars'] );
                }
+
+               parent::__construct( $options );
        }
 
        public function getLessVars( ResourceLoaderContext $context ) {
                return $this->lessVars;
        }
+
+       /** @return array */
+       protected function getFileDependencies( ResourceLoaderContext $context ) {
+               // No-op
+               return [];
+       }
+
+       protected function saveFileDependencies( ResourceLoaderContext $context, $refs ) {
+               // No-op
+       }
 }
 
-class ResourceLoaderFileModuleTestModule extends ResourceLoaderFileModule {
+class ResourceLoaderFileModuleTestingSubclass extends ResourceLoaderFileModule {
 }
 
 class EmptyResourceLoader extends ResourceLoader {
index 6279cf6..68bb1e9 100644 (file)
@@ -6,14 +6,17 @@
  */
 class WfShellExecTest extends MediaWikiTestCase {
        public function testT69870() {
-               $command = wfIsWindows()
-                       // 333 = 331 + CRLF
-                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
-                       : 'printf "%-333333s" "*"';
+               if ( wfIsWindows() ) {
+                       // T209159: Anonymous pipe under Windows does not support asynchronous read and write,
+                       // and the default buffer is too small (~4K), it is easy to be blocked.
+                       $this->markTestSkipped(
+                               'T209159: Anonymous pipe under Windows cannot withstand such a large amount of data'
+                       );
+               }
 
                // Test several times because it involves a race condition that may randomly succeed or fail
                for ( $i = 0; $i < 10; $i++ ) {
-                       $output = wfShellExec( $command );
+                       $output = wfShellExec( 'printf "%-333333s" "*"' );
                        $this->assertEquals( 333333, strlen( $output ) );
                }
        }
index 5d9f63d..f9735c1 100644 (file)
@@ -69,7 +69,7 @@ class WfUrlencodeTest extends MediaWikiTestCase {
                        }
                } else {
                        throw new MWException( __METHOD__ . " given invalid expectation for "
-                               . "'$server'. Should be a string or an array( <http server name> => <string> ).\n" );
+                               . "'$server'. Should be a string or an array [ <http server name> => <string> ].\n" );
                }
        }
 
index 999e0bb..388b914 100644 (file)
@@ -316,7 +316,7 @@ class HtmlTest extends MediaWikiTestCase {
 
        /**
         * How do we handle duplicate keys in HTML attributes expansion?
-        * We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false )
+        * We could pass a "class" the values: 'GREEN' and [ 'GREEN' => false ]
         * The latter will take precedence.
         *
         * Feature added by r96188
index 1272b01..5f0067d 100644 (file)
@@ -1570,34 +1570,15 @@ class OutputPageTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideAddWikiText
-        * @covers OutputPage::addWikiText
         * @covers OutputPage::addWikiTextAsInterface
         * @covers OutputPage::wrapWikiTextAsInterface
         * @covers OutputPage::addWikiTextAsContent
-        * @covers OutputPage::addWikiTextWithTitle
-        * @covers OutputPage::addWikiTextTitle
-        * @covers OutputPage::addWikiTextTidy
-        * @covers OutputPage::addWikiTextTitleTidy
         * @covers OutputPage::getHTML
         */
        public function testAddWikiText( $method, array $args, $expected ) {
                $op = $this->newInstance();
                $this->assertSame( '', $op->getHTML() );
 
-               $this->hideDeprecated( 'OutputPage::addWikiText' );
-               $this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
-               $this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
-               $this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
-               $this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
-               $this->hideDeprecated( 'disabling tidy' );
-
-               if ( in_array(
-                       $method,
-                       [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
-               ) && count( $args ) >= 2 && $args[1] === null ) {
-                       // Special placeholder because we can't get the actual title in the provider
-                       $args[1] = $op->getTitle();
-               }
                if ( in_array(
                        $method,
                        [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
@@ -1612,37 +1593,7 @@ class OutputPageTest extends MediaWikiTestCase {
 
        public function provideAddWikiText() {
                $tests = [
-                       'addWikiText' => [
-                               // Not tidied; this API is deprecated.
-                               'Simple wikitext' => [
-                                       [ "'''Bold'''" ],
-                                       "<p><b>Bold</b>\n</p>",
-                               ], 'List at start' => [
-                                       [ '* List' ],
-                                       "<ul><li>List</li></ul>\n",
-                               ], 'List not at start' => [
-                                       [ '* Not a list', false ],
-                                       '* Not a list',
-                               ], 'Non-interface' => [
-                                       [ "'''Bold'''", true, false ],
-                                       "<p><b>Bold</b>\n</p>",
-                               ], 'No section edit links' => [
-                                       [ '== Title ==' ],
-                                       "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
-                               ],
-                       ],
-                       'addWikiTextWithTitle' => [
-                               // Untidied; this API is deprecated
-                               'With title at start' => [
-                                       [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
-                                       "<ul><li>Some page</li></ul>\n",
-                               ], 'With title at start' => [
-                                       [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
-                                       "* Some page",
-                               ],
-                       ],
                        'addWikiTextAsInterface' => [
-                               // Preferred interface: output is tidied
                                'Simple wikitext' => [
                                        [ "'''Bold'''" ],
                                        "<p><b>Bold</b>\n</p>",
@@ -1670,7 +1621,6 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                        ],
                        'addWikiTextAsContent' => [
-                               // Preferred interface: output is tidied
                                'SpecialNewimages' => [
                                        [ "<p lang='en' dir='ltr'>\nMy message" ],
                                        '<p lang="en" dir="ltr">' . "\nMy message</p>"
@@ -1708,41 +1658,6 @@ class OutputPageTest extends MediaWikiTestCase {
                        ],
                ];
 
-               // Test all the others on addWikiTextTitle as well
-               foreach ( $tests['addWikiText'] as $key => $val ) {
-                       $args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
-                       $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
-                               array_merge( [ $args ], array_slice( $val, 1 ) );
-               }
-               foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
-                       $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
-                       $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
-                               array_merge( [ $args ], array_slice( $val, 1 ) );
-               }
-               foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
-                       $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
-                       $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
-                               array_merge( [ $args ], array_slice( $val, 1 ) );
-               }
-               foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
-                       $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
-                       $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
-                               array_merge( [ $args ], array_slice( $val, 1 ) );
-               }
-               // addWikiTextTidy / addWikiTextTitleTidy were old aliases of
-               // addWikiTextAsContent
-               foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
-                       if ( count( $val[0] ) > 2 ) {
-                               $args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
-                               $tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
-                                       array_merge( [ $args ], array_slice( $val, 1 ) );
-                       } else {
-                               $args = [ $val[0][0], $val[0][1] ?? true ];
-                               $tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
-                                       array_merge( [ $args ], array_slice( $val, 1 ) );
-                       }
-               }
-
                // We have to reformat our array to match what PHPUnit wants
                $ret = [];
                foreach ( $tests as $key => $subarray ) {
@@ -1755,17 +1670,6 @@ class OutputPageTest extends MediaWikiTestCase {
                return $ret;
        }
 
-       /**
-        * @covers OutputPage::addWikiText
-        */
-       public function testAddWikiTextNoTitle() {
-               $this->hideDeprecated( 'OutputPage::addWikiText' );
-               $this->setExpectedException( MWException::class, 'Title is null' );
-
-               $op = $this->newInstance( [], null, 'notitle' );
-               $op->addWikiText( 'a' );
-       }
-
        /**
         * @covers OutputPage::addWikiTextAsInterface
         */
@@ -2342,14 +2246,13 @@ class OutputPageTest extends MediaWikiTestCase {
         *
         * @covers OutputPage::addVaryHeader
         * @covers OutputPage::getVaryHeader
-        * @covers OutputPage::getKeyHeader
         *
         * @param array[] $calls For each array, call addVaryHeader() with those arguments
         * @param string[] $cookies Array of cookie names to vary on
         * @param string $vary Text of expected Vary header (including the 'Vary: ')
         * @param string $key Text of expected Key header (including the 'Key: ')
         */
-       public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
+       public function testVaryHeaders( array $calls, array $cookies, $vary ) {
                // Get rid of default Vary fields
                $op = $this->getMockBuilder( OutputPage::class )
                        ->setConstructorArgs( [ new RequestContext() ] )
@@ -2360,22 +2263,19 @@ class OutputPageTest extends MediaWikiTestCase {
                        ->will( $this->returnValue( $cookies ) );
                TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
 
-               $this->hideDeprecated( '$wgUseKeyHeader' );
+               $this->hideDeprecated( 'addVaryHeader $option is ignored' );
                foreach ( $calls as $call ) {
                        $op->addVaryHeader( ...$call );
                }
                $this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
-               $this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
        }
 
        public function provideVaryHeaders() {
-               // note: getKeyHeader() automatically adds Vary: Cookie
                return [
                        'No header' => [
                                [],
                                [],
                                'Vary: ',
-                               'Key: Cookie',
                        ],
                        'Single header' => [
                                [
@@ -2383,7 +2283,6 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                                [],
                                'Vary: Cookie',
-                               'Key: Cookie',
                        ],
                        'Non-unique headers' => [
                                [
@@ -2393,26 +2292,26 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                                [],
                                'Vary: Cookie, Accept-Language',
-                               'Key: Cookie,Accept-Language',
                        ],
                        'Two headers with single options' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=phpsessid' ] ],
                                        [ 'Accept-Language', [ 'substr=en' ] ],
                                ],
                                [],
                                'Vary: Cookie, Accept-Language',
-                               'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
                        ],
                        'One header with multiple options' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
                                ],
                                [],
                                'Vary: Cookie',
-                               'Key: Cookie;param=phpsessid;param=userId',
                        ],
                        'Duplicate option' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=phpsessid' ] ],
                                        [ 'Cookie', [ 'param=phpsessid' ] ],
@@ -2420,30 +2319,28 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                                [],
                                'Vary: Cookie, Accept-Language',
-                               'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
                        ],
                        'Same header, different options' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=phpsessid' ] ],
                                        [ 'Cookie', [ 'param=userId' ] ],
                                ],
                                [],
                                'Vary: Cookie',
-                               'Key: Cookie;param=phpsessid;param=userId',
                        ],
                        'No header, vary cookies' => [
                                [],
                                [ 'cookie1', 'cookie2' ],
                                'Vary: Cookie',
-                               'Key: Cookie;param=cookie1;param=cookie2',
                        ],
                        'Cookie header with option plus vary cookies' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=cookie1' ] ],
                                ],
                                [ 'cookie2', 'cookie3' ],
                                'Vary: Cookie',
-                               'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
                        ],
                        'Non-cookie header plus vary cookies' => [
                                [
@@ -2451,16 +2348,15 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                                [ 'cookie' ],
                                'Vary: Accept-Language, Cookie',
-                               'Key: Accept-Language,Cookie;param=cookie',
                        ],
                        'Cookie and non-cookie headers plus vary cookies' => [
+                               // Options are deprecated since 1.34
                                [
                                        [ 'Cookie', [ 'param=cookie1' ] ],
                                        [ 'Accept-Language' ],
                                ],
                                [ 'cookie2' ],
                                'Vary: Cookie, Accept-Language',
-                               'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
                        ],
                ];
        }
@@ -2513,10 +2409,9 @@ class OutputPageTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideAddAcceptLanguage
         * @covers OutputPage::addAcceptLanguage
-        * @covers OutputPage::getKeyHeader
         */
        public function testAddAcceptLanguage(
-               $code, array $variants, array $expected, array $options = []
+               $code, array $variants, $expected, array $options = []
        ) {
                $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
                $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
@@ -2540,41 +2435,38 @@ class OutputPageTest extends MediaWikiTestCase {
 
                // This will run addAcceptLanguage()
                $op->sendCacheControl();
-
-               $this->hideDeprecated( '$wgUseKeyHeader' );
-               $keyHeader = $op->getKeyHeader();
-
-               if ( !$expected ) {
-                       $this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
-                       return;
-               }
-
-               $keyHeader = explode( ' ', $keyHeader, 2 )[1];
-               $keyHeader = explode( ',', $keyHeader );
-
-               $acceptLanguage = null;
-               foreach ( $keyHeader as $item ) {
-                       if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
-                               $acceptLanguage = $item;
-                               break;
-                       }
-               }
-
-               $expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
-               $this->assertSame( $expectedString, $acceptLanguage );
+               $this->assertSame( "Vary: $expected", $op->getVaryHeader() );
        }
 
        public function provideAddAcceptLanguage() {
                return [
-                       'No variants' => [ 'en', [ 'en' ], [] ],
-                       'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
+                       'No variants' => [
+                               'en',
+                               [ 'en' ],
+                               'Accept-Encoding, Cookie',
+                       ],
+                       'One simple variant' => [
+                               'en',
+                               [ 'en', 'en-x-piglatin' ],
+                               'Accept-Encoding, Cookie, Accept-Language',
+                       ],
                        'Multiple variants with BCP47 alternatives' => [
                                'zh',
                                [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
-                               [ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
+                               'Accept-Encoding, Cookie, Accept-Language',
+                       ],
+                       'No title' => [
+                               'en',
+                               [ 'en', 'en-x-piglatin' ],
+                               'Accept-Encoding, Cookie',
+                               [ 'notitle' ]
+                       ],
+                       'Variant in URL' => [
+                               'en',
+                               [ 'en', 'en-x-piglatin' ],
+                               'Accept-Encoding, Cookie',
+                               [ 'varianturl' ]
                        ],
-                       'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
-                       'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
                ];
        }
 
@@ -2715,14 +2607,14 @@ class OutputPageTest extends MediaWikiTestCase {
                        [
                                [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
                                "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
-                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts");'
                                        . "});</script>"
                        ],
                        // Multiple only=styles load
                        [
                                [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
 
-                               '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
+                               '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles"/>'
                        ],
                        // Private embed (only=scripts)
                        [
@@ -2747,14 +2639,14 @@ class OutputPageTest extends MediaWikiTestCase {
                        // noscript group
                        [
                                [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
-                               '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
+                               '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.noscript&amp;only=styles"/></noscript>'
                        ],
                        // Load two modules in separate groups
                        [
                                [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
                                "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
-                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar\u0026skin=fallback");'
-                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo\u0026skin=fallback");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar");'
+                                       . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo");'
                                        . "});</script>"
                        ],
                ];
@@ -2819,13 +2711,13 @@ class OutputPageTest extends MediaWikiTestCase {
                        'default logged-out' => [
                                'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
                                '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>',
+                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>',
                        ],
                        'default logged-in' => [
                                '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&amp;skin=fallback"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
+                               '<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"/>',
                        ],
                        'custom modules' => [
                                'exemptStyleModules' => [
@@ -2833,10 +2725,10 @@ class OutputPageTest extends MediaWikiTestCase {
                                        'user' => [ 'user.styles', 'example.user' ],
                                ],
                                '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.site.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;skin=fallback&amp;version=0a56zyi"/>' . "\n" .
-                               '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
+                               '<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"/>',
                        ],
                ];
                // phpcs:enable
diff --git a/tests/phpunit/includes/Rest/EntryPointTest.php b/tests/phpunit/includes/Rest/EntryPointTest.php
new file mode 100644 (file)
index 0000000..4f87a70
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use EmptyBagOStuff;
+use GuzzleHttp\Psr7\Uri;
+use GuzzleHttp\Psr7\Stream;
+use MediaWiki\Rest\Handler;
+use MediaWikiTestCase;
+use MediaWiki\Rest\EntryPoint;
+use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWiki\Rest\Router;
+use WebResponse;
+
+/**
+ * @covers \MediaWiki\Rest\EntryPoint
+ * @covers \MediaWiki\Rest\Router
+ */
+class EntryPointTest extends MediaWikiTestCase {
+       private static $mockHandler;
+
+       private function createRouter() {
+               return new Router(
+                       [ __DIR__ . '/testRoutes.json' ],
+                       [],
+                       '/rest',
+                       new EmptyBagOStuff(),
+                       new ResponseFactory() );
+       }
+
+       private function createWebResponse() {
+               return $this->getMockBuilder( WebResponse::class )
+                       ->setMethods( [ 'header' ] )
+                       ->getMock();
+       }
+
+       public static function mockHandlerHeader() {
+               return new class extends Handler {
+                       public function execute() {
+                               $response = $this->getResponseFactory()->create();
+                               $response->setHeader( 'Foo', 'Bar' );
+                               return $response;
+                       }
+               };
+       }
+
+       public function testHeader() {
+               $webResponse = $this->createWebResponse();
+               $webResponse->expects( $this->any() )
+                       ->method( 'header' )
+                       ->withConsecutive(
+                               [ 'HTTP/1.1 200 OK', true, null ],
+                               [ 'Foo: Bar', true, null ]
+                       );
+
+               $entryPoint = new EntryPoint(
+                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
+                       $webResponse,
+                       $this->createRouter() );
+               $entryPoint->execute();
+               $this->assertTrue( true );
+       }
+
+       public static function mockHandlerBodyRewind() {
+               return new class extends Handler {
+                       public function execute() {
+                               $response = $this->getResponseFactory()->create();
+                               $stream = new Stream( fopen( 'php://memory', 'w+' ) );
+                               $stream->write( 'hello' );
+                               $response->setBody( $stream );
+                               return $response;
+                       }
+               };
+       }
+
+       /**
+        * Make sure EntryPoint rewinds a seekable body stream before reading.
+        */
+       public function testBodyRewind() {
+               $entryPoint = new EntryPoint(
+                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
+                       $this->createWebResponse(),
+                       $this->createRouter() );
+               ob_start();
+               $entryPoint->execute();
+               $this->assertSame( 'hello', ob_get_clean() );
+       }
+
+}
diff --git a/tests/phpunit/includes/Rest/Handler/HelloHandlerTest.php b/tests/phpunit/includes/Rest/Handler/HelloHandlerTest.php
new file mode 100644 (file)
index 0000000..afbaafb
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+namespace MediaWiki\Tests\Rest\Handler;
+
+use EmptyBagOStuff;
+use GuzzleHttp\Psr7\Uri;
+use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWiki\Rest\Router;
+use MediaWikiTestCase;
+
+/**
+ * @covers \MediaWiki\Rest\Handler\HelloHandler
+ */
+class HelloHandlerTest extends MediaWikiTestCase {
+       public static function provideTestViaRouter() {
+               return [
+                       'normal' => [
+                               [
+                                       'method' => 'GET',
+                                       'uri' => self::makeUri( '/user/Tim/hello' ),
+                               ],
+                               [
+                                       'statusCode' => 200,
+                                       'reasonPhrase' => 'OK',
+                                       'protocolVersion' => '1.1',
+                                       'body' => '{"message":"Hello, Tim!"}',
+                               ],
+                       ],
+                       'method not allowed' => [
+                               [
+                                       'method' => 'POST',
+                                       'uri' => self::makeUri( '/user/Tim/hello' ),
+                               ],
+                               [
+                                       'statusCode' => 405,
+                                       'reasonPhrase' => 'Method Not Allowed',
+                                       'protocolVersion' => '1.1',
+                                       'body' => '{"httpCode":405,"httpReason":"Method Not Allowed"}',
+                               ],
+                       ],
+               ];
+       }
+
+       private static function makeUri( $path ) {
+               return new Uri( "http://www.example.com/rest$path" );
+       }
+
+       /** @dataProvider provideTestViaRouter */
+       public function testViaRouter( $requestInfo, $responseInfo ) {
+               $router = new Router(
+                       [ __DIR__ . '/../testRoutes.json' ],
+                       [],
+                       '/rest',
+                       new EmptyBagOStuff(),
+                       new ResponseFactory() );
+               $request = new RequestData( $requestInfo );
+               $response = $router->execute( $request );
+               if ( isset( $responseInfo['statusCode'] ) ) {
+                       $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() );
+               }
+               if ( isset( $responseInfo['reasonPhrase'] ) ) {
+                       $this->assertSame( $responseInfo['reasonPhrase'], $response->getReasonPhrase() );
+               }
+               if ( isset( $responseInfo['protocolVersion'] ) ) {
+                       $this->assertSame( $responseInfo['protocolVersion'], $response->getProtocolVersion() );
+               }
+               if ( isset( $responseInfo['body'] ) ) {
+                       $this->assertSame( $responseInfo['body'], $response->getBody()->getContents() );
+               }
+               $this->assertSame(
+                       [],
+                       array_diff( array_keys( $responseInfo ), [
+                               'statusCode',
+                               'reasonPhrase',
+                               'protocolVersion',
+                               'body'
+                       ] ),
+                       '$responseInfo may not contain unknown keys' );
+       }
+}
diff --git a/tests/phpunit/includes/Rest/HeaderContainerTest.php b/tests/phpunit/includes/Rest/HeaderContainerTest.php
new file mode 100644 (file)
index 0000000..e0dbfdf
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use MediaWikiTestCase;
+use MediaWiki\Rest\HeaderContainer;
+
+/**
+ * @covers \MediaWiki\Rest\HeaderContainer
+ */
+class HeaderContainerTest extends MediaWikiTestCase {
+       public static function provideSetHeader() {
+               return [
+                       'simple' => [
+                               [
+                                       [ 'Test', 'foo' ]
+                               ],
+                               [ 'Test' => [ 'foo' ] ],
+                               [ 'Test' => 'foo' ]
+                       ],
+                       'replace' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'Test', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'bar' ] ],
+                               [ 'Test' => 'bar' ],
+                       ],
+                       'array value' => [
+                               [
+                                       [ 'Test', [ '1', '2' ] ],
+                                       [ 'Test', [ '3', '4' ] ],
+                               ],
+                               [ 'Test' => [ '3', '4' ] ],
+                               [ 'Test' => '3, 4' ]
+                       ],
+                       'preserve most recent case' => [
+                               [
+                                       [ 'test', 'foo' ],
+                                       [ 'tesT', 'bar' ],
+                               ],
+                               [ 'tesT' => [ 'bar' ] ],
+                               [ 'tesT' => 'bar' ]
+                       ],
+                       'empty' => [ [], [], [] ],
+               ];
+       }
+
+       /** @dataProvider provideSetHeader */
+       public function testSetHeader( $setOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $setOps as list( $name, $value ) ) {
+                       $hc->setHeader( $name, $value );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public static function provideAddHeader() {
+               return [
+                       'simple' => [
+                               [
+                                       [ 'Test', 'foo' ]
+                               ],
+                               [ 'Test' => [ 'foo' ] ],
+                               [ 'Test' => 'foo' ]
+                       ],
+                       'add' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'Test', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'foo', 'bar' ] ],
+                               [ 'Test' => 'foo, bar' ],
+                       ],
+                       'array value' => [
+                               [
+                                       [ 'Test', [ '1', '2' ] ],
+                                       [ 'Test', [ '3', '4' ] ],
+                               ],
+                               [ 'Test' => [ '1', '2', '3', '4' ] ],
+                               [ 'Test' => '1, 2, 3, 4' ]
+                       ],
+                       'preserve original case' => [
+                               [
+                                       [ 'Test', 'foo' ],
+                                       [ 'tesT', 'bar' ],
+                               ],
+                               [ 'Test' => [ 'foo', 'bar' ] ],
+                               [ 'Test' => 'foo, bar' ]
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideAddHeader */
+       public function testAddHeader( $addOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $addOps as list( $name, $value ) ) {
+                       $hc->addHeader( $name, $value );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public static function provideRemoveHeader() {
+               return [
+                       'simple' => [
+                               [ [ 'Test', 'foo' ] ],
+                               [ 'Test' ],
+                               [],
+                               []
+                       ],
+                       'case mismatch' => [
+                               [ [ 'Test', 'foo' ] ],
+                               [ 'tesT' ],
+                               [],
+                               []
+                       ],
+                       'remove nonexistent' => [
+                               [ [ 'A', '1' ] ],
+                               [ 'B' ],
+                               [ 'A' => [ '1' ] ],
+                               [ 'A' => '1' ]
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideRemoveHeader */
+       public function testRemoveHeader( $addOps, $removeOps, $headers, $lines ) {
+               $hc = new HeaderContainer;
+               foreach ( $addOps as list( $name, $value ) ) {
+                       $hc->addHeader( $name, $value );
+               }
+               foreach ( $removeOps as $name ) {
+                       $hc->removeHeader( $name );
+               }
+               $this->assertSame( $headers, $hc->getHeaders() );
+               $this->assertSame( $lines, $hc->getHeaderLines() );
+       }
+
+       public function testHasHeader() {
+               $hc = new HeaderContainer;
+               $hc->addHeader( 'A', '1' );
+               $hc->addHeader( 'B', '2' );
+               $hc->addHeader( 'C', '3' );
+               $hc->removeHeader( 'B' );
+               $hc->removeHeader( 'c' );
+               $this->assertTrue( $hc->hasHeader( 'A' ) );
+               $this->assertTrue( $hc->hasHeader( 'a' ) );
+               $this->assertFalse( $hc->hasHeader( 'B' ) );
+               $this->assertFalse( $hc->hasHeader( 'c' ) );
+               $this->assertFalse( $hc->hasHeader( 'C' ) );
+       }
+
+       public function testGetRawHeaderLines() {
+               $hc = new HeaderContainer;
+               $hc->addHeader( 'A', '1' );
+               $hc->addHeader( 'a', '2' );
+               $hc->addHeader( 'b', '3' );
+               $hc->addHeader( 'Set-Cookie', 'x' );
+               $hc->addHeader( 'SET-cookie', 'y' );
+               $this->assertSame(
+                       [
+                               'A: 1, 2',
+                               'b: 3',
+                               'Set-Cookie: x',
+                               'Set-Cookie: y',
+                       ],
+                       $hc->getRawHeaderLines()
+               );
+       }
+}
diff --git a/tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php b/tests/phpunit/includes/Rest/PathTemplateMatcher/PathMatcherTest.php
new file mode 100644 (file)
index 0000000..935cec1
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace MediaWiki\Tests\Rest\PathTemplateMatcher;
+
+use MediaWiki\Rest\PathTemplateMatcher\PathConflict;
+use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
+use MediaWikiTestCase;
+
+/**
+ * @covers \MediaWiki\Rest\PathTemplateMatcher\PathMatcher
+ * @covers \MediaWiki\Rest\PathTemplateMatcher\PathConflict
+ */
+class PathMatcherTest extends MediaWikiTestCase {
+       private static $normalRoutes = [
+               '/a/b',
+               '/b/{x}',
+               '/c/{x}/d',
+               '/c/{x}/e',
+               '/c/{x}/{y}/d',
+       ];
+
+       public static function provideConflictingRoutes() {
+               return [
+                       [ '/a/b', 0, '/a/b' ],
+                       [ '/a/{x}', 0, '/a/b' ],
+                       [ '/{x}/c', 1, '/b/{x}' ],
+                       [ '/b/a', 1, '/b/{x}' ],
+                       [ '/b/{x}', 1, '/b/{x}' ],
+                       [ '/{x}/{y}/d', 2, '/c/{x}/d' ],
+               ];
+       }
+
+       public static function provideMatch() {
+               return [
+                       [ '', false ],
+                       [ '/a/b', [ 'params' => [], 'userData' => 0 ] ],
+                       [ '/b', false ],
+                       [ '/b/1', [ 'params' => [ 'x' => '1' ], 'userData' => 1 ] ],
+                       [ '/c/1/d', [ 'params' => [ 'x' => '1' ], 'userData' => 2 ] ],
+                       [ '/c/1/e', [ 'params' => [ 'x' => '1' ], 'userData' => 3 ] ],
+                       [ '/c/000/e', [ 'params' => [ 'x' => '000' ], 'userData' => 3 ] ],
+                       [ '/c/1/f', false ],
+                       [ '/c//e', [ 'params' => [ 'x' => '' ], 'userData' => 3 ] ],
+                       [ '/c///e', false ],
+               ];
+       }
+
+       public function createNormalRouter() {
+               $pm = new PathMatcher;
+               foreach ( self::$normalRoutes as $i => $route ) {
+                       $pm->add( $route, $i );
+               }
+               return $pm;
+       }
+
+       /** @dataProvider provideConflictingRoutes */
+       public function testAddConflict( $attempt, $expectedUserData, $expectedTemplate ) {
+               $pm = $this->createNormalRouter();
+               $actualTemplate = null;
+               $actualUserData = null;
+               try {
+                       $pm->add( $attempt, 'conflict' );
+               } catch ( PathConflict $pc ) {
+                       $actualTemplate = $pc->existingTemplate;
+                       $actualUserData = $pc->existingUserData;
+               }
+               $this->assertSame( $expectedUserData, $actualUserData );
+               $this->assertSame( $expectedTemplate, $actualTemplate );
+       }
+
+       /** @dataProvider provideMatch */
+       public function testMatch( $path, $expectedResult ) {
+               $pm = $this->createNormalRouter();
+               $result = $pm->match( $path );
+               $this->assertSame( $expectedResult, $result );
+       }
+}
diff --git a/tests/phpunit/includes/Rest/ResponseFactoryTest.php b/tests/phpunit/includes/Rest/ResponseFactoryTest.php
new file mode 100644 (file)
index 0000000..6ccacda
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use ArrayIterator;
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWikiTestCase;
+
+/** @covers \MediaWiki\Rest\ResponseFactory */
+class ResponseFactoryTest extends MediaWikiTestCase {
+       public static function provideEncodeJson() {
+               return [
+                       [ (object)[], '{}' ],
+                       [ '/', '"/"' ],
+                       [ '£', '"£"' ],
+                       [ [], '[]' ],
+               ];
+       }
+
+       /** @dataProvider provideEncodeJson */
+       public function testEncodeJson( $input, $expected ) {
+               $rf = new ResponseFactory;
+               $this->assertSame( $expected, $rf->encodeJson( $input ) );
+       }
+
+       public function testCreateJson() {
+               $rf = new ResponseFactory;
+               $response = $rf->createJson( [] );
+               $response->getBody()->rewind();
+               $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
+               $this->assertSame( '[]', $response->getBody()->getContents() );
+               // Make sure getSize() is functional, since testCreateNoContent() depends on it
+               $this->assertSame( 2, $response->getBody()->getSize() );
+       }
+
+       public function testCreateNoContent() {
+               $rf = new ResponseFactory;
+               $response = $rf->createNoContent();
+               $this->assertSame( [], $response->getHeader( 'Content-Type' ) );
+               $this->assertSame( 0, $response->getBody()->getSize() );
+               $this->assertSame( 204, $response->getStatusCode() );
+       }
+
+       public function testCreatePermanentRedirect() {
+               $rf = new ResponseFactory;
+               $response = $rf->createPermanentRedirect( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 301, $response->getStatusCode() );
+       }
+
+       public function testCreateTemporaryRedirect() {
+               $rf = new ResponseFactory;
+               $response = $rf->createTemporaryRedirect( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 307, $response->getStatusCode() );
+       }
+
+       public function testCreateSeeOther() {
+               $rf = new ResponseFactory;
+               $response = $rf->createSeeOther( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 303, $response->getStatusCode() );
+       }
+
+       public function testCreateNotModified() {
+               $rf = new ResponseFactory;
+               $response = $rf->createNotModified();
+               $this->assertSame( 0, $response->getBody()->getSize() );
+               $this->assertSame( 304, $response->getStatusCode() );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testCreateHttpErrorInvalid() {
+               $rf = new ResponseFactory;
+               $rf->createHttpError( 200 );
+       }
+
+       public function testCreateHttpError() {
+               $rf = new ResponseFactory;
+               $response = $rf->createHttpError( 415, [ 'message' => '...' ] );
+               $this->assertSame( 415, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 415, $data['httpCode'] );
+               $this->assertSame( '...', $data['message'] );
+       }
+
+       public function testCreateFromExceptionUnlogged() {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromException( new HttpException( 'hello', 415 ) );
+               $this->assertSame( 415, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 415, $data['httpCode'] );
+               $this->assertSame( 'hello', $data['message'] );
+       }
+
+       public function testCreateFromExceptionLogged() {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromException( new \Exception( "hello", 415 ) );
+               $this->assertSame( 500, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 500, $data['httpCode'] );
+               $this->assertSame( 'Error: exception of type Exception', $data['message'] );
+       }
+
+       public static function provideCreateFromReturnValue() {
+               return [
+                       [ 'hello', '{"value":"hello"}' ],
+                       [ true, '{"value":true}' ],
+                       [ [ 'x' => 'y' ], '{"x":"y"}' ],
+                       [ [ 'x', 'y' ], '["x","y"]' ],
+                       [ [ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
+                       [ (object)[ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
+                       [ [], '[]' ],
+                       [ (object)[], '{}' ],
+               ];
+       }
+
+       /** @dataProvider provideCreateFromReturnValue */
+       public function testCreateFromReturnValue( $input, $expected ) {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromReturnValue( $input );
+               $body = $response->getBody();
+               $body->rewind();
+               $this->assertSame( $expected, $body->getContents() );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testCreateFromReturnValueInvalid() {
+               $rf = new ResponseFactory;
+               $rf->createFromReturnValue( new ArrayIterator );
+       }
+}
diff --git a/tests/phpunit/includes/Rest/StringStreamTest.php b/tests/phpunit/includes/Rest/StringStreamTest.php
new file mode 100644 (file)
index 0000000..f474643
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use MediaWiki\Rest\StringStream;
+use MediaWikiTestCase;
+
+/** @covers \MediaWiki\Rest\StringStream */
+class StringStreamTest extends MediaWikiTestCase {
+       public static function provideSeekGetContents() {
+               return [
+                       [ 'abcde', 0, SEEK_SET, 'abcde' ],
+                       [ 'abcde', 1, SEEK_SET, 'bcde' ],
+                       [ 'abcde', 5, SEEK_SET, '' ],
+                       [ 'abcde', 1, SEEK_CUR, 'cde' ],
+                       [ 'abcde', 0, SEEK_END, '' ],
+               ];
+       }
+
+       /** @dataProvider provideSeekGetContents */
+       public function testCopyToStream( $input, $offset, $whence, $expected ) {
+               $ss = new StringStream;
+               $ss->write( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $destStream = fopen( 'php://memory', 'w+' );
+               $ss->copyToStream( $destStream );
+               fseek( $destStream, 0 );
+               $result = stream_get_contents( $destStream );
+               $this->assertSame( $expected, $result );
+       }
+
+       public function testGetSize() {
+               $ss = new StringStream;
+               $this->assertSame( 0, $ss->getSize() );
+               $ss->write( "hello" );
+               $this->assertSame( 5, $ss->getSize() );
+               $ss->rewind();
+               $this->assertSame( 5, $ss->getSize() );
+       }
+
+       public function testTell() {
+               $ss = new StringStream;
+               $this->assertSame( $ss->tell(), 0 );
+               $ss->write( "abc" );
+               $this->assertSame( $ss->tell(), 3 );
+               $ss->seek( 0 );
+               $ss->read( 1 );
+               $this->assertSame( $ss->tell(), 1 );
+       }
+
+       public function testEof() {
+               $ss = new StringStream( 'abc' );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertFalse( $ss->eof() );
+               $ss->read( 1 );
+               $this->assertTrue( $ss->eof() );
+               $ss->rewind();
+               $this->assertFalse( $ss->eof() );
+       }
+
+       public function testIsSeekable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isSeekable() );
+       }
+
+       public function testIsReadable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isReadable() );
+       }
+
+       public function testIsWritable() {
+               $ss = new StringStream;
+               $this->assertTrue( $ss->isWritable() );
+       }
+
+       public function testSeekWrite() {
+               $ss = new StringStream;
+               $this->assertSame( '', (string)$ss );
+               $ss->write( 'a' );
+               $this->assertSame( 'a', (string)$ss );
+               $ss->write( 'b' );
+               $this->assertSame( 'ab', (string)$ss );
+               $ss->seek( 1 );
+               $ss->write( 'c' );
+               $this->assertSame( 'ac', (string)$ss );
+       }
+
+       /** @dataProvider provideSeekGetContents */
+       public function testSeekGetContents( $input, $offset, $whence, $expected ) {
+               $ss = new StringStream( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $this->assertSame( $expected, $ss->getContents() );
+       }
+
+       public static function provideSeekRead() {
+               return [
+                       [ 'abcde', 0, SEEK_SET, 1, 'a' ],
+                       [ 'abcde', 0, SEEK_SET, 2, 'ab' ],
+                       [ 'abcde', 4, SEEK_SET, 2, 'e' ],
+                       [ 'abcde', 5, SEEK_SET, 1, '' ],
+                       [ 'abcde', 1, SEEK_CUR, 1, 'c' ],
+                       [ 'abcde', 0, SEEK_END, 1, '' ],
+                       [ 'abcde', -1, SEEK_END, 1, 'e' ],
+               ];
+       }
+
+       /** @dataProvider provideSeekRead */
+       public function testSeekRead( $input, $offset, $whence, $length, $expected ) {
+               $ss = new StringStream( $input );
+               $ss->seek( 1 );
+               $ss->seek( $offset, $whence );
+               $this->assertSame( $expected, $ss->read( $length ) );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testReadBeyondEnd() {
+               $ss = new StringStream( 'abc' );
+               $ss->seek( 1, SEEK_END );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testReadBeforeStart() {
+               $ss = new StringStream( 'abc' );
+               $ss->seek( -1 );
+       }
+}
diff --git a/tests/phpunit/includes/Rest/testRoutes.json b/tests/phpunit/includes/Rest/testRoutes.json
new file mode 100644 (file)
index 0000000..7e43bb0
--- /dev/null
@@ -0,0 +1,14 @@
+[
+       {
+               "path": "/user/{name}/hello",
+               "class": "MediaWiki\\Rest\\Handler\\HelloHandler"
+       },
+       {
+               "path": "/mock/EntryPoint/header",
+               "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerHeader"
+       },
+       {
+               "path": "/mock/EntryPoint/bodyRewind",
+               "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerBodyRewind"
+       }
+]
index aedf292..898a35f 100644 (file)
@@ -11,13 +11,11 @@ use Title;
  */
 class FallbackSlotRoleHandlerTest extends MediaWikiTestCase {
 
+       /**
+        * @return Title
+        */
        private function makeBlankTitleObject() {
-               /** @var Title $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $title;
+               return $this->createMock( Title::class );
        }
 
        /**
index 071ea68..d57625b 100644 (file)
@@ -6,6 +6,7 @@ use CommentStoreComment;
 use Content;
 use Language;
 use LogicException;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\MainSlotRoleHandler;
 use MediaWiki\Revision\RevisionRecord;
@@ -29,6 +30,20 @@ use WikitextContent;
  */
 class RevisionRendererTest extends MediaWikiTestCase {
 
+       /** @var PermissionManager|\PHPUnit_Framework_MockObject_MockObject $permissionManagerMock */
+       private $permissionManagerMock;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->permissionManagerMock = $this->createMock( PermissionManager::class );
+               $this->overrideMwServices( null, [
+                       'PermissionManager' => function (): PermissionManager {
+                               return $this->permissionManagerMock;
+                       }
+               ] );
+       }
+
        /**
         * @param int $articleId
         * @param int $revisionId
@@ -73,10 +88,10 @@ class RevisionRendererTest extends MediaWikiTestCase {
                                        return $mock->getArticleID() === $other->getArticleID();
                                }
                        );
-               $mock->expects( $this->any() )
+               $this->permissionManagerMock->expects( $this->any() )
                        ->method( 'userCan' )
                        ->willReturnCallback(
-                               function ( $perm, User $user ) use ( $mock ) {
+                               function ( $perm, User $user ) {
                                        return $user->isAllowed( $perm );
                                }
                        );
index 138d6bc..4f06ee2 100644 (file)
@@ -151,13 +151,10 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
        }
 
        /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
+        * @return SlotRoleRegistry
         */
        private function getMockSlotRoleRegistry() {
-               $mock = $this->getMockBuilder( SlotRoleRegistry::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               return $mock;
+               return $this->createMock( SlotRoleRegistry::class );
        }
 
        /**
index 5246e36..a8c8581 100644 (file)
@@ -16,7 +16,7 @@ use MediaWikiTestCase;
 use MWException;
 use Title;
 use WANObjectCache;
-use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\TestingAccessWrapper;
 use WikitextContent;
@@ -70,10 +70,10 @@ class RevisionStoreTest extends MediaWikiTestCase {
        }
 
        /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|Database
+        * @return \PHPUnit_Framework_MockObject_MockObject|IDatabase
         */
        private function getMockDatabase() {
-               return $this->getMockBuilder( Database::class )
+               return $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()->getMock();
        }
 
index 67e9464..372a879 100644 (file)
@@ -11,13 +11,11 @@ use Title;
  */
 class SlotRoleHandlerTest extends MediaWikiTestCase {
 
+       /**
+        * @return Title
+        */
        private function makeBlankTitleObject() {
-               /** @var Title $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $title;
+               return $this->createMock( Title::class );
        }
 
        /**
index 4d8030d..c48a33a 100644 (file)
@@ -17,13 +17,11 @@ use Wikimedia\Assert\PostconditionException;
  */
 class SlotRoleRegistryTest extends MediaWikiTestCase {
 
+       /**
+        * @return Title
+        */
        private function makeBlankTitleObject() {
-               /** @var Title $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $title;
+               return $this->createMock( Title::class );
        }
 
        private function makeNameTableStore( array $names = [] ) {
index 6e62afd..37ebf4c 100644 (file)
@@ -237,7 +237,7 @@ class StatusTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @param array $messageDetails E.g. array( 'KEY' => array(/PARAMS/) )
+        * @param array $messageDetails E.g. [ 'KEY' => [ /PARAMS/ ] ]
         * @return Message[]
         */
        protected function getMockMessages( $messageDetails ) {
index ca87b49..47d3b92 100644 (file)
@@ -10,7 +10,7 @@ use MediaWiki\Storage\NameTableStore;
 use MediaWikiTestCase;
 use Psr\Log\NullLogger;
 use WANObjectCache;
-use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\TestingAccessWrapper;
 
@@ -57,37 +57,25 @@ class NameTableStoreTest extends MediaWikiTestCase {
        }
 
        private function getCallCheckingDb( $insertCalls, $selectCalls ) {
-               $mock = $this->getMockBuilder( Database::class )
+               $proxiedMethods = [
+                       'select' => $selectCalls,
+                       'insert' => $insertCalls,
+                       'affectedRows' => null,
+                       'insertId' => null,
+                       'getSessionLagStatus' => null,
+                       'writesPending' => null,
+                       'onTransactionPreCommitOrIdle' => null
+               ];
+               $mock = $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
-               $mock->expects( $this->exactly( $insertCalls ) )
-                       ->method( 'insert' )
-                       ->willReturnCallback( function ( ...$args ) {
-                               return call_user_func_array( [ $this->db, 'insert' ], $args );
-                       } );
-               $mock->expects( $this->exactly( $selectCalls ) )
-                       ->method( 'select' )
-                       ->willReturnCallback( function ( ...$args ) {
-                               return call_user_func_array( [ $this->db, 'select' ], $args );
-                       } );
-               $mock->expects( $this->exactly( $insertCalls ) )
-                       ->method( 'affectedRows' )
-                       ->willReturnCallback( function ( ...$args ) {
-                               return call_user_func_array( [ $this->db, 'affectedRows' ], $args );
-                       } );
-               $mock->expects( $this->any() )
-                       ->method( 'insertId' )
-                       ->willReturnCallback( function ( ...$args ) {
-                               return call_user_func_array( [ $this->db, 'insertId' ], $args );
-                       } );
-               $mock->expects( $this->any() )
-                       ->method( 'query' )
-                       ->willReturn( [] );
-               $mock->expects( $this->any() )
-                       ->method( 'isOpen' )
-                       ->willReturn( true );
-               $wrapper = TestingAccessWrapper::newFromObject( $mock );
-               $wrapper->queryLogger = new NullLogger();
+               foreach ( $proxiedMethods as $method => $count ) {
+                       $mock->expects( is_int( $count ) ? $this->exactly( $count ) : $this->any() )
+                               ->method( $method )
+                               ->willReturnCallback( function ( ...$args ) use ( $method ) {
+                                       return call_user_func_array( [ $this->db, $method ], $args );
+                               } );
+               }
                return $mock;
        }
 
index e50e1bc..fd45732 100644 (file)
@@ -73,8 +73,8 @@ class TestLogger extends \Psr\Log\AbstractLogger {
 
        /**
         * Return the collected logs
-        * @return array Array of array( string $level, string $message ), or
-        *   array( string $level, string $message, array $context ) if $collectContext was true.
+        * @return array Array of [ string $level, string $message ], or
+        *   [ string $level, string $message, array $context ] if $collectContext was true.
         */
        public function getBuffer() {
                return $this->buffer;
index d6c3401..529d9fb 100644 (file)
@@ -338,7 +338,7 @@ class TitleTest extends MediaWikiTestCase {
        public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
                // $wgWhitelistReadRegexp must be an array. Since the provided test cases
                // usually have only one regex, it is more concise to write the lonely regex
-               // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp
+               // as a string. Thus we cast to a [] to honor $wgWhitelistReadRegexp
                // type requisite.
                if ( is_string( $whitelistRegexp ) ) {
                        $whitelistRegexp = [ $whitelistRegexp ];
index f4bab02..6bbdd3b 100644 (file)
@@ -1,10 +1,8 @@
 <?php
 
 /**
- * FIXME Temporary disabled per T225244
  * @group API
  * @group medium
- * @group Broken
  *
  * @covers ApiQueryLanguageinfo
  */
@@ -27,6 +25,7 @@ class ApiQueryLanguageinfoTest extends ApiTestCase {
                                }
                        }
                );
+               Language::clearCaches();
        }
 
        private function doQuery( array $params, $microtimeFunction = null ): array {
index c6ed8a7..47a6d81 100644 (file)
@@ -347,8 +347,6 @@ class ApiStashEditTest extends ApiTestCase {
                $cache = $editStash->cache;
 
                $editInfo = $cache->get( $key );
-               $outputKey = $cache->makeKey( 'stashed-edit-output', $editInfo->outputID );
-               $editInfo->output = $cache->get( $outputKey );
                $editInfo->output->setCacheTime( wfTimestamp( TS_MW,
                        wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ) - $howOld - 1 ) );
 
index 7869bbd..71a77b6 100644 (file)
@@ -86,7 +86,7 @@ STR;
        /**
         * Checks that the request's result matches the expected results.
         * Assumes no rawcontinue and a complete batch.
-        * @param array $values Array is a two element array( request, expected_results )
+        * @param array $values Array is a two element [ request, expected_results ]
         * @param array|null $session
         * @param bool $appendModule
         * @param User|null $user
index 39a5534..40fe4c8 100644 (file)
@@ -2,6 +2,7 @@
 
 use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Block\SystemBlock;
 
 /**
  * @group Blocking
@@ -288,4 +289,38 @@ class BlockManagerTest extends MediaWikiTestCase {
                        ],
                ];
        }
+
+       /**
+        * @covers ::getUniqueBlocks
+        */
+       public function testGetUniqueBlocks() {
+               $blockId = 100;
+
+               $class = new ReflectionClass( BlockManager::class );
+               $method = $class->getMethod( 'getUniqueBlocks' );
+               $method->setAccessible( true );
+
+               $blockManager = $this->getBlockManager( [] );
+
+               $block = $this->getMockBuilder( DatabaseBlock::class )
+                       ->setMethods( [ 'getId' ] )
+                       ->getMock();
+               $block->expects( $this->any() )
+                       ->method( 'getId' )
+                       ->willReturn( $blockId );
+
+               $autoblock = $this->getMockBuilder( DatabaseBlock::class )
+                       ->setMethods( [ 'getParentBlockId', 'getType' ] )
+                       ->getMock();
+               $autoblock->expects( $this->any() )
+                       ->method( 'getParentBlockId' )
+                       ->willReturn( $blockId );
+               $autoblock->expects( $this->any() )
+                       ->method( 'getType' )
+                       ->willReturn( DatabaseBlock::TYPE_AUTO );
+
+               $blocks = [ $block, $block, $autoblock, new SystemBlock() ];
+
+               $this->assertSame( 2, count( $method->invoke( $blockManager, $blocks ) ) );
+       }
 }
diff --git a/tests/phpunit/includes/block/CompositeBlockTest.php b/tests/phpunit/includes/block/CompositeBlockTest.php
new file mode 100644 (file)
index 0000000..5cd86b8
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+
+use MediaWiki\Block\BlockRestrictionStore;
+use MediaWiki\Block\CompositeBlock;
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
+use MediaWiki\Block\SystemBlock;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @group Database
+ * @group Blocking
+ * @coversDefaultClass \MediaWiki\Block\CompositeBlock
+ */
+class CompositeBlockTest extends MediaWikiLangTestCase {
+       private function getPartialBlocks() {
+               $sysopId = $this->getTestSysop()->getUser()->getId();
+
+               $userBlock = new Block( [
+                       'address' => $this->getTestUser()->getUser(),
+                       'by' => $sysopId,
+                       'sitewide' => false,
+               ] );
+               $ipBlock = new Block( [
+                       'address' => '127.0.0.1',
+                       'by' => $sysopId,
+                       'sitewide' => false,
+               ] );
+
+               $userBlock->insert();
+               $ipBlock->insert();
+
+               return [
+                       'user' => $userBlock,
+                       'ip' => $ipBlock,
+               ];
+       }
+
+       private function deleteBlocks( $blocks ) {
+               foreach ( $blocks as $block ) {
+                       $block->delete();
+               }
+       }
+
+       /**
+        * @covers ::__construct
+        * @dataProvider provideTestStrictestParametersApplied
+        */
+       public function testStrictestParametersApplied( $blocks, $expected ) {
+               $this->setMwGlobals( [
+                       'wgBlockDisablesLogin' => false,
+                       'wgBlockAllowsUTEdit' => true,
+               ] );
+
+               $block = new CompositeBlock( [
+                       'originalBlocks' => $blocks,
+               ] );
+
+               $this->assertSame( $expected[ 'hideName' ], $block->getHideName() );
+               $this->assertSame( $expected[ 'sitewide' ], $block->isSitewide() );
+               $this->assertSame( $expected[ 'blockEmail' ], $block->isEmailBlocked() );
+               $this->assertSame( $expected[ 'allowUsertalk' ], $block->isUsertalkEditAllowed() );
+       }
+
+       public static function provideTestStrictestParametersApplied() {
+               return [
+                       'Sitewide block and partial block' => [
+                               [
+                                       new Block( [
+                                               'sitewide' => false,
+                                               'blockEmail' => true,
+                                               'allowUsertalk' => true,
+                                       ] ),
+                                       new Block( [
+                                               'sitewide' => true,
+                                               'blockEmail' => false,
+                                               'allowUsertalk' => false,
+                                       ] ),
+                               ],
+                               [
+                                       'hideName' => false,
+                                       'sitewide' => true,
+                                       'blockEmail' => true,
+                                       'allowUsertalk' => false,
+                               ],
+                       ],
+                       'Partial block and system block' => [
+                               [
+                                       new Block( [
+                                               'sitewide' => false,
+                                               'blockEmail' => true,
+                                               'allowUsertalk' => false,
+                                       ] ),
+                                       new SystemBlock( [
+                                               'systemBlock' => 'proxy',
+                                       ] ),
+                               ],
+                               [
+                                       'hideName' => false,
+                                       'sitewide' => true,
+                                       'blockEmail' => true,
+                                       'allowUsertalk' => false,
+                               ],
+                       ],
+                       'System block and user name hiding block' => [
+                               [
+                                       new Block( [
+                                               'hideName' => true,
+                                               'sitewide' => true,
+                                               'blockEmail' => true,
+                                               'allowUsertalk' => false,
+                                       ] ),
+                                       new SystemBlock( [
+                                               'systemBlock' => 'proxy',
+                                       ] ),
+                               ],
+                               [
+                                       'hideName' => true,
+                                       'sitewide' => true,
+                                       'blockEmail' => true,
+                                       'allowUsertalk' => false,
+                               ],
+                       ],
+                       'Two lenient partial blocks' => [
+                               [
+                                       new Block( [
+                                               'sitewide' => false,
+                                               'blockEmail' => false,
+                                               'allowUsertalk' => true,
+                                       ] ),
+                                       new Block( [
+                                               'sitewide' => false,
+                                               'blockEmail' => false,
+                                               'allowUsertalk' => true,
+                                       ] ),
+                               ],
+                               [
+                                       'hideName' => false,
+                                       'sitewide' => false,
+                                       'blockEmail' => false,
+                                       'allowUsertalk' => true,
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers ::appliesToTitle
+        */
+       public function testBlockAppliesToTitle() {
+               $this->setMwGlobals( [
+                       'wgBlockDisablesLogin' => false,
+               ] );
+
+               $blocks = $this->getPartialBlocks();
+
+               $block = new CompositeBlock( [
+                       'originalBlocks' => $blocks,
+               ] );
+
+               $pageFoo = $this->getExistingTestPage( 'Foo' );
+               $pageBar = $this->getExistingTestPage( 'User:Bar' );
+
+               $this->getBlockRestrictionStore()->insert( [
+                       new PageRestriction( $blocks[ 'user' ]->getId(), $pageFoo->getId() ),
+                       new NamespaceRestriction( $blocks[ 'ip' ]->getId(), NS_USER ),
+               ] );
+
+               $this->assertTrue( $block->appliesToTitle( $pageFoo->getTitle() ) );
+               $this->assertTrue( $block->appliesToTitle( $pageBar->getTitle() ) );
+
+               $this->deleteBlocks( $blocks );
+       }
+
+       /**
+        * @covers ::appliesToUsertalk
+        * @covers ::appliesToPage
+        * @covers ::appliesToNamespace
+        */
+       public function testBlockAppliesToUsertalk() {
+               $this->setMwGlobals( [
+                       'wgBlockAllowsUTEdit' => true,
+                       'wgBlockDisablesLogin' => false,
+               ] );
+
+               $blocks = $this->getPartialBlocks();
+
+               $block = new CompositeBlock( [
+                       'originalBlocks' => $blocks,
+               ] );
+
+               $title = $blocks[ 'user' ]->getTarget()->getTalkPage();
+               $page = $this->getExistingTestPage( 'User talk:' . $title->getText() );
+
+               $this->getBlockRestrictionStore()->insert( [
+                       new PageRestriction( $blocks[ 'user' ]->getId(), $page->getId() ),
+                       new NamespaceRestriction( $blocks[ 'ip' ]->getId(), NS_USER ),
+               ] );
+
+               $this->assertTrue( $block->appliesToUsertalk( $blocks[ 'user' ]->getTarget()->getTalkPage() ) );
+
+               $this->deleteBlocks( $blocks );
+       }
+
+       /**
+        * @covers ::appliesToRight
+        * @dataProvider provideTestBlockAppliesToRight
+        */
+       public function testBlockAppliesToRight( $blocks, $right, $expected ) {
+               $this->setMwGlobals( [
+                       'wgBlockDisablesLogin' => false,
+               ] );
+
+               $block = new CompositeBlock( [
+                       'originalBlocks' => $blocks,
+               ] );
+
+               $this->assertSame( $block->appliesToRight( $right ), $expected );
+       }
+
+       public static function provideTestBlockAppliesToRight() {
+               return [
+                       'Read is not blocked' => [
+                               [
+                                       new Block(),
+                                       new Block(),
+                               ],
+                               'read',
+                               false,
+                       ],
+                       'Email is blocked if blocked by any blocks' => [
+                               [
+                                       new Block( [
+                                               'blockEmail' => true,
+                                       ] ),
+                                       new Block( [
+                                               'blockEmail' => false,
+                                       ] ),
+                               ],
+                               'sendemail',
+                               true,
+                       ],
+               ];
+       }
+
+       /**
+        * Get an instance of BlockRestrictionStore
+        *
+        * @return BlockRestrictionStore
+        */
+       protected function getBlockRestrictionStore() : BlockRestrictionStore {
+               return MediaWikiServices::getInstance()->getBlockRestrictionStore();
+       }
+}
index 857988c..0f5c1f2 100644 (file)
@@ -540,7 +540,7 @@ class DatabaseSqliteTest extends MediaWikiTestCase {
 
                $toString = (string)$db;
 
-               $this->assertContains( 'SQLite ', $toString );
+               $this->assertContains( 'sqlite object', $toString );
        }
 
        /**
index f7007e7..123b080 100644 (file)
@@ -302,6 +302,8 @@ class LBFactoryTest extends MediaWikiTestCase {
                        ->getMock();
                $lb1->method( 'getConnection' )->willReturn( $mockDB1 );
                $lb1->method( 'getServerCount' )->willReturn( 2 );
+               $lb1->method( 'hasReplicaServers' )->willReturn( true );
+               $lb1->method( 'hasStreamingReplicaServers' )->willReturn( true );
                $lb1->method( 'getAnyOpenConnection' )->willReturn( $mockDB1 );
                $lb1->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback(
                                function () use ( $mockDB1 ) {
@@ -327,6 +329,8 @@ class LBFactoryTest extends MediaWikiTestCase {
                        ->getMock();
                $lb2->method( 'getConnection' )->willReturn( $mockDB2 );
                $lb2->method( 'getServerCount' )->willReturn( 2 );
+               $lb2->method( 'hasReplicaServers' )->willReturn( true );
+               $lb2->method( 'hasStreamingReplicaServers' )->willReturn( true );
                $lb2->method( 'getAnyOpenConnection' )->willReturn( $mockDB2 );
                $lb2->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback(
                        function () use ( $mockDB2 ) {
@@ -373,6 +377,8 @@ class LBFactoryTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()
                        ->getMock();
                $lb1->method( 'getServerCount' )->willReturn( 2 );
+               $lb1->method( 'hasReplicaServers' )->willReturn( true );
+               $lb1->method( 'hasStreamingReplicaServers' )->willReturn( true );
                $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
                $lb1->expects( $this->once() )
                        ->method( 'waitFor' )->with( $this->equalTo( $m1Pos ) );
@@ -381,6 +387,8 @@ class LBFactoryTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()
                        ->getMock();
                $lb2->method( 'getServerCount' )->willReturn( 2 );
+               $lb2->method( 'hasReplicaServers' )->willReturn( true );
+               $lb2->method( 'hasStreamingReplicaServers' )->willReturn( true );
                $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
                $lb2->expects( $this->once() )
                        ->method( 'waitFor' )->with( $this->equalTo( $m2Pos ) );
index 7fc070c..1645b85 100644 (file)
@@ -26,9 +26,11 @@ use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\Rdbms\LoadMonitorNull;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @group Database
+ * @group medium
  * @covers \Wikimedia\Rdbms\LoadBalancer
  */
 class LoadBalancerTest extends MediaWikiTestCase {
@@ -112,6 +114,10 @@ class LoadBalancerTest extends MediaWikiTestCase {
                // Simulate web request with DBO_TRX
                $lb = $this->newMultiServerLocalLoadBalancer( DBO_TRX );
 
+               $this->assertEquals( 8, $lb->getServerCount() );
+               $this->assertTrue( $lb->hasReplicaServers() );
+               $this->assertTrue( $lb->hasStreamingReplicaServers() );
+
                $dbw = $lb->getConnection( DB_MASTER );
                $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
                $this->assertEquals(
@@ -165,7 +171,8 @@ class LoadBalancerTest extends MediaWikiTestCase {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
 
                $servers = [
-                       [ // master
+                       // Master DB
+                       0 => [
                                'host' => $wgDBserver,
                                'dbname' => $wgDBname,
                                'tablePrefix' => $this->dbPrefix(),
@@ -176,7 +183,8 @@ class LoadBalancerTest extends MediaWikiTestCase {
                                'load' => 0,
                                'flags' => $flags
                        ],
-                       [ // emulated replica
+                       // Main replica DBs
+                       1 => [
                                'host' => $wgDBserver,
                                'dbname' => $wgDBname,
                                'tablePrefix' => $this->dbPrefix(),
@@ -186,6 +194,93 @@ class LoadBalancerTest extends MediaWikiTestCase {
                                'dbDirectory' => $wgSQLiteDataDir,
                                'load' => 100,
                                'flags' => $flags
+                       ],
+                       2 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 100,
+                               'flags' => $flags
+                       ],
+                       // RC replica DBs
+                       3 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 0,
+                               'groupLoads' => [
+                                       'recentchanges' => 100,
+                                       'watchlist' => 100
+                               ],
+                               'flags' => $flags
+                       ],
+                       // Logging replica DBs
+                       4 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 0,
+                               'groupLoads' => [
+                                       'logging' => 100
+                               ],
+                               'flags' => $flags
+                       ],
+                       5 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 0,
+                               'groupLoads' => [
+                                       'logging' => 100
+                               ],
+                               'flags' => $flags
+                       ],
+                       // Maintenance query replica DBs
+                       6 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 0,
+                               'groupLoads' => [
+                                       'vslow' => 100
+                               ],
+                               'flags' => $flags
+                       ],
+                       // Replica DB that only has a copy of some static tables
+                       7 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 0,
+                               'groupLoads' => [
+                                       'archive' => 100
+                               ],
+                               'flags' => $flags,
+                               'is static' => true
                        ]
                ];
 
@@ -488,4 +583,47 @@ class LoadBalancerTest extends MediaWikiTestCase {
 
                $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
        }
+
+       public function testQueryGroupIndex() {
+               $lb = $this->newMultiServerLocalLoadBalancer();
+               /** @var LoadBalancer $lbWrapper */
+               $lbWrapper = TestingAccessWrapper::newFromObject( $lb );
+
+               $rGeneric = $lb->getConnectionRef( DB_REPLICA );
+               $mainIndexPicked = $rGeneric->getLBInfo( 'serverIndex' );
+
+               $this->assertEquals( $mainIndexPicked, $lbWrapper->getExistingReaderIndex( false ) );
+               $this->assertTrue( in_array( $mainIndexPicked, [ 1, 2 ] ) );
+               for ( $i = 0; $i < 300; ++$i ) {
+                       $rLog = $lb->getConnectionRef( DB_REPLICA, [] );
+                       $this->assertEquals(
+                               $mainIndexPicked,
+                               $rLog->getLBInfo( 'serverIndex' ),
+                               "Main index unchanged" );
+               }
+
+               $rRC = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
+               $rWL = $lb->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
+
+               $this->assertEquals( 3, $rRC->getLBInfo( 'serverIndex' ) );
+               $this->assertEquals( 3, $rWL->getLBInfo( 'serverIndex' ) );
+
+               $rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
+               $logIndexPicked = $rLog->getLBInfo( 'serverIndex' );
+
+               $this->assertEquals( $logIndexPicked, $lbWrapper->getExistingReaderIndex( 'logging' ) );
+               $this->assertTrue( in_array( $logIndexPicked, [ 4, 5 ] ) );
+
+               for ( $i = 0; $i < 300; ++$i ) {
+                       $rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
+                       $this->assertEquals(
+                               $logIndexPicked, $rLog->getLBInfo( 'serverIndex' ), "Index unchanged" );
+               }
+
+               $rVslow = $lb->getConnectionRef( DB_REPLICA, [ 'vslow', 'logging' ] );
+               $vslowIndexPicked = $rVslow->getLBInfo( 'serverIndex' );
+
+               $this->assertEquals( $vslowIndexPicked, $lbWrapper->getExistingReaderIndex( 'vslow' ) );
+               $this->assertEquals( 6, $vslowIndexPicked );
+       }
 }
diff --git a/tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php b/tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php
new file mode 100644 (file)
index 0000000..3c92ecb
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+/** @covers ForeignDBFile */
+class ForeignDBFileTest extends \PHPUnit\Framework\TestCase {
+
+       use PHPUnit4And6Compat;
+
+       public function testShouldConstructCorrectInstanceFromTitle() {
+               $title = Title::makeTitle( NS_FILE, 'Awesome_file' );
+               $repoMock = $this->createMock( LocalRepo::class );
+
+               $file = ForeignDBFile::newFromTitle( $title, $repoMock );
+
+               $this->assertInstanceOf( ForeignDBFile::class, $file );
+       }
+}
index ce07f78..8f8dde5 100644 (file)
@@ -48,7 +48,7 @@ class JobQueueTest extends MediaWikiTestCase {
                        } catch ( MWException $e ) {
                                // unsupported?
                                // @todo What if it was another error?
-                       };
+                       }
                }
        }
 
index acaeb02..4afe3b5 100644 (file)
@@ -316,8 +316,8 @@ EOT;
 
                // Hash of known correct values from C code
                $this->assertEquals(
-                       'c69ac9eb7a8a630c0cded201cefeaace',
-                       md5( $ketama_test( 1e5 ) ),
+                       'd1a4912a80e4654ec2e4e462c8b911c6',
+                       md5( $ketama_test( 1e3 ) ),
                        'Ketama mode (large, MD5 check)'
                );
 
index 9ec53c0..9f2fb1c 100644 (file)
@@ -481,7 +481,7 @@ class IPTest extends PHPUnit\Framework\TestCase {
                $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
 
                // Check internal logic
-               # 0 mask always result in array(0,0)
+               # 0 mask always result in [ 0, 0 ]
                $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) );
                $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) );
                $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) );
diff --git a/tests/phpunit/includes/libs/ParamValidator/ParamValidatorTest.php b/tests/phpunit/includes/libs/ParamValidator/ParamValidatorTest.php
new file mode 100644 (file)
index 0000000..01b1c02
--- /dev/null
@@ -0,0 +1,506 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use Psr\Container\ContainerInterface;
+use Wikimedia\ObjectFactory;
+
+/**
+ * @covers Wikimedia\ParamValidator\ParamValidator
+ */
+class ParamValidatorTest extends \PHPUnit\Framework\TestCase {
+
+       public function testTypeRegistration() {
+               $validator = new ParamValidator(
+                       new SimpleCallbacks( [] ),
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) )
+               );
+               $this->assertSame( array_keys( ParamValidator::$STANDARD_TYPES ), $validator->knownTypes() );
+
+               $validator = new ParamValidator(
+                       new SimpleCallbacks( [] ),
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       [ 'typeDefs' => [ 'foo' => [], 'bar' => [] ] ]
+               );
+               $validator->addTypeDef( 'baz', [] );
+               try {
+                       $validator->addTypeDef( 'baz', [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+               }
+               $validator->overrideTypeDef( 'bar', null );
+               $validator->overrideTypeDef( 'baz', [] );
+               $this->assertSame( [ 'foo', 'baz' ], $validator->knownTypes() );
+
+               $this->assertTrue( $validator->hasTypeDef( 'foo' ) );
+               $this->assertFalse( $validator->hasTypeDef( 'bar' ) );
+               $this->assertTrue( $validator->hasTypeDef( 'baz' ) );
+               $this->assertFalse( $validator->hasTypeDef( 'bazz' ) );
+       }
+
+       public function testGetTypeDef() {
+               $callbacks = new SimpleCallbacks( [] );
+               $factory = $this->getMockBuilder( ObjectFactory::class )
+                       ->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
+                       ->setMethods( [ 'createObject' ] )
+                       ->getMock();
+               $factory->method( 'createObject' )
+                       ->willReturnCallback( function ( $spec, $options ) use ( $callbacks ) {
+                               $this->assertInternalType( 'array', $spec );
+                               $this->assertSame(
+                                       [ 'extraArgs' => [ $callbacks ], 'assertClass' => TypeDef::class ], $options
+                               );
+                               $ret = $this->getMockBuilder( TypeDef::class )
+                                       ->setConstructorArgs( [ $callbacks ] )
+                                       ->getMockForAbstractClass();
+                               $ret->spec = $spec;
+                               return $ret;
+                       } );
+               $validator = new ParamValidator( $callbacks, $factory );
+
+               $def = $validator->getTypeDef( 'boolean' );
+               $this->assertInstanceOf( TypeDef::class, $def );
+               $this->assertSame( ParamValidator::$STANDARD_TYPES['boolean'], $def->spec );
+
+               $def = $validator->getTypeDef( [] );
+               $this->assertInstanceOf( TypeDef::class, $def );
+               $this->assertSame( ParamValidator::$STANDARD_TYPES['enum'], $def->spec );
+
+               $def = $validator->getTypeDef( 'missing' );
+               $this->assertNull( $def );
+       }
+
+       public function testGetTypeDef_caching() {
+               $callbacks = new SimpleCallbacks( [] );
+
+               $mb = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ $callbacks ] );
+               $def1 = $mb->getMockForAbstractClass();
+               $def2 = $mb->getMockForAbstractClass();
+               $this->assertNotSame( $def1, $def2, 'sanity check' );
+
+               $factory = $this->getMockBuilder( ObjectFactory::class )
+                       ->setConstructorArgs( [ $this->getMockForAbstractClass( ContainerInterface::class ) ] )
+                       ->setMethods( [ 'createObject' ] )
+                       ->getMock();
+               $factory->expects( $this->once() )->method( 'createObject' )->willReturn( $def1 );
+
+               $validator = new ParamValidator( $callbacks, $factory, [ 'typeDefs' => [
+                       'foo' => [],
+                       'bar' => $def2,
+               ] ] );
+
+               $this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
+
+               // Second call doesn't re-call ObjectFactory
+               $this->assertSame( $def1, $validator->getTypeDef( 'foo' ) );
+
+               // When registered a TypeDef directly, doesn't call ObjectFactory
+               $this->assertSame( $def2, $validator->getTypeDef( 'bar' ) );
+       }
+
+       /**
+        * @expectedException \UnexpectedValueException
+        * @expectedExceptionMessage Expected instance of Wikimedia\ParamValidator\TypeDef, got stdClass
+        */
+       public function testGetTypeDef_error() {
+               $validator = new ParamValidator(
+                       new SimpleCallbacks( [] ),
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       [ 'typeDefs' => [ 'foo' => [ 'class' => \stdClass::class ] ] ]
+               );
+               $validator->getTypeDef( 'foo' );
+       }
+
+       /** @dataProvider provideNormalizeSettings */
+       public function testNormalizeSettings( $input, $expect ) {
+               $callbacks = new SimpleCallbacks( [] );
+
+               $mb = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ $callbacks ] )
+                       ->setMethods( [ 'normalizeSettings' ] );
+               $mock1 = $mb->getMockForAbstractClass();
+               $mock1->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
+                       $s['foo'] = 'FooBar!';
+                       return $s;
+               } );
+               $mock2 = $mb->getMockForAbstractClass();
+               $mock2->method( 'normalizeSettings' )->willReturnCallback( function ( $s ) {
+                       $s['bar'] = 'FooBar!';
+                       return $s;
+               } );
+
+               $validator = new ParamValidator(
+                       $callbacks,
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       [ 'typeDefs' => [ 'foo' => $mock1, 'bar' => $mock2 ] ]
+               );
+
+               $this->assertSame( $expect, $validator->normalizeSettings( $input ) );
+       }
+
+       public static function provideNormalizeSettings() {
+               return [
+                       'Plain value' => [
+                               'ok?',
+                               [ ParamValidator::PARAM_DEFAULT => 'ok?', ParamValidator::PARAM_TYPE => 'string' ],
+                       ],
+                       'Simple array' => [
+                               [ 'test' => 'ok?' ],
+                               [ 'test' => 'ok?', ParamValidator::PARAM_TYPE => 'NULL' ],
+                       ],
+                       'A type with overrides' => [
+                               [ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?' ],
+                               [ ParamValidator::PARAM_TYPE => 'foo', 'test' => 'ok?', 'foo' => 'FooBar!' ],
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideExplodeMultiValue */
+       public function testExplodeMultiValue( $value, $limit, $expect ) {
+               $this->assertSame( $expect, ParamValidator::explodeMultiValue( $value, $limit ) );
+       }
+
+       public static function provideExplodeMultiValue() {
+               return [
+                       [ 'foobar', 100, [ 'foobar' ] ],
+                       [ 'foo|bar|baz', 100, [ 'foo', 'bar', 'baz' ] ],
+                       [ "\x1Ffoo\x1Fbar\x1Fbaz", 100, [ 'foo', 'bar', 'baz' ] ],
+                       [ 'foo|bar|baz', 2, [ 'foo', 'bar|baz' ] ],
+                       [ "\x1Ffoo\x1Fbar\x1Fbaz", 2, [ 'foo', "bar\x1Fbaz" ] ],
+                       [ '|bar|baz', 100, [ '', 'bar', 'baz' ] ],
+                       [ "\x1F\x1Fbar\x1Fbaz", 100, [ '', 'bar', 'baz' ] ],
+                       [ '', 100, [] ],
+                       [ "\x1F", 100, [] ],
+               ];
+       }
+
+       /**
+        * @expectedException DomainException
+        * @expectedExceptionMessage Param foo's type is unknown - string
+        */
+       public function testGetValue_badType() {
+               $validator = new ParamValidator(
+                       new SimpleCallbacks( [] ),
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       [ 'typeDefs' => [] ]
+               );
+               $validator->getValue( 'foo', 'default', [] );
+       }
+
+       /** @dataProvider provideGetValue */
+       public function testGetValue(
+               $settings, $parseLimit, $get, $value, $isSensitive, $isDeprecated
+       ) {
+               $callbacks = new SimpleCallbacks( $get );
+               $dummy = (object)[];
+               $options = [ $dummy ];
+
+               $settings += [
+                       ParamValidator::PARAM_TYPE => 'xyz',
+                       ParamValidator::PARAM_DEFAULT => null,
+               ];
+
+               $mockDef = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ $callbacks ] )
+                       ->getMockForAbstractClass();
+
+               // Mock the validateValue method so we can test only getValue
+               $validator = $this->getMockBuilder( ParamValidator::class )
+                       ->setConstructorArgs( [
+                               $callbacks,
+                               new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                               [ 'typeDefs' => [ 'xyz' => $mockDef ] ]
+                       ] )
+                       ->setMethods( [ 'validateValue' ] )
+                       ->getMock();
+               $validator->expects( $this->once() )->method( 'validateValue' )
+                       ->with(
+                               $this->identicalTo( 'foobar' ),
+                               $this->identicalTo( $value ),
+                               $this->identicalTo( $settings ),
+                               $this->identicalTo( $options )
+                       )
+                       ->willReturn( $dummy );
+
+               $this->assertSame( $dummy, $validator->getValue( 'foobar', $settings, $options ) );
+
+               $expectConditions = [];
+               if ( $isSensitive ) {
+                       $expectConditions[] = new ValidationException(
+                               'foobar', $value, $settings, 'param-sensitive', []
+                       );
+               }
+               if ( $isDeprecated ) {
+                       $expectConditions[] = new ValidationException(
+                               'foobar', $value, $settings, 'param-deprecated', []
+                       );
+               }
+               $this->assertEquals( $expectConditions, $callbacks->getRecordedConditions() );
+       }
+
+       public static function provideGetValue() {
+               $sen = [ ParamValidator::PARAM_SENSITIVE => true ];
+               $dep = [ ParamValidator::PARAM_DEPRECATED => true ];
+               $dflt = [ ParamValidator::PARAM_DEFAULT => 'DeFaUlT' ];
+               return [
+                       'Simple case' => [ [], false, [ 'foobar' => '!!!' ], '!!!', false, false ],
+                       'Not provided' => [ $sen + $dep, false, [], null, false, false ],
+                       'Not provided, default' => [ $sen + $dep + $dflt, true, [], 'DeFaUlT', false, false ],
+                       'Provided' => [ $dflt, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, false ],
+                       'Provided, sensitive' => [ $sen, false, [ 'foobar' => 'XYZ' ], 'XYZ', true, false ],
+                       'Provided, deprecated' => [ $dep, false, [ 'foobar' => 'XYZ' ], 'XYZ', false, true ],
+                       'Provided array' => [ $dflt, false, [ 'foobar' => [ 'XYZ' ] ], [ 'XYZ' ], false, false ],
+               ];
+       }
+
+       /**
+        * @expectedException DomainException
+        * @expectedExceptionMessage Param foo's type is unknown - string
+        */
+       public function testValidateValue_badType() {
+               $validator = new ParamValidator(
+                       new SimpleCallbacks( [] ),
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       [ 'typeDefs' => [] ]
+               );
+               $validator->validateValue( 'foo', null, 'default', [] );
+       }
+
+       /** @dataProvider provideValidateValue */
+       public function testValidateValue(
+               $value, $settings, $highLimits, $valuesList, $calls, $expect, $expectConditions = [],
+               $constructorOptions = []
+       ) {
+               $callbacks = new SimpleCallbacks( [] );
+               $settings += [
+                       ParamValidator::PARAM_TYPE => 'xyz',
+                       ParamValidator::PARAM_DEFAULT => null,
+               ];
+               $dummy = (object)[];
+               $options = [ $dummy, 'useHighLimits' => $highLimits ];
+               $eOptions = $options;
+               $eOptions2 = $eOptions;
+               if ( $valuesList !== null ) {
+                       $eOptions2['values-list'] = $valuesList;
+               }
+
+               $mockDef = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ $callbacks ] )
+                       ->setMethods( [ 'validate', 'getEnumValues' ] )
+                       ->getMockForAbstractClass();
+               $mockDef->method( 'getEnumValues' )
+                       ->with(
+                               $this->identicalTo( 'foobar' ), $this->identicalTo( $settings ), $this->identicalTo( $eOptions )
+                       )
+                       ->willReturn( [ 'a', 'b', 'c', 'd', 'e', 'f' ] );
+               $mockDef->expects( $this->exactly( count( $calls ) ) )->method( 'validate' )->willReturnCallback(
+                       function ( $n, $v, $s, $o ) use ( $settings, $eOptions2, $calls ) {
+                               $this->assertSame( 'foobar', $n );
+                               $this->assertSame( $settings, $s );
+                               $this->assertSame( $eOptions2, $o );
+
+                               if ( !array_key_exists( $v, $calls ) ) {
+                                       $this->fail( "Called with unexpected value '$v'" );
+                               }
+                               if ( $calls[$v] === null ) {
+                                       throw new ValidationException( $n, $v, $s, 'badvalue', [] );
+                               }
+                               return $calls[$v];
+                       }
+               );
+
+               $validator = new ParamValidator(
+                       $callbacks,
+                       new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
+                       $constructorOptions + [ 'typeDefs' => [ 'xyz' => $mockDef ] ]
+               );
+
+               if ( $expect instanceof ValidationException ) {
+                       try {
+                               $validator->validateValue( 'foobar', $value, $settings, $options );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( ValidationException $ex ) {
+                               $this->assertSame( $expect->getFailureCode(), $ex->getFailureCode() );
+                               $this->assertSame( $expect->getFailureData(), $ex->getFailureData() );
+                       }
+               } else {
+                       $this->assertSame(
+                               $expect, $validator->validateValue( 'foobar', $value, $settings, $options )
+                       );
+
+                       $conditions = [];
+                       foreach ( $callbacks->getRecordedConditions() as $c ) {
+                               $conditions[] = array_merge( [ $c->getFailureCode() ], $c->getFailureData() );
+                       }
+                       $this->assertSame( $expectConditions, $conditions );
+               }
+       }
+
+       public static function provideValidateValue() {
+               return [
+                       'No value' => [ null, [], false, null, [], null ],
+                       'No value, required' => [
+                               null,
+                               [ ParamValidator::PARAM_REQUIRED => true ],
+                               false,
+                               null,
+                               [],
+                               new ValidationException( 'foobar', null, [], 'missingparam', [] ),
+                       ],
+                       'Non-multi value' => [ 'abc', [], false, null, [ 'abc' => 'def' ], 'def' ],
+                       'Simple multi value' => [
+                               'a|b|c|d',
+                               [ ParamValidator::PARAM_ISMULTI => true ],
+                               false,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
+                               [ 'A', 'B', 'C', 'D' ],
+                       ],
+                       'Array multi value' => [
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ ParamValidator::PARAM_ISMULTI => true ],
+                               false,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
+                               [ 'A', 'B', 'C', 'D' ],
+                       ],
+                       'Multi value with PARAM_ALL' => [
+                               '*',
+                               [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
+                               false,
+                               null,
+                               [],
+                               [ 'a', 'b', 'c', 'd', 'e', 'f' ],
+                       ],
+                       'Multi value with PARAM_ALL = "x"' => [
+                               'x',
+                               [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
+                               false,
+                               null,
+                               [],
+                               [ 'a', 'b', 'c', 'd', 'e', 'f' ],
+                       ],
+                       'Multi value with PARAM_ALL = "x", passing "*"' => [
+                               '*',
+                               [ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => "x" ],
+                               false,
+                               [ '*' ],
+                               [ '*' => '?' ],
+                               [ '?' ],
+                       ],
+
+                       'Too many values' => [
+                               'a|b|c|d',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
+                               ],
+                               false,
+                               null,
+                               [],
+                               new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
+                       ],
+                       'Too many values as array' => [
+                               [ 'a', 'b', 'c', 'd' ],
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
+                               ],
+                               false,
+                               null,
+                               [],
+                               new ValidationException(
+                                       'foobar', [ 'a', 'b', 'c', 'd' ], [], 'toomanyvalues', [ 'limit' => 2 ]
+                               ),
+                       ],
+                       'Not too many values for highlimits' => [
+                               'a|b|c|d',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
+                               ],
+                               true,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
+                               [ 'A', 'B', 'C', 'D' ],
+                       ],
+                       'Too many values for highlimits' => [
+                               'a|b|c|d|e',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT1 => 2,
+                                       ParamValidator::PARAM_ISMULTI_LIMIT2 => 4,
+                               ],
+                               true,
+                               null,
+                               [],
+                               new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
+                       ],
+
+                       'Too many values via default' => [
+                               'a|b|c|d',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                               ],
+                               false,
+                               null,
+                               [],
+                               new ValidationException( 'foobar', 'a|b|c|d', [], 'toomanyvalues', [ 'limit' => 2 ] ),
+                               [],
+                               [ 'ismultiLimits' => [ 2, 4 ] ],
+                       ],
+                       'Not too many values for highlimits via default' => [
+                               'a|b|c|d',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                               ],
+                               true,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D' ],
+                               [ 'A', 'B', 'C', 'D' ],
+                               [],
+                               [ 'ismultiLimits' => [ 2, 4 ] ],
+                       ],
+                       'Too many values for highlimits via default' => [
+                               'a|b|c|d|e',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                               ],
+                               true,
+                               null,
+                               [],
+                               new ValidationException( 'foobar', 'a|b|c|d|e', [], 'toomanyvalues', [ 'limit' => 4 ] ),
+                               [],
+                               [ 'ismultiLimits' => [ 2, 4 ] ],
+                       ],
+
+                       'Invalid values' => [
+                               'a|b|c|d',
+                               [ ParamValidator::PARAM_ISMULTI => true ],
+                               false,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => null ],
+                               new ValidationException( 'foobar', 'b', [], 'badvalue', [] ),
+                       ],
+                       'Ignored invalid values' => [
+                               'a|b|c|d',
+                               [
+                                       ParamValidator::PARAM_ISMULTI => true,
+                                       ParamValidator::PARAM_IGNORE_INVALID_VALUES => true,
+                               ],
+                               false,
+                               [ 'a', 'b', 'c', 'd' ],
+                               [ 'a' => 'A', 'b' => null, 'c' => null, 'd' => 'D' ],
+                               [ 'A', 'D' ],
+                               [
+                                       [ 'unrecognizedvalues', 'values' => [ 'b', 'c' ] ],
+                               ],
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/SimpleCallbacksTest.php b/tests/phpunit/includes/libs/ParamValidator/SimpleCallbacksTest.php
new file mode 100644 (file)
index 0000000..ebe1dcc
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+use Psr\Http\Message\UploadedFileInterface;
+
+/**
+ * @covers Wikimedia\ParamValidator\SimpleCallbacks
+ */
+class SimpleCallbacksTest extends \PHPUnit\Framework\TestCase {
+
+       public function testDataAccess() {
+               $callbacks = new SimpleCallbacks(
+                       [ 'foo' => 'Foo!', 'bar' => null ],
+                       [
+                               'file1' => [
+                                       'name' => 'example.txt',
+                                       'type' => 'text/plain',
+                                       'tmp_name' => '...',
+                                       'error' => UPLOAD_ERR_OK,
+                                       'size' => 123,
+                               ],
+                               'file2' => [
+                                       'name' => '',
+                                       'type' => '',
+                                       'tmp_name' => '',
+                                       'error' => UPLOAD_ERR_NO_FILE,
+                                       'size' => 0,
+                               ],
+                       ]
+               );
+
+               $this->assertTrue( $callbacks->hasParam( 'foo', [] ) );
+               $this->assertFalse( $callbacks->hasParam( 'bar', [] ) );
+               $this->assertFalse( $callbacks->hasParam( 'baz', [] ) );
+               $this->assertFalse( $callbacks->hasParam( 'file1', [] ) );
+
+               $this->assertSame( 'Foo!', $callbacks->getValue( 'foo', null, [] ) );
+               $this->assertSame( null, $callbacks->getValue( 'bar', null, [] ) );
+               $this->assertSame( 123, $callbacks->getValue( 'bar', 123, [] ) );
+               $this->assertSame( null, $callbacks->getValue( 'baz', null, [] ) );
+               $this->assertSame( null, $callbacks->getValue( 'file1', null, [] ) );
+
+               $this->assertFalse( $callbacks->hasUpload( 'foo', [] ) );
+               $this->assertFalse( $callbacks->hasUpload( 'bar', [] ) );
+               $this->assertTrue( $callbacks->hasUpload( 'file1', [] ) );
+               $this->assertTrue( $callbacks->hasUpload( 'file2', [] ) );
+               $this->assertFalse( $callbacks->hasUpload( 'baz', [] ) );
+
+               $this->assertNull( $callbacks->getUploadedFile( 'foo', [] ) );
+               $this->assertNull( $callbacks->getUploadedFile( 'bar', [] ) );
+               $this->assertInstanceOf(
+                       UploadedFileInterface::class, $callbacks->getUploadedFile( 'file1', [] )
+               );
+               $this->assertInstanceOf(
+                       UploadedFileInterface::class, $callbacks->getUploadedFile( 'file2', [] )
+               );
+               $this->assertNull( $callbacks->getUploadedFile( 'baz', [] ) );
+
+               $file = $callbacks->getUploadedFile( 'file1', [] );
+               $this->assertSame( 'example.txt', $file->getClientFilename() );
+               $file = $callbacks->getUploadedFile( 'file2', [] );
+               $this->assertSame( UPLOAD_ERR_NO_FILE, $file->getError() );
+
+               $this->assertFalse( $callbacks->useHighLimits( [] ) );
+               $this->assertFalse( $callbacks->useHighLimits( [ 'useHighLimits' => false ] ) );
+               $this->assertTrue( $callbacks->useHighLimits( [ 'useHighLimits' => true ] ) );
+       }
+
+       public function testRecording() {
+               $callbacks = new SimpleCallbacks( [] );
+
+               $this->assertSame( [], $callbacks->getRecordedConditions() );
+
+               $ex1 = new ValidationException( 'foo', 'Foo!', [], 'foo', [] );
+               $callbacks->recordCondition( $ex1, [] );
+               $ex2 = new ValidationException( 'bar', null, [], 'barbar', [ 'bAr' => 'BaR' ] );
+               $callbacks->recordCondition( $ex2, [] );
+               $callbacks->recordCondition( $ex2, [] );
+               $this->assertSame( [ $ex1, $ex2, $ex2 ], $callbacks->getRecordedConditions() );
+
+               $callbacks->clearRecordedConditions();
+               $this->assertSame( [], $callbacks->getRecordedConditions() );
+               $callbacks->recordCondition( $ex1, [] );
+               $this->assertSame( [ $ex1 ], $callbacks->getRecordedConditions() );
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/BooleanDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/BooleanDefTest.php
new file mode 100644 (file)
index 0000000..75afb33
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers \Wikimedia\ParamValidator\TypeDef\BooleanDef
+ */
+class BooleanDefTest extends TypeDefTestCase {
+
+       protected static $testClass = BooleanDef::class;
+
+       public function provideValidate() {
+               $ex = new ValidationException( 'test', '', [], 'badbool', [
+                       'truevals' => BooleanDef::$TRUEVALS,
+                       'falsevals' => array_merge( BooleanDef::$FALSEVALS, [ 'the empty string' ] ),
+               ] );
+
+               foreach ( [
+                       [ BooleanDef::$TRUEVALS, true ],
+                       [ BooleanDef::$FALSEVALS, false ],
+                       [ [ '' ], false ],
+                       [ [ '2', 'foobar' ], $ex ],
+               ] as list( $vals, $expect ) ) {
+                       foreach ( $vals as $v ) {
+                               yield "Value '$v'" => [ $v, $expect ];
+                               $v2 = ucfirst( $v );
+                               if ( $v2 !== $v ) {
+                                       yield "Value '$v2'" => [ $v2, $expect ];
+                               }
+                               $v3 = strtoupper( $v );
+                               if ( $v3 !== $v2 ) {
+                                       yield "Value '$v3'" => [ $v3, $expect ];
+                               }
+                       }
+               }
+       }
+
+       public function provideStringifyValue() {
+               return [
+                       [ true, 'true' ],
+                       [ false, 'false' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/EnumDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/EnumDefTest.php
new file mode 100644 (file)
index 0000000..18d0aca
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\EnumDef
+ */
+class EnumDefTest extends TypeDefTestCase {
+
+       protected static $testClass = EnumDef::class;
+
+       public function provideValidate() {
+               $settings = [
+                       ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
+                       EnumDef::PARAM_DEPRECATED_VALUES => [
+                               'b' => [ 'not-to-be' ],
+                               'c' => true,
+                       ],
+               ];
+
+               return [
+                       'Basic' => [ 'a', 'a', $settings ],
+                       'Deprecated' => [ 'c', 'c', $settings, [], [ [ 'deprecated-value', 'flag' => true ] ] ],
+                       'Deprecated with message' => [
+                               'b', 'b', $settings, [],
+                               [ [ 'deprecated-value', 'flag' => [ 'not-to-be' ] ] ],
+                       ],
+                       'Bad value, non-multi' => [
+                               'x', new ValidationException( 'test', 'x', $settings, 'badvalue', [] ),
+                               $settings,
+                       ],
+                       'Bad value, non-multi but looks like it' => [
+                               'x|y', new ValidationException( 'test', 'x|y', $settings, 'notmulti', [] ),
+                               $settings,
+                       ],
+                       'Bad value, multi' => [
+                               'x|y', new ValidationException( 'test', 'x|y', $settings, 'badvalue', [] ),
+                               $settings + [ ParamValidator::PARAM_ISMULTI => true ],
+                               [ 'values-list' => [ 'x|y' ] ],
+                       ],
+               ];
+       }
+
+       public function provideGetEnumValues() {
+               return [
+                       'Basic test' => [
+                               [ ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ] ],
+                               [ 'a', 'b', 'c', 'd' ],
+                       ],
+               ];
+       }
+
+       public function provideStringifyValue() {
+               return [
+                       'Basic test' => [ 123, '123' ],
+                       'Array' => [ [ 1, 2, 3 ], '1|2|3' ],
+                       'Array with pipes' => [ [ 1, 2, '3|4', 5 ], "\x1f1\x1f2\x1f3|4\x1f5" ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/FloatDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/FloatDefTest.php
new file mode 100644 (file)
index 0000000..7bd053a
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\SimpleCallbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\FloatDef
+ */
+class FloatDefTest extends TypeDefTestCase {
+
+       protected static $testClass = FloatDef::class;
+
+       public function provideValidate() {
+               return [
+                       [ '123', 123.0 ],
+                       [ '123.4', 123.4 ],
+                       [ '0.4', 0.4 ],
+                       [ '.4', 0.4 ],
+
+                       [ '+123', 123.0 ],
+                       [ '+123.4', 123.4 ],
+                       [ '+0.4', 0.4 ],
+                       [ '+.4', 0.4 ],
+
+                       [ '-123', -123.0 ],
+                       [ '-123.4', -123.4 ],
+                       [ '-.4', -0.4 ],
+                       [ '-.4', -0.4 ],
+
+                       [ '123e5', 12300000.0 ],
+                       [ '123E5', 12300000.0 ],
+                       [ '123.4e+5', 12340000.0 ],
+                       [ '123E5', 12300000.0 ],
+                       [ '-123.4e-5', -0.001234 ],
+                       [ '.4E-5', 0.000004 ],
+
+                       [ '0', 0 ],
+                       [ '000000', 0 ],
+                       [ '0000.0000', 0 ],
+                       [ '000001.0002000000', 1.0002 ],
+                       [ '1e0', 1 ],
+                       [ '1e-0000', 1 ],
+                       [ '1e+00010', 1e10 ],
+
+                       'Weird, but ok' => [ '-0', 0 ],
+                       'Underflow is ok' => [ '1e-9999', 0 ],
+
+                       'Empty decimal part' => [ '1.', new ValidationException( 'test', '1.', [], 'badfloat', [] ) ],
+                       'Bad sign' => [ ' 1', new ValidationException( 'test', ' 1', [], 'badfloat', [] ) ],
+                       'Comma as decimal separator or thousands grouping?'
+                               => [ '1,234', new ValidationException( 'test', '1,234', [], 'badfloat', [] ) ],
+                       'U+2212 minus' => [ '−1', new ValidationException( 'test', '−1', [], 'badfloat', [] ) ],
+                       'Overflow' => [ '1e9999', new ValidationException( 'test', '1e9999', [], 'notfinite', [] ) ],
+                       'Overflow, -INF'
+                               => [ '-1e9999', new ValidationException( 'test', '-1e9999', [], 'notfinite', [] ) ],
+                       'Bogus value' => [ 'foo', new ValidationException( 'test', 'foo', [], 'badfloat', [] ) ],
+                       'Bogus value (2)' => [ '123f4', new ValidationException( 'test', '123f4', [], 'badfloat', [] ) ],
+                       'Newline' => [ "123\n", new ValidationException( 'test', "123\n", [], 'badfloat', [] ) ],
+               ];
+       }
+
+       public function provideStringifyValue() {
+               $digits = defined( 'PHP_FLOAT_DIG' ) ? PHP_FLOAT_DIG : 15;
+
+               return [
+                       [ 1.2, '1.2' ],
+                       [ 10 / 3, '3.' . str_repeat( '3', $digits - 1 ) ],
+                       [ 1e100, '1.0e+100' ],
+                       [ 6.022e-23, '6.022e-23' ],
+               ];
+       }
+
+       /** @dataProvider provideLocales */
+       public function testStringifyValue_localeWeirdness( $locale ) {
+               static $cats = [ LC_ALL, LC_MONETARY, LC_NUMERIC ];
+
+               $curLocales = [];
+               foreach ( $cats as $c ) {
+                       $curLocales[$c] = setlocale( $c, '0' );
+                       if ( $curLocales[$c] === false ) {
+                               $this->markTestSkipped( 'Locale support is unavailable' );
+                       }
+               }
+               try {
+                       foreach ( $cats as $c ) {
+                               if ( setlocale( $c, $locale ) === false ) {
+                                       $this->markTestSkipped( "Locale \"$locale\" is unavailable" );
+                               }
+                       }
+
+                       $typeDef = $this->getInstance( new SimpleCallbacks( [] ), [] );
+                       $this->assertSame( '123456.789', $typeDef->stringifyValue( 'test', 123456.789, [], [] ) );
+                       $this->assertSame( '-123456.789', $typeDef->stringifyValue( 'test', -123456.789, [], [] ) );
+                       $this->assertSame( '1.0e+20', $typeDef->stringifyValue( 'test', 1e20, [], [] ) );
+                       $this->assertSame( '1.0e-20', $typeDef->stringifyValue( 'test', 1e-20, [], [] ) );
+               } finally {
+                       foreach ( $curLocales as $c => $v ) {
+                               setlocale( $c, $v );
+                       }
+               }
+       }
+
+       public function provideLocales() {
+               return [
+                       // May as well test these.
+                       [ 'C' ],
+                       [ 'C.UTF-8' ],
+
+                       // Some hopefullt-common locales with decimal_point = ',' and thousands_sep = '.'
+                       [ 'de_DE' ],
+                       [ 'de_DE.utf8' ],
+                       [ 'es_ES' ],
+                       [ 'es_ES.utf8' ],
+
+                       // This one, on my system at least, has decimal_point as U+066B.
+                       [ 'ps_AF' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/IntegerDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/IntegerDefTest.php
new file mode 100644 (file)
index 0000000..21fc987
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\IntegerDef
+ */
+class IntegerDefTest extends TypeDefTestCase {
+
+       protected static $testClass = IntegerDef::class;
+
+       /**
+        * @param string $v Representing a positive integer
+        * @return string Representing $v + 1
+        */
+       private static function plusOne( $v ) {
+               for ( $i = strlen( $v ) - 1; $i >= 0; $i-- ) {
+                       if ( $v[$i] === '9' ) {
+                               $v[$i] = '0';
+                       } else {
+                               $v[$i] = $v[$i] + 1;
+                               return $v;
+                       }
+               }
+               return '1' . $v;
+       }
+
+       public function provideValidate() {
+               $badinteger = new ValidationException( 'test', '...', [], 'badinteger', [] );
+               $belowminimum = new ValidationException(
+                       'test', '...', [], 'belowminimum', [ 'min' => 0, 'max' => 2, 'max2' => '' ]
+               );
+               $abovemaximum = new ValidationException(
+                       'test', '...', [], 'abovemaximum', [ 'min' => 0, 'max' => 2, 'max2' => '' ]
+               );
+               $abovemaximum2 = new ValidationException(
+                       'test', '...', [], 'abovemaximum', [ 'min' => 0, 'max' => 2, 'max2' => 4 ]
+               );
+               $abovehighmaximum = new ValidationException(
+                       'test', '...', [], 'abovehighmaximum', [ 'min' => 0, 'max' => 2, 'max2' => 4 ]
+               );
+               $asWarn = function ( ValidationException $ex ) {
+                       return [ $ex->getFailureCode() ] + $ex->getFailureData();
+               };
+
+               $minmax = [
+                       IntegerDef::PARAM_MIN => 0,
+                       IntegerDef::PARAM_MAX => 2,
+               ];
+               $minmax2 = [
+                       IntegerDef::PARAM_MIN => 0,
+                       IntegerDef::PARAM_MAX => 2,
+                       IntegerDef::PARAM_MAX2 => 4,
+               ];
+               $ignore = [
+                       IntegerDef::PARAM_IGNORE_RANGE => true,
+               ];
+               $usehigh = [ 'useHighLimits' => true ];
+
+               return [
+                       [ '123', 123 ],
+                       [ '-123', -123 ],
+                       [ '000123', 123 ],
+                       [ '000', 0 ],
+                       [ '-0', 0 ],
+                       [ (string)PHP_INT_MAX, PHP_INT_MAX ],
+                       [ '0000' . PHP_INT_MAX, PHP_INT_MAX ],
+                       [ (string)PHP_INT_MIN, PHP_INT_MIN ],
+                       [ '-0000' . substr( PHP_INT_MIN, 1 ), PHP_INT_MIN ],
+
+                       'Overflow' => [ self::plusOne( (string)PHP_INT_MAX ), $badinteger ],
+                       'Negative overflow' => [ '-' . self::plusOne( substr( PHP_INT_MIN, 1 ) ), $badinteger ],
+
+                       'Float' => [ '1.5', $badinteger ],
+                       'Float (e notation)' => [ '1e1', $badinteger ],
+                       'Bad sign (space)' => [ ' 1', $badinteger ],
+                       'Bad sign (newline)' => [ "\n1", $badinteger ],
+                       'Bogus value' => [ 'foo', $badinteger ],
+                       'Bogus value (2)' => [ '1foo', $badinteger ],
+                       'Hex value' => [ '0x123', $badinteger ],
+                       'Newline' => [ "1\n", $badinteger ],
+
+                       'Ok with range' => [ '1', 1, $minmax ],
+                       'Below minimum' => [ '-1', $belowminimum, $minmax ],
+                       'Below minimum, ignored' => [ '-1', 0, $minmax + $ignore, [], [ $asWarn( $belowminimum ) ] ],
+                       'Above maximum' => [ '3', $abovemaximum, $minmax ],
+                       'Above maximum, ignored' => [ '3', 2, $minmax + $ignore, [], [ $asWarn( $abovemaximum ) ] ],
+                       'Not above max2 but can\'t use it' => [ '3', $abovemaximum2, $minmax2, [] ],
+                       'Not above max2 but can\'t use it, ignored'
+                               => [ '3', 2, $minmax2 + $ignore, [], [ $asWarn( $abovemaximum2 ) ] ],
+                       'Not above max2' => [ '3', 3, $minmax2, $usehigh ],
+                       'Above max2' => [ '5', $abovehighmaximum, $minmax2, $usehigh ],
+                       'Above max2, ignored'
+                               => [ '5', 4, $minmax2 + $ignore, $usehigh, [ $asWarn( $abovehighmaximum ) ] ],
+               ];
+       }
+
+       public function provideNormalizeSettings() {
+               return [
+                       [ [], [] ],
+                       [
+                               [ IntegerDef::PARAM_MAX => 2 ],
+                               [ IntegerDef::PARAM_MAX => 2 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 2 ],
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 4 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MAX2 => 2 ],
+                               [],
+                       ],
+               ];
+       }
+
+       public function provideDescribeSettings() {
+               return [
+                       'Basic' => [ [], [], [] ],
+                       'Default' => [
+                               [ ParamValidator::PARAM_DEFAULT => 123 ],
+                               [ 'default' => '123' ],
+                               [ 'default' => [ 'value' => '123' ] ],
+                       ],
+                       'Min' => [
+                               [ ParamValidator::PARAM_DEFAULT => 123, IntegerDef::PARAM_MIN => 0 ],
+                               [ 'default' => '123', 'min' => 0 ],
+                               [ 'default' => [ 'value' => '123' ], 'min' => [ 'min' => 0, 'max' => '', 'max2' => '' ] ],
+                       ],
+                       'Max' => [
+                               [ IntegerDef::PARAM_MAX => 2 ],
+                               [ 'max' => 2 ],
+                               [ 'max' => [ 'min' => '', 'max' => 2, 'max2' => '' ] ],
+                       ],
+                       'Max2' => [
+                               [ IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                               [ 'max' => 2, 'max2' => 4 ],
+                               [ 'max2' => [ 'min' => '', 'max' => 2, 'max2' => 4 ] ],
+                       ],
+                       'Minmax' => [
+                               [ IntegerDef::PARAM_MIN => 0, IntegerDef::PARAM_MAX => 2 ],
+                               [ 'min' => 0, 'max' => 2 ],
+                               [ 'minmax' => [ 'min' => 0, 'max' => 2, 'max2' => '' ] ],
+                       ],
+                       'Minmax2' => [
+                               [ IntegerDef::PARAM_MIN => 0, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                               [ 'min' => 0, 'max' => 2, 'max2' => 4 ],
+                               [ 'minmax2' => [ 'min' => 0, 'max' => 2, 'max2' => 4 ] ],
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/LimitDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/LimitDefTest.php
new file mode 100644 (file)
index 0000000..2bf25e5
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+require_once __DIR__ . '/IntegerDefTest.php';
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\LimitDef
+ */
+class LimitDefTest extends IntegerDefTest {
+
+       protected static $testClass = LimitDef::class;
+
+       public function provideValidate() {
+               yield from parent::provideValidate();
+
+               $useHigh = [ 'useHighLimits' => true ];
+               $max = [ IntegerDef::PARAM_MAX => 2 ];
+               $max2 = [ IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ];
+
+               yield 'Max' => [ 'max', 2, $max ];
+               yield 'Max, use high' => [ 'max', 2, $max, $useHigh ];
+               yield 'Max2' => [ 'max', 2, $max2 ];
+               yield 'Max2, use high' => [ 'max', 4, $max2, $useHigh ];
+       }
+
+       public function provideNormalizeSettings() {
+               return [
+                       [ [], [ IntegerDef::PARAM_MIN => 0 ] ],
+                       [
+                               [ IntegerDef::PARAM_MAX => 2 ],
+                               [ IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MIN => 0 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 2, IntegerDef::PARAM_MAX2 => 4 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 2 ],
+                               [ IntegerDef::PARAM_MIN => 1, IntegerDef::PARAM_MAX => 4, IntegerDef::PARAM_MAX2 => 4 ],
+                       ],
+                       [
+                               [ IntegerDef::PARAM_MAX2 => 2 ],
+                               [ IntegerDef::PARAM_MIN => 0 ],
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/PasswordDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/PasswordDefTest.php
new file mode 100644 (file)
index 0000000..dd97903
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+
+require_once __DIR__ . '/StringDefTest.php';
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\PasswordDef
+ */
+class PasswordDefTest extends StringDefTest {
+
+       protected static $testClass = PasswordDef::class;
+
+       public function provideNormalizeSettings() {
+               return [
+                       [ [], [ ParamValidator::PARAM_SENSITIVE => true ] ],
+                       [ [ ParamValidator::PARAM_SENSITIVE => false ], [ ParamValidator::PARAM_SENSITIVE => true ] ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/PresenceBooleanDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/PresenceBooleanDefTest.php
new file mode 100644 (file)
index 0000000..dd690de
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef
+ */
+class PresenceBooleanDefTest extends TypeDefTestCase {
+
+       protected static $testClass = PresenceBooleanDef::class;
+
+       public function provideValidate() {
+               return [
+                       [ null, false ],
+                       [ '', true ],
+                       [ '0', true ],
+                       [ '1', true ],
+                       [ 'anything really', true ],
+               ];
+       }
+
+       public function provideDescribeSettings() {
+               return [
+                       [ [], [], [] ],
+                       [ [ ParamValidator::PARAM_DEFAULT => 'foo' ], [], [] ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/StringDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/StringDefTest.php
new file mode 100644 (file)
index 0000000..bae2f02
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\SimpleCallbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\StringDef
+ */
+class StringDefTest extends TypeDefTestCase {
+
+       protected static $testClass = StringDef::class;
+
+       protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
+               if ( static::$testClass === null ) {
+                       throw new \LogicException( 'Either assign static::$testClass or override ' . __METHOD__ );
+               }
+
+               return new static::$testClass( $callbacks, $options );
+       }
+
+       public function provideValidate() {
+               $req = [
+                       ParamValidator::PARAM_REQUIRED => true,
+               ];
+               $maxBytes = [
+                       StringDef::PARAM_MAX_BYTES => 4,
+               ];
+               $maxChars = [
+                       StringDef::PARAM_MAX_CHARS => 2,
+               ];
+
+               return [
+                       'Basic' => [ '123', '123' ],
+                       'Empty' => [ '', '' ],
+                       'Empty, required' => [
+                               '',
+                               new ValidationException( 'test', '', [], 'missingparam', [] ),
+                               $req,
+                       ],
+                       'Empty, required, allowed' => [ '', '', $req, [ 'allowEmptyWhenRequired' => true ] ],
+                       'Max bytes, ok' => [ 'abcd', 'abcd', $maxBytes ],
+                       'Max bytes, exceeded' => [
+                               'abcde',
+                               new ValidationException( 'test', '', [], 'maxbytes', [ 'maxbytes' => 4, 'maxchars' => '' ] ),
+                               $maxBytes,
+                       ],
+                       'Max bytes, ok (2)' => [ '😄', '😄', $maxBytes ],
+                       'Max bytes, exceeded (2)' => [
+                               '😭?',
+                               new ValidationException( 'test', '', [], 'maxbytes', [ 'maxbytes' => 4, 'maxchars' => '' ] ),
+                               $maxBytes,
+                       ],
+                       'Max chars, ok' => [ 'ab', 'ab', $maxChars ],
+                       'Max chars, exceeded' => [
+                               'abc',
+                               new ValidationException( 'test', '', [], 'maxchars', [ 'maxbytes' => '', 'maxchars' => 2 ] ),
+                               $maxChars,
+                       ],
+                       'Max chars, ok (2)' => [ '😄😄', '😄😄', $maxChars ],
+                       'Max chars, exceeded (2)' => [
+                               '😭??',
+                               new ValidationException( 'test', '', [], 'maxchars', [ 'maxbytes' => '', 'maxchars' => 2 ] ),
+                               $maxChars,
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/TimestampDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/TimestampDefTest.php
new file mode 100644 (file)
index 0000000..8adf190
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+use Wikimedia\ParamValidator\SimpleCallbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\TimestampDef
+ */
+class TimestampDefTest extends TypeDefTestCase {
+
+       protected static $testClass = TimestampDef::class;
+
+       protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
+               if ( static::$testClass === null ) {
+                       throw new \LogicException( 'Either assign static::$testClass or override ' . __METHOD__ );
+               }
+
+               return new static::$testClass( $callbacks, $options );
+       }
+
+       /** @dataProvider provideValidate */
+       public function testValidate(
+               $value, $expect, array $settings = [], array $options = [], array $expectConds = []
+       ) {
+               $reset = ConvertibleTimestamp::setFakeTime( 1559764242 );
+               try {
+                       parent::testValidate( $value, $expect, $settings, $options, $expectConds );
+               } finally {
+                       ConvertibleTimestamp::setFakeTime( $reset );
+               }
+       }
+
+       public function provideValidate() {
+               $specific = new ConvertibleTimestamp( 1517630706 );
+               $specificMs = new ConvertibleTimestamp( 1517630706.999 );
+               $now = new ConvertibleTimestamp( 1559764242 );
+
+               $formatDT = [ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'DateTime' ];
+               $formatMW = [ TimestampDef::PARAM_TIMESTAMP_FORMAT => TS_MW ];
+
+               return [
+                       // We don't try to validate all formats supported by ConvertibleTimestamp, just
+                       // some of the interesting ones.
+                       'ISO format' => [ '2018-02-03T04:05:06Z', $specific ],
+                       'ISO format with TZ' => [ '2018-02-03T00:05:06-04:00', $specific ],
+                       'ISO format without punctuation' => [ '20180203T040506', $specific ],
+                       'ISO format with ms' => [ '2018-02-03T04:05:06.999000Z', $specificMs ],
+                       'ISO format with ms without punctuation' => [ '20180203T040506.999', $specificMs ],
+                       'MW format' => [ '20180203040506', $specific ],
+                       'Generic format' => [ '2018-02-03 04:05:06', $specific ],
+                       'Generic format + GMT' => [ '2018-02-03 04:05:06 GMT', $specific ],
+                       'Generic format + TZ +0100' => [ '2018-02-03 05:05:06+0100', $specific ],
+                       'Generic format + TZ -01' => [ '2018-02-03 03:05:06-01', $specific ],
+                       'Seconds-since-epoch format' => [ '1517630706', $specific ],
+                       'Now' => [ 'now', $now ],
+
+                       // Warnings
+                       'Empty' => [ '', $now, [], [], [ [ 'unclearnowtimestamp' ] ] ],
+                       'Zero' => [ '0', $now, [], [], [ [ 'unclearnowtimestamp' ] ] ],
+
+                       // Error handling
+                       'Bad value' => [
+                               'bogus',
+                               new ValidationException( 'test', 'bogus', [], 'badtimestamp', [] ),
+                       ],
+
+                       // Formatting
+                       '=> DateTime' => [ 'now', $now->timestamp, $formatDT ],
+                       '=> TS_MW' => [ 'now', '20190605195042', $formatMW ],
+                       '=> TS_MW as default' => [ 'now', '20190605195042', [], [ 'defaultFormat' => TS_MW ] ],
+                       '=> TS_MW overriding default'
+                               => [ 'now', '20190605195042', $formatMW, [ 'defaultFormat' => TS_ISO_8601 ] ],
+               ];
+       }
+
+       public function provideStringifyValue() {
+               $specific = new ConvertibleTimestamp( '20180203040506' );
+
+               return [
+                       [ '20180203040506', '2018-02-03T04:05:06Z' ],
+                       [ $specific, '2018-02-03T04:05:06Z' ],
+                       [ $specific->timestamp, '2018-02-03T04:05:06Z' ],
+                       [ $specific, '20180203040506', [], [ 'stringifyFormat' => TS_MW ] ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/TypeDefTestCase.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/TypeDefTestCase.php
new file mode 100644 (file)
index 0000000..fa86c79
--- /dev/null
@@ -0,0 +1,193 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\SimpleCallbacks;
+use Wikimedia\ParamValidator\TypeDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Test case infrastructure for TypeDef subclasses
+ *
+ * Generally you'll only need to override static::$testClass and data providers
+ * for methods the TypeDef actually implements.
+ */
+abstract class TypeDefTestCase extends \PHPUnit\Framework\TestCase {
+
+       /** @var string|null TypeDef class name being tested */
+       protected static $testClass = null;
+
+       /**
+        * Create a SimpleCallbacks for testing
+        *
+        * The object created here should result in a call to the TypeDef's
+        * `getValue( 'test' )` returning an appropriate result for testing.
+        *
+        * @param mixed $value Value to return for 'test'
+        * @param array $options Options array.
+        * @return SimpleCallbacks
+        */
+       protected function getCallbacks( $value, array $options ) {
+               return new SimpleCallbacks( [ 'test' => $value ] );
+       }
+
+       /**
+        * Create an instance of the TypeDef subclass being tested
+        *
+        * @param SimpleCallbacks $callbacks From $this->getCallbacks()
+        * @param array $options Options array.
+        * @return TypeDef
+        */
+       protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
+               if ( static::$testClass === null ) {
+                       throw new \LogicException( 'Either assign static::$testClass or override ' . __METHOD__ );
+               }
+
+               return new static::$testClass( $callbacks );
+       }
+
+       /**
+        * @dataProvider provideValidate
+        * @param mixed $value Value for getCallbacks()
+        * @param mixed|ValidationException $expect Expected result from TypeDef::validate().
+        *  If a ValidationException, it is expected that a ValidationException
+        *  with matching failure code and data will be thrown. Otherwise, the return value must be equal.
+        * @param array $settings Settings array.
+        * @param array $options Options array
+        * @param array[] $expectConds Expected conditions reported. Each array is
+        *  `[ $ex->getFailureCode() ] + $ex->getFailureData()`.
+        */
+       public function testValidate(
+               $value, $expect, array $settings = [], array $options = [], array $expectConds = []
+       ) {
+               $callbacks = $this->getCallbacks( $value, $options );
+               $typeDef = $this->getInstance( $callbacks, $options );
+
+               if ( $expect instanceof ValidationException ) {
+                       try {
+                               $v = $typeDef->getValue( 'test', $settings, $options );
+                               $typeDef->validate( 'test', $v, $settings, $options );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( ValidationException $ex ) {
+                               $this->assertEquals( $expect->getFailureCode(), $ex->getFailureCode() );
+                               $this->assertEquals( $expect->getFailureData(), $ex->getFailureData() );
+                       }
+               } else {
+                       $v = $typeDef->getValue( 'test', $settings, $options );
+                       $this->assertEquals( $expect, $typeDef->validate( 'test', $v, $settings, $options ) );
+               }
+
+               $conditions = [];
+               foreach ( $callbacks->getRecordedConditions() as $ex ) {
+                       $conditions[] = array_merge( [ $ex->getFailureCode() ], $ex->getFailureData() );
+               }
+               $this->assertSame( $expectConds, $conditions );
+       }
+
+       /**
+        * @return array|Iterable
+        */
+       abstract public function provideValidate();
+
+       /**
+        * @dataProvider provideNormalizeSettings
+        * @param array $settings
+        * @param array $expect
+        * @param array $options Options array
+        */
+       public function testNormalizeSettings( array $settings, array $expect, array $options = [] ) {
+               $typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
+               $this->assertSame( $expect, $typeDef->normalizeSettings( $settings ) );
+       }
+
+       /**
+        * @return array|Iterable
+        */
+       public function provideNormalizeSettings() {
+               return [
+                       'Basic test' => [ [ 'param-foo' => 'bar' ], [ 'param-foo' => 'bar' ] ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetEnumValues
+        * @param array $settings
+        * @param array|null $expect
+        * @param array $options Options array
+        */
+       public function testGetEnumValues( array $settings, $expect, array $options = [] ) {
+               $typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
+               $this->assertSame( $expect, $typeDef->getEnumValues( 'test', $settings, $options ) );
+       }
+
+       /**
+        * @return array|Iterable
+        */
+       public function provideGetEnumValues() {
+               return [
+                       'Basic test' => [ [], null ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideStringifyValue
+        * @param mixed $value
+        * @param string|null $expect
+        * @param array $settings
+        * @param array $options Options array
+        */
+       public function testStringifyValue( $value, $expect, array $settings = [], array $options = [] ) {
+               $typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
+               $this->assertSame( $expect, $typeDef->stringifyValue( 'test', $value, $settings, $options ) );
+       }
+
+       /**
+        * @return array|Iterable
+        */
+       public function provideStringifyValue() {
+               return [
+                       'Basic test' => [ 123, '123' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDescribeSettings
+        * @param array $settings
+        * @param array $expectNormal
+        * @param array $expectCompact
+        * @param array $options Options array
+        */
+       public function testDescribeSettings(
+               array $settings, array $expectNormal, array $expectCompact, array $options = []
+       ) {
+               $typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
+               $this->assertSame(
+                       $expectNormal,
+                       $typeDef->describeSettings( 'test', $settings, $options ),
+                       'Normal mode'
+               );
+               $this->assertSame(
+                       $expectCompact,
+                       $typeDef->describeSettings( 'test', $settings, [ 'compact' => true ] + $options ),
+                       'Compact mode'
+               );
+       }
+
+       /**
+        * @return array|Iterable
+        */
+       public function provideDescribeSettings() {
+               yield 'Basic test' => [ [], [], [] ];
+
+               foreach ( $this->provideStringifyValue() as $i => $v ) {
+                       yield "Default value (from provideStringifyValue data set \"$i\")" => [
+                               [ ParamValidator::PARAM_DEFAULT => $v[0] ] + ( $v[2] ?? [] ),
+                               [ 'default' => $v[1] ],
+                               [ 'default' => [ 'value' => $v[1] ] ],
+                               $v[3] ?? [],
+                       ];
+               }
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDef/UploadDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDef/UploadDefTest.php
new file mode 100644 (file)
index 0000000..c81647c
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+namespace Wikimedia\ParamValidator\TypeDef;
+
+use Wikimedia\ParamValidator\SimpleCallbacks;
+use Wikimedia\ParamValidator\Util\UploadedFile;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef\UploadDef
+ */
+class UploadDefTest extends TypeDefTestCase {
+
+       protected static $testClass = UploadDef::class;
+
+       protected function getCallbacks( $value, array $options ) {
+               if ( $value instanceof UploadedFile ) {
+                       return new SimpleCallbacks( [], [ 'test' => $value ] );
+               } else {
+                       return new SimpleCallbacks( [ 'test' => $value ] );
+               }
+       }
+
+       protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
+               $ret = $this->getMockBuilder( UploadDef::class )
+                       ->setConstructorArgs( [ $callbacks ] )
+                       ->setMethods( [ 'getIniSize' ] )
+                       ->getMock();
+               $ret->method( 'getIniSize' )->willReturn( $options['inisize'] ?? 2 * 1024 * 1024 );
+               return $ret;
+       }
+
+       private function makeUpload( $err = UPLOAD_ERR_OK ) {
+               return new UploadedFile( [
+                       'name' => 'example.txt',
+                       'type' => 'text/plain',
+                       'size' => 0,
+                       'tmp_name' => '...',
+                       'error' => $err,
+               ] );
+       }
+
+       public function testGetNoFile() {
+               $typeDef = $this->getInstance(
+                       $this->getCallbacks( $this->makeUpload( UPLOAD_ERR_NO_FILE ), [] ),
+                       []
+               );
+
+               $this->assertNull( $typeDef->getValue( 'test', [], [] ) );
+               $this->assertNull( $typeDef->getValue( 'nothing', [], [] ) );
+       }
+
+       public function provideValidate() {
+               $okFile = $this->makeUpload();
+               $iniFile = $this->makeUpload( UPLOAD_ERR_INI_SIZE );
+               $exIni = new ValidationException(
+                       'test', '', [], 'badupload-inisize', [ 'size' => 2 * 1024 * 1024 * 1024 ]
+               );
+
+               return [
+                       'Valid upload' => [ $okFile, $okFile ],
+                       'Not an upload' => [
+                               'bar',
+                               new ValidationException( 'test', 'bar', [], 'badupload-notupload', [] ),
+                       ],
+
+                       'Too big (bytes)' => [ $iniFile, $exIni, [], [ 'inisize' => 2 * 1024 * 1024 * 1024 ] ],
+                       'Too big (k)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 * 1024 ) . 'k' ] ],
+                       'Too big (K)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 * 1024 ) . 'K' ] ],
+                       'Too big (m)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 ) . 'm' ] ],
+                       'Too big (M)' => [ $iniFile, $exIni, [], [ 'inisize' => ( 2 * 1024 ) . 'M' ] ],
+                       'Too big (g)' => [ $iniFile, $exIni, [], [ 'inisize' => '2g' ] ],
+                       'Too big (G)' => [ $iniFile, $exIni, [], [ 'inisize' => '2G' ] ],
+
+                       'Form size' => [
+                               $this->makeUpload( UPLOAD_ERR_FORM_SIZE ),
+                               new ValidationException( 'test', '', [], 'badupload-formsize', [] ),
+                       ],
+                       'Partial' => [
+                               $this->makeUpload( UPLOAD_ERR_PARTIAL ),
+                               new ValidationException( 'test', '', [], 'badupload-partial', [] ),
+                       ],
+                       'No tmp' => [
+                               $this->makeUpload( UPLOAD_ERR_NO_TMP_DIR ),
+                               new ValidationException( 'test', '', [], 'badupload-notmpdir', [] ),
+                       ],
+                       'Can\'t write' => [
+                               $this->makeUpload( UPLOAD_ERR_CANT_WRITE ),
+                               new ValidationException( 'test', '', [], 'badupload-cantwrite', [] ),
+                       ],
+                       'Ext abort' => [
+                               $this->makeUpload( UPLOAD_ERR_EXTENSION ),
+                               new ValidationException( 'test', '', [], 'badupload-phpext', [] ),
+                       ],
+                       'Unknown' => [
+                               $this->makeUpload( -43 ), // Should be safe from ever being an UPLOAD_ERR_* constant
+                               new ValidationException( 'test', '', [], 'badupload-unknown', [ 'code' => -43 ] ),
+                       ],
+
+                       'Validating null' => [
+                               null,
+                               new ValidationException( 'test', '', [], 'badupload', [] ),
+                       ],
+               ];
+       }
+
+       public function provideStringifyValue() {
+               return [
+                       'Yeah, right' => [ $this->makeUpload(), null ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/TypeDefTest.php b/tests/phpunit/includes/libs/ParamValidator/TypeDefTest.php
new file mode 100644 (file)
index 0000000..7675a8c
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace Wikimedia\ParamValidator;
+
+/**
+ * @covers Wikimedia\ParamValidator\TypeDef
+ */
+class TypeDefTest extends \PHPUnit\Framework\TestCase {
+
+       public function testMisc() {
+               $typeDef = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
+                       ->getMockForAbstractClass();
+
+               $this->assertSame( [ 'foobar' ], $typeDef->normalizeSettings( [ 'foobar' ] ) );
+               $this->assertNull( $typeDef->getEnumValues( 'foobar', [], [] ) );
+               $this->assertSame( '123', $typeDef->stringifyValue( 'foobar', 123, [], [] ) );
+       }
+
+       public function testGetValue() {
+               $options = [ (object)[] ];
+
+               $callbacks = $this->getMockBuilder( Callbacks::class )->getMockForAbstractClass();
+               $callbacks->expects( $this->once() )->method( 'getValue' )
+                       ->with(
+                               $this->identicalTo( 'foobar' ),
+                               $this->identicalTo( null ),
+                               $this->identicalTo( $options )
+                       )
+                       ->willReturn( 'zyx' );
+
+               $typeDef = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ $callbacks ] )
+                       ->getMockForAbstractClass();
+
+               $this->assertSame(
+                       'zyx',
+                       $typeDef->getValue( 'foobar', [ ParamValidator::PARAM_DEFAULT => 'foo' ], $options )
+               );
+       }
+
+       public function testDescribeSettings() {
+               $typeDef = $this->getMockBuilder( TypeDef::class )
+                       ->setConstructorArgs( [ new SimpleCallbacks( [] ) ] )
+                       ->getMockForAbstractClass();
+
+               $this->assertSame(
+                       [],
+                       $typeDef->describeSettings(
+                               'foobar',
+                               [ ParamValidator::PARAM_TYPE => 'xxx' ],
+                               []
+                       )
+               );
+
+               $this->assertSame(
+                       [
+                               'default' => '123',
+                       ],
+                       $typeDef->describeSettings(
+                               'foobar',
+                               [ ParamValidator::PARAM_DEFAULT => 123 ],
+                               []
+                       )
+               );
+
+               $this->assertSame(
+                       [
+                               'default' => [ 'value' => '123' ],
+                       ],
+                       $typeDef->describeSettings(
+                               'foobar',
+                               [ ParamValidator::PARAM_DEFAULT => 123 ],
+                               [ 'compact' => true ]
+                       )
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileStreamTest.php b/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileStreamTest.php
new file mode 100644 (file)
index 0000000..9eaddf6
--- /dev/null
@@ -0,0 +1,294 @@
+<?php
+
+namespace Wikimedia\ParamValidator\Util;
+
+require_once __DIR__ . '/UploadedFileTestBase.php';
+
+use RuntimeException;
+use Wikimedia\AtEase\AtEase;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers Wikimedia\ParamValidator\Util\UploadedFileStream
+ */
+class UploadedFileStreamTest extends UploadedFileTestBase {
+
+       /**
+        * @expectedException RuntimeException
+        * @expectedExceptionMessage Failed to open file:
+        */
+       public function testConstruct_doesNotExist() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               unlink( $filename );
+
+               $this->assertFileNotExists( $filename, 'sanity check' );
+               $stream = new UploadedFileStream( $filename );
+       }
+
+       /**
+        * @expectedException RuntimeException
+        * @expectedExceptionMessage Failed to open file:
+        */
+       public function testConstruct_notReadable() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+
+               chmod( $filename, 0000 );
+               $stream = new UploadedFileStream( $filename );
+       }
+
+       public function testCloseOnDestruct() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+               $fp = TestingAccessWrapper::newFromObject( $stream )->fp;
+               $this->assertSame( 'f', fread( $fp, 1 ), 'sanity check' );
+               unset( $stream );
+               $this->assertFalse( AtEase::quietCall( 'fread', $fp, 1 ) );
+       }
+
+       public function testToString() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               // Always starts at the start of the stream
+               $stream->seek( 3 );
+               $this->assertSame( 'foobar', (string)$stream );
+
+               // No exception when closed
+               $stream->close();
+               $this->assertSame( '', (string)$stream );
+       }
+
+       public function testToString_Error() {
+               if ( !class_exists( \Error::class ) ) {
+                       $this->markTestSkipped( 'No PHP Error class' );
+               }
+
+               // ... Yeah
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = $this->getMockBuilder( UploadedFileStream::class )
+                       ->setConstructorArgs( [ $filename ] )
+                       ->setMethods( [ 'getContents' ] )
+                       ->getMock();
+               $stream->method( 'getContents' )->willReturnCallback( function () {
+                       throw new \Error( 'Bogus' );
+               } );
+               $this->assertSame( '', (string)$stream );
+       }
+
+       public function testClose() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $stream->close();
+
+               // Second call doesn't error
+               $stream->close();
+       }
+
+       public function testDetach() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               // We got the file descriptor
+               $fp = $stream->detach();
+               $this->assertNotNull( $fp );
+               $this->assertSame( 'f', fread( $fp, 1 ) );
+
+               // Stream operations now fail.
+               try {
+                       $stream->seek( 0 );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+
+               // Stream close doesn't affect the file descriptor
+               $stream->close();
+               $this->assertSame( 'o', fread( $fp, 1 ) );
+
+               // Stream destruction doesn't affect the file descriptor
+               unset( $stream );
+               $this->assertSame( 'o', fread( $fp, 1 ) );
+
+               // On a closed stream, we don't get a file descriptor
+               $stream = new UploadedFileStream( $filename );
+               $stream->close();
+               $this->assertNull( $stream->detach() );
+       }
+
+       public function testGetSize() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+               file_put_contents( $filename, 'foobarbaz' );
+               $this->assertSame( 9, $stream->getSize() );
+
+               // Cached
+               file_put_contents( $filename, 'baz' );
+               clearstatcache();
+               $this->assertSame( 3, stat( $filename )['size'], 'sanity check' );
+               $this->assertSame( 9, $stream->getSize() );
+
+               // No error if closed
+               $stream = new UploadedFileStream( $filename );
+               $stream->close();
+               $this->assertSame( null, $stream->getSize() );
+
+               // No error even if the fd goes bad
+               $stream = new UploadedFileStream( $filename );
+               fclose( TestingAccessWrapper::newFromObject( $stream )->fp );
+               $this->assertSame( null, $stream->getSize() );
+       }
+
+       public function testSeekTell() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $stream->seek( 2 );
+               $this->assertSame( 2, $stream->tell() );
+               $stream->seek( 2, SEEK_CUR );
+               $this->assertSame( 4, $stream->tell() );
+               $stream->seek( -5, SEEK_END );
+               $this->assertSame( 1, $stream->tell() );
+               $stream->read( 2 );
+               $this->assertSame( 3, $stream->tell() );
+
+               $stream->close();
+               try {
+                       $stream->seek( 0 );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+               try {
+                       $stream->tell();
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+       public function testEof() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $this->assertFalse( $stream->eof() );
+               $stream->getContents();
+               $this->assertTrue( $stream->eof() );
+               $stream->seek( -1, SEEK_END );
+               $this->assertFalse( $stream->eof() );
+
+               // No error if closed
+               $stream = new UploadedFileStream( $filename );
+               $stream->close();
+               $this->assertTrue( $stream->eof() );
+
+               // No error even if the fd goes bad
+               $stream = new UploadedFileStream( $filename );
+               fclose( TestingAccessWrapper::newFromObject( $stream )->fp );
+               $this->assertInternalType( 'boolean', $stream->eof() );
+       }
+
+       public function testIsFuncs() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+               $this->assertTrue( $stream->isSeekable() );
+               $this->assertTrue( $stream->isReadable() );
+               $this->assertFalse( $stream->isWritable() );
+
+               $stream->close();
+               $this->assertFalse( $stream->isSeekable() );
+               $this->assertFalse( $stream->isReadable() );
+               $this->assertFalse( $stream->isWritable() );
+       }
+
+       public function testRewind() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $stream->seek( 2 );
+               $this->assertSame( 2, $stream->tell() );
+               $stream->rewind();
+               $this->assertSame( 0, $stream->tell() );
+
+               $stream->close();
+               try {
+                       $stream->rewind();
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+       public function testWrite() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               try {
+                       $stream->write( 'foo' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+       public function testRead() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $this->assertSame( 'foo', $stream->read( 3 ) );
+               $this->assertSame( 'bar', $stream->read( 10 ) );
+               $this->assertSame( '', $stream->read( 10 ) );
+               $stream->rewind();
+               $this->assertSame( 'foobar', $stream->read( 10 ) );
+
+               $stream->close();
+               try {
+                       $stream->read( 1 );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+       public function testGetContents() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $stream = new UploadedFileStream( $filename );
+
+               $this->assertSame( 'foobar', $stream->getContents() );
+               $this->assertSame( '', $stream->getContents() );
+               $stream->seek( 3 );
+               $this->assertSame( 'bar', $stream->getContents() );
+
+               $stream->close();
+               try {
+                       $stream->getContents();
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+       public function testGetMetadata() {
+               // Whatever
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $fp = fopen( $filename, 'r' );
+               $expect = stream_get_meta_data( $fp );
+               fclose( $fp );
+
+               $stream = new UploadedFileStream( $filename );
+               $this->assertSame( $expect, $stream->getMetadata() );
+               foreach ( $expect as $k => $v ) {
+                       $this->assertSame( $v, $stream->getMetadata( $k ) );
+               }
+               $this->assertNull( $stream->getMetadata( 'bogus' ) );
+
+               $stream->close();
+               try {
+                       $stream->getMetadata();
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTest.php b/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTest.php
new file mode 100644 (file)
index 0000000..80a74e7
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+
+namespace Wikimedia\ParamValidator\Util;
+
+require_once __DIR__ . '/UploadedFileTestBase.php';
+
+use Psr\Http\Message\StreamInterface;
+use RuntimeException;
+
+/**
+ * @covers Wikimedia\ParamValidator\Util\UploadedFile
+ */
+class UploadedFileTest extends UploadedFileTestBase {
+
+       public function testGetStream() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => $filename ], false );
+
+               // getStream() fails for non-OK uploads
+               foreach ( [
+                       UPLOAD_ERR_INI_SIZE,
+                       UPLOAD_ERR_FORM_SIZE,
+                       UPLOAD_ERR_PARTIAL,
+                       UPLOAD_ERR_NO_FILE,
+                       UPLOAD_ERR_NO_TMP_DIR,
+                       UPLOAD_ERR_CANT_WRITE,
+                       UPLOAD_ERR_EXTENSION,
+                       -42
+               ] as $code ) {
+                       $file2 = new UploadedFile( [ 'error' => $code, 'tmp_name' => $filename ], false );
+                       try {
+                               $file2->getStream();
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                               throw $ex;
+                       } catch ( RuntimeException $ex ) {
+                       }
+               }
+
+               // getStream() works
+               $stream = $file->getStream();
+               $this->assertInstanceOf( StreamInterface::class, $stream );
+               $stream->seek( 0 );
+               $this->assertSame( 'foobar', $stream->getContents() );
+
+               // Second call also works
+               $this->assertInstanceOf( StreamInterface::class, $file->getStream() );
+
+               // getStream() throws after move, and the stream is invalidated too
+               $file->moveTo( $filename . '.xxx' );
+               try {
+                       try {
+                               $file->getStream();
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                               throw $ex;
+                       } catch ( RuntimeException $ex ) {
+                               $this->assertSame( 'File has already been moved', $ex->getMessage() );
+                       }
+                       try {
+                               $stream->seek( 0 );
+                               $stream->getContents();
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                               throw $ex;
+                       } catch ( RuntimeException $ex ) {
+                       }
+               } finally {
+                       unlink( $filename . '.xxx' ); // Clean up
+               }
+
+               // getStream() fails if the file is missing
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => $filename ], true );
+               try {
+                       $file->getStream();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Uploaded file is missing', $ex->getMessage() );
+               }
+       }
+
+       public function testMoveTo() {
+               // Successful move
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $this->assertFileExists( $filename, 'sanity check' );
+               $this->assertFileNotExists( "$filename.xxx", 'sanity check' );
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => $filename ], false );
+               $file->moveTo( $filename . '.xxx' );
+               $this->assertFileNotExists( $filename );
+               $this->assertFileExists( "$filename.xxx" );
+
+               // Fails on a second move attempt
+               $this->assertFileNotExists( "$filename.yyy", 'sanity check' );
+               try {
+                       $file->moveTo( $filename . '.yyy' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'File has already been moved', $ex->getMessage() );
+               }
+               $this->assertFileNotExists( $filename );
+               $this->assertFileExists( "$filename.xxx" );
+               $this->assertFileNotExists( "$filename.yyy" );
+
+               // Fails if the file is missing
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => "$filename.aaa" ], false );
+               $this->assertFileNotExists( "$filename.aaa", 'sanity check' );
+               $this->assertFileNotExists( "$filename.bbb", 'sanity check' );
+               try {
+                       $file->moveTo( $filename . '.bbb' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Uploaded file is missing', $ex->getMessage() );
+               }
+               $this->assertFileNotExists( "$filename.aaa" );
+               $this->assertFileNotExists( "$filename.bbb" );
+
+               // Fails for non-upload file (when not flagged to ignore that)
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $this->assertFileExists( $filename, 'sanity check' );
+               $this->assertFileNotExists( "$filename.xxx", 'sanity check' );
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => $filename ] );
+               try {
+                       $file->moveTo( $filename . '.xxx' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Specified file is not an uploaded file', $ex->getMessage() );
+               }
+               $this->assertFileExists( $filename );
+               $this->assertFileNotExists( "$filename.xxx" );
+
+               // Fails for error uploads
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $this->assertFileExists( $filename, 'sanity check' );
+               $this->assertFileNotExists( "$filename.xxx", 'sanity check' );
+               foreach ( [
+                       UPLOAD_ERR_INI_SIZE,
+                       UPLOAD_ERR_FORM_SIZE,
+                       UPLOAD_ERR_PARTIAL,
+                       UPLOAD_ERR_NO_FILE,
+                       UPLOAD_ERR_NO_TMP_DIR,
+                       UPLOAD_ERR_CANT_WRITE,
+                       UPLOAD_ERR_EXTENSION,
+                       -42
+               ] as $code ) {
+                       $file = new UploadedFile( [ 'error' => $code, 'tmp_name' => $filename ], false );
+                       try {
+                               $file->moveTo( $filename . '.xxx' );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                               throw $ex;
+                       } catch ( RuntimeException $ex ) {
+                       }
+                       $this->assertFileExists( $filename );
+                       $this->assertFileNotExists( "$filename.xxx" );
+               }
+
+               // Move failure triggers exception
+               $filename = $this->makeTemp( __FUNCTION__, 'file1' );
+               $filename2 = $this->makeTemp( __FUNCTION__, 'file2' );
+               $this->assertFileExists( $filename, 'sanity check' );
+               $file = new UploadedFile( [ 'error' => UPLOAD_ERR_OK, 'tmp_name' => $filename ], false );
+               try {
+                       $file->moveTo( $filename2 . DIRECTORY_SEPARATOR . 'foobar' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \PHPUnit\Framework\AssertionFailedError $ex ) {
+                       throw $ex;
+               } catch ( RuntimeException $ex ) {
+               }
+               $this->assertFileExists( $filename );
+       }
+
+       public function testInfoMethods() {
+               $filename = $this->makeTemp( __FUNCTION__ );
+               $file = new UploadedFile( [
+                       'name' => 'C:\\example.txt',
+                       'type' => 'text/plain',
+                       'size' => 1025,
+                       'error' => UPLOAD_ERR_OK,
+                       'tmp_name' => $filename,
+               ], false );
+               $this->assertSame( 1025, $file->getSize() );
+               $this->assertSame( UPLOAD_ERR_OK, $file->getError() );
+               $this->assertSame( 'C:\\example.txt', $file->getClientFilename() );
+               $this->assertSame( 'text/plain', $file->getClientMediaType() );
+
+               // None of these are allowed to error
+               $file = new UploadedFile( [], false );
+               $this->assertSame( null, $file->getSize() );
+               $this->assertSame( UPLOAD_ERR_NO_FILE, $file->getError() );
+               $this->assertSame( null, $file->getClientFilename() );
+               $this->assertSame( null, $file->getClientMediaType() );
+
+               // "if none was provided" behavior, given that $_FILES often contains
+               // the empty string.
+               $file = new UploadedFile( [
+                       'name' => '',
+                       'type' => '',
+                       'size' => 100,
+                       'error' => UPLOAD_ERR_NO_FILE,
+                       'tmp_name' => $filename,
+               ], false );
+               $this->assertSame( null, $file->getClientFilename() );
+               $this->assertSame( null, $file->getClientMediaType() );
+       }
+
+}
diff --git a/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTestBase.php b/tests/phpunit/includes/libs/ParamValidator/Util/UploadedFileTestBase.php
new file mode 100644 (file)
index 0000000..6e1bd6a
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace Wikimedia\ParamValidator\Util;
+
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use Wikimedia\AtEase\AtEase;
+
+class UploadedFileTestBase extends \PHPUnit\Framework\TestCase {
+
+       /** @var string|null */
+       protected static $tmpdir;
+
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               // Create a temporary directory for this test's files.
+               self::$tmpdir = null;
+               $base = sys_get_temp_dir() . DIRECTORY_SEPARATOR .
+                       'phpunit-ParamValidator-UploadedFileTest-' . time() . '-' . getmypid() . '-';
+               for ( $i = 0; $i < 10000; $i++ ) {
+                       $dir = $base . sprintf( '%04d', $i );
+                       if ( AtEase::quietCall( 'mkdir', $dir, 0700, false ) === true ) {
+                               self::$tmpdir = $dir;
+                               break;
+                       }
+               }
+               if ( self::$tmpdir === null ) {
+                       self::fail( "Could not create temporary directory '{$base}XXXX'" );
+               }
+       }
+
+       public static function tearDownAfterClass() {
+               parent::tearDownAfterClass();
+
+               // Clean up temporary directory.
+               if ( self::$tmpdir !== null ) {
+                       $iter = new RecursiveIteratorIterator(
+                               new RecursiveDirectoryIterator( self::$tmpdir, RecursiveDirectoryIterator::SKIP_DOTS ),
+                               RecursiveIteratorIterator::CHILD_FIRST
+                       );
+                       foreach ( $iter as $file ) {
+                               if ( $file->isDir() ) {
+                                       rmdir( $file->getRealPath() );
+                               } else {
+                                       unlink( $file->getRealPath() );
+                               }
+                       }
+                       rmdir( self::$tmpdir );
+                       self::$tmpdir = null;
+               }
+       }
+
+       protected static function assertTmpdir() {
+               if ( self::$tmpdir === null || !is_dir( self::$tmpdir ) ) {
+                       self::fail( 'No temporary directory for ' . static::class );
+               }
+       }
+
+       /**
+        * @param string $prefix For tempnam()
+        * @param string $content Contents of the file
+        * @return string Filename
+        */
+       protected function makeTemp( $prefix, $content = 'foobar' ) {
+               self::assertTmpdir();
+
+               $filename = tempnam( self::$tmpdir, $prefix );
+               if ( $filename === false ) {
+                       self::fail( 'Failed to create temporary file' );
+               }
+
+               self::assertSame(
+                       strlen( $content ),
+                       file_put_contents( $filename, $content ),
+                       'Writing test temporary file'
+               );
+
+               return $filename;
+       }
+
+}
index 4a09a2e..1d11fd8 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @author Matthias Mullie <mmullie@wikimedia.org>
@@ -98,13 +99,13 @@ class BagOStuffTest extends MediaWikiTestCase {
                        $this->cache->merge( $key, $callback, 5, 1 ),
                        'Non-blocking merge (CAS)'
                );
+
                if ( $this->cache instanceof MultiWriteBagOStuff ) {
-                       $wrapper = \Wikimedia\TestingAccessWrapper::newFromObject( $this->cache );
-                       $n = count( $wrapper->caches );
+                       $wrapper = TestingAccessWrapper::newFromObject( $this->cache );
+                       $this->assertEquals( count( $wrapper->caches ), $calls );
                } else {
-                       $n = 1;
+                       $this->assertEquals( 1, $calls );
                }
-               $this->assertEquals( $n, $calls );
        }
 
        /**
@@ -115,10 +116,17 @@ class BagOStuffTest extends MediaWikiTestCase {
                $value = 'meow';
 
                $this->cache->add( $key, $value, 5 );
-               $this->assertTrue( $this->cache->changeTTL( $key, 5 ) );
+               $this->assertEquals( $value, $this->cache->get( $key ) );
+               $this->assertTrue( $this->cache->changeTTL( $key, 10 ) );
+               $this->assertTrue( $this->cache->changeTTL( $key, 10 ) );
+               $this->assertTrue( $this->cache->changeTTL( $key, 0 ) );
                $this->assertEquals( $this->cache->get( $key ), $value );
                $this->cache->delete( $key );
-               $this->assertFalse( $this->cache->changeTTL( $key, 5 ) );
+               $this->assertFalse( $this->cache->changeTTL( $key, 15 ) );
+
+               $this->cache->add( $key, $value, 5 );
+               $this->assertTrue( $this->cache->changeTTL( $key, time() - 3600 ) );
+               $this->assertFalse( $this->cache->get( $key ) );
        }
 
        /**
@@ -126,7 +134,9 @@ class BagOStuffTest extends MediaWikiTestCase {
         */
        public function testAdd() {
                $key = $this->cache->makeKey( self::TEST_KEY );
+               $this->assertFalse( $this->cache->get( $key ) );
                $this->assertTrue( $this->cache->add( $key, 'test', 5 ) );
+               $this->assertFalse( $this->cache->add( $key, 'test', 5 ) );
        }
 
        /**
@@ -237,20 +247,73 @@ class BagOStuffTest extends MediaWikiTestCase {
                        $this->cache->makeKey( 'test-6' ) => 'ever'
                ];
 
-               $this->cache->setMulti( $map, 5 );
+               $this->assertTrue( $this->cache->setMulti( $map ) );
                $this->assertEquals(
                        $map,
                        $this->cache->getMulti( array_keys( $map ) )
                );
 
-               $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) );
+               $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ) ) );
 
+               $this->assertEquals(
+                       [],
+                       $this->cache->getMulti( array_keys( $map ), BagOStuff::READ_LATEST )
+               );
                $this->assertEquals(
                        [],
                        $this->cache->getMulti( array_keys( $map ) )
                );
        }
 
+       /**
+        * @covers BagOStuff::get
+        * @covers BagOStuff::getMulti
+        * @covers BagOStuff::merge
+        * @covers BagOStuff::delete
+        */
+       public function testSetSegmentable() {
+               $key = $this->cache->makeKey( self::TEST_KEY );
+               $tiny = 418;
+               $small = wfRandomString( 32 );
+               // 64 * 8 * 32768 = 16777216 bytes
+               $big = str_repeat( wfRandomString( 32 ) . '-' . wfRandomString( 32 ), 32768 );
+
+               $callback = function ( $cache, $key, $oldValue ) {
+                       return $oldValue . '!';
+               };
+
+               foreach ( [ $tiny, $small, $big ] as $value ) {
+                       $this->cache->set( $key, $value, 10, BagOStuff::WRITE_ALLOW_SEGMENTS );
+                       $this->assertEquals( $value, $this->cache->get( $key ) );
+                       $this->assertEquals( $value, $this->cache->getMulti( [ $key ] )[$key] );
+
+                       $this->assertTrue( $this->cache->merge( $key, $callback, 5 ) );
+                       $this->assertEquals( "$value!", $this->cache->get( $key ) );
+                       $this->assertEquals( "$value!", $this->cache->getMulti( [ $key ] )[$key] );
+
+                       $this->assertTrue( $this->cache->deleteMulti( [ $key ] ) );
+                       $this->assertFalse( $this->cache->get( $key ) );
+                       $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) );
+
+                       $this->cache->set( $key, "@$value", 10, BagOStuff::WRITE_ALLOW_SEGMENTS );
+                       $this->assertEquals( "@$value", $this->cache->get( $key ) );
+                       $this->assertTrue( $this->cache->delete( $key, BagOStuff::WRITE_PRUNE_SEGMENTS ) );
+                       $this->assertFalse( $this->cache->get( $key ) );
+                       $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) );
+               }
+
+               $this->cache->set( $key, 666, 10, BagOStuff::WRITE_ALLOW_SEGMENTS );
+
+               $this->assertEquals( 667, $this->cache->incr( $key ) );
+               $this->assertEquals( 667, $this->cache->get( $key ) );
+
+               $this->assertEquals( 664, $this->cache->decr( $key, 3 ) );
+               $this->assertEquals( 664, $this->cache->get( $key ) );
+
+               $this->assertTrue( $this->cache->delete( $key ) );
+               $this->assertFalse( $this->cache->get( $key ) );
+       }
+
        /**
         * @covers BagOStuff::getScopedLock
         */
@@ -316,4 +379,11 @@ class BagOStuffTest extends MediaWikiTestCase {
                $this->assertTrue( $this->cache->unlock( $key2 ) );
                $this->assertTrue( $this->cache->unlock( $key2 ) );
        }
+
+       public function tearDown() {
+               $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) );
+               $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' );
+
+               parent::tearDown();
+       }
 }
index dd86a73..857f709 100644 (file)
@@ -13,6 +13,7 @@ use Wikimedia\Rdbms\ConnectionManager;
  * @author Daniel Kinzler
  */
 class ConnectionManagerTest extends \PHPUnit\Framework\TestCase {
+       use \PHPUnit4And6Compat;
 
        /**
         * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
@@ -26,11 +27,7 @@ class ConnectionManagerTest extends \PHPUnit\Framework\TestCase {
         * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
         */
        private function getLoadBalancerMock() {
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $lb;
+               return $this->createMock( LoadBalancer::class );
        }
 
        public function testGetReadConnection_nullGroups() {
index 8d7d104..3492c3d 100644 (file)
@@ -13,6 +13,7 @@ use Wikimedia\Rdbms\SessionConsistentConnectionManager;
  * @author Daniel Kinzler
  */
 class SessionConsistentConnectionManagerTest extends \PHPUnit\Framework\TestCase {
+       use \PHPUnit4And6Compat;
 
        /**
         * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
@@ -26,11 +27,7 @@ class SessionConsistentConnectionManagerTest extends \PHPUnit\Framework\TestCase
         * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
         */
        private function getLoadBalancerMock() {
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $lb;
+               return $this->createMock( LoadBalancer::class );
        }
 
        public function testGetReadConnection() {
index 33e5c3b..833ac2c 100644 (file)
@@ -1,9 +1,8 @@
 <?php
 
-use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBConnRef;
 use Wikimedia\Rdbms\FakeResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\ILoadBalancer;
 use Wikimedia\Rdbms\ResultWrapper;
 
@@ -40,7 +39,7 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase {
         * @return IDatabase
         */
        private function getDatabaseMock() {
-               $db = $this->getMockBuilder( Database::class )
+               $db = $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
@@ -60,12 +59,6 @@ class DBConnRefTest extends PHPUnit\Framework\TestCase {
                $db->method( 'isOpen' )->willReturnCallback( function () use ( &$open ) {
                        return $open;
                } );
-               $db->method( 'open' )->willReturnCallback( function () use ( &$open ) {
-                       $open = true;
-
-                       return $open;
-               } );
-               $db->method( '__toString' )->willReturn( 'MOCK_DB' );
 
                return $db;
        }
index c0d2555..0e133d8 100644 (file)
@@ -1878,7 +1878,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
 
        /**
         * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
-        * @covers \Wikimedia\Rdbms\Database::assertTransactionStatus
+        * @covers \Wikimedia\Rdbms\Database::assertQueryIsCurrentlyAllowed
         */
        public function testTransactionErrorState1() {
                $wrapper = TestingAccessWrapper::newFromObject( $this->database );
index bc0ca2a..b6f8f9c 100644 (file)
@@ -34,6 +34,30 @@ class BlockLogFormatterTest extends LogFormatterTestCase {
                                                'duration' => 'infinite',
                                                'flags' => [ 'anononly' ],
                                        ],
+                                       'preload' => [ new TitleValue( NS_USER_TALK, 'Logtestuser' ) ],
+                               ],
+                       ],
+
+                       // With blank page title (T224811)
+                       [
+                               [
+                                       'type' => 'block',
+                                       'action' => 'block',
+                                       'comment' => 'Block comment',
+                                       'user' => 0,
+                                       'user_text' => 'Sysop',
+                                       'namespace' => NS_USER,
+                                       'title' => '',
+                                       'params' => [],
+                               ],
+                               [
+                                       'text' => 'Sysop blocked (no username available) '
+                                               . 'with an expiration time of indefinite',
+                                       'api' => [
+                                               'duration' => 'infinite',
+                                               'flags' => [],
+                                       ],
+                                       'preload' => [],
                                ],
                        ],
 
index 0e6855d..6648c31 100644 (file)
@@ -457,7 +457,7 @@ class DeleteLogFormatterTest extends LogFormatterTestCase {
                                ],
                        ],
 
-                       // Legacy format
+                       // Legacy formats
                        [
                                [
                                        'type' => 'suppress',
@@ -495,6 +495,27 @@ class DeleteLogFormatterTest extends LogFormatterTestCase {
                                        ],
                                ],
                        ],
+                       [
+                               [
+                                       'type' => 'delete',
+                                       'action' => 'revision',
+                                       'comment' => 'Old rows might lack ofield/nfield (T224815)',
+                                       'namespace' => NS_MAIN,
+                                       'title' => 'Page',
+                                       'params' => [
+                                               'oldid',
+                                               '1234',
+                                       ],
+                               ],
+                               [
+                                       'legacy' => true,
+                                       'text' => 'User changed visibility of revisions on page Page',
+                                       'api' => [
+                                               'type' => 'oldid',
+                                               'ids' => [ '1234' ],
+                                       ],
+                               ],
+                       ]
                ];
        }
 
index 883af71..fc2ab91 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+use MediaWiki\Linker\LinkTarget;
 
 /**
  * @since 1.26
@@ -22,6 +23,22 @@ abstract class LogFormatterTestCase extends MediaWikiLangTestCase {
                        self::removeApiMetaData( $formatter->formatParametersForApi() ),
                        'Api log params is equal to expected array'
                );
+
+               if ( isset( $extra['preload'] ) ) {
+                       $this->assertArrayEquals(
+                               $this->getLinkTargetsAsStrings( $extra['preload'] ),
+                               $this->getLinkTargetsAsStrings(
+                                       $formatter->getPreloadTitles()
+                               )
+                       );
+               }
+       }
+
+       private function getLinkTargetsAsStrings( array $linkTargets ) {
+               return array_map( function ( LinkTarget $t ) {
+                       return $t->getInterwiki() . ':' . $t->getNamespace() . ':'
+                               . $t->getDBkey() . '#' . $t->getFragment();
+               }, $linkTargets );
        }
 
        protected function isLegacy( $extra ) {
index 83554d2..32a6b6a 100644 (file)
@@ -77,21 +77,18 @@ class BitmapMetadataHandlerTest extends MediaWikiTestCase {
                $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
                        'iptc-timetest.jpg' );
 
+               // raw date is 2020:07:13 14:04:05+11:32
                $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] );
+               // raw date is 1997:03:02 03:01:02-03:00
                $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] );
-       }
 
-       /**
-        * File has an invalid time (+ one valid but really weird time)
-        * that shouldn't be included
-        * @covers BitmapMetadataHandler::Jpeg
-        */
-       public function testIPTCDatesInvalid() {
                $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
                        'iptc-timetest-invalid.jpg' );
 
+               // raw date is 1845:03:02 03:01:02-03:00
                $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] );
-               $this->assertFalse( isset( $meta['DateTimeDigitized'] ) );
+               // raw date is 1942:07:13 25:05:02+00:00
+               $this->assertSame( '1942:07:14 01:05:02', $meta['DateTimeDigitized'] );
        }
 
        /**
index 432754b..45971da 100644 (file)
@@ -8,7 +8,7 @@ class MemcachedBagOStuffTest extends MediaWikiTestCase {
 
        protected function setUp() {
                parent::setUp();
-               $this->cache = new MemcachedBagOStuff( [ 'keyspace' => 'test' ] );
+               $this->cache = new MemcachedPhpBagOStuff( [ 'keyspace' => 'test', 'servers' => [] ] );
        }
 
        /**
index 6b3e05d..3b2b105 100644 (file)
@@ -48,6 +48,9 @@ class PreprocessorTest extends MediaWikiTestCase {
                $this->mOptions = ParserOptions::newFromUserAndLang( new User,
                        MediaWikiServices::getInstance()->getContentLanguage() );
 
+               # Suppress deprecation warning for Preprocessor_DOM while testing
+               $this->hideDeprecated( 'Preprocessor_DOM::__construct' );
+
                $this->mPreprocessors = [];
                foreach ( self::$classNames as $className ) {
                        $this->mPreprocessors[$className] = new $className( $this );
index 1b67bbd..8ddd798 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use Wikimedia\TestingAccessWrapper;
+
 /**
  * @todo Tests covering decodeCharReferences can be refactored into a single
  * method and dataprovider.
@@ -249,6 +251,8 @@ class SanitizerTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideDeprecatedAttributes
         * @covers Sanitizer::fixTagAttributes
+        * @covers Sanitizer::validateTagAttributes
+        * @covers Sanitizer::validateAttributes
         */
        public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) {
                $this->assertEquals( " $inputAttr",
@@ -274,6 +278,59 @@ class SanitizerTest extends MediaWikiTestCase {
                ];
        }
 
+       /**
+        * @dataProvider provideValidateTagAttributes
+        * @covers Sanitizer::validateTagAttributes
+        * @covers Sanitizer::validateAttributes
+        */
+       public function testValidateTagAttributes( $element, $attribs, $expected ) {
+               $actual = Sanitizer::validateTagAttributes( $attribs, $element );
+               $this->assertArrayEquals( $expected, $actual, false, true );
+       }
+
+       public static function provideValidateTagAttributes() {
+               return [
+                       [ 'math',
+                         [ 'id' => 'foo bar', 'bogus' => 'stripped', 'data-foo' => 'bar' ],
+                         [ 'id' => 'foo_bar', 'data-foo' => 'bar' ],
+                       ],
+                       [ 'meta',
+                         [ 'id' => 'foo bar', 'itemprop' => 'foo', 'content' => 'bar' ],
+                         [ 'itemprop' => 'foo', 'content' => 'bar' ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAttributeWhitelist
+        * @covers Sanitizer::attributeWhitelist
+        */
+       public function testAttributeWhitelist( $element, $attribs ) {
+               $this->hideDeprecated( 'Sanitizer::attributeWhitelist' );
+               $this->hideDeprecated( 'Sanitizer::setupAttributeWhitelist' );
+               $actual = Sanitizer::attributeWhitelist( $element );
+               $this->assertArrayEquals( $attribs, $actual );
+       }
+
+       /**
+        * @dataProvider provideAttributeWhitelist
+        * @covers Sanitizer::attributeWhitelistInternal
+        */
+       public function testAttributeWhitelistInternal( $element, $attribs ) {
+               $sanitizer = TestingAccessWrapper::newFromClass( Sanitizer::class );
+               $actual = $sanitizer->attributeWhitelistInternal( $element );
+               $this->assertArrayEquals( $attribs, array_keys( $actual ) );
+       }
+
+       public function provideAttributeWhitelist() {
+               /** [ <element>, [ <good attribute 1>, <good attribute 2>, ...] ] */
+               return [
+                       [ 'math', [ 'class', 'style', 'id', 'title' ] ],
+                       [ 'meta', [ 'itemprop', 'content' ] ],
+                       [ 'link', [ 'itemprop', 'href', 'title' ] ],
+               ];
+       }
+
        /**
         * @dataProvider provideCssCommentsFixtures
         * @covers Sanitizer::checkCss
diff --git a/tests/phpunit/includes/password/PasswordFactoryTest.php b/tests/phpunit/includes/password/PasswordFactoryTest.php
deleted file mode 100644 (file)
index a7b3557..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-/**
- * @covers PasswordFactory
- */
-class PasswordFactoryTest extends MediaWikiTestCase {
-       public function testConstruct() {
-               $pf = new PasswordFactory();
-               $this->assertEquals( [ '' ], array_keys( $pf->getTypes() ) );
-               $this->assertEquals( '', $pf->getDefaultType() );
-
-               $pf = new PasswordFactory( [
-                       'foo' => [ 'class' => 'FooPassword' ],
-                       'bar' => [ 'class' => 'BarPassword', 'baz' => 'boom' ],
-               ], 'foo' );
-               $this->assertEquals( [ '', 'foo', 'bar' ], array_keys( $pf->getTypes() ) );
-               $this->assertArraySubset( [ 'class' => 'BarPassword', 'baz' => 'boom' ], $pf->getTypes()['bar'] );
-               $this->assertEquals( 'foo', $pf->getDefaultType() );
-       }
-
-       public function testRegister() {
-               $pf = new PasswordFactory;
-               $pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
-               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
-       }
-
-       public function testSetDefaultType() {
-               $pf = new PasswordFactory;
-               $pf->register( '1', [ 'class' => InvalidPassword::class ] );
-               $pf->register( '2', [ 'class' => InvalidPassword::class ] );
-               $pf->setDefaultType( '1' );
-               $this->assertSame( '1', $pf->getDefaultType() );
-               $pf->setDefaultType( '2' );
-               $this->assertSame( '2', $pf->getDefaultType() );
-       }
-
-       /**
-        * @expectedException Exception
-        */
-       public function testSetDefaultTypeError() {
-               $pf = new PasswordFactory;
-               $pf->setDefaultType( 'bogus' );
-       }
-
-       public function testInit() {
-               $config = new HashConfig( [
-                       'PasswordConfig' => [
-                               'foo' => [ 'class' => InvalidPassword::class ],
-                       ],
-                       'PasswordDefault' => 'foo'
-               ] );
-               $pf = new PasswordFactory;
-               $pf->init( $config );
-               $this->assertSame( 'foo', $pf->getDefaultType() );
-               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
-       }
-
-       public function testNewFromCiphertext() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
-               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
-       }
-
-       public function provideNewFromCiphertextErrors() {
-               return [ [ 'blah' ], [ ':blah:' ] ];
-       }
-
-       /**
-        * @dataProvider provideNewFromCiphertextErrors
-        * @expectedException PasswordError
-        */
-       public function testNewFromCiphertextErrors( $hash ) {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->newFromCiphertext( $hash );
-       }
-
-       public function testNewFromType() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pw = $pf->newFromType( 'B' );
-               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
-       }
-
-       /**
-        * @expectedException PasswordError
-        */
-       public function testNewFromTypeError() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->newFromType( 'bogus' );
-       }
-
-       public function testNewFromPlaintext() {
-               $pf = new PasswordFactory;
-               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->setDefaultType( 'A' );
-
-               $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) );
-               $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) );
-               $this->assertInstanceOf( MWSaltedPassword::class,
-                       $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
-       }
-
-       public function testNeedsUpdate() {
-               $pf = new PasswordFactory;
-               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->setDefaultType( 'A' );
-
-               $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
-               $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
-       }
-
-       public function testGenerateRandomPasswordString() {
-               $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
-       }
-
-       public function testNewInvalidPassword() {
-               $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() );
-       }
-}
index 206160c..408a0a2 100644 (file)
@@ -116,9 +116,9 @@ Deprecation message.' ]
                        . '<script>(RLQ=window.RLQ||[]).push(function(){'
                        . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
                        . '});</script>' . "\n"
-                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles&amp;skin=fallback"/>' . "\n"
+                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles"/>' . "\n"
                        . '<style>.private{}</style>' . "\n"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback"></script>';
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
                // phpcs:enable
                $expected = self::expandVariables( $expected );
 
@@ -136,7 +136,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"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback&amp;target=example"></script>';
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;target=example"></script>';
                // phpcs:enable
 
                $this->assertSame( $expected, (string)$client->getHeadHtml() );
@@ -153,7 +153,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"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;safemode=1&amp;skin=fallback"></script>';
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;safemode=1"></script>';
                // phpcs:enable
 
                $this->assertSame( $expected, (string)$client->getHeadHtml() );
@@ -170,7 +170,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"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback"></script>';
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
                // phpcs:enable
 
                $this->assertSame( $expected, (string)$client->getHeadHtml() );
@@ -224,54 +224,54 @@ Deprecation message.' ]
                        ],
                        [
                                'context' => [],
-                               // Eg. startup module
-                               'modules' => [ 'test.scripts.raw' ],
+                               'modules' => [ 'test.scripts' ],
                                'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [],
-                               'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;skin=fallback"></script>',
+                               // Eg. startup module
+                               'extra' => [ 'raw' => '1' ],
+                               'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts&amp;only=scripts&amp;raw=1"></script>',
                        ],
                        [
                                'context' => [],
-                               'modules' => [ 'test.scripts.raw' ],
+                               'modules' => [ 'test.scripts' ],
                                'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [ 'sync' => '1' ],
-                               'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;skin=fallback&amp;sync=1"></script>',
+                               'extra' => [ 'raw' => '1', 'sync' => '1' ],
+                               'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts&amp;only=scripts&amp;raw=1&amp;sync=1"></script>',
                        ],
                        [
                                'context' => [],
                                '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\u0026skin=fallback\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=0a56zyi");});</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\u0026skin=fallback\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=0a56zyi");});</script>',
                        ],
                        [
                                'context' => [ 'debug' => 'true' ],
                                'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'extra' => [],
-                               'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles&amp;skin=fallback"/>' . "\n"
-                                       . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles&amp;skin=fallback"/>',
+                               'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles"/>' . "\n"
+                                       . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>',
                        ],
                        [
                                'context' => [ 'debug' => 'false' ],
                                'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'extra' => [],
-                               'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles&amp;skin=fallback"/>',
+                               'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles"/>',
                        ],
                        [
                                'context' => [],
                                'modules' => [ 'test.styles.noscript' ],
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'extra' => [],
-                               'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles&amp;skin=fallback"/></noscript>',
+                               'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles"/></noscript>',
                        ],
                        [
                                '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\u0026skin=fallback");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@09p30q0",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
@@ -307,7 +307,7 @@ Deprecation message.' ]
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'extra' => [],
                                'output' =>
-                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles&amp;skin=fallback"/>' . "\n"
+                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>' . "\n"
                                        . '<style>.shouldembed{}</style>'
                        ],
                        [
@@ -316,9 +316,9 @@ Deprecation message.' ]
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'extra' => [],
                                'output' =>
-                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n"
+                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles"/>' . "\n"
                                        . '<style>.orderingC{}.orderingD{}</style>' . "\n"
-                                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles&amp;skin=fallback"/>'
+                                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles"/>'
                        ],
                ];
                // phpcs:enable
@@ -418,7 +418,6 @@ Deprecation message.' ]
                        'test.scripts' => [],
                        'test.scripts.user' => [ 'group' => 'user' ],
                        'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
-                       'test.scripts.raw' => [ 'isRaw' => true ],
                        'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
 
                        'test.ordering.a' => [ 'shouldEmbed' => false ],
index 7f4d9a8..c3d5ec1 100644 (file)
@@ -47,8 +47,10 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
 
        public function testAccessors() {
                $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
+               $this->assertInstanceOf( ResourceLoader::class, $ctx->getResourceLoader() );
+               $this->assertInstanceOf( Config::class, $ctx->getConfig() );
                $this->assertInstanceOf( WebRequest::class, $ctx->getRequest() );
-               $this->assertInstanceOf( \Psr\Log\LoggerInterface::class, $ctx->getLogger() );
+               $this->assertInstanceOf( Psr\Log\LoggerInterface::class, $ctx->getLogger() );
        }
 
        public function testTypicalRequest() {
@@ -76,6 +78,38 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( 'zh|fallback|||styles|||||', $ctx->getHash() );
        }
 
+       public static function provideDirection() {
+               yield 'LTR language' => [
+                       [ 'lang' => 'en' ],
+                       'ltr',
+               ];
+               yield 'RTL language' => [
+                       [ 'lang' => 'he' ],
+                       'rtl',
+               ];
+               yield 'explicit LTR' => [
+                       [ 'lang' => 'he', 'dir' => 'ltr' ],
+                       'ltr',
+               ];
+               yield 'explicit RTL' => [
+                       [ 'lang' => 'en', 'dir' => 'rtl' ],
+                       'rtl',
+               ];
+               // Not supported, but tested to cover the case and detect change
+               yield 'invalid dir' => [
+                       [ 'lang' => 'he', 'dir' => 'xyz' ],
+                       'rtl',
+               ];
+       }
+
+       /**
+        * @dataProvider provideDirection
+        */
+       public function testDirection( array $params, $expected ) {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( $params ) );
+               $this->assertEquals( $expected, $ctx->getDirection() );
+       }
+
        public function testShouldInclude() {
                $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
                $this->assertTrue( $ctx->shouldIncludeScripts(), 'Scripts in combined' );
index fbef12e..5be0f9b 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 
 /**
- * @group Database
  * @group ResourceLoader
  */
 class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
@@ -19,11 +18,14 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                        }
                );
                $this->setService( 'SkinFactory', $skinFactory );
+
+               // This test is not expected to query any database
+               MediaWiki\MediaWikiServices::disableStorageBackend();
        }
 
        private static function getModules() {
                $base = [
-                       'localBasePath' => realpath( __DIR__ ),
+                       'localBasePath' => __DIR__,
                ];
 
                return [
@@ -229,12 +231,12 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
         */
        public function testMixedCssAnnotations() {
                $basePath = __DIR__ . '/../../data/css';
-               $testModule = new ResourceLoaderFileModule( [
+               $testModule = new ResourceLoaderFileTestModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'test.css' ],
                ] );
                $testModule->setName( 'testing' );
-               $expectedModule = new ResourceLoaderFileModule( [
+               $expectedModule = new ResourceLoaderFileTestModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'expected.css' ],
                ] );
@@ -319,7 +321,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
         */
        public function testBomConcatenation() {
                $basePath = __DIR__ . '/../../data/css';
-               $testModule = new ResourceLoaderFileModule( [
+               $testModule = new ResourceLoaderFileTestModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'bom.css' ],
                ] );
@@ -347,7 +349,6 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                $module = new ResourceLoaderFileTestModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'styles.less' ],
-               ], [
                        'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
                ] );
                $module->setName( 'test.less' );
@@ -355,27 +356,110 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
        }
 
+       public function provideGetVersionHash() {
+               $a = [];
+               $b = [
+                       'lessVars' => [ 'key' => 'value' ],
+               ];
+               yield 'with and without Less variables' => [ $a, $b, false ];
+
+               $a = [
+                       'lessVars' => [ 'key' => 'value1' ],
+               ];
+               $b = [
+                       'lessVars' => [ 'key' => 'value2' ],
+               ];
+               yield 'different Less variables' => [ $a, $b, false ];
+
+               $x = [
+                       'lessVars' => [ 'key' => 'value' ],
+               ];
+               yield 'identical Less variables' => [ $x, $x, true ];
+
+               $a = [
+                       'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
+                               return [ 'aaa' ];
+                       } ] ]
+               ];
+               $b = [
+                       'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
+                               return [ 'bbb' ];
+                       } ] ]
+               ];
+               yield 'packageFiles with different callback' => [ $a, $b, false ];
+
+               $a = [
+                       'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => function () {
+                               return [ 'x' ];
+                       } ] ]
+               ];
+               $b = [
+                       'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => function () {
+                               return [ 'x' ];
+                       } ] ]
+               ];
+               yield 'packageFiles with different file name and a callback' => [ $a, $b, false ];
+
+               $a = [
+                       'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
+                               return [ 'A-version' ];
+                       }, 'callback' => function () {
+                               throw new Exception( 'Unexpected computation' );
+                       } ] ]
+               ];
+               $b = [
+                       'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
+                               return [ 'B-version' ];
+                       }, 'callback' => function () {
+                               throw new Exception( 'Unexpected computation' );
+                       } ] ]
+               ];
+               yield 'packageFiles with different versionCallback' => [ $a, $b, false ];
+
+               $a = [
+                       'packageFiles' => [ [ 'name' => 'aaa.json',
+                               'versionCallback' => function () {
+                                       return [ 'X-version' ];
+                               },
+                               'callback' => function () {
+                                       throw new Exception( 'Unexpected computation' );
+                               }
+                       ] ]
+               ];
+               $b = [
+                       'packageFiles' => [ [ 'name' => 'bbb.json',
+                               'versionCallback' => function () {
+                                       return [ 'X-version' ];
+                               },
+                               'callback' => function () {
+                                       throw new Exception( 'Unexpected computation' );
+                               }
+                       ] ]
+               ];
+               yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
+       }
+
        /**
+        * @dataProvider provideGetVersionHash
         * @covers ResourceLoaderFileModule::getDefinitionSummary
         * @covers ResourceLoaderFileModule::getFileHashes
         */
-       public function testGetVersionHash() {
+       public function testGetVersionHash( $a, $b, $isEqual ) {
                $context = $this->getResourceLoaderContext();
 
-               // Less variables
-               $module = new ResourceLoaderFileTestModule();
-               $version = $module->getVersionHash( $context );
-               $module = new ResourceLoaderFileTestModule( [], [
-                       'lessVars' => [ 'key' => 'value' ],
-               ] );
-               $this->assertNotEquals(
-                       $version,
-                       $module->getVersionHash( $context ),
-                       'Using less variables is significant'
+               $moduleA = new ResourceLoaderFileTestModule( $a );
+               $versionA = $moduleA->getVersionHash( $context );
+               $moduleB = new ResourceLoaderFileTestModule( $b );
+               $versionB = $moduleB->getVersionHash( $context );
+
+               $this->assertSame(
+                       $isEqual,
+                       ( $versionA === $versionB ),
+                       'Whether versions hashes are equal'
                );
        }
 
-       public function providerGetScriptPackageFiles() {
+       public function provideGetScriptPackageFiles() {
                $basePath = __DIR__ . '/../../data/resourceloader';
                $base = [ 'localBasePath' => $basePath ];
                $commentScript = file_get_contents( "$basePath/script-comment.js" );
@@ -451,7 +535,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                                        'main' => 'init.js'
                                ]
                        ],
-                       [
+                       'package file with callback' => [
                                $base + [
                                        'packageFiles' => [
                                                [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
@@ -498,6 +582,34 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                                        'lang' => 'fy'
                                ]
                        ],
+                       'package file with callback and versionCallback' => [
+                               $base + [
+                                       'packageFiles' => [
+                                               [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
+                                               [ 'name' => 'data.json', 'versionCallback' => function ( $context ) {
+                                                       return $context->getLanguage();
+                                               }, 'callback' => function ( $context ) {
+                                                       return [ 'langCode' => $context->getLanguage() ];
+                                               } ],
+                                       ]
+                               ],
+                               [
+                                       'files' => [
+                                               'bar.js' => [
+                                                       'type' => 'script',
+                                                       'content' => "console.log('Hello');",
+                                               ],
+                                               'data.json' => [
+                                                       'type' => 'data',
+                                                       'content' => [ 'langCode' => 'fy' ]
+                                               ],
+                                       ],
+                                       'main' => 'bar.js'
+                               ],
+                               [
+                                       'lang' => 'fy'
+                               ]
+                       ],
                        [
                                $base + [
                                        'packageFiles' => [
@@ -506,7 +618,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                                ],
                                false
                        ],
-                       [
+                       'package file with invalid callback' => [
                                $base + [
                                        'packageFiles' => [
                                                [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
@@ -559,7 +671,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
        }
 
        /**
-        * @dataProvider providerGetScriptPackageFiles
+        * @dataProvider provideGetScriptPackageFiles
         * @covers ResourceLoaderFileModule::getScript
         * @covers ResourceLoaderFileModule::getPackageFiles
         * @covers ResourceLoaderFileModule::expandPackageFiles
index b0512fa..c3fc55a 100644 (file)
@@ -62,7 +62,6 @@ class ResourceLoaderImageTest extends ResourceLoaderTestCase {
                        'he' => 'rtl',
                        'ar' => 'rtl',
                ];
-               static $contexts = [];
 
                $image = $this->getTestImage( $imageName );
                $context = $this->getResourceLoaderContext( [
index 0c707d5..3f6e9b0 100644 (file)
@@ -56,7 +56,7 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase {
                );
 
                // Subclass
-               $module = new ResourceLoaderFileModuleTestModule( $baseParams );
+               $module = new ResourceLoaderFileModuleTestingSubclass( $baseParams );
                $this->assertNotEquals(
                        $version,
                        json_encode( $module->getVersionHash( $context ) ),
index b5dd008..99f5e1b 100644 (file)
@@ -105,6 +105,83 @@ mw.loader.register( [
         "c",
         "{blankVer}"
     ]
+] );',
+                       ] ],
+                       [ [
+                               // Regression test for T223402.
+                               'msg' => 'Optimise the dependency tree (indirect circular dependency)',
+                               'modules' => [
+                                       'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle1', 'util' ] ] ),
+                                       'middle1' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle2', 'util' ] ] ),
+                                       'middle2' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'bottom' ] ] ),
+                                       'bottom' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'top' ] ] ),
+                                       'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                               ],
+                               'out' => '
+mw.loader.addSource( {
+    "local": "/w/load.php"
+} );
+mw.loader.register( [
+    [
+        "top",
+        "{blankVer}",
+        [
+            1,
+            4
+        ]
+    ],
+    [
+        "middle1",
+        "{blankVer}",
+        [
+            2,
+            4
+        ]
+    ],
+    [
+        "middle2",
+        "{blankVer}",
+        [
+            3
+        ]
+    ],
+    [
+        "bottom",
+        "{blankVer}",
+        [
+            0
+        ]
+    ],
+    [
+        "util",
+        "{blankVer}"
+    ]
+] );',
+                       ] ],
+                       [ [
+                               // Regression test for T223402.
+                               'msg' => 'Optimise the dependency tree (direct circular dependency)',
+                               'modules' => [
+                                       'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'util', 'top' ] ] ),
+                                       'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                               ],
+                               'out' => '
+mw.loader.addSource( {
+    "local": "/w/load.php"
+} );
+mw.loader.register( [
+    [
+        "top",
+        "{blankVer}",
+        [
+            1,
+            0
+        ]
+    ],
+    [
+        "util",
+        "{blankVer}"
+    ]
 ] );',
                        ] ],
                        [ [
index 2e03163..c5e8e89 100644 (file)
@@ -119,15 +119,18 @@ class CommandTest extends PHPUnit\Framework\TestCase {
        }
 
        public function testT69870() {
-               $commandLine = wfIsWindows()
-                       // 333 = 331 + CRLF
-                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
-                       : 'printf "%-333333s" "*"';
+               if ( wfIsWindows() ) {
+                       // T209159: Anonymous pipe under Windows does not support asynchronous read and write,
+                       // and the default buffer is too small (~4K), it is easy to be blocked.
+                       $this->markTestSkipped(
+                               'T209159: Anonymous pipe under Windows cannot withstand such a large amount of data'
+                       );
+               }
 
                // Test several times because it involves a race condition that may randomly succeed or fail
                for ( $i = 0; $i < 10; $i++ ) {
                        $command = new Command();
-                       $output = $command->unsafeParams( $commandLine )
+                       $output = $command->unsafeParams( 'printf "%-333333s" "*"' )
                                ->execute()
                                ->getStdout();
                        $this->assertEquals( 333333, strlen( $output ) );
index 4f4fa25..4dd6c80 100644 (file)
@@ -15,9 +15,9 @@ class SpecialSearchTest extends MediaWikiTestCase {
         * @covers SpecialSearch::load
         * @dataProvider provideSearchOptionsTests
         * @param array $requested Request parameters. For example:
-        *   array( 'ns5' => true, 'ns6' => true). Null to use default options.
+        *   [ 'ns5' => true, 'ns6' => true ]. Null to use default options.
         * @param array $userOptions User options to test with. For example:
-        *   array('searchNs5' => 1 );. Null to use default options.
+        *   [ 'searchNs5' => 1 ];. Null to use default options.
         * @param string $expectedProfile An expected search profile name
         * @param array $expectedNS Expected namespaces
         * @param string $message
index b1262a3..7eb8fd5 100644 (file)
@@ -402,7 +402,7 @@ class NamespaceInfoTest extends MediaWikiTestCase {
        }
 
        /**
-        * @param $contentNamespaces To pass to constructor
+        * @param mixed $contentNamespaces To pass to constructor
         * @param array $expected
         * @dataProvider provideGetContentNamespaces
         * @covers NamespaceInfo::getContentNamespaces
index 55a29e3..b0c0fec 100644 (file)
@@ -2,6 +2,7 @@
 
 use MediaWiki\Auth\AuthManager;
 use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Block\CompositeBlock;
 use MediaWiki\Block\SystemBlock;
 
 /**
@@ -141,6 +142,34 @@ class PasswordResetTest extends MediaWikiTestCase {
                                'globalBlock' => null,
                                'isAllowed' => false,
                        ],
+                       'blocked with multiple blocks, all allowing password reset' => [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'block' => new CompositeBlock( [
+                                       'originalBlocks' => [
+                                               new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
+                                               new Block( [] ),
+                                       ]
+                               ] ),
+                               'globalBlock' => null,
+                               'isAllowed' => true,
+                       ],
+                       'blocked with multiple blocks, not all allowing password reset' => [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'block' => new CompositeBlock( [
+                                       'originalBlocks' => [
+                                               new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
+                                               new SystemBlock( [ 'systemBlock' => 'proxy' ] ),
+                                       ]
+                               ] ),
+                               'globalBlock' => null,
+                               'isAllowed' => false,
+                       ],
                        'all OK' => [
                                'passwordResetRoutes' => [ 'username' => true ],
                                'enableEmail' => true,
index 14ddd9f..79c6e96 100644 (file)
@@ -4,6 +4,7 @@ define( 'NS_UNITTEST', 5600 );
 define( 'NS_UNITTEST_TALK', 5601 );
 
 use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Block\CompositeBlock;
 use MediaWiki\Block\Restriction\PageRestriction;
 use MediaWiki\Block\Restriction\NamespaceRestriction;
 use MediaWiki\Block\SystemBlock;
@@ -66,6 +67,15 @@ class UserTest extends MediaWikiTestCase {
                ];
        }
 
+       private function setSessionUser( User $user, WebRequest $request ) {
+               $this->setMwGlobals( 'wgUser', $user );
+               RequestContext::getMain()->setUser( $user );
+               RequestContext::getMain()->setRequest( $request );
+               TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
+               $request->getSession()->setUser( $user );
+               $this->overrideMwServices();
+       }
+
        /**
         * @covers User::getGroupPermissions
         */
@@ -779,28 +789,20 @@ class UserTest extends MediaWikiTestCase {
         * @covers User::getBlockedStatus
         */
        public function testSoftBlockRanges() {
-               $setSessionUser = function ( User $user, WebRequest $request ) {
-                       $this->setMwGlobals( 'wgUser', $user );
-                       RequestContext::getMain()->setUser( $user );
-                       RequestContext::getMain()->setRequest( $request );
-                       TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
-                       $request->getSession()->setUser( $user );
-                       $this->overrideMwServices();
-               };
                $this->setMwGlobals( 'wgSoftBlockRanges', [ '10.0.0.0/8' ] );
 
                // IP isn't in $wgSoftBlockRanges
                $wgUser = new User();
                $request = new FauxRequest();
                $request->setIP( '192.168.0.1' );
-               $setSessionUser( $wgUser, $request );
+               $this->setSessionUser( $wgUser, $request );
                $this->assertNull( $wgUser->getBlock() );
 
                // IP is in $wgSoftBlockRanges
                $wgUser = new User();
                $request = new FauxRequest();
                $request->setIP( '10.20.30.40' );
-               $setSessionUser( $wgUser, $request );
+               $this->setSessionUser( $wgUser, $request );
                $block = $wgUser->getBlock();
                $this->assertInstanceOf( SystemBlock::class, $block );
                $this->assertSame( 'wgSoftBlockRanges', $block->getSystemBlockType() );
@@ -809,7 +811,7 @@ class UserTest extends MediaWikiTestCase {
                $wgUser = $this->getTestUser()->getUser();
                $request = new FauxRequest();
                $request->setIP( '10.20.30.40' );
-               $setSessionUser( $wgUser, $request );
+               $this->setSessionUser( $wgUser, $request );
                $this->assertFalse( $wgUser->isAnon(), 'sanity check' );
                $this->assertNull( $wgUser->getBlock() );
        }
@@ -1316,6 +1318,35 @@ class UserTest extends MediaWikiTestCase {
                $this->assertFalse( $user->isBlockedFrom( $ut ) );
        }
 
+       /**
+        * @covers User::getBlockedStatus
+        */
+       public function testCompositeBlocks() {
+               $user = $this->getMutableTestUser()->getUser();
+               $request = $user->getRequest();
+               $this->setSessionUser( $user, $request );
+
+               $ipBlock = new Block( [
+                       'address' => $user->getRequest()->getIP(),
+                       'by' => $this->getTestSysop()->getUser()->getId(),
+                       'createAccount' => true,
+               ] );
+               $ipBlock->insert();
+
+               $userBlock = new Block( [
+                       'address' => $user,
+                       'by' => $this->getTestSysop()->getUser()->getId(),
+                       'createAccount' => false,
+               ] );
+               $userBlock->insert();
+
+               $block = $user->getBlock();
+               $this->assertInstanceOf( CompositeBlock::class, $block );
+               $this->assertTrue( $block->isCreateAccountBlocked() );
+               $this->assertTrue( $block->appliesToPasswordReset() );
+               $this->assertTrue( $block->appliesToNamespace( NS_MAIN ) );
+       }
+
        /**
         * @covers User::isBlockedFrom
         * @dataProvider provideIsBlockedFrom
index 52b1433..dd21add 100644 (file)
@@ -49,7 +49,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase {
                        $this->assertEquals( $response[$pos], $rows, "Testing row in position $pos" );
                        $pos++;
                }
-               // -1 is because the final array() marks the end and isnt included
+               // -1 is because the final [] marks the end and isn't included
                $this->assertEquals( count( $response ) - 1, $pos );
        }
 
index 9c7c50f..6f8aa52 100644 (file)
@@ -29,10 +29,21 @@ class ClassCollectorTest extends PHPUnit\Framework\TestCase {
                                "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Bar' );",
                                [ 'Example\Foo', 'Bar' ],
                        ],
+                       [
+                               // Support a multiline 'class' statement
+                               "namespace Example;\nclass Foo extends\n\tFooBase {\n\t"
+                                               . "public function x() {}\n}\nclass_alias( 'Example\Foo', 'Bar' );",
+                               [ 'Example\Foo', 'Bar' ],
+                       ],
                        [
                                "class_alias( Foo::class, 'Bar' );",
                                [ 'Bar' ],
                        ],
+                       [
+                               // Support nested class_alias() calls
+                                       "if ( false ) {\n\tclass_alias( Foo::class, 'Bar' );\n}",
+                                       [ 'Bar' ],
+                       ],
                        [
                                // Namespaced class is not currently supported. Must use namespace declaration
                                // earlier in the file.
index 6b81a66..e600021 100644 (file)
@@ -67,7 +67,7 @@ class UIDGeneratorTest extends PHPUnit\Framework\TestCase {
        }
 
        /**
-        * array( method, length, bits, hostbits )
+        * [ method, length, bits, hostbits ]
         * NOTE: When adding a new method name here please update the covers tags for the tests!
         */
        public static function provider_testTimestampedUID() {
index 5068e70..be38aff 100644 (file)
@@ -58,7 +58,7 @@ class CategoriesRdfTest extends MediaWikiLangTestCase {
                        'wgServer' => 'http://acme.test',
                        'wgCanonicalServer' => 'http://acme.test',
                        'wgArticlePath' => '/wiki/$1',
-                       'wgRightsUrl' => '//creativecommons.org/licenses/by-sa/3.0/',
+                       'wgRightsUrl' => 'https://creativecommons.org/licenses/by-sa/3.0/',
                ] );
 
                $dumpScript =
index 37babce..c20be57 100644 (file)
@@ -198,7 +198,7 @@ class AutoLoaderStructureTest extends MediaWikiTestCase {
        }
 
        public function testAutoloadOrder() {
-               $path = realpath( __DIR__ . '/../../..' );
+               $path = __DIR__ . '/../../..';
                $oldAutoload = file_get_contents( $path . '/autoload.php' );
                $generator = new AutoloadGenerator( $path, 'local' );
                $generator->setPsr4Namespaces( AutoLoader::getAutoloadNamespaces() );
index 4c34208..78e5763 100644 (file)
@@ -9,11 +9,9 @@ use Wikimedia\TestingAccessWrapper;
  * @author Antoine Musso
  * @author Niklas Laxström
  * @author Santhosh Thottingal
- * @author Timo Tijhof
  * @copyright © 2012, Antoine Musso
  * @copyright © 2012, Niklas Laxström
  * @copyright © 2012, Santhosh Thottingal
- * @copyright © 2012, Timo Tijhof
  */
 class ResourcesTest extends MediaWikiTestCase {
 
index de68fec..cc6ac31 100644 (file)
                <testsuite name="documentation">
                        <directory>documentation</directory>
                </testsuite>
+               <testsuite name="unit">
+                       <directory>unit</directory>
+               </testsuite>
        </testsuites>
        <groups>
                <exclude>
-                       <group>Utility</group>
                        <group>Broken</group>
-                       <group>Stub</group>
                </exclude>
        </groups>
        <filter>
                        </exclude>
                </whitelist>
        </filter>
+       <listeners>
+               <listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener">
+                       <arguments>
+                               <array>
+                                       <element key="slowThreshold">
+                                               <integer>50</integer>
+                                       </element>
+                                       <element key="reportLength">
+                                               <integer>50</integer>
+                                       </element>
+                               </array>
+                       </arguments>
+               </listener>
+       </listeners>
 </phpunit>
diff --git a/tests/phpunit/unit-tests.xml b/tests/phpunit/unit-tests.xml
new file mode 100644 (file)
index 0000000..cd4118c
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="unit/initUnitTests.php"
+                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
+
+                colors="true"
+                backupGlobals="false"
+                convertErrorsToExceptions="true"
+                convertNoticesToExceptions="true"
+                convertWarningsToExceptions="true"
+                forceCoversAnnotation="true"
+                stopOnFailure="false"
+                timeoutForSmallTests="10"
+                timeoutForMediumTests="30"
+                timeoutForLargeTests="60"
+                beStrictAboutTestsThatDoNotTestAnything="true"
+                beStrictAboutOutputDuringTests="true"
+                beStrictAboutTestSize="true"
+                verbose="false">
+       <testsuites>
+               <testsuite name="tests">
+                       <directory>unit</directory>
+               </testsuite>
+       </testsuites>
+       <groups>
+               <exclude>
+                       <group>Broken</group>
+               </exclude>
+       </groups>
+       <filter>
+               <whitelist addUncoveredFilesFromWhitelist="true">
+                       <directory suffix=".php">../../includes</directory>
+                       <directory suffix=".php">../../languages</directory>
+                       <directory suffix=".php">../../maintenance</directory>
+                       <exclude>
+                               <directory suffix=".php">../../languages/messages</directory>
+                               <file>../../languages/data/normalize-ar.php</file>
+                               <file>../../languages/data/normalize-ml.php</file>
+                       </exclude>
+               </whitelist>
+       </filter>
+</phpunit>
diff --git a/tests/phpunit/unit/includes/password/PasswordFactoryTest.php b/tests/phpunit/unit/includes/password/PasswordFactoryTest.php
new file mode 100644 (file)
index 0000000..cbfddd4
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @covers PasswordFactory
+ */
+class PasswordFactoryTest extends MediaWikiUnitTestCase {
+       public function testConstruct() {
+               $pf = new PasswordFactory();
+               $this->assertEquals( [ '' ], array_keys( $pf->getTypes() ) );
+               $this->assertEquals( '', $pf->getDefaultType() );
+
+               $pf = new PasswordFactory( [
+                       'foo' => [ 'class' => 'FooPassword' ],
+                       'bar' => [ 'class' => 'BarPassword', 'baz' => 'boom' ],
+               ], 'foo' );
+               $this->assertEquals( [ '', 'foo', 'bar' ], array_keys( $pf->getTypes() ) );
+               $this->assertArraySubset( [ 'class' => 'BarPassword', 'baz' => 'boom' ], $pf->getTypes()['bar'] );
+               $this->assertEquals( 'foo', $pf->getDefaultType() );
+       }
+
+       public function testRegister() {
+               $pf = new PasswordFactory;
+               $pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testSetDefaultType() {
+               $pf = new PasswordFactory;
+               $pf->register( '1', [ 'class' => InvalidPassword::class ] );
+               $pf->register( '2', [ 'class' => InvalidPassword::class ] );
+               $pf->setDefaultType( '1' );
+               $this->assertSame( '1', $pf->getDefaultType() );
+               $pf->setDefaultType( '2' );
+               $this->assertSame( '2', $pf->getDefaultType() );
+       }
+
+       /**
+        * @expectedException Exception
+        */
+       public function testSetDefaultTypeError() {
+               $pf = new PasswordFactory;
+               $pf->setDefaultType( 'bogus' );
+       }
+
+       public function testInit() {
+               $config = new HashConfig( [
+                       'PasswordConfig' => [
+                               'foo' => [ 'class' => InvalidPassword::class ],
+                       ],
+                       'PasswordDefault' => 'foo'
+               ] );
+               $pf = new PasswordFactory;
+               $pf->init( $config );
+               $this->assertSame( 'foo', $pf->getDefaultType() );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testNewFromCiphertext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       public function provideNewFromCiphertextErrors() {
+               return [ [ 'blah' ], [ ':blah:' ] ];
+       }
+
+       /**
+        * @dataProvider provideNewFromCiphertextErrors
+        * @expectedException PasswordError
+        */
+       public function testNewFromCiphertextErrors( $hash ) {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->newFromCiphertext( $hash );
+       }
+
+       public function testNewFromType() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pw = $pf->newFromType( 'B' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       /**
+        * @expectedException PasswordError
+        */
+       public function testNewFromTypeError() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->newFromType( 'bogus' );
+       }
+
+       public function testNewFromPlaintext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) );
+               $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) );
+               $this->assertInstanceOf( MWSaltedPassword::class,
+                       $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testNeedsUpdate() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
+               $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testGenerateRandomPasswordString() {
+               $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
+       }
+
+       public function testNewInvalidPassword() {
+               $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() );
+       }
+}
diff --git a/tests/phpunit/unit/initUnitTests.php b/tests/phpunit/unit/initUnitTests.php
new file mode 100644 (file)
index 0000000..2121877
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * PHPUnit bootstrap file for the unit test suite.
+ *
+ * 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 Testing
+ */
+
+if ( PHP_SAPI !== 'cli' ) {
+       die( 'This file is only meant to be executed indirectly by PHPUnit\'s bootstrap process!' );
+}
+
+/**
+ * PHPUnit includes the bootstrap file inside a method body, while most MediaWiki startup files
+ * assume to be included in the global scope.
+ * This utility provides a way to include these files: it makes all globals available in the
+ * inclusion scope before including the file, then exports all new or changed globals.
+ *
+ * @param string $fileName the file to include
+ */
+function wfRequireOnceInGlobalScope( $fileName ) {
+       // phpcs:disable MediaWiki.Usage.ForbiddenFunctions.extract
+       extract( $GLOBALS, EXTR_REFS | EXTR_SKIP );
+       // phpcs:enable
+
+       require_once $fileName;
+
+       foreach ( get_defined_vars() as $varName => $value ) {
+               $GLOBALS[$varName] = $value;
+       }
+}
+
+define( 'MEDIAWIKI', true );
+define( 'MW_PHPUNIT_TEST', true );
+
+// We don't use a settings file here but some code still assumes that one exists
+define( 'MW_CONFIG_FILE', 'LocalSettings.php' );
+
+$IP = realpath( __DIR__ . '/../../..' );
+
+// these variables must be defined before setup runs
+$GLOBALS['IP'] = $IP;
+$GLOBALS['wgCommandLineMode'] = true;
+
+require_once "$IP/tests/common/TestSetup.php";
+
+wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/DefaultSettings.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/GlobalFunctions.php" );
+
+require_once "$IP/tests/common/TestsAutoLoader.php";
+
+TestSetup::applyInitialConfig();
index 9f57190..1525f04 100644 (file)
@@ -18,9 +18,7 @@
  * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
- * @package MediaWiki
  * @author Lupo
- * @since 1.20
  */
 
 // This file doesn't run as part of MediaWiki
index db96fd5..5d229a3 100644 (file)
@@ -18,9 +18,6 @@
  * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
- * @package MediaWiki
- * @author Timo Tijhof
- * @since 1.20
  */
 
 // This file doesn't run as part of MediaWiki
index a18ee41..5a0f603 100644 (file)
@@ -66,7 +66,7 @@ describe( 'Rollback with confirmation', function () {
                }, 5000, 'Expected rollback page to appear.' );
        } );
 
-       it( 'should verify rollbacks via GET requests are confirmed on a follow-up page', function () {
+       it.skip( 'should verify rollbacks via GET requests are confirmed on a follow-up page', function () {
                var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' );
                browser.url( rollbackActionUrl );
 
@@ -120,7 +120,7 @@ describe( 'Rollback without confirmation', function () {
                }, 5000, 'Expected rollback page to appear.' );
        } );
 
-       it( 'should perform rollback via GET request without asking the user to confirm', function () {
+       it.skip( 'should perform rollback via GET request without asking the user to confirm', function () {
                var rollbackActionUrl = HistoryPage.rollbackLink.getAttribute( 'href' );
                browser.url( rollbackActionUrl );
 
index cf9bd2c..4e5c213 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -155,7 +155,11 @@ function wfStreamThumb( array $params ) {
        // Check permissions if there are read restrictions
        $varyHeader = [];
        if ( !in_array( 'read', User::getGroupPermissions( [ '*' ] ), true ) ) {
-               if ( !$img->getTitle() || !$img->getTitle()->userCan( 'read' ) ) {
+               $user = RequestContext::getMain()->getUser();
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+               $imgTitle = $img->getTitle();
+
+               if ( !$imgTitle || !$permissionManager->userCan( 'read', $user, $imgTitle ) ) {
                        wfThumbError( 403, 'Access denied. You do not have permission to access ' .
                                'the source file.' );
                        return;