Merge "Update type of PermissionManager::resultToError"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 18 Jun 2019 19:38:39 +0000 (19:38 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 18 Jun 2019 19:38:39 +0000 (19:38 +0000)
441 files changed:
.phpcs.xml
.travis.yml
RELEASE-NOTES-1.33 [deleted file]
RELEASE-NOTES-1.34
autoload.php
composer.json
docs/extension.schema.v1.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/ForeignResourceManager.php
includes/GlobalFunctions.php
includes/Html.php
includes/Linker.php
includes/OutputPage.php
includes/PHPVersionCheck.php
includes/PathRouter.php
includes/Pingback.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/RevisionRecord.php
includes/Revision/RevisionStore.php
includes/ServiceWiring.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/InfoAction.php
includes/actions/McrUndoAction.php
includes/api/ApiCSPReport.php
includes/api/ApiImageRotate.php
includes/api/ApiLogin.php
includes/api/ApiMove.php
includes/api/ApiQueryBacklinksprop.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryImageInfo.php
includes/api/i18n/es.json
includes/api/i18n/fa.json
includes/api/i18n/it.json
includes/api/i18n/ko.json
includes/api/i18n/pl.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/block/AbstractBlock.php
includes/block/BlockManager.php
includes/block/CompositeBlock.php [new file with mode: 0644]
includes/block/DatabaseBlock.php
includes/cache/BacklinkCache.php
includes/cache/LinkBatch.php
includes/changetags/ChangeTags.php
includes/content/FileContentHandler.php
includes/context/RequestContext.php
includes/db/DatabaseOracle.php
includes/debug/logger/monolog/CeeFormatter.php
includes/deferred/LinksUpdate.php
includes/deferred/WANCacheReapUpdate.php
includes/diff/DifferenceEngine.php
includes/export/WikiExporter.php
includes/export/XmlDumpWriter.php
includes/externalstore/ExternalStore.php
includes/externalstore/ExternalStoreDB.php
includes/externalstore/ExternalStoreMwstore.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignDBFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/LocalFileMoveBatch.php
includes/filerepo/file/OldLocalFile.php
includes/filerepo/file/UnregisteredLocalFile.php
includes/gallery/TraditionalImageGallery.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLSelectAndOtherField.php
includes/import/ImportableUploadRevisionImporter.php
includes/import/WikiImporter.php
includes/installer/CliInstaller.php
includes/installer/DatabaseUpdater.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/cs.json
includes/installer/i18n/en.json
includes/installer/i18n/fr.json
includes/installer/i18n/ia.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/uk.json
includes/jobqueue/JobSpecification.php
includes/jobqueue/jobs/ThumbnailRenderJob.php
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/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/database/IMaintainableDatabase.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/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/services/CannotReplaceActiveServiceException.php
includes/libs/services/ContainerDisabledException.php
includes/libs/services/NoSuchServiceException.php
includes/libs/services/ServiceAlreadyDefinedException.php
includes/libs/services/ServiceContainer.php
includes/libs/services/ServiceDisabledException.php
includes/libs/stats/BufferingStatsdDataFactory.php
includes/logging/DeleteLogFormatter.php
includes/logging/LogPage.php
includes/logging/ManualLogEntry.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/CoreParserFunctions.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/Preprocessor_DOM.php
includes/parser/Sanitizer.php
includes/password/LayeredParameterizedPassword.php
includes/rcfeed/RedisPubSubFeedEngine.php
includes/registration/ExtensionJsonValidator.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/resourceloader/ResourceLoaderModule.php
includes/resourceloader/ResourceLoaderOOUIIconPackModule.php [new file with mode: 0644]
includes/resourceloader/ResourceLoaderOOUIImageModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/revisiondelete/RevDelFileList.php
includes/search/SearchEngine.php
includes/search/SearchHighlighter.php
includes/search/SearchNearMatcher.php
includes/search/SearchResult.php
includes/search/SearchResultSet.php
includes/search/SqlSearchResultSet.php
includes/session/SessionBackend.php
includes/shell/Command.php
includes/skins/SkinTemplate.php
includes/specialpage/SpecialPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialEmailUser.php
includes/specials/SpecialExport.php
includes/specials/SpecialFileDuplicateSearch.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialRedirect.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUpload.php
includes/specials/SpecialUploadStash.php
includes/specials/SpecialUserLogout.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialVersion.php
includes/specials/SpecialWantedfiles.php
includes/specials/pagers/ImageListPager.php
includes/tidy/RemexCompatMunger.php
includes/tidy/RemexMungerData.php
includes/upload/UploadBase.php
includes/user/User.php
includes/watcheditem/WatchedItemStore.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/classes/LanguageZh.php
languages/i18n/aeb-arab.json
languages/i18n/an.json
languages/i18n/ar.json
languages/i18n/arz.json
languages/i18n/ast.json
languages/i18n/awa.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/co.json
languages/i18n/crh-cyrl.json
languages/i18n/crh-latn.json
languages/i18n/cs.json
languages/i18n/cv.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/exif/bg.json
languages/i18n/exif/mk.json
languages/i18n/exif/qqq.json
languages/i18n/exif/sdc.json [new file with mode: 0644]
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/fy.json
languages/i18n/gl.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hyw.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/ie.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/mk.json
languages/i18n/ml.json
languages/i18n/my.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/nqo.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/sdc.json
languages/i18n/sh.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sv.json
languages/i18n/th.json
languages/i18n/uk.json
languages/i18n/vec.json
languages/i18n/vi.json
languages/i18n/wo.json
languages/i18n/yo.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesAz.php
maintenance/Maintenance.php
maintenance/cleanupPreferences.php
maintenance/deduplicateArchiveRevId.php
maintenance/deleteBatch.php
maintenance/dumpUploads.php
maintenance/eraseArchivedFile.php
maintenance/generateSitemap.php
maintenance/importImages.php
maintenance/importTextFiles.php
maintenance/includes/DeleteLocalPasswords.php
maintenance/migrateArchiveText.php
maintenance/populateArchiveRevId.php
maintenance/populateCategory.php
maintenance/populateImageSha1.php
maintenance/populateInterwiki.php
maintenance/populateIpChanges.php
maintenance/rebuildImages.php
mw-config/config.css
mw-config/config.js
package-lock.json [new file with mode: 0644]
package.json
resources/Resources.php
resources/lib/foreign-resources.yaml
resources/lib/jquery.chosen/README.md
resources/lib/jquery.ui/jquery.ui.effect-bounce.js [deleted file]
resources/lib/jquery.ui/jquery.ui.effect-explode.js [deleted file]
resources/lib/jquery.ui/jquery.ui.effect-fold.js [deleted file]
resources/lib/jquery.ui/jquery.ui.effect-pulsate.js [deleted file]
resources/lib/jquery.ui/jquery.ui.effect-slide.js [deleted file]
resources/lib/jquery.ui/jquery.ui.effect-transfer.js [deleted file]
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.makeCollapsible.styles.less
resources/src/jquery/jquery.suggestions.js
resources/src/jquery/jquery.textSelection.js
resources/src/mediawiki.Title/Title.js
resources/src/mediawiki.Uri/Uri.js
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.page.ready.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less
resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.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/data/upload/jpeg-a-href-in-metadata.jpg [new file with mode: 0644]
tests/phpunit/data/upload/png-embedded-breaks-ie5.png [new file with mode: 0644]
tests/phpunit/data/upload/png-plain.png [new file with mode: 0644]
tests/phpunit/includes/ActorMigrationTest.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/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionStoreTest.php
tests/phpunit/includes/StatusTest.php
tests/phpunit/includes/Storage/NameTableStoreTest.php
tests/phpunit/includes/TestLogger.php
tests/phpunit/includes/TitleArrayFromResultTest.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/LoadBalancerTest.php
tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php [new file with mode: 0644]
tests/phpunit/includes/import/ImportTest.php
tests/phpunit/includes/jobqueue/JobQueueTest.php
tests/phpunit/includes/libs/HashRingTest.php
tests/phpunit/includes/libs/IPTest.php
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/logging/DeleteLogFormatterTest.php
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
tests/phpunit/includes/parser/PreprocessorTest.php
tests/phpunit/includes/password/PasswordFactoryTest.php [deleted file]
tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
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/ResourceLoaderTest.php
tests/phpunit/includes/session/SessionBackendTest.php
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/includes/upload/UploadBaseTest.php
tests/phpunit/includes/user/PasswordResetTest.php
tests/phpunit/includes/user/UserArrayFromResultTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/utils/BatchRowUpdateTest.php
tests/phpunit/includes/utils/UIDGeneratorTest.php
tests/phpunit/maintenance/categoriesRdfTest.php
tests/phpunit/structure/ApiPrefixUniquenessTest.php
tests/phpunit/structure/ApiStructureTest.php
tests/phpunit/structure/AutoLoaderStructureTest.php
tests/phpunit/structure/AvailableRightsTest.php
tests/phpunit/structure/ContentHandlerSanityTest.php
tests/phpunit/structure/DatabaseIntegrationTest.php
tests/phpunit/structure/ExtensionJsonValidationTest.php
tests/phpunit/structure/PasswordPolicyStructureTest.php
tests/phpunit/structure/ResourcesTest.php
tests/phpunit/structure/SpecialPageFatalTest.php
tests/phpunit/structure/StructureTest.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/generateJqueryMsgData.php
tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
tests/selenium/specs/rollback.js
thumb.php

index 22b74b5..9ccf565 100644 (file)
                -->
                <exclude-pattern>*/maintenance/mwdocgen\.php</exclude-pattern>
        </rule>
+       <rule ref="MediaWiki.Commenting.MissingCovers.MissingCovers">
+               <exclude-pattern>tests/phpunit/structure/*</exclude-pattern>
+       </rule>
        <file>.</file>
        <arg name="encoding" value="UTF-8"/>
        <arg name="extensions" value="php,php5,inc,sample"/>
index e4a173d..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
 
@@ -58,14 +50,15 @@ addons:
     - tidy
 
 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
diff --git a/RELEASE-NOTES-1.33 b/RELEASE-NOTES-1.33
deleted file mode 100644 (file)
index f9826e2..0000000
+++ /dev/null
@@ -1,493 +0,0 @@
-= MediaWiki 1.33 =
-
-== MediaWiki 1.33.0-PRERELEASE ==
-
-THIS IS NOT A RELEASE YET
-
-MediaWiki 1.33 is a pre-release testing branch, and is not recommended for use
-in production.
-
-== Upgrading notes for 1.33 ==
-1.33 has several database changes since 1.32, and will not work without schema
-updates. Note that due to changes to some very large tables like the revision
-table, the schema update may take quite long (minutes on a medium sized site,
-many hours on a large site).
-
-Don't forget to always back up your database before upgrading!
-
-See the file UPGRADE for more detailed upgrade instructions, including
-important information when upgrading from versions prior to 1.11.
-
-Some specific notes for MediaWiki 1.33 upgrades are below:
-
-* Some external link searches will not work correctly until update.php (or
-  refreshExternallinksIndex.php) is run. These include searches for links using
-  IP addresses, internationalized domain names, and possibly mailto links.
-* If you ran migrateActors.php using an older version of MediaWiki and want to
-  run your wiki with $wgActorTableSchemaMigrationStage SCHEMA_COMPAT_READ_OLD,
-  note that log_search rows needed to find revision deletions by target user
-  were incorrectly deleted. See T215464 for details.
-* If revision deletions were performed when the wiki was configured with
-  $wgActorTableSchemaMigrationStage SCHEMA_COMPAT_WRITE_BOTH and without
-  migrateActors.php having been run, the log_search table may contain rows with
-  empty values for "target_author_actor" which will prevent log searches for
-  revision deletions by target user from finding those log entries. These rows
-  may be corrected by (re-)running migrateActors.php.
-
-For notes on 1.32.x and older releases, see HISTORY.
-
-=== Configuration changes for system administrators in 1.33 ===
-
-==== New configuration ====
-* $wgEnablePartialBlocks – This enables the Partial Blocks feature, which gives
-  accounts with block permissions the ability to block users, IPs, and IP ranges
-  from editing specific pages, while allowing them to edit the rest of the wiki.
-  It is a temporary setting for gradual enablement, current default to `false`,
-  and will be set to `true` and then removed once initial development completes.
-
-==== Changed configuration ====
-* $wgChangeTagsSchemaMigrationStage (T193868) — This temporary setting, added in
-  MediaWiki 1.32, now defaults to MIGRATION_NEW instead of MIGRATION_WRITE_BOTH.
-* $wgPasswordPolicy – There is a new password policy to check that the account's
-  password is not in the large blacklist. This is enabled by default for the
-  built-in user groups bureaucrat, sysop, interface-admin, and bot. To configure
-  this for other user groups, set the `PasswordNotInLargeBlacklist` flag `true`.
-* wgPasswordDefault – There is a new password type configuration using Argon2
-  password hashing (which requires PHP 7.2 and above). It's designed to resist
-  timing attacks, and (on systems with PHP 7.3+) GPU hacking; if you configure
-  argon2 to be used, by default, it will automatically choose the best available
-  algorithm depending on which version of PHP you have available. To use this,
-  you can set `$wgPasswordDefault = 'argon2';`.
-* $wgActorTableSchemaMigrationStage now defaults to reading the new schema.
-  update.php will back-populate the new database fields due to the changed
-  setting, which may take some time on large wikis. You can avoid downtime by
-  following a process like that described in T188327.
-
-==== Removed configuration ====
-* $wgTagStatisticsNewTable (T199334) — This temporary setting, added in
-  MediaWiki 1.32, has now been removed. When loading Special:Tags, MediaWiki
-  will now always use the `change_tag_def` instead of the `change_tag` table.
-* $wgUseTidy, $wgTidyBin, $wgTidyConf, $wgTidyOpts, $wgTidyInternal, and
-  $wgDebugTidy – These options, all deprecated since 1.26, have now all been
-  removed, as MediaWiki now always tidies user output. The $wgTidyConfig setting
-  remains only for experimental features and debugging, and should not be used.
-* $wgEnableParserCache – This setting has been deprecated since 1.26, has now
-  been removed. If you still desire to disable the parser cache, instead you can
-  set `$wgParserCacheType = CACHE_NONE;`.
-* $wgCommentTableSchemaMigrationStage – This temporary migration setting has now
-  been removed. Code finding it unset should treat it as being MIGRATION_NEW.
-* $wgAuth – This old setting, deprecated in 1.27, has been removed as part of
-  the removal of AuthPlugin.
-* $wgSitesCacheFile – This configuration was introduced in 1.25 with the intent
-  to allow sites to configure a file in which to cache the SiteStore database
-  table, but it was never used. SiteStore already caches its information by
-  default using BagOStuff (e.g. Memcached or APC).
-* $wgClockSkewFudge – This setting was used by User.php to let sites adjust by
-  how much MediaWiki would fudge when trying to minimize the chances of a
-  user.user_touched database update to the "current" timestamp being before the
-  value already there (e.g. due to clock skew between different servers). This
-  is no longer a problem, because the code now ensures the timestamp is always
-  higher than the previous one. The writes are guarded with CAS logic (check
-  and set), which prevents updates that would overlap.
-* $wgDBmysql5 (T196185) - This experimental setting, deprecated in 1.31, has
-  been removed.
-
-=== New user-facing features in 1.33 ===
-* (T96041) __EXPECTUNUSEDCATEGORY__ on a category page causes the category
-  to be hidden on Special:UnusedCategories.
-* (T210814) SVGs are now by default displayed in wiki language on image
-  pages.
-* Special:CreateAccount now warns the user if their chosen username has to be
-  normalized.
-* (T205040) Multilingual images are now be displayed in the current parse
-  language where available.
-* Special:ActiveUsers will no longer filter out users who became inactive since
-  the last time the active users query cache was updated.
-* (T215675) RecentChange and ManualLogEntry implement new Taggable interface.
-* (T215675) Added a hook, ManualLogEntryBeforePublish, to allow extensions
-  to modify (example: add tags) log entries.
-
-=== New developer features in 1.33 ===
-* The AuthManagerLoginAuthenticateAudit hook has a new parameter for
-  additional information about the authentication event.
-* TextContent::getText() was introduced as a replacement for
-  Content::getNativeData() for text-based content models.
-* (T214706) LinksUpdate::getAddedExternalLinks() and
-  LinksUpdate::getRemovedExternalLinks() were introduced.
-* (T213893) Added 'MaintenanceUpdateAddParams' hook
-* (T219655) The MarkPatrolled hook has a new parameter for the tags
-  associated with this entry in the patrol log.
-* (T212472) Extensions can now specify platform abilities they require to work,
-  limited to shell access for now.
-
-=== External library changes in 1.33 ===
-
-==== New external libraries ====
-* Added wikimedia/password-blacklist 0.1.4.
-* Added guzzlehttp/guzzle 6.3.3.
-* Added jakub-onderka/php-console-highlighter 0.3.2 explicitly (dev-only).
-
-==== Changed external libraries ====
-* Updated OOUI from v0.29.2 to v0.31.2.
-* Updated OOjs Router from pre-release to v0.2.0.
-* Updated moment from v2.19.3 to v2.24.0.
-* Updated wikimedia/xmp-reader from 0.6.0 to 0.6.2.
-* Updated wikimedia/scoped-callback from 2.0.0 to 3.0.0.
-* Updated wikimedia/ip-set from 1.2.0 to 2.0.1.
-  * The deprecated IPSet\IPSet alias was removed, Wikimedia\IPSet must be
-    used instead.
-* Updated qunitjs from 2.6.2 to 2.9.1.
-* Updated jquery-client from 2.0.1 to 2.0.2.
-* Updated psy/psysh from 0.9.6 to 0.9.9 (dev-only).
-* Updated nikic/php-parser from 3.1.3 to 3.1.5 (dev-only).
-* Updated pear/net_smtp from 1.8.0 to 1.8.1.
-* Updated cssjanus/cssjanus from 1.2.0 to 1.2.1.
-* Updated wikimedia/php-session-serializer from 1.0.6 to 1.0.7.
-
-==== Removed external libraries ====
-* (T219403) jquery.ui.spinner, deprecated since 1.31, was removed.
-
-=== Bug fixes in 1.33 ===
-* (T164211) Special:UserRights could sometimes fail with a
-  "conflict detected" error when there weren't any conflicts.
-* (T216029) Chrome redirects to Special:BadTitle after editing a section with
-  a non-Latin name on a page with non-Latin characters in title.
-
-=== Action API changes in 1.33 ===
-* (T198913) Added 'ApiOptions' hook.
-* The JSON formatversion=2 is no longer experimental.
-* Internal API errors (those with code beginning "internal_api_error") will
-  include the exception class name in a data field named "errorclass".
-  * Class names are not guaranteed to remain stable, and in particular database
-    exceptions will now include the "Wikimedia\Rdbms\" prefix in the class name.
-  * The code including an exception class name is deprecated. In the future,
-    all internal errors will use code "internal_api_error".
-* (T212356) When using action=delete on pages with many revisions, the module
-  may return a boolean-true 'scheduled' and no 'logid'. This signifies that the
-  deletion will be processed via the job queue.
-* action=setnotificationtimestamp will now update the watchlist asynchronously
-  if entirewatchlist is set, so updates may not be visible immediately
-* Block info will be added to "blocked" errors from more modules.
-* (T216245) Autoblocks will now be spread by action=edit and action=move.
-* action=query&meta=userinfo has a new uiprop, 'latestcontrib', that returns
-  the date of user's latest contribution.
-* (T25227) action=logout now requires to be posted and have a csrf token.
-
-=== Action API internal changes in 1.33 ===
-* A number of deprecated methods for API documentation, intended for overriding
-  by extensions, are no longer called by MediaWiki, and will emit deprecation
-  notices if your extension attempts to use them:
-  * ApiBase::getDescription() (deprecated in 1.25)
-  * ApiBase::getParamDescription() (deprecated in 1.25)
-  * ApiBase::getExamples() (deprecated in 1.25)
-  * ApiBase::getDescriptionMessage() (deprecated in 1.30)
-  Additionally, the  'APIGetDescription' and 'APIGetParamDescription' hooks have
-  been removed, as their only use was to let extensions override values returned
-  by getDescription() and getParamDescription(), respectively.
-* API error codes may only contain ASCII letters, numbers, underscore, and
-  hyphen. Methods such as ApiBase::dieWithError() and
-  ApiMessageTrait::setApiCode() will throw an InvalidArgumentException if
-  passed a bad code.
-* ApiBase::checkTitleUserPermissions() now takes an options array as its third
-  parameter. Passing a User object or null is deprecated.
-* The api-feature-usage log channel now has log context. The text message is
-  deprecated and will be removed in the future.
-
-=== Languages updated in 1.33 ===
-MediaWiki supports over 350 languages. Many localisations are updated regularly.
-Below only new and removed languages are listed, as well as changes to languages
-because of Phabricator reports.
-
-* (T203908) Added language support for Eastern Pwo (kjp).
-* (T213717) Fixed a translation error on Goan Konkani (gom-deva) translations
-  for NS_TEMPLATE.
-* (T212221) Added $digitTransformTable for Santali (sat).
-* (T216479) Added language support for Saisiyat (xsy).
-* (T219728) Added support for new Japanese era name "Reiwa"
-
-=== Breaking changes in 1.33 ===
-* The parameteter $lang in DifferenceEngine::setTextLanguage must be of type
-  Language. Other types are deprecated since 1.32.
-* Skin::doEditSectionLink requires type Language for the parameter $lang.
-  The parameters $tooltip and $lang are mandatory. Omitting the parameters is
-  deprecated since 1.32.
-* Language::truncate(), deprecated in 1.31, has been removed.
-* UtfNormal, deprecated in 1.25, was removed. Use UtfNormal\Validator directly
-  instead.
-* (T197179) In OOUI HTMLForm fields, the parameters 'notice', 'notice-messages',
-  and 'notice-message', which were deprecated in 1.32, were removed. Instead,
-  use 'help', 'help-message', and 'help-messages'.
-* (T197179) HTMLFormField::getNotices(), deprecated in 1.32, was removed.
-* The "Parsoid v1" compatibility mappings in ParsoidVirtualRESTService and
-  RestbaseVirtualRESTService, deprecated since 1.26, have been removed.
-  Use the RESTBase v1 or Parsoid v3 API instead.
-* ParserOptions defaults 'tidy' to true now, since the untidy modes of the
-  parser are being deprecated and ParserOptions::getCanonicalOverrides()
-  has always been true at any rate.
-* Support for disabling tidy and external tidy implementations has been removed.
-  This was deprecated in 1.32. The pure PHP Remex tidy implementation is now
-  used and no configuration is necessary.
-* A number of deprecated methods for API documentation, intended for overriding
-  by extensions, are no longer called by MediaWiki, and will emit deprecation
-  notices if your extension attempts to use them:
-  * ApiBase::getDescription() (deprecated in 1.25)
-  * ApiBase::getParamDescription() (deprecated in 1.25)
-  * ApiBase::getExamples() (deprecated in 1.25)
-  * ApiBase::getDescriptionMessage() (deprecated in 1.30)
-  Additionally, the  'APIGetDescription' and 'APIGetParamDescription' hooks have
-  been removed, as their only use was to let extensions override values returned
-  by getDescription() and getParamDescription(), respectively.
-* The authentication hooks 'AbortAutoAccount' 'AbortNewAccount', 'AbortLogin',
-  'LoginUserMigrated', 'UserCreateForm', and 'UserLoginForm', all deprecated by
-  the creation of AuthManager in 1.27, have been removed. This also means that
-  the FakeAuthTemplate and LoginForm classes are removed, that FakeAuthTemplate
-  is no longer passed into LoginSignupSpecialPage->getFieldDefinitions(), and
-  that LoginSignupSpecialPage->getBCFieldDefinitions() is removed.
-* The 'jquery.localize' module, deprecated in 1.32, has been removed. Instead,
-  use 'jquery.i18n'.
-* The hooks LanguageGetSpecialPageAliases and LanguageGetMagic, deprecated since
-  1.16, have now been removed. Instead, use $specialPageAliases or $magicWords
-  respectively in a $wgExtensionMessagesFiles file.
-* The following methods of the Preferences class, deprecated in 1.31, have been
-  removed:
-  * getSaveBlacklist()
-  * loadPreferenceValues()
-  * getOptionFromUser()
-  * profilePreferences()
-  * skinPreferences()
-  * filesPreferences()
-  * datetimePreferences()
-  * renderingPreferences()
-  * editingPreferences()
-  * rcPreferences()
-  * watchlistPreferences()
-  * searchPreferences()
-  * miscPreferences()
-  * generateSkinOptions()
-  * getDateOptions()
-  * getImageSizes()
-  * getThumbSizes()
-  * validateSignature()
-  * cleanSignature()
-  * getTimezoneOptions()
-  * filterIntval()
-  * filterTimezoneInput()
-  * getTimeZoneList()
-* mw.util.jsMessage(), deprecated in 1.20, was removed. Use mw.notify instead.
-* (T61113) User::EDIT_TOKEN_SUFFIX was removed. It was deprecated since 1.27.
-* The 'mediawiki.api' module aliases, deprecated in 1.32, have been removed.
-  Specifically: mediawiki.api.category, mediawiki.api.edit,
-  mediawiki.api.login, mediawiki.api.options, mediawiki.api.parse,
-  mediawiki.api.upload, mediawiki.api.user, mediawiki.api.watch,
-  mediawiki.api.messages, and mediawiki.api.rollback.
-* The 'jquery.byteLimit' module alias for 'jquery.lengthLimit',
-  deprecated in 1.31, was removed.
-* Revision::fetchRevision(), deprecated in 1.28, was removed.
-* Class SquidUpdate, deprecated in 1.27, was removed.
-* Title->getSquidURLs(), deprecated in 1.27, was removed. Instead, use
-  Title->getCdnUrls().
-* Title::escapeFragmentForURL(), deprecated in 1.30, was removed. Use
-  Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki() instead.
-* Title->canTalk(), deprecated in 1.30, was removed. Instead, use
-  Title->canHaveTalkPage().
-* Title's methods for site and user page related to CSS and JS, deprecated in
-  1.31, were removed:
-  * Title->isCssOrJsPage() — Use Title->isSiteConfigPage()
-  * Title->isCssJsSubpage() – Use Title->isUserConfigPage()
-  * Title->getSkinFromCssJsSubpage() – Use Title->getSkinFromConfigSubpage()
-  * Title->isCssSubpage() – Use Title->isUserCssConfigPage()
-  * Title->isJsSubpage() – Use Title->isUserJsConfigPage()
-* SiteSQLStore, deprecated in 1.27 and whose only method, ::newInstance(),
-  would return the global SiteStore instance, has been removed. You can get to
-  this via MediaWiki\MediaWikiServices::getInstance()->getSiteStore() directly.
-* Linker::formatSize, deprecated in 1.28, has been removed (with DummyLinker's).
-  Instead, use Language->formatSize() with the relevant Language object.
-* Linker::formatTemplates, deprecated in 1.28, has been removed (along with the
-  version in DummyLinker). You can use TemplatesOnThisPageFormatter directly.
-* EventRelayerGroup::singleton(), deprecated in 1.27, has been removed. You can
-  use MediaWikiServices::getInstance()->getEventRelayerGroup() directly.
-* LinkCache->addLink(), deprecated in 1.27, has been removed. It is thought to
-  be unused, and is distinct from OutputPage->addLink(), which remains.
-* JsonContent->getJsonData(), deprecated in 1.25, has been removed. Instead, use
-  JsonContent->getData().
-* MWExceptionHandler::getLogId(), deprecated in 1.27, has been removed, as the
-  exception ID is the same as the request ID, from WebRequest::getRequestId().
-* SearchEngine::getNearMatchResultSet(), deprecated in 1.27, has been removed.
-  You can use SearchEngine::getNearMatcher() instead.
-* EmailNotification::updateWatchlistTimestamp, deprecated in 1.27, has been
-  removed. Instead, use WatchedItemStore::updateNotificationTimestamp directly.
-* User::getGroupName() and ::getGroupMember(), both deprecated in 1.29, have
-  been removed. Instead, please use UserGroupMembership::getGroupName() and
-  UserGroupMembership::getGroupMemberName().
-* Backwards compatibility for setting wgSessionsInObjectCache to false or using
-  wgSessionHandler, both of which were deprecated in 1.27 with the introduction
-  of SessionManager, has been removed.
-* SessionManager::autoCreateUser, deprecated in 1.27, has been removed. Use
-  MediaWiki\Auth\AuthManager::autoCreateUser instead.
-* The mw.libs.jpegmeta property, deprecated in 1.31, was removed.
-  Use require( 'mediawiki.libs.jpegmeta' ) instead.
-* The mw.user.stickyRandomId() method, deprecated in 1.32, was removed.
-  Use mw.user.getPageviewToken() instead.
-* Removed deprecated class property WikiRevision::$importer.
-* ResourceLoaderFileModule::readStyleFiles() now requires its $context
-  parameter.
-* The ChangeList::insertArticleLink() method, that was deprecated in 1.27, has
-  been removed.
-* MessageBlobStore::__construct() now requires its $rl parameter.
-* Second parameter to Sanitizer::escapeIdReferenceList() (deprecated in 1.31)
-  has been removed.
-* The 'jquery.xmldom' module has been removed.
-* The 'jquery.mockjax' module has been removed.
-* The 'jquery.hidpi' module, deprecated in 1.32, has been removed.
-* AuthPlugin and related code, deprecated in 1.27, has been removed. Extensions
-  should instead use AuthManager. The following no longer exist:
-  * The AuthPlugin class itself and the related AuthPluginUser class and i18n
-  * The AuthPluginSetup and AuthPluginAutoCreate hooks
-  * The transitional wrapper classes AuthPluginPrimaryAuthenticationProvider,
-    AuthManagerAuthPlugin, and AuthManagerAuthPluginUser.
-  * The $wgAuth configuration setting and its use in Setup.php and unit tests
-* (T217772) The 'wgAvailableSkins' mw.config key in JavaScript, was removed.
-* Language::markNoConversion, deprecated in 1.32, has been removed. Use
-  LanguageConverter::markNoConversion instead.
-* BagOStuff::modifySimpleRelayEvent() method has been removed.
-* ParserOutput::getLegacyOptions, deprecated in 1.30, has been removed.
-  Use ParserOutput::allCacheVaryingOptions instead.
-* CdnCacheUpdate::newSimplePurge, deprecated in 1.27, has been removed.
-  Use CdnCacheUpdate::newFromTitles() instead.
-* Handling of multiple arguments by the Block constructor, deprecated in 1.26,
-  has been removed.
-* The translation of main page in Sardinian (sc) was changed from "Pàgina Base"
-  to "Pàgina printzipale". Existing wikis using this content language need to
-  move the main page or change the name through MediaWiki:Mainpage page.
-* wfSplitWikiID(), deprecated in 1.32, has been removed.
-* MessageBlobStore::getBlob(), deprecated in 1.27, has been removed.
-  Use ::getBlobs() instead.
-* The .background-size() LESS mixin, deprecated in 1.27, has been removed.
-* ReadOnlyMode::clearCache() and ConfiguredReadOnlyMode::clearCache() have been
-  removed. Use MediaWikiTestCase::overrideMwServices() instead.
-
-=== Deprecations in 1.33 ===
-* The configuration option $wgUseESI has been deprecated, and is expected
-  to be removed in a future release.
-* The configuration option $wgSquidPurgeUseHostHeader has been deprecated,
-  and is expected to be removed in a future release.
-* The configuration options $wgFixArabicUnicode and $wgFixMalayalamUnicode,
-  introduced in MW 1.17, have been deprecated.  These fixes will always be
-  applied for Arabic and Malayalam in the future.  Please enable these on
-  your local wiki (if you have them explicitly set to false) and run
-  maintenance/cleanupTitles.php to fix any existing page titles.
-* The LegacyHookPreAuthenticationProvider class, deprecated since its creation
-  in 1.27 as part of the AuthManager re-write, now emits deprecation warnings.
-  This will help identify the issue if you added it to $wgAuthManagerConfig.
-* wfSplitWikiId() is now deprecated. Cache key generation should have the wiki
-  domain ID as a key component and use makeGlobalKey().
-* (T202094) Title::getUserCaseDBKey() is deprecated; instead, please use
-  Title::getDBKey(), which doesn't vary case.
-* User::getPasswordValidity() is now deprecated. User::checkPasswordValidity()
-  returns the same information in a more useful format.
-* For Linker::generateTOC() and Linker::tocList(), passing strings or booleans
-  as the $lang parameter was deprecated. The same applies to DummyLinker.
-* The PasswordPolicy 'PasswordCannotBePopular' has been deprecated. To
-  follow best practices, it is reccommended to use 'PasswordNotInLargeBlacklist'
-  instead which blacklists 100,000 commonly used passwords.
-* (T208862) Action::requiresUnblock() is now called from
-  Title::getUserPermissionsErrors() and Title::userCan(). Previously, the method
-  was only called in Action::checkCanExecute(). Actions should ensure that their
-  requiresUnblock() returns the proper result (the default is `true`).
-* (T211608) The MediaWiki\Services namespace has been renamed to
-  Wikimedia\Services. The old name is still supported, but deprecated.
-* (T155582) Content::getNativeData has been deprecated. Please use model-
-  specific getters, such as TextContent::getText().
-* The class WebInstallerOutput is now marked as @private.
-* (T209699) The jquery.async module has been deprecated. JavaScript code that
-  needs asynchronous behaviour should use Promises.
-* Password::equals() is deprecated, use verify().
-* BaseTemplate::msgWiki() and QuickTemplate::msgWiki() will be removed. Use
-  other means to fetch a properly escaped message string or Message object.
-* (T126091) The 'ResourceLoaderTestModules' hook, which lets you declare QUnit
-  testing code for your JavaScript modules, is deprecated. Instead, you can now
-  use the new extension registration key 'QUnitTestModule'.
-* (T213426) The jquery.throttle-debounce module has been deprecated. JavaScript
-  code that needs this behaviour should use OO.ui.debounce/throttle.
-* The mw.language.specialCharacters property from the
-  'mediawiki.language.specialCharacters' module has been deprecated.
-  Use require( 'mediawiki.language.specialCharacters' ) instead.
-* ChangeTags::purgeTagUsageCache() has been deprecated, and is expected to be
-  removed in a future release.
-* Passing a User object or null as the third parameter to
-  ApiBase::checkTitleUserPermissions() has been deprecated. Pass an array
-  [ 'user' => $user ] instead.
-* (T211578) Block::prevents is deprecated. Use Block::isEmailBlocked,
-  Block::isCreateAccountBlocked and Block::isUsertalkEditAllowed to get and set
-  block properties; use Block::appliesToRight and Block::appliesToUsertalk to
-  check block behaviour.
-* The api-feature-usage log channel now has log context. The text message is
-  deprecated and will be removed in the future.
-* The FileBasedSiteLookup class has been deprecated. For a cacheable SiteLookup
-  implementation, use CachingSiteStore instead.
-* Language::viewPrevNext function is deprecated, use
-  SpecialPage::buildPrevNextNavigation instead
-* ManualLogEntry::setTags() is deprecated, use ManualLogEntry::addTags()
-  instead. The setTags() method was overriding the tags, addTags() doesn't
-  override, only adds new tags.
-* Block::isValid is deprecated, since it is no longer needed in core.
-* Calling Maintenance::hasArg() as well as Maintenance::getArg() with no
-  parameter has been deprecated. Please pass the argument number 0.
-* ResourceLoaderContext::expandModuleNames has been deprecated.
-  Use ResourceLoader::expandModuleNames instead.
-
-=== Other changes in 1.33 ===
-* (T201747) Html::openElement() warns if given an element name with a space
-  in it.
-* The implementation of buildStringCast() in Wikimedia\Rdbms\Database has
-  changed to explicitly cast. Subclasses relying on the base-class
-  implementation should check whether they need to override it now.
-* BagOStuff::add is now abstract and must explicitly be defined in subclasses.
-* LinksDeletionUpdate is now a subclass of LinksUpdate. As a consequence,
-  the following hooks will now be triggered upon page deletion in addition
-  to page updates: LinksUpdateConstructed, LinksUpdate, LinksUpdateComplete.
-  LinksUpdateAfterInsert is not triggered since deletions do not cause
-  insertions into links tables.
-* Category::newFromID( $id )->getID() will now return $id without any
-  validation, to avoid a mostly unnecessary DB query.
-* On Special:Version, the name for an extension can no longer be arbitrary
-  html when no link is specified.
-
-== Compatibility ==
-MediaWiki 1.33 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
-supported, it is generally advised to use PHP 7.0.13 or later for long term
-support.
-
-MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
-but support for them is somewhat less mature. There is experimental support for
-Oracle and Microsoft SQL Server.
-
-The supported versions are:
-
-* MySQL 5.5.8 or later
-* PostgreSQL 9.2 or later
-* SQLite 3.8.0 or later
-* Oracle 9.0.1 or later
-* Microsoft SQL Server 2005 (9.00.1399)
-
-== Online documentation ==
-Documentation for both end-users and site administrators is available on
-MediaWiki.org, and is covered under the GNU Free Documentation License (except
-for pages that explicitly state that their contents are in the public domain):
-
-       https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation
-
-== Mailing list ==
-A mailing list is available for MediaWiki user support and discussion:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
-
-A low-traffic announcements-only list is also available:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
-
-It's highly recommended that you sign up for one of these lists if you're
-going to run a public MediaWiki, so you can be notified of security fixes.
-
-== IRC help ==
-There's usually someone online in #mediawiki on irc.freenode.net.
index dca64bd..819c202 100644 (file)
@@ -42,6 +42,13 @@ For notes on 1.33.x and older releases, see HISTORY.
   variable $wgCdnMaxageLagged. The previous configuration variable names are
   deprecated, but will be used as the fall back if they are still set.
   Note that wgSquidPurgeUseHostHeader has not been renamed, as it is deprecated.
+* (T27707) File type checks for image uploads have been relaxed to allow files
+  containing some HTML markup in metadata. As a result, the $wgAllowTitlesInSVG
+  setting is no longer applied and is now always true. Note that MSIE 7 may
+  still be able to misinterpret certain malformed PNG files as HTML.
+* Introduced $wgVerifyMimeTypeIE to allow disabling the MSIE 6/7 file type
+  detection heuristic on upload, which is more conservative than the checks
+  that were changed above.
 * …
 
 ==== Removed configuration ====
@@ -193,6 +200,17 @@ because of Phabricator reports.
   directly.
 * HTMLForm::getErrors(), deprecated in 1.28, has been removed. Use
   getErrorsOrWarnings() instead.
+* SpecialPage::getTitle(), deprecated in 1.23, has been removed. Use
+  SpecialPage::getPageTitle() instead.
+* 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.
 * …
 
 === Deprecations in 1.34 ===
@@ -241,6 +259,17 @@ because of Phabricator reports.
 * (T62260) Hard deprecate Language::getExtraUserToggles() method.
 * Language::viewPrevNext function is deprecated, use
   PrevNextNavigationRenderer::buildPrevNextNavigation instead
+* User::trackBlockWithCookie and DatabaseBlock::clearCookie are deprecated. Use
+  BlockManager::trackBlockWithCookie and BlockManager::clearCookie instead.
+* 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.
 
 === Other changes in 1.34 ===
 * …
index eb8ba09..b3900a8 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',
@@ -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',
@@ -1246,6 +1242,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderLessVarFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLessVarFileModule.php',
        'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
        'ResourceLoaderOOUIFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIFileModule.php',
+       'ResourceLoaderOOUIIconPackModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIIconPackModule.php',
        'ResourceLoaderOOUIImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIImageModule.php',
        'ResourceLoaderOOUIModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIModule.php',
        'ResourceLoaderSiteModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSiteModule.php',
@@ -1323,6 +1320,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 751da3f..7a90804 100644 (file)
@@ -32,6 +32,7 @@
                "pear/mail_mime": "1.10.2",
                "pear/net_smtp": "1.8.1",
                "php": ">=5.6.99",
+               "psr/container": "1.0.0",
                "psr/log": "1.0.2",
                "wikimedia/assert": "0.2.2",
                "wikimedia/at-ease": "2.0.0",
@@ -76,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 cf02f2b..86fa1b3 100644 (file)
                },
                "ParserTestFiles": {
                        "type": "array",
-                       "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
+                       "description": "DEPRECATED: Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
                "ServiceWiringFiles": {
                        "type": "array",
index f29f850..c1db2b6 100644 (file)
                },
                "ParserTestFiles": {
                        "type": "array",
-                       "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
+                       "description": "DEPRECATED: Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
                "ServiceWiringFiles": {
                        "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..b275adc 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.
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..57e4341 100644 (file)
@@ -136,6 +136,7 @@ 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/',
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 ab1afe2..2f793b5 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".
@@ -1214,17 +1221,12 @@ $wgSVGMaxSize = 5120;
 $wgSVGMetadataCutoff = 262144;
 
 /**
- * Disallow <title> element in SVG files.
- *
- * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic
- * browsers which can not perform basic stuff like MIME detection and which are
- * vulnerable to further idiots uploading crap files as images.
+ * Obsolete, no longer used.
+ * SVG file uploads now always allow <title> elements.
  *
- * When this directive is on, "<title>" will be allowed in files with an
- * "image/svg+xml" MIME type. You should leave this disabled if your web server
- * is misconfigured and doesn't send appropriate MIME types for SVG images.
+ * @deprecated 1.34
  */
-$wgAllowTitlesInSVG = false;
+$wgAllowTitlesInSVG = true;
 
 /**
  * Whether thumbnails should be generated in target language (usually, same as
@@ -1390,6 +1392,16 @@ $wgAntivirusRequired = true;
  */
 $wgVerifyMimeType = true;
 
+/**
+ * Determines whether extra checks for IE type detection should be applied.
+ * This is a conservative check for exactly what IE 6 or so checked for,
+ * and shouldn't trigger on for instance JPEG files containing links in EXIF
+ * metadata.
+ *
+ * @since 1.34
+ */
+$wgVerifyMimeTypeIE = true;
+
 /**
  * Sets the MIME type definition file to use by includes/libs/mime/MimeAnalyzer.php.
  * Set to null, to use built-in defaults only.
@@ -4141,6 +4153,9 @@ $wgInvalidRedirectTargets = [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect' ];
  *                    If this parameter is not given, it uses Preprocessor_DOM if the
  *                    DOM module is available, otherwise 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 --
  * the configuration will change at runtime via Parser member functions, so
@@ -5412,20 +5427,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**):
@@ -5436,25 +5451,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
  *
@@ -6408,7 +6423,7 @@ $wgDeprecationReleaseLimit = false;
  *
  * @code
  *   $wgProfiler['class'] = 'ProfilerXhprof';
- *   $wgProfiler['output'] = array( 'ProfilerOutputDb' );
+ *   $wgProfiler['output'] = [ 'ProfilerOutputDb' ];
  *   $wgProfiler['sampling'] = 50; // one every 50 requests
  * @endcode
  *
@@ -8081,10 +8096,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 4bf00d0..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,3 +53,7 @@ if ( $logDir ) {
        $wgDebugLogGroups['error'] = "$logDir/mw-error.log";
 }
 unset( $logDir );
+
+// Disable rate-limiting to allow integration tests to run unthrottled
+// in CI and for devs locally (T225796)
+$wgRateLimits = [];
index 0bcc893..d27ef9c 100644 (file)
@@ -622,7 +622,8 @@ class EditPage {
 
                        if ( $this->context->getUser()->getBlock() ) {
                                // track block with a cookie if it doesn't exists already
-                               $this->context->getUser()->trackBlockWithCookie();
+                               MediaWikiServices::getInstance()->getBlockManager()
+                                       ->trackBlockWithCookie( $this->context->getUser() );
 
                                // Auto-block user's IP if the account was "hard" blocked
                                if ( !wfReadOnly() ) {
@@ -2591,7 +2592,7 @@ ERROR;
                        }
                } elseif ( $namespace == NS_FILE ) {
                        # Show a hint to shared repo
-                       $file = wfFindFile( $this->mTitle );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
                        if ( $file && !$file->isLocal() ) {
                                $descUrl = $file->getDescriptionUrl();
                                # there must be a description url to show a hint to shared repo
@@ -4129,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 dddbad5..80a12fe 100644 (file)
@@ -19,6 +19,8 @@
  * @ingroup Maintenance
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Manage foreign resources registered with ResourceLoader.
  *
@@ -150,7 +152,7 @@ class ForeignResourceManager {
 
        /** @return string|false */
        private function cacheGet( $key ) {
-               return Wikimedia\quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
+               return AtEase::quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
        }
 
        private function cacheSet( $key, $data ) {
index 7256eab..759732f 100644 (file)
@@ -2984,7 +2984,7 @@ function wfUnpack( $format, $data, $length = false ) {
 function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
        # Handle redirects; callers almost always hit wfFindFile() anyway,
        # so just use that method because it has a fast process cache.
-       $file = wfFindFile( $name ); // get the final name
+       $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name ); // get the final name
        $name = $file ? $file->getTitle()->getDBkey() : $name;
 
        # Run the extension hook
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 ae50c66..39f4394 100644 (file)
@@ -555,7 +555,8 @@ class Linker {
                                # Use manually specified thumbnail
                                $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
                                if ( $manual_title ) {
-                                       $manual_img = wfFindFile( $manual_title );
+                                       $manual_img = MediaWikiServices::getInstance()->getRepoGroup()
+                                               ->findFile( $manual_title );
                                        if ( $manual_img ) {
                                                $thumb = $manual_img->getUnscaledThumb( $handlerParams );
                                                $manualthumb = true;
@@ -693,7 +694,8 @@ class Linker {
                        $label = $title->getPrefixedText();
                }
                $encLabel = htmlspecialchars( $label );
-               $currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
+               $currentExists = $time ? ( $file != false ) : false;
 
                if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
                        && !$currentExists
@@ -756,11 +758,13 @@ class Linker {
         * @since 1.16.3
         * @param LinkTarget $title
         * @param string $html Pre-sanitized HTML
-        * @param string $time MW timestamp of file creation time
+        * @param string|false $time MW timestamp of file creation time
         * @return string HTML
         */
        public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
-               $img = wfFindFile( $title, [ 'time' => $time ] );
+               $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
+                       $title, [ 'time' => $time ]
+               );
                return self::makeMediaLinkFile( $title, $img, $html );
        }
 
index 54b3ee5..57cd74a 100644 (file)
@@ -1723,7 +1723,7 @@ 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() {
@@ -2882,8 +2882,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 +2898,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 {
@@ -4214,9 +4221,7 @@ class OutputPage extends ContextSource {
                        'oojs-ui.styles.indicators',
                        'oojs-ui.styles.textures',
                        'mediawiki.widgets.styles',
-                       'oojs-ui.styles.icons-content',
-                       'oojs-ui.styles.icons-alerts',
-                       'oojs-ui.styles.icons-interactions',
+                       'oojs-ui-core.icons',
                ] );
        }
 
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 f4e85ad..3b0ab2b 100644 (file)
@@ -196,7 +196,7 @@ class Pingback {
                                        'updatelog',
                                        [ 'ul_key' => 'PingBack', 'ul_value' => $id ],
                                        __METHOD__,
-                                       'IGNORE'
+                                       [ 'IGNORE' ]
                                );
 
                                if ( !$dbw->affectedRows() ) {
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 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 29d7848..00c9d04 100644 (file)
@@ -1644,7 +1644,7 @@ class RevisionStore
                        throw new RevisionAccessException(
                                'Main slot of revision ' . $revId . ' not found in database!'
                        );
-               };
+               }
 
                return $slots;
        }
index c5764d2..e371b5a 100644 (file)
@@ -102,6 +102,7 @@ return [
                        $config->get( 'EnableDnsBlacklist' ),
                        $config->get( 'ProxyList' ),
                        $config->get( 'ProxyWhitelist' ),
+                       $config->get( 'SecretKey' ),
                        $config->get( 'SoftBlockRanges' )
                );
        },
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..0008ef7 100644 (file)
@@ -60,7 +60,7 @@ use SiteStatsUpdate;
 use Title;
 use User;
 use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\Rdbms\ILBFactory;
 use WikiPage;
 
 /**
@@ -132,7 +132,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
        private $messageCache;
 
        /**
-        * @var LBFactory
+        * @var ILBFactory
         */
        private $loadbalancerFactory;
 
@@ -268,7 +268,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 +279,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                JobQueueGroup $jobQueueGroup,
                MessageCache $messageCache,
                Language $contLang,
-               LBFactory $loadbalancerFactory
+               ILBFactory $loadbalancerFactory
        ) {
                $this->wikiPage = $wikiPage;
 
index 46f957f..9c2b3e7 100644 (file)
@@ -338,7 +338,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 +393,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 +402,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 +448,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 +474,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 0e95791..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
@@ -3584,7 +3584,8 @@ class Title implements LinkTarget, IDBAccessObject {
 
                # Is it an existing file?
                if ( $nt->getNamespace() == NS_FILE ) {
-                       $file = wfLocalFile( $nt );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $nt );
                        $file->load( File::READ_LATEST );
                        if ( $file->exists() ) {
                                wfDebug( __METHOD__ . ": file exists\n" );
@@ -4056,15 +4057,15 @@ class Title implements LinkTarget, IDBAccessObject {
                        return true; // any interwiki link might be viewable, for all we know
                }
 
+               $services = MediaWikiServices::getInstance();
                switch ( $this->mNamespace ) {
                        case NS_MEDIA:
                        case NS_FILE:
                                // file exists, possibly in a foreign repo
-                               return (bool)wfFindFile( $this );
+                               return (bool)$services->getRepoGroup()->findFile( $this );
                        case NS_SPECIAL:
                                // valid special page
-                               return MediaWikiServices::getInstance()->getSpecialPageFactory()->
-                                       exists( $this->mDbkeyform );
+                               return $services->getSpecialPageFactory()->exists( $this->mDbkeyform );
                        case NS_MAIN:
                                // selflink, possibly with fragment
                                return $this->mDbkeyform == '';
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 ab95b97..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() ),
@@ -450,7 +452,7 @@ class InfoAction extends FormlessAction {
 
                // Display image SHA-1 value
                if ( $title->inNamespace( NS_FILE ) ) {
-                       $fileObj = wfFindFile( $title );
+                       $fileObj = $services->getRepoGroup()->findFile( $title );
                        if ( $fileObj !== false ) {
                                // Convert the base-36 sha1 value obtained from database to base-16
                                $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
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 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 7045138..668bd0e 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
+
 /**
  * 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
@@ -56,7 +59,9 @@ class ApiImageRotate extends ApiBase {
                                }
                        }
 
-                       $file = wfFindFile( $title, [ 'latest' => true ] );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
+                               $title, [ 'latest' => true ]
+                       );
                        if ( !$file ) {
                                $r['result'] = 'Failure';
                                $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
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 89ecc43..540860b 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * API Module to move pages
  * @ingroup API
@@ -59,7 +61,7 @@ class ApiMove extends ApiBase {
 
                if ( $toTitle->getNamespace() == NS_FILE
                        && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle )
-                       && wfFindFile( $toTitle )
+                       && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $toTitle )
                ) {
                        if ( !$params['ignorewarnings'] && $user->isAllowed( 'reupload-shared' ) ) {
                                $this->dieWithError( 'apierror-fileexists-sharedrepo-perm' );
index f04ac66..b8672ee 100644 (file)
@@ -334,6 +334,12 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                                        $this->setContinue( $row, $sortby );
                                        break;
                                }
+
+                               if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) {
+                                       // Miser mode namespace check
+                                       continue;
+                               }
+
                                $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
                        }
                        $resultPageSet->populateFromTitles( $titles );
index 2505334..59ec4f6 100644 (file)
@@ -151,7 +151,8 @@ abstract class ApiQueryBase extends ApiBase {
 
        /**
         * Add a set of tables to the internal array
-        * @param string|string[] $tables Table name or array of table names
+        * @param string|array $tables Table name or array of table names
+        *  or nested arrays for joins using parentheses for grouping
         * @param string|null $alias Table alias, or null for no alias. Cannot be
         *  used with multiple tables
         */
@@ -251,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
         */
index 051e127..e123a2a 100644 (file)
@@ -110,7 +110,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                if ( !isset( $images[$title] ) ) {
                                        if ( isset( $prop['uploadwarning'] ) || isset( $prop['badfile'] ) ) {
                                                // uploadwarning and badfile need info about non-existing files
-                                               $images[$title] = wfLocalFile( $title );
+                                               $images[$title] = MediaWikiServices::getInstance()->getRepoGroup()
+                                                       ->getLocalRepo()->newFile( $title );
                                                // Doesn't exist, so set an empty image repository
                                                $info['imagerepository'] = '';
                                        } else {
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..d33b8b8 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-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 6c83ca2..e29c34a 100644 (file)
        "apihelp-query+langlinks-paramvalue-prop-url": "Aggiunge l'URL completo.",
        "apihelp-query+langlinks-paramvalue-prop-autonym": "Aggiunge il nome nativo della lingua.",
        "apihelp-query+langlinks-param-dir": "La direzione in cui elencare.",
+       "apihelp-query+languageinfo-summary": "Restituisce informazioni sulle lingue disponibili.",
        "apihelp-query+languageinfo-paramvalue-prop-bcp47": "Il codice lingua BCP-47.",
+       "apihelp-query+languageinfo-example-simple": "Ottieni i codici lingua di tutte le lingue supportate.",
        "apihelp-query+links-param-namespace": "Mostra collegamenti solo in questi namespace.",
        "apihelp-query+links-param-limit": "Quanti collegamenti restituire.",
        "apihelp-query+links-param-dir": "La direzione in cui elencare.",
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 f068ec0..e71a9dc 100644 (file)
        "apihelp-login-param-name": "Nazwa użytkownika.",
        "apihelp-login-param-password": "Hasło.",
        "apihelp-login-param-domain": "Domena (opcjonalnie).",
-       "apihelp-login-param-token": "Token logowania zdobyty w pierwszym zapytaniu.",
+       "apihelp-login-param-token": "Token logowania pobrany w pierwszym zapytaniu.",
        "apihelp-login-example-login": "Zaloguj się",
        "apihelp-logout-summary": "Wyloguj i wyczyść dane sesji.",
        "apihelp-logout-example-logout": "Wyloguj obecnego użytkownika.",
        "api-help-param-multi-all": "Aby wskazać wszystkie wartości, użyj <kbd>$1</kbd>.",
        "api-help-param-default": "Domyślnie: $1",
        "api-help-param-default-empty": "Domyślnie: <span class=\"apihelp-empty\">(puste)</span>",
-       "api-help-param-token": "Token \"$1\" zdobyty z [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]",
+       "api-help-param-token": "Token \"$1\" pobrany z [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]",
        "api-help-param-continue": "Gdy będzie dostępnych więcej wyników, użyj tego do kontynuowania.",
        "api-help-param-no-description": "<span class=\"apihelp-empty\">(bez opisu)</span>",
        "api-help-examples": "{{PLURAL:$1|Przykład|Przykłady}}:",
index 6800b91..1e0d508 100644 (file)
        "apihelp-query+langlinks-param-inlanguagecode": "Código de idioma para nomes de idiomas localizados.",
        "apihelp-query+langlinks-example-simple": "Obter links de interligação da página <kbd>Main Page</kbd>.",
        "apihelp-query+languageinfo-summary": "Retornar informações sobre os idiomas disponíveis.",
+       "apihelp-query+languageinfo-extended-description": "Pode ser aplicada uma [[mw:API:Query#Continuing queries|continuação]] se a obtenção das informações demorar muito tempo para um só pedido.",
+       "apihelp-query+languageinfo-param-prop": "Quais informações obter para cada idioma.",
+       "apihelp-query+languageinfo-paramvalue-prop-code": "O código do idioma (este código é específico do MediaWiki, embora tenha semelhanças com outros padrões).",
+       "apihelp-query+languageinfo-paramvalue-prop-bcp47": "O código do idioma BCP-47.",
+       "apihelp-query+languageinfo-paramvalue-prop-dir": "A direção de escrita do idioma (<code>ltr</code>, da esquerda para a direita, ou <code>rtl</code>, da direita para a esquerda).",
+       "apihelp-query+languageinfo-paramvalue-prop-autonym": "O autônimo do idioma, isto é, o seu nome nesse idioma.",
+       "apihelp-query+languageinfo-paramvalue-prop-name": "O nome do idioma no idioma especificado pelo parâmetro <var>lilang</var>, com a aplicação de idiomas de recurso se necessário.",
+       "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "Os códigos de idioma das idiomas de recurso configuradas para esta língua. O recurso final implícito para 'en' não é incluído (mas algum idiomas podem especificar 'en' como último recurso explicitamente).",
+       "apihelp-query+languageinfo-paramvalue-prop-variants": "Os códigos de idioma das variantes suportadas por esse idioma.",
+       "apihelp-query+languageinfo-param-code": "Códigos de idioma dos idiomas que devem ser devolvidas, ou <code>*</code> para todos os idiomas.",
+       "apihelp-query+languageinfo-example-simple": "Obter os códigos de idioma de todos os idiomas suportados.",
+       "apihelp-query+languageinfo-example-autonym-name-de": "Obter os autônimos e nomes em alemão de todos os idioma suportados.",
+       "apihelp-query+languageinfo-example-fallbacks-variants-oc": "Obter os idiomas de recurso e as variantes de occitânico.",
+       "apihelp-query+languageinfo-example-bcp47-dir": "Obter o código de língua BCP-47 e a direção de escrita de todas os idiomas suportados.",
        "apihelp-query+links-summary": "Retorna todos os links das páginas fornecidas.",
        "apihelp-query+links-param-namespace": "Mostre apenas links nesses espaços de nominais.",
        "apihelp-query+links-param-limit": "Quantos links retornar.",
index 8eb0d0f..f85840b 100644 (file)
        "apihelp-query+langlinks-param-dir": "A direção de listagem.",
        "apihelp-query+langlinks-param-inlanguagecode": "O código de língua para os nomes de língua localizados.",
        "apihelp-query+langlinks-example-simple": "Obter as hiperligações interlínguas da página <kbd>Main Page</kbd>.",
+       "apihelp-query+languageinfo-summary": "Devolver informações sobre as línguas disponíveis.",
+       "apihelp-query+languageinfo-extended-description": "Pode ser aplicada uma [[mw:API:Query#Continuing queries|continuação]] se a obtenção das informações demorar demasiado tempo para um só pedido.",
+       "apihelp-query+languageinfo-param-prop": "A informação a ser obtida para cada língua.",
+       "apihelp-query+languageinfo-paramvalue-prop-code": "O código da língua (este código é específico do MediaWiki, embora tenha semelhanças com outros padrões).",
+       "apihelp-query+languageinfo-paramvalue-prop-bcp47": "O código de língua BCP-47.",
+       "apihelp-query+languageinfo-paramvalue-prop-dir": "A direção de escrita da língua (<code>ltr</code>, da esquerda para a direita, ou <code>rtl</code>, da direita para a esquerda).",
+       "apihelp-query+languageinfo-paramvalue-prop-autonym": "O autónimo da língua, isto é, o seu nome nessa língua.",
+       "apihelp-query+languageinfo-paramvalue-prop-name": "O nome da língua na língua especificada pelo parâmetro <var>lilang</var>, com a aplicação de línguas de recurso se necessário.",
+       "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "Os códigos de língua das línguas de recurso configuradas para esta língua. O recurso final implícito para 'en' não é incluído (mas algumas línguas podem especificar 'en' como último recurso explicitamente).",
+       "apihelp-query+languageinfo-paramvalue-prop-variants": "Os códigos de língua das variantes suportadas por esta língua.",
+       "apihelp-query+languageinfo-param-code": "Códigos de língua das línguas que devem ser devolvidas, ou <code>*</code> para todas as línguas.",
+       "apihelp-query+languageinfo-example-simple": "Obter os códigos de língua de todas as línguas suportadas.",
+       "apihelp-query+languageinfo-example-autonym-name-de": "Obter os autónimos e nomes em alemão de todas as línguas suportadas.",
+       "apihelp-query+languageinfo-example-fallbacks-variants-oc": "Obter as línguas de recurso e as variantes de occitânico.",
+       "apihelp-query+languageinfo-example-bcp47-dir": "Obter o código de língua BCP-47 e a direção de escrita de todas as línguas suportadas.",
        "apihelp-query+links-summary": "Devolve todas as hiperligações das páginas indicadas.",
        "apihelp-query+links-param-namespace": "Mostrar apenas as hiperligações destes espaços nominais.",
        "apihelp-query+links-param-limit": "O número de hiperligações a serem devolvidas.",
index fb49dfc..c7ba68d 100644 (file)
@@ -655,10 +655,13 @@ abstract class AbstractBlock {
         * Check if the block should be tracked with a cookie.
         *
         * @since 1.33
+        * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead
+        *  of calling this directly.
         * @param bool $isAnon The user is logged out
         * @return bool The block should be tracked with a cookie
         */
        public function shouldTrackWithCookie( $isAnon ) {
+               wfDeprecated( __METHOD__, '1.34' );
                return false;
        }
 
index 8d2fe0c..41ff893 100644 (file)
 
 namespace MediaWiki\Block;
 
+use DateTime;
 use IP;
 use MediaWiki\User\UserIdentity;
+use MWCryptHash;
 use User;
 use WebRequest;
+use WebResponse;
 use Wikimedia\IPSet;
 
 /**
@@ -61,6 +64,9 @@ class BlockManager {
        /** @var array */
        private $proxyWhitelist;
 
+       /** @var string|bool */
+       private $secretKey;
+
        /** @var array */
        private $softBlockRanges;
 
@@ -74,6 +80,7 @@ class BlockManager {
         * @param bool $enableDnsBlacklist
         * @param array $proxyList
         * @param array $proxyWhitelist
+        * @param string $secretKey
         * @param array $softBlockRanges
         */
        public function __construct(
@@ -86,6 +93,7 @@ class BlockManager {
                $enableDnsBlacklist,
                $proxyList,
                $proxyWhitelist,
+               $secretKey,
                $softBlockRanges
        ) {
                $this->currentUser = $currentUser;
@@ -97,11 +105,14 @@ class BlockManager {
                $this->enableDnsBlacklist = $enableDnsBlacklist;
                $this->proxyList = $proxyList;
                $this->proxyWhitelist = $proxyWhitelist;
+               $this->secretKey = $secretKey;
                $this->softBlockRanges = $softBlockRanges;
        }
 
        /**
-        * 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
         *
@@ -134,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,
@@ -166,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 )
                ) {
@@ -176,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(),
@@ -199,7 +202,46 @@ 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 blocks where each block either has a
+        * unique ID or has ID null.
+        *
+        * @param AbstractBlock[] $blocks
+        * @return AbstractBlock[]
+        */
+       private function getUniqueBlocks( $blocks ) {
+               $blockIds = [];
+               $uniqueBlocks = [];
+               foreach ( $blocks as $block ) {
+                       $id = $block->getId();
+                       if ( $id === null ) {
+                               $uniqueBlocks[] = $block;
+                       } elseif ( !isset( $blockIds[$id] ) ) {
+                               $uniqueBlocks[] = $block;
+                               $blockIds[$block->getId()] = true;
+                       }
+               }
+               return $uniqueBlocks;
        }
 
        /**
@@ -221,8 +263,7 @@ class BlockManager {
                        return false;
                }
                // Load the block from the ID in the cookie.
-               // TODO: remove dependency on DatabaseBlock
-               $blockCookieId = DatabaseBlock::getIdFromCookieValue( $blockCookieVal );
+               $blockCookieId = $this->getIdFromCookieValue( $blockCookieVal );
                if ( $blockCookieId !== null ) {
                        // An ID was found in the cookie.
                        // TODO: remove dependency on DatabaseBlock
@@ -248,15 +289,9 @@ class BlockManager {
                                        // Use the block.
                                        return $tmpBlock;
                                }
-
-                               // If the block is not valid, remove the cookie.
-                               // TODO: remove dependency on DatabaseBlock
-                               DatabaseBlock::clearCookie( $response );
-                       } else {
-                               // If the block doesn't exist, remove the cookie.
-                               // TODO: remove dependency on DatabaseBlock
-                               DatabaseBlock::clearCookie( $response );
                        }
+                       // If the block is invalid or doesn't exist, remove the cookie.
+                       $this->clearBlockCookie( $response );
                }
                return false;
        }
@@ -382,4 +417,141 @@ class BlockManager {
                return gethostbynamel( $hostname );
        }
 
+       /**
+        * Set the 'BlockID' cookie depending on block type and user authentication status.
+        *
+        * @since 1.34
+        * @param User $user
+        */
+       public function trackBlockWithCookie( User $user ) {
+               $block = $user->getBlock();
+               $request = $user->getRequest();
+               $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 );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be
+        * the same as the block's, to a maximum of 24 hours.
+        *
+        * @since 1.34
+        * @internal Should be private.
+        *  Left public for backwards compatibility, until DatabaseBlock::setCookie is removed.
+        * @param DatabaseBlock $block
+        * @param WebResponse $response The response on which to set the cookie.
+        */
+       public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
+               // Calculate the default expiry time.
+               $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
+
+               // Use the block's expiry time only if it's less than the default.
+               $expiryTime = $block->getExpiry();
+               if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
+                       $expiryTime = $maxExpiryTime;
+               }
+
+               // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
+               $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' );
+               $cookieOptions = [ 'httpOnly' => false ];
+               $cookieValue = $this->getCookieValue( $block );
+               $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
+       }
+
+       /**
+        * Check if the block should be tracked with a cookie.
+        *
+        * @param AbstractBlock $block
+        * @param bool $isAnon The user is logged out
+        * @return bool The block sould be tracked with a cookie
+        */
+       private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
+               if ( $block instanceof DatabaseBlock ) {
+                       switch ( $block->getType() ) {
+                               case DatabaseBlock::TYPE_IP:
+                               case DatabaseBlock::TYPE_RANGE:
+                                       return $isAnon && $this->cookieSetOnIpBlock;
+                               case DatabaseBlock::TYPE_USER:
+                                       return !$isAnon && $this->cookieSetOnAutoblock && $block->isAutoblocking();
+                               default:
+                                       return false;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Unset the 'BlockID' cookie.
+        *
+        * @since 1.34
+        * @param WebResponse $response
+        */
+       public static function clearBlockCookie( WebResponse $response ) {
+               $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
+       }
+
+       /**
+        * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of
+        * the ID and a HMAC (see DatabaseBlock::setCookie), but will sometimes only be the ID.
+        *
+        * @since 1.34
+        * @internal Should be private.
+        *  Left public for backwards compatibility, until DatabaseBlock::getIdFromCookieValue is removed.
+        * @param string $cookieValue The string in which to find the ID.
+        * @return int|null The block ID, or null if the HMAC is present and invalid.
+        */
+       public function getIdFromCookieValue( $cookieValue ) {
+               // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
+               $bangPos = strpos( $cookieValue, '!' );
+               $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
+               if ( !$this->secretKey ) {
+                       // If there's no secret key, just use the ID as given.
+                       return $id;
+               }
+               $storedHmac = substr( $cookieValue, $bangPos + 1 );
+               $calculatedHmac = MWCryptHash::hmac( $id, $this->secretKey, false );
+               if ( $calculatedHmac === $storedHmac ) {
+                       return $id;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Get the BlockID cookie's value for this block. This is usually the block ID concatenated
+        * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just
+        * be the block ID.
+        *
+        * @since 1.34
+        * @internal Should be private.
+        *  Left public for backwards compatibility, until DatabaseBlock::getCookieValue is removed.
+        * @param DatabaseBlock $block
+        * @return string The block ID, probably concatenated with "!" and the HMAC.
+        */
+       public function getCookieValue( DatabaseBlock $block ) {
+               $id = $block->getId();
+               if ( !$this->secretKey ) {
+                       // If there's no secret key, don't append a HMAC.
+                       return $id;
+               }
+               $hmac = MWCryptHash::hmac( $id, $this->secretKey, false );
+               $cookieValue = $id . '!' . $hmac;
+               return $cookieValue;
+       }
+
 }
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 df9eebe..ba08d54 100644 (file)
@@ -26,7 +26,6 @@ use ActorMigration;
 use AutoCommitUpdate;
 use BadMethodCallException;
 use CommentStore;
-use DateTime;
 use DeferredUpdates;
 use Hooks;
 use Html;
@@ -36,7 +35,6 @@ use MediaWiki\Block\Restriction\NamespaceRestriction;
 use MediaWiki\Block\Restriction\PageRestriction;
 use MediaWiki\Block\Restriction\Restriction;
 use MediaWiki\MediaWikiServices;
-use MWCryptHash;
 use MWException;
 use RequestContext;
 use stdClass;
@@ -1161,26 +1159,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 [];
        }
 
        /**
@@ -1383,35 +1395,22 @@ class DatabaseBlock extends AbstractBlock {
         * the same as the block's, to a maximum of 24 hours.
         *
         * @since 1.29
-        *
+        * @deprecated since 1.34 Set a cookie via BlockManager::trackBlockWithCookie instead.
         * @param WebResponse $response The response on which to set the cookie.
         */
        public function setCookie( WebResponse $response ) {
-               // Calculate the default expiry time.
-               $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
-
-               // Use the block's expiry time only if it's less than the default.
-               $expiryTime = $this->getExpiry();
-               if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
-                       $expiryTime = $maxExpiryTime;
-               }
-
-               // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
-               $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' );
-               $cookieOptions = [ 'httpOnly' => false ];
-               $cookieValue = $this->getCookieValue();
-               $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
+               MediaWikiServices::getInstance()->getBlockManager()->setBlockCookie( $this, $response );
        }
 
        /**
         * Unset the 'BlockID' cookie.
         *
         * @since 1.29
-        *
+        * @deprecated since 1.34 Use BlockManager::clearBlockCookie instead
         * @param WebResponse $response The response on which to unset the cookie.
         */
        public static function clearCookie( WebResponse $response ) {
-               $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
+               MediaWikiServices::getInstance()->getBlockManager()->clearBlockCookie( $response );
        }
 
        /**
@@ -1420,20 +1419,12 @@ class DatabaseBlock extends AbstractBlock {
         * be the block ID.
         *
         * @since 1.29
-        *
+        * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead of calling this
+        *  directly
         * @return string The block ID, probably concatenated with "!" and the HMAC.
         */
        public function getCookieValue() {
-               $config = RequestContext::getMain()->getConfig();
-               $id = $this->getId();
-               $secretKey = $config->get( 'SecretKey' );
-               if ( !$secretKey ) {
-                       // If there's no secret key, don't append a HMAC.
-                       return $id;
-               }
-               $hmac = MWCryptHash::hmac( $id, $secretKey, false );
-               $cookieValue = $id . '!' . $hmac;
-               return $cookieValue;
+               return MediaWikiServices::getInstance()->getBlockManager()->getCookieValue( $this );
        }
 
        /**
@@ -1441,29 +1432,12 @@ class DatabaseBlock extends AbstractBlock {
         * the ID and a HMAC (see DatabaseBlock::setCookie), but will sometimes only be the ID.
         *
         * @since 1.29
-        *
+        * @deprecated since 1.34 Use BlockManager::getUserBlock instead
         * @param string $cookieValue The string in which to find the ID.
-        *
         * @return int|null The block ID, or null if the HMAC is present and invalid.
         */
        public static function getIdFromCookieValue( $cookieValue ) {
-               // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
-               $bangPos = strpos( $cookieValue, '!' );
-               $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
-               // Get the site-wide secret key.
-               $config = RequestContext::getMain()->getConfig();
-               $secretKey = $config->get( 'SecretKey' );
-               if ( !$secretKey ) {
-                       // If there's no secret key, just use the ID as given.
-                       return $id;
-               }
-               $storedHmac = substr( $cookieValue, $bangPos + 1 );
-               $calculatedHmac = MWCryptHash::hmac( $id, $secretKey, false );
-               if ( $calculatedHmac === $storedHmac ) {
-                       return $id;
-               } else {
-                       return null;
-               }
+               return MediaWikiServices::getInstance()->getBlockManager()->getIdFromCookieValue( $cookieValue );
        }
 
        /**
@@ -1600,9 +1574,12 @@ class DatabaseBlock extends AbstractBlock {
        }
 
        /**
+        * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead of calling this
+        *  directly.
         * @inheritDoc
         */
        public function shouldTrackWithCookie( $isAnon ) {
+               wfDeprecated( __METHOD__, '1.34' );
                $config = RequestContext::getMain()->getConfig();
                switch ( $this->getType() ) {
                        case self::TYPE_IP:
index 1407271..c2fb52a 100644 (file)
@@ -25,9 +25,9 @@
  * @copyright © 2011, Antoine Musso
  */
 
-use Wikimedia\Rdbms\ResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\IResultWrapper;
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -67,7 +67,7 @@ class BacklinkCache {
         *
         * Initialized with BacklinkCache::getLinks()
         * Cleared with BacklinkCache::clear()
-        * @var ResultWrapper[]
+        * @var IResultWrapper[]
         */
        protected $fullResultCache = [];
 
@@ -179,7 +179,7 @@ class BacklinkCache {
         * @param int|bool $endId
         * @param int $max
         * @param string $select 'all' or 'ids'
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        protected function queryLinks( $table, $startId, $endId, $max, $select = 'all' ) {
                $fromField = $this->getPrefix( $table ) . '_from';
@@ -472,7 +472,7 @@ class BacklinkCache {
 
        /**
         * Partition a DB result with backlinks in it into batches
-        * @param ResultWrapper $res Database result
+        * @param IResultWrapper $res Database result
         * @param int $batchSize
         * @param bool $isComplete Whether $res includes all the backlinks
         * @throws MWException
index 7a0826e..2573f8a 100644 (file)
@@ -22,7 +22,7 @@
  */
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -152,7 +152,7 @@ class LinkBatch {
         * parsing to avoid extra DB queries.
         *
         * @param LinkCache $cache
-        * @param ResultWrapper $res
+        * @param IResultWrapper $res
         * @return array Array of remaining titles
         */
        public function addResultToCache( $cache, $res ) {
@@ -188,7 +188,7 @@ class LinkBatch {
 
        /**
         * Perform the existence test query, return a ResultWrapper with page_id fields
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         */
        public function doQuery() {
                if ( $this->isEmpty() ) {
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 3028dfd..6a1cc62 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Content handler for File: files
  * TODO: this handler s not used directly now,
@@ -41,7 +43,8 @@ class FileContentHandler extends WikitextContentHandler {
                if ( NS_FILE != $title->getNamespace() ) {
                        return [];
                }
-               $file = wfLocalFile( $title );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                       ->newFile( $title );
                if ( !$file || !$file->exists() ) {
                        return [];
                }
index aada514..4393abb 100644 (file)
@@ -22,6 +22,7 @@
  * @file
  */
 
+use Wikimedia\AtEase\AtEase;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 use Wikimedia\ScopedCallback;
@@ -553,7 +554,7 @@ class RequestContext implements IContextSource, MutableContext {
                        $wgUser = $context->getUser(); // b/c
                        if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
                                session_id( $session->getId() );
-                               Wikimedia\quietCall( 'session_start' );
+                               AtEase::quietCall( 'session_start' );
                        }
                        $request = new FauxRequest( [], false, $session );
                        $request->setIP( $params['ip'] );
index f4753d6..5df7aef 100644 (file)
@@ -26,6 +26,7 @@ use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Blob;
 use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\DBUnexpectedError;
 use Wikimedia\Rdbms\DBExpectedError;
@@ -250,7 +251,7 @@ class DatabaseOracle extends Database {
 
        /**
         * Frees resources associated with the LOB descriptor
-        * @param ResultWrapper|ORAResult $res
+        * @param IResultWrapper|ORAResult $res
         */
        function freeResult( $res ) {
                if ( $res instanceof ResultWrapper ) {
@@ -261,8 +262,8 @@ class DatabaseOracle extends Database {
        }
 
        /**
-        * @param ResultWrapper|ORAResult $res
-        * @return mixed
+        * @param IResultWrapper|ORAResult $res
+        * @return stdClass|bool
         */
        function fetchObject( $res ) {
                if ( $res instanceof ResultWrapper ) {
@@ -273,8 +274,8 @@ class DatabaseOracle extends Database {
        }
 
        /**
-        * @param ResultWrapper|ORAResult $res
-        * @return mixed
+        * @param IResultWrapper|ORAResult $res
+        * @return stdClass|bool
         */
        function fetchRow( $res ) {
                if ( $res instanceof ResultWrapper ) {
@@ -285,7 +286,7 @@ class DatabaseOracle extends Database {
        }
 
        /**
-        * @param ResultWrapper|ORAResult $res
+        * @param IResultWrapper|ORAResult $res
         * @return int
         */
        function numRows( $res ) {
@@ -297,7 +298,7 @@ class DatabaseOracle extends Database {
        }
 
        /**
-        * @param ResultWrapper|ORAResult $res
+        * @param IResultWrapper|ORAResult $res
         * @return int
         */
        function numFields( $res ) {
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 9d3309b..266d768 100644 (file)
@@ -497,7 +497,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
                $insertBatches = array_chunk( $insertions, $bSize );
                foreach ( $insertBatches as $insertBatch ) {
-                       $this->getDB()->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
+                       $this->getDB()->insert( $table, $insertBatch, __METHOD__, [ 'IGNORE' ] );
                        $lbf->commitAndWaitForReplication(
                                __METHOD__, $this->ticket, [ 'domain' => $domainId ]
                        );
@@ -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 a218f76..b193d5f 100644 (file)
@@ -114,7 +114,8 @@ class WANCacheReapUpdate implements DeferrableUpdate {
                }
 
                if ( $t->inNamespace( NS_FILE ) ) {
-                       $entities[] = wfLocalFile( $t->getText() );
+                       $entities[] = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $t->getText() );
                }
                if ( $t->inNamespace( NS_USER ) ) {
                        $entities[] = User::newFromName( $t->getText(), false );
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..8b42be1 100644 (file)
@@ -320,7 +320,7 @@ class WikiExporter {
                        }
 
                        $lastLogId = $this->outputLogStream( $result );
-               };
+               }
        }
 
        /**
index d3fd374..0659ec1 100644 (file)
@@ -462,7 +462,8 @@ class XmlDumpWriter {
         */
        function writeUploads( $row, $dumpContents = false ) {
                if ( $row->page_namespace == NS_FILE ) {
-                       $img = wfLocalFile( $row->page_title );
+                       $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $row->page_title );
                        if ( $img && $img->exists() ) {
                                $out = '';
                                foreach ( array_reverse( $img->getHistory() ) as $ver ) {
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 92be7d4..ee7ee6f 100644 (file)
@@ -5,6 +5,7 @@
  *
  * Represents files in a repository.
  */
+use Wikimedia\AtEase\AtEase;
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -44,8 +45,16 @@ use MediaWiki\MediaWikiServices;
  *
  * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
  *
- * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
- * in most cases.
+ * Consider the services container below;
+ *
+ * $services = MediaWikiServices::getInstance();
+ *
+ * The convenience services $services->getRepoGroup()->getLocalRepo()->newFile()
+ * and $services->getRepoGroup()->findFile() should be sufficient in most cases.
+ *
+ * @TODO: DI - Instead of using MediaWikiServices::getInstance(), a service should
+ * ideally accept a RepoGroup in its constructor and then, use $this->repoGroup->findFile()
+ * and $this->repoGroup->getLocalRepo()->newFile().
  *
  * @ingroup FileAbstraction
  */
@@ -1952,8 +1961,7 @@ abstract class File implements IDBAccessObject {
         * @param array $versions Set of record ids of deleted items to restore,
         *   or empty to restore all revisions.
         * @param bool $unsuppress Remove restrictions on content upon restoration?
-        * @return int|bool The number of file revisions restored if successful,
-        *   or false on failure
+        * @return Status
         * STUB
         * Overridden by LocalFile
         */
@@ -2171,7 +2179,7 @@ abstract class File implements IDBAccessObject {
                        $metadata = $this->getMetadata();
 
                        if ( is_string( $metadata ) ) {
-                               $metadata = Wikimedia\quietCall( 'unserialize', $metadata );
+                               $metadata = AtEase::quietCall( 'unserialize', $metadata );
                        }
 
                        if ( !is_array( $metadata ) ) {
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 86b8bbb..1e1bde3 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup FileAbstraction
  */
 
+use Wikimedia\AtEase\AtEase;
 use MediaWiki\Logger\LoggerFactory;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
@@ -38,8 +39,16 @@ use MediaWiki\MediaWikiServices;
  *
  * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
  *
- * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
- * in most cases.
+ * Consider the services container below;
+ *
+ * $services = MediaWikiServices::getInstance();
+ *
+ * The convenience services $services->getRepoGroup()->getLocalRepo()->newFile()
+ * and $services->getRepoGroup()->findFile() should be sufficient in most cases.
+ *
+ * @TODO: DI - Instead of using MediaWikiServices::getInstance(), a service should
+ * ideally accept a RepoGroup in its constructor and then, use $this->repoGroup->findFile()
+ * and $this->repoGroup->getLocalRepo()->newFile().
  *
  * @ingroup FileAbstraction
  */
@@ -141,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 );
        }
 
        /**
@@ -154,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;
@@ -181,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;
                }
@@ -1337,7 +1346,7 @@ class LocalFile extends File {
                $options = [];
                $handler = MediaHandler::getHandler( $props['mime'] );
                if ( $handler ) {
-                       $metadata = Wikimedia\quietCall( 'unserialize', $props['metadata'] );
+                       $metadata = AtEase::quietCall( 'unserialize', $props['metadata'] );
 
                        if ( !is_array( $metadata ) ) {
                                $metadata = [];
@@ -1494,7 +1503,7 @@ class LocalFile extends File {
                                'img_sha1' => $this->sha1
                        ] + $commentFields + $actorFields,
                        __METHOD__,
-                       'IGNORE'
+                       [ 'IGNORE' ]
                );
                $reupload = ( $dbw->affectedRows() == 0 );
 
@@ -1897,6 +1906,7 @@ class LocalFile extends File {
         * @return Status
         */
        function move( $target ) {
+               $localRepo = MediaWikiServices::getInstance()->getRepoGroup();
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
                        return $this->readOnlyFatalStatus();
                }
@@ -1913,8 +1923,8 @@ class LocalFile extends File {
                wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
 
                // Purge the source and target files...
-               $oldTitleFile = wfLocalFile( $this->title );
-               $newTitleFile = wfLocalFile( $target );
+               $oldTitleFile = $localRepo->findFile( $this->title );
+               $newTitleFile = $localRepo->findFile( $target );
                // To avoid slow purges in the transaction, move them outside...
                DeferredUpdates::addUpdate(
                        new AutoCommitUpdate(
index 5594004..21980b9 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup FileAbstraction
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -125,7 +126,8 @@ class LocalFileMoveBatch {
        public function execute() {
                $repo = $this->file->repo;
                $status = $repo->newGood();
-               $destFile = wfLocalFile( $this->target );
+               $destFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                       ->newFile( $this->target );
 
                $this->file->lock();
                $destFile->lock(); // quickly fail if destination is not available
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 4d5222c..d25d9aa 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
+
 /**
  * Image gallery.
  *
@@ -87,7 +90,7 @@ class TraditionalImageGallery extends ImageGalleryBase {
                                        # Fetch and register the file (file title may be different via hooks)
                                        list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $options );
                                } else {
-                                       $img = wfFindFile( $nt );
+                                       $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $nt );
                                }
                        } else {
                                $img = 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 f1ac42c..40c9417 100644 (file)
@@ -62,7 +62,8 @@ class ImportableUploadRevisionImporter implements UploadRevisionImporter {
                        $file = OldLocalFile::newFromArchiveName( $importableRevision->getTitle(),
                                RepoGroup::singleton()->getLocalRepo(), $archiveName );
                } else {
-                       $file = wfLocalFile( $importableRevision->getTitle() );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $importableRevision->getTitle() );
                        $file->load( File::READ_LATEST );
                        $this->logger->debug( __METHOD__ . 'Importing new file as ' . $file->getName() . "\n" );
                        if ( $file->exists() && $file->getTimestamp() > $importableRevision->getTimestamp() ) {
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 b32be39..de7a347 100644 (file)
@@ -531,7 +531,7 @@ abstract class DatabaseUpdater {
                if ( $val && $this->canUseNewUpdatelog() ) {
                        $values['ul_value'] = $val;
                }
-               $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
+               $this->db->insert( 'updatelog', $values, __METHOD__, [ 'IGNORE' ] );
                $this->db->setFlag( DBO_DDLMODE );
        }
 
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..65e7457 100644 (file)
@@ -285,7 +285,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 +304,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 +323,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 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 3705a8d..5b9742b 100644 (file)
@@ -45,8 +45,8 @@
        "config-env-bad": "The environment has been checked.\nYou cannot install MediaWiki.",
        "config-env-php": "PHP $1 is installed.",
        "config-env-hhvm": "HHVM $1 is installed.",
-       "config-unicode-using-intl": "Using the [https://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
-       "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [https://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+       "config-unicode-using-intl": "Using the [https://php.net/manual/en/book.intl.php PHP intl extension] for Unicode normalization.",
+       "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [https://php.net/manual/en/book.intl.php PHP intl extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
        "config-unicode-update-warning": "<strong>Warning:</strong> The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.\nYou should [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
        "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database {{PLURAL:$2|type is|types are}} supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php-mysql</code> package.",
        "config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $2, which is lower than minimum required version $1. SQLite will be unavailable.",
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 8bf48d8..67f769f 100644 (file)
@@ -55,7 +55,7 @@
        "config-unicode-pure-php-warning": "'''Aviso''': Le [https://pecl.php.net/intl extension PECL intl] non es disponibile pro exequer le normalisation Unicode; le systema recurre al implementation lente in PHP pur.\nSi tu sito ha un alte volumine de traffico, tu deberea informar te un poco super le [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisation Unicode].",
        "config-unicode-update-warning": "'''Aviso''': Le version installate del bibliotheca inveloppante pro normalisation Unicode usa un version ancian del bibliotheca del [http://site.icu-project.org/ projecto ICU].\nTu deberea [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations actualisar lo] si le uso de Unicode importa a te.",
        "config-no-db": "Non poteva trovar un driver appropriate pro le base de datos! Es necessari installar un driver de base de datos pro PHP.\nLe sequente {{PLURAL:$2|typo|typos}} de base de datos es supportate: $1.\n\nSi tu compilava PHP tu mesme, reconfigura lo con un cliente de base de datos activate, per exemplo, usante <code>./configure --with-mysqli</code>.\nSi tu installava PHP ex un pacchetto Debian o Ubuntu, tu debe etiam installar, per exemplo, le modulo <code>php-mysql</code>.",
-       "config-outdated-sqlite": "'''Attention''': tu ha SQLite $1, que es inferior al version minimal requirite, $2. SQLite essera indisponibile.",
+       "config-outdated-sqlite": "<strong>Attention</strong>: tu ha SQLite $2, que es inferior al minime version requirite, $1. SQLite essera indisponibile.",
        "config-no-fts3": "'''Attention''': SQLite es compilate sin [//sqlite.org/fts3.html modulo FTS3]; functionalitate de recerca non essera disponibile in iste back-end.",
        "config-pcre-old": "<strong>Fatal:</strong> PCRE $1 o plus tarde es necessari.\nTu binario de PHP binary es ligate con PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Plus information].",
        "config-pcre-no-utf8": "'''Fatal''': Le modulo PCRE de PHP pare haber essite compilate sin supporto de PCRE_UTF8.\nMediaWiki require supporto de UTF-8 pro functionar correctemente.",
index 4e9781b..eb3042b 100644 (file)
@@ -38,6 +38,7 @@
        "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-apc": "[https://www.php.net/apc APC] instalesis",
        "config-apcu": "[https://www.php.net/apcu APCu] instalesis",
        "config-profile-private": "Privata wiki",
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..934c7c6 100644 (file)
@@ -71,8 +71,8 @@
        "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 は利用できません。",
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 5a88deb..321caef 100644 (file)
@@ -65,7 +65,7 @@
        "config-unicode-pure-php-warning": "'''Увага''': [https://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
        "config-unicode-update-warning": "'''Увага''': Встановлена версія обгортки нормалізації Юнікоду використовує стару версію бібліотеки [http://site.icu-project.org/ проекту ICU].\nВи маєте [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations оновити версію], якщо плануєте повноцінно використовувати Юнікод.",
        "config-no-db": "Не вдалося знайти потрібний драйвер бази даних! Вам необхідно встановити драйвер бази даних для PHP. Підтримуються {{PLURAL:$2|такий тип|такі типи}} баз даних: $1.\n\nЯкщо ви скомпілювали PHP самостійно, переналаштуйте його з увімкненим клієнтом бази даних, наприклад за допомогою <code>./configure --with-mysqli</code>.\n\nЯкщо установлено PHP з пакетів Debian або Ubuntu, тоді ви також повинні встановити, наприклад, пакунок <code>php-mysql</code>.",
-       "config-outdated-sqlite": "'''Увага''': у Вас встановлена версія SQLite $1, а це нижче, ніж мінімально необхідна версія $2. SQLite буде недоступним.",
+       "config-outdated-sqlite": "<strong>Увага:</strong> у Вас встановлена версія SQLite $2, а це нижче, ніж мінімально необхідна версія $1. SQLite буде недоступним.",
        "config-no-fts3": "'''Увага''': SQLite зібраний без [//sqlite.org/fts3.html модуля FTS3], функції пошуку не будуть працювати у цій системі.",
        "config-pcre-old": "'''Фатальна помилка:''' потрібно PCRE версії $1 або пізнішої.\nВаш виконуваний файл PHP пов'язаний з PCRE версії $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Подробиці].",
        "config-pcre-no-utf8": "'''Помилка''': PCRE-модуть PHP, вочевидь, було зібрано без підтримки PCRE_UTF8.\nMediaWiki вимагає підтримку UTF-8 для коректної роботи.",
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 eb8b1a2..85e3af9 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup JobQueue
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Job for asynchronous rendering of thumbnails.
  *
@@ -36,7 +38,8 @@ class ThumbnailRenderJob extends Job {
 
                $transformParams = $this->params['transformParams'];
 
-               $file = wfLocalFile( $this->title );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                       ->newFile( $this->title );
                $file->load( File::READ_LATEST );
 
                if ( $file && $file->exists() ) {
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 0dd7b57..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
         *
@@ -806,18 +1025,38 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        }
 
        /**
+        * @internal For testing only
         * @return float UNIX timestamp
         * @codeCoverageIgnore
         */
-       protected function getCurrentTime() {
+       public function getCurrentTime() {
                return $this->wallClockOverride ?: microtime( true );
        }
 
        /**
-        * @param float|null &$time Mock UNIX timestamp for testing
+        * @internal For testing only
+        * @param float|null &$time Mock UNIX timestamp
         * @codeCoverageIgnore
         */
        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 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 de9ea55..5451476 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|ResultWrapper 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
@@ -4112,8 +4089,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * a wrapper. Nowadays, raw database objects are never exposed to external
         * callers, so this is unnecessary in external code.
         *
-        * @param bool|ResultWrapper|resource $result
-        * @return bool|ResultWrapper
+        * @param bool|IResultWrapper|resource $result
+        * @return bool|IResultWrapper
         */
        protected function resultObject( $result ) {
                if ( !$result ) {
@@ -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;
@@ -4497,17 +4474,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;
        }
@@ -4603,7 +4580,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * Delete a table
         * @param string $tableName
         * @param string $fName
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         * @since 1.18
         */
        public function dropTable( $tableName, $fName = __METHOD__ ) {
@@ -4689,12 +4666,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 6d266ae..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)
@@ -1358,7 +1361,7 @@ class DatabaseMssql extends Database {
         * Delete a table
         * @param string $tableName
         * @param string $fName
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         * @since 1.18
         */
        public function dropTable( $tableName, $fName = __METHOD__ ) {
index 36c947f..e871ab9 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
                        }
                }
 
@@ -277,7 +278,7 @@ abstract class DatabaseMysqlBase extends Database {
        abstract protected function mysqlSetCharset( $charset );
 
        /**
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @throws DBUnexpectedError
         */
        public function freeResult( $res ) {
@@ -301,7 +302,7 @@ abstract class DatabaseMysqlBase extends Database {
        abstract protected function mysqlFreeResult( $res );
 
        /**
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @return stdClass|bool
         * @throws DBUnexpectedError
         */
@@ -374,7 +375,7 @@ abstract class DatabaseMysqlBase extends Database {
 
        /**
         * @throws DBUnexpectedError
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @return int
         */
        function numRows( $res ) {
@@ -402,7 +403,7 @@ abstract class DatabaseMysqlBase extends Database {
        abstract protected function mysqlNumRows( $res );
 
        /**
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @return int
         */
        public function numFields( $res ) {
@@ -422,7 +423,7 @@ abstract class DatabaseMysqlBase extends Database {
        abstract protected function mysqlNumFields( $res );
 
        /**
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $n
         * @return string
         */
@@ -437,7 +438,7 @@ abstract class DatabaseMysqlBase extends Database {
        /**
         * Get the name of the specified field in a result
         *
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $n
         * @return string
         */
@@ -445,7 +446,7 @@ abstract class DatabaseMysqlBase extends Database {
 
        /**
         * mysql_field_type() wrapper
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $n
         * @return string
         */
@@ -460,14 +461,14 @@ abstract class DatabaseMysqlBase extends Database {
        /**
         * Get the type of the specified field in a result
         *
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $n
         * @return string
         */
        abstract protected function mysqlFieldType( $res, $n );
 
        /**
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $row
         * @return bool
         */
@@ -482,7 +483,7 @@ abstract class DatabaseMysqlBase extends Database {
        /**
         * Move internal result pointer
         *
-        * @param ResultWrapper|resource $res
+        * @param IResultWrapper|resource $res
         * @param int $row
         * @return bool
         */
@@ -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
@@ -1490,7 +1492,7 @@ abstract class DatabaseMysqlBase extends Database {
        /**
         * @param string $tableName
         * @param string $fName
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         */
        public function dropTable( $tableName, $fName = __METHOD__ ) {
                if ( !$this->tableExists( $tableName, $fName ) ) {
index 1a5cdab..937f381 100644 (file)
@@ -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 c9942a5..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" );
                        }
 
@@ -303,7 +303,7 @@ class DatabaseSqlite extends Database {
         * @param bool|string $file Database file name. If omitted, will be generated
         *   using $name and configured data directory
         * @param string $fname Calling function name
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
                if ( !$file ) {
@@ -330,7 +330,7 @@ class DatabaseSqlite extends Database {
         * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
         *
         * @param string $sql
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         */
        protected function doQuery( $sql ) {
                $res = $this->getBindingHandle()->query( $sql );
@@ -346,7 +346,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper|mixed $res
+        * @param IResultWrapper|mixed $res
         */
        function freeResult( $res ) {
                if ( $res instanceof ResultWrapper ) {
@@ -357,7 +357,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper|array $res
+        * @param IResultWrapper|array $res
         * @return stdClass|bool
         */
        function fetchObject( $res ) {
@@ -384,7 +384,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper|mixed $res
+        * @param IResultWrapper|mixed $res
         * @return array|bool
         */
        function fetchRow( $res ) {
@@ -406,7 +406,7 @@ class DatabaseSqlite extends Database {
        /**
         * The PDO::Statement class implements the array interface so count() will work
         *
-        * @param ResultWrapper|array|false $res
+        * @param IResultWrapper|array|false $res
         * @return int
         */
        function numRows( $res ) {
@@ -417,7 +417,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper $res
+        * @param IResultWrapper $res
         * @return int
         */
        function numFields( $res ) {
@@ -432,7 +432,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper $res
+        * @param IResultWrapper $res
         * @param int $n
         * @return bool
         */
@@ -474,7 +474,7 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @param ResultWrapper|array $res
+        * @param IResultWrapper|array $res
         * @param int $row
         */
        function dataSeek( $res, $row ) {
@@ -990,7 +990,7 @@ class DatabaseSqlite extends Database {
         * @param string $newName
         * @param bool $temporary
         * @param string $fname
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         * @throws RuntimeException
         */
        function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
@@ -1086,7 +1086,7 @@ class DatabaseSqlite extends Database {
         *
         * @param string $tableName
         * @param string $fName
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         * @throws DBReadOnlyError
         */
        public function dropTable( $tableName, $fName = __METHOD__ ) {
@@ -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 333bd81..faed1bf 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
@@ -217,7 +221,7 @@ interface IDatabase {
         * the LB info array is set to that parameter. If it is called with two
         * parameters, the member with the given name is set to the given value.
         *
-        * @param string $name
+        * @param array|string $name
         * @param array|null $value
         */
        public function setLBInfo( $name, $value = null );
@@ -1216,9 +1220,10 @@ interface IDatabase {
         *   $query .= $dbr->buildLike( $pattern );
         *
         * @since 1.16
+        * @param array[]|string|LikeMatch $param
         * @return string Fully built LIKE statement
         */
-       public function buildLike();
+       public function buildLike( $param );
 
        /**
         * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
@@ -2195,6 +2200,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 5706435..28e94a0 100644 (file)
@@ -150,7 +150,7 @@ interface IMaintainableDatabase extends IDatabase {
         * Delete a table
         * @param string $tableName
         * @param string $fName
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         */
        public function dropTable( $tableName, $fName = __METHOD__ );
 
@@ -303,7 +303,7 @@ interface IMaintainableDatabase extends IDatabase {
         * @param string $table Table name
         * @param string $field Field name
         *
-        * @return Field
+        * @return false|Field
         */
        public function fieldInfo( $table, $field );
 }
index 8608a7d..f48487a 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;
                } );
 
@@ -605,7 +609,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 +619,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 );
index faa9654..0258878 100644 (file)
@@ -116,6 +116,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 );
@@ -414,18 +415,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 +438,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 +453,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
index 51fda52..ffb7a34 100644 (file)
@@ -105,6 +105,8 @@ class LoadBalancer implements ILoadBalancer {
        private $errorConnection;
        /** @var int The generic (not query grouped) replica DB index */
        private $genericReadIndex = -1;
+       /** @var int[] The group replica DB indexes keyed by group */
+       private $readIndexByGroup = [];
        /** @var bool|DBMasterPos False if not set */
        private $waitForPos;
        /** @var bool Whether the generic reader fell back to a lagged replica DB */
@@ -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 */
@@ -249,6 +253,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                $this->defaultGroup = $params['defaultGroup'] ?? null;
+               $this->ownerId = $params['ownerId'] ?? null;
        }
 
        public function getLocalDomainID() {
@@ -395,9 +400,12 @@ class LoadBalancer implements ILoadBalancer {
                if ( count( $this->servers ) == 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 ) {
@@ -432,11 +440,11 @@ class LoadBalancer implements ILoadBalancer {
                        $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 +453,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
@@ -1328,13 +1370,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 +1401,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 +1435,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 +1460,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 +1499,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 +1569,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 +1597,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 +1617,7 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * @param string|string[] $stage
+        * @throws DBTransactionError
         */
        private function assertTransactionRoundStage( $stage ) {
                $stages = (array)$stage;
@@ -1585,6 +1636,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
         *
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
+       }
 }
 
 /**
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 aae2700..e139529 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use Exception;
+use Psr\Container\ContainerExceptionInterface;
 use RuntimeException;
 
 /**
@@ -31,7 +32,8 @@ use RuntimeException;
 /**
  * Exception thrown when trying to replace an already active service.
  */
-class CannotReplaceActiveServiceException extends RuntimeException {
+class CannotReplaceActiveServiceException extends RuntimeException
+       implements ContainerExceptionInterface {
 
        /**
         * @param string $serviceName
index 66fe97a..11a4d3d 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use Exception;
+use Psr\Container\ContainerExceptionInterface;
 use RuntimeException;
 
 /**
@@ -31,7 +32,8 @@ use RuntimeException;
 /**
  * Exception thrown when trying to access a service on a disabled container or factory.
  */
-class ContainerDisabledException extends RuntimeException {
+class ContainerDisabledException extends RuntimeException
+       implements ContainerExceptionInterface {
 
        /**
         * @param Exception|null $previous
index da51081..b6146ab 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use Exception;
+use Psr\Container\NotFoundExceptionInterface;
 use RuntimeException;
 
 /**
@@ -31,7 +32,8 @@ use RuntimeException;
 /**
  * Exception thrown when the requested service is not known.
  */
-class NoSuchServiceException extends RuntimeException {
+class NoSuchServiceException extends RuntimeException
+       implements NotFoundExceptionInterface {
 
        /**
         * @param string $serviceName
index 339b8a1..7b53cb8 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use Exception;
+use Psr\Container\ContainerExceptionInterface;
 use RuntimeException;
 
 /**
@@ -33,7 +34,8 @@ use RuntimeException;
  * Exception thrown when a service was already defined, but the
  * caller expected it to not exist.
  */
-class ServiceAlreadyDefinedException extends RuntimeException {
+class ServiceAlreadyDefinedException extends RuntimeException
+       implements ContainerExceptionInterface {
 
        /**
         * @param string $serviceName
index dd0d081..d1f1052 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use InvalidArgumentException;
+use Psr\Container\ContainerInterface;
 use RuntimeException;
 use Wikimedia\Assert\Assert;
 
@@ -44,7 +45,7 @@ use Wikimedia\Assert\Assert;
  * @see docs/injection.txt for an overview of using dependency injection in the
  *      MediaWiki code base.
  */
-class ServiceContainer implements DestructibleService {
+class ServiceContainer implements ContainerInterface, DestructibleService {
 
        /**
         * @var object[]
@@ -193,6 +194,11 @@ class ServiceContainer implements DestructibleService {
                return isset( $this->serviceInstantiators[$name] );
        }
 
+       /** @inheritDoc */
+       public function has( $name ) {
+               return $this->hasService( $name );
+       }
+
        /**
         * Returns the service instance for $name only if that service has already been instantiated.
         * This is intended for situations where services get destroyed/cleaned up, so we can
@@ -418,6 +424,11 @@ class ServiceContainer implements DestructibleService {
                return $this->services[$name];
        }
 
+       /** @inheritDoc */
+       public function get( $name ) {
+               return $this->getService( $name );
+       }
+
        /**
         * @param string $name
         *
index fabe1b3..bdca518 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wikimedia\Services;
 
 use Exception;
+use Psr\Container\ContainerExceptionInterface;
 use RuntimeException;
 
 /**
@@ -31,7 +32,8 @@ use RuntimeException;
 /**
  * Exception thrown when trying to access a disabled service.
  */
-class ServiceDisabledException extends RuntimeException {
+class ServiceDisabledException extends RuntimeException
+       implements ContainerExceptionInterface {
 
        /**
         * @param string $serviceName
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 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 1fc56bb..fe9e26f 100644 (file)
@@ -403,7 +403,7 @@ class LogPage {
                }
 
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->insert( 'log_search', $data, __METHOD__, 'IGNORE' );
+               $dbw->insert( 'log_search', $data, __METHOD__, [ 'IGNORE' ] );
 
                return true;
        }
index 90c0a72..1d0bbfd 100644 (file)
@@ -335,7 +335,7 @@ class ManualLogEntry extends LogEntryBase implements Taggable {
                        }
                }
                if ( count( $rows ) ) {
-                       $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
+                       $dbw->insert( 'log_search', $rows, __METHOD__, [ 'IGNORE' ] );
                }
 
                return $this->id;
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 088f94e..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;
        }
@@ -522,7 +526,7 @@ class SqlBagOStuff extends BagOStuff {
                                        'exptime' => $row->exptime
                                ],
                                __METHOD__,
-                               'IGNORE'
+                               [ 'IGNORE' ]
                        );
 
                        if ( $db->affectedRows() == 0 ) {
@@ -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 86c59ad..12cfe83 100644 (file)
@@ -75,9 +75,10 @@ class ImagePage extends Article {
 
                Hooks::run( 'ImagePageFindFile', [ $this, &$img, &$this->displayImg ] );
                if ( !$img ) { // not set by hook?
-                       $img = wfFindFile( $this->getTitle() );
+                       $services = MediaWikiServices::getInstance();
+                       $img = $services->getRepoGroup()->findFile( $this->getTitle() );
                        if ( !$img ) {
-                               $img = wfLocalFile( $this->getTitle() );
+                               $img = $services->getRepoGroup()->getLocalRepo()->newFile( $this->getTitle() );
                        }
                }
                $this->mPage->setFile( $img );
@@ -934,7 +935,7 @@ EOT
                                ) . "\n"
                        );
 
-               };
+               }
                $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
                $res->free();
 
index a314f3a..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
@@ -420,7 +420,9 @@ class PageArchive {
                $restoreFiles = $restoreAll || !empty( $fileVersions );
 
                if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
-                       $img = wfLocalFile( $this->title );
+                       /** @var LocalFile $img */
+                       $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $this->title );
                        $img->load( File::READ_LATEST );
                        $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
                        if ( !$this->fileStatus->isOK() ) {
index c457a34..013dd75 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\FakeResultWrapper;
 
 /**
@@ -55,14 +56,16 @@ class WikiFilePage extends WikiPage {
         * @return bool
         */
        protected function loadFile() {
+               $services = MediaWikiServices::getInstance();
                if ( $this->mFileLoaded ) {
                        return true;
                }
                $this->mFileLoaded = true;
 
-               $this->mFile = wfFindFile( $this->mTitle );
+               $this->mFile = $services->getRepoGroup()->findFile( $this->mTitle );
                if ( !$this->mFile ) {
-                       $this->mFile = wfLocalFile( $this->mTitle ); // always a File
+                       $this->mFile = $services->getRepoGroup()->getLocalRepo()
+                               ->newFile( $this->mTitle ); // always a File
                }
                $this->mRepo = $this->mFile->getRepo();
                return true;
@@ -149,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 3814112..d65d87b 100644 (file)
@@ -1342,7 +1342,7 @@ class WikiPage implements Page, IDBAccessObject {
                                'page_len'          => 0, // Fill this in shortly...
                        ] + $pageIdForInsert,
                        __METHOD__,
-                       'IGNORE'
+                       [ 'IGNORE' ]
                );
 
                if ( $dbw->affectedRows() > 0 ) {
@@ -1904,7 +1904,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 +3077,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 +3271,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 0c745c9..7fece00 100644 (file)
@@ -1030,7 +1030,7 @@ class CoreParserFunctions {
         * @return array|string
         */
        public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) {
-               $file = wfFindFile( $name );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
 
                if ( $argA == 'nowiki' ) {
                        // {{filepath: | option [| size] }}
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 6249791..c61de38 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;
        }
 
@@ -3898,7 +3887,7 @@ class Parser {
                } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
                        $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
                } else { // get by (name,timestamp)
-                       $file = wfFindFile( $title, $options );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title, $options );
                }
                return $file;
        }
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..d8e5e3e 100644 (file)
@@ -1245,7 +1245,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.
@@ -1948,7 +1948,7 @@ class Sanitizer {
                        # rbc
                        'rb'         => $common,
                        'rp'         => $common,
-                       'rt'         => $common, # array_merge( $common, array( 'rbspan' ) ),
+                       'rt'         => $common, # array_merge( $common, [ 'rbspan' ] ),
                        'rtc'        => $common,
 
                        # MathML root element, where used for extensions
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 ba5df52..0d95b22 100644 (file)
@@ -141,6 +141,12 @@ class ExtensionJsonValidator {
                        }
                }
 
+               // Deprecated stuff
+               if ( isset( $data->ParserTestFiles ) ) {
+                       // phpcs:ignore Generic.Files.LineLength.TooLong
+                       $extraErrors[] = '[ParserTestFiles] DEPRECATED: see <https://www.mediawiki.org/wiki/Manual:Extension.json/Schema#ParserTestFiles>';
+               }
+
                $validator = new Validator;
                $validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
                if ( $validator->isValid() && !$extraErrors ) {
index faaaece..e71de84 100644 (file)
@@ -65,6 +65,7 @@ class ExtensionProcessor implements Processor {
        protected static $coreAttributes = [
                'SkinOOUIThemes',
                'TrackingCategories',
+               'RestRoutes',
        ];
 
        /**
index 418f532..2ae6d74 100644 (file)
@@ -196,8 +196,7 @@ class ResourceLoader implements LoggerAwareInterface {
                $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
 
                $key = $cache->makeGlobalKey(
-                       'resourceloader',
-                       'filter',
+                       'resourceloader-filter',
                        $filter,
                        self::CACHE_VERSION,
                        md5( $data )
@@ -236,15 +235,14 @@ class ResourceLoader implements LoggerAwareInterface {
 
        /**
         * Register core modules and runs registration hooks.
-        * @param Config|null $config [optional]
+        * @param Config|null $config
         * @param LoggerInterface|null $logger [optional]
         */
        public function __construct( Config $config = null, LoggerInterface $logger = null ) {
                $this->logger = $logger ?: new NullLogger();
 
                if ( !$config ) {
-                       // TODO: Deprecate and remove.
-                       $this->logger->debug( __METHOD__ . ' was called without providing a Config instance' );
+                       wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
                        $config = MediaWikiServices::getInstance()->getMainConfig();
                }
                $this->config = $config;
@@ -1710,7 +1708,6 @@ MESSAGE;
         * @param bool $printable
         * @param bool $handheld
         * @param array $extraQuery
-        *
         * @return array
         */
        public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
@@ -1719,9 +1716,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 !== 'qqx' ) {
+                       $query['lang'] = $lang;
+               }
+               if ( $skin !== 'fallback' ) {
+                       $query['skin'] = $skin;
+               }
                if ( $debug === true ) {
                        $query['debug'] = 'true';
                }
index 58152ea..f57f13b 100644 (file)
@@ -134,6 +134,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 +150,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
         */
@@ -186,8 +190,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..47c8987 100644 (file)
@@ -334,7 +334,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 +617,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 +1083,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 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.
+        * 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().
+        *
+        * 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 +1134,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 +1218,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 +1234,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 9b50d80..db292cc 100644 (file)
@@ -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 66a4edf..dd7857e 100644 (file)
@@ -954,8 +954,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                return $cache->getWithSetCallback(
                        $cache->makeGlobalKey(
-                               'resourceloader',
-                               'jsparse',
+                               'resourceloader-jsparse',
                                self::$parseCacheVersion,
                                md5( $contents ),
                                $fileName
diff --git a/includes/resourceloader/ResourceLoaderOOUIIconPackModule.php b/includes/resourceloader/ResourceLoaderOOUIIconPackModule.php
new file mode 100644 (file)
index 0000000..c860362
--- /dev/null
@@ -0,0 +1,81 @@
+<?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
+ */
+
+/**
+ * Allows loading arbitrary sets of OOUI icons.
+ *
+ * @since 1.34
+ */
+class ResourceLoaderOOUIIconPackModule extends ResourceLoaderOOUIImageModule {
+       public function __construct( $options = [], $localBasePath = null ) {
+               parent::__construct( $options, $localBasePath );
+
+               if ( !isset( $this->definition['icons'] ) || !$this->definition['icons'] ) {
+                       throw new InvalidArgumentException( "Parameter 'icons' must be given." );
+               }
+
+               // A few things check for the "icons" prefix on this value, so specify it even though
+               // we don't use it for actually loading the data, like in the other modules.
+               $this->definition['themeImages'] = 'icons';
+       }
+
+       private function getIcons() {
+               return $this->definition['icons'];
+       }
+
+       protected function loadOOUIDefinition( $theme, $unused ) {
+               // This is shared between instances of this class, so we only have to load the JSON files once
+               static $data = [];
+
+               if ( !isset( $data[$theme] ) ) {
+                       $data[$theme] = [];
+                       // Load and merge the JSON data for all "icons-foo" modules
+                       foreach ( self::$knownImagesModules as $module ) {
+                               if ( substr( $module, 0, 5 ) === 'icons' ) {
+                                       $moreData = $this->readJSONFile( $this->getThemeImagesPath( $theme, $module ) );
+                                       if ( $moreData ) {
+                                               $data[$theme] = array_replace_recursive( $data[$theme], $moreData );
+                                       }
+                               }
+                       }
+               }
+
+               $definition = $data[$theme];
+
+               // Filter out the data for all other icons, leaving only the ones we want for this module
+               $iconsNames = $this->getIcons();
+               foreach ( array_keys( $definition['images'] ) as $iconName ) {
+                       if ( !in_array( $iconName, $iconsNames ) ) {
+                               unset( $definition['images'][$iconName] );
+                       }
+               }
+
+               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 313d789..34079c3 100644 (file)
@@ -19,7 +19,8 @@
  */
 
 /**
- * Secret special sauce.
+ * Loads the module definition from JSON files in the format that OOUI uses, converting it to the
+ * format we use. (Previously known as secret special sauce.)
  *
  * @since 1.26
  */
@@ -39,36 +40,12 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
 
                $definition = [];
                foreach ( $themes as $skin => $theme ) {
-                       // Find the path to the JSON file which contains the actual image definitions for this theme
-                       if ( $module ) {
-                               $dataPath = $this->getThemeImagesPath( $theme, $module );
-                       } else {
-                               // Backwards-compatibility for things that probably shouldn't have used this class...
-                               $dataPath =
-                                       $this->definition['rootPath'] . '/' .
-                                       strtolower( $theme ) . '/' .
-                                       $this->definition['name'] . '.json';
-                       }
-                       $localDataPath = $this->localBasePath . '/' . $dataPath;
+                       $data = $this->loadOOUIDefinition( $theme, $module );
 
-                       // If there's no file for this module of this theme, that's okay, it will just use the defaults
-                       if ( !file_exists( $localDataPath ) ) {
+                       if ( !$data ) {
+                               // If there's no file for this module of this theme, that's okay, it will just use the defaults
                                continue;
                        }
-                       $data = json_decode( file_get_contents( $localDataPath ), true );
-
-                       // Expand the paths to images (since they are relative to the JSON file that defines them, not
-                       // our base directory)
-                       $fixPath = function ( &$path ) use ( $dataPath ) {
-                               $path = dirname( $dataPath ) . '/' . $path;
-                       };
-                       array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
-                               if ( is_string( $value['file'] ) ) {
-                                       $fixPath( $value['file'] );
-                               } elseif ( is_array( $value['file'] ) ) {
-                                       array_walk_recursive( $value['file'], $fixPath );
-                               }
-                       } );
 
                        // Convert into a definition compatible with the parent vanilla ResourceLoaderImageModule
                        foreach ( $data as $key => $value ) {
@@ -107,4 +84,59 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
 
                parent::loadFromDefinition();
        }
+
+       /**
+        * Load the module definition from the JSON file(s) for the given theme and module.
+        *
+        * @since 1.34
+        * @param string $theme
+        * @param string $module
+        * @return array
+        */
+       protected function loadOOUIDefinition( $theme, $module ) {
+               // Find the path to the JSON file which contains the actual image definitions for this theme
+               if ( $module ) {
+                       $dataPath = $this->getThemeImagesPath( $theme, $module );
+               } else {
+                       // Backwards-compatibility for things that probably shouldn't have used this class...
+                       $dataPath =
+                               $this->definition['rootPath'] . '/' .
+                               strtolower( $theme ) . '/' .
+                               $this->definition['name'] . '.json';
+               }
+
+               return $this->readJSONFile( $dataPath );
+       }
+
+       /**
+        * Read JSON from a file, and transform all paths in it to be relative to the module's base path.
+        *
+        * @since 1.34
+        * @param string $dataPath Path relative to the module's base bath
+        * @return array|false
+        */
+       protected function readJSONFile( $dataPath ) {
+               $localDataPath = $this->localBasePath . '/' . $dataPath;
+
+               if ( !file_exists( $localDataPath ) ) {
+                       return false;
+               }
+
+               $data = json_decode( file_get_contents( $localDataPath ), true );
+
+               // Expand the paths to images (since they are relative to the JSON file that defines them, not
+               // our base directory)
+               $fixPath = function ( &$path ) use ( $dataPath ) {
+                       $path = dirname( $dataPath ) . '/' . $path;
+               };
+               array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
+                       if ( is_string( $value['file'] ) ) {
+                               $fixPath( $value['file'] );
+                       } elseif ( is_array( $value['file'] ) ) {
+                               array_walk_recursive( $value['file'], $fixPath );
+                       }
+               } );
+
+               return $data;
+       }
 }
index c834db1..efed418 100644 (file)
@@ -70,14 +70,14 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                // Build list of variables
                $skin = $context->getSkin();
                $vars = [
-                       'wgLoadScript' => wfScript( 'load' ),
+                       'wgLoadScript' => $conf->get( 'LoadScript' ),
                        'debug' => $context->getDebug(),
                        'skin' => $skin,
                        'stylepath' => $conf->get( 'StylePath' ),
                        'wgUrlProtocols' => wfUrlProtocols(),
                        'wgArticlePath' => $conf->get( 'ArticlePath' ),
                        'wgScriptPath' => $conf->get( 'ScriptPath' ),
-                       'wgScript' => wfScript(),
+                       'wgScript' => $conf->get( 'Script' ),
                        'wgSearchType' => $conf->get( 'SearchType' ),
                        'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
                        // Force object to avoid "empty" associative array from
@@ -258,7 +258,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,
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 6a6b86c..ca7bc04 100644 (file)
@@ -19,6 +19,7 @@
  * @ingroup RevisionDelete
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -109,7 +110,8 @@ class RevDelFileList extends RevDelList {
        }
 
        public function doPostCommitUpdates( array $visibilityChangeMap ) {
-               $file = wfLocalFile( $this->title );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                       ->newFile( $this->title );
                $file->purgeCache();
                $file->purgeDescription();
 
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 9ee3e17..d400267 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Implementation of near match title search.
  * TODO: split into service/implementation.
@@ -150,7 +152,7 @@ class SearchNearMatcher {
                # There may have been a funny upload, or it may be on a shared
                # file repository such as Wikimedia Commons.
                if ( $title->getNamespace() == NS_FILE ) {
-                       $image = wfFindFile( $title );
+                       $image = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
                        if ( $image ) {
                                return $title;
                        }
index f85c58f..a27d719 100644 (file)
@@ -86,16 +86,17 @@ class SearchResult {
         */
        protected function initFromTitle( $title ) {
                $this->mTitle = $title;
+               $services = MediaWikiServices::getInstance();
                if ( !is_null( $this->mTitle ) ) {
                        $id = false;
                        Hooks::run( 'SearchResultInitFromTitle', [ $title, &$id ] );
                        $this->mRevision = Revision::newFromTitle(
                                $this->mTitle, $id, Revision::READ_NORMAL );
                        if ( $this->mTitle->getNamespace() === NS_FILE ) {
-                               $this->mImage = wfFindFile( $this->mTitle );
+                               $this->mImage = $services->getRepoGroup()->findFile( $this->mTitle );
                        }
                }
-               $this->searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
+               $this->searchEngine = $services->newSearchEngine();
        }
 
        /**
@@ -146,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 0ea13e2..3f12563 100644 (file)
@@ -27,6 +27,7 @@ use CachedBagOStuff;
 use Psr\Log\LoggerInterface;
 use User;
 use WebRequest;
+use Wikimedia\AtEase\AtEase;
 
 /**
  * This is the actual workhorse for Session.
@@ -262,7 +263,7 @@ final class SessionBackend {
 
                        if ( $restart ) {
                                session_id( (string)$this->id );
-                               \Wikimedia\quietCall( 'session_start' );
+                               AtEase::quietCall( 'session_start' );
                        }
 
                        $this->autosave();
@@ -785,7 +786,7 @@ final class SessionBackend {
                                                'session' => $this->id,
                                ] );
                                session_id( (string)$this->id );
-                               \Wikimedia\quietCall( 'session_start' );
+                               AtEase::quietCall( 'session_start' );
                        }
                }
        }
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 ef45d15..a7b7569 100644 (file)
@@ -622,7 +622,6 @@ class SkinTemplate extends Skin {
                        $returnto['returnto'] = $page;
                        $query = $request->getVal( 'returntoquery', $this->thisquery );
                        $paramsArray = wfCgiToArray( $query );
-                       unset( $paramsArray['logoutToken'] );
                        $query = wfArrayToCgi( $paramsArray );
                        if ( $query != '' ) {
                                $returnto['returntoquery'] = $query;
@@ -695,8 +694,7 @@ class SkinTemplate extends Skin {
                                        'href' => self::makeSpecialUrl( 'Userlogout',
                                                // Note: userlogout link must always contain an & character, otherwise we might not be able
                                                // to detect a buggy precaching proxy (T19790)
-                                               ( $title->isSpecial( 'Preferences' ) ? [] : $returnto )
-                                               + [ 'logoutToken' => $this->getUser()->getEditToken( 'logoutToken', $this->getRequest() ) ] ),
+                                               ( $title->isSpecial( 'Preferences' ) ? [] : $returnto ) ),
                                        'active' => false
                                ];
                        }
index b4e244c..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)
@@ -658,18 +658,6 @@ class SpecialPage implements MessageLocalizer {
                return $this->msg( strtolower( $this->mName ) )->text();
        }
 
-       /**
-        * Get a self-referential title object
-        *
-        * @param string|bool $subpage
-        * @return Title
-        * @deprecated since 1.23, use SpecialPage::getPageTitle
-        */
-       function getTitle( $subpage = false ) {
-               wfDeprecated( __METHOD__, '1.23' );
-               return $this->getPageTitle( $subpage );
-       }
-
        /**
         * Get a self-referential title object
         *
index 1053bda..9a793c3 100644 (file)
@@ -361,7 +361,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 2f87c47..8396b4d 100644 (file)
@@ -23,6 +23,7 @@
 
 use MediaWiki\Auth\AuthManager;
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Implements Special:CreateAccount
@@ -65,7 +66,7 @@ class SpecialCreateAccount extends LoginSignupSpecialPage {
                if ( !$status->isGood() ) {
                        // track block with a cookie if it doesn't exists already
                        if ( $user->isBlockedFromCreateAccount() ) {
-                               $user->trackBlockWithCookie();
+                               MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $user );
                        }
                        throw new ErrorPageError( 'createacct-error', $status->getMessage() );
                }
index 5f80215..e1606b2 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 2326950..5d8a415 100644 (file)
@@ -109,7 +109,7 @@ class FileDuplicateSearchPage extends QueryPage {
                $this->hash = '';
                $title = Title::newFromText( $this->filename, NS_FILE );
                if ( $title && $title->getText() != '' ) {
-                       $this->file = wfFindFile( $title );
+                       $this->file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
                }
 
                $out = $this->getOutput();
index 507cd45..252df5b 100644 (file)
@@ -537,7 +537,7 @@ class MovePageForm extends UnlistedSpecialPage {
                if ( $nt->getNamespace() == NS_FILE
                        && !( $this->moveOverShared && $user->isAllowed( 'reupload-shared' ) )
                        && !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
-                       && wfFindFile( $nt )
+                       && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $nt )
                ) {
                        $this->showForm( [ [ 'file-exists-sharedrepo' ] ] );
 
@@ -567,7 +567,8 @@ class MovePageForm extends UnlistedSpecialPage {
 
                        // Delete an associated image if there is
                        if ( $nt->getNamespace() == NS_FILE ) {
-                               $file = wfLocalFile( $nt );
+                               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                                       ->newFile( $nt );
                                $file->load( File::READ_LATEST );
                                if ( $file->exists() ) {
                                        $file->delete( $reason, false, $user );
@@ -596,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() ) {
@@ -645,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 49f1b3c..c1409ff 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page that redirects to: the user for a numeric user id,
  * the file for a given filename, or the page for a given revision id.
@@ -101,7 +103,7 @@ class SpecialRedirect extends FormSpecialPage {
                } catch ( MalformedTitleException $e ) {
                        return Status::newFatal( $e->getMessageObject() );
                }
-               $file = wfFindFile( $title );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
 
                if ( !$file || !$file->exists() ) {
                        // Message: redirect-not-exists
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 dcc35fc..68fda49 100644 (file)
@@ -669,7 +669,8 @@ class SpecialUpload extends SpecialPage {
                        return true;
                }
 
-               $local = wfLocalFile( $this->mDesiredDestName );
+               $local = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                       ->newFile( $this->mDesiredDestName );
                if ( $local && $local->exists() ) {
                        // We're uploading a new version of an existing file.
                        // No creation, so don't watch it if we're not already.
index c278bab..dac19a3 100644 (file)
@@ -62,7 +62,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         *
         * @param string|null $subPage Subpage, e.g. in
         *   https://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
-        * @return bool Success
         */
        public function execute( $subPage ) {
                $this->useTransactionalTimeLimit();
@@ -71,10 +70,10 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                $this->checkPermissions();
 
                if ( $subPage === null || $subPage === '' ) {
-                       return $this->showUploads();
+                       $this->showUploads();
+               } else {
+                       $this->showUpload( $subPage );
                }
-
-               return $this->showUpload( $subPage );
        }
 
        /**
@@ -83,7 +82,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         *
         * @param string $key The key of a particular requested file
         * @throws HttpError
-        * @return bool
         */
        public function showUpload( $key ) {
                // prevent callers from doing standard HTML output -- we'll take it from here
@@ -92,10 +90,11 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                try {
                        $params = $this->parseKey( $key );
                        if ( $params['type'] === 'thumb' ) {
-                               return $this->outputThumbFromStash( $params['file'], $params['params'] );
+                               $this->outputThumbFromStash( $params['file'], $params['params'] );
                        } else {
-                               return $this->outputLocalFile( $params['file'] );
+                               $this->outputLocalFile( $params['file'] );
                        }
+                       return;
                } catch ( UploadStashFileNotFoundException $e ) {
                        $code = 404;
                        $message = $e->getMessage();
@@ -187,7 +186,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         * @param array $params Scaling parameters ( e.g. [ width => '50' ] );
         * @param int $flags Scaling flags ( see File:: constants )
         * @throws MWException|UploadStashFileNotFoundException
-        * @return bool Success
         */
        private function outputLocallyScaledThumb( $file, $params, $flags ) {
                // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
@@ -219,7 +217,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                        );
                }
 
-               return $this->outputLocalFile( $thumbFile );
+               $this->outputLocalFile( $thumbFile );
        }
 
        /**
@@ -239,7 +237,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         * @param array $params Scaling parameters ( e.g. [ width => '50' ] );
         * @param int $flags Scaling flags ( see File:: constants )
         * @throws MWException
-        * @return bool Success
         */
        private function outputRemoteScaledThumb( $file, $params, $flags ) {
                // This option probably looks something like
@@ -303,7 +300,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                        );
                }
 
-               return $this->outputContents( $req->getContent(), $contentType );
+               $this->outputContents( $req->getContent(), $contentType );
        }
 
        /**
@@ -313,7 +310,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         * @param File $file File object with a local path (e.g. UnregisteredLocalFile,
         *   LocalFile. Oddly these don't share an ancestor!)
         * @throws SpecialUploadStashTooLargeException
-        * @return bool
         */
        private function outputLocalFile( File $file ) {
                if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
@@ -322,10 +318,10 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                        );
                }
 
-               return $file->getRepo()->streamFileWithStatus( $file->getPath(),
+               $file->getRepo()->streamFileWithStatus( $file->getPath(),
                        [ 'Content-Transfer-Encoding: binary',
                                'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
-               )->isOK();
+               );
        }
 
        /**
@@ -334,7 +330,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
         * @param string $content
         * @param string $contentType MIME type
         * @throws SpecialUploadStashTooLargeException
-        * @return bool
         */
        private function outputContents( $content, $contentType ) {
                $size = strlen( $content );
@@ -347,8 +342,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                wfResetOutputBuffers();
                self::outputFileHeaders( $contentType, $size );
                print $content;
-
-               return true;
        }
 
        /**
@@ -394,7 +387,6 @@ class SpecialUploadStash extends UnlistedSpecialPage {
        /**
         * Default action when we don't have a subpage -- just show links to the uploads we have,
         * Also show a button to clear stashed files
-        * @return bool
         */
        private function showUploads() {
                // sets the title, etc.
@@ -459,7 +451,5 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                                . $refreshHtml
                        ) );
                }
-
-               return true;
        }
 }
index 568327d..62010d9 100644 (file)
@@ -26,7 +26,7 @@
  *
  * @ingroup SpecialPage
  */
-class SpecialUserLogout extends UnlistedSpecialPage {
+class SpecialUserLogout extends FormSpecialPage {
        function __construct() {
                parent::__construct( 'Userlogout' );
        }
@@ -35,41 +35,49 @@ class SpecialUserLogout extends UnlistedSpecialPage {
                return true;
        }
 
-       function execute( $par ) {
-               /**
-                * Some satellite ISPs use broken precaching schemes that log people out straight after
-                * they're logged in (T19790). Luckily, there's a way to detect such requests.
-                */
-               if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
-                       wfDebug( "Special:UserLogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
-                       throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
-               }
+       public function isListed() {
+               return false;
+       }
 
-               $this->setHeaders();
-               $this->outputHeader();
+       protected function getGroupName() {
+               return 'login';
+       }
 
-               $out = $this->getOutput();
-               $user = $this->getUser();
-               $request = $this->getRequest();
+       protected function getFormFields() {
+               return [];
+       }
 
-               $logoutToken = $request->getVal( 'logoutToken' );
-               $urlParams = [
-                       'logoutToken' => $user->getEditToken( 'logoutToken', $request )
-               ] + $request->getValues();
-               unset( $urlParams['title'] );
-               $continueLink = $this->getFullTitle()->getFullUrl( $urlParams );
+       protected function getDisplayFormat() {
+               return 'ooui';
+       }
 
-               if ( $logoutToken === null ) {
-                       $this->getOutput()->addWikiMsg( 'userlogout-continue', $continueLink );
-                       return;
-               }
-               if ( !$this->getUser()->matchEditToken(
-                       $logoutToken, 'logoutToken', $this->getRequest(), 24 * 60 * 60
-               ) ) {
-                       $this->getOutput()->addWikiMsg( 'userlogout-sessionerror', $continueLink );
+       public function execute( $par ) {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->setHeaders();
+                       $this->showSuccess();
                        return;
                }
 
+               parent::execute( $par );
+       }
+
+       public function alterForm( HTMLForm $form ) {
+               $form->setTokenSalt( 'logoutToken' );
+               $form->addHeaderText( $this->msg( 'userlogout-continue' ) );
+
+               $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+       }
+
+       /**
+        * Process the form.  At this point we know that the user passes all the criteria in
+        * userCanExecute(), and if the data array contains 'Username', etc, then Username
+        * resets are allowed.
+        * @param array $data
+        * @throws MWException
+        * @throws ThrottledError|PermissionsError
+        * @return Status
+        */
+       public function onSubmit( array $data ) {
                // Make sure it's possible to log out
                $session = MediaWiki\Session\SessionManager::getGlobalSession();
                if ( !$session->canSetUser() ) {
@@ -83,25 +91,37 @@ class SpecialUserLogout extends UnlistedSpecialPage {
                }
 
                $user = $this->getUser();
-               $oldName = $user->getName();
 
                $user->logout();
+               return new Status();
+       }
 
-               $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
-                       $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+       public function onSuccess() {
+               $this->showSuccess();
 
+               $user = $this->getUser();
+               $oldName = $user->getName();
                $out = $this->getOutput();
-               $out->addWikiMsg( 'logouttext', $loginURL );
-
                // Hook.
                $injected_html = '';
                Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
                $out->addHTML( $injected_html );
+       }
+
+       private function showSuccess() {
+               $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
+                       $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
+               $out = $this->getOutput();
+               $out->addWikiMsg( 'logouttext', $loginURL );
 
                $out->returnToMain();
        }
 
-       protected function getGroupName() {
-               return 'login';
+       /**
+        * Let blocked users to log out and come back with their sockpuppets
+        */
+       public function requiresUnblock() {
+               return false;
        }
 }
index 87bc259..fc54890 100644 (file)
@@ -1001,12 +1001,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 2ebbc2d..aa3a971 100644 (file)
@@ -24,6 +24,8 @@
  * @author Soxred93 <soxred93@gmail.com>
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Querypage that lists the most wanted files
  *
@@ -97,14 +99,14 @@ class WantedFilesPage extends WantedQueryPage {
        /**
         * Does the file exist?
         *
-        * Use wfFindFile so we still think file namespace pages without
-        * files are missing, but valid file redirects and foreign files are ok.
+        * Use findFile() so we still think file namespace pages without files
+        * are missing, but valid file redirects and foreign files are ok.
         *
         * @param Title $title
         * @return bool
         */
        protected function existenceCheck( Title $title ) {
-               return (bool)wfFindFile( $title );
+               return (bool)MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
        }
 
        function getQueryInfo() {
index 8f31f3e..36909aa 100644 (file)
@@ -436,7 +436,8 @@ class ImageListPager extends TablePager {
         * @throws MWException
         */
        function formatValue( $field, $value ) {
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $services = MediaWikiServices::getInstance();
+               $linkRenderer = $services->getLinkRenderer();
                switch ( $field ) {
                        case 'thumb':
                                $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
@@ -468,15 +469,18 @@ class ImageListPager extends TablePager {
                                                $filePage,
                                                $filePage->getText()
                                        );
-                                       $download = Xml::element( 'a',
-                                               [ 'href' => wfLocalFile( $filePage )->getUrl() ],
+                                       $download = Xml::element(
+                                               'a',
+                                               [ 'href' => $services->getRepoGroup()->findFile( $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 0cc9905..a37f4f7 100644 (file)
@@ -482,9 +482,8 @@ class RemexCompatMunger implements TreeHandler {
        }
 
        public function comment( $preposition, $refElement, $text, $sourceStart, $sourceLength ) {
-               list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement );
-               $this->serializer->comment( $preposition, $refNode, $text,
-                       $sourceStart, $sourceLength );
+               list( , $refNode ) = $this->getParentForInsert( $preposition, $refElement );
+               $this->serializer->comment( $preposition, $refNode, $text, $sourceStart, $sourceLength );
        }
 
        public function error( $text, $pos ) {
index 08d148f..c0dd00b 100644 (file)
@@ -83,6 +83,8 @@ class RemexMungerData {
         * @return string
         */
        public function dump() {
+               $parts = [];
+
                if ( $this->childPElement ) {
                        $parts[] = 'childPElement=' . $this->childPElement->getDebugTag();
                }
index d905aa4..ae5b732 100644 (file)
@@ -404,7 +404,7 @@ abstract class UploadBase {
         * @return mixed True if the file is verified, an array otherwise
         */
        protected function verifyMimeType( $mime ) {
-               global $wgVerifyMimeType;
+               global $wgVerifyMimeType, $wgVerifyMimeTypeIE;
                if ( $wgVerifyMimeType ) {
                        wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>\n" );
                        global $wgMimeTypeBlacklist;
@@ -412,17 +412,19 @@ abstract class UploadBase {
                                return [ 'filetype-badmime', $mime ];
                        }
 
-                       # Check what Internet Explorer would detect
-                       $fp = fopen( $this->mTempPath, 'rb' );
-                       $chunk = fread( $fp, 256 );
-                       fclose( $fp );
-
-                       $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
-                       $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
-                       $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
-                       foreach ( $ieTypes as $ieType ) {
-                               if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
-                                       return [ 'filetype-bad-ie-mime', $ieType ];
+                       if ( $wgVerifyMimeTypeIE ) {
+                               # Check what Internet Explorer would detect
+                               $fp = fopen( $this->mTempPath, 'rb' );
+                               $chunk = fread( $fp, 256 );
+                               fclose( $fp );
+
+                               $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
+                               $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
+                               $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
+                               foreach ( $ieTypes as $ieType ) {
+                                       if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
+                                               return [ 'filetype-bad-ie-mime', $ieType ];
+                                       }
                                }
                        }
                }
@@ -1262,12 +1264,11 @@ abstract class UploadBase {
         * @return bool True if the file contains something looking like embedded scripts
         */
        public static function detectScript( $file, $mime, $extension ) {
-               global $wgAllowTitlesInSVG;
-
                # ugly hack: for text files, always look at the entire file.
                # For binary field, just check the first K.
 
-               if ( strpos( $mime, 'text/' ) === 0 ) {
+               $isText = strpos( $mime, 'text/' ) === 0;
+               if ( $isText ) {
                        $chunk = file_get_contents( $file );
                } else {
                        $fp = fopen( $file, 'rb' );
@@ -1312,36 +1313,19 @@ abstract class UploadBase {
                        }
                }
 
-               /**
-                * Internet Explorer for Windows performs some really stupid file type
-                * autodetection which can cause it to interpret valid image files as HTML
-                * and potentially execute JavaScript, creating a cross-site scripting
-                * attack vectors.
-                *
-                * Apple's Safari browser also performs some unsafe file type autodetection
-                * which can cause legitimate files to be interpreted as HTML if the
-                * web server is not correctly configured to send the right content-type
-                * (or if you're really uploading plain text and octet streams!)
-                *
-                * Returns true if IE is likely to mistake the given file for HTML.
-                * Also returns true if Safari would mistake the given file for HTML
-                * when served with a generic content-type.
-                */
+               // Quick check for HTML heuristics in old IE and Safari.
+               //
+               // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
+               // don't need them all here as it can cause many false positives.
+               //
+               // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
                $tags = [
-                       '<a href',
                        '<body',
                        '<head',
                        '<html', # also in safari
-                       '<img',
-                       '<pre',
                        '<script', # also in safari
-                       '<table'
                ];
 
-               if ( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
-                       $tags[] = '<title';
-               }
-
                foreach ( $tags as $tag ) {
                        if ( strpos( $chunk, $tag ) !== false ) {
                                wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" );
@@ -2085,10 +2069,10 @@ abstract class UploadBase {
                $partname = $n ? substr( $filename, 0, $n ) : $filename;
 
                return (
-                       substr( $partname, 3, 3 ) == 'px-' ||
-                       substr( $partname, 2, 3 ) == 'px-'
-               ) &&
-               preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
+                               substr( $partname, 3, 3 ) == 'px-' ||
+                               substr( $partname, 2, 3 ) == 'px-'
+                       ) &&
+                       preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
        }
 
        /**
index c41697f..3a57c0b 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;
        }
 
@@ -1363,7 +1363,7 @@ class User implements IDBAccessObject, UserIdentity {
                                // If this user is autoblocked, set a cookie to track the block. This has to be done on
                                // every session load, because an autoblocked editor might not edit again from the same
                                // IP address after being blocked.
-                               $this->trackBlockWithCookie();
+                               MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
                        }
 
                        // Other code expects these to be set in the session, so set them.
@@ -1379,15 +1379,11 @@ class User implements IDBAccessObject, UserIdentity {
 
        /**
         * Set the 'BlockID' cookie depending on block type and user authentication status.
+        *
+        * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead
         */
        public function trackBlockWithCookie() {
-               $block = $this->getBlock();
-
-               if ( $block && $this->getRequest()->getCookie( 'BlockID' ) === null
-                       && $block->shouldTrackWithCookie( $this->isAnon() )
-               ) {
-                       $block->setCookie( $this->getRequest()->response() );
-               }
+               MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
        }
 
        /**
@@ -1826,8 +1822,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();
@@ -2549,7 +2544,7 @@ class User implements IDBAccessObject, UserIdentity {
                $dbw->insert( 'user_newtalk',
                        [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
                        __METHOD__,
-                       'IGNORE' );
+                       [ 'IGNORE' ] );
                if ( $dbw->affectedRows() ) {
                        wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
                        return true;
@@ -3302,7 +3297,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().
@@ -5049,10 +5044,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;
@@ -5122,10 +5117,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 eed0b6c..1a39945 100644 (file)
@@ -641,7 +641,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                        // @todo: Should we add these to the process cache?
                        $watchedItems[] = new WatchedItem(
                                $user,
-                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
+                               $target,
                                $this->getLatestNotificationTimestamp(
                                        $row->wl_notificationtimestamp, $user, $target )
                        );
@@ -769,7 +769,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                foreach ( $rowBatches as $toInsert ) {
                        // Use INSERT IGNORE to avoid overwriting the notification timestamp
                        // if there's already an entry for this page
-                       $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
+                       $dbw->insert( 'watchlist', $toInsert, __METHOD__, [ 'IGNORE' ] );
                        $affectedRows += $dbw->affectedRows();
                        if ( $ticket ) {
                                $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
index 66fc030..f648535 100644 (file)
@@ -6,6 +6,7 @@ use Category;
 use Hooks;
 use HtmlArmor;
 use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\MediaWikiServices;
 use SearchResult;
 use SpecialSearch;
 use Title;
@@ -30,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
         */
@@ -47,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>";
                }
 
@@ -117,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
         */
@@ -248,7 +252,8 @@ class FullSearchResultWidget implements SearchResultWidget {
                $descHtml = null;
                $thumbHtml = null;
 
-               $img = $result->getFile() ?: wfFindFile( $title );
+               $img = $result->getFile() ?: MediaWikiServices::getInstance()->getRepoGroup()
+                       ->findFile( $title );
                if ( $img ) {
                        $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
                        if ( $thumb ) {
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;
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 c5e09b8..e168b0e 100644 (file)
        "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": "قارن بين النسختين المختارتين",
        "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": "الزمن الحقيقي المستغرق",
        "passwordpolicies-policyflag-suggestchangeonlogin": "اقتراح التغيير عند تسجيل الدخول",
        "easydeflate-invaliddeflate": "المحتوى المقدم لا يتم تفريغه بشكل صحيح",
        "unprotected-js": "لأسباب تتعلق بالأمان; لا يمكن تحميل جافا سكريبت من الصفحات غير المحمية; الرجاء إنشاء جافا سكريبت فقط في نطاق ميدياويكي: أو كصفحة فرعية للمستخدم",
-       "userlogout-continue": "إذا كنت ترغب في تسجيل الخروج، تُرجَى [$1 المتابعة إلى صفحة تسجيل الخروج].",
-       "userlogout-sessionerror": "فشل تسجيل الخروج بسبب خطأ في الجلسة، تُرجَى [$1 المحاولة مرة أخرى]."
+       "userlogout-continue": "هل تريد تسجيل الخروج؟"
 }
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 789362e..beaab1d 100644 (file)
@@ -15,7 +15,8 @@
                        "Matma Rex",
                        "Tokvo",
                        "Crucifunked",
-                       "Enolp"
+                       "Enolp",
+                       "Matěj Suchánek"
                ]
        },
        "tog-underline": "Sorrayar enllaces:",
        "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:$1El 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",
        "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-sessionerror": "Falló salir por un error de sesión. [$1 Tenta nuevamente]."
+       "userlogout-continue": "¿Desees zarrar la sesión?"
 }
index 28449c2..c4273e9 100644 (file)
@@ -6,7 +6,8 @@
                        "Macofe",
                        "Matma Rex",
                        "Sfic",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Ajeetsinghawadh"
                ]
        },
        "tog-underline": "कड़ि अधोरेखन:",
        "special-characters-group-khmer": "खमेर",
        "special-characters-title-endash": "डैश",
        "special-characters-title-emdash": "बड्का डैश",
-       "special-characters-title-minus": "माइनस चिन्ह"
+       "special-characters-title-minus": "माइनस चिन्ह",
+       "userlogout-continue": "का आप लॉग आउट करा चाहत अहैं?"
 }
index 1aa60c6..44ddda8 100644 (file)
@@ -15,7 +15,8 @@
                        "Macofe",
                        "Matěj Suchánek",
                        "Rachitrali",
-                       "Sultanselim baloch"
+                       "Sultanselim baloch",
+                       "FarsiNevis"
                ]
        },
        "tog-underline": ":لینکاں کِشک کن",
        "index-category": "سرتاک بوتگێن پێجان",
        "noindex-category": "سرتاک نبوتگین پیجان",
        "broken-file-category": "پیج گون پرشتگین لینک فایل",
-       "about": "باره",
+       "about": "بارہ‌ئا",
        "article": "محتوا صفحه",
        "newwindow": "(ته نوکین پنچره ی پچ کن)",
        "cancel": "کنسل",
        "morenotlisted": "ائ لیست پکا نه انت",
        "mypage": "دیم یا تاک",
        "mytalk": "گپ",
-       "anontalk": "گپ کن گون ای آی پی",
+       "anontalk": "گپ",
        "navigation": "گردگ",
        "and": "&#32;و",
        "faq": "ب.ج.س",
        "history_short": "دپتر",
        "history_small": "تاریخچگ",
        "updatedmarker": "په روچ بیتگین چه منی اهری  اهری  چارگ",
-       "printableversion": "نسخه چهاپی",
+       "printableversion": "چاپی بھر",
        "permalink": "دایمی لینک",
        "print": "چهاپ",
        "view": "دیستین",
        "viewhelppage": "بگیند کومکی دیما",
        "categorypage": "بگیند کتیگوریی دیما",
        "viewtalkpage": "به گند بحث آ",
-       "otherlanguages": "بی دگه زبانانی تا",
+       "otherlanguages": "پہ دگہ زباناں",
        "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": "کومک",
        "mainpage": "بُنیادی دیم",
        "mainpage-description": "بُنیادی دیم",
        "policy-url": "Project:سیاست",
-       "portal": "پرتال انجمن",
+       "portal": "دیوانءِ درگت",
        "portal-url": "Project:پرتال انجمن",
-       "privacy": "سÛ\8cاست Ø­Ù\81ظ Ø§Ø³Ø±Ø§Ø±",
+       "privacy": "رازدارÛ\8cØ¡Ù\90 Ù¾Ø¦Û\8cÙ\85",
        "privacypage": "Project:سیاست حفظ اسرار",
        "badaccess": "حطا اجازت",
        "badaccess-group0": "شما مجاز نهیت عملی که درخواست کت اجرا کنیت",
        "nstab-template": "تراشوان",
        "nstab-help": "رهنمایی تاکدیم",
        "nstab-category": "تهر",
+       "mainpage-nstab": "بنیادیءِ دݔم",
        "nosuchaction": "نی چشین عمل",
        "nosuchactiontext": "ای کاری که گون اای یو ار ال مشخص بیتت نامشخص انت.\nشما بلکین یو‌ارال شر ننوشتت یا رند چه هرابیت لینکی اتکگیت\nشی بلکین یک خطایی ته برنامه سایت {{SITENAME}} پیش داریت.",
        "nosuchspecialpage": "نی چشین حاصین صفحه",
        "createacct-reason": "دلیل:",
        "createacct-reason-ph": "پرچا شما ادگر نوکین اکانتء اڈ کن ات",
        "createacct-submit": "وتی اکانتء اڈ کن ات",
-       "createacct-another-submit": "ادگر Ø§Ú©Ø§Ù\86تء Ø§Ú\88 Ø¨Ú©Ù\86 Ø§Øª",
+       "createacct-another-submit": "سابÛ\92 Ø¬Û\8fÚ\88Ý\94Ù\86",
        "createacct-benefit-heading": "{{SITENAME}} شهسانی واسته هنچوش که شمئیء اڈ بیتگ",
        "createacct-benefit-body1": "$1 {{PLURAL:$1|اصلاح|اصلاح کتگان}}",
        "createacct-benefit-body2": "{{PLURAL:$1|تاک|تاکان}}",
        "nocookieslogin": "{{SITENAME}} په ورود کابران چه کوکی استفاده کنت.\nشمی کوکی غیر فعالنت.\nلطفا آییا فعال کنیت و دگه  سعی کنیت.",
        "nocookiesfornew": "اکانت اڈ نبیت، پرچا که ما نتوانت آئی منبعء رء تأیید کنین.\nپکا بزان ات که کوکی‌هان فعال انت، رندا پیجء چه نوک رلود کن ات و دوبارگ بچکاس ات.",
        "noname": "شما یک معتبرین نام کاربر مشخص نه کتت.",
-       "loginsuccesstitle": "Ù\88رÙ\88د Ù\85Ù\88Ù\81Ù\82Û\8cت Ø¢Ù\85Û\8cز",
+       "loginsuccesstitle": "Ù\85اÙ\86 Ø¨Û\8cت Ø§Ù\90ت",
        "loginsuccess": "''''شما الان وارد {{SITENAME}} په عنوان \"$1\".'''",
-       "nosuchuser": "Ù\87Ú\86 Ú©Ø§Ø±Ø¨Ø±Û\8c Ú¯Ù\88Ù\86 Ù\86اÙ\85 \"$1\" Ù\86Û\8cستÙ\86.\nکاربرÛ\8c Ù\86اÙ\85 Ø­Ø±Ù\81Ø´ Ù¾Ù\87 Ù\87Ù\88ر Ù\88 Ù\85زÙ\86Û\8c Ø­Ø³Ø§Ø³ Ø§Ù\86ت.\nÙ\88تÛ\8c Ø§Ù\85Ù\84اÙ\8aا Ú\86Ú© Ú©Ù\86Û\8cت Û\8cا [[Special:CreateAccount|Ù\86Ù\88Ú©Û\8cÙ\86 Ø­Ø³Ø§Ø¨Û\8c Ø´Ø±Ú©Ù\86Û\8cت]].",
+       "nosuchuser": "Ù\87Ú\86 Ú©Ø§Ø±Ø²Ù\88رکÛ\92 Ú¯Û\8fÚº \"$1\"Ø¡Ù\8e Ù\86اÙ\85ا Ù\86Ý\94ست Ø§Ù\90Ù\86ت.\nکارزÙ\88رÙ\88Ú©Û\8cØ¡Ù\90 Ù\86اÙ\85 Ù¾Û\81 Ú¯Ø§Ù\84Ø¡Ù\90 Ù\85زÙ\86Û\8c Ø¡Ù\8f Ú¾Ù\8fردÛ\8câ\80\8cئا Ø­Ø³Ø§Ø³ Ø§Ù\90Ù\86ت.\nÙ\88تÛ\8c Ù\84کتگÝ\94Úº Ú¯Ø§Ù\84اں Ø´Ø±Ù\91 Ø¨Û\81 Ú\86ار Ø§Ù\90ت Û\8cا[[Special:CreateAccount|Ù\86Û\8fÚ©Ý\94Úº Ø³Ø§Ø¨Û\92 Ø¬Û\8fÚ\88Ý\94Ù\86]]اÙ\90ت.",
        "nosuchusershort": "هچ کاربری گون نام  \"$1\"نیستن.\nوتی املايا کنترل کنیت",
        "nouserspecified": "شما باید یک نام کاربری مشخص کنیت.",
        "login-userblocked": "ائ کابر بلاک بیتگ. لاگین مان سیستمء اجازت نه انت.",
-       "wrongpassword": "اشتباهین کلمه رمز وارد بوت. دگه سعی کن.",
+       "wrongpassword": "گالگوَز اشی نئں واجہ میر. الکاپݔں گالگوَزا بہ لِک.",
        "wrongpasswordempty": "کلمه رمز وارد بیتگین هالیکنت. دگه سعی کن",
        "passwordtooshort": "پسورد ضرورانت چکم {{PLURAL:$1|۱ کرکتر|$1 کرکتر}} داشتگ بیت.",
+       "passwordinlargeblacklist": "اے گالگوَز سک آسانں دگہ گرانݔں گالگوَزے گچݔں کن",
        "password-name-match": "شمئی پسورد ضرورنت چه شمئی یوزرنامء پرک بیت انت.",
        "password-login-forbidden": "ائ یوزرنام ءُ پسوردء کارمرز اجازت نه انت.",
        "mailmypassword": "نوکین پسوردء بلوٹ",
        "pt-login": "لاگین",
        "pt-login-button": "لاگین",
        "pt-createaccount": "اکانتء اڈ بکن",
-       "pt-userlogout": "در Ø´Ù\8fتÙ\86",
+       "pt-userlogout": "در Ø¨Û\8cÛ\8cÚ¯",
        "php-mail-error-unknown": "نامالومین ارور مان تابع  mail()‎ پی‌اچ‌پی",
        "user-mail-no-addy": "جهد پر ایمیلء راهیگ گیر چه ایمیل ادرس",
        "user-mail-no-body": "جهد پر هالیگ یانکه هوردین ایمیلء راهیگء",
        "changepassword-success": "شمئی پسورد پر درستیء ٹگل بیت!",
        "changepassword-throttled": "شما انیگ پر لاگین کتنء چنت بار جهد کتگ ات. دزبندی انت پیسر چه پدایین جهدء $1 موه بداریت.",
        "botpasswords-label-create": "جوڑ کورتین",
+       "botpasswords-label-update": "پہ رۏچ",
+       "botpasswords-label-cancel": "بجَگ",
+       "botpasswords-label-delete": "کۏر کنگ",
+       "botpasswords-label-resetpassword": "گالگوَزءِ پاک کنگ",
        "resetpass_forbidden": "کلمات رمز نه توننت عوض بنت.",
        "resetpass-no-info": "په مستقیمین دسترسی په ای صفحه شما بایدن وارد سایت بیت",
        "resetpass-submit-loggedin": "عوض کتن کلمه رمز",
        "loginreqpagetext": "شما باید $1 په گندگ دگه صفحات.",
        "accmailtitle": "کلمه رمز دیم دات",
        "accmailtext": "یک پسوردء [[User talk:$1|$1]] پر $2 راهیگ بوت. بیت آئرا چه پیجء ''[[Special:ChangePassword|پسوردء ٹگل]]'' که لاگینء درگتء پیش دارگ بیت ٹگل دئیت.",
-       "newarticle": "(نوکین)",
+       "newarticle": "(نۏک)",
        "newarticletext": "شما رند چه یک لینکی په یک صفحه ی که هنو نیستند اتکگیت.\nپه شر کتن صفحه، شروع کن نوشتن ته جعبه جهلی(بچار  [$1 صفحه کمک]  په گیشترین اطلاعات).\nاگر شما اشتباهی ادانیت ته وتی بروزر دکمه ''Back'' بجن.",
        "anontalkpagetext": "----'' ای صفحه بحث انت په یک ناشناس کاربری که هنگت یک حسابی شر نه کتت یا آی ا ستفاده نه کتت. اچه ما بایدن آدرس آی پی عددی په پچاه آرگ آیی استفاده کنین.\nچوشن آدرس آی پی گون چندین کاربر استفاده بیت.\nاگه شما یک کاربر ناشناس ایت وی حس کنیت بی ربطین نظر مربوط شمی هست، لطفا [[Special:UserLogin|وارد بیت ]] یا [[Special:CreateAccount|حسابی شرکن]] دان چه هور بییگ گون ناسناسین کاربران پرهیز بیت.''",
        "noarticletext": "هنو هچ متنی ته ای صفحه نیست.\nشما تونیت [[Special:Search/{{PAGENAME}}|گردیت په عنوان صفحه]]  ته دگه صفحات یا<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} گردگ په مربوطین آمار],\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} اصلاح ای صفحه]</span>.",
        "editwarning-warning": "گون در شتن چه ائ پیج ممکن انت شمئی پهکین شانس که تان انیگء کٹ کتگ ات بیران بیت.\nاگان شما لاگین کتگ ات، بیت که ائ هژاریء مان ای بهر «{{int:prefs-editing}}» وتی پریفرنسء نافعال بکن ات..",
        "editpage-notsupportedcontentformat-title": "توکداریگء فرمت ساپورٹ نه بیت",
        "editpage-notsupportedcontentformat-text": "ائ توکداریگء فرمت $1 مان ائ توکداریگء تهر $2 ساپورٹ نبیتگ انت.",
+       "slot-name-main": "بُنیگ",
        "content-model-wikitext": "ویکیسیاهگ",
        "content-model-text": "سادگین سیاهگ",
        "content-model-javascript": "جاوا اسکریپٹ",
        "histfirst": "پیسریگ ترین",
        "histlast": "نوکترین",
        "historysize": "({{PLURAL:$1|1 بایت|$1 بایت}})",
-       "historyempty": "(هالیک)",
+       "historyempty": "(ھچ)",
        "history-feed-title": "تاریح بازبینی",
        "history-feed-description": "تاریح بازبینی په ای صفحه ته ویکی",
        "history-feed-item-nocomment": "$1 ته $2",
        "notextmatches": "هچ متن صفحه هم دپ نهنت",
        "prevn": "پیشگین {{PLURAL:$1|$1}}",
        "nextn": "بعدی {{PLURAL:$1|$1}}",
+       "prev-page": "پُشتی تاک",
+       "next-page": "اݔدگہ تاک",
        "prevn-title": "$1 {{PLURAL:$1|نتیجهٔ|نتیجهٔ}} پیشگین",
        "nextn-title": "$1 {{PLURAL:$1|نتیجهٔ|نتیجهٔ}} دگه",
        "shown-title": "پیش دار $1 {{PLURAL:$1|نتیجه|نتیجه}} ته هر صفحه",
        "search-interwiki-caption": "پروژه آن گوهار",
        "search-interwiki-default": "نتایج چه $1 :",
        "search-interwiki-more": "(گیشتر)",
+       "search-interwiki-more-results": "گݔشتر",
        "search-relatedarticle": "مربوطین",
        "searchrelated": "مربوط",
        "searchall": "کل",
        "prefs-labs": "اپشن پر چکاس",
        "prefs-user-pages": "کاربریگین تاکان",
        "prefs-personal": "نمایه کاربر",
-       "prefs-rc": "نوکین تغییرات",
+       "prefs-rc": "نۏکݔں ٹگلاں",
        "prefs-watchlist": "لیست چارگ",
+       "prefs-editwatchlist": "چارگ لیستءِ ٹگلݔنگ",
        "prefs-watchlist-days": "روچان په پیش دارگ ته لیست چارگ",
        "prefs-watchlist-days-max": "(مکسیمم $1 {{PLURAL:$1|روچ|روچ}})",
        "prefs-watchlist-edits": "گشیترین تعداد تغییرات په پیشدارگ ته پچین لیست چارگ:",
        "grouppage-bureaucrat": "{{ns:project}}:دیواندارآن",
        "grouppage-suppress": "{{ns:project}}:رویت",
        "right-read": "بوان صفحاتء",
-       "right-edit": "اصÙ\84اح Ú©Ù\86 ØµÙ\81حاتء",
+       "right-edit": "تاکءÙ\90 Ù¹Ú¯Ù\84",
        "right-createpage": "شرکن صفحاتء(که صفحات بحث نهنت)",
        "right-createtalk": "شرکتن صفحات بحث",
        "right-createaccount": "شرکتن نوکین حسابان کاربری",
        "newuserlogpagetext": ".شی یک ورودی چه شرکتن کاربر",
        "rightslog": "ورودان حقوق کاربر",
        "rightslogtext": "شی یک آماری چه تغییرات په حقوق کاربری انت.",
-       "action-read": "وانگ این صفحه",
+       "action-read": "اے تاکءِ وانگ",
        "action-edit": "اصلاح ای صفحه",
        "action-createpage": "شرکتن ای صفحه",
        "action-createtalk": "شرکتن صفحات بحث",
        "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\" وجود ندارد.",
        "double-redirect-fixer": "تعمیرکنوک غیر مستقیم",
        "brokenredirects": "پروشتگین غیر مستقیمان",
        "brokenredirectstext": "جهلیگین غیر مستقیم لینک بوتگن په صفحات نیستن:",
-       "brokenredirects-edit": "اصلاح",
+       "brokenredirects-edit": "ٹگلݔنگ",
        "brokenredirects-delete": "حذف",
        "withoutinterwiki": "صفحاتی بی لینکان زبان",
        "withoutinterwiki-summary": "جهلیگین صفحات په دگه نسخه آن زبان لینک نه بوتت:",
        "sp-contributions-search": "گردگ په مشارکتان",
        "sp-contributions-username": "آدرس آی پی یا نام کاربری",
        "sp-contributions-submit": "گردگ",
-       "whatlinkshere": "اÛ\8c Ù\84Û\8cÙ\86Ú©Û\8c Ú©Ù\87 Ø§Ø¯Ø§ Ù\87ست",
+       "whatlinkshere": "اÛ\92 Ù\84Û\8cÙ\86Ú©Û\92 Ú©Û\81 Ø§Ø¯Ø§ Ú¾Û\81",
        "whatlinkshere-title": "صفحاتی که لینگ بوتگنت په \"$1\"",
        "whatlinkshere-page": "صفحه:",
        "linkshere": "جهلیگی صفحات لینک بوت '''$2''':",
index c8cbee5..4d850a1 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-адрас былі аўтаматычна заблякаваныя 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_deleted": "СÑ\82аÑ\80онка {{GRAMMAR:Ñ\80однÑ\8b|{{SITENAME}}}} Â«$1» Ð±Ñ\8bла Ð²Ñ\8bдаленаÑ\8f {{GENDER:$2|Ñ\9eдзелÑ\8cнÑ\96кам|Ñ\9eдзельніцай}} $2",
        "enotif_subject_created": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была створаная {{GENDER:$2|удзельнікам|удзельніцай}} $2",
        "enotif_subject_moved": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была перанесеная {{GENDER:$2|удзельнікам|удзельніцай}} $2",
        "enotif_subject_restored": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была адноўленая {{GENDER:$2|удзельнікам|удзельніцай}} $2",
        "passwordpolicies-policyflag-suggestchangeonlogin": "прапаноўваць зьмену па ўваходзе",
        "easydeflate-invaliddeflate": "Пададзены зьмест ня сьціснуты адпаведным чынам",
        "unprotected-js": "З прычынаў бясьпекі JavaScript ня можа быць загружаны зь неабароненых сайтаў. Калі ласка, стварайце javascript выключна ў прасторы назваў MediaWiki: ці як падстаронку ўдзельніка",
-       "userlogout-continue": "Калі вы захочаце выйсьці з сыстэмы, калі ласка, [$1 пераходзьце на старонку выхаду].",
-       "userlogout-sessionerror": "Выхад з сыстэмы не адбыўся праз памылку сэсіі. Калі ласка, [$1 паспрабуйце зноў]."
+       "userlogout-continue": "Вы жадаеце выйсьці з сыстэмы?"
 }
index 919c6a6..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|от последното посещение}}",
        "deleteprotected": "Не можете да изтриете страницата, защото е защитена.",
        "deleting-backlinks-warning": "<strong>Внимание:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Други страници]] сочат към или включват като шаблон страницата, която се опитвате да изтриете.",
        "rollback": "Отмяна на промените",
+       "rollback-confirmation-no": "Отказ",
        "rollbacklink": "отмяна",
        "rollbacklinkcount": "отмяна на $1 {{PLURAL:$1|редакция|редакции}}",
        "rollbacklinkcount-morethan": "отмяна на повече от $1 {{PLURAL:$1|редакция|редакции}}",
index fe1c4d6..314ad7b 100644 (file)
        "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..d1906f5 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": "بەردەوامبوون لە دروستکردنی ھەژمار",
        "nocookiesnew": "ھەژماری بەکارھێنەری دروست کرا، بەڵام نەچوویتەوە ژوورەوە.\n{{SITENAME}} بۆ چوونەوە ژوورەوەی بەکارھێنەر کوکی بەکاردەھێنێت.\nتۆ کوکییەکەکەت لەکارخستووە.\nتکایە کوکییەکە کارا بکە، پاشان بە ناوی بەکارھێنەری و تێپەڕوشەکەت بچۆ ژوورەوە.",
        "nocookieslogin": "{{SITENAME}} بۆ چوونەژوورەوە لە کووکی‌یەکان کەڵک وەرئەگرێت.\nڕێگەت نەداوە بە کووکی‌یەکان.\nڕێگەیان پێ بدەو و دیسان تێبکۆشە.",
        "nocookiesfornew": "ھەژماری بەکارھێنەری دروست نەکرا، چون ناتوانین سەرچاوەکەی پشتڕاست بکەینەوە.\nدڵنیا بە کوکییەکانت چالاک کردووە، پەڕەکە بار بکەوە و دیسان ھەوڵ بدە.",
-       "createacct-loginerror": "ھەژمارەکە بە سەرکەوتوانە دروست کرا، بەڵام ناتوانرێت بە شێوەیەکی ئۆتۆماتیکی بکرێیتە ژوورەوە. تکایە سەردانی [[Special:UserLogin|ڕێنماییەکانی چوونەژوورەوە]] بکە.",
+       "createacct-loginerror": "ھەژمارەکە بە سەرکەوتووانە دروست کرا، بەڵام ناتوانرێت بە شێوەیەکی خۆکارانە بکرێیتە ژوورەوە. تکایە سەردانی [[Special:UserLogin|ڕێنماییەکانی چوونەژوورەوە]] بکە.",
        "noname": "ناوی بەکارهێنەرییەکی گۆنجاوت دیاری نەکردووه.",
        "loginsuccesstitle": "چوویە ناوەوە",
        "loginsuccess": "'''ئێستا بە ناوی «$1»ەوە لە {{SITENAME}} چوویتەتەژوورەوە.'''",
index 311e82b..8c1b440 100644 (file)
@@ -85,6 +85,7 @@
        "category-empty": "''Sta categuria ùn cuntene alcuna pagina o file multimediale.''",
        "hidden-categories": "{{PLURAL:$1|Categuria nascosta|Categurie nascoste}}",
        "hidden-category-category": "Categurie nascoste",
+       "listingcontinuesabbrev": "sèguita",
        "index-category": "Pagine indicizate",
        "about": "À prupositu",
        "article": "Articulu",
        "viewtalkpage": "Vede a discussione",
        "otherlanguages": "In altre lingue",
        "redirectpagesub": "Pagina di reindirizzamentu",
+       "redirectto": "Reindirizzamentu à:",
        "lastmodifiedat": "Ultima mudifica di sta pagina u $1 à e $2.",
        "protectedpage": "Pagina prutetta",
        "jumpto": "Andà à:",
        "mycustomcssprotected": "You do not have permission to edit this CSS page.",
        "virus-unknownscanner": "antivirus scunnisciutu:",
        "yourname": "Nome di cuntributore:",
+       "userlogin-yourname": "Nome di cuntributore",
        "yourpassword": "Parolla secreta:",
+       "userlogin-yourpassword": "Parolla secreta",
        "yourpasswordagain": "Ripete a parolla secreta:",
        "yourdomainname": "U to duminiu:",
        "login": "Cunnessione",
        "nav-login-createaccount": "Cunnessione / registramentu",
        "logout": "Scunnessione",
        "userlogout": "Scunnessione",
+       "userlogin-noaccount": "Ùn hai ancu un accessu?",
        "createaccount": "Registramentu",
+       "userlogin-resetpassword-link": "Ti sì scurdatu/a di a to parolla secreta?",
        "createacct-reason": "Mutivu",
+       "createacct-submit": "Registramentu",
+       "createacct-benefit-body2": "$1 {{PLURAL:$1|pàgina|pàgine}}",
        "loginsuccesstitle": "Cunnessione fatta",
        "acct_creation_throttle_hit": "Desulatu, ai digià fattu $1 registramenti. Ùn ne poi micca fà d'altri.",
        "accountcreated": "Registramentu fattu",
        "accountcreatedtext": "U registramentu di l'utilizatore $1 hè statu fattu.",
        "loginlanguagelabel": "Lingua: $1",
        "pt-login": "Cunnessione",
+       "pt-login-button": "Cunnessione",
+       "pt-userlogout": "Scunnessione",
        "retypenew": "Scrive torna a nova parulla secreta:",
        "resetpass-submit-cancel": "Cancillà",
        "bold_sample": "Grassettu",
        "italic_tip": "Italicu",
        "link_sample": "Titulu di u ligame",
        "link_tip": "Ligame internu",
+       "extlink_sample": "http://www.example.com tìtulu di ligame",
        "extlink_tip": "Ligamu esternu (cù u prefissu http:// )",
        "headline_sample": "Testu di intestatura",
        "headline_tip": "Intestamentu di 2° livellu",
        "revisionasof": "Versione di e $1",
        "revision-info": "Versione di e $4 à e $5 di $2",
        "previousrevision": "← Versione menu ricente",
+       "nextrevision": "Versione più nova →",
        "currentrevisionlink": "Ultima revisione",
        "cur": "att",
        "last": "ante",
        "searchprofile-articles-tooltip": "Circà in $1",
        "searchprofile-everything-tooltip": "Circà dapertuttu (incluse e pagine di discussione)",
        "search-result-size": "$1 ({{PLURAL:$2|1 parolla|$2 parolle}})",
+       "search-redirect": "(Reindirizzamentu da $1)",
        "search-section": "(sezzione $1)",
        "search-suggest": "Forse vulii dì $1",
        "searchrelated": "currilati",
+       "searchall": "tutti",
        "search-nonefound": "A ricerca ùn hà micca datu risultati.",
        "powersearch-ns": "Circà in u spaziu di nomi",
        "preferences": "Preferenze",
        "rcnotefrom": "Quì seguitanu e mudifiche dapoi u '''$2''' ('''$1''' à u massimu).",
        "rclistfrom": "Mustrà e mudifiche dapoi u $3 $2",
        "rcshowhideminor": "$1 i cambiamenti minori",
+       "rcshowhideminor-show": "Muscià",
+       "rcshowhideminor-hide": "piattà",
        "rcshowhidebots": "$1 i boti",
+       "rcshowhidebots-show": "Muscià",
+       "rcshowhidebots-hide": "piattà",
        "rcshowhideliu": "$1 i cuntributori righjistrati",
+       "rcshowhideliu-show": "Muscià",
+       "rcshowhideliu-hide": "piattà",
        "rcshowhideanons": "$1 i cuntributori anonimi",
        "rcshowhideanons-show": "Muscià",
+       "rcshowhideanons-hide": "piattà",
        "rcshowhidepatr": "$1 e mudifiche verificate",
        "rcshowhidemine": "$1 e mo cuntribuzioni",
-       "rclinks": "Mustrà l'ultime $1 mudifiche in i $2 ghjorni scorsi",
+       "rcshowhidemine-show": "Mustrà",
+       "rcshowhidemine-hide": "piattà",
+       "rclinks": "Mustrà l'ùltime $1 mudifiche in i $2 ghjorni scorsi",
+       "diff": "Differenza",
        "hist": "cron",
        "hide": "piattà",
        "show": "mustrà",
        "upload": "Incaricà un schedariu",
        "uploadbtn": "Incaricà un schedariu",
        "filename": "Nome di u schedariu",
+       "filedesc": "sommariu",
        "filestatus": "Statu di u dirittu d'autore:",
        "upload-file-error": "Errore internu",
        "license": "Licenzia:",
        "pager-newer-n": "{{PLURAL:$1|1 più ricente|$1 più ricenti}}",
        "pager-older-n": "{{PLURAL:$1|1 menu ricente|$1 menu ricenti}}",
        "booksources": "Libri di fonti",
+       "booksources-search": "Circà",
        "specialloguserlabel": "Utilizatore:",
        "speciallogtitlelabel": "Titulu:",
        "log": "Righjistramenti",
        "contributions": "Mudifiche fatte da i {{GENDER:$1|cuntributori|cuntributrici}}",
        "contributions-title": "Cuntribuzione di $1",
        "mycontris": "Cuntribuzioni",
+       "anoncontribs": "Cuntribuzioni",
        "contribsub2": "Per {{GENDER:$3|$1}} ($2)",
        "uctop": "attuale",
        "month": "Da u mese (è nanzu):",
        "sp-contributions-newbies": "Mustrà solu e mudifiche di i novi cuntributori",
        "sp-contributions-talk": "discussione",
        "sp-contributions-search": "Ricercà e cuntribuzione",
+       "sp-contributions-username": "Adrizzu IP o nome di cuntributore",
+       "sp-contributions-toponly": "Solu mustrà versioni attuali",
        "sp-contributions-submit": "Circà",
        "whatlinkshere": "Pagine chì leganu quì",
        "whatlinkshere-title": "Pagine ligate à \"$1\"",
+       "whatlinkshere-page": "Pàgina:",
        "linkshere": "E seguente pagine sò culligate à '''$2''':",
+       "isredirect": "Pàgina di reindirizzamentu",
        "istemplate": "inclusione",
        "whatlinkshere-prev": "{{PLURAL:$1|precidente|precidenti $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|seguente|seguenti $1}}",
        "whatlinkshere-links": "← ligami",
+       "whatlinkshere-hideredirs": "$1 reindirizzamenti",
        "whatlinkshere-hidetrans": "$1 inclusione",
        "whatlinkshere-hidelinks": "$1 ligami",
+       "whatlinkshere-filters": "Filtri",
        "ipaddressorusername": "Adrizzu IP o nome di cuntributore",
        "ipbreason": "Mutivu:",
        "ipboptions": "2 ore:2 hours,1 ghjornu:1 day,3 ghjorni:3 days,1 sittimana:1 week,2 sittimane:2 weeks,1 mese:1 month,3 mesi:3 months,6 mesi:6 months,1 annu:1 year,infinitu:infinite",
        "importfailed": "Importu fiascatu: $1",
        "importlogpage": "Importu log",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|revisione|revisione}}",
-       "tooltip-pt-userpage": "A to pagina di cuntributore",
-       "tooltip-pt-mytalk": "A to pagina di discussione",
-       "tooltip-pt-preferences": "E to preferenze",
+       "tooltip-pt-userpage": "{{GENDER:|A to}} pàgina di cuntributore",
+       "tooltip-pt-mytalk": "{{GENDER:|A to}} pàgina di discussione",
+       "tooltip-pt-preferences": "{{GENDER:|E to}}} preferenze",
        "tooltip-pt-watchlist": "Lista di e pagine ch'è tù suviti",
-       "tooltip-pt-mycontris": "Lista di e to cuntribuzioni",
+       "tooltip-pt-mycontris": "Lista di {{GENDER:|e to}} cuntribuzioni",
        "tooltip-pt-login": "U registramentu hè suggeritu, micca ubligatoriu",
        "tooltip-pt-logout": "Esce da a sessione",
        "tooltip-ca-talk": "Vede e discussione relative à sta pagina",
        "tooltip-ca-delete": "Supprime sta pagina",
        "tooltip-ca-move": "Move 'ssa pagina",
        "tooltip-ca-watch": "Aghjunghje 'ssa pagina à u listinu di e pagine ch'è tù suviti",
+       "tooltip-ca-unwatch": "Supprimà 'ssa pàgina da u listinu di e pàgine ch'è tù suviti",
        "tooltip-search": "Circà in {{SITENAME}}",
        "tooltip-search-go": "Andà à una pagina incù u titolu indicatu, s'ella esiste",
        "tooltip-search-fulltext": "Circà e pagine cuntinenti stu testu",
        "tooltip-n-help": "Pagine di aiutu",
        "tooltip-t-whatlinkshere": "Listinu di tutte e pagine chì sò ligate à quessa",
        "tooltip-t-recentchangeslinked": "Versione di l'ultime mudifiche à e pagine legate à quessa",
-       "tooltip-t-contributions": "Listinu di e mudifiche di 'ssu cuntributore",
+       "tooltip-t-contributions": "Listinu di e mudifiche {{GENDER:$1|di 'ssu cuntributore}}",
        "tooltip-t-specialpages": "Listinu di tutte e pagine spiciale",
        "tooltip-t-print": "Versione stampevule di 'ssa pagina",
        "tooltip-t-permalink": "Ligame permanente à e revisione di sta pagina",
        "tooltip-ca-nstab-main": "Vede u cuntenutu di l'articulu",
        "tooltip-ca-nstab-user": "Vede a pagina di cuntributore",
+       "tooltip-ca-nstab-special": "Questa hè una pàgina particulare chi ùn si pó micca esse mudificata",
        "tooltip-ca-nstab-project": "Vede a pagina di u prugettu",
        "tooltip-ca-nstab-template": "Vede u mudellu",
        "tooltip-ca-nstab-category": "Vede a pagina di categuria",
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 cb6b65b..9e18328 100644 (file)
@@ -43,7 +43,8 @@
                        "Radana",
                        "Jan Růžička",
                        "Jaroslav Cerny",
-                       "Slepi"
+                       "Slepi",
+                       "Tchoř"
                ]
        },
        "tog-underline": "Podtrhávat odkazy:",
        "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",
        "mw-widgets-abandonedit-discard": "Zahodit úpravy",
        "mw-widgets-abandonedit-keep": "Pokračovat v editování",
        "mw-widgets-abandonedit-title": "Jste si {{GENDER:|jist|jista|jisti}}?",
+       "mw-widgets-copytextlayout-copy": "Zkopírovat",
+       "mw-widgets-copytextlayout-copy-fail": "Nepodařilo se zkopírovat do schránky.",
+       "mw-widgets-copytextlayout-copy-success": "Zkopírováno do schránky.",
        "mw-widgets-dateinput-no-date": "Nevybráno žádné datum",
        "mw-widgets-dateinput-placeholder-day": "RRRR-MM-DD",
        "mw-widgets-dateinput-placeholder-month": "RRRR-MM",
        "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ý.",
        "passwordpolicies-policyflag-suggestchangeonlogin": "navrhnout změnu při přihlášení",
        "easydeflate-invaliddeflate": "Poskytnutý obsah nebyl správně zkomprimován",
        "unprotected-js": "Z bezpečnostních důvodů nelze načítat JavaScript z nechráněných stran. Vyrábějte prosím JavaScriptové skripty jen ve jmenném prostoru MediaWiki: nebo jako uživatelskou podstránku",
-       "userlogout-continue": "Pokud se chcete odhlásit, [$1 pokračujte na odhlašovací stránku].",
-       "userlogout-sessionerror": "Kvůli chybě sezení se odhlášení nezdařilo. [$1 Zkuste to prosím znovu]."
+       "userlogout-continue": "Chcete se odhlásit?"
 }
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 fd44443..169cda8 100644 (file)
        "passwordpolicies": "Password politikker",
        "passwordpolicies-group": "Gruppe",
        "passwordpolicies-policies": "Politikker",
-       "passwordpolicies-policy-passwordcannotmatchusername": "Adgangskoden kan ikke være det samme som brugernavnet"
+       "passwordpolicies-policy-passwordcannotmatchusername": "Adgangskoden kan ikke være det samme som brugernavnet",
+       "userlogout-continue": "Ønsker du at logge af?"
 }
index dcf3aaf..fb7dd16 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "Änderung bei der Anmeldung vorschlagen",
        "easydeflate-invaliddeflate": "Der angegebene Inhalt ist nicht ordnungsgemäß komprimiert",
        "unprotected-js": "Aus Sicherheitsgründen kann JavaScript-Code nicht mehr von ungeschützten Seiten geladen werden. Erstelle die JavaScript-Seite bitte ausschließlich im Namensraum „MediaWiki“ oder als Benutzerunterseite.",
-       "userlogout-continue": "Falls du dich abmelden möchtest, [$1 fahre bitte auf der Abmeldeseite fort].",
-       "userlogout-sessionerror": "Abmeldung aufgrund eines Sitzungsfehlers fehlgeschlagen. Bitte [$1 erneut versuchen]."
+       "userlogout-continue": "Falls du dich abmelden möchtest, [$1 fahre bitte auf der Abmeldeseite fort]."
 }
index 6ae5f80..9c1c150 100644 (file)
        "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",
        "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",
        "filestatus": "Weziyetê heqa telifi:",
        "filesource": "Çıme:",
        "ignorewarning": "İqazi qebul meke û dosya reyna bar ke",
-       "ignorewarnings": "Îkazi kebul meke",
+       "ignorewarnings": "Tembey qebul mekerê",
        "minlength1": "Nameyanê dosyayî de gani bî ezamî yew herf est biyê.",
        "illegalfilename": "\"$1\" no nameyê dosya de tayê karakteri nêşuxulyenî. newe ra tesel bıkerê",
        "filename-toolong": "Nameyê dosyayan 240 bayt ra derg do nêbo.",
        "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.",
        "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.",
        "listusers-blocked": "(kılit biyo)",
        "activeusers": "Lista karberanê aktifan",
        "activeusers-intro": "Ena yew lista karberê ke $1 {{PLURAL:$1|roc|rocan}} ra tepiya iştirak kerdo inan motneno.",
-       "activeusers-count": "{{PLURAL:$3|roce de|$3 rocan de}} '''$1''' {{PLURAL:$1|iştırak kerdo|iştıraki kerdê}}",
+       "activeusers-count": "{{PLURAL:$3|roce de|$3 rocan de}} $1 {{PLURAL:$1|iştırak kerdo|iştıraki kerdê}}",
        "activeusers-from": "Enê karberi ra tepya bımocne:",
        "activeusers-noresult": "Karberi nêdiyayê.",
        "activeusers-submit": "Karberanê aktivan bıasene",
        "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:",
index 59e5370..af10a59 100644 (file)
        "action-upload_by_url": "να επιφορτώσετε αυτό το αρχείο από μια διεύθυνση URL",
        "action-writeapi": "να χρησιμοποιήσετε το API για εγγραφή",
        "action-delete": "να διαγράψετε αυτή τη σελίδα",
-       "action-deleterevision": "διαγράψτε αναθεωρήσεις",
+       "action-deleterevision": "διαγράψετε αναθεωρήσεις",
        "action-deletelogentry": "διαγράψτε καταχωρήσεις καταγραφών",
        "action-deletedhistory": "προβάλετε διαγεγραμμένο ιστορικό σελίδας",
        "action-deletedtext": "να προβάλετε κείμενο διαγεγραμμένων αναθεωρήσεων",
        "deletionlog": "Καταγραφές διαγραφών",
        "log-name-create": "Αρχείο καταγραφών δημιουργίας σελίδων",
        "log-description-create": "Παρακάτω υπάρχει ένας κατάλογος των πιο πρόσφατων δημιουργιών σελίδας.",
+       "logentry-create-create": "$1 δημιούργησε τη σελίδα $3",
        "reverted": "Επαναφορά σε προηγούμενη αναθεώρηση",
        "deletecomment": "Λόγος:",
        "deleteotherreason": "Άλλος/πρόσθετος λόγος:",
index 0dd9fe0..4c32ec6 100644 (file)
        "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",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suggest change on login",
        "easydeflate-invaliddeflate": "Content provided is not properly deflated",
        "unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
-       "userlogout-continue": "If you wish to log out please [$1 continue to the log out page].",
-       "userlogout-sessionerror": "Log out failed due to session error. Please [$1 try again]."
+       "userlogout-continue": "Do you want to log out?"
 }
index c665d2c..d94550f 100644 (file)
        "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.",
        "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-sessionerror": "Elsalutado malsukcesis pro sesia eraro. Bonvolu [$1 reprovi]."
+       "userlogout-continue": "Ĉu vi volas elsaluti?"
 }
index 902e7dc..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-sessionerror": "No se pudo cerrar la sesión debido a un error de sesión. [$1 Inténtalo de nuevo]."
+       "userlogout-continue": "¿Quieres finalizar la sesión?"
 }
index ed6879a..f447601 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "soovita muutmist sisselogimisel",
        "easydeflate-invaliddeflate": "Ette antud sisu ei ole õigesti vähendatud",
        "unprotected-js": "Turvalisuse huvides ei saa JavaScripti laadida kaitsmata lehekülgedelt. Palun koosta JavaScripti ainult nimeruumis MediaWiki või kasutajate nimeruumi alamleheküljel.",
-       "userlogout-continue": "Kui soovid välja logida, siis palun [$1 mine väljalogimise leheküljele].",
-       "userlogout-sessionerror": "Väljalogimine ebaõnnestus seansitõrke tõttu. Palun [$1 proovi uuesti]."
+       "userlogout-continue": "Kui soovid välja logida, siis palun [$1 mine väljalogimise leheküljele]."
 }
index 92c83d9..7b12798 100644 (file)
        "exif-pngfilecomment": "Kоментар на PNG файл",
        "exif-disclaimer": "Уточнение",
        "exif-contentwarning": "Предупреждение за съдържанието",
-       "exif-giffilecomment": "Kоментар на GIF файл",
-       "exif-intellectualgenre": "Тип ÐµÐ»ÐµÐ¼ÐµÐ½Ñ\82",
+       "exif-giffilecomment": "Коментар на GIF файл",
+       "exif-intellectualgenre": "Тип Ð½Ð° Ð¾Ð±ÐµÐºÑ\82а",
        "exif-subjectnewscode": "Код на темата",
        "exif-event": "Изобразено събитие",
        "exif-organisationinimage": "Изобразена организация",
        "exif-personinimage": "Изобразена личност",
-       "exif-originalimageheight": "Ð\92иÑ\81оÑ\87ина Ð½Ð° Ð¸Ð·Ð¾Ð±Ñ\80ажениеÑ\82о Ð¿Ñ\80еди Ð½Ð°Ð¼Ð°Ð»Ñ\8fването",
-       "exif-originalimagewidth": "ШиÑ\80ина Ð½Ð° Ð¸Ð·Ð¾Ð±Ñ\80ажениеÑ\82о Ð¿Ñ\80еди Ð½Ð°Ð¼Ð°Ð»Ñ\8fването",
-       "exif-compression-1": "Ð\9dекомпресиран",
+       "exif-originalimageheight": "Ð\92иÑ\81оÑ\87ина Ð½Ð° Ð¸Ð·Ð¾Ð±Ñ\80ажениеÑ\82о Ð¿Ñ\80еди Ð¸Ð·Ñ\80Ñ\8fзването",
+       "exif-originalimagewidth": "ШиÑ\80ина Ð½Ð° Ð¸Ð·Ð¾Ð±Ñ\80ажениеÑ\82о Ð¿Ñ\80еди Ð¸Ð·Ñ\80Ñ\8fзването",
+       "exif-compression-1": "Ð\94екомпресиран",
        "exif-compression-5": "LZW",
        "exif-compression-6": "JPEG (стар)",
        "exif-compression-7": "JPEG",
index 89a5d4b..cad4da9 100644 (file)
@@ -42,7 +42,7 @@
        "exif-pixelxdimension": "Ширина на сликата",
        "exif-pixelydimension": "Висина на сликата",
        "exif-usercomment": "Кориснички коментари",
-       "exif-relatedsoundfile": "Ð\9fовÑ\80зана Ð°Ñ\83диоснимка",
+       "exif-relatedsoundfile": "Ð\9fовÑ\80зана Ð·Ð²Ñ\83Ñ\87на снимка",
        "exif-datetimeoriginal": "Датум и време на сликање",
        "exif-datetimedigitized": "Датум и време на дигитализација",
        "exif-subsectime": "Дел од секундата во кој е сликано",
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.",
diff --git a/languages/i18n/exif/sdc.json b/languages/i18n/exif/sdc.json
new file mode 100644 (file)
index 0000000..d555538
--- /dev/null
@@ -0,0 +1,63 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Felis",
+                       "Jun Misugi",
+                       "Midnight Gambler"
+               ]
+       },
+       "exif-imagewidth": "Larghèzia",
+       "exif-imagelength": "Althèzia",
+       "exif-bitspersample": "Bit pa campioni",
+       "exif-compression": "Tipu di cumprissioni",
+       "exif-photometricinterpretation": "Sthruttura di li punti",
+       "exif-orientation": "Orientamentu",
+       "exif-xresolution": "Difinizioni orizontari",
+       "exif-yresolution": "Difinizioni verthicari",
+       "exif-datetime": "Data e ora di lu ciambamentu di lu file",
+       "exif-imagedescription": "Deschrizioni di l'immàgina",
+       "exif-model": "Mudellu",
+       "exif-software": "Software usaddu",
+       "exif-artist": "Autori",
+       "exif-copyright": "Infuimmazioni i' lu dirittu d'autori",
+       "exif-exifversion": "Versioni di lu fuimmaddu Exif",
+       "exif-colorspace": "Ippàziu di li curori",
+       "exif-usercomment": "Noti di l'utenti",
+       "exif-exposuretime-format": "$1 sigundu ($2)",
+       "exif-flash": "Caratterìsthiga e cundizioni di lu lampu",
+       "exif-flashenergy": "Putènzia di lu lampu",
+       "exif-contrast": "Cuntrollu cuntrasthu",
+       "exif-languagecode": "Linga",
+       "exif-iimcategory": "Categuria",
+       "exif-orientation-1": "Noimmari",
+       "exif-componentsconfiguration-0": "assenti",
+       "exif-subjectdistance-value": "$1 metri",
+       "exif-meteringmode-0": "Ischunisciddu",
+       "exif-meteringmode-1": "Mèdia",
+       "exif-meteringmode-2": "Mèdia pisadda cintradda",
+       "exif-meteringmode-3": "Luzi puntuari",
+       "exif-meteringmode-4": "MultiLuzi",
+       "exif-meteringmode-5": "Taurozza basi",
+       "exif-meteringmode-255": "Althru",
+       "exif-lightsource-1": "Luzi diurna",
+       "exif-lightsource-4": "Lampu",
+       "exif-lightsource-17": "Luzi standard A",
+       "exif-lightsource-18": "Luzi standard B",
+       "exif-lightsource-19": "Luzi standard C",
+       "exif-lightsource-20": "Illuminanti D55",
+       "exif-lightsource-21": "Illuminanti D65",
+       "exif-lightsource-22": "Illuminanti D75",
+       "exif-lightsource-23": "Illuminanti D50",
+       "exif-focalplaneresolutionunit-2": "póddighi",
+       "exif-sensingmethod-1": "Nò difiniddu",
+       "exif-gaincontrol-0": "Nisciunu",
+       "exif-contrast-0": "Noimmari",
+       "exif-contrast-1": "Althu cuntrasthu",
+       "exif-contrast-2": "Bassu cuntrasthu",
+       "exif-saturation-0": "Noimmari",
+       "exif-sharpness-0": "Noimmari",
+       "exif-sharpness-1": "Minori nitiddèzia",
+       "exif-sharpness-2": "Maggiori nitiddèzia",
+       "exif-subjectdistancerange-0": "Ischuniscidda",
+       "exif-gpsspeed-n": "Nodi"
+}
index 137815f..7293225 100644 (file)
        "virus-scanfailed": "پویش ناموفق (کد $1)",
        "virus-unknownscanner": "ضدویروس ناشناخته:",
        "logouttext": "'''اکنون شما ثبت خروج کرده‌اید.'''\nتوجه داشته باشید که تا حافظهٔ نهان مرورگرتان را پاک نکنید، بعضی از صفحات ممکن است همچنان به گونه‌ای نمایش یابند که انگار وارد شده‌اید.",
+       "logging-out-notify": "از سامانه خارج‌شده‌اید، لطفا صبر پیشه کنید.",
+       "logout-failed": "الان امکان خروج از سامانه وجود ندارد:$1",
        "cannotlogoutnow-title": "الان امکان خروج از سامانه نیست",
        "cannotlogoutnow-text": "در زمان استفاده از $1 امکان خروج از سامانه وجود ندارد.",
        "welcomeuser": "خوشامدید $1!",
        "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.",
        "rcfilters-savedqueries-already-saved": "این پالایه‌ها اکنون ذخیره شده‌اند. تنظیمات‌تان را تغییر دهید تا یک پالایهٔ ذخیره شدهٔ جدید بسازید.",
        "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": "پالایه‌ای فعال نیست. همهٔ مشارکت‌های دیده می‌شوند.",
        "blocklist-addressblocks": "پنهان کردن تک آی‌پی‌های بسته شده",
        "blocklist-type": "نوع:",
        "blocklist-type-opt-all": "همه",
+       "blocklist-type-opt-sitewide": "کلی",
        "blocklist-type-opt-partial": "جزئی",
        "blocklist-rangeblocks": "پنهان کردن قطع دسترسی بازه‌ها",
        "blocklist-timestamp": "برچسب زمان",
        "blocklink": "بستن",
        "unblocklink": "باز شود",
        "change-blocklink": "تغییر قطع دسترسی",
+       "empty-username": "(نام کاربری موجود نیست)",
        "contribslink": "مشارکت‌ها",
        "emaillink": "ارسال ایمیل",
        "autoblocker": "به طور خودکار بسته شد چون آی‌پی شما به تازگی توسط کاربر «[[User:$1|$1]]» استفاده شده‌است.\nدلیل قطع دسترسی $1 چنین است \"$2\"",
        "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 زبان $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": "آمار رسانه‌ها",
        "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-mediasearch-input-placeholder": "جستجو برای رسانه‌ها",
        "mw-widgets-mediasearch-noresults": "هیچ نتیجه‌ای پیدا نشد.",
        "passwordpolicies-policyflag-forcechange": "در هنگام ورود باید تغییر دهید",
        "passwordpolicies-policyflag-suggestchangeonlogin": "در هنگام ورود، پیشنهاد تغییر بده",
        "easydeflate-invaliddeflate": "محتوی تهیه‌شده به صورت درست خالی نشده‌است",
-       "unprotected-js": "به دلایل امنیتی، جاوااسکریپت نمی‌تواند از صفحات محافظت‌نشده بارگیری شود. لطفا جاوااسکریپت را تنها در فضای نام مدیاویکی: و یا در زیرصفحهٔ کاربری خودتان ایجاد کنید."
+       "unprotected-js": "به دلایل امنیتی، جاوااسکریپت نمی‌تواند از صفحات محافظت‌نشده بارگیری شود. لطفا جاوااسکریپت را تنها در فضای نام مدیاویکی: و یا در زیرصفحهٔ کاربری خودتان ایجاد کنید.",
+       "userlogout-continue": "آیا قصد خروج از سامانه را دارید؟"
 }
index 09417f4..afd803e 100644 (file)
        "virus-scanfailed": "virustarkistus epäonnistui (virhekoodi $1)",
        "virus-unknownscanner": "tuntematon virustutka:",
        "logouttext": "<strong>Olet nyt kirjautunut ulos.</strong>\n\nOta huomioon, että jotkut sivut saattavat näkyä edelleen ikään kuin olisit vielä kirjautuneena sisään siihen saakka kunnes tyhjennät selaimesi välimuistin.",
+       "logging-out-notify": "Sinua kirjataan ulos, odota hetki.",
        "cannotlogoutnow-title": "Nyt ei voi kirjautua ulos",
        "cannotlogoutnow-text": "Kirjautuminen ulos ei ole mahdollista käytettäessä $1.",
        "welcomeuser": "Tervetuloa $1!",
        "blankarticle": "<strong>Varoitus:</strong> Sivu, jota olet luomassa on tyhjä.\nJos napsautat ”$1” uudelleen, sivu luodaan ilman sisältöä.",
        "anoneditwarning": "<strong>Varoitus:</strong> Et ole kirjautunut sisään. IP-osoitteesi näkyy julkisesti kaikille, jos muokkaat. Jos <strong>[$1 kirjaudut sisään]</strong> tai <strong>[$2 luot tunnuksen]</strong>, muokkauksesi kirjataan käyttäjätunnuksesi tekemiksi ja samalla saat käyttöösi hyödyllisiä välineitä.",
        "anonpreviewwarning": "''Et ole kirjautunut sisään. Tallentaminen kirjaa IP-osoitteesi tämän sivun muutoshistoriaan.''",
-       "missingsummary": "Et ole antanut yhteenvetoa. Jos valitset Tallenna uudelleen, niin muokkauksesi tallennetaan ilman yhteenvetoa.",
+       "missingsummary": "<strong>Huomautus:</strong> Et ole antanut yhteenvetoa.\nJos painat \"$1\" uudelleen, niin muokkauksesi tallennetaan ilman yhteenvetoa.",
        "selfredirect": "<strong>Varoitus:</strong> Olet tekemässä uudelleenohjausta, joka johtaa tästä sivusta tähän samaan sivuun. \n\nOlet ehkä määrittänyt ohjauksen kohteen väärin tai kenties muokkaat parhaillaan väärää sivua.\n\nJos painat toimintoa ”$1” uudestaan, tämä ohjaussivu luodaan joka tapauksessa.",
        "missingcommenttext": "Kirjoita kommentti.",
        "missingcommentheader": "<strong>Muistutus:</strong> Et ole antanut aiheotsikkoa tälle kommentille. Napsauta ”$1”, jos haluat tallentaa kommenttisi ilman sellaista.",
        "uploadnewversion-linktext": "Tallenna uusi versio tästä tiedostosta",
        "shared-repo-from": "kohteesta $1",
        "shared-repo": "yhteinen mediavarasto",
+       "shared-repo-name-wikimediacommons": "Wikimedia Commons",
        "filepage.css": "/* Tänne syötetty CSS-koodi sisältyy tiedoston kuvaussivulle sekä muunkielisille asiakaswikeille */",
        "upload-disallowed-here": "Et voi tallentaa uutta tiedostoa tämän tilalle.",
        "filerevert": "Tiedoston $1 palautus",
        "ipb-confirm": "Vahvista esto",
        "ipb-sitewide": "Sivuston laajuinen",
        "ipb-partial": "Osittainen",
+       "ipb-sitewide-help": "Kaikki sivut wikissä ja kaikki muu muokkaustoiminta.",
        "ipb-partial-help": "Tietyt sivut tai nimiavaruudet.",
        "ipb-pages-label": "Sivut",
        "ipb-namespaces-label": "Nimiavaruudet",
        "passwordpolicies-policy-maximalpasswordlength": "Salasanan tulee olla lyhyempi kuin $1 {{PLURAL:$1|merkki|merkkiä}}",
        "passwordpolicies-policy-passwordcannotbepopular": "Salasana ei saa olla {{PLURAL:$1|suosittu salasana|$1 suosituimman salasanan listalla}}",
        "passwordpolicies-policy-passwordnotinlargeblacklist": "Salasana ei voi olla 100,000 yleisimmin käytetyn joukossa.",
-       "unprotected-js": "Turvallisuussyistä JavaScriptiä ei voi ladata suojaamattomilta sivuilta. Luo JavaScript-sivuja vain MediaWiki-nimiavaruuteen tai käyttäjän alasivulle."
+       "unprotected-js": "Turvallisuussyistä JavaScriptiä ei voi ladata suojaamattomilta sivuilta. Luo JavaScript-sivuja vain MediaWiki-nimiavaruuteen tai käyttäjän alasivulle.",
+       "userlogout-continue": "Haluatko kirjautua ulos?"
 }
index ae29db9..e2e5507 100644 (file)
        "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é",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suggérer une modification à la connexion",
        "easydeflate-invaliddeflate": "Le contenu fourni n'est pas correctement développé",
        "unprotected-js": "Pour des raisons de sécurité, JavaScript ne peut pas être chargé depuis des pages non protégées. Veuillez ne créer du javascript que dans l’espace de noms MediaWiki: ou comme sous-page utilisateur",
-       "userlogout-continue": "Si vous voulez vous déconnecter, veuillez [$1 continuer vers la page de déconnexion].",
-       "userlogout-sessionerror": "Déconnexion échouée à cause d’une erreur de session. Veuillez [$1 réessayer]."
+       "userlogout-continue": "Voulez-vous vous déconnecter ?"
 }
index 3705352..cd3def4 100644 (file)
@@ -56,6 +56,7 @@
        "tog-watchlisthidebots": "Botbewurkings ferbergje yn 'e folchlist",
        "tog-watchlisthideminor": "Feroarings fan lytse betsjutting ferbergje yn 'e folchlist",
        "tog-watchlisthideliu": "Bewurkings fan oanmelde meidoggers ferbergje yn 'e folchlist",
+       "tog-watchlistreloadautomatically": "Folchlist automatysk werlade at in filter feroare wurdt (JavaScript fereaske)",
        "tog-watchlistunwatchlinks": "Direkte folch-/ûntfolchmarkearders taheakje ({{int:Watchlist-unwatch-undo}}/{{int:Watchlist-unwatch}}) oan folchlistsiden mei wizigings (JavaScript fereaske foar omskeakelmooglikheid)",
        "tog-watchlisthideanons": "Bewurkings fan anonime meidoggers ferbergje yn 'e folchlist",
        "tog-watchlisthidepatrolled": "Markearre feroarings op myn folchlist ferskûlje",
@@ -65,6 +66,7 @@
        "tog-showhiddencats": "Ferburgen kategoryen werjaan",
        "tog-norollbackdiff": "Gjin ferskillen sjen litte nei it útfieren fan weromdraaien",
        "tog-useeditwarning": "My warskôgje at ik in bewurkingsside mei net-bewarre wizigings ferlit",
+       "tog-prefershttps": "Oanmeld altiten in befeilige ferbining brûke",
        "tog-showrollbackconfirmation": "Befêstigingsdialooch sjen litte by it klikken op 'weromdraaie'",
        "underline-always": "Altyd",
        "underline-never": "Nea",
        "category-empty": "<em>Dizze kategory befettet op it stuit gjin siden of media.</em>",
        "hidden-categories": "Ferburgen {{PLURAL:$1|kategory|kategoryen}}",
        "hidden-category-category": "Ferburgen kategoryen",
-       "category-subcat-count": "{{PLURAL:$2|Dizze kategory hat allinne de folgjende ûnderkategory.|Dizze kategory hat de folgjende {{PLURAL:$1|ûnderkategory|$1 ûnderkategoryen}}, fan yn totaal $2.}}",
+       "category-subcat-count": "{{PLURAL:$2|Dizze kategory hat allinne de neikommende ûnderkategory.|Dizze kategory hat de neikommende {{PLURAL:$1|ûnderkategory|$1 ûnderkategoryen}}, fan yn totaal $2.}}",
        "category-subcat-count-limited": "Dizze kategory hat de folgjende {{PLURAL:$1|ûnderkategory|$1 ûnderkategoryen}}.",
-       "category-article-count": "{{PLURAL:$2|Dizze kategory befettet allinne de folgjende side.|De folgjende {{PLURAL:$1|side is|$1 siden binne}} yn dizze kategory, fan yn totaal $2.}}",
+       "category-article-count": "{{PLURAL:$2|Dizze kategory befettet allinne de neikommende side.|De neikommende {{PLURAL:$1|side sit|$1 siden sitte}} yn dizze kategory, fan yn totaal $2.}}",
        "category-article-count-limited": "De folgjende {{PLURAL:$1|side is|$1 siden binne}} yn dizze kategory.",
        "category-file-count": "{{PLURAL:$2|Dizze kategory befettet it folgjende bestân.|Dizze kategory befettet {{PLURAL:$1|it folgjende bestân|$1 de folgjende bestannen}}, fan yn totaal $2.}}",
        "category-file-count-limited": "Dizze kategory befettet {{PLURAL:$1|it folgjende bestân|de folgjende $1 bestannen}}.",
        "toolbox": "Ark",
        "tool-link-userrights": "{{GENDER:$1|Meidochgroepen}} feroarje",
        "tool-link-userrights-readonly": "{{GENDER:$1|Meidochgroepen}} besjen",
+       "tool-link-emailuser": "Dizze {{GENDER:$1|meidogger|meidochster}} e-maile",
        "imagepage": "Besjoch bestânsside",
        "mediawikipage": "Berjochtside sjen litte",
        "templatepage": "Berjochtside lêze",
        "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.",
        "emailauthenticated": "Jo e-mailadres is befêstige op $2 om $3.",
        "emailnotauthenticated": "Jo e-mailadres is noch net befêstige.\nDer sil gjin e-mail stjoerd wurde foar alle neikommende funksjes.",
        "noemailprefs": "Jou in e-mailadres op om dizze funksjes te brûken.",
-       "emailconfirmlink": "Befêstigje jo netpostadres.",
+       "emailconfirmlink": "Jo e-mailadres befêstigje",
        "invalidemailaddress": "It e-mailadres is net akseptearre om't it in ûnjildige opmaak hat.\nJou beleaven in jildich e-mailadres op of lit it fjild leech.",
        "accountcreated": "Meidogger oanmakke",
        "accountcreatedtext": "It meidoggersakkount [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|oerlis]]) is oanmakke.",
        "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",
        "template-protected": "(befeilige)",
        "template-semiprotected": "(semy-befeilige)",
        "hiddencategories": "Dizze side falt yn de folgjende ferburgen\n{{PLURAL:$1|kategory|kategoryen}}:",
-       "edittools": "<!-- Tekst hjir stiet ûnder bewurkingsfjilden en oanbringfjilden.  -->",
+       "edittools": "<!-- De tekst hjirre wurdt werjûn ûnder it bewurkingsfjild en oanbiedformulier. -->",
        "edittools-upload": "-",
        "nocreatetext": "{{SITENAME}} hat de mûglikheid beheind om nije siden oan te meitsjen.\nJo kinne al weromgean en besteande siden bewurkje, [[Special:UserLogin|jo oanmelde of in akkount oanmeitsje]].",
        "nocreate-loggedin": "Jo meie gjin nije siden meitsje",
        "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",
        "prefs-skin": "Foarmjouwing",
        "skin-preview": "Proefbyld",
        "datedefault": "Gjin foarkar",
+       "prefs-labs": "Eksperimintele funksjes",
        "prefs-user-pages": "Meidoggersiden",
        "prefs-personal": "Meidogger",
        "prefs-rc": "Koartlyn feroare",
        "prefs-pageswatchlist": "Folchsiden",
        "prefs-tokenwatchlist": "Kaai",
        "prefs-diffs": "Ferskillen",
+       "prefs-help-prefershttps": "Dizze foarkar wurdt by jo neikommende oanmelding tapast.",
        "userrights": "Behear fan meidoggerrjochten",
        "userrights-lookup-user": "Behear fan meidoggerrjochten",
        "userrights-user-editname": "Jou in meidochnamme:",
        "recentchanges-label-minor": "Dizze feroaring is fan lytse betsjutting",
        "recentchanges-label-bot": "Dizze bewurking is troch in bot útfierd",
        "recentchanges-label-unpatrolled": "Dizze wiziging is noch net neisjoen",
-       "recentchanges-label-plusminus": "De sidegrutte is mei dit oantal bytes wizige",
+       "recentchanges-label-plusminus": "Sidegrutte is safolle bytes wizige",
        "recentchanges-legend-heading": "<strong>Leginda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}}<br />(sjoch ek de [[Special:NewPages|list mei nije siden]])",
        "recentchanges-submit": "Werjaan",
        "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",
        "upload": "Bestân oanbiede",
        "uploadbtn": "Bestân oplade",
        "reuploaddesc": "Opladen annulearje en weromgean nei it oanbiedformulier",
+       "upload-tryagain": "Bestânsbeskriuwing bywurkje",
        "uploadnologin": "Net oanmeld",
        "uploadnologintext": "Jo moatte $1 om bestannen oplade te kinnen.",
        "upload_directory_missing": "De heechlaadmap ($1) is der net en koe net oanmakke wurde troch de webserver.",
        "upload_directory_read_only": "De webserver kin net skriuwe yn de oanbiedpad ($1).",
-       "uploaderror": "Oanbiedfout",
+       "uploaderror": "Flater by it opladen",
        "uploadtext": "Brûk it formulier dat hjir folget om bestannen op te laden.\nGean nei de [[Special:FileList|bestânslist]], om earder opladen bestannen te besjen of op te sykjen. De (wer)opladen bestannen wurde ek opnommen yn it [[Special:Log/upload|oanbiedloch]], fuortsmiten bestannen yn it [[Special:Log/delete|wiskloch]].\n\nBrûk in keppeling yn ien fan 'e neikommende foarmen, en heakje in bestân oan in side ta:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Bestân.jpg]]</nowiki></code></strong> om de folsleine ferzje fan it bestân te brûken\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Bestân.png|200px|thumb|left|alt. tekst]]</nowiki></code></strong> om in ôfbylding fan 200 pixel breed te brûken, yn in ramt oan de lofter kant mei \"alt. tekst\" as beskriuwing\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:Bestân.ogg]]</nowiki></code></strong> foar it sûnder werjefte, streekrjocht ferwizen nei it bestân",
        "upload-permitted": "Talitten bestânstypen: $1.",
        "upload-preferred": "Oanwiisde bestânstypen: $1.",
        "filereuploadsummary": "Bestânsferoarings:",
        "filestatus": "Auteursrjochtensituaasje:",
        "filesource": "Boarne:",
-       "ignorewarning": "Negearje de warskôging en lis bestân dochs fêst.",
-       "ignorewarnings": "Negearje warskôgings",
+       "ignorewarning": "Warskôging negearje en bestân dochs bewarje",
+       "ignorewarnings": "Mooglike warskôgings negearje",
        "minlength1": "Bestânsnammen moatte minstens út ien teken bestean.",
        "illegalfilename": "De bestânsnamme \"$1\" befettet ûnjildige tekens.\nJou it bestân in oare namme en besykje him dan op 'e nij heech te laden.",
        "badfilename": "De ôfbyldnamme is feroare nei \"$1\".",
        "destfilename": "Bestânsnamme om op te slaan:",
        "upload-maxfilesize": "Maksimale bestânsgrutte: $1",
        "upload-description": "Bestânsbeskriuwing",
-       "upload-options": "Oplaadynstellingen",
-       "watchthisupload": "Folgje dit bestân",
+       "upload-options": "Opsjes foar it opladen",
+       "watchthisupload": "Dit bestân folgje",
        "filewasdeleted": "Der is earder in bestân mei dizze namme fuorthelle.\nRieplachtsje it $1 foar't jo him op'e nij tafoegje.",
        "filename-bad-prefix": "De namme fan it bestân dat jo oanbiede begjint mei '''\"$1\"''', dit wiist op in namme dy't automatysk troch in digitale kamera oanmakke wurdt. Feroarje de namme as jo wolle yn ien dy't in omskriuwing jout fan it bestân.",
        "filename-prefix-blacklist": " #<!-- lit dizze line exakt sa't er is --> <pre>\n# Syntax is as folget:\n#   * Alles fan in \"#\"-teken oan't de ein fan de line is in kommintaar\n#   * Elke net blanke line is a foarheaksel foar bestânsnammen sa't dy automatysk jûn wurde troch digitale kamera's\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # guon mobile tillefoanen\nIMG # algemien\nJD # Jenoptik\nMGP # Pentax\nPICT # ferskaat\n #</pre> <!-- lit dizze line exakt sa't er is -->",
        "filehist-filesize": "Bestânsgrutte",
        "filehist-comment": "Opmerkings",
        "imagelinks": "Bestânsgebrûk",
-       "linkstoimage": "Dizze {{PLURAL:$1|side is|$1 siden binne}} keppele oan it ôfbyld:",
+       "linkstoimage": "De neikommende {{PLURAL:$1|side brûkt|$1 siden brûke}} dit bestân:",
        "linkstoimage-more": "Der {{PLURAL:$2|is|binne}} mear as $1 {{PLURAL:$1|ferwizing|ferwizings}} nei dit bestân.\nDe folgjende list jout allinne de earste {{PLURAL:$1|ferwizing|$1 ferwizings}} nei dit bestân wer.\nDer is ek in [[Special:WhatLinksHere/$2|folsleine list]].",
        "nolinkstoimage": "Der binne gjin siden oan dit ôfbyld keppele.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Mear ferwizings]] nei dit bestân 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].",
        "trackingcategories-name": "Berjochtnamme",
        "mailnologin": "Gjin adres beskikber",
        "mailnologintext": "Jo moatte [[Special:UserLogin|oanmelden]] wêze, en in jildich e-postadres [[Special:Preferences|ynsteld]] hawwe, om oan oare meidoggers e-post stjoere te kinnen.",
-       "emailuser": "E-mail dizze meidogger",
+       "emailuser": "Dizze meidogger e-maile",
+       "emailuser-title-target": "Dizze {{GENDER:$1|meidogger|meidochster}} e-maile",
        "emailuser-title-notarget": "E-mail nei meidogger",
        "emailpagetext": "Jo kinne it formulier hjirûnder brûke om in e-mailberjocht nei dizze {{GENDER:$1|meidogger|meidochster}} te stjoeren.\nIt e-mailadres opjûn yn [[Special:Preferences|jo ynstellings]] wurdt sichtber as it 'Fan'-adres yn de e-mail, dat de ûntfanger jo streekrjocht antwurdzje kin.",
        "defemailsubject": "E-mail fan {{SITENAME}}-meidogger \"$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",
        "tooltip-feed-rss": "RSS-feed foar dizze side",
        "tooltip-feed-atom": "Atom-feed foar dizze side",
        "tooltip-t-contributions": "List fan bydragen troch dizze {{GENDER:$1|meidogger|meidochster}}",
-       "tooltip-t-emailuser": "Stjoer in e-mail nei dizze {{GENDER:$1|meidogger|meidochster}}",
+       "tooltip-t-emailuser": "In e-mail nei dizze {{GENDER:$1|meidogger|meidochster}} stjoere",
        "tooltip-t-upload": "Bestannen oplade",
        "tooltip-t-specialpages": "List fan alle bysûndere siden",
        "tooltip-t-print": "Ofdrukferzje fan dizze side",
        "tooltip-ca-nstab-mediawiki": "It systeemberjocht sjen litte",
        "tooltip-ca-nstab-template": "It berjocht sjen litte",
        "tooltip-ca-nstab-help": "Helpside sjen litte",
-       "tooltip-ca-nstab-category": "Kategory-side sjen litte",
+       "tooltip-ca-nstab-category": "De kategoryside sjen litte",
        "tooltip-minoredit": "Markearje dizze feroaring as fan lytse betsjutting",
        "tooltip-save": "Jo feroarings bewarje",
        "tooltip-preview": "Oerlêze foar't de side fêstlein is!",
        "file-nohires": "Gjin hegere resolúsje beskikber.",
        "svg-long-desc": "SVG-bestân, nominaal $1 × $2 pixels, bestânsgrutte: $3",
        "show-big-image": "Oarspronklik bestân",
+       "show-big-image-preview": "Grutte fan dit proefbyld: $1.",
        "show-big-image-other": "Oare {{PLURAL:$2|resolúsje|resolúsjes}}: $1.",
        "show-big-image-size": "$1 × $2 pixels",
        "newimages": "Galery mei nije ôfbylden",
-       "imagelisttext": "Dit is in list fan '''$1''' {{PLURAL:$1|bestân|bestannen}}, op $2.",
+       "imagelisttext": "Hjirûnder folget in list fan <strong>$1</strong> {{PLURAL:$1|bestân|bestannen}}, sortearre $2.",
        "newimages-summary": "Dizze bysûndere side lit de lêst opladen bestannen sjen.",
        "newimages-legend": "Filter",
        "noimages": "Neat te sjen.",
        "ilsubmit": "Sykje",
-       "bydate": "datum",
+       "bydate": "op datum",
        "sp-newimages-showfrom": "Nije bestannen besjen fan $2, $1 ôf",
        "video-dims": "$1, $2 × $3",
        "seconds-abbrev": "$1 s",
index 97dd12e..48b6019 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "suxerir cambio ó iniciar sesión",
        "easydeflate-invaliddeflate": "O contido fornecido non está debidamente comprimido",
        "unprotected-js": "Por motivos de seguridade non se pode cargar JavaScript desde páxinas non protexidas. Por favor, cree só JavaScript no espazo de nomes MediaWiki ou como subpáxina de usuario",
-       "userlogout-continue": "Se quere pechar a sesión, por favor, [$1 continúe á páxina de peche de sesión].",
-       "userlogout-sessionerror": "No se puido pechar a sesión debido a un erro de sesión. Por favor, [$1 ténteo de novo]."
+       "userlogout-continue": "Se quere pechar a sesión, por favor, [$1 continúe á páxina de peche de sesión]."
 }
index 50aa535..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": "הפסקה לא נמצאה",
        "passwordpolicies-policyflag-suggestchangeonlogin": "להציע שינוי בעת כניסה לחשבון",
        "easydeflate-invaliddeflate": "התוכן שהועבר אינו דחוס כנדרש",
        "unprotected-js": "מסיבות אבטחה, לא ניתן לטעון JavaScript מדפים שאינם מוגנים. ניתן ליצור סקריפטי JavaScript רק במרחב השם \"מדיה ויקי:\" או בדפי משנה של דף המשתמש.",
-       "userlogout-continue": "יש [$1 להמשיך לדף היציאה מהחשבון] כדי לצאת מהחשבון.",
-       "userlogout-sessionerror": "היציאה מהחשבון נכשלה בשל שגיאת אימות. יש [$1 לנסות שוב]."
+       "userlogout-continue": "האם ברצונך לצאת מהחשבון?"
 }
index 359cb7c..d7eb11f 100644 (file)
        "tooltip-summary": "Unesite kratki sažetak",
        "common.css": "/** Uređivanje ove CSS datoteke će se odraziti na sve skinove */",
        "common.js": "/* JavaScript kod na ovoj stranici će biti izvršen kod svakog suradnika pri svakom učitavanju svake stranice wikija. */",
-       "anonymous": "{{PLURAL:$1|anonimnoga suradnika/suradnice|$1 anonimna suradnika|$1 anonimnih suradnika}} projekta {{SITENAME}}",
-       "siteuser": "Suradnik $1 na projektu {{SITENAME}}",
+       "anonymous": "{{PLURAL:$1|1=anonimnoga suradnika/suradnice|$1 anonimnoga suradnika|$1 anonimna suradnika|$1 anonimnih suradnika}} projekta {{SITENAME}}",
+       "siteuser": "{{GENDER:$2|suradnik|suradnica}} $1 na projektu {{SITENAME}}",
        "anonuser": "{{SITENAME}} anonimni suradnik $1",
        "lastmodifiedatby": "Ova stranica posljednji je put uređena u $2, dana $1 a uređivao/la je $3.",
        "othercontribs": "Temelji se na radu $1.",
        "others": "drugih",
-       "siteusers": "{{PLURAL:$2|1={{GENDER:$1|suradnika|suradnice}}|suradnica i suradnika}} na projektu {{SITENAME}} $1",
+       "siteusers": "{{PLURAL:$2|1={{GENDER:$1|suradnika|suradnica}}|suradnica i suradnika}} na projektu {{SITENAME}} $1",
        "anonusers": "{{SITENAME}} {{PLURAL:$2|anonimni suradnik|anonimni suradnici}} $1",
        "creditspage": "Autori stranice",
        "nocredits": "Za ovu stranicu nema podataka o autorima.",
index bfc9303..dd290a3 100644 (file)
        "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-sessionerror": "Sikertelen kijelentkezés munkamenethiba miatt. Kérlek [$1 próbáld újra]."
+       "userlogout-continue": "Biztos ki szeretnél jelentkezni?"
 }
index 85d9246..09b4518 100644 (file)
        "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": "‎Յառաջանալ",
        "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 900e4b1..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",
        "passwordpolicies-policyflag-suggestchangeonlogin": "suggerer cambio al apertura de session",
        "easydeflate-invaliddeflate": "Le contento fornite non es correctemente comprimite",
        "unprotected-js": "Pro motivos de securitate, non es possibile cargar codice JavaScript de paginas non protegite. Crea JavaScript solmente in le spatio de nomines \"MediaWiki:\" o como un subpagina de usator.",
-       "userlogout-continue": "Si tu vole clauder le session, [$1 continua al pagina pro clauder session].",
-       "userlogout-sessionerror": "Le clausura del session ha fallite a causa de un error de session. Per favor [$1 reproba]."
+       "userlogout-continue": "Vole tu clauder le session?"
 }
index 751bcf7..9785ce2 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "sarankan penggantian ketika masuk log",
        "easydeflate-invaliddeflate": "Isi yang disediakan tidak dikempiskan secara tepat",
        "unprotected-js": "Karena alasan keamanan Javascript tidak dapat dimuat dari halaman yang tidak dilindungi. Mohon hanya buat javascript di ruangnama MediaWiki: atau sebagai subhalaman  Pengguna",
-       "userlogout-continue": "Jika Anda yakin untuk keluar log, silakan [$1 melanjutkan].",
-       "userlogout-sessionerror": "Gagal keluar log karena galat sesi. Silakan [$1 coba lagi]."
+       "userlogout-continue": "Jika Anda yakin untuk keluar log, silakan [$1 melanjutkan]."
 }
index e507bca..4ac2710 100644 (file)
@@ -16,7 +16,8 @@
                        "Macofe",
                        "Stavanger7",
                        "Fanjiayi",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "RWMuc"
                ]
        },
        "tog-underline": "Ultracatenun:",
@@ -33,6 +34,8 @@
        "tog-watchdefault": "Automaticmen vigilar págines e files, queles yo ha redactet.",
        "tog-watchmoves": "Automaticmen vigilar págines e files, queles yo move.",
        "tog-watchdeletion": "Adjunter págines e dossieres, queles yo ha deleet a mi liste de vigilantie",
+       "tog-watchuploads": "Adjute nov files yo upload al mi liste del vigilat págines",
+       "tog-watchrollback": "Adjute págines, where yo ha fat un iteration, al mi liste del vigliat págines",
        "tog-minordefault": "Marcar omni li redactiones minori per contumacie",
        "tog-previewontop": "Monstrar prevision ante de buxe de redaction",
        "tog-previewonfirst": "Monstrar prevision in prim redaction",
        "tog-watchlisthidebots": "Ocultar redactiones de machine del liste de págines vigilat",
        "tog-watchlisthideminor": "Ocultar redactiones minori del liste de págines vigilat",
        "tog-watchlisthideliu": "Ocultar redactiones de usatores registrat del liste de págines vigilat",
+       "tog-watchlistreloadautomatically": "Recharge li liste del vigliat págines automaticamen quandeunc un filter es changear (yo besona JaveScript)",
+       "tog-watchlistunwatchlinks": "Adjunte directmen invigilat/vigilat ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) por vigilat págines con modificationes (yo besona JavaScript)",
        "tog-watchlisthideanons": "Ocultar redactiones de usatores anonim del liste de págines vigilat",
        "tog-watchlisthidepatrolled": "Ocultar redactiones vigilat del liste de págines vigilat",
+       "tog-watchlisthidecategorization": "Ocultar li categorisation de págines",
        "tog-ccmeonemails": "Inviar me copies de e-mailes que yo invia por altri usatores",
        "tog-diffonly": "Ne monstrar li contenete de págine in infra del changes",
        "tog-showhiddencats": "Monstrar categories ne visibil",
        "tog-norollbackdiff": "Omisser change pos de efectuar un rollback",
        "tog-useeditwarning": "Averti me, si yo abandona un págine con ínconservat changes",
-       "tog-prefershttps": "Sempre usar un secur connection, si tui session es activ.",
+       "tog-prefershttps": "Sempre usa un secur connection, si tui session es activ.",
        "underline-always": "Sempre",
        "underline-never": "Nequande",
        "underline-default": "secun li usatori surfacie o li navigator",
index d695f7e..41e4c54 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",
        "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 f311e75..5e3bc65 100644 (file)
        "passwordpolicies-policy-passwordnotinlargeblacklist": "La password non può essere nell'elenco delle 100 000 password utilizzate più comunemente.",
        "easydeflate-invaliddeflate": "Il contenuto fornito non è compresso correttamente",
        "unprotected-js": "Per motivi di sicurezza, non è possibile caricare JavaScript da pagine non protette. Crea javascript solo nel namespace MediaWiki o come sottopagina Utente",
-       "userlogout-continue": "Se vuoi uscire [$1 vai alla pagina di logout].",
-       "userlogout-sessionerror": "Logout non riuscito per un errore nella sessione. [$1 Riprova]."
+       "userlogout-continue": "Vuoi uscire?"
 }
index 072e486..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階層まで含めた書き出し",
        "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": "YYYY-MM-DD",
        "passwordpolicies-policyflag-suggestchangeonlogin": "ログイン時に変更を提案",
        "easydeflate-invaliddeflate": "提供されたコンテンツが適切に圧縮されていません",
        "unprotected-js": "セキュリティ上の理由から、JavaScriptは保護されていないページからは読み込みできません。MediaWiki: 名前空間内、利用者下位ページのいずれかでのみjavascriptを作成してください。",
-       "userlogout-continue": "ログアウトを行いたい場合、[$1 ログアウトページから実施]してください。",
-       "userlogout-sessionerror": "セッションエラーによりログアウトに失敗しました。再度 [$1 試行して]ください。"
+       "userlogout-continue": "ログアウトしますか?"
 }
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 5efc974..1a85840 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-sessionerror": "세션 오류로 인해 로그아웃을 실패했습니다. [$1 다시 시도]해 주십시오."
+       "userlogout-continue": "로그아웃하시겠습니까?"
 }
index c275c29..853cd14 100644 (file)
        "virus-scanfailed": "De Scan huet net funktionéiert (Code $1)",
        "virus-unknownscanner": "onbekannten Antivirus:",
        "logouttext": "'''Dir sidd elo ausgeloggt.'''\n\nOpgepasst: Op verschiddene Säite kann et nach sou aus gesinn, wéi wann Dir nach ageloggt wiert, bis Dir Ärem Browser säin Tëschespäicher (cache) eidel maacht.",
+       "logout-failed": "Ausloggen ass elo net méiglech: $1",
        "cannotlogoutnow-title": "Ausloggen ass elo net méiglech",
        "cannotlogoutnow-text": "Ausloggen ass net méiglech wann Dir $1 benotzt.",
        "welcomeuser": "Wëllkomm $1!",
index fb8d8d7..3f4dc7c 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..a41b0e8 100644 (file)
        "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.",
+       "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": "سی یە کئ ھیچ آلئشتکاری د نیسئسە أنجوم نأگئرئتە د ڤیرایئشتکای شوم تیە پوٙشی بییە.",
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 9c7c014..e4b3eb3 100644 (file)
@@ -26,7 +26,8 @@
                        "逆襲的天邪鬼",
                        "Fitoschido",
                        "A2093064",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "WAN233"
                ]
        },
        "tog-underline": "以底線識鏈接:",
        "group-autoconfirmed": "自證其簿",
        "group-bot": "僕",
        "group-sysop": "有秩",
+       "group-interface-admin": "司空",
        "group-bureaucrat": "門下",
        "group-suppress": "監",
        "group-all": "(眾)",
        "group-autoconfirmed-member": "自證其簿",
        "group-bot-member": "僕",
        "group-sysop-member": "有秩",
+       "group-interface-admin-member": "{{GENDER:$1|司空}}",
        "group-bureaucrat-member": "門下",
        "group-suppress-member": "監",
        "grouppage-user": "{{ns:project}}:簿",
        "grouppage-autoconfirmed": "{{ns:project}}:自證其簿",
        "grouppage-bot": "{{ns:project}}:僕",
        "grouppage-sysop": "{{ns:project}}:有秩",
+       "grouppage-interface-admin": "{{ns:project}}:司空",
        "grouppage-bureaucrat": "{{ns:project}}:門下",
        "grouppage-suppress": "{{ns:project}}:監",
        "right-read": "閱頁",
index 2584b70..34d19c4 100644 (file)
@@ -42,7 +42,7 @@
        "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 помош]). Ако сте овде по грешка, само систнете на копчето '''назад''' во вашиот прелистувач.",
+       "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>, но немате дозвола да ја создадете страницата.",
        "protect-badnamespace-title": "Незаштитлив именски простор",
        "protect-badnamespace-text": "Страниците во овој именски простор не можат да се заштитуваат.",
        "protect-norestrictiontypes-text": "Страницава не може да се заштити бидејќи нема расположиви типови на ограничување.",
-       "protect-norestrictiontypes-title": "Ð\9dезаÑ\88Ñ\82иÑ\82ливи Ñ\81Ñ\82Ñ\80аниÑ\86и",
+       "protect-norestrictiontypes-title": "Ð\9dезаÑ\88Ñ\82иÑ\82лива Ñ\81Ñ\82Ñ\80аниÑ\86а",
        "protect-legend": "Потврдете ја заштитата",
        "protectcomment": "Причина:",
        "protectexpiry": "Истекува:",
        "protect-expiring-local": "истекува $1",
        "protect-expiry-indefinite": "бесконечно",
        "protect-cascade": "Заштити страници вклучени во оваа страница (каскадна заштита)",
-       "protect-cantedit": "Ð\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° Ð³Ð¾ Ð¿Ñ\80омениÑ\82е Ñ\81Ñ\82епеноÑ\82 Ð½Ð° Ð·Ð°Ñ\88Ñ\82иÑ\82а Ð½Ð° Ð¾Ð²Ð°Ð° Ñ\81Ñ\82Ñ\80аниÑ\86а, Ð±Ð¸Ð´ÐµÑ\98Ñ\9cи Ð½ÐµÐ¼Ð°Ñ\82е Ð´Ð¾Ð·Ð²Ð¾Ð»Ð° Ð·Ð° Ñ\82оа.",
+       "protect-cantedit": "Ð\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° Ð³Ð¾ Ð¿Ñ\80омениÑ\82е Ñ\81Ñ\82епеноÑ\82 Ð½Ð° Ð·Ð°Ñ\88Ñ\82иÑ\82а Ð½Ð° Ð¾Ð²Ð°Ð° Ñ\81Ñ\82Ñ\80аниÑ\86а, Ð±Ð¸Ð´ÐµÑ\98Ñ\9cи Ð½ÐµÐ¼Ð°Ñ\82е Ð´Ð¾Ð·Ð²Ð¾Ð»Ð° Ð´Ð° Ñ\98а Ñ\83Ñ\80едÑ\83ваÑ\82е.",
        "protect-othertime": "Друго време:",
        "protect-othertime-op": "друго време",
        "protect-existing-expiry": "Постоечки рок на истекување: $3, $2",
        "tooltip-t-specialpages": "Список на сите службени страници",
        "tooltip-t-print": "Верзија на страницава наменета за печатење",
        "tooltip-t-permalink": "Постојана врска до оваа верзија на страницата",
-       "tooltip-ca-nstab-main": "Преглед на содржината",
+       "tooltip-ca-nstab-main": "Преглед на содржинската страница",
        "tooltip-ca-nstab-user": "Преглед на корисничката страница",
        "tooltip-ca-nstab-media": "Преглед на мултимедијалната податотека",
        "tooltip-ca-nstab-special": "Ова е службена страница и затоа не можете да ја уредувате",
        "passwordpolicies-policyflag-suggestchangeonlogin": "предложи измена при најава",
        "easydeflate-invaliddeflate": "Содржината не е соодветно прочистена",
        "unprotected-js": "JavaScript не може да се вчита од незаштитени страници од безбедносни причини. Создавајте JavaScript само во именскиот простор МедијаВики: или како корисничка потстраница",
-       "userlogout-continue": "Ако сакате да се одјавите, [$1 продолжете на одјавната стрнаица].",
-       "userlogout-sessionerror": "Одјавата не успеа поради седничка грешка. [$1 Обидете се пак]."
+       "userlogout-continue": "Дали сакате да се одјавите?"
 }
index 1d0acc3..ac5d598 100644 (file)
        "passwordpolicies-policyflag-forcechange": "ലോഗിൻ മാറ്റിയിരിക്കണം",
        "passwordpolicies-policyflag-suggestchangeonlogin": "ലോഗിൻ മാറ്റാൻ നിർദ്ദേശിക്കുന്നു",
        "unprotected-js": "സുരക്ഷാകാരണങ്ങളാൽ സംരക്ഷണമില്ലാത്ത താളുകളിൽ നിന്നും ജാവാസ്ക്രിപ്റ്റ് എടുത്തുപയോഗിക്കാൻ കഴിയില്ല. ജാവാസ്ക്രിപ്റ്റ് താളുകൾ മീഡിയവിക്കി: നാമമേഖലയിലോ ഉപയോക്തൃ ഉപതാളായോ മാത്രം സൃഷ്ടിക്കുക",
-       "userlogout-continue": "താങ്കൾ പുറത്ത് കടക്കാൻ ആഗ്രഹിക്കുന്നുവെങ്കിൽ [$1 ലോഗ് ഔട്ട് താളിലേക്ക് തുടരുക].",
-       "userlogout-sessionerror": "സെഷൻ പിഴവ് ഉണ്ടായതിനാൽ ലോഗ് ഔട്ട് പരാജയപ്പെട്ടു. ദയവായി [$1 വീണ്ടും ശ്രമിക്കുക]."
+       "userlogout-continue": "താങ്കൾ പുറത്ത് കടക്കാൻ ആഗ്രഹിക്കുന്നുവെങ്കിൽ [$1 ലോഗ് ഔട്ട് താളിലേക്ക് തുടരുക]."
 }
index dadcf9f..7584b6b 100644 (file)
        "exception-nologin-text-manual": "ဤစာမျက်နှာကို ဝင်ရောက်နိုင်ရန် သို့မဟုတ် အခြားလုပ်ဆောင်ချက်များ ရရှိနိုင်ရန် ကျေးဇူးပြု၍ $1 ပါ။",
        "virus-unknownscanner": "အမည်မသိအန်တီဗိုင်းရပ်စ် -",
        "logouttext": "<strong>သင်သည် လော့ဂ်အောက် လုပ်လိုက်ပြီဖြစ်သည်။</strong>",
+       "logging-out-notify": "အကောင့်မှ ထွက်နေပါသည်၊ ခေတ္တခဏ စောင့်ဆိုင်းပါ။",
        "cannotlogoutnow-title": "လော့ဂ်အောက်ထွက်၍ မရနိုင်သေးပါ",
        "cannotlogoutnow-text": "$1 ကိုအသုံးပြုနေစဉ်အတွင်း အကောင့်ထွက်ရန် မဖြစ်နိုင်ပါ။",
        "welcomeuser": "ကြိုဆိုပါတယ် $1!",
        "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": "အချိန်ဇုန် -",
        "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": "စောင့်ကြည့်စာရင်း",
        "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": "အကွာအဝေးလိုက် ပိတ်ပင်မှုများ ဝှက်",
        "passwordpolicies-group": "အုပ်စု",
        "passwordpolicies-policies": "မူဝါဒများ",
        "passwordpolicies-policy-minimalpasswordlength": "စကားဝှက်တွင် အနည်းဆုံး {{PLURAL:$1|စကားလုံး|စကားလုံးများ}} $1 ခုရှိရမည်။",
-       "passwordpolicies-policy-passwordcannotmatchusername": "စကားဝှက်သည် အသုံးပြုသူအမည်နှင့် မတူညီရပါ"
+       "passwordpolicies-policy-passwordcannotmatchusername": "စကားဝှက်သည် အသုံးပြုသူအမည်နှင့် မတူညီရပါ",
+       "userlogout-continue": "အကောင့်မှ ထွက်လိုပါသလား"
 }
index c97fb36..3c8f618 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "foreslå endring ved innlogging",
        "easydeflate-invaliddeflate": "Det gitte innholdet er ikke riktig komprimert",
        "unprotected-js": "Av sikkerhetsårsaker kan ikke JavaScript lastes fra ubeskyttede sider. Bare skap JavaScript i MediaWiki-navnerommet eller som en brukerunderside",
-       "userlogout-continue": "Hvis du ønsker å logge ut, [$1 fortsett til utloggingssiden].",
-       "userlogout-sessionerror": "Utlogging mislyktes på grunn av en øktfeil. [$1 Prøv igjen]."
+       "userlogout-continue": "Hvis du ønsker å logge ut, [$1 fortsett til utloggingssiden]."
 }
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 be6185a..09d06dd 100644 (file)
        "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",
        "passwordpolicies-policyflag-forcechange": "moet gewijzigd worden bij het aanmelden",
        "passwordpolicies-policyflag-suggestchangeonlogin": "raad wijzigen aan bij het aanmelden",
        "unprotected-js": "Vanwege veiligheidsredenen kan er geen JavaScript geladen worden vanaf onbeveiligde pagina's. Gelieve alleen JavaScript pagina's aan te maken in de MediaWiki: naamruimte of als een subpagina van een gebruikerspagina.",
-       "userlogout-continue": "Als u zich wilt afmelden, [$1 gaat u naar de afmeldpagina].",
-       "userlogout-sessionerror": "Afmelden is mislukt vanwege een fout met de sessie. [$1 Probeer het opnieuw]."
+       "userlogout-continue": "Wilt u zich afmelden?"
 }
index c5d67e2..446174f 100644 (file)
        "revid": "versjon $1",
        "interfaceadmin-info": "$1\n\nLøyva for endring av CSS/JS/JSON-filer som gjeld heile nettstaden vart nyleg skilde ut frå <code>editinterface</code>-retten. Om du ikkje skjøner kvifor du får denne feilmeldinga, sjå [[mw:MediaWiki_1.32/interface-admin]].",
        "passwordpolicies-policy-passwordcannotmatchusername": "Passordet kan ikkje vera det same som brukarnamnet",
-       "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord",
-       "userlogout-sessionerror": "Utlogging gjekk ikkje grunna ein øktfeil. [$1 Freist om att]."
+       "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord"
 }
index 618f18d..e9d817b 100644 (file)
        "printableversion": "ߓߐߞߏߣߊ߲߫ ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ",
        "permalink": "ߛߘߌ߬ߜߋ߲߬ ߓߟߏߕߍ߰ߓߊߟߌ",
        "print": "ߜߌ߬ߙߌ߲߬ߘߌ߬ߟߌ",
-       "view": "ß\8a߬ ß\98ß\90ß\9eß\8a߬ß\99ß\8a߲߬",
+       "view": "ߦß\8c߬ß\98ß\8a߬ß\9fß\8c",
        "view-foreign": "ߊ߬ ߦߋ߫ ߦߊ߲߬ $1",
        "edit": "ߊ߬ ߢߟߊߞߎߘߦߊ߫",
        "edit-local": "ߕߌ߲߬ߞߎߘߎ߲ ߞߊ߲߬ߛߓߍߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
        "invalidtitle": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ",
        "exception-nologin": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫",
        "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-created-title": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߘߊ߫ ߛߌ߲ߘߌ߫",
+       "botpasswords-created-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫  \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߓߘߊ߫ ߛߌ߲ߘߌ߫.",
+       "botpasswords-updated-title": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫",
+       "botpasswords-updated-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫.",
+       "botpasswords-deleted-title": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߘߊ߫ ߖߏ߬ߛߌ߬",
+       "botpasswords-deleted-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.",
        "resetpass_forbidden": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߘߋ߲߬ ߠߊ߫.",
        "resetpass_forbidden-reason": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߟߋ߲߬ ߠߊ߫: $1",
        "resetpass-no-info": "ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߬ ߡߎߣߎ߲߬ ߞߣߊ߬ ߕߏ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫.",
        "changeemail-nochange": "ߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ߫ ߜߘߍ߫ ߟߊߘߏ߲߬.",
        "resettokens": "ߖߐߟߐ߲ߞߐ ߡߊߝߊ߬ߟߋ߲߬",
        "resettokens-text": "ߌ ߦߋ߫ ߖߐߟߐ߲ߞߐ ߡߊߝߊ߬ߟߋ߲߬ ߡߍ߲ ߦߋ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߓߟߏߡߟߊ ߘߏ߫ ߟߎ߫ ߡߊߛߐ߬ߘߐ߲ ߠߊߘߤߊ߬ ߟߴߌ ߦߋ߫߸ ߡߍ߲ ߠߎ߬ ߛߘߌ߬ߣߍ߲߬ ߦߴߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊ߫ ߦߊ߲߬.\n\nߌ ߞߊ߫ ߞߊ߲߫ ߞߵߏ߬ ߞߍ߫ ߣߴߌ ߣߐ߬ ߞߍ߫ ߘߴߊ߬ߟߎ߬ ߟߊߖߍ߲ߛߍ߲߫ ߠߊ߫ ߡߐ߱ ߘߏ߫ ߡߊ߬ ߓߌ߬ߟߊ߬ߒߘߐ߬ ߓߟߏߡߊ߬߸ ߥߟߊ߫ ߣߴߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߢߊߓߐߣߍ߲߫ ߞߍ߫ ߘߊ߫.",
+       "resettokens-no-tokens": "ߖߐߟߐ߲ߞߐ߫ ߡߊߝߊ߬ߟߋ߲߬ߕߊ߬ ߛߌ߫ ߕߍ߫ ߦߋ߲߬.",
+       "resettokens-tokens": "ߖߐߟߐ߲ߞߐ",
+       "resettokens-token-label": "$1 (ߛߋ߲߬ߠߊ߫ ߡߐߟߐ߲:$2)",
+       "resettokens-watchlist-token": "ߓߟߐߟߐ ߓߊߟߏ߫ ߖߐߟߐ߲ߞߐ (Atom/RSS) ߞߊ߬ ߓߍ߲߬ [[Special:Watchlist|changes to pages on your watchlist]] ߡߊ߬.",
+       "resettokens-done": "ߖߐߟߐ߲ߞߐ ߡߊߝߊ߬ߟߋ߲߬",
+       "resettokens-resetbutton": "ߖߐߟߐ߲ߞߐ߫ ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߡߊߝߊ߬ߟߋ߲߬",
        "bold_sample": "ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ",
        "bold_tip": "ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ",
        "italic_sample": "ߛߓߍߟߌ߫ ߡߊߖߍ߲߬ߞߍ߬ߣߍ߲",
        "sig_tip": "ߌ ߟߊ߫ ߞߟߊ߬ߣߐ ߕߎ߬ߡߊ߬ߘߊ ߓߊ߬ߘߌ߬ߟߊ߲߬ߡߊ",
        "hr_tip": "ߛߌ߬ߕߊ߬ߙߌ߬ ߢߊߡߌߟߏߡߊ (ߊ߬ ߕߐ߬ߝߍ߬ߦߊ߬ ߟߊߓߊ߯ߙߊ߫)",
        "summary": "ߟߊ߬ߘߛߏ߬ߟߌ:",
+       "subject": "ߝߐߡߊ",
        "minoredit": "ߣߌ߲߬ ߦߋ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߘߏ߫ ߟߋ߬ ߘߌ߫",
        "watchthis": "ߞߐߜߍ ߣߌ߲߬ ߘߐߜߍ߫",
        "savearticle": "ߞߐߜߍ ߟߊߞߎ߲߬ߘߎ߬",
        "blankarticle": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</strong> ߌ ߦߋ߫ ߞߐߜߍ ߡߍ߲ ߛߌ߲ߘߌ ߞߊ߲߬ ߣߌ߲߬߸ ߊ߬ ߘߐߞߏߟߏ߲ ߠߋ߬.\nߣߴߌ ߞߊ߬  \"$1\" ߛߐ߲߬ߞߌ߲߫ ߡߎ߬ߕߎ߲߬߸ ߞߐߜߍ ߘߌ߫ ߛߌ߲ߘߌ߫ ߞߵߊ߬ ߕߘߍ߬ ߞߣߐߘߐ ߛߎ߯-ߎ߯-ߛߎ߫ ߕߴߊ߬ ߞߣߐ߫.",
        "anoneditwarning": "<strong>Warning:</strong> ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫.ߌ ߓߊ߯ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߛߎ߯-ߎ߯-ߛߎ߫ ߞߍ߫߸ ߌ ߟߊ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߘߌ߫ ߞߍ߫ ߦߋߕߊ ߘߌ߫.ߣߴߌ ߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲߬ ߖߐ߲ߖߐ߲ ߞߍ߫ ߕߎ߬ߡߊ ߡߍ߲ <strong>[$1 log in]</strong> or <strong>[$2 create an account]</strong> ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߣߍ߲ ߠߎ߬ ߘߌ߫ ߓߌ߬ߟߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬ ߕߐ߮ ߟߊ߫߸ ߊ߬ ߣߌ߫ ߣߝߊ߬ ߜߘߍ߫ ߟߎ߫.",
        "anonpreviewwarning": "<em>ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫. ߟߊ߬ߞߎ߲߬ߘߎ߬ߟߌ ߘߴߌ ߟߊ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߟߊߡߙߊ߬ ߞߐߜߍ ߣߌ߲߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߬ߝߐ ߘߐ߫</em>",
+       "missingsummary": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</strong> ߌ ߡߊ߫ ߟߊ߬ߘߛߏ߬ߟߌ߬ ߡߊߦߟߍ߬ߡߊ߲߬ߣߍ߲߬ ߛߌ߫ ߡߡߊߛߐ߫.ߣߴߌ ߞߊ߬  \"$1\" ߛߐ߲߬ߞߌ߲߫ ߏ߬ ߞߐ߫߸ ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߌ߫ ߟߊߞߎ߲߬ߘߎ߬ ߞߵߊ߬ ߕߘߍ߬ ߝߏߦߌ߬ ߕߴߊ߬ ߘߐ߫.",
+       "missingcommenttext": "ߞߊ߲߬ߞߎߡߊ ߘߏ߫ ߟߊߘߏ߲߬ ߖߊ߰ߣߌ߲߬.",
+       "missingcommentheader": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ:</strong> ߌ ߡߊ߫ ߝߐߡߊ߫ ߛߌ߫ ߡߊߛߐ߫ ߞߊ߲߬ߞߎߡߊ ߣߌ߲߬ ߘߐ߫.ߣߴߌ ߞߊ߬ \"$1\" ߛߐ߲߬ߞߌ߲߬ ߕߎ߲߯ߣߌ߲߫߸ ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߌ߫ ߟߊߞߎ߲߬ߘߎ߫ ߞߵߊ߬ ߕߘߍ߬ ߝߋ߲߫ ߕߴߊ߬ ߞߣߐ߫.",
+       "summary-preview": "ߟߊ߬ߘߛߏ߬ߟߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߢߍߦߋߟߌ:",
+       "subject-preview": "ߝߐߡߊ ߢߍߦߋߟߌ:",
+       "blockedtitle": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߓߊ߬ߟߊ߲߬ߣߍ߲߫ ߠߋ߬",
        "blockedtext": "<strong>ߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߥߟߊ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߓߘߊ߫ ߓߊ߬ߟߊ߲߬߸</strong>\n\nߌ ߓߊ߬ߟߊ߲߬ߣߍ߲߬ ߦߋ߫ $1 ߟߋ߬ ߓߟߏ߫.\nߞߎ߲߭ ߡߍ߲ ߦߴߊ߬ ߟ߫ߊ߫ <em>$2</em>.\n\n•ߓߊ߬ߟߊ߲߬ߠߌ߲ ߘߊߡߌ߬ߣߊ: $8\n•ߓߊ߬ߟߊ߲߬ߠߌ߲ ߛߕߊ ߝߊ: $6\n•ߓߊ߬ߟߊ߲߬ߠߌ߲ ߘߊ߬ߟߎ: $7 \n\nߌ ߘߌ߫ ߛߋ߫ ߗߋߛߓߍ ߗߋ߫ ߟߊ߫ $1 ߡߊ߬ ߥߟߊ߫ ߡߐ߰ ߜߘߍ߫ \n[[{{MediaWiki:Grouppage-sysop}}|administrator]] ߞߊ߬ ߘߊߘߐߖߊߥߏ ߞߍ߫ ߓߊ߬ߟߊ߲߬ߠߌ߲ ߞߊ߲߬.\nߌ ߕߍ߫ ߣߊ߬ ߛߋ߫ ߟߊ߫  \"{{int:emailuser}}\" ߟߊߓߊ߯ߙߊ߫ ߟߊ߫߸ ߟߊ߬ߓߊ߰ߙߊ߬ߢߊ߬ ߖߐ߲ߖߐ߲ ߡߍ߲ ߦߋ߫ ߦߋ߲߬߸ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ߫ ߖߐ߲ߖߐ߲߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߦߴߌ ߟߊ߫ [[Special:Preferences|account preferences]] ߘߐ߫߸ ߊ߬ ߣߴߌ ߡߊ߫ ߓߊ߬ߟߊ߲߬ ߊ߬ ߟߊߓߊ߯ߙߊ ߞߏߛߐ߲߬ ߘߋ߫. ߌ ߟߊ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߦߋ߫ $3 ߟߋ߬ ߘߌ߫ ߕߊ߲߬߸ ߊ߬ ߣߴߌ ߟߊ߫ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߓߊ߬ߟߊ߲߬ߣߍ߲ ߦߋ߫ #$5 ߟߋ߬ ߘߌ߫.\nߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߛߊ߲ߝߍ߫ ߝߊߙߊ߲ߝߊ߯ߛߌ ߣߌ߲߬ ߓߍ߯ ߟߊߘߏ߲߬ ߌ ߟߊ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߘߐ߫.",
+       "blockednoreason": "ߊ߬ ߞߎ߲߬ ߛߌ߫ ߡߊ߫ ߝߐ߫",
        "whitelistedittext": "ߖߊ߰ߣߌ߲߫ $1 ߞߊ߬ ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.",
        "confirmedittext": "ߌ ߦߴߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߟߊߛߙߋߦߊ߫߸ ߦߊ߲߬ߣߴߌ ߦߋ߫ ߞߐߜߍ ߡߊ߬ߦߟߍ߬ߡߊ߲߬.\nߖߊ߰ߣߌ߲߬ ߌ ߦߴߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߟߊߛߙߋߦߊ߫ ߌ ߟߊ߫   [[Special:Preferences|user preferences]] ߘߐ߫.",
+       "nosuchsectiontitle": "ߊ߬ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߛߌ߰ߘߊ ߛߐ߬ߘߐ߲߬ ߠߊ߫",
+       "nosuchsectiontext": "ߌ ߦߋ߫ ߛߌ߰ߘߊ ߘߏ߫ ߟߋ߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߞߏ ߘߐ߫ ߣߌ߲߬߸ ߡߍ߲ ߕߍ߫ ߦߋ߲߬.\nߊ߬ ߊ߬ ߛߋ߲߬ߓߐ߬ߣߍ߲߬ ߘߌ߫ ߞߍ߫ ߥߟߴߊ߬ ߖߏ߬ߛߌ߬ߣߍ߲߬ ߘߌ߫ ߞߍ߫ ߞߐߜߍ ߣߌ߲߬ ߦߋߟߌ߫ ߕߎߡߊ ߟߊ߫.",
        "loginreqtitle": "ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߞߊ߬ߣߌ߲߬ ߣߍ߲߫",
        "loginreqlink": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "loginreqpagetext": "ߖߊ߰ߣߌ߲߫ $1 ߛߊ߫ ߞߐߜߍ ߕߐ߭ ߟߎ߬ ߘߌ߫ ߦߋ߫.",
        "userpage-userdoesnotexist": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ \"$1\" ߛߌ߲ߘߌߣߍ߲߫ ߕߍ߫. \nߝߛߍ߬ߝߛߍ߬ߟߌ ߞߍ߫߸ ߣߴߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߞߐߜߍ ߣߌ߲߬ ߛߌ߲ߘߌ߫/ߡߊߦߟߍ߬ߡߊ߲߫.",
        "userpage-userdoesnotexist-view": "ߟߊ߬ߓߊ߰ߙߊ߬ ߖߊߕߋߘߊ \"$1\" ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߫ ߕߍ߫.",
        "clearyourcache": "<strong>ߖߊ߲߬ߕߏ߬ߒߘߐ:</strong> ߞߎ߲߬ߘߎ߬ߟߌ ߞߐ߫، ߌ ߓߍߣߊ߬ ߢߌߣߌ߲߫ ߌ ߟߊ߫ ߓߟߐߟߐߞߐߜߍ ߞߙߏ ߘߐߞߊ߭ ߡߊ߬ ߞߊ߬ ߡߝߊ߬ߟߋ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫. * <strong>ߝߦߊߝߐߞߛ / ߛߝߊߙߌ:</strong> ߊ߬ ߡߌ߬ߣߊ߬ <em>Shift</em> ߘߌ߯ߟߌ߫ ߕߎߡߊ <em>Reload</em>، ߥߟߊ߫ ߞߵߊ߬ ߛߐ߲߬ߞߌ߲߫ ߥߟߊ߫ <em>Ctrl-F5</em> ߤߊߡߊ߲߫ <em>Ctrl-R</em> (<em>⌘-R</em> ߡߊߞߌ ߞߊ߲߬) * <strong>ߜ߭ߎߜ߭ߐߟ ߞߊ߲߬:</strong> ߊ߬ ߛߐ߲߬ߞߌ߲߫ <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> ߡߊߞߌ ߞߊ߲߬) * <strong>ߍ߲ߕߍߙߑߣߍߕ ߍߞߛߌߔߟߏߙߊ ߞߊ߲߬:</strong> ߊ߬ ߡߌ߬ߣߊ߬ <em>Ctrl</em> ߊ߬ ߛߐ߲߬ߞߌ߲߬ ߕߎߡߊ <em>Refresh</em>، ߥߟߊ߫ ߞߵߊ߬ ߛߐ߲߬ߞߌ߲߫ <em>Ctrl-F5</em> * <strong>ߏߔߋߙߊ:</strong> ߕߊ߯ ߞߊߕߙߍ߬ <em>Menu → ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߦߊ߬ߘߊ</em> (<em>Opera → ߞߐߡߊߛߙߋ</em> ߡߊߞߌ ߟߊ߫) ߞߊ߬ ߕߊ߯ ߏ߬ ߞߐ߫ <em>ߘߎ߲߬ߘߎ߬ߡߊ߬ & ߞߎ߲߬ߠߊ߬ߝߎߟߋ߲ → ߓߟߐߟߞߐߜߍߦߊ ߟߐ߲ߕߊ ߟߎ߫ ߖߏ߬ߛߌ߫ → ߖߌ߬ߦߊ߬ߓߍ ߟߎ߬ ߣߌ߫ ߞߐߕߐ߯ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬</em>.",
+       "updated": "(ߊ߬ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫)",
+       "note": "<strong>ߦߟߌߣߐ:</strong>",
        "previewnote": "<strong>ߌ ߖߊ߲߬ߓߌ߬ߟߊ߬ ߞߏ߫ ߣߌ߲߬ ߦߋ߫ ߢߍߝߟߍߟߌ ߘߐߙߐ߲߫ ߠߋ߬ ߘߌ߫.</strong>\nߌ ߟߊ߫ ߡߝߊ߬ߟߋ߲߬ߠߌ ߟߎ߫ ߡߊ߫ ߟߊߞߎ߲߬ߘߎ߬ ߝߟߐ߫ ߘߋ߬߹",
        "continue-editing": "ߥߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߣߍ ߞߊ߲߬",
        "editing": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ $1",
        "creating": "$1 ߛߌ߲ߘߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫",
        "editingsection": "(ߛߌ߰ߘߊ߬) $1 ߡߊߦߟߍ߬ߡߊ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫",
+       "editingcomment": "(ߛߌ߰ߘߊ߬ ߞߎߘߊ߫) ߡߊߦߟߍ߬ߡߊ߲ ߦߴߌ ߘߐ߫ $1",
+       "editconflict": "ߝߐߢߐ߲߯ߞߐ ߡߊߦߟߍ߬ߡߊ߲߬: $1",
+       "yourtext": "ߌ ߟߊ߫ ߛߓߍߟߌ",
        "templatesused": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߫}} ߟߎ߫ ߟߊߓߊ߯ߙߊ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫",
        "templatesusedpreview": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߬}} ߟߋ߬ ߟߊߓߊ߯ߙߊ߫ ߣߍ߲߫ ߢߍߦߋߟߌ ߣߌ߲߬ ߘߐ߫",
        "template-protected": "(ߊ߬ ߡߊߞߊ߲ߞߊ߲ߣߍ߲߫ ߠߋ߬)",
        "permissionserrorstext-withaction": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ߬ ߛߌ߫ ߕߴߌ ߦߋ߫ ߞߊ߬ $2߸ {{PLURAL:$1|ߞߏߛߐ߲߬|ߟߎ߬ ߞߏߛߐ߲߬}}",
        "recreate-moveddeleted-warn": "<strong>ߌ ߖߊ߲߬ߕߏ߫: ߌ ߦߋ߫ ߞߐߜߍ ߘߏ߫ ߟߋ߬ ߟߊߘߊ߲߫ ߞߏ ߘߐ߫ ߣߌ߲߬߸ ߡߍ߲ ߖߏ߬ߛߌ߬ߣߍ߲߬ ߡߎߣߎ߲߬.</strong> \nߌ ߓߛߌ߬ߞߌ߬ ߕߐ߫ ߟߋ߬ ߛߍ߲߸ ߣߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߓߊ߲߫ ߠߊ߫. \nߞߐߜߍ ߣߌ߲߬ ߦߟߌߣߐ ߖߏ߬ߛߌ߬ߣߍ߲ ߣߴߊ߬ ߛߋ߲߬ߓߐ߬ߣߍ߲ ߠߎ߬ ߡߊߘߊ߲ߣߍ߲߫ ߦߊ߲߬ ߠߋ ߟߊ߬ߣߐ߰ߦߊ߬ߟߌ ߘߌ߫:",
        "moveddeleted-notice": "ߞߐߜߍ ߣߌ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.\nߖߏ߬ߛߌ߬ߟߌ߸ ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ߸ ߊ߬ ߣߌ߫ ߞߐߜߍ ߛߓߍߟߌ ߟߎ߬ ߛߋ߲߬ߓߐ߸ ߏ߬ ߟߎ߫ ߓߍ߯ ߡߊߛߐߣߍ߲߫ ߦߋ߫ ߘߎ߰ߟߊ ߘߐ߫.",
+       "log-fulllog": "ߘߎ߲ߛߓߍ ߘߝߊߣߍ߲ ߦߋ߫",
+       "edit-conflict": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߝߐߢߐ߲߯ߞߐ.",
+       "edit-no-change": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߎ߲߬ ߓߘߊ߫ ߡߊߓߌ߬ߟߊ߬߸ ߓߊߏ߬ ߡߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߛߌ߫ ߕߎ߲߬ ߡߊ߫ ߞߍ߫ ߛߓߍߟߌ ߘߐ߫.",
        "postedit-confirmation-created": "ߞߐߜߍ ߓߘߊ߫ ߓߊ߲߫ ߛߌ߲ߘߌ߫ ߟߊ߫.",
        "postedit-confirmation-restored": "ߞߐߜߍ ߓߘߊ߫ ߓߊ߲߫ ߘߐߓߍ߲߬ ߠߊ߫.",
        "postedit-confirmation-saved": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߞߎ߲߬ߘߎ߬.",
        "postedit-confirmation-published": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߥߊ߲߬ߞߊ߫.",
+       "edit-already-exists": "ߌ ߕߴߛߋ߫ ߞߐߜߍ߫ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫ ߟߊ߫.\nߊ߬ ߦߋ߫ ߦߋ߲߬ ߞߘߐ߬ߡߊ߲߬.",
+       "invalid-content-data": "ߞߣߐߘߐ ߓߟߏߡߟߊ ߓߍ߲߬ߓߊߟߌ",
+       "content-not-allowed-here": "\"$1\" ߞߣߐߘߐ ߟߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ ߞߐߜߍ ߘߐ߫ [[:$2]] ߛߍ߲ߞߍߘߊ ߘߐ߫  \"$3\"",
+       "editwarning-warning": "ߣߴߌ ߓߐ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߌ ߘߌ߫ ߓߣߐ߬ ߌ ߟߊ߫ ߡߊ߬ߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߞߍߣߍ߲ ߠߎ߬ ߓߍ߯ ߘߐ߫.\nߣߴߌ ߘߏ߲߬ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߞߍ߫ ߘߊ߫߸ ߌ ߘߌ߫ ߛߋ߫ ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ ߣߌ߲߬ ߓߐ߫ ߟߴߊ߬ ߟߊ߫  \"{{int:prefs-editing}}\" ߘߐ߫߸ ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߥߟߊ߬ߘߊ ߘߐ߫.",
        "editpage-invalidcontentmodel-title": "ߞߣߐߘߐ ߛߎ߯ߦߊ ߞߐߡߊߓߌ߲ߓߌ߲߫ ߣߍ߲߫ ߕߍ߫",
        "editpage-invalidcontentmodel-text": "ߞߣߐߘߐ ߛߎ߯ߦߊ  \"$1\" ߞߐߡߊߓߌ߲ߓߌ߲߫ ߣߍ߲߫ ߕߍ߫.",
        "editpage-notsupportedcontentformat-title": "ߞߣߐߘߐ ߢߊ߲ߞߊ߲ ߞߐߡߊߓߌ߲ߓߌ߲߫ ߣߍ߲߫ ߕߍ߫.",
        "deprecated-self-close-category": "ߞߐߜߍ ߣߌ߲߬ ߦߋ߫ ߖߘߍ߬-ߘߊ߬ߕߎ߲߰ HTML ߟߊߓߊ߯ߙߊ߫ ߟߊ߫.",
        "deprecated-self-close-category-desc": "ߖߘߍ߬-ߘߊ߬ߕߎ߲߰ HTML ߘߎ߲ߛߓߍ߫ ߓߍ߲߬ߓߊߟߌ ߟߋ߬ ߦߋ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫߸ ߦߏ߫ <code>&lt;b/></code>ߥߟߊ߫<code>&lt;span/></code>.ߏ߬ ߢߝߍߕߊ߯ߟߌ ߘߌ߫ ߣߊ߬ ߦߟߍ߬ߡߊ߲߬ ߖߏߣߊ߫ ߞߊ߬ ߞߍ߫ HTLM5 ߘߎ߲߬ߘߎ߬ߡߦߊ߬ߟߌ ߘߌ߫߸ ߏ߬ ߘߐ߫ ߊ߬ߟߎ߬ ߥߞߌ߫ ߛߓߍߟߌ ߟߊߜߎ߬ߝߎ߲߬ߣߍ߲ ߠߋ߬.",
        "duplicate-args-warning": "<strong>ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ</strong> [[:$1]] ߦߋ߫ ߞߟߌ߫ ߟߊ߫ [[:$2]] ߞߊ߬ ߕߊ߬ߡߌ߲߬ ߡߐ߬ߟߐ߲߬ ߞߋߟߋ߲߫ ߠߊ߫  \"$3\" ߘߊߘߐߓߍ߲ߠߌ߲ ߘߐ߫.ߡߐ߬ߟߐ߲߬ ߡߊߛߐߣߍ߲ ߞߐߟߕߊ ߘߐߙߐ߲߫ ߠߋ߬ ߟߊߓߊ߯ߙߊ߫ ߕߐ߫.",
+       "duplicate-args-category": "ߞߐߜߍ ߦߋ߫ ߘߊߘߐߡߌߘߊߞߎ߲ߢߊ߫ ߓߊߟߌߣߍ߲ ߠߎ߬ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫ ߞߙߊߞߏ ߞߟߌߟߌ ߘߐ߫",
+       "duplicate-args-category-desc": "ߞߙߊߞߏ ߞߟߌߟߌ ߟߎ߬ ߦߋ߫ ߞߐߜߍ ߘߐ߫߸ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߘߊߘߐߡߌߘߊߞߎ߲ߢߊ߫ ߓߊߟߌߣߍ߲ ߠߎ߬ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫߸ ߦߏ߫ <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 ߘߎ߰ߟߊ߫ \n{{PLURAL:$2|ߞߟߌߟߌ|ߞߟߌߟߌ ߟߎ߬}}߸ ߘߌ߫ ߞߍ߫ {{PLURAL:$1|ߦߋ߫ ߞߟߌߟߌ $1 ߟߋ߬ ߘߌ߫ ߡߎ߬ߕߎ߲߬|ߦߋ߫ ߞߟߌߟߌ ߟߎ߬ $1 ߟߋ߬ ߘߌ߫ ߡߎ߬ߕߎ߲߬}}.",
        "undo-failure": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߍ߫ ߣߊ߬ ߛߋ߫ ߟߊ߫ ߘߐߛߊ߬ ߟߊ߫߸ ߝߘߏ߬ߒ߬ߡߊ߬ߟߌ߬ ߡߊߦߟߍߡߊ߲ߠߌ߲ ߞߏߛߐ߲߬.",
        "viewpagelogs": "ߞߐߜߍ ߣߌ߲߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫",
+       "nohistory": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߬ߝߐ߬ ߛߌ߫ ߕߍ߫ ߞߐߜߍ ߣߌ߲߬ ߠߊ߫",
        "currentrev-asof": "$1 ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲",
        "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫ 1$",
        "revision-info": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߣߍ߲߫ $1 ߟߋ߬ ߓߟߏ߫ {{GENDER:$6|$2}}$7",
        "history-fieldset-title": "ߣߐ߬ߡߊ߬ߛߊߦߌ߲ ߠߎ߬ ߛߍ߲ߛߍ߲߫",
        "histfirst": "ߞߘߐ߬ߡߊ߲ ߠߎ߬",
        "histlast": "ߞߎߘߊ ߟߎ߬",
+       "historysize": "{{PLURAL:$1|ߝߙߐ߬ߢߐ|$1 ߝߙߐ߬ߢߐ ߟߎ߬}}",
+       "historyempty": "ߘߐߞߏߟߏ߲",
        "history-feed-title": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ",
        "history-feed-description": "ߞߐߜߍ ߣߌ߲߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߬ߝߐ߸ ߥߞߌ ߘߐ߫",
+       "history-feed-item-nocomment": "$1 $2 ߟߊ߫",
+       "history-feed-empty": "ߞߐߜߍ߫ ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲ ߕߍ߫ ߦߋ߲߬ ߏ߬ ߞߐ߫.\nߊ߬ ߖߏ߬ߛߌ߬ߣߍ߲߬ ߘߌ߫ ߞߍ߫ ߥߞߌ ߘߐ߫ ߥߟߴߊ߬ ߕߐ߮ ߓߘߊ߫ ߦߟߍ߬ߡߊ߲߬.\nߊ߬ ߡߊߝߍߣߍ߲߫ [[Special:Search|searching on the wiki]] ߘߐ߫߸ ߞߐߜߍ߫ ߞߎߘߊ߫ ߟߎ߫ ߟߊ߫ ߞߏ ߘߐ߫.",
+       "history-edit-tags": "ߡߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߠߎ߬ ߞߏ߲߭ ߡߊߝߊ߬ߟߋ߲߬",
+       "rev-deleted-comment": "(ߟߊ߬ߘߛߏ߬ߟߌ ߛߋ߲߬ߓߐߣߍ߲ ߡߊߦߟߍ߬ߡߊ߲߫)",
+       "rev-deleted-user": "(ߟߊ߬ߓߊ߰ߙߊ߬ߕߐ߮ ߓߘߊ߫ ߛߋ߲߬ߓߐ߫)",
+       "rev-deleted-event": "(ߘߎ߲ߛߓߍ ߝߊߙߊ߲ߝߊ߯ߛߌ ߓߘߊ߫ ߛߋ߲߬ߓߐ߫)",
        "rev-delundel": "ߊ߬ ߦߋߢߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "rev-showdeleted": "ߦߌ߬ߘߊ߬ߟߌ",
+       "revisiondelete": "ߛߌ߰ߘߊ ߖߏ߬ߛߌ߬/ߖߏ߬ߛߌ߬ߣߍ߲ ߓߐ߫",
        "revdelete-show-file-submit": "ߐ߲߬ߐ߲߬ߐ߲߫",
+       "revdelete-legend": "ߦߋߟߌ ߟߊ߬ߘߐ߰ߦߊ߬ߟߌ ߟߊߘߏ߲߬",
+       "revdelete-hide-text": "ߛߓߍߟߌ ߟߊߢߊ߬",
+       "revdelete-hide-image": "ߞߐߕߐ߮ ߞߣߐߘߐ ߢߡߊߘߏ߲߰",
+       "revdelete-hide-name": "ߞߏ߲߭ ߣߌ߫ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߢߡߊߘߏ߲߰",
+       "revdelete-hide-comment": "ߟߊ߬ߘߛߏ߬ߣߍ߲ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "revdelete-hide-user": "ߛߓߍߦߟߊ ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮/IP ߛߊ߲߬ߓߊ߬ߕߐ߮",
+       "revdelete-hide-restricted": "ߓߟߏߡߟߊ ߖߏ߬ߛߌ߫ ߡߊ߬ߡߙߊ߬ߟߌ߬ߟߊ ߞߎ߲߬߸ ߊ߬ ߣߌ߫ ߘߏ ߟߎ߬ ߝߣߊ߫ ߞߎ߲߬.",
+       "revdelete-radio-same": "(ߌ ߞߊߣߵߊ߬ ߡߊߝߊ߬ߟߋ߲߬)",
+       "revdelete-radio-set": "ߢߡߊߘߏ߲߯ߠߌ߲ ߦߴߌ ߘߐ߫",
+       "revdelete-radio-unset": "ߦߋߕߊ",
+       "revdelete-suppress": "ߓߟߏߡߟߊ ߖߏ߬ߛߌ߫ ߡߊ߬ߡߙߊ߬ߟߌ߬ߟߊ ߞߎ߲߬߸ ߊ߬ ߣߌ߫ ߘߏ ߟߎ߬ ߝߣߊ߫ ߞߎ߲߬",
+       "revdelete-unsuppress": "ߟߊ߬ߘߐ߰ߦߊ߬ߟߌ ߛߋ߲߬ߓߐ߫ ߞߐߜߍ߫ ߟߢߊ߬ߟߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߫",
+       "revdelete-log": "ߊ߬ ߛߊߓߎ",
+       "revdelete-submit": "ߊ߬ ߟߊߓߏ߬ߙߌ߬ {{PLURAL:$1|ߟߢߊ߬ߟߌ|ߟߢߊ߬ߟߌ ߟߎ߬}} ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߠߎ߬ ߞߊ߲߬",
+       "revdelete-success": "ߡߛߊ߬ߦߌ߲߬ߠߌ߲ ߦߋߟߌ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫.",
+       "revdelete-failure": "ߟߢߊ߬ߟߌ ߦߋߟߌ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߟߏ߲ߘߐߦߊ߫ ߟߊ߫: $1",
+       "logdelete-success": "ߘߎ߲ߛߓߍ ߦߋߟߌ ߟߊߘߏ߲߬ߠߌ߲.",
+       "logdelete-failure": "ߘߎ߲ߛߓߍ ߦߋߟߌ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߟߊߘߏ߲߬ ߠߊ߫: $1",
+       "revdel-restore": "ߊ߬ ߦߋߢߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "pagehist": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ",
+       "deletedhist": "ߘߐ߬ߝߐ ߖߏ߬ߛߌ߬",
+       "revdelete-hide-current": "ߦߌߟߡߊ ߕߎ߬ߡߊ߬ߘߊ ߢߡߊߘߏ߲߯ߠߌ߲ ߝߎ߬ߕߎ߲߬ߕߌ $2߸ $1: ߣߌ߲߬ ߦߋ߫ ߥߊ߯ߕߌߣߍ߲ ߣߌ߲߬ ߟߢߊ߬ߟߌ ߟߋ߬ ߘߌ߫. ߊ߬ ߕߍߣߊ߬ ߢߡߊߘߏ߲߰ ߠߊ߫.",
+       "revdelete-show-no-access": "ߦߌߟߡߊ ߕߎ߬ߡߊ߬ߘߊ ߦߌ߬ߘߊ߬ߟߌ ߝߎ߬ߕߎ߲߬ߕߌ $2߸ $1: ߦߌߟߡߊ ߣߌ߲߬ ߓߘߊ߫ ߓߊ߲߫ ߣߐ߬ߣߐ߬ ߟߊ߫ \"ߡߊߓߍ߲߬ߣߍ߲\" ߘߌ߫.\nߌ ߕߍߣߊ߬ ߛߋ߫ ߟߴߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫.",
+       "revdelete-modify-no-access": "ߦߌߟߡߊ ߕߎ߬ߡߊ߬ߘߊ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߝߎ߬ߕߎ߲߬ߕߌ $2߸ $1: ߦߌߟߡߊ ߣߌ߲߬ ߓߘߊ߫ ߓߊ߲߫ ߣߐ߬ߣߐ߬ ߟߊ߫ ߴߴߡߊߓߍ߲߬ߣߍ߲ߴߴ ߘߌ߫.\nߌ ߕߍߣߊ߬ ߛߋ߫ ߟߴߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫.",
+       "revdelete-modify-missing": "ߦߌߟߡߊ ID $1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߝߎ߬ߕߎ߲߬ߕߌ: ߊ߬ ߓߘߊ߫ ߕߎߣߎ߲߫ ߓߟߏߡߟߊ ߝߊ߲ ߘߐ߫߹",
+       "revdelete-no-change": "<strong>ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ</strong> ߦߌߟߡߊ ߟߢߊ߬ߟߌ ߕߎ߬ߡߊ߬ߘߊ $2߸ $1 ߦߋߟߌ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߡߊ߬ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߦߴߊ߬ ߓߟߏ߫ ߞߘߐ߬ߡߊ߲߫.",
+       "revdelete-otherreason": "ߞߎ߲߬ ߡߊߞߊ߬ߝߏ߬ߕߊ߬/ߜߘߍ:",
+       "revdelete-reasonotherlist": "ߞߎ߲߬ ߜߘߍ ߟߎ߬",
+       "revdelete-edit-reasonlist": "ߖߏ߬ߛߌ߬ߟߌ ߞߎ߲߭ ߕߎ߬ߡߊ߬ߘߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "revdelete-offender": "ߟߢߊ߬ߟߌ ߛߓߍߦߟߊ:",
+       "suppressionlog": "ߘߎ߲ߛߓߍ ߖߏ߬ߛߌ߬ߟߌ",
+       "mergehistory": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
+       "mergehistory-header": "ߞߐߜߍ ߣߌ߲߬ ߟߊߢߊ߬ߟߌ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬ ߞߐߜߍ߫ ߛߎ߲߫ ߛߎ߮ ߞߋߟߋ߲ ߠߎ߬ ߟߊ߫ ߘߐ߬ߝߐ ߘߌ߫߸ ߞߐߜߍ߫ ߞߎߘߊ ߟߎ߬ ߘߐ߫.ߌ ߦߋ߫ ߟߴߊ߬ ߟߊ߫ ߞߏ߫ ߡߝߊ߬ߟߋ߲߬ߠߌ߲ ߏ߬ ߟߎ߬ ߘߐߡߌ߬ߣߊ߬ ߕߐ߫ ߟߋ߬ ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߘߐ߫ ߘߊߓߊ߲ߓߟߏߡߊ߬.",
+       "mergehistory-box": "ߞߐߜߍ߫ ߝߌ߬ߟߊ߬ ߟߎ߫ ߟߊ߫ ߘߐ߬ߝߐ ߞߍߢߐ߲߮ߞߊ߲߬:",
+       "mergehistory-from": "ߓߐߛߎ߲ ߞߐߜߍ:",
+       "mergehistory-into": "ߕߊ߯ߦߙߐ ߞߐߜߍ:",
+       "mergehistory-list": "ߞߍߢߐ߲߮ߞߊ߲ߕߊ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߐ߬ߝߐ",
+       "mergehistory-go": "ߞߍߢߐ߲߮ߞߊ߲ߕߊ ߟߎ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߌ߬ߘߊ߬",
+       "mergehistory-submit": "ߟߢߊ߬ߟߌ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
+       "mergehistory-empty": "ߟߢߊ߬ߟߌ ߕߴߛߋ߫ ߞߍ߫ ߟߊ߫ ߢߐ߲߮ߞߊ߲߬.",
+       "mergehistory-done": "$3 {{PLURAL:$3|ߟߢߊ߬ߟߌ|ߟߢߊ߬ߟߌ ߟߎ߬}} $1 {{PLURAL:$3|ߕߘߍ߬ ߦߋ߫|ߕߎ߲߬ ߦߋ߫}} ߞߍߢߐ߲߮ߞߊ߲߬ [[:$2]] ߘߐ߫.",
+       "mergehistory-fail-bad-timestamp": "ߕߎ߬ߡߊ߬ߘߊ ߓߊ߬ߘߌ߬ߟߊ߲ ߓߍ߲߬ߣߍ߲߬ ߕߍ߫.",
+       "mergehistory-fail-invalid-source": "ߞߐߜߍ ߓߐߛߎ߲ ߓߍ߲߬ߣߍ߲߫ ߕߍ߫.",
+       "mergehistory-fail-invalid-dest": "ߞߐߜߍ ߞߎ߲߬ߕߋߟߋ߲ ߓߍ߲߬ߣߍ߲߬ ߕߍ߫",
+       "mergehistory-fail-no-change": "ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬ ߡߊ߫ ߟߢߊ߬ߟߌ߬ ߛߌ߫ ߞߍߢߐ߲߮ߞߊ߲߬. ߞߐߜߍ ߣߌ߫ ߕߎ߬ߡߊ߬ߘߊ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߝߛߍ߬ߝߛߍ߫ ߖߊ߰ߣߌ߲߬.",
+       "mergehistory-fail-permission": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ߬ ߘߐߥߛߊ߬ߣߍ߲߬ ߕߍ߫ ߞߊ߬ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬.",
+       "mergehistory-fail-self-merge": "ߞߐߜߍ ߛߎ߲ ߣߴߊ߬ ߞߎ߲߬ߕߋߟߋ߲ ߕߍ߫ ߞߋߟߋ߲߫ ߘߌ߫.",
+       "mergehistory-no-source": "ߞߐߜߍ ߓߐߛߎ߲ $1 ߕߍ߫ ߦߋ߲߬.",
+       "mergehistory-no-destination": "ߞߐߜߍ ߞߎ߲߬ߕߋߟߋ߲ $1 ߕߍ߫ ߦߋ߲߬.",
+       "mergehistory-invalid-source": "ߞߐߜߍ ߓߐߛߎ߲ ߦߋ߫ ߞߍ߫ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߣߍ߲ ߘߌ߫.",
+       "mergehistory-invalid-destination": "ߞߐߜߍ ߞߎ߲߬ߕߋߟߋ߲ ߦߋ߫ ߞߍ߫ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߣߍ߲ ߘߌ߫.",
+       "mergehistory-autocomment": "ߞߍߢߐ߲߮ߞߊ߲߬ [[:$1]] ߦߊ߲߬ [[:$2]] ߘߐ߫",
+       "mergehistory-comment": "ߞߍߢߐ߲߮ߞߊ߲߬ [[:$1]] ߦߊ߲߬ ߘߐ߫ [[:$2]]: $3",
+       "mergehistory-same-destination": "ߓߐߛߎ߲ ߣߌ߫ ߞߐߜߍ ߞߎ߲߬ߕߋߟߋ߲ ߕߍߣߊ߬ ߞߍ߫ ߟߊ߫ ߞߋߟߋ߲߫ ߘߌ߫.",
+       "mergehistory-reason": "ߊ߬ ߛߊߓߎ:",
        "mergelog": "ߥߴߌ ߜߊ߲߬ߞߎ߲߬",
+       "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-result-category-size": "{{PLURAL:$1|1 ߛߌ߲߬ߝߏ߲|$1 ߛߌ߲߬ߝߏ߲ ߠߎ߬}} ({{PLURAL:$2|1 ߦߌߟߡߊߙߋ߲|$2 ߦߌߟߡߊߙߋ߲ ߠߎ߬}}, {{PLURAL:$3|1 ߞߐߕߐ߮|$3 ߞߐߕߐ߮ ߟߎ߬}})",
        "search-redirect": "(ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ $1)",
        "search-section": "(ߕߍߕߍ߮ $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": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬ ߦߙߌߞߊ:",
+       "prefsnologintext2": "ߌ ߜߊ߲߬ߞߎ߲߫ ߖߊ߰ߣߌ߲߫ ߞߴߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߡߊߝߊ߬ߟߋ߲߬.",
+       "prefs-skin": "ߝߊ߬ߘߌ",
+       "skin-preview": "ߊ߬ ߘߐߜߍ߫ ߡߎߣߎ߲߬",
+       "datedefault": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ߬ ߕߴߦߋ߲߬",
+       "prefs-labs": "ߖߎ߯ߓߍߟߊ߲ ߢߍߕߊ߮",
+       "prefs-user-pages": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ ߟߎ߬",
+       "prefs-personal": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߢߊߞߙߍ",
+       "prefs-rc": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬",
+       "prefs-watchlist": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ",
+       "prefs-editwatchlist": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "prefs-editwatchlist-label": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߘߏ߲߬ߘߊ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "prefs-editwatchlist-edit": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߞߎ߲߬ߕߐ߮ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
+       "prefs-editwatchlist-raw": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߎ߰ߡߍ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "prefs-editwatchlist-clear": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߖߏ߬ߛߌ߬",
+       "prefs-watchlist-days": "ߦߌ߬ߘߊ߬ߟߌ ߞߊߞߊ߲߫ ߞߊ߬ ߞߍ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫ ߟߏ߲ ߡߍ߲ ߠߎ߬ ߘߐ߫:",
+       "prefs-watchlist-days-max": "{{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}} ߞߐߘߊ߲ $1",
+       "prefs-watchlist-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-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-all": "(ߊ߬ ߓߍ߯)",
+       "group-user-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-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
        "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬",
        "rightslog": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߜߊ߲߬ߞߎ߲߬ ߢߊ߬ ߓߘߍ",
        "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": " ߞߐߜߍ߫ ߛߌ߫ ߡߊ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ߫ ߡߎߣߎ߲߬",
index 0bd6958..2e9a11d 100644 (file)
        "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",
        "passwordpolicies-policyflag-suggestchangeonlogin": "sugerowana zmiana po zalogowaniu",
        "easydeflate-invaliddeflate": "Dostarczona zawartość nie jest poprawnie skompresowana",
        "unprotected-js": "Ze względów bezpieczeństwa kod JavaScript nie może zostać załadowany z niezabezpieczonych stron. Prosimy dodawać JavaScript w przestrzeni MediaWiki lub jako podstronę strony użytkownika.",
-       "userlogout-continue": "Jeżeli chcesz się wylogować, [$1 przejdź so strony wylogowywania].",
-       "userlogout-sessionerror": "Wylogowywanie nie powiodło się ze względu na błąd związany z sesją. [$1 Spróbuj ponownie]."
+       "userlogout-continue": "Chcesz się wylogować?"
 }
index 3f2b4b0..dff37f4 100644 (file)
        "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",
        "passwordpolicies-policyflag-suggestchangeonlogin": "sugerir mudança na entrada",
        "easydeflate-invaliddeflate": "O conteúdo fornecido não está devidamente comprimido",
        "unprotected-js": "Por razões de segurança o JavaScript não pode ser carregado de páginas desprotegidas. Por favor, crie apenas javascript no MediaWiki: namespace ou como uma subpágina do usuário",
-       "userlogout-continue": "Se pretende terminar a sessão [$1 continue para a página de saída], por favor.",
-       "userlogout-sessionerror": "A sua saída falhou devido a um erro da sessão. [$1 Tente novamente], por favor."
+       "userlogout-continue": "Você quer sair?"
 }
index 6cac6f1..3e6d572 100644 (file)
        "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-sessionerror": "A sua saída falhou devido a um erro da sessão. [$1 Tente novamente], por favor."
+       "userlogout-continue": "Se pretende terminar a sessão [$1 prossiga para a página de saída], por favor."
 }
index 364fff4..507bbfd 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.",
        "passwordpolicies-policyflag-suggestchangeonlogin": "Password policy flag that suggests changing invalid passwords on login.",
        "easydeflate-invaliddeflate": "Error message if the content passed to easydeflate was not deflated (compressed) properly",
        "unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
-       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out.",
-       "userlogout-sessionerror": "Shown when a user tries to log out with an invalid token. $1 is url with correct token that user should click."
+       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
 }
index c60aecb..fe264b6 100644 (file)
        "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 44cc422..43629e3 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:<em>$2</em>\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.",
+       "blockedtext-composite-reason": "Есть несколько блокировок вашей учётной записи и/или IP-адреса",
        "whitelistedittext": "Вы должны $1 для изменения страниц.",
        "confirmedittext": "Вы должны подтвердить свой адрес электронной почты перед правкой страниц.\nПожалуйста, введите и подтвердите свой адрес электронной почты в своих [[Special:Preferences|персональных настройках]].",
        "nosuchsectiontitle": "Невозможно найти раздел",
        "emailccsubject": "Копия вашего сообщения для $1: $2",
        "emailsent": "Письмо отправлено",
        "emailsenttext": "Ваше электронное сообщение отправлено.",
-       "emailuserfooter": "Это письмо было отправлено {{GENDER:$2|участнику|участнице}} $2 от {{GENDER:$1|участника|участницы}} $1 с помощью функции «{{int:emailuser}}» проекта {{SITENAME}}. Если {{GENDER:$2|вы}} ответите на это письмо, оно будет отослано напрямую {{GENDER:$1|отправителю}}, так что {{GENDER:$2|ваш}} адрес электронной почты станет известен {{GENDER:$1|ему|ей}}.",
+       "emailuserfooter": "Это письмо было отправлено {{GENDER:$2|участнику|участнице}} $2 от {{GENDER:$1|участника|участницы}} $1 с помощью функции «{{int:emailuser}}» проекта {{SITENAME}}. {{gender:$1|Отправителю|Отправительнице}} неизвестен ваш электронный адрес, пока вы не ответите {{gender:$1|ему|ей}}.",
        "usermessage-summary": "Оставить системное сообщение.",
        "usermessage-editor": "Системная доставка",
        "watchlist": "Список наблюдения",
        "passwordpolicies-policyflag-suggestchangeonlogin": "предложить изменение при входе",
        "easydeflate-invaliddeflate": "Предоставленное содержимое не спущено надлежащим образом",
        "unprotected-js": "По соображениям безопасности JavaScript нельзя загружать с незащищённых страниц. Пожалуйста, создавайте скрипты только в пространстве имён MediaWiki: или как подстраницы участника.",
-       "userlogout-continue": "Если вы хотите выйти, [$1 перейдите на страницу выхода].",
-       "userlogout-sessionerror": "Выход из системы не удался из-за ошибки сеанса. Пожалуйста, [$1 попробуйте ещё раз]."
+       "userlogout-continue": "Вы хотите выйти?"
 }
index 4bc580f..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",
        "category-article-count": "{{PLURAL:$2|Chistha categuria cunteni un'única pàgina, indicadda inogghi.|Chistha categuria cunteni {{PLURAL:$1|la pàgina indicadda|li $1 pàgini indicaddi}} inogghi, i' un tutari di $2.}}",
        "category-file-count": "{{PLURAL:$2|Chistha categuria cunteni unu únicu file, indicaddu inogghi.|{{PLURAL:$1|Lu file sighenti è|$1 Li file sighenti so}} inogghi, i' un tutari di $2.}}",
        "listingcontinuesabbrev": "(séguiddu)",
+       "broken-file-category": "Pàgini ki hani liadduri di file cu difetti",
        "about": "Infuimmazioni",
        "article": "Pagina",
        "newwindow": "(s'abbri in d'unu nobu balchoni)",
        "actions": "Azioni",
        "namespaces": "Tipi di pàgina:",
        "variants": "Varianti",
+       "navigation-heading": "Nabiggazioni",
        "errorpagetitle": "Errori",
        "returnto": "Turra a $1.",
        "tagline": "Da {{SITENAME}}.",
        "printableversion": "Versioni sthampabiri",
        "permalink": "Cullegamentu peimmanenti",
        "print": "Sthampa",
+       "view": "Liggì",
+       "view-foreign": "Vedi innantu $1",
        "edit": "Mudifigga",
        "create": "Cria",
        "delete": "Canzella",
        "otherlanguages": "Althri linghi",
        "redirectedfrom": "(Rinviu da $1)",
        "redirectpagesub": "Pàgina di rinviu",
-       "lastmodifiedat": "Ulthima mudìfigga pa la pàgina: $2, $1.",
+       "redirectto": "Rimandu a:",
+       "lastmodifiedat": "Ulthima mudìfigga pa la pàgina: $1, $2.",
        "viewcount": "Chistha pàgina è isthadda liggidda {{PLURAL:$1|una voltha|$1 volthi}}.",
        "protectedpage": "Pàgina broccadda",
        "jumpto": "Vai a:",
        "nstab-template": "Mudellu",
        "nstab-help": "Aggiuddu",
        "nstab-category": "Categuria",
+       "mainpage-nstab": "Pàgina prinzipari",
        "nosuchaction": "Operazioni no ricuniscidda",
        "nosuchactiontext": "L'indirizzu immessu no curripondi a unu cumandu ricunisciddu da lu software MediaWiki",
        "nosuchspecialpage": "Pàgina ippiziari no dipunìbiri",
        "perfcachedts": "Li dati chi seghini so cabaddi da una còpia i' la mimória cache di la bancadati. Ulthimu aggiornamentu: $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.",
        "querypage-no-updates": "L'aggiornamenti di la pàgina so timpuraniamenti suippesi. Li dati in edda cuntinuddi no sarani aggiornaddi.",
        "viewsource": "Vèdi còdizi",
+       "viewsource-title": "Vidé la fonti di $1",
        "actionthrottled": "Azioni limitadda",
        "actionthrottledtext": "Cumenti rimédiu anti-spam, v'è un lìmiti a l'azioni ch'è pussìbiri eseguì i'nu tempu isthabiriddu, e abà suparaddu. Pògu tèmpu e pói riprubà.",
        "protectedpagetext": "Chistha pàgina è isthadda prutiggidda pa impidinni la mudìfigga.",
-       "viewsourcetext": "È pussìbiri visuarizzà e cupià lu còdizi di chistha pàgina:",
+       "viewsourcetext": "È pussìbiri visuarizzà e cupià lu còdizi di chistha pàgina.",
        "protectedinterface": "Chistha pàgina cunteni un'erementu chi fazzi parthi di l'interfàccia utenti di lu software; è dunca prutiggidda pa evità pussìbiri abusi.",
        "editinginterface": "'''Attinzioni:''' Lu testhu di chistha pàgina fazzi parthi di l'interfàccia utenti di lu situ. Tutti li mudìfigghi arriggaddi a chistha pàgina si rifrèttini i' l'imbasciaddi visuarizzaddi pa tutti l'utenti. Pa li traduzioni, pa piazeri utirizà [https://translatewiki.net/wiki/Main_Page?setlang=sdc translatewiki.net], lu prugettu di lucarizazioni MediaWiki.",
        "cascadeprotected": "In chistha pàgina nò è pussìbiri effettuà mudìfigghi parchí è isthadda incrusa {{PLURAL:$1|i la sighenti pàgina indicadda, ch'è isthadda prutiggidda|i li sighenti pàgini indicaddi, chi so isthaddi prutiggiddi}} chirriendi la prutizioni \"ricussiba\":\n$2",
        "titleprotected": "Chisthu tìturu è isthaddu prutiggiddu da la criazioni da [[User:$1|$1]].\nLa rasgioni frunidda è <em>$2</em>.",
        "logouttext": "'''Iscidda effettuadda.'''\n\nSi pò sighì a usà {{SITENAME}} cumenti utenti anònimu oppuru eseguì una noba intradda, cu' lu matessi innòmu utenti o un'innòmu dibessu.\nZerthuni pàgini pudìani continuà a apparì cumenti si la iscidda nò fùssia avvinudda finaghì nò vèni puridda la mimória cache di lu propriu nabiggadori.",
        "yourname": "Innòmu utenti",
+       "userlogin-yourname": "Innòmu utenti",
+       "userlogin-yourname-ph": "Ischribi l'innommu d'utenti tóiu.",
        "yourpassword": "Paràura d'órdhini",
+       "userlogin-yourpassword": "Paràura d'órdhini",
+       "userlogin-yourpassword-ph": "Ischribi lu còditzi tóiu.",
+       "createacct-yourpassword-ph": "Ischribi un còditzi",
        "yourpasswordagain": "Ripeti la paràura d'órdhini",
+       "createacct-yourpasswordagain": "Cunfeimmà paràura d'órdini",
+       "createacct-yourpasswordagain-ph": "Turrà a ischribì lu còditzi",
+       "userlogin-remembermypassword": "Sighiddi a lassammi intraddu/a.",
        "yourdomainname": "Ippizzificà lu dumìniu",
        "externaldberror": "S'è verifiggaddu un errori cu lu server di autentificazioni esthernu, oppuru nò si diponi di l'autorizazioni nezzessàri pa aggiornà la propria registhrazioni estherna.",
        "login": "Intra",
        "logout": "Esci",
        "userlogout": "Esci",
        "notloggedin": "Intradda no effettuadda",
+       "userlogin-noaccount": "No ài una registhrazioni?",
+       "userlogin-joinproject": "Registhrà in {{SITENAME}}",
        "createaccount": "Crea una noba registhrazioni",
+       "userlogin-resetpassword-link": "Hai immintigaddu lu còdizi tóiu?",
+       "userlogin-helplink2": "Aggiuddu cun l'intradda.",
+       "createacct-emailoptional": "Indirizzu di postha erettrònica (opzionari)",
+       "createacct-email-ph": "Ischribi lu tóiu indirizzu di postha erettrònica",
        "createaccountmail": "via postha erettrònica",
        "createacct-reason": "Mutibu",
+       "createacct-submit": "Registhrazioni",
+       "createacct-benefit-heading": "{{SITENAME}} è criaddu pa jenti cumenti sei tu.",
+       "createacct-benefit-body1": "{{PLURAL:$1|Mudìfiggu|Mudìfigghi}}",
+       "createacct-benefit-body2": "{{PLURAL:$1|pàgina|pàgini}}",
+       "createacct-benefit-body3": "{{PLURAL:$1|rizzenti autori}}",
        "badretype": "Li paràuri d'órdhini insiriddi nò cuinzidhini tra èddi.",
        "userexists": "L'innòmu utenti insiriddu è già utirizaddu. Pa pazieri chirria un'innòmu utenti dibessu.",
        "loginerror": "Errori i' l'intradda",
        "loginlanguagelabel": "Linga: $1",
        "pt-login": "Intra",
        "pt-login-button": "Intra",
+       "pt-createaccount": "Crea una noba registhrazioni",
        "pt-userlogout": "Iscidda",
        "changepassword": "Ciamba paràura d'órdhini",
        "resetpass_announce": "L'intradda è isthadda effettuadda cun un còdizi timpuràniu, inviaddu via postha erettrònica.\n\nPa cumprità la registhrazioni è nezzessàriu impusthà una noba paràura d'órdhini inogghi:",
        "resetpass_submit": "Impustha la paràura d'órdhini e intra",
        "changepassword-success": "La paràura d'órdhini tóia è isthadda mudìfiggadda. Abà sei intrendi...",
        "resetpass_forbidden": "No è pussìbiri mudifiggà li paràuri d'órdhini in {{SITENAME}}.",
+       "passwordreset": "Invia una noba paràura d'órdhini pa postha erettrònica",
        "passwordreset-username": "Innòmu utenti:",
        "changeemail-none": "(nisciunu)",
        "resettokens-tokens": "Token:",
        "preview": "Antiprimma",
        "showpreview": "Visuarizza antiprimma",
        "showdiff": "Musthra ciambamenti",
-       "anoneditwarning": "'''Attinzioni:''' Intradda nò effettuadda. I' la cronologia di la pàgina sarà rigisthraddu l'indirizzu IP tóiu.",
+       "anoneditwarning": "<strong>Warning:</strong> '''Attinzioni:''' Intradda nò effettuadda. I' la cronologia di la pàgina sarà rigisthraddu l'indirizzu IP tóiu si tu fai modìfichi.\nSi <strong>[$1 fai l'intradda]</strong> oppuru <strong>[$2 si tu crii una pàgina d'utenti]</strong>, li tó modìfichi sarani assignaddi a l'innommu d'utenti tóiu.",
        "missingsummary": "'''Promimória:''' Nò hai ippizzificaddu l'oggettu di la mudìfigga. Turrendi à incalchà '''Saivva la pàgina''' lu mudìfigga sarà saivvadda cun l'oggettu bioddu.",
        "missingcommenttext": "Insirì un cummentu in giossu.",
        "missingcommentheader": "'''Promimória:''' Nò hai ippizzificaddu l'intisthazioni di chisthu cummentu. Turrendi à incalchà '''Saivva la pagina''' lu mudìfigga sarà saivvadda chena intisthazioni.",
        "newarticle": "(Nóbu)",
        "newarticletext": "Lu cullegamentu sighiddu curripondi a'na pàgina nò ancora esisthenti.\n\nSi vói crià la pàgina abà, pói sùbidu ischribì in giossu (abbaidda li [$1 pàgini d'aggiuddu] pà maggiori infuimmazioni).\n\nS'ài sighiddu lu cullegamentu pa un'errori, è suffizenti incalchà lu buttoni '''Indareddu''' i' lu propriu nabiggadori.",
        "anontalkpagetext": "----''Chistha è la pàgina di dischussioni di un'utenti anònimu, chi no ha ancora criaddu una registhrazioni o, in dugna modu, no la usa. Pa identifiggallu è dunca nezzessàriu usà lu sóiu nùmaru di l'indirizzu IP. L'indirizzi IP, parò, poni assé cundibisi da più utenti. Si sei un'utenti anònimu e vói chi li cummenti prisenti in chistha pàgina no si rifèrini a te, [[Special:UserLogin|crea una noba registhrazion o intra]] cu' chidda ch'hai già pa evità d'assé confusu cu' althri utenti anònimi in futuru.''",
-       "noarticletext": "Abà chistha pàgina è biodda. È pussìbiri [[Special:Search/{{PAGENAME}}|zirchà chistu tituru]] i' l'althri pàgini di lu situ, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zirchà i' li rigisthri curriraddi] oppuru [{{fullurl:{{FULLPAGENAME}}|action=edit}} mudifiggà la pagina abà]</span>.",
+       "noarticletext": "Abà chistha pàgina è biodda. È pussìbiri [[Special:Search/{{PAGENAME}}|zirchà chistu tìturu]] i' l'althri pàgini di lu situ, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zirchà i' li rigisthri curriraddi] oppuru [{{fullurl:{{FULLPAGENAME}}|action=edit}} crià la pàgina abà]</span>.",
        "noarticletext-nopermission": "Abà chistha pàgina è biodda. È pussìbiri [[Special:Search/{{PAGENAME}}|zirchà chistu tìturu]] i' l'althri pàgini di lu situ, oppuru <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zirchà i' li rigisthri reratibi]</span>, parò nò pói crià chistha pàgina.",
        "userpage-userdoesnotexist": "La registhrazioni \"<nowiki>$1</nowiki>\" nò curripundi a un'utenti rigisthraddu. Verifiggà chi s'aggia avveru gana di crià o mudìfiggà chistha pàgina.",
+       "userpage-userdoesnotexist-view": "No v'è nisciuna pàgina di l'utenti \"$1\".",
        "clearyourcache": "'''Nota:''' daboi abé saivaddu è nezzessàriu pulì la mimória cache di lu propriu nabiggadori pà vidé li ciambamenti. Pa '''Mozilla / Firefox / Safari''': fà clic i Ricàrrigga incalchendi lu buttoni di li maiuschuri, oppuru incalchà ''Ctrl-Maiusc-R'' (''Cmd-Maiusc-R'' i Mac); pa '''Internet Explorer:''' mantinì incalchaddu lu tasthu ''Ctrl'' mentri s'incalcha lu buttoni ''Aggiorna'' o incalchà ''Ctrl-F5''; pa '''Konqueror''': incalchà lu buttoni ''Ricarica'' o lu tasthu ''F5''; pa '''Opera''' pò assé nezzessàriu ibbuiddà cumpretamenti la mimória cache da lu menù ''Strumenti → Preferenze''.",
        "usercssyoucanpreview": "'''Suggerimentu:''' Usa lu buttoni '''Visuarizza antiprimma''' pa prubà li nobi CSS primma di sàivvaddi.",
        "userjsyoucanpreview": "'''Suggerimentu:''' Usa lu buttoni '''Visuarizza antiprimma''' pa prubà li nobi JS primma di sàivvaddi.",
        "session_fail_preview_html": "'''Semmu dipiazuddi, no è isthaddu pussìbiri elaburà la mudìfigga parchì sò andaddi pessi li dati reratibi a la sissioni.'''\n\n''Parchì in {{SITENAME}} è cunsintiddu l'usu di l'HTML chena limitazioni, l'antiprimma no è visuarizzadda, pa sigguriddai contru l'attacchi JavaScript.''\n\n'''Si lu probrema prisisthi, pói prubà à iscì e turrà a intrà.'''",
        "token_suffix_mismatch": "'''La mudìfigga nò è isthadda sàivvadda parchí lu nabiggadori à musthraddu di gesthì in modu erraddu i caràtteri di punteggiaddura i' lu identifigganti di la mudìfigga. Pa evità una pussìbiri corruzioni di lu testhu di la pàgina, è isthadda rifiutadda l'intrea mudìfigga. Chistha situazioni pó verifiggassi, calch’e voltha, candu so usaddi zerthuni sivvìzi di proxy anònimi via reti chi àni di l'errori.'''",
        "editing": "Mudìfigga di $1",
+       "creating": "Crià $1",
        "editingsection": "Mudifigga di $1 (sezzioni)",
        "editingcomment": "Mudifigga di $1 (cummentu)",
        "editconflict": "Cuntrasthu d'edizioni i $1",
        "semiprotectedpagewarning": "'''Nota:''' Chista pàgina è isthadda broccadda parchì soru li utenti registhraddi possiano mudìfiggarla.",
        "cascadeprotectedwarning": "'''Attinzioni:''' Chistha pàgina è isthadda broccadda in modu chi soru l'utenti cun pribiréggi di amministhradori possiano mudìfiggarla. Lu chi avvini parchí la pàgina è incrusa {{PLURAL:$1|i la pàgina indicadda ..., ch'è isthadda prutiggidda|i li pàgini indicaddi ..., chi so isthaddi prutiggiddi}} chirriendi la prutizioni \"ricussiba\":",
        "titleprotectedwarning": "'''ATTINZIONI: Chistha pàgina è isthadda broccadda in modu chi soru zerthuni utenti possiano crialla.'''",
-       "templatesused": "Mudelli utirizaddi in chistha pàgina:",
+       "templatesused": "{{PLURAL:$1|Mudellu utirizaddu|Mudelli utirizaddi}} in chistha pàgina:",
        "templatesusedpreview": "Mudelli utirizaddi in chisth'antiprimma:",
        "templatesusedsection": "Mudelli utirizaddi in chistha sezzioni:",
        "template-protected": "(prutiggiddu)",
        "permissionserrorstext": "Nò si diponi di li pimmissi nezzessàri a eseguì l'azioni dumandadda, pa {{PLURAL:$1|lu sighenti mutibu|li sighenti mutibi}}:",
        "permissionserrorstext-withaction": "Nò si diponi di li primmissi nezzessàri pa $2, pa {{PLURAL:$1|lu sighenti mutibu|li sighenti mutibi}}:",
        "recreate-moveddeleted-warn": "'''Attinzioni: s'è pa ricrià una pàgina già canzilladda in passadu.'''\n\nS'azzirthà chi sia avveru opporthunu continuà a mudìfiggà chistha pàgina. L'erencu di li reratibi canzilladduri vèni ripurthaddu inogghi pa cumudiddai:",
+       "moveddeleted-notice": "No z'è più chista pàgina.\nThe deletion, protection, and move log for the page are provided below for reference.",
        "undo-success": "Chistha mudìfigga pò assé annulladda. Verifiggà lu sighenti cuntrasthu prisintaddu pa s'azzirthà chi lu cuntinuddu curripundi a cantu disizaddu e dunca saivvà li mudìfigghi pa cumprità la procedura di annullamentu.",
        "undo-failure": "Impussìbiri annullà la mudìfigga a càusa d'un cuntrasthu cun mudìfigghi intermédi.",
        "undo-summary": "Annulladda la mudìfigga $1 di [[Special:Contributions/$2|$2]] ([[User talk:$2|Dischussioni]])",
        "currentrev": "Versioni currenti",
        "currentrev-asof": "Versioni currenti di li $1",
        "revisionasof": "Versioni di lu $1",
-       "revision-info": "Versioni di lu $1, autori: $2",
+       "revision-info": "Versioni di lu $1 pa {{GENDER:$6|$2}}$7",
        "previousrevision": "← Versioni mancu rizzenti",
        "nextrevision": "Versioni più rizzenti →",
        "currentrevisionlink": "Versioni currenti",
        "revertmerge": "Anulla unioni",
        "mergelogpagetext": "Inogghi v'è una listha di l'ulthimi operazioni d'unioni di la cronologia d'una pàgina in un'althra.",
        "history-title": "Cronologia di li mudìfigghi di \"$1\"",
+       "difference-title": "Diffarènzi tra li versioni di \"$1\"",
        "lineno": "Riga $1:",
        "compareselectedversions": "Cunfronta li versioni sciubaraddi",
        "editundo": "annulla",
        "shown-title": "Musthra {{PLURAL:$1|un risulthaddu|$1 risulthaddi}} pa pàgina",
        "viewprevnext": "Vèdi ($1 {{int:pipe-separator}} $2) ($3).",
        "searchmenu-exists": "'''Z'è una pàgina ciamadda\"[[:$1]]\" in chisthu vichi.''' {{PLURAL:$2|0=|Vèdi puru li althri risulthaddi agattaddi.}}",
-       "searchmenu-new": "'''Crea la pàgina \"[[:$1]]\" in chistha vichi!''' {{PLURAL:$2|0=|Vèdi puru la pàgina agattadda cun la zercha tòia.|Vèdi puru li risulthaddi agattaddi .}}",
+       "searchmenu-new": "<strong>Crea la pàgina \"[[:$1]]\" in kistha vichi!</strong> {{PLURAL:$2|0=|Vèdi puru la pàgina agattadda cun la zercha tòia.|Vèdi puru li risulthaddi agattaddi.}}",
        "searchprofile-articles": "Bozi",
        "searchprofile-images": "Mùrthimediari",
        "searchprofile-everything": "Tuttu",
        "searchprofile-advanced-tooltip": "Zercha in althri tipi di pàgina",
        "search-result-size": "$1 ({{PLURAL:$2|una paraura|$2 parauri}})",
        "search-result-category-size": "{{PLURAL:$1|1 erementu|$1 erementi}} ({{PLURAL:$2|1 sottucateguria|$2 sottucateguri}}, {{PLURAL:$3|1 file|$3 file}})",
-       "search-redirect": "(rinviu $1)",
+       "search-redirect": "(Rinviu da $1)",
        "search-section": "(sezzioni $1)",
        "search-suggest": "Forsi zerchabi: $1",
        "search-interwiki-caption": "Prugetti fraddeddi",
        "searchrelated": "curriraddi",
        "searchall": "tutti",
        "showingresults": "Accó {{PLURAL:$1|màssimu '''1''' risulthaddu|màssimu li '''$1''' risulthaddi}} à partì da lu nùmaru #'''$2'''.",
+       "search-showingresults": "{{PLURAL:$4|Risulthaddu <strong>$1</strong> di <strong>$3</strong>|Risulthaddi <strong>$1 a $2</strong> di <strong>$3</strong>}}",
        "search-nonefound": "Nisciuni risulthaddi pa la to' zercha",
        "powersearch-legend": "Zercha abanzadda",
        "powersearch-ns": "Zercha i' li tipi di pàgina:",
        "recentchanges": "Ulthimi mudìfigghi",
        "recentchanges-legend": "Opzioni ùlthimi mudìfigghi",
        "recentchanges-summary": "Chistha pàgina prisinta li mudìfigghi più rizzenti a li cuntinuddi di lu situ.",
+       "recentchanges-noresult": "Nisciun ciambamentu i' lu tempu insignaddu.",
        "recentchanges-feed-description": "Chisthu feed cunteni li mudìfigghi più rizzenti a li cuntinuddi di lu situ.",
        "recentchanges-label-newpage": "Noba pàgina",
        "recentchanges-label-minor": "Chistha è una mudìfigga minori",
        "recentchanges-label-bot": "Chistha è una mudìfigga pa unu bot",
        "recentchanges-label-unpatrolled": "Mudìfigga nò ancora contrulladda",
-       "rcnotefrom": "Inogghi so erencaddi li mudìfigghi arriggaddi a parthì da '''$2''' (finz'a '''$1''').",
+       "recentchanges-label-plusminus": "A misura di la pàgina è isthadda ciambiadda di kisthu  innùmaru di byte",
+       "recentchanges-legend-heading": "<strong>Legenda:</strong>",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (vedi puru [[Special:NewPages|lu listhinu di li pàgini nobi]])",
+       "rcnotefrom": "Inogghi sottu {{PLURAL:$5|z'è la mudìfigga|zi sò li mudìfigghi}} a partì da <strong>$3, $4</strong> (màssimu <strong>$1</strong> ).",
        "rclistfrom": "Musthra li mudìfigghi arriggaddi à partì da $3 $2",
        "rcshowhideminor": "$1 li mudìfigghi minori",
+       "rcshowhideminor-show": "Ammusthrà",
+       "rcshowhideminor-hide": "Cuà",
        "rcshowhidebots": "$1 li bot",
+       "rcshowhidebots-show": "Ammusthrà",
+       "rcshowhidebots-hide": "Cuà",
        "rcshowhideliu": "$1 li utenti registhraddi",
+       "rcshowhideliu-show": "Ammusthrà",
+       "rcshowhideliu-hide": "Cuà",
        "rcshowhideanons": "$1 li utenti anònimi",
+       "rcshowhideanons-show": "Ammusthrà",
+       "rcshowhideanons-hide": "Cuà",
        "rcshowhidepatr": "$1 li mudìfigghi contrulladdi",
        "rcshowhidemine": "$1 li me' mudìfigghi",
+       "rcshowhidemine-show": "Ammusthrà",
+       "rcshowhidemine-hide": "Cuà",
        "rclinks": "Musthra li $1 mudìfigghi più rizzenti arriggaddi i' l'ulthimi $2 dì",
        "diff": "diff",
        "hist": "cron",
        "minoreditletter": "m",
        "newpageletter": "N",
        "boteditletter": "b",
+       "rc-change-size-new": "$1 {{PLURAL:$1|byte|bytes}} addabòi mudìfigga",
        "newsectionsummary": "/* $1 */ noba sezzioni",
        "rc-enhanced-expand": "Musthrà dettagli (dumanda JavaScript)",
        "rc-enhanced-hide": "Cua dettàgli",
        "filehist-comment": "Oggettu",
        "imagelinks": "Utirizazioni di lu file",
        "linkstoimage": "{{PLURAL:$1|La sighenti pàgina pùnta|Li sighenti $1 pàgini pùntani}} a l'immàgina:",
-       "nolinkstoimage": "Nisciuna pàgina cunteni cullegamenti a l'immàgina.",
+       "nolinkstoimage": "Nisciuna pàgina utirizeggia chistha immàgina.",
        "sharedupload": "Chisthu file prubeni da $1 e pó assé utirizaddu da althri prugetti.",
        "sharedupload-desc-here": "Chisthu file prubeni da $1 e pó assé utirizaddu da althri prugetti. La deschrizioni di la [$2 pàgina di deschrizioni] è indicadda in giossu.",
        "uploadnewversion-linktext": "Carrigga una nóba versioni di chistu file",
        "pager-older-n": "{{PLURAL:$1|1 mancu rizzenti|$1 mancu rizzenti}}",
        "booksources": "Rifirimenti di libri",
        "booksources-search-legend": "Zercha rifirimenti di libri",
+       "booksources-search": "Zercha",
        "booksources-text": "Inogghi v'è una listha di cullegamenti bessu siti estherni chi vindani libri nobi e usaddi, attrabessu li quari è pussìbiri uttinì maggiori infuimmazioni i' lu testhu zirchaddu.",
        "specialloguserlabel": "Utenti:",
        "speciallogtitlelabel": "Tìturu:",
        "contributions": "{{GENDER:$1|Cuntributi utenti}}",
        "contributions-title": "Cuntributi di $1",
        "mycontris": "Li me' cuntributi",
+       "anoncontribs": "Cuntributi",
        "contribsub2": "Par {{GENDER:$3|$1}} ($2)",
        "nocontribs": "Nò so isthaddi acciappaddi mudifigghi cunfoimmi a li criteri sciubaraddi.",
        "uctop": "currenti",
        "sp-contributions-search": "Zercha cuntributi",
        "sp-contributions-username": "Indirizzu IP o nommu utenti:",
        "sp-contributions-toponly": "Soru musthrà li versioni attuari",
+       "sp-contributions-newonly": "Soru musthrà la primma versioni di la bozi (oppuru li criazioni di pàgini)",
        "sp-contributions-submit": "Zercha",
        "whatlinkshere": "Puntani inogghi",
        "whatlinkshere-title": "Pàgini chi pùntani a \"$1\"",
        "whatlinkshere-links": "← cullegamenti",
        "whatlinkshere-hideredirs": "$1 rinvii",
        "whatlinkshere-hidetrans": "$1 incrusioni",
-       "whatlinkshere-hidelinks": "$1 cullegamenti",
+       "whatlinkshere-hidelinks": "$1 liadduri",
        "whatlinkshere-hideimages": "$1 liadduri a file",
        "whatlinkshere-filters": "Filthri",
        "blockip": "Brocca utenti",
        "importlogpagetext": "Rigisthru di l'impurthazioni di pàgini d'althri wiki, cumpreti di cronologia.",
        "import-logentry-upload-detail": "{{PLURAL:$1|una ribisioni impurthadda|$1 ribisioni impurthaddi}}",
        "import-logentry-interwiki-detail": "{{PLURAL:$1|una ribisioni impurthadda|$1 ribisioni impurthaddi}} da $2",
-       "tooltip-pt-userpage": "La pàgina utenti tóia",
+       "tooltip-pt-userpage": "{{GENDER:|La tóia}} pàgina utenti",
        "tooltip-pt-anonuserpage": "La pàgina utenti di chistu indirizzu IP",
-       "tooltip-pt-mytalk": "Pàgina di li tó dischussioni",
+       "tooltip-pt-mytalk": "{{GENDER:|La}} pàgina di li tó dischussioni",
        "tooltip-pt-anontalk": "Dischussioni i' li mudìfigghi arriggaddi da chisthu indirizzu IP",
-       "tooltip-pt-preferences": "Li tó prifirènzi",
+       "tooltip-pt-preferences": "{{GENDER:|Li tó}} prifirènzi",
        "tooltip-pt-watchlist": "La listha di li pàgini ch'isthai tinendi sottu osseivvazioni",
-       "tooltip-pt-mycontris": "Listha di li tó cuntributi",
+       "tooltip-pt-mycontris": "Listha di {{GENDER:|li tó}} cuntributi",
        "tooltip-pt-login": "La registhrazioni è cunsigliadda, puru si nò è ubbrigatória",
        "tooltip-pt-logout": "Iscidda",
+       "tooltip-pt-createaccount": "T'incuraggiemmu di crià una pàgina utenti e di registhratti, ma non è ubbrigatóriu.",
        "tooltip-ca-talk": "Vèdi li dischussioni reratibi a chistha pàgina",
-       "tooltip-ca-edit": "Pói mudìfiggà chistha pàgina. Pa piazeri usa lu buttoni d'antiprimma primma di saivvà",
+       "tooltip-ca-edit": "Pói mudifiggà chistha pàgina. Pa piazeri usa lu buttoni d'antiprimma primma di saivvà.",
        "tooltip-ca-addsection": "Ischuminzà una sezzioni noba",
        "tooltip-ca-viewsource": "Chistha pàgina è prutiggidda, ma pói vidé lu còdizi soiu.",
        "tooltip-ca-history": "Versioni prizzidenti di chistha pàgina",
        "tooltip-t-recentchangeslinked": "Erencu di li ulthimi mudìfigghi a li pàgini culligaddi a chistha",
        "tooltip-feed-rss": "Feed RSS pa chistha pàgina",
        "tooltip-feed-atom": "Feed Atom pa chistha pàgina",
-       "tooltip-t-contributions": "Listha di li cuntributi di chistu utenti",
+       "tooltip-t-contributions": "Listha di li cuntributi di {{GENDER:$1|chistu utenti}}",
        "tooltip-t-emailuser": "Invia un'imbasciadda di postha erettrònica a chisth'utenti",
        "tooltip-t-upload": "Carrigga file mùrthimediari",
        "tooltip-t-specialpages": "Listha di tutti li pàgini ippiziari",
        "spambot_username": "MediaWiki buggadda spam",
        "spam_reverting": "Turradda a l'ulthima versioni chena cullegamenti a $1",
        "spam_blanking": "Pàgina ibbiuddadda, tutti li ribisioni abìani cullegamenti a $1",
+       "simpleantispam-label": "Cumprobu cuntraspam\nNo <strong>not</strong> ischribì nudda drentu!",
+       "pageinfo-toolboxlink": "Iffuimmaziòni pa la pàgina",
        "pageinfo-contentpage-yes": "Sì",
        "pageinfo-protect-cascading-yes": "Sì",
        "markaspatrolleddiff": "Signa la mudìffiga cumenti verifiggadda",
        "file-nohires": "Nò so dipunìbiri versioni a risoruzioni maggiori.",
        "svg-long-desc": "file in fuimmaddu SVG, misuri nominari $1 × $2 punti, misuri di lu file: $3",
        "show-big-image": "File d'orìgini",
+       "show-big-image-preview": "Tàglia di kistha antiprimma: $1.",
+       "show-big-image-size": "$1 × $2 punti",
        "newimages": "Galleria di li file nobi",
        "imagelisttext": "Inogghi una listha di '''$1''' {{PLURAL:$1|file|file}} ordhinaddi pa $2.",
        "noimages": "Nò v'è nudda da vidé.",
        "metadata-help": "Chisthu file cunteni infuimmazzioni aggiuntibe, pó assé da la fotocamera o da lu scanner. Si lu file é isthaddu mudìfiggaddu, zerthuni dettàgli pudiani nò curripundì a li mudìfigghi arriggaddi.",
        "metadata-expand": "Musthra dettàgli",
        "metadata-collapse": "Cua dettàgli",
-       "metadata-fields": "Li campi reratibi a li metadi EXIF erencaddi in chist'imbasciadda sarani musthraddi i' la pàgina di l'immàgina candu la tabella di li metadati è prisenti i' lu fuimmaddu brebi. Pà impusthazioni pridifinidda, l'althri campi sarani cuaddi.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
+       "metadata-fields": "Li campi reratibi a li metadati EXIF erencaddi in chist'imbasciadda sarani musthraddi i' la pàgina di l'immàgina candu la tabella di li metadati è prisenti i' lu fuimmaddu brebi. Pà impusthazioni pridifinidda, l'althri campi sarani cuaddi.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "namespacesall": "tutti",
        "monthsall": "tutti",
        "confirmemail": "Cunfèimma indirizzu di postha erettrònica",
        "watchlisttools-view": "Visuarizza li mudìfigghi attinenti",
        "watchlisttools-edit": "Visuarizza e mudìfigga la listha",
        "watchlisttools-raw": "Mudìfigga la listha in fuimmaddu testhu",
+       "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|dischussioni]])",
        "version": "Versioni",
        "version-other": "Althru",
        "version-software-version": "Versioni",
        "specialpages": "Pagini ippiziari",
        "specialpages-group-login": "Intra / registhrazioni",
        "tag-filter": "[[Special:Tags|Tag]] filthru:",
+       "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Tag|Tags}}]]: $2",
        "tags-active-yes": "Sì",
        "tags-edit": "mudifigga",
        "htmlform-submit": "Invia",
        "htmlform-reset": "Annulla mudifigghi",
        "htmlform-selectorother-other": "Althru",
        "htmlform-yes": "Sì",
-       "rightsnone": "(nisciunu)"
+       "logentry-delete-delete": "$1 {{GENDER:$2|à isthudaddu}} ra pàgina $3",
+       "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}}",
+       "userlogout-continue": "Bói iscí?"
 }
index 1c2e2ee..48bf8f8 100644 (file)
        "help": "Pomoć",
        "help-mediawiki": "Pomoć o MediaWikiju",
        "search": "Traži / Тражи",
+       "search-ignored-headings": " #<!-- ne mijenjajte ništa u ovom redu --> <pre>\n# Zaglavlja što će biti zanemarena pri pretrazi.\n# Izmjene u ovome stupit će na snagu čim se stranica sa zaglavljem indeksira.\n# Možete nametnuti preindeksiranje stranice ako izvršite prazno uređivanje.\n# Sintaksa je sljedeća:\n#   * Sve od znaka \"#\" pa do kraja reda je komentar\n#   * Svaki neprazan red jest tačan naziv što treba se zanemariti, s tim da se razlikuju mala i velika slova i sve ostalo\nReference\nVanjski linkovi\nTakođer pogledajte\n #</pre> <!-- ne mijenjajte ništa u ovom redu -->",
        "searchbutton": "Traži",
        "go": "Idi / Иди",
        "searcharticle": "Idi",
        "botpasswords-updated-body": "Lozinka za bota \"$1\" {{GENDER:$2|korisnika|korisnice}} \"$2\" je izmjenjena.",
        "botpasswords-deleted-title": "Lozinka bota je obrisana",
        "botpasswords-deleted-body": "Lozinka za bota \"$1\" {{GENDER:$2|korisnika|korisnice}} \"$2\" je obrisana.",
+       "botpasswords-newpassword": "Nova lozinka za prijavu <strong>$1</strong> je <strong>$2</strong>. <em>Zapišite je za kasnije.</em> <br> (Za stare botove što zahtijevaju da prijavno ime bude isto kao i eventualno korisničko ime, možete uporabiti i <strong>$3</strong> kao korisničko ime, a <strong>$4</strong> kao lozinku.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider nije dostupan.",
        "botpasswords-restriction-failed": "Ne možete se prijaviti zbog ograničenja lozinki za botove.",
        "botpasswords-invalid-name": "Navedeno korisničko ime ne sadrži rastavni znak za lozinke botova (\"$1\").",
        "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.",
        "mw-widgets-abandonedit-discard": "Odbaci uređivanja",
        "mw-widgets-abandonedit-keep": "Nastavi s uređivanjem",
        "mw-widgets-abandonedit-title": "Da li ste sigurni?",
+       "mw-widgets-copytextlayout-copy": "Kopiraj",
+       "mw-widgets-copytextlayout-copy-fail": "Nije uspjelo iskopirati u međuspremnik.",
+       "mw-widgets-copytextlayout-copy-success": "Iskopirano u međuspremnik.",
        "mw-widgets-dateinput-no-date": "Datum nije izabran",
        "mw-widgets-mediasearch-input-placeholder": "Pretražite slike/snimke",
        "mw-widgets-mediasearch-noresults": "Nisam pronašao ništa.",
        "linkaccounts-submit": "Spoji račune",
        "unlinkaccounts": "Razdvajanje računa",
        "unlinkaccounts-success": "Račun je razdvojen.",
+       "authenticationdatachange-ignored": "Promjena podataka u autenifikaciji nije obrađena. Možda nije postavljen provajder?",
        "userjsispublic": "Napomena: podstranice s JavaScriptom ne bi trebale sadržavati povjerljive podatke budući da ih drugi korisnici mogu vidjeti.",
        "userjsonispublic": "Imajte na umu: Podstranice s JSONom ne bi trebale sadržavati povjerljive podatke budući da su vidljive drugim korisnicima.",
        "usercssispublic": "Napomena: podstranice s CSS-om ne bi trebale sadržavati povjerljive podatke budući da ih drugi korisnici mogu vidjeti.",
        "passwordpolicies-policyflag-suggestchangeonlogin": "predloži izmjenu pri prijavi",
        "easydeflate-invaliddeflate": "Sadržaj nije ispravno pročišćen",
        "unprotected-js": "JavaScript ne može da se učita sa nezaštićenih stranica iz bezbednosnih razloga. Samo napravite JavaScript u imenskom prostoru MediaWiki: ili kao korisničku podstranicu",
-       "userlogout-continue": "Ako se želite odjaviti, [$1 nastavite na odjavnoj strnaici].",
-       "userlogout-sessionerror": "Odjava nije uspjela zbog sesijske pogreške. [$1 Pokušajte ponovo]."
+       "userlogout-continue": "Ako se želite odjaviti, [$1 nastavite na odjavnoj strnaici]."
 }
index 767546d..fad8fb8 100644 (file)
        "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",
        "passwordpolicies-policyflag-suggestchangeonlogin": "predlagaj zamenjavo ob prijavi",
        "easydeflate-invaliddeflate": "Dana vsebina ni pravilno stisnjena",
        "unprotected-js": "Iz varnostnih razlogov JavaScripta ni možno naložiti z nezaščitenih strani. Prosimo, da JavaScript ustvarite samo v imenskem prostoru MediaWiki ali kot uporabniško podstran.",
-       "userlogout-continue": "Če se želite odjaviti, [$1 pojdite na stran za odjavo].",
-       "userlogout-sessionerror": "Odjava je spodletela zaradi napake seje. Prosimo, [$1 poskusite znova]."
+       "userlogout-continue": "Se želite odjaviti?"
 }
index 3da455d..f30aa1c 100644 (file)
        "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×",
        "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 0331027..302e8b0 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "föreslå ändring vid inloggning",
        "easydeflate-invaliddeflate": "Innehåll som tillhandahålls är inte helt komprimerat",
        "unprotected-js": "Av säkerhetsskäl kan inte JavaScript läsas in från oskyddade sidor. Skapa endast JavaScript i namnrymden MediaWiki: eller som en användarundersida.",
-       "userlogout-continue": "Om du vill logga ut, var god [$1 fortsätt till utloggningssidan].",
-       "userlogout-sessionerror": "Utloggning misslyckades p.g.a. sessionsfel. Var god [$1 försök igen]."
+       "userlogout-continue": "Vill du logga ut?"
 }
index 9ef0dbb..aa43bb3 100644 (file)
        "passwordpolicies-policy-passwordcannotmatchblacklist": "ห้ามรหัสผ่านตรงกับรหัสผ่านที่ขึ้นบัญชีดำโดยเจาะจง",
        "passwordpolicies-policy-maximalpasswordlength": "รหัสผ่านจะต้องมีความยาวน้อยกว่า $1 อักขระ",
        "passwordpolicies-policy-passwordcannotbepopular": "ห้ามรหัสผ่านเป็น{{PLURAL:$1|รหัสผ่านยอดนิยม|ติดรายการ $1 รหัสผ่านยอดนิยม}}",
-       "userlogout-continue": "หากคุณต้องการออกจากระบบ โปรด[$1 ดำเนินการต่อไปยังหน้าออกจากระบบ]",
-       "userlogout-sessionerror": "การออกจากระบบล้มเหลวเนื่องจากเซสชันผิดพลาด โปรด[$1 ลองอีกครั้ง]"
+       "userlogout-continue": "หากคุณต้องการออกจากระบบ โปรด[$1 ดำเนินการต่อไปยังหน้าออกจากระบบ]"
 }
index f25095e..3537faa 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "запропонувати зміну при вході",
        "easydeflate-invaliddeflate": "Наданий вміст не стиснений належним чином",
        "unprotected-js": "З міркувань безпеки JavaScript не можна запускати з незахищених сторінок. Будь ласка, створюйте javascript лише в просторі MediaWiki, або як особисту підсторінку користувача.",
-       "userlogout-continue": "Якщо Ви хочете вийти із системи, [$1 перейдіть на сторінку виходу].",
-       "userlogout-sessionerror": "Вихід із системи не відбувся через помилку сесії. Будь ласка, [$1 спробуйте знову]."
+       "userlogout-continue": "Ви хочете вийти із системи?"
 }
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 d799a17..8dfbd74 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "khuyên thay khi đăng nhập",
        "easydeflate-invaliddeflate": "Nội dung được cung cấp không được giải nén đúng cách",
        "unprotected-js": "Vì lý do an toàn JavaScript sẽ không được tải tại các trang không được khóa. Xin vui lòng chỉ tạo javascript tại không gian tên MediaWiki: hoặc tại trang con của trang Thành viên",
-       "userlogout-continue": "Nếu bạn muốn đăng xuất, xin hãy [$1 mở trang đăng xuất].",
-       "userlogout-sessionerror": "Thất bại khi đăng xuất vì lỗi phiên làm việc. Xin hãy [$1 thử lại]."
+       "userlogout-continue": "Nếu bạn muốn đăng xuất, xin hãy [$1 mở trang đăng xuất]."
 }
index ccf44ba..d4baf21 100644 (file)
        "redirectedfrom": "(Yoonalaat gu jóge $1)",
        "redirectpagesub": "Xëtu yoonalaat",
        "redirectto": "Jëmalewaat:",
-       "lastmodifiedat": "Coppite gu mujj gu xët wii $1 ci $2.<br />",
+       "lastmodifiedat": "$1 ci $2 lañ mujjee soppi xët wi.<br />",
        "viewcount": "Xët wii nemmeeku nañ ko {{PLURAL:$1|$1 yoon|$1 yoon}}.",
        "protectedpage": "Xët wees aar",
        "jumpto": "Dem:",
        "newarticle": "(Bees)",
        "newarticletext": "Da ngaa topp ab lëkkalekaay buy jëme ci aw xët wu amagul. ngir sos xët wi léegi, duggalal sa mbind ci boyot bii ci suuf (man ngaa yër [$1 xëtu ndimbal wi] ngir yeneeni xamle). Su fekkee njuumtee la fi indi cuqal ci '''dellu''' bu sa joowukaay.",
        "anontalkpagetext": "---- ''Yaa ngi ci xëtu waxtaanuwaayu ab jëfandikukatu alaxam, bu bindoogul ba fim ne mbaa jëfandikoowul am sàqam.\nKon ngir xàmmee ko fàw nga jëfandikoo màkkaanub IP wam. Te màkkaanub IP jëfandikukat yu bari man nañ koo bokk.\nSu fekkee jëfandikukatu alaxam nga, te nga gis ne dees laa féetale ay kàddu yoo moomul, ngalla [[Special:UserLogin|bindu]] walla [[Special:UserLogin|dugg]] ngir benn jaxase bañatee am ëllëg .''",
-       "noarticletext": "Fi mu ne ni amul menn mbind ci xët wii; man ngaa [[Special:Search/{{PAGENAME}}|seet koju xët wi]] ci yeneen xët, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} seet ci xëtu jagleel wi ],\nwalla [{{fullurl:{{FULLPAGENAME}}|action=edit}} soppi xët wii]</span>.",
+       "noarticletext": "Fi mu ne ni amul menn mbind ci xët wii.\nMan ngaa [[Special:Search/{{PAGENAME}}|seet koju xët wi]] ci yeneen xëti dal bi, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} seet ci xët yim séqal],\nwalla [{{fullurl:{{FULLPAGENAME}}|action=edit}} sos xët wii]</span>.",
        "noarticletext-nopermission": "Nii-nii amul menn mbind ci wii xët.\nMan ngaa [[Special:Search/{{PAGENAME}}|seet bii koj]] ci yeneen xët walla <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} nga seet ciy yéenekaayam]</span>. Numu man a deme, amoo sañ-sañu sos wii xët.",
        "userpage-userdoesnotexist": "Mii sàqum jëfandikukat « <nowiki>$1</nowiki> » du bu ku-bindu. Seetal bu baax ndax da ngaa namma sos walla soppi wii xët.",
        "clearyourcache": "'''Karmat :''' Soo dence xët wi ba noppi, faaw nga far nëmbiitu sa joowukaay ngir man a gis say coppite, te nga, su dee '''Mozilla / Firefox / Safari :''' cuq ci ''yeesal'' te bësaale ''shift'', walla nga bës ''Shift-R'' walla ''Ctrl-F5'' (Command-R ci Mac ), su dee '''Konqueror''': cuq ''yeesal'' walla nga bës ''F5''; su dee '''Opera''' faral nëbiit li (''Jumtukaay → Tànneef'') su dee '''Internet Explorer:''' cuq ci ''yeesal te bësaale ''Ctrl''  walla nga bës ''Ctrl-F5''.",
        "permissionserrorstext": "Amuloo sañ-sañu àggali jëf ji nga tambali, ngax {{PLURAL:$1|lii toftal|yii toftal}} :",
        "permissionserrorstext-withaction": "Amoo sañ-sañu $2, ngir {{PLURAL:$1|lii di toftal |yii di toftal}} :",
        "recreate-moveddeleted-warn": "'''Moytul: yaa ngi nekk di sosaat aw xët wees faroon.'''\n\nWóorlul bu baax ndax sosaat xët wi di na doon li gën.\nXoolal yéenekaayu far gi ci suuf.",
-       "moveddeleted-notice": "Xët wii dañu koo far.\nJaar-jaaru far yeek tuddewaat yi moo ngi ci suuf ngir yeneen xibaar.",
+       "moveddeleted-notice": "Xët wii dañu koo far.\nJaar-jaaru far yeek aar yeek tuddewaat yu wii xët moo ngi ci suuf ngir yeneen xibaar.",
        "log-fulllog": "Wone yéenekaay bu matale",
        "edit-hook-aborted": "Dogug coppite gi ak xeet yi.\nLi ko waral xameesu ko",
        "edit-gone-missing": "Yeesalug xët wi antuwul.\nMel na ne dañu koo far.",
        "rcshowhidebots-show": "Wone",
        "rcshowhidebots-hide": "Nëbb",
        "rcshowhideliu": "$1 jëfandikukat yi bindu",
+       "rcshowhideliu-show": "Wone",
        "rcshowhideliu-hide": "Nëbb",
        "rcshowhideanons": "$1 jëfandikukat yu binduwul",
        "rcshowhideanons-show": "Wone",
        "recentchangeslinked-feed": "Coppite yi ko ñeel",
        "recentchangeslinked-toolbox": "Coppite yi ko ñeel",
        "recentchangeslinked-title": "Coppite yi ñeel $1",
-       "recentchangeslinked-summary": "Wii xëtu jagleel moo lay won coppite yu mujj ci xët yi lëkkalook wii. Xët yi ci sa [[Special:Watchlist|limu toppte]] ñoo '''duuf'''.",
+       "recentchangeslinked-summary": "Duggalal aw turu xët ngir gis coppitey xët yiy jóge walla yiy jëme ci moom. (ngir gis yi bokk ciw wàll, duggalal {{ns:category}}:Turu wàll wi). Coppitey xët yi ci [[Special:Watchlist|xët yi ngay topp]] ñoo '''duuf'''.",
        "recentchangeslinked-page": "Turu xët wi :",
        "recentchangeslinked-to": "Wone coppite yi ñeel xët yi lëkkalook xët wi nga joxe",
        "upload": "Yeb ab dencukaay",
        "imagelinks": "Njëfandikug dencukaay bi",
        "linkstoimage": "{{PLURAL:$1|Xët wii ci suuf ëmb na|$1 xët yii ci suuf ëmb nañu}} bii dencukaay:",
        "linkstoimage-more": "Lu ëpp $1 {{PLURAL:$1|xët lëkkale nañu leen|xët lëkkale nañu leen}} ak bii dencukaay.\nLim bii di toftal moo lay won {{PLURAL:$1|xët wi ñu njëkk a|xët yi ñu njëkk a}} lëkkale ak wii.\nAb [[Special:WhatLinksHere/$2|lim bu mat]] jàppandi na.",
-       "nolinkstoimage": "Amul wenn xët wu ëmb bii dencukaay.",
+       "nolinkstoimage": "Amul wenn xët wuy jëfandikoo bii dencukaay.",
        "morelinkstoimage": "Xool [[Special:WhatLinksHere/$1|yeneeni lëkkalekaay]] yuy jëme ci bii dencukaay.",
        "duplicatesoffile": "{{PLURAL:$1|Dencukaay bii|$1 Dencukaay  yii}} di toftal {{PLURAL:$1|ab duppitu|ay duppitu}} bii {{PLURAL:$2|la|lañu}} ([[Special:FileDuplicateSearch/$2|yeneeni faramfacce]])::",
        "sharedupload": "Dencukaay bii $1 la bàyyikoo, te man nañu koo jëfandikoo ci yeneen sémb.",
        "whatlinkshere-next": "{{PLURAL:$1|wi toftal|$1 yi toftal}}",
        "whatlinkshere-links": "← lëkkalekaay",
        "whatlinkshere-hideredirs": "$1 jubluwaat",
-       "whatlinkshere-hidetrans": "$1 mboole",
+       "whatlinkshere-hidetrans": "$1 boole",
        "whatlinkshere-hidelinks": "$1 lëkkalekaay",
        "whatlinkshere-hideimages": "$1 lëkkalekaayi nataal",
        "whatlinkshere-filters": "Seggukaay",
        "allmessagescurrent": "Bataaxal bi fi nekk",
        "allmessagestext": "Lii mo'y limu bataaxal yëpp yi am ci biir MediaWiki",
        "thumbnail-more": "Ngandal",
-       "tooltip-pt-userpage": "Sa xëtu jëfandikukat",
+       "tooltip-pt-userpage": "{GENDER:|Sa xëtu}} jëfandikukat",
        "tooltip-pt-anonuserpage": "Xëtu jëfandikukat wu bii màkkaanu IP",
-       "tooltip-pt-mytalk": "Sa xëtu waxtaanuwaay",
+       "tooltip-pt-mytalk": "{{GENDER:|Sa xëtu}} waxtaanuwaay",
        "tooltip-pt-anontalk": "Xëtu diisoowaay wu bii màkkaanu IP",
-       "tooltip-pt-preferences": "Say tànneef",
+       "tooltip-pt-preferences": "{{GENDER:|Say}} tànneef",
        "tooltip-pt-watchlist": "Limu xët yi ngay topp",
-       "tooltip-pt-mycontris": "Limu say cëru",
+       "tooltip-pt-mycontris": "{{GENDER:|Limu}} say cëru",
        "tooltip-pt-login": "Woo nan la ngir nga xammeku, waaye doonul lu manul-ñàkk.",
        "tooltip-pt-logout": "Génn",
        "tooltip-pt-createaccount": "Dees na la digal nga bindu te dugg, donte doonul lu manul-ñàkk",
        "tooltip-t-recentchangeslinked": "Limu coppite yu mujj yu xët yi lëkkalook wii",
        "tooltip-feed-rss": "Walug RSS ngir wii xët",
        "tooltip-feed-atom": "Walug Atom ngir wii xët",
-       "tooltip-t-contributions": "Xool limu cëru bu bii jëfandikukat",
+       "tooltip-t-contributions": "Limu cëru bu {{GENDER:$1|bii jëfandikukat}}",
        "tooltip-t-emailuser": "Yónne ab m-bataaxal bii jëfandikukat",
        "tooltip-t-upload": "Yeb ay dencukaay",
        "tooltip-t-specialpages": "Limu xëti jagleel yépp",
        "logentry-newusers-create": "Sàqum jëfandikukat $1 sos nañu ko",
        "logentry-upload-upload": "$1 {{GENDER:$2|moo yeb}} $3",
        "rightsnone": "(menn)",
-       "searchsuggest-search": "Seet"
+       "searchsuggest-search": "Seet ci {{SITENAME}}"
 }
index 7ddd206..d581da9 100644 (file)
        "category-file-count-limited": "{{PLURAL:$1|Fáìlì yìí|Àwọn fáìlì $1 yìí}} wà nìnú ẹ̀ka yìí.",
        "listingcontinuesabbrev": "tẹ̀síwájú",
        "index-category": "Àwọn ojúewé atọ́kasí",
-       "noindex-category": "Àwọn ojúewé àìjẹ́ atọ́kasí",
+       "noindex-category": "Àwọn ojúewé àì",
        "broken-file-category": "Àwọn ojúewé pẹ̀lú àwọn ìjápọ̀ fáìlì gígé",
        "about": "Nípa",
        "article": "Ojúewé àkóónú",
        "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",
-       "blockedtext": "'''Orúkọ oníṣe yín tàbí àdírẹ́sì IP yín ti jẹ́ dídílọ́nà.'''\n\n$1 ni ó ṣe ìdínà.\nÌdí tó fun ni ''$2''.\n\n* Ìbẹ̀rẹ̀ ìdínà: $8\n* Ìparí ì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 è 'ránṣẹ́ sí oníṣe yìí pẹ̀lú e-mail' àyàfi tí ojúọ̀nà e-mail tó dájú 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.",
+       "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:<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.",
        "accmailtext": "A ti fi ọ̀rọ̀ìpamọ́ àrìnàkò tí a pèsè fún [[User talk:$1|$1]] ránṣẹ́ sí $2. Ẹ le ṣe àyípadà ọ̀rọ̀ìpamọ́ fún àpamọ́ tuntun yìí ní ibi ''[[Special:ChangePassword|àyípadà ọ̀rọ̀ìpamọ́]]'' lẹ́yìn tí ẹ bá ti jáwọlé.",
        "newarticle": "(Tuntun)",
        "newarticletext": "Ẹ ti tẹ̀lé ìjápọ̀ mọ́ ojúewé tí kò sí.\nLáti dá ojúewé yí ẹ bẹ̀rẹ̀ síní tẹ́kọ sí inú àpótí ìsàlẹ̀ yí (ẹ wo [$1 ojúewé ìrànlọ́wọ́ ] fun ẹ̀kúnrẹ́rẹ́ ).\nT'óbá sepé àsìse ló gbé yin dé bi, ẹ kọn bọ́tìnì ìpadàsẹ́yìn.",
-       "anontalkpagetext": "----\n<em>Ojúewé ìfọ̀rọ̀wérọ̀ yìí wà fún oníṣe aláílórúkọ tí kò tíì dá àkópamọ́, tàbí tí kò lò ó rárá.</em>\nBí bẹ́ẹ̀ laṣe únlo àdírẹ́ẹ̀sì IP oníyenọ́mbà láti dáamọ̀.\nIrú àdírẹ́ẹ̀sì IP báun ṣeéṣe kó jẹ́ pínpínlọ̀ pẹ̀lú àwọn oníṣe míràn.\nTó bá jẹ́ pé oníṣe aláìlórúkọ ni yín, tí ẹ sì ri pé wọ́n ùnsọ̀rọ̀ tí kò kàn yín sí i yín, ẹ jọ̀wọ́ [[Special:CreateAccount|ẹ dá àkópamọ́ kan]] tàbí [[Special:UserLogin|kí ẹ forúkọ wọlẹ́]] kó mọ́ baà sí ìdàrúpọ̀ lọ́jọ́ọwájú mọ́ àwọn oníṣe aláìlórúkọ mírán.",
+       "anontalkpagetext": "----\n<em>Ojúewé ìfọ̀rọ̀wérọ̀ yìí wà fún oníṣe aláílórúkọ tí kò tíì dá àkópamọ́, tàbí tí kò lò ó rárá.</em>\nBí bẹ́ẹ̀ laṣe únlo àdírẹ́ẹ̀sì IP oníyenọ́mbà láti dáamọ̀.\nIrú àdírẹ́ẹ̀sì IP báun ṣeéṣe kó jẹ́ pínpínlò pẹ̀lú àwọn oníṣe míràn.\nTó bá jẹ́ pé oníṣe aláìlórúkọ ni yín, tí ẹ sì rò pé wọ́n ùnsọ̀rọ̀ tí kò kàn yín sí i yín, ẹ jọ̀wọ́ [[Special:CreateAccount|ẹ dá àkópamọ́ kan]] tàbí [[Special:UserLogin|kí ẹ forúkọ wọlẹ́]] kó mọ́ baà sí ìdàrúpọ̀ lọ́jọ́ọwájú mọ́ àwọn oníṣe aláìlórúkọ mírán.",
        "noarticletext": "Lọ́wọ́lọ́wọ́ kò sí ìkọ̀ nínú ojúewé yìí.\nẸ le [[Special:Search/{{PAGENAME}}|wá àkọlé ojúewé yìí]] nínú àwọn ojúewé mìíràn,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wá àkọọ́lẹ̀ rẹ̀], tàbí [{{fullurl:{{FULLPAGENAME}}|action=edit}} kí ẹ ṣ'àtúnṣe ojúewé òún]</span>.",
        "noarticletext-nopermission": "Lọ́wọ́lọ́wọ́ kò sí ìkọ̀ nínú ojúewé yìí.\nẸ le [[Special:Search/{{PAGENAME}}|wá àkọlé ojúewé yìí]] nínú àwọn ojúewé mìíràn, tàbí\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wá àwọn àkọọ́lẹ̀ tó bámu]</span>, sùgbọ́n ẹ kò ní àṣẹ láti ṣ'ẹ̀dá ojúewé yìí.",
        "missing-revision": "Àtúnyẹ̀wò #$1 ojúewé tó únjẹ́ \"{{FULLPAGENAME}}\" kò sí.\n\nÈyí únsábà ṣẹlẹ̀ nítorípé ẹ tẹ̀lé ìtàn àjápọ̀ tí kò ṣiṣẹ́ mọ́ wá sí orí ojúewé tó ti jẹ́ píparẹ́.\nẸ̀kúnrẹ́rẹ́ wà nínú [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} àkọọ́lẹ̀ ìparẹ́].",
        "userpage-userdoesnotexist": "Àkópamọ́ oníṣe \"<nowiki>$1</nowiki>\" kò tíì jẹ́ fíforúkọsílẹ̀.\nẸjọ̀wọ́ ẹ ṣ'àgbéyẹ̀wò bóyá ẹ fẹ́ dá/ṣàtúnṣe ojúewé yìí.",
        "userpage-userdoesnotexist-view": "Àpamọ́ oníṣe \"$1\" kò jẹ́ fífilórúkọsílẹ̀.",
        "blocked-notice-logextract": "Lọ́wọ́lọ́wọ́ oníṣe yìí jẹ́ dídílọ́nà.\nÀkọsílẹ̀ ìdínà àìpẹ́ nìyí nísàlẹ̀ fún ìtọ́kasí:",
-       "clearyourcache": "'''Àkíyèsí:''' Lẹ́yìn ìmúpamọ́, ó ṣe é ṣe kó jẹ́ pé ẹ gbọ́dọ̀ fo cache agbétàkùn yín láti rí àwọn ìyípadà.\n* '''Firefox / Safari:''' Ẹ di ''Shift'' mú bí ẹ ṣe ún tẹ ''Reload'', tàbí kí ẹ tẹ ''Ctrl-F5'' tàbí ''Ctrl-R'' (''⌘-R'' lórí Mac)\n* '''Google Chrome:''' Ẹ tẹ ''Ctrl-Shift-R'' (''⌘-Shift-R'' lórí Mac)\n* '''Internet Explorer:''' Ẹ di ''Ctrl'' mú bí ẹ ṣe ún tẹ ''Refresh,'' tàbí kí ẹ tẹ ''Ctrl-F5''\n* '''Opera:''' Ẹ pa cache rẹ́ nínú ''Tools → Preferences''",
+       "clearyourcache": "<strong>Àkíyèsí:</strong> Lẹ́yìn ìmúpamọ́, ó ṣe é ṣe kó jẹ́ pé ẹ gbọ́dọ̀ fo cache agbétàkùn yín láti rí àwọn àtúnṣe.\n* <strong>Firefox / Safari:</strong> Ẹ di <em>Shift</em> mú bí ẹ ṣe ún tẹ <em>Reload</em>, tàbí kí ẹ tẹ <em>Ctrl-F5</em> tàbí <em>Ctrl-R</em> (<em>⌘-R</em> lórí Mac)\n* <strong>Google Chrome:</strong> Ẹ tẹ <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> lórí Mac)\n* <strong>Internet Explorer:</strong> Ẹ di <em>Ctrl</em> mú bí ẹ ṣe ún tẹ <em>Refresh,</em> tàbí kí ẹ tẹ <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Ẹ lọ sí <em>Menu→Settings</em> (<em>Opera → Preferences</em> lórí Mac) lẹ́yìn náà ẹ lọ sí <em>Privacy & security → Clear browsing data → Cached images and files</em>",
        "usercssyoucanpreview": "'''Ìrànlọ́wọ́:''' Ẹ lo bọ́tìnì \"{{int:showpreview}}\" fún dídánwò CSS tuntun yín kí ẹ tó múupamọ́.",
        "userjsyoucanpreview": "'''Ìrànlọ́wọ́:''' Ẹ lo bọ́tìnì \"{{int:showpreview}}\" fún dídánwò JavaScript tuntun yín kí ẹ tó múupamọ́.",
        "usercsspreview": "''''Ẹ mọ́ gbàgbé pé àkọ́yẹ̀wò CSS oníṣe yín nìyí.'''\n'''Kò tíì jẹ́ mímúpamọ́!'''",
        "page_first": "àkọ́kọ́",
        "page_last": "tógbẹ̀yìn",
        "histlegend": "Àṣàyàn ìyàtọ̀: ẹ fagi sínú àpótí àwọn átúnyẹ̀wò tí ẹ fẹ́ ṣàfiwè, lẹ́yìn náà ẹ tẹ enter tàbí bọ́tìnì ìsàlẹ̀.<br />\nÀlàyé: '''({{int:cur}})''' = ìyàtọ̀ sí àtúnyẹ̀wò tìsinyìí, '''({{int:last}})''' = ìyàtọ̀ sí àtúnyẹ̀wò tókọjá, '''{{int:minoreditletter}}''' = àtúnṣe kékeré.",
-       "history-fieldset-title": "Ìwádìí fún àwọn àtùnyẹ̀wò",
+       "history-fieldset-title": "Ajọ̀ àwọn àtùnyẹ̀wò",
        "history-show-deleted": "Ajẹ́píparẹ́ níkan",
        "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",
        "editundo": "dápadà",
        "diff-empty": "(Kò ní yàtọ̀)",
        "diff-multi-sameuser": "({{PLURAL:$1|Àtúnyẹ̀wò inú àrin kan|Àwọn àtúnyẹ̀wò inú àrin $1}} látọwọ́ oníṣe kan náà kò jẹ́ híhàn)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Àtúnyẹ̀wò inú àrin kan|Àwọn àtúnyẹ̀wò inú àrin $1}} látọwọ́ {{PLURAL:$2|oníṣe|àwọn oníṣe}} kò hàn)",
        "diff-multi-manyusers": "({{PLURAL:$1|Àtúnyẹ̀wò inú àrin kan|Àwọn àtúnyẹ̀wò inú àrin $1}} látọwọ́ {{PLURAL:$2|oníṣe|àwọn oníṣe}} tó pọ̀ju $2 lọ kò jẹ́ fífihàn)",
        "difference-missing-revision": "{{PLURAL:$2|Àtúnyẹ̀wò kan|Àwọn àtúnyẹ̀wò $2}} ìyàtọ̀ yìí ($1) kò {{PLURAL:$2|sí|sí}}.\n\nÈyí ṣẹlẹ̀ nítorí pé ẹ tẹ̀lé àjápọ̀ ìyàtọ̀ tí kò ṣiṣẹ́ mọ́ wá sí ojúewé tó ti jẹ́ píparẹ́.\nẸ̀kúnrẹ́rẹ́ wà nínú [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} àkọọ́lẹ̀ ìparẹ́].",
        "searchresults": "Àwọn èsì àwárí",
        "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é",
-       "rcnotefrom": "Àwọn àtúnṣe láti ''''$2''' (títí dé '''$1''' hàn) lábẹ́.",
+       "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é",
        "rcshowhideminor-show": "Fi hàn",
        "filehist-comment": "Àríwí",
        "imagelinks": "Ìlò fáìlì",
        "linkstoimage": "{{PLURAL:$1|Ojúewé kan yìí|Àwọn ojúewé $1 wọ̀nyí}} únlo fáìlì yí:",
-       "linkstoimage-more": "{{PLURAL:$1|Ojúewé|Àwọn ojúewé}} tó pọ̀ju $1 lọ jápọ̀ mọ́ fáìlì yìí.\nÀkòjọ ìṣàlẹ̀ yìí ṣàfihàn {{PLURAL:$1|ojúewé àkọ́kọ́|ojúewé $1 àkọ́kọ́}} tó jápọ̀ mọ́ fáìlì yìí nìkan.\n[[Special:WhatLinksHere/$2|Àkójọ kíkúnrẹ́rẹ́]] wà nígbèéwọ́.",
+       "linkstoimage-more": "{{PLURAL:$1|Ojúewé|Àwọn ojúewé}} tó pọ̀ju $1 lọ ló únlo fáìlì yìí.\nÀtòjọ ìṣàlẹ̀ yìí ṣàfihàn {{PLURAL:$1|ojúewé àkọ́kọ́|ojúewé $1 àkọ́kọ́}} tó únlo fáìlì yìí nìkan.\n[[Special:WhatLinksHere/$2|Àtòjọ kíkúnrẹ́rẹ́]] wà nígbèéwọ́.",
        "nolinkstoimage": "Kò sí ojúewé tó únlo fáìlì yìí.",
        "morelinkstoimage": "Ìwòrán [[Special:WhatLinksHere/$1|àwọn ìjápọ̀ míhìn]] sí fáìlì yìí.",
        "linkstoimage-redirect": "$1 (àtúnjúwe fáìlì) $2",
        "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í",
        "booksources-text": "Nísàlẹ̀ ni àtòjọ àwọn àjápọ̀ mọ́ àwọn ibiìtakùn míràn tí wọ́n únta ìwé tuntun àti ìwé àtijọ́, wọ́n sì le ní ọ̀rọ̀ ẹ̀kúnrẹ́rẹ́ nípa àwọn ìwé tí ẹ únwá:",
        "booksources-invalid-isbn": "ISBN náà kò dà bíi pé ó jẹ́ oníìbámu; ẹ yẹ̀ ẹ́ wò bóyá àsìṣe wà láti ibi tó jẹ́ kíkọ wá.",
        "specialloguserlabel": "Olùṣe:",
-       "speciallogtitlelabel": "Àfojúsùn (àkọlé tàbí oníṣe):",
+       "speciallogtitlelabel": "Àfojúsùn (àkọlé tàbí{{ns:oníṣe}}:orúkọ fún oníṣe):",
        "log": "Àwọn àkọọ́lẹ̀",
        "all-logs-page": "Gbogbo àkọsílẹ̀",
        "alllogstext": "Ìfihàn àpapọ̀ gbogbo àwọn àkọọ́lẹ̀ tó wà fún {{SITENAME}}.\nẸ le dín iwó kù nípa yíyan irú àkọọ́lẹ̀, orúkọ oníṣe (irú lẹ́tà ṣe kókó), tàbí ojúewé tókàn (irú lẹ́tà ṣe kókó).",
        "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ọ́",
        "pageinfo-length": "Ìgùn ojúewé (ní iye byte)",
        "pageinfo-article-id": "Nọ́mbà ìdámọ̀ ojúewé",
        "pageinfo-language": "Èdè àkóónú ojúewé",
-       "pageinfo-robot-policy": "Ipò ẹ̀rọ ìṣàwárí",
+       "pageinfo-content-model": "Aṣèjúwe àkóónú ojúewé",
+       "pageinfo-robot-policy": "Ìtò látọwọ́ róbọ́tì",
        "pageinfo-robot-index": "Gbígbàláàyè",
        "pageinfo-robot-noindex": "Àìgbàláàyè",
        "pageinfo-watchers": "Iye àwọn olùṣọ́ ojúewé",
        "pageinfo-few-watchers": "Iye {{PLURAL:$1|olùwòran|àwọn olùwòran}} kò ju $1 lọ",
-       "pageinfo-redirects-name": "Àwọn àtúnjúwe sí ojúewé yìí",
+       "pageinfo-redirects-name": "Iye àwọn àtúnjúwe sí ojúewé yìí",
        "pageinfo-subpages-name": "Àwọn ojúewé tó wà lábẹ́ ojúewé yìí",
        "pageinfo-subpages-value": "$1 ({{PLURAL:$2|àtúnjúwe|àtúnjúwe}} $2; {{PLURAL:$3|àìjẹ́-àtúnjúwe|àìjẹ́-àtúnjúwe}} $3)",
        "pageinfo-firstuser": "Olùdá ojúewé",
        "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",
        "version-entrypoints-header-entrypoint": "Ojú ìwọlé",
        "version-entrypoints-header-url": "URL",
        "redirect": "Àþúnjúwe látọ̀dọ̀ fáìlì, oníṣe, ojúewé, àtúnwò, tàbí ID àkọọ́lẹ̀",
+       "redirect-summary": "Ojúewé pàtàkì yìí ṣàtúnjúwe sí fáìlì kan (nítorí orúkọ fáìlì), ojúewé kan (nítorí ID àtúnyẹ̀wò tàbí ID ojúewé), ojúewé oníṣe kan (nítorí ID onínọ́mbà oníṣe), tàbí àkọsílẹ̀ ìlò kan (nítorí ID àkọsílẹ̀). Ìlò: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], or [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Lọ",
        "redirect-lookup": "Bojúwò:",
        "redirect-value": "Iye:",
        "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",
        "revdelete-unrestricted": "yọ ìpàlà fún àwọn olúmójútó",
        "logentry-move-move": "$1 {{GENDER:$2|ṣeyípòdà}} ojúewé $3 sí $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|ṣeyípòdà}} ojúewé $3 sí $4 láìfi àtúnjúwe sílẹ̀",
-       "logentry-move-move_redir": "$1 ṣeyípòdà ojúewé $3 sí $4 lórí àtúnjúwe",
+       "logentry-move-move_redir": "$1 {{GENDER:$2|ṣeyípòdà}} ojúewé $3 sí $4 lórí àtúnjúwe",
        "logentry-move-move_redir-noredirect": "$1 ṣeyípòdà ojúewé $3 sí $4 lórí àtúnjúwe láìfi àtúnjúwe sílẹ̀",
        "logentry-patrol-patrol": "$1 ṣe àmí àtúnyẹ̀wò $4 ojúewé $3 bíi sísọ́",
        "logentry-patrol-patrol-auto": "$1 fúnraẹni {{GENDER:$2|ṣàmì}} àtúnyẹ̀wò $4 sí ojúewé $3 bíi síṣọ́",
        "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 d178e25..a7183f3 100644 (file)
                        "佛壁灯",
                        "94rain",
                        "Viztor",
-                       "Ps2049"
+                       "Ps2049",
+                       "Suchichi02"
                ]
        },
        "tog-underline": "链接下划线:",
        "changeemail-no-info": "\n您必须登录以直接访问本页。",
        "changeemail-oldemail": "当前电子邮件地址:",
        "changeemail-newemail": "新的电子邮件地址:",
-       "changeemail-newemail-help": "此字段应留空,如果您希望移除您的电子邮件地址的话。如果电子邮件地址被移除,您将无法重置忘记的密码,并将不会接收来自此wiki的电子邮件。",
+       "changeemail-newemail-help": "如果您希望移除您的电子邮件地址的话此字段应留空。如果电子邮件地址被移除,您将无法重置忘记的密码,并将不会接收来自此wiki的电子邮件。",
        "changeemail-none": "(无)",
        "changeemail-password": "您的{{SITENAME}}密码:",
        "changeemail-submit": "更改电子邮件地址",
        "createaccountblock": "账户创建已禁用",
        "emailblock": "电子邮件停用",
        "blocklist-nousertalk": "不能编辑自己的讨论页",
-       "blocklist-editing": "编辑",
+       "blocklist-editing": "编辑",
        "blocklist-editing-sitewide": "编辑 (全站)",
        "blocklist-editing-page": "页面",
        "blocklist-editing-ns": "名字空间",
        "passwordpolicies-policyflag-suggestchangeonlogin": "建议在登录时更改",
        "easydeflate-invaliddeflate": "提供的内容未被适当缩小",
        "unprotected-js": "基于安全原因,JavaScript不能在未保护页面中载入。请在“MediaWiki:”名字空间或者用户子页面中添加JavaScript。",
-       "userlogout-continue": "如果你希望登出请[$1 点这里]。",
-       "userlogout-sessionerror": "登出失败,会话错误。请[$1 重试]"
+       "userlogout-continue": "如果你希望登出请[$1 点这里]。"
 }
index e723ec9..c01c98a 100644 (file)
        "passwordpolicies-policyflag-suggestchangeonlogin": "建議在登入時更改",
        "easydeflate-invaliddeflate": "提供的內容未被正常的壓縮",
        "unprotected-js": "基於安全因素,JavaScript 不能從未保護的頁面來載入。請僅在 MediaWiki:命名空間或使用者子頁面中建立 JavaScript。",
-       "userlogout-continue": "若您想要登出請[$1 繼續前至登出頁面]。",
-       "userlogout-sessionerror": "出於 session 錯誤造成登出失敗。請[$1 重試]。"
+       "userlogout-continue": "若您想要登出請[$1 繼續前至登出頁面]。"
 }
index 45afe2a..675d537 100644 (file)
@@ -59,3 +59,4 @@ $magicWords = [
 ];
 
 $separatorTransformTable = [ ',' => '.', '.' => ',' ];
+$linkTrail = '/^([a-zçəğıöşü]+)(.*)$/sDu';
index 44ce9a5..6beda4e 100644 (file)
@@ -26,6 +26,7 @@ require_once __DIR__ . '/../includes/PHPVersionCheck.php';
 wfEntryPointCheck( 'text' );
 
 use MediaWiki\Shell\Shell;
+use Wikimedia\Rdbms\IResultWrapper;
 
 /**
  * @defgroup MaintenanceArchive Maintenance archives
@@ -1478,9 +1479,9 @@ abstract class Maintenance {
        /**
         * Perform a search index update with locking
         * @param int $maxLockTime The maximum time to keep the search index locked.
-        * @param string $callback The function that will update the function.
+        * @param callable $callback The function that will update the function.
         * @param IMaintainableDatabase $dbw
-        * @param array $results
+        * @param array|IResultWrapper $results
         */
        public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
                $lockTime = time();
@@ -1724,7 +1725,7 @@ abstract class LoggedUpdateMaintenance extends Maintenance {
                        return false;
                }
 
-               $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' );
+               $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, [ 'IGNORE' ] );
 
                return true;
        }
index 66fc6d3..bed3956 100644 (file)
@@ -34,7 +34,9 @@ require_once __DIR__ . '/Maintenance.php';
 class CleanupPreferences extends Maintenance {
        public function __construct() {
                parent::__construct();
-               $this->mDescription = 'Clean up hidden preferences, removed preferences, and normalizes values';
+               $this->addDescription(
+                       'Clean up hidden preferences, removed preferences, and normalizes values'
+               );
                $this->setBatchSize( 50 );
                $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' );
                $this->addOption( 'hidden', 'Drop hidden preferences ($wgHiddenPrefs)' );
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 4f9e488..ee6e3e5 100644 (file)
@@ -28,6 +28,8 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -98,7 +100,9 @@ class DeleteBatch extends Maintenance {
 
                        $this->output( $title->getPrefixedText() );
                        if ( $title->getNamespace() == NS_FILE ) {
-                               $img = wfFindFile( $title, [ 'ignoreRedirect' => true ] );
+                               $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
+                                       $title, [ 'ignoreRedirect' => true ]
+                               );
                                if ( $img && $img->isLocal() && !$img->delete( $reason ) ) {
                                        $this->output( " FAILED to delete associated file... " );
                                }
index a5bc6cc..c4ca056 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -109,7 +111,7 @@ By default, outputs relative paths against the parent directory of $wgUploadDire
        }
 
        function outputItem( $name, $shared ) {
-               $file = wfFindFile( $name );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name );
                if ( $file && $this->filterItem( $file, $shared ) ) {
                        $filename = $file->getLocalRefPath();
                        $rel = wfRelativePath( $filename, $this->mBasePath );
index ef6d3d8..49fadaa 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -66,7 +68,7 @@ class EraseArchivedFile extends Maintenance {
                        $afile = ArchivedFile::newFromRow( $row );
                }
 
-               $file = wfLocalFile( $filename );
+               $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $filename );
                if ( $file->exists() ) {
                        $this->fatalError( "File '$filename' is still a public file, use the delete form.\n" );
                }
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 f27ea2f..381926a 100644 (file)
@@ -32,6 +32,8 @@
  * @author Mij <mij@bitchx.it>
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 class ImportImages extends Maintenance {
@@ -219,7 +221,8 @@ class ImportImages extends Maintenance {
                                }
 
                                # Check existence
-                               $image = wfLocalFile( $title );
+                               $image = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                                       ->newFile( $title );
                                if ( $image->exists() ) {
                                        if ( $this->hasOption( 'overwrite' ) ) {
                                                $this->output( "{$base} exists, overwriting..." );
@@ -306,7 +309,7 @@ class ImportImages extends Maintenance {
                                        $publishOptions = [];
                                        $handler = MediaHandler::getHandler( $props['mime'] );
                                        if ( $handler ) {
-                                               $metadata = Wikimedia\quietCall( 'unserialize', $props['metadata'] );
+                                               $metadata = \Wikimedia\AtEase\AtEase::quietCall( 'unserialize', $props['metadata'] );
 
                                                $publishOptions['headers'] = $handler->getContentHeaders( $metadata );
                                        } else {
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 a79d9f3..c9b3b66 100644 (file)
@@ -50,7 +50,7 @@ class DeleteLocalPasswords extends Maintenance {
 
        public function __construct() {
                parent::__construct();
-               $this->mDescription = "Deletes local password for users.";
+               $this->addDescription( "Deletes local password for users." );
                $this->setBatchSize( 1000 );
 
                $this->addOption( 'user', 'If specified, only checks the given user', false, true );
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 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 ce1506c..508960d 100644 (file)
@@ -137,7 +137,7 @@ TEXT
                        'updatelog',
                        [ 'ul_key' => 'populate category' ],
                        __METHOD__,
-                       'IGNORE'
+                       [ 'IGNORE' ]
                );
 
                return true;
index a71abb6..0de9d67 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Shell\Shell;
 
 require_once __DIR__ . '/Maintenance.php';
@@ -125,7 +126,8 @@ class PopulateImageSha1 extends LoggedUpdateMaintenance {
                                wfWaitForSlaves();
                        }
 
-                       $file = wfLocalFile( $row->img_name );
+                       $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
+                               ->newFile( $row->img_name );
                        if ( !$file ) {
                                continue;
                        }
index a654a1f..6cc86e0 100644 (file)
@@ -141,7 +141,7 @@ TEXT
                                                'iw_local' => 1
                                        ],
                                        __METHOD__,
-                                       'IGNORE'
+                                       [ 'IGNORE' ]
                                );
                        }
 
index 6e88dfa..80f8d30 100644 (file)
@@ -128,7 +128,7 @@ TEXT
                        }
 
                        if ( $insertRows ) {
-                               $dbw->insert( 'ip_changes', $insertRows, __METHOD__, 'IGNORE' );
+                               $dbw->insert( 'ip_changes', $insertRows, __METHOD__, [ 'IGNORE' ] );
 
                                $inserted += $dbw->affectedRows();
                        }
index c0de334..dfce202 100644 (file)
@@ -195,9 +195,9 @@ class ImageBuilder extends Maintenance {
 
        function addMissingImage( $filename, $fullpath ) {
                $timestamp = $this->dbw->timestamp( $this->getRepo()->getFileTimestamp( $fullpath ) );
+               $services = MediaWikiServices::getInstance();
 
-               $altname = MediaWikiServices::getInstance()->getContentLanguage()->
-                       checkTitleEncoding( $filename );
+               $altname = $services->getContentLanguage()->checkTitleEncoding( $filename );
                if ( $altname != $filename ) {
                        if ( $this->dryrun ) {
                                $filename = $altname;
@@ -214,7 +214,7 @@ class ImageBuilder extends Maintenance {
                        return;
                }
                if ( !$this->dryrun ) {
-                       $file = wfLocalFile( $filename );
+                       $file = $services->getRepoGroup()->getLocalRepo()->newFile( $filename );
                        if ( !$file->recordUpload(
                                '',
                                '(recovered file, missing upload log entry)',
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 () {
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..bb52c61
--- /dev/null
@@ -0,0 +1,8409 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+      "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.0.0"
+      }
+    },
+    "@babel/core": {
+      "version": "7.4.5",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.5.tgz",
+      "integrity": "sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/generator": "^7.4.4",
+        "@babel/helpers": "^7.4.4",
+        "@babel/parser": "^7.4.5",
+        "@babel/template": "^7.4.4",
+        "@babel/traverse": "^7.4.5",
+        "@babel/types": "^7.4.4",
+        "convert-source-map": "^1.1.0",
+        "debug": "^4.1.0",
+        "json5": "^2.1.0",
+        "lodash": "^4.17.11",
+        "resolve": "^1.3.2",
+        "semver": "^5.4.1",
+        "source-map": "^0.5.0"
+      }
+    },
+    "@babel/generator": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",
+      "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.4.4",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.11",
+        "source-map": "^0.5.0",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
+      "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "^7.0.0",
+        "@babel/template": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
+      "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+      "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.4.4"
+      }
+    },
+    "@babel/helpers": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz",
+      "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.4.4",
+        "@babel/traverse": "^7.4.4",
+        "@babel/types": "^7.4.4"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+      "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.4.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz",
+      "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==",
+      "dev": true
+    },
+    "@babel/template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+      "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/parser": "^7.4.4",
+        "@babel/types": "^7.4.4"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.4.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz",
+      "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/generator": "^7.4.4",
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/helper-split-export-declaration": "^7.4.4",
+        "@babel/parser": "^7.4.5",
+        "@babel/types": "^7.4.4",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0",
+        "lodash": "^4.17.11"
+      }
+    },
+    "@babel/types": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
+      "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.11",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@mrmlnc/readdir-enhanced": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+      "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
+      "dev": true,
+      "requires": {
+        "call-me-maybe": "^1.0.1",
+        "glob-to-regexp": "^0.3.0"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+      "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
+      "dev": true
+    },
+    "@types/events": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
+      "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
+      "dev": true
+    },
+    "@types/glob": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
+      "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+      "dev": true,
+      "requires": {
+        "@types/events": "*",
+        "@types/minimatch": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/minimatch": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+      "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "12.0.7",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.7.tgz",
+      "integrity": "sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A==",
+      "dev": true
+    },
+    "@types/q": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
+      "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
+      "dev": true
+    },
+    "@types/unist": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
+      "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==",
+      "dev": true
+    },
+    "@types/vfile": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz",
+      "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "@types/unist": "*",
+        "@types/vfile-message": "*"
+      }
+    },
+    "@types/vfile-message": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-1.0.1.tgz",
+      "integrity": "sha512-mlGER3Aqmq7bqR1tTTIVHq8KSAFFRyGbrxuM8C/H82g6k7r2fS+IMEkIu3D7JHzG10NvPdR8DNx0jr0pwpp4dA==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "@types/unist": "*"
+      }
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.40.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+          "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.24",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+          "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.40.0"
+          }
+        }
+      }
+    },
+    "acorn": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
+      "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
+      "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+      "dev": true
+    },
+    "adm-zip": {
+      "version": "0.4.13",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz",
+      "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+      "dev": true
+    },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+      "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^2.0.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-escapes": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+      "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+      "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+      "dev": true,
+      "requires": {
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+          "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+          "dev": true,
+          "requires": {
+            "remove-trailing-separator": "^1.0.1"
+          }
+        }
+      }
+    },
+    "archiver": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz",
+      "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=",
+      "dev": true,
+      "requires": {
+        "archiver-utils": "^1.3.0",
+        "async": "^2.0.0",
+        "buffer-crc32": "^0.2.1",
+        "glob": "^7.0.0",
+        "lodash": "^4.8.0",
+        "readable-stream": "^2.0.0",
+        "tar-stream": "^1.5.0",
+        "zip-stream": "^1.2.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+          "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.11"
+          }
+        }
+      }
+    },
+    "archiver-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz",
+      "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "graceful-fs": "^4.1.0",
+        "lazystream": "^1.0.0",
+        "lodash": "^4.8.0",
+        "normalize-path": "^2.0.0",
+        "readable-stream": "^2.0.0"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+          "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+          "dev": true,
+          "requires": {
+            "remove-trailing-separator": "^1.0.1"
+          }
+        }
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-slice": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+      "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+      "dev": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "async": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+      "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+      "dev": true
+    },
+    "async-each": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+      "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+      "dev": true
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "9.6.0",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.0.tgz",
+      "integrity": "sha512-kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.6.1",
+        "caniuse-lite": "^1.0.30000971",
+        "chalk": "^2.4.2",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^7.0.16",
+        "postcss-value-parser": "^3.3.1"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+      "dev": true
+    },
+    "babel-runtime": {
+      "version": "5.8.38",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz",
+      "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=",
+      "dev": true,
+      "requires": {
+        "core-js": "^1.0.0"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "1.2.7",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+          "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
+          "dev": true
+        }
+      }
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+      "dev": true
+    },
+    "bail": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.4.tgz",
+      "integrity": "sha512-S8vuDB4w6YpRhICUDET3guPlQpaJl7od94tpZ0Fvnyp+MKW/HyDTcRDck+29C9g+d/qQHnddRH3+94kZdrW0Ww==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "dev": true,
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "binary-extensions": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+      "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+      "dev": true
+    },
+    "bl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+      "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
+      "dev": true,
+      "requires": {
+        "readable-stream": "^2.3.5",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
+      "dev": true
+    },
+    "bluebird": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
+      "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
+      "dev": true
+    },
+    "body": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz",
+      "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=",
+      "dev": true,
+      "requires": {
+        "continuable-cache": "^0.3.1",
+        "error": "^7.0.0",
+        "raw-body": "~1.1.0",
+        "safe-json-parse": "~1.0.1"
+      }
+    },
+    "body-parser": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.1.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
+      },
+      "dependencies": {
+        "bytes": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+          "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "raw-body": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+          "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+          "dev": true,
+          "requires": {
+            "bytes": "3.1.0",
+            "http-errors": "1.7.2",
+            "iconv-lite": "0.4.24",
+            "unpipe": "1.0.0"
+          }
+        }
+      }
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+      "dev": true
+    },
+    "boom": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
+      "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
+      "dev": true,
+      "requires": {
+        "hoek": "4.x.x"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "browserslist": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.2.tgz",
+      "integrity": "sha512-2neU/V0giQy9h3XMPwLhEY3+Ao0uHSwHvU8Q1Ea6AgLVL1sXbX3dzPrJ8NWe5Hi4PoTkCYXOtVR9rfRLI0J/8Q==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30000974",
+        "electron-to-chromium": "^1.3.150",
+        "node-releases": "^1.1.23"
+      }
+    },
+    "buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
+      "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4"
+      }
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
+      "dev": true
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+      "dev": true
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
+      "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=",
+      "dev": true
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
+    "call-me-maybe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+      "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
+      "dev": true
+    },
+    "caller-callsite": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+      "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+      "dev": true,
+      "requires": {
+        "callsites": "^2.0.0"
+      },
+      "dependencies": {
+        "callsites": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+          "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
+          "dev": true
+        }
+      }
+    },
+    "caller-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+      "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+      "dev": true,
+      "requires": {
+        "caller-callsite": "^2.0.0"
+      }
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+      "dev": true
+    },
+    "callsites": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
+      "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^2.0.0",
+        "map-obj": "^1.0.0"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30000974",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000974.tgz",
+      "integrity": "sha512-xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww==",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "ccount": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.4.tgz",
+      "integrity": "sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      }
+    },
+    "character-entities": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.3.tgz",
+      "integrity": "sha512-yB4oYSAa9yLcGyTbB4ItFwHw43QHdH129IJ5R+WvxOkWlyFnR5FAaBNnUq4mcxsTVZGh28bHoeTHMKXH1wZf3w==",
+      "dev": true
+    },
+    "character-entities-html4": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.3.tgz",
+      "integrity": "sha512-SwnyZ7jQBCRHELk9zf2CN5AnGEc2nA+uKMZLHvcqhpPprjkYhiLn0DywMHgN5ttFZuITMATbh68M6VIVKwJbcg==",
+      "dev": true
+    },
+    "character-entities-legacy": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.3.tgz",
+      "integrity": "sha512-YAxUpPoPwxYFsslbdKkhrGnXAtXoHNgYjlBM3WMXkWGTl5RsY3QmOyhwAgL8Nxm9l5LBThXGawxKPn68y6/fww==",
+      "dev": true
+    },
+    "character-reference-invalid": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz",
+      "integrity": "sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==",
+      "dev": true
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+      "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
+      "dev": true,
+      "requires": {
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.1",
+        "braces": "^2.3.2",
+        "fsevents": "^1.2.7",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.3",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "normalize-path": "^3.0.0",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.2.1",
+        "upath": "^1.1.1"
+      }
+    },
+    "circular-json": {
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz",
+      "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==",
+      "dev": true
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "cli-cursor": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+      "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^2.0.0"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
+    "clone-regexp": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
+      "integrity": "sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==",
+      "dev": true,
+      "requires": {
+        "is-regexp": "^1.0.0",
+        "is-supported-regexp-flag": "^1.0.0"
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "coa": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+      "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+      "dev": true,
+      "requires": {
+        "@types/q": "^1.5.1",
+        "chalk": "^2.4.1",
+        "q": "^1.1.2"
+      }
+    },
+    "coffeescript": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz",
+      "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=",
+      "dev": true
+    },
+    "collapse-white-space": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.5.tgz",
+      "integrity": "sha512-703bOOmytCYAX9cXYqoikYIx6twmFCXsnzRQheBcTG3nzKYBR4P/+wkYeH+Mvj7qUz8zZDtdyzbxfnEi/kYzRQ==",
+      "dev": true
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "colors": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+      "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+      "dev": true
+    },
+    "combine-lists": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
+      "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.5.0"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.12.2",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
+      "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==",
+      "dev": true
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+      "dev": true
+    },
+    "component-emitter": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+      "dev": true
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+      "dev": true
+    },
+    "compress-commons": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
+      "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "^0.2.1",
+        "crc32-stream": "^2.0.0",
+        "normalize-path": "^2.0.0",
+        "readable-stream": "^2.0.0"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+          "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+          "dev": true,
+          "requires": {
+            "remove-trailing-separator": "^1.0.1"
+          }
+        }
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "~1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "dev": true
+    },
+    "continuable-cache": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz",
+      "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true
+    },
+    "core-js": {
+      "version": "2.6.5",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
+      "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "cosmiconfig": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
+      "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+      "dev": true,
+      "requires": {
+        "import-fresh": "^2.0.0",
+        "is-directory": "^0.3.1",
+        "js-yaml": "^3.13.1",
+        "parse-json": "^4.0.0"
+      },
+      "dependencies": {
+        "import-fresh": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
+          "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
+          "dev": true,
+          "requires": {
+            "caller-path": "^2.0.0",
+            "resolve-from": "^3.0.0"
+          }
+        },
+        "js-yaml": {
+          "version": "3.13.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "parse-json": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+          "dev": true,
+          "requires": {
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1"
+          }
+        },
+        "resolve-from": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+          "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+          "dev": true
+        }
+      }
+    },
+    "crc": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+      "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+      "dev": true,
+      "requires": {
+        "buffer": "^5.1.0"
+      }
+    },
+    "crc32-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
+      "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
+      "dev": true,
+      "requires": {
+        "crc": "^3.4.4",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "dev": true,
+      "requires": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
+    "cryptiles": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.4.tgz",
+      "integrity": "sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw==",
+      "dev": true,
+      "requires": {
+        "boom": "5.x.x"
+      },
+      "dependencies": {
+        "boom": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
+          "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
+          "dev": true,
+          "requires": {
+            "hoek": "4.x.x"
+          }
+        }
+      }
+    },
+    "css": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+      "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "source-map": "^0.6.1",
+        "source-map-resolve": "^0.5.2",
+        "urix": "^0.1.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "css-parse": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
+      "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
+      "dev": true,
+      "requires": {
+        "css": "^2.0.0"
+      }
+    },
+    "css-select": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz",
+      "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==",
+      "dev": true,
+      "requires": {
+        "boolbase": "^1.0.0",
+        "css-what": "^2.1.2",
+        "domutils": "^1.7.0",
+        "nth-check": "^1.0.2"
+      }
+    },
+    "css-select-base-adapter": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+      "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+      "dev": true
+    },
+    "css-tree": {
+      "version": "1.0.0-alpha.28",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
+      "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==",
+      "dev": true,
+      "requires": {
+        "mdn-data": "~1.1.0",
+        "source-map": "^0.5.3"
+      }
+    },
+    "css-url-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz",
+      "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
+      "dev": true
+    },
+    "css-value": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz",
+      "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=",
+      "dev": true
+    },
+    "css-what": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
+      "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
+      "dev": true
+    },
+    "csso": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
+      "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==",
+      "dev": true,
+      "requires": {
+        "css-tree": "1.0.0-alpha.29"
+      },
+      "dependencies": {
+        "css-tree": {
+          "version": "1.0.0-alpha.29",
+          "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz",
+          "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==",
+          "dev": true,
+          "requires": {
+            "mdn-data": "~1.1.0",
+            "source-map": "^0.5.3"
+          }
+        }
+      }
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "^1.0.1"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+      "dev": true
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "date-format": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
+      "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=",
+      "dev": true
+    },
+    "dateformat": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+      "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^4.0.1",
+        "meow": "^3.3.0"
+      }
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "dev": true,
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decamelize-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
+      "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
+      "dev": true,
+      "requires": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      }
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "deepmerge": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.0.1.tgz",
+      "integrity": "sha512-VIPwiMJqJ13ZQfaCsIFnp5Me9tnjURiaIFxfz7EH0Ci0dTSQpZtSLrqOicXqEd/z2r+z+Klk9GzmnRsgpgbOsQ==",
+      "dev": true
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "detect-libc": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+      "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
+      "dev": true
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+      "dev": true
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "dir-glob": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
+      "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
+      "dev": true,
+      "requires": {
+        "path-type": "^3.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "dev": true,
+          "requires": {
+            "pify": "^3.0.0"
+          }
+        },
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "dev": true
+        }
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "dev": true,
+      "requires": {
+        "custom-event": "~1.0.0",
+        "ent": "~2.2.0",
+        "extend": "^3.0.0",
+        "void-elements": "^2.0.0"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
+      "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.0",
+        "entities": "^1.1.1"
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "dot-prop": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
+      "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
+      "dev": true,
+      "requires": {
+        "is-obj": "^1.0.0"
+      }
+    },
+    "each-async": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz",
+      "integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=",
+      "dev": true,
+      "requires": {
+        "onetime": "^1.0.0",
+        "set-immediate-shim": "^1.0.0"
+      },
+      "dependencies": {
+        "onetime": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+          "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+          "dev": true
+        }
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "ejs": {
+      "version": "2.5.9",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz",
+      "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==",
+      "dev": true
+    },
+    "electron-to-chromium": {
+      "version": "1.3.155",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.155.tgz",
+      "integrity": "sha512-/ci/XgZG8jkLYOgOe3mpJY1onxPPTDY17y7scldhnSjjZqV6VvREG/LvwhRuV7BJbnENFfuDWZkSqlTh4x9ZjQ==",
+      "dev": true
+    },
+    "emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "dev": true,
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+      "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.0",
+        "ws": "~3.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+      "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "~3.3.1",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+      "dev": true,
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+      "dev": true
+    },
+    "error": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz",
+      "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=",
+      "dev": true,
+      "requires": {
+        "string-template": "~0.2.1",
+        "xtend": "~4.0.0"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
+      "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.2.0",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "is-callable": "^1.1.4",
+        "is-regex": "^1.0.4",
+        "object-keys": "^1.0.12"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+      "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
+      "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "eslint": {
+      "version": "5.15.3",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.15.3.tgz",
+      "integrity": "sha512-vMGi0PjCHSokZxE0NLp2VneGw5sio7SSiDNgIUn2tC0XkWJRNOIoHIg3CliLVfXnJsiHxGAYrkw0PieAu8+KYQ==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.9.1",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^4.0.3",
+        "eslint-utils": "^1.3.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^5.0.1",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob": "^7.1.2",
+        "globals": "^11.7.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^6.2.2",
+        "js-yaml": "^3.12.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.11",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.2",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^5.5.1",
+        "strip-ansi": "^4.0.0",
+        "strip-json-comments": "^2.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0"
+      }
+    },
+    "eslint-config-wikimedia": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.12.0.tgz",
+      "integrity": "sha512-ZkmGLvwmoEacj55t8Z6VH6wUu4/XTlgkSCerHkj+VU4tmyCD4mlzvTeSaPzOEDmZTVWUoiKnB6mvUx06l7uIbw==",
+      "dev": true,
+      "requires": {
+        "eslint": "^5.16.0",
+        "eslint-plugin-json": "^1.4.0",
+        "eslint-plugin-no-jquery": "^2.0.0",
+        "eslint-plugin-qunit": "^4.0.0"
+      },
+      "dependencies": {
+        "eslint": {
+          "version": "5.16.0",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+          "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "ajv": "^6.9.1",
+            "chalk": "^2.1.0",
+            "cross-spawn": "^6.0.5",
+            "debug": "^4.0.1",
+            "doctrine": "^3.0.0",
+            "eslint-scope": "^4.0.3",
+            "eslint-utils": "^1.3.1",
+            "eslint-visitor-keys": "^1.0.0",
+            "espree": "^5.0.1",
+            "esquery": "^1.0.1",
+            "esutils": "^2.0.2",
+            "file-entry-cache": "^5.0.1",
+            "functional-red-black-tree": "^1.0.1",
+            "glob": "^7.1.2",
+            "globals": "^11.7.0",
+            "ignore": "^4.0.6",
+            "import-fresh": "^3.0.0",
+            "imurmurhash": "^0.1.4",
+            "inquirer": "^6.2.2",
+            "js-yaml": "^3.13.0",
+            "json-stable-stringify-without-jsonify": "^1.0.1",
+            "levn": "^0.3.0",
+            "lodash": "^4.17.11",
+            "minimatch": "^3.0.4",
+            "mkdirp": "^0.5.1",
+            "natural-compare": "^1.4.0",
+            "optionator": "^0.8.2",
+            "path-is-inside": "^1.0.2",
+            "progress": "^2.0.0",
+            "regexpp": "^2.0.1",
+            "semver": "^5.5.1",
+            "strip-ansi": "^4.0.0",
+            "strip-json-comments": "^2.0.1",
+            "table": "^5.2.3",
+            "text-table": "^0.2.0"
+          }
+        }
+      }
+    },
+    "eslint-plugin-json": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-1.4.0.tgz",
+      "integrity": "sha512-CECvgRAWtUzuepdlPWd+VA7fhyF9HT183pZnl8wQw5x699Mk/MbME/q8xtULBfooi3LUbj6fToieNmsvUcDxWA==",
+      "dev": true,
+      "requires": {
+        "vscode-json-languageservice": "^3.2.1"
+      }
+    },
+    "eslint-plugin-no-jquery": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.0.0.tgz",
+      "integrity": "sha512-aFy3fMBlc630/qeasjocb9uIqmwoyOmmTQiBaDs70Aryqi9uPH0EZLPtIOshDMcGeAkyyAkcc+WuIw6bRsoLuw==",
+      "dev": true
+    },
+    "eslint-plugin-qunit": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-qunit/-/eslint-plugin-qunit-4.0.0.tgz",
+      "integrity": "sha512-+0i2xcYryUoLawi47Lp0iJKzkP931G5GXwIOq1KBKQc2pknV1VPjfE6b4mI2mR2RnL7WRoS30YjwC9SjQgJDXQ==",
+      "dev": true
+    },
+    "eslint-scope": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+      "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
+      "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
+      "dev": true
+    },
+    "eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+      "dev": true
+    },
+    "espree": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
+      "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+      "dev": true,
+      "requires": {
+        "acorn": "^6.0.7",
+        "acorn-jsx": "^5.0.0",
+        "eslint-visitor-keys": "^1.0.0"
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+    },
+    "esquery": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+      "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "eventemitter2": {
+      "version": "0.4.14",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+      "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
+      "dev": true
+    },
+    "eventemitter3": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+      "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
+      "dev": true
+    },
+    "execall": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/execall/-/execall-1.0.0.tgz",
+      "integrity": "sha1-c9CQTjlbPKsGWLCNCewlMH8pu3M=",
+      "dev": true,
+      "requires": {
+        "clone-regexp": "^1.0.0"
+      }
+    },
+    "exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+      "dev": true
+    },
+    "expand-braces": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
+      "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
+      "dev": true,
+      "requires": {
+        "array-slice": "^0.2.3",
+        "array-unique": "^0.2.1",
+        "braces": "^0.1.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
+          "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^0.1.0"
+          }
+        }
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "expand-range": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
+      "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
+      "dev": true,
+      "requires": {
+        "is-number": "^0.1.1",
+        "repeat-string": "^0.2.2"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
+          "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
+          "dev": true
+        },
+        "repeat-string": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
+          "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
+          "dev": true
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "external-editor": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
+      "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "dev": true,
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "2.2.7",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
+      "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==",
+      "dev": true,
+      "requires": {
+        "@mrmlnc/readdir-enhanced": "^2.2.1",
+        "@nodelib/fs.stat": "^1.1.2",
+        "glob-parent": "^3.1.0",
+        "is-glob": "^4.0.0",
+        "merge2": "^1.2.3",
+        "micromatch": "^3.1.10"
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "dev": true,
+      "requires": {
+        "websocket-driver": ">=0.5.1"
+      }
+    },
+    "fibers": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fibers/-/fibers-3.1.1.tgz",
+      "integrity": "sha512-dl3Ukt08rHVQfY8xGD0ODwyjwrRALtaghuqGH2jByYX1wpY+nAnRQjJ6Dbqq0DnVgNVQ9yibObzbF4IlPyiwPw==",
+      "dev": true,
+      "requires": {
+        "detect-libc": "^1.0.3"
+      }
+    },
+    "figures": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+      "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "file-sync-cmp": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+      "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
+      "dev": true
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.1",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.3.1",
+        "unpipe": "~1.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "statuses": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+          "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+          "dev": true
+        }
+      }
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "dev": true,
+      "requires": {
+        "path-exists": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "findup-sync": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+      "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+      "dev": true,
+      "requires": {
+        "glob": "~5.0.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "5.0.15",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+          "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+          "dev": true,
+          "requires": {
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "2 || 3",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        }
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      }
+    },
+    "flatted": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
+      "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
+      "dev": true
+    },
+    "follow-redirects": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
+      "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.2.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "dev": true,
+      "requires": {
+        "null-check": "^1.0.0"
+      }
+    },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+      "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+          "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+          "dev": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+          "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+          "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+          "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+          "dev": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+          "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+          "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+          "dev": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+          "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+          "dev": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+          "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+          "dev": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+          "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "deep-extend": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+          "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+          "dev": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+          "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+          "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
+          "dev": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
+          "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+          "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+          "dev": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+          "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+          "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+          "dev": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
+          "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+          "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+          "dev": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+          "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+          "dev": true
+        },
+        "minipass": {
+          "version": "2.3.5",
+          "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
+          "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
+          "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz",
+          "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^4.1.0",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.12.0",
+          "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
+          "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.1",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.2.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+          "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.6",
+          "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
+          "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
+          "dev": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
+          "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+          "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+          "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+          "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+          "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+          "dev": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+          "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+          "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+          "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+          "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.8",
+          "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+          "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.6.0",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+              "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.3",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+          "dev": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+          "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+          "dev": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+          "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+          "dev": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.7.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+          "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+          "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+          "dev": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+          "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.8",
+          "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
+          "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.1.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+          "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+          "dev": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+          "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2 || 2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+          "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
+          "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
+          "dev": true
+        }
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "gaze": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+      "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+      "dev": true,
+      "requires": {
+        "globule": "^1.0.0"
+      }
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true
+    },
+    "getobject": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz",
+      "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "glob-to-regexp": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+      "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
+      "dev": true
+    },
+    "global-modules": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+      "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+      "dev": true,
+      "requires": {
+        "global-prefix": "^3.0.0"
+      }
+    },
+    "global-prefix": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+      "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+      "dev": true,
+      "requires": {
+        "ini": "^1.3.5",
+        "kind-of": "^6.0.2",
+        "which": "^1.3.1"
+      }
+    },
+    "globals": {
+      "version": "11.11.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz",
+      "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==",
+      "dev": true
+    },
+    "globby": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
+      "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==",
+      "dev": true,
+      "requires": {
+        "@types/glob": "^7.1.1",
+        "array-union": "^1.0.2",
+        "dir-glob": "^2.2.2",
+        "fast-glob": "^2.2.6",
+        "glob": "^7.1.3",
+        "ignore": "^4.0.3",
+        "pify": "^4.0.1",
+        "slash": "^2.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        }
+      }
+    },
+    "globjoin": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+      "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=",
+      "dev": true
+    },
+    "globule": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+      "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+      "dev": true,
+      "requires": {
+        "glob": "~7.1.1",
+        "lodash": "~4.17.10",
+        "minimatch": "~3.0.2"
+      }
+    },
+    "gonzales-pe": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz",
+      "integrity": "sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "1.1.x"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz",
+          "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=",
+          "dev": true
+        }
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.15",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+      "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+      "dev": true
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
+    "grunt": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.4.tgz",
+      "integrity": "sha512-PYsMOrOC+MsdGEkFVwMaMyc6Ob7pKmq+deg1Sjr+vvMWp35sztfwKE7qoN51V+UEtHsyNuMcGdgMLFkBHvMxHQ==",
+      "dev": true,
+      "requires": {
+        "coffeescript": "~1.10.0",
+        "dateformat": "~1.0.12",
+        "eventemitter2": "~0.4.13",
+        "exit": "~0.1.1",
+        "findup-sync": "~0.3.0",
+        "glob": "~7.0.0",
+        "grunt-cli": "~1.2.0",
+        "grunt-known-options": "~1.1.0",
+        "grunt-legacy-log": "~2.0.0",
+        "grunt-legacy-util": "~1.1.1",
+        "iconv-lite": "~0.4.13",
+        "js-yaml": "~3.13.0",
+        "minimatch": "~3.0.2",
+        "mkdirp": "~0.5.1",
+        "nopt": "~3.0.6",
+        "path-is-absolute": "~1.0.0",
+        "rimraf": "~2.6.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.0.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+          "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.2",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "grunt-cli": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz",
+          "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=",
+          "dev": true,
+          "requires": {
+            "findup-sync": "~0.3.0",
+            "grunt-known-options": "~1.1.0",
+            "nopt": "~3.0.6",
+            "resolve": "~1.1.0"
+          }
+        },
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+          "dev": true
+        }
+      }
+    },
+    "grunt-banana-checker": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/grunt-banana-checker/-/grunt-banana-checker-0.7.0.tgz",
+      "integrity": "sha512-HmHSK7IFIo5ygDjhjtdrpNATg3Pjb6OJLibJadWhFHDtXLcaEpkNU2tExmS6tgSaBdiprhXEd231w4dSsfHFAQ==",
+      "dev": true
+    },
+    "grunt-contrib-copy": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+      "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.1",
+        "file-sync-cmp": "^0.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "grunt-contrib-watch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz",
+      "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==",
+      "dev": true,
+      "requires": {
+        "async": "^2.6.0",
+        "gaze": "^1.1.0",
+        "lodash": "^4.17.10",
+        "tiny-lr": "^1.1.1"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+          "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.11"
+          }
+        }
+      }
+    },
+    "grunt-eslint": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-21.0.0.tgz",
+      "integrity": "sha512-HJocD9P35lpCvy6pPPCTgzBavzckrT1nt7lpqV55Vy8E6LQJv4RortXoH1jJTYhO5DYY7RPATv7Uc4383PUYqQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.1.0",
+        "eslint": "^5.0.0"
+      }
+    },
+    "grunt-karma": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/grunt-karma/-/grunt-karma-3.0.2.tgz",
+      "integrity": "sha512-imNhQO1bR1O7X6/3F5vO0o7mKy4xdkpSd40QVfxGO70cBAFcOqjv2Zu5QzsfEsSrppuu3N0vIQPbfBRjeGdpWg==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.10"
+      }
+    },
+    "grunt-known-options": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz",
+      "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==",
+      "dev": true
+    },
+    "grunt-legacy-log": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz",
+      "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==",
+      "dev": true,
+      "requires": {
+        "colors": "~1.1.2",
+        "grunt-legacy-log-utils": "~2.0.0",
+        "hooker": "~0.2.3",
+        "lodash": "~4.17.5"
+      }
+    },
+    "grunt-legacy-log-utils": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz",
+      "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==",
+      "dev": true,
+      "requires": {
+        "chalk": "~2.4.1",
+        "lodash": "~4.17.10"
+      }
+    },
+    "grunt-legacy-util": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz",
+      "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==",
+      "dev": true,
+      "requires": {
+        "async": "~1.5.2",
+        "exit": "~0.1.1",
+        "getobject": "~0.1.0",
+        "hooker": "~0.2.3",
+        "lodash": "~4.17.10",
+        "underscore.string": "~3.3.4",
+        "which": "~1.3.0"
+      }
+    },
+    "grunt-stylelint": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/grunt-stylelint/-/grunt-stylelint-0.11.0.tgz",
+      "integrity": "sha512-/6LOPh8ftRS70tKa676ZrGG+eNCQQHJPH5QWe4gmzdW+K3Ud0YwbmUe1Bly3x9ymfllNTCALRmMJoV9xEh9RFA==",
+      "dev": true,
+      "requires": {
+        "chalk": "2.4.2"
+      }
+    },
+    "grunt-svgmin": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-svgmin/-/grunt-svgmin-5.0.0.tgz",
+      "integrity": "sha1-8O4pOtFi++hcjD5o2xUt/3J3qCQ=",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.3.0",
+        "each-async": "^1.1.1",
+        "log-symbols": "^2.1.0",
+        "pretty-bytes": "^4.0.2",
+        "svgo": "^1.0.3"
+      }
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        }
+      }
+    },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "dev": true,
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+          "dev": true
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "dev": true,
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "hawk": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
+      "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
+      "dev": true,
+      "requires": {
+        "boom": "4.x.x",
+        "cryptiles": "3.x.x",
+        "hoek": "4.x.x",
+        "sntp": "2.x.x"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "hoek": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
+      "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==",
+      "dev": true
+    },
+    "hooker": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+      "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=",
+      "dev": true
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "html-tags": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz",
+      "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=",
+      "dev": true
+    },
+    "htmlparser2": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.1",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^3.1.1"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+          "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
+          "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "http-errors": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+      "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+      "dev": true,
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.1",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.0"
+      }
+    },
+    "http-parser-js": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz",
+      "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==",
+      "dev": true
+    },
+    "http-proxy": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
+      "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "^3.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "humanize-duration": {
+      "version": "3.18.0",
+      "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.18.0.tgz",
+      "integrity": "sha512-reYy4EJMqlhX13TDlgSqLYfVGKOoixoEzsSL6DBlp22dScWN8Q2eMgDF4L0q28mzbgO40rnBy3WyEUQEhfYALw==",
+      "dev": true
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
+      "dev": true
+    },
+    "ignore": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
+      "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "import-lazy": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz",
+      "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==",
+      "dev": true
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "indexes-of": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+      "dev": true
+    },
+    "inquirer": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
+      "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^3.2.0",
+        "chalk": "^2.4.2",
+        "cli-cursor": "^2.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^2.0.0",
+        "lodash": "^4.17.11",
+        "mute-stream": "0.0.7",
+        "run-async": "^2.2.0",
+        "rxjs": "^6.4.0",
+        "string-width": "^2.1.0",
+        "strip-ansi": "^5.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-alphabetical": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.3.tgz",
+      "integrity": "sha512-eEMa6MKpHFzw38eKm56iNNi6GJ7lf6aLLio7Kr23sJPAECscgRtZvOBYybejWDQ2bM949Y++61PY+udzj5QMLA==",
+      "dev": true
+    },
+    "is-alphanumeric": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz",
+      "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=",
+      "dev": true
+    },
+    "is-alphanumerical": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.3.tgz",
+      "integrity": "sha512-A1IGAPO5AW9vSh7omxIlOGwIqEvpW/TA+DksVOPM5ODuxKlZS09+TEM1E3275lJqO2oJ38vDpeAL3DCIiHE6eA==",
+      "dev": true,
+      "requires": {
+        "is-alphabetical": "^1.0.0",
+        "is-decimal": "^1.0.0"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+      "dev": true
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+      "dev": true
+    },
+    "is-decimal": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.3.tgz",
+      "integrity": "sha512-bvLSwoDg2q6Gf+E2LEPiklHZxxiSi3XAh4Mav65mKqTfCO1HM3uBs24TjEH8iJX3bbDdLXKJXBTmGzuTUuAEjQ==",
+      "dev": true
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "dev": true,
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "is-directory": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+      "dev": true
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-hexadecimal": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.3.tgz",
+      "integrity": "sha512-zxQ9//Q3D/34poZf8fiy3m3XVpbQc7ren15iKqrTtLPwkPD/t3Scy9Imp63FujULGxuK0ZlCwoo5xNpktFgbOA==",
+      "dev": true
+    },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+      "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+      "dev": true
+    },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+      "dev": true
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+      "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
+      "dev": true
+    },
+    "is-supported-regexp-flag": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz",
+      "integrity": "sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==",
+      "dev": true
+    },
+    "is-symbol": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+      "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
+      "dev": true,
+      "requires": {
+        "has-symbols": "^1.0.0"
+      }
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "is-whitespace-character": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.3.tgz",
+      "integrity": "sha512-SNPgMLz9JzPccD3nPctcj8sZlX9DAMJSKH8bP7Z6bohCwuNgX8xbWr1eTAYXX9Vpi/aSn8Y1akL9WgM3t43YNQ==",
+      "dev": true
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true
+    },
+    "is-word-character": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.3.tgz",
+      "integrity": "sha512-0wfcrFgOOOBdgRNT9H33xe6Zi6yhX/uoc4U8NBZGeQQB0ctU1dnlNTyL9JM2646bHDTpsDm1Brb3VPoCIMrd/A==",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
+    "isbinaryfile": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
+      "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc": "^1.2.0"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "jpeg-js": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.5.tgz",
+      "integrity": "sha512-hvaExqwmQDS8O9qnZAVDXGWU43Tbu1V0wMZmjROjT11jloSgGICZpscG+P6Nyi1BVAvyu2ARRx8qmEW30sxgdQ==",
+      "dev": true
+    },
+    "js-base64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
+      "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
+      "dev": true
+    },
+    "js-reporters": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/js-reporters/-/js-reporters-1.2.1.tgz",
+      "integrity": "sha1-+IxgjjJKM3OpW8xFrTBeXJecRZs=",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json5": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
+      "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "jsonc-parser": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.1.0.tgz",
+      "integrity": "sha512-n9GrT8rrr2fhvBbANa1g+xFmgGK5X91KFeDwlKQ3+SJfmH5+tKv/M/kahx/TXOMflfWHKGKqKyfHQaLKTNzJ6w==",
+      "dev": true
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "junit-report-builder": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-1.3.2.tgz",
+      "integrity": "sha512-TPpe1hWatrBnBxiRT1M8ss6nCaaoEzZ0fFEdRkv45jVwrpZm9HAqNz1vBVfsrN4Z2PLwhIxpxPAoWfW/b5Kzpw==",
+      "dev": true,
+      "requires": {
+        "date-format": "0.0.2",
+        "lodash": "^4.17.10",
+        "mkdirp": "^0.5.0",
+        "xmlbuilder": "^10.0.0"
+      },
+      "dependencies": {
+        "date-format": {
+          "version": "0.0.2",
+          "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.2.tgz",
+          "integrity": "sha1-+v1Ej3IRXvHitzkVWukvK+bCjdE=",
+          "dev": true
+        }
+      }
+    },
+    "karma": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-3.1.4.tgz",
+      "integrity": "sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.3.0",
+        "body-parser": "^1.16.1",
+        "chokidar": "^2.0.3",
+        "colors": "^1.1.0",
+        "combine-lists": "^1.0.0",
+        "connect": "^3.6.0",
+        "core-js": "^2.2.0",
+        "di": "^0.0.1",
+        "dom-serialize": "^2.2.0",
+        "expand-braces": "^0.1.1",
+        "flatted": "^2.0.0",
+        "glob": "^7.1.1",
+        "graceful-fs": "^4.1.2",
+        "http-proxy": "^1.13.0",
+        "isbinaryfile": "^3.0.0",
+        "lodash": "^4.17.5",
+        "log4js": "^3.0.0",
+        "mime": "^2.3.1",
+        "minimatch": "^3.0.2",
+        "optimist": "^0.6.1",
+        "qjobs": "^1.1.4",
+        "range-parser": "^1.2.0",
+        "rimraf": "^2.6.0",
+        "safe-buffer": "^5.0.1",
+        "socket.io": "2.1.1",
+        "source-map": "^0.6.1",
+        "tmp": "0.0.33",
+        "useragent": "2.3.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "karma-chrome-launcher": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
+      "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+      "dev": true,
+      "requires": {
+        "fs-access": "^1.0.0",
+        "which": "^1.2.1"
+      }
+    },
+    "karma-firefox-launcher": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz",
+      "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==",
+      "dev": true
+    },
+    "karma-mocha-reporter": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz",
+      "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.1.0",
+        "log-symbols": "^2.1.0",
+        "strip-ansi": "^4.0.0"
+      }
+    },
+    "karma-qunit": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/karma-qunit/-/karma-qunit-2.1.0.tgz",
+      "integrity": "sha512-QFt2msjpFNx1ZqB1EcD7rXaFRa3P+kLrgm6uRDYV/1MO7qGMxnTDgsFB1KyAKCpMreOmB5MMpEm5sX52j4c0aw==",
+      "dev": true
+    },
+    "kind-of": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+      "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+      "dev": true
+    },
+    "known-css-properties": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.13.0.tgz",
+      "integrity": "sha512-6VWDxNr7cQXPDtMdCWLZMK3E8hdLrpyPPRdx6RbyvqklqgM6/XNFsVopv8QOZ+hRB6iHG/urEDwzlWbmMCv/kw==",
+      "dev": true
+    },
+    "lazystream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
+      "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "^2.0.5"
+      }
+    },
+    "leven": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+      "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+      "dev": true
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "livereload-js": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz",
+      "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==",
+      "dev": true
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "dependencies": {
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        }
+      }
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "dev": true
+    },
+    "log-symbols": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+      "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.1"
+      }
+    },
+    "log4js": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.6.tgz",
+      "integrity": "sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==",
+      "dev": true,
+      "requires": {
+        "circular-json": "^0.5.5",
+        "date-format": "^1.2.0",
+        "debug": "^3.1.0",
+        "rfdc": "^1.1.2",
+        "streamroller": "0.7.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "longest-streak": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.3.tgz",
+      "integrity": "sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw==",
+      "dev": true
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "^0.4.1",
+        "signal-exit": "^3.0.0"
+      }
+    },
+    "lru-cache": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dev": true,
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "dev": true,
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "markdown-escapes": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.3.tgz",
+      "integrity": "sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==",
+      "dev": true
+    },
+    "markdown-table": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz",
+      "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==",
+      "dev": true
+    },
+    "mathml-tag-names": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz",
+      "integrity": "sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw==",
+      "dev": true
+    },
+    "mdast-util-compact": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.3.tgz",
+      "integrity": "sha512-nRiU5GpNy62rZppDKbLwhhtw5DXoFMqw9UNZFmlPsNaQCZ//WLjGKUwWMdJrUH+Se7UvtO2gXtAMe0g/N+eI5w==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit": "^1.1.0"
+      }
+    },
+    "mdn-data": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
+      "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
+      "dev": true
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "requires": {
+        "camelcase-keys": "^2.0.0",
+        "decamelize": "^1.1.2",
+        "loud-rejection": "^1.0.0",
+        "map-obj": "^1.0.1",
+        "minimist": "^1.1.3",
+        "normalize-package-data": "^2.3.4",
+        "object-assign": "^4.0.1",
+        "read-pkg-up": "^1.0.1",
+        "redent": "^1.0.0",
+        "trim-newlines": "^1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "merge2": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz",
+      "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
+    "mime": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz",
+      "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+      "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.22",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
+      "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
+      "dev": true,
+      "requires": {
+        "mime-db": "~1.38.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "dev": true
+    },
+    "minimist-options": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz",
+      "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0"
+      }
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "ms": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+      "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+      "dev": true
+    },
+    "mute-stream": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+      "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+      "dev": true
+    },
+    "mwbot": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/mwbot/-/mwbot-1.0.10.tgz",
+      "integrity": "sha1-pEC9ZmOnYoq1t5lgnpjLL8ThM8k=",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.4.6",
+        "request": "^2.75.0",
+        "semlog": "^0.6.10"
+      }
+    },
+    "nan": {
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+      "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+      "dev": true,
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+      "dev": true
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "node-releases": {
+      "version": "1.1.23",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.23.tgz",
+      "integrity": "sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==",
+      "dev": true,
+      "requires": {
+        "semver": "^5.3.0"
+      }
+    },
+    "node-watch": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.0.tgz",
+      "integrity": "sha512-XAgTL05z75ptd7JSVejH1a2Dm1zmXYhuDr9l230Qk6Z7/7GPcnAs/UyJJ4ggsXSvWil8iOzwQLW0zuGUvHpG8g==",
+      "dev": true
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "normalize-selector": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz",
+      "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
+      "dev": true
+    },
+    "npm-install-package": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/npm-install-package/-/npm-install-package-2.1.0.tgz",
+      "integrity": "sha1-1+/jz816sAYUuJbqUxGdyaslkSU=",
+      "dev": true
+    },
+    "nth-check": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
+      "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0"
+      }
+    },
+    "null-check": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
+      "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
+      "dev": true
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
+      "dev": true
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "dev": true,
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.5.1"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "object.values": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz",
+      "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.12.0",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+      "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^1.0.0"
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "parent-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz",
+      "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-entities": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
+      "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==",
+      "dev": true,
+      "requires": {
+        "character-entities": "^1.0.0",
+        "character-entities-legacy": "^1.0.0",
+        "character-reference-invalid": "^1.0.0",
+        "is-alphanumerical": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-hexadecimal": "^1.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+      "dev": true
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+      "dev": true,
+      "requires": {
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "picomatch": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz",
+      "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==",
+      "dev": true
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+      "dev": true
+    },
+    "postcss": {
+      "version": "5.2.18",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+      "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "js-base64": "^2.1.9",
+        "source-map": "^0.5.6",
+        "supports-color": "^3.2.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          },
+          "dependencies": {
+            "supports-color": {
+              "version": "2.0.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+              "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+              "dev": true
+            }
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "postcss-html": {
+      "version": "0.36.0",
+      "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz",
+      "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==",
+      "dev": true,
+      "requires": {
+        "htmlparser2": "^3.10.0"
+      }
+    },
+    "postcss-jsx": {
+      "version": "0.36.1",
+      "resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.36.1.tgz",
+      "integrity": "sha512-xaZpy01YR7ijsFUtu5rViYCFHurFIPHir+faiOQp8g/NfTfWqZCKDhKrydQZ4d8WlSAmVdXGwLjpFbsNUI26Sw==",
+      "dev": true,
+      "requires": {
+        "@babel/core": ">=7.2.2"
+      }
+    },
+    "postcss-less": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-2.0.0.tgz",
+      "integrity": "sha512-pPNsVnpCB13nBMOcl5GVh8JGmB0JGFjqkLUDzKdVpptFFKEe9wFdEzvh2j4lD2AD+7qcrUfw9Ta+oi5+Fw7jjQ==",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.2.16"
+      }
+    },
+    "postcss-markdown": {
+      "version": "0.36.0",
+      "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz",
+      "integrity": "sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==",
+      "dev": true,
+      "requires": {
+        "remark": "^10.0.1",
+        "unist-util-find-all-after": "^1.0.2"
+      }
+    },
+    "postcss-media-query-parser": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+      "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=",
+      "dev": true
+    },
+    "postcss-reporter": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz",
+      "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "lodash": "^4.17.11",
+        "log-symbols": "^2.2.0",
+        "postcss": "^7.0.7"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "postcss-resolve-nested-selector": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
+      "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=",
+      "dev": true
+    },
+    "postcss-safe-parser": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz",
+      "integrity": "sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.0"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "postcss-sass": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.3.5.tgz",
+      "integrity": "sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A==",
+      "dev": true,
+      "requires": {
+        "gonzales-pe": "^4.2.3",
+        "postcss": "^7.0.1"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "postcss-scss": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.0.0.tgz",
+      "integrity": "sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.0"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
+      "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+      "dev": true,
+      "requires": {
+        "dot-prop": "^4.1.1",
+        "indexes-of": "^1.0.1",
+        "uniq": "^1.0.1"
+      }
+    },
+    "postcss-syntax": {
+      "version": "0.36.2",
+      "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz",
+      "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==",
+      "dev": true
+    },
+    "postcss-value-parser": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+      "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "pretty-bytes": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
+      "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=",
+      "dev": true
+    },
+    "prettyjson": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.1.tgz",
+      "integrity": "sha1-/P+rQdGcq0365eV15kJGYZsS0ok=",
+      "dev": true,
+      "requires": {
+        "colors": "^1.1.2",
+        "minimist": "^1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+      "dev": true
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.1.31",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+      "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
+      "dev": true
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "q": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+      "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
+      "dev": true
+    },
+    "qjobs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+      "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.7.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+      "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
+      "dev": true
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "quick-lru": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
+      "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=",
+      "dev": true
+    },
+    "qunit": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.9.1.tgz",
+      "integrity": "sha512-ipXgW4SD557GrQtiBhj+g7eHk76pmSIYKglEXuAD/WsC06XzXDc4r9qlm4DSG5LxqxvpgK8naGlJ1Zcnj9/NdQ==",
+      "dev": true,
+      "requires": {
+        "commander": "2.12.2",
+        "js-reporters": "1.2.1",
+        "minimatch": "3.0.4",
+        "node-watch": "0.6.0",
+        "resolve": "1.5.0"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
+          "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.5"
+          }
+        }
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
+      "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=",
+      "dev": true,
+      "requires": {
+        "bytes": "1",
+        "string_decoder": "0.10"
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+      "dev": true,
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      },
+      "dependencies": {
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "readdirp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "requires": {
+        "indent-string": "^2.1.0",
+        "strip-indent": "^1.0.1"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "dev": true
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "remark": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz",
+      "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==",
+      "dev": true,
+      "requires": {
+        "remark-parse": "^6.0.0",
+        "remark-stringify": "^6.0.0",
+        "unified": "^7.0.0"
+      }
+    },
+    "remark-parse": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz",
+      "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==",
+      "dev": true,
+      "requires": {
+        "collapse-white-space": "^1.0.2",
+        "is-alphabetical": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-whitespace-character": "^1.0.0",
+        "is-word-character": "^1.0.0",
+        "markdown-escapes": "^1.0.0",
+        "parse-entities": "^1.1.0",
+        "repeat-string": "^1.5.4",
+        "state-toggle": "^1.0.0",
+        "trim": "0.0.1",
+        "trim-trailing-lines": "^1.0.0",
+        "unherit": "^1.0.4",
+        "unist-util-remove-position": "^1.0.0",
+        "vfile-location": "^2.0.0",
+        "xtend": "^4.0.1"
+      }
+    },
+    "remark-stringify": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz",
+      "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==",
+      "dev": true,
+      "requires": {
+        "ccount": "^1.0.0",
+        "is-alphanumeric": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-whitespace-character": "^1.0.0",
+        "longest-streak": "^2.0.1",
+        "markdown-escapes": "^1.0.0",
+        "markdown-table": "^1.1.0",
+        "mdast-util-compact": "^1.0.0",
+        "parse-entities": "^1.0.2",
+        "repeat-string": "^1.5.4",
+        "state-toggle": "^1.0.0",
+        "stringify-entities": "^1.0.1",
+        "unherit": "^1.0.4",
+        "xtend": "^4.0.1"
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "replace-ext": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+      "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
+      "dev": true
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+          "dev": true
+        }
+      }
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
+      "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
+      "dev": true,
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true
+    },
+    "restore-cursor": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+      "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+      "dev": true,
+      "requires": {
+        "onetime": "^2.0.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
+    "rfdc": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
+      "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==",
+      "dev": true
+    },
+    "rgb2hex": {
+      "version": "0.1.9",
+      "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.1.9.tgz",
+      "integrity": "sha512-32iuQzhOjyT+cv9aAFRBJ19JgHwzQwbjUhH3Fj2sWW2EEGAW8fpFrDFP5ndoKDxJaLO06x1hE3kyuIFrUQtybQ==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-async": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "rx-lite": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+      "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=",
+      "dev": true
+    },
+    "rx-lite-aggregates": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+      "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+      "dev": true,
+      "requires": {
+        "rx-lite": "*"
+      }
+    },
+    "rxjs": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
+      "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "safe-json-parse": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz",
+      "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=",
+      "dev": true
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "sauce-connect-launcher": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/sauce-connect-launcher/-/sauce-connect-launcher-1.2.6.tgz",
+      "integrity": "sha512-yBTYfzI6AWRwoXJoIqmVgz+eCpWX6CsJ4Ap8fowjsGlN+27OKbnQxv6POd4Rzh57BH9WeA9K8orIzNxO8mMBQA==",
+      "dev": true,
+      "requires": {
+        "adm-zip": "~0.4.3",
+        "async": "^2.1.2",
+        "https-proxy-agent": "^2.2.1",
+        "lodash": "^4.16.6",
+        "rimraf": "^2.5.4"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+          "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.11"
+          }
+        }
+      }
+    },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+      "dev": true
+    },
+    "semlog": {
+      "version": "0.6.10",
+      "resolved": "https://registry.npmjs.org/semlog/-/semlog-0.6.10.tgz",
+      "integrity": "sha1-DyJa6o6zwvJM6TWNhnjQ9Bp/4Fs=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "prettyjson": "^1.1.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "semver": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+      "dev": true
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setprototypeof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+      "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
+      "dev": true
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "slash": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+      "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+      "dev": true
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      }
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "dev": true,
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "sntp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz",
+      "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==",
+      "dev": true,
+      "requires": {
+        "hoek": "4.x.x"
+      }
+    },
+    "socket.io": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+      "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+      "dev": true,
+      "requires": {
+        "debug": "~3.1.0",
+        "engine.io": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.1.1",
+        "socket.io-parser": "~3.2.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=",
+      "dev": true
+    },
+    "socket.io-client": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+      "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+      "dev": true,
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.2.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+      "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "dev": true,
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "dev": true
+    },
+    "spdx-correct": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
+      "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==",
+      "dev": true
+    },
+    "specificity": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
+      "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
+      "dev": true
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "sshpk": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+      "dev": true,
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "stable": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+      "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+      "dev": true
+    },
+    "state-toggle": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.2.tgz",
+      "integrity": "sha512-8LpelPGR0qQM4PnfLiplOQNJcIN1/r2Gy0xKB2zKnIW2YzPMt2sR4I/+gtPjhN7Svh9kw+zqEg2SFwpBO9iNiw==",
+      "dev": true
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "dev": true,
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+      "dev": true
+    },
+    "streamroller": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
+      "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==",
+      "dev": true,
+      "requires": {
+        "date-format": "^1.2.0",
+        "debug": "^3.1.0",
+        "mkdirp": "^0.5.1",
+        "readable-stream": "^2.3.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "string-template": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
+      "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=",
+      "dev": true
+    },
+    "string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "dev": true,
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "0.10.31",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+      "dev": true
+    },
+    "stringify-entities": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
+      "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==",
+      "dev": true,
+      "requires": {
+        "character-entities-html4": "^1.0.0",
+        "character-entities-legacy": "^1.0.0",
+        "is-alphanumerical": "^1.0.0",
+        "is-hexadecimal": "^1.0.0"
+      }
+    },
+    "stringstream": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
+      "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==",
+      "dev": true
+    },
+    "strip-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+      "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^3.0.0"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^4.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+      "dev": true
+    },
+    "style-search": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
+      "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=",
+      "dev": true
+    },
+    "stylelint": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-10.0.1.tgz",
+      "integrity": "sha512-NbpD9BvQRmPe7QfaLB2OqhhDr5g6SAn43AAH2XLyqtQ9ZcioQECgadkIbormfhzxLhccAQWBZbVNiZz1oqEf8g==",
+      "dev": true,
+      "requires": {
+        "autoprefixer": "^9.5.1",
+        "balanced-match": "^1.0.0",
+        "chalk": "^2.4.2",
+        "cosmiconfig": "^5.2.0",
+        "debug": "^4.1.1",
+        "execall": "^1.0.0",
+        "file-entry-cache": "^5.0.1",
+        "get-stdin": "^7.0.0",
+        "global-modules": "^2.0.0",
+        "globby": "^9.2.0",
+        "globjoin": "^0.1.4",
+        "html-tags": "^2.0.0",
+        "ignore": "^5.0.6",
+        "import-lazy": "^3.1.0",
+        "imurmurhash": "^0.1.4",
+        "known-css-properties": "^0.13.0",
+        "leven": "^3.1.0",
+        "lodash": "^4.17.11",
+        "log-symbols": "^2.2.0",
+        "mathml-tag-names": "^2.1.0",
+        "meow": "^5.0.0",
+        "micromatch": "^4.0.0",
+        "normalize-selector": "^0.2.0",
+        "pify": "^4.0.1",
+        "postcss": "^7.0.14",
+        "postcss-html": "^0.36.0",
+        "postcss-jsx": "^0.36.0",
+        "postcss-less": "^3.1.4",
+        "postcss-markdown": "^0.36.0",
+        "postcss-media-query-parser": "^0.2.3",
+        "postcss-reporter": "^6.0.1",
+        "postcss-resolve-nested-selector": "^0.1.1",
+        "postcss-safe-parser": "^4.0.1",
+        "postcss-sass": "^0.3.5",
+        "postcss-scss": "^2.0.0",
+        "postcss-selector-parser": "^3.1.0",
+        "postcss-syntax": "^0.36.2",
+        "postcss-value-parser": "^3.3.1",
+        "resolve-from": "^5.0.0",
+        "signal-exit": "^3.0.2",
+        "slash": "^2.0.0",
+        "specificity": "^0.4.1",
+        "string-width": "^4.1.0",
+        "style-search": "^0.1.0",
+        "sugarss": "^2.0.0",
+        "svg-tags": "^1.0.0",
+        "table": "^5.2.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "braces": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+          "dev": true,
+          "requires": {
+            "fill-range": "^7.0.1"
+          }
+        },
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "camelcase-keys": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz",
+          "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0",
+            "map-obj": "^2.0.0",
+            "quick-lru": "^1.0.0"
+          }
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
+        "fill-range": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+          "dev": true,
+          "requires": {
+            "to-regex-range": "^5.0.1"
+          }
+        },
+        "find-up": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+          "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+          "dev": true,
+          "requires": {
+            "locate-path": "^2.0.0"
+          }
+        },
+        "get-stdin": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
+          "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+          "dev": true
+        },
+        "ignore": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz",
+          "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==",
+          "dev": true
+        },
+        "indent-string": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+          "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "is-number": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+          "dev": true
+        },
+        "load-json-file": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+          "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "parse-json": "^4.0.0",
+            "pify": "^3.0.0",
+            "strip-bom": "^3.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+              "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+              "dev": true
+            }
+          }
+        },
+        "map-obj": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz",
+          "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=",
+          "dev": true
+        },
+        "meow": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz",
+          "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==",
+          "dev": true,
+          "requires": {
+            "camelcase-keys": "^4.0.0",
+            "decamelize-keys": "^1.0.0",
+            "loud-rejection": "^1.0.0",
+            "minimist-options": "^3.0.1",
+            "normalize-package-data": "^2.3.4",
+            "read-pkg-up": "^3.0.0",
+            "redent": "^2.0.0",
+            "trim-newlines": "^2.0.0",
+            "yargs-parser": "^10.0.0"
+          }
+        },
+        "micromatch": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+          "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+          "dev": true,
+          "requires": {
+            "braces": "^3.0.1",
+            "picomatch": "^2.0.5"
+          }
+        },
+        "parse-json": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+          "dev": true,
+          "requires": {
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1"
+          }
+        },
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "dev": true,
+          "requires": {
+            "pify": "^3.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+              "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+              "dev": true
+            }
+          }
+        },
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        },
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "postcss-less": {
+          "version": "3.1.4",
+          "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz",
+          "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==",
+          "dev": true,
+          "requires": {
+            "postcss": "^7.0.14"
+          }
+        },
+        "read-pkg": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+          "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "^4.0.0",
+            "normalize-package-data": "^2.3.2",
+            "path-type": "^3.0.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz",
+          "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=",
+          "dev": true,
+          "requires": {
+            "find-up": "^2.0.0",
+            "read-pkg": "^3.0.0"
+          }
+        },
+        "redent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz",
+          "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=",
+          "dev": true,
+          "requires": {
+            "indent-string": "^3.0.0",
+            "strip-indent": "^2.0.0"
+          }
+        },
+        "resolve-from": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz",
+          "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^5.2.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        },
+        "strip-indent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
+          "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        },
+        "to-regex-range": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+          "dev": true,
+          "requires": {
+            "is-number": "^7.0.0"
+          }
+        },
+        "trim-newlines": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz",
+          "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=",
+          "dev": true
+        }
+      }
+    },
+    "stylelint-config-wikimedia": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/stylelint-config-wikimedia/-/stylelint-config-wikimedia-0.6.0.tgz",
+      "integrity": "sha512-7KqgpWCjKc2zT0JBI+BmSn/lzwHga056dZw6H//2qEbs1XRmR25gTGPsiMYOm3jVUlE0OSbuaOdyuE1KOlz/5w==",
+      "dev": true,
+      "requires": {
+        "stylelint": "10.0.1"
+      }
+    },
+    "sugarss": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz",
+      "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.2"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "7.0.17",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+          "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.2",
+            "source-map": "^0.6.1",
+            "supports-color": "^6.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "svg-tags": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+      "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
+      "dev": true
+    },
+    "svgo": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.2.tgz",
+      "integrity": "sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "coa": "^2.0.2",
+        "css-select": "^2.0.0",
+        "css-select-base-adapter": "^0.1.1",
+        "css-tree": "1.0.0-alpha.28",
+        "css-url-regex": "^1.1.0",
+        "csso": "^3.5.1",
+        "js-yaml": "^3.13.1",
+        "mkdirp": "~0.5.1",
+        "object.values": "^1.1.0",
+        "sax": "~1.2.4",
+        "stable": "^0.1.8",
+        "unquote": "~1.1.1",
+        "util.promisify": "~1.0.0"
+      },
+      "dependencies": {
+        "js-yaml": {
+          "version": "3.13.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        }
+      }
+    },
+    "table": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz",
+      "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.9.1",
+        "lodash": "^4.17.11",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "tar-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
+      "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
+      "dev": true,
+      "requires": {
+        "bl": "^1.0.0",
+        "buffer-alloc": "^1.2.0",
+        "end-of-stream": "^1.0.0",
+        "fs-constants": "^1.0.0",
+        "readable-stream": "^2.3.0",
+        "to-buffer": "^1.1.1",
+        "xtend": "^4.0.0"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "tiny-lr": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz",
+      "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==",
+      "dev": true,
+      "requires": {
+        "body": "^5.1.0",
+        "debug": "^3.1.0",
+        "faye-websocket": "~0.10.0",
+        "livereload-js": "^2.3.0",
+        "object-assign": "^4.1.0",
+        "qs": "^6.4.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
+      "dev": true
+    },
+    "to-buffer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
+      "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+      "dev": true
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+      "dev": true
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "trim": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+      "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+      "dev": true
+    },
+    "trim-trailing-lines": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.2.tgz",
+      "integrity": "sha512-MUjYItdrqqj2zpcHFTkMa9WAv4JHTI6gnRQGPFLrt5L9a6tRMiDnIqYl8JBvu2d2Tc3lWJKQwlGCp0K8AvCM+Q==",
+      "dev": true
+    },
+    "trough": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.4.tgz",
+      "integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==",
+      "dev": true
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.40.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+          "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.24",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+          "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.40.0"
+          }
+        }
+      }
+    },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
+      "dev": true
+    },
+    "underscore.string": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
+      "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "^1.0.3",
+        "util-deprecate": "^1.0.2"
+      }
+    },
+    "unherit": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.2.tgz",
+      "integrity": "sha512-W3tMnpaMG7ZY6xe/moK04U9fBhi6wEiCYHUW5Mop/wQHf12+79EQGwxYejNdhEz2mkqkBlGwm7pxmgBKMVUj0w==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "xtend": "^4.0.1"
+      }
+    },
+    "unified": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz",
+      "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "@types/vfile": "^3.0.0",
+        "bail": "^1.0.0",
+        "extend": "^3.0.0",
+        "is-plain-obj": "^1.1.0",
+        "trough": "^1.0.0",
+        "vfile": "^3.0.0",
+        "x-is-string": "^0.1.0"
+      }
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "uniq": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
+    },
+    "unist-util-find-all-after": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.4.tgz",
+      "integrity": "sha512-CaxvMjTd+yF93BKLJvZnEfqdM7fgEACsIpQqz8vIj9CJnUb9VpyymFS3tg6TCtgrF7vfCJBF5jbT2Ox9CBRYRQ==",
+      "dev": true,
+      "requires": {
+        "unist-util-is": "^3.0.0"
+      }
+    },
+    "unist-util-is": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz",
+      "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==",
+      "dev": true
+    },
+    "unist-util-remove-position": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.3.tgz",
+      "integrity": "sha512-CtszTlOjP2sBGYc2zcKA/CvNdTdEs3ozbiJ63IPBxh8iZg42SCCb8m04f8z2+V1aSk5a7BxbZKEdoDjadmBkWA==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit": "^1.1.0"
+      }
+    },
+    "unist-util-stringify-position": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
+      "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==",
+      "dev": true
+    },
+    "unist-util-visit": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz",
+      "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit-parents": "^2.0.0"
+      }
+    },
+    "unist-util-visit-parents": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz",
+      "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==",
+      "dev": true,
+      "requires": {
+        "unist-util-is": "^3.0.0"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
+    "unquote": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
+      "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
+      "dev": true
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "dev": true,
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "dev": true,
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "dev": true,
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+          "dev": true
+        }
+      }
+    },
+    "upath": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+      "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true
+    },
+    "useragent": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+      "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.x",
+        "tmp": "0.0.x"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "util.promisify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
+      "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "object.getownpropertydescriptors": "^2.0.3"
+      }
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vfile": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz",
+      "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==",
+      "dev": true,
+      "requires": {
+        "is-buffer": "^2.0.0",
+        "replace-ext": "1.0.0",
+        "unist-util-stringify-position": "^1.0.0",
+        "vfile-message": "^1.0.0"
+      },
+      "dependencies": {
+        "is-buffer": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==",
+          "dev": true
+        }
+      }
+    },
+    "vfile-location": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.5.tgz",
+      "integrity": "sha512-Pa1ey0OzYBkLPxPZI3d9E+S4BmvfVwNAAXrrqGbwTVXWaX2p9kM1zZ+n35UtVM06shmWKH4RPRN8KI80qE3wNQ==",
+      "dev": true
+    },
+    "vfile-message": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
+      "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
+      "dev": true,
+      "requires": {
+        "unist-util-stringify-position": "^1.1.1"
+      }
+    },
+    "void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+      "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
+      "dev": true
+    },
+    "vscode-json-languageservice": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-3.2.1.tgz",
+      "integrity": "sha512-ee9MJ70/xR55ywvm0bZsDLhA800HCRE27AYgMNTU14RSg20Y+ngHdQnUt6OmiTXrQDI/7sne6QUOtHIN0hPQYA==",
+      "dev": true,
+      "requires": {
+        "jsonc-parser": "^2.0.2",
+        "vscode-languageserver-types": "^3.13.0",
+        "vscode-nls": "^4.0.0",
+        "vscode-uri": "^1.0.6"
+      }
+    },
+    "vscode-languageserver-types": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz",
+      "integrity": "sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==",
+      "dev": true
+    },
+    "vscode-nls": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.0.tgz",
+      "integrity": "sha512-zKsFWVzL1wlCezgaI3XiN42IT8DIPM1Qr+G+RBhiU3U0bJCdC8pPELakRCtuVT4wF3gBZjBrUDQ8mowL7hmgwA==",
+      "dev": true
+    },
+    "vscode-uri": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz",
+      "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==",
+      "dev": true
+    },
+    "wdio-dot-reporter": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/wdio-dot-reporter/-/wdio-dot-reporter-0.0.10.tgz",
+      "integrity": "sha512-A0TCk2JdZEn3M1DSG9YYbNRcGdx/YRw19lTiRpgwzH4qqWkO/oRDZRmi3Snn4L2j54KKTfPalBhlOtc8fojVgg==",
+      "dev": true
+    },
+    "wdio-junit-reporter": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/wdio-junit-reporter/-/wdio-junit-reporter-0.2.0.tgz",
+      "integrity": "sha1-88QXRHftcXN2k9wKFBU6wEhiG8g=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^5.8.25",
+        "junit-report-builder": "^1.1.1",
+        "mkdirp": "^0.5.1"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "5.8.38",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz",
+          "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=",
+          "dev": true,
+          "requires": {
+            "core-js": "^1.0.0"
+          }
+        },
+        "core-js": {
+          "version": "1.2.7",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+          "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
+          "dev": true
+        }
+      }
+    },
+    "wdio-mediawiki": {
+      "version": "file:tests/selenium/wdio-mediawiki",
+      "dev": true,
+      "requires": {
+        "mwbot": "1.0.10"
+      }
+    },
+    "wdio-mocha-framework": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/wdio-mocha-framework/-/wdio-mocha-framework-0.6.4.tgz",
+      "integrity": "sha512-GZsXwoW60/fkkfqZJR/ZAdiALaM+hW+BbnTT9x214qPR4Pe5XeyYxhJNEdyf0dNI9625cMdkyZYaWoFHN5zDcA==",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.23.0",
+        "mocha": "^5.2.0",
+        "wdio-sync": "0.7.3"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+          "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+          "dev": true,
+          "requires": {
+            "core-js": "^2.4.0",
+            "regenerator-runtime": "^0.11.0"
+          }
+        }
+      }
+    },
+    "wdio-sauce-service": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/wdio-sauce-service/-/wdio-sauce-service-0.3.1.tgz",
+      "integrity": "sha1-++wJG+UeaUgkGnsMdmAKfnguqM0=",
+      "dev": true,
+      "requires": {
+        "request": "^2.67.0",
+        "sauce-connect-launcher": "^1.1.1"
+      }
+    },
+    "wdio-spec-reporter": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/wdio-spec-reporter/-/wdio-spec-reporter-0.0.5.tgz",
+      "integrity": "sha1-0PuP0UrxU/4BAFG7dAqjCrMQY/U=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^5.8.25",
+        "humanize-duration": "^3.9.0"
+      }
+    },
+    "wdio-sync": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/wdio-sync/-/wdio-sync-0.7.3.tgz",
+      "integrity": "sha512-ukASSHOQmOxaz5HTILR0jykqlHBtAPsBpMtwhpiG0aW9uc7SO7PF+E5LhVvTG4ypAh+UGmY3rTjohOsqDr39jw==",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "fibers": "^3.0.0",
+        "object.assign": "^4.0.3"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+          "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+          "dev": true,
+          "requires": {
+            "core-js": "^2.4.0",
+            "regenerator-runtime": "^0.11.0"
+          }
+        }
+      }
+    },
+    "webdriverio": {
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-4.12.0.tgz",
+      "integrity": "sha1-40De8nIYPIFopN0LOCMi+de+4Q0=",
+      "dev": true,
+      "requires": {
+        "archiver": "~2.1.0",
+        "babel-runtime": "^6.26.0",
+        "css-parse": "~2.0.0",
+        "css-value": "~0.0.1",
+        "deepmerge": "~2.0.1",
+        "ejs": "~2.5.6",
+        "gaze": "~1.1.2",
+        "glob": "~7.1.1",
+        "inquirer": "~3.3.0",
+        "json-stringify-safe": "~5.0.1",
+        "mkdirp": "~0.5.1",
+        "npm-install-package": "~2.1.0",
+        "optimist": "~0.6.1",
+        "q": "~1.5.0",
+        "request": "~2.83.0",
+        "rgb2hex": "~0.1.0",
+        "safe-buffer": "~5.1.1",
+        "supports-color": "~5.0.0",
+        "url": "~0.11.0",
+        "wdio-dot-reporter": "~0.0.8",
+        "wgxpath": "~1.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "babel-runtime": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+          "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+          "dev": true,
+          "requires": {
+            "core-js": "^2.4.0",
+            "regenerator-runtime": "^0.11.0"
+          }
+        },
+        "chardet": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+          "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
+          "dev": true
+        },
+        "external-editor": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+          "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+          "dev": true,
+          "requires": {
+            "chardet": "^0.4.0",
+            "iconv-lite": "^0.4.17",
+            "tmp": "^0.0.33"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+          "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+          "dev": true
+        },
+        "har-validator": {
+          "version": "5.0.3",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+          "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.1.0",
+            "har-schema": "^2.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+          "dev": true
+        },
+        "inquirer": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+          "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.0.0",
+            "chalk": "^2.0.0",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^2.0.4",
+            "figures": "^2.0.0",
+            "lodash": "^4.3.0",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rx-lite": "^4.0.8",
+            "rx-lite-aggregates": "^4.0.8",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^4.0.0",
+            "through": "^2.3.6"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+          "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+          "dev": true
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+          "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+          "dev": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+          "dev": true
+        },
+        "request": {
+          "version": "2.83.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
+          "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
+          "dev": true,
+          "requires": {
+            "aws-sign2": "~0.7.0",
+            "aws4": "^1.6.0",
+            "caseless": "~0.12.0",
+            "combined-stream": "~1.0.5",
+            "extend": "~3.0.1",
+            "forever-agent": "~0.6.1",
+            "form-data": "~2.3.1",
+            "har-validator": "~5.0.3",
+            "hawk": "~6.0.2",
+            "http-signature": "~1.2.0",
+            "is-typedarray": "~1.0.0",
+            "isstream": "~0.1.2",
+            "json-stringify-safe": "~5.0.1",
+            "mime-types": "~2.1.17",
+            "oauth-sign": "~0.8.2",
+            "performance-now": "^2.1.0",
+            "qs": "~6.5.1",
+            "safe-buffer": "^5.1.1",
+            "stringstream": "~0.0.5",
+            "tough-cookie": "~2.3.3",
+            "tunnel-agent": "^0.6.0",
+            "uuid": "^3.1.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.0.1.tgz",
+          "integrity": "sha512-7FQGOlSQ+AQxBNXJpVDj8efTA/FtyB5wcNE1omXXJ0cq6jm1jjDwuROlYDbnzHqdNPqliWFhcioCWSyav+xBnA==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^2.0.0"
+          }
+        },
+        "tough-cookie": {
+          "version": "2.3.4",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+          "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+          "dev": true,
+          "requires": {
+            "punycode": "^1.4.1"
+          }
+        }
+      }
+    },
+    "websocket-driver": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+      "dev": true,
+      "requires": {
+        "http-parser-js": ">=0.4.0",
+        "websocket-extensions": ">=0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+      "dev": true
+    },
+    "wgxpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wgxpath/-/wgxpath-1.0.0.tgz",
+      "integrity": "sha1-7vikudVYzEla06mit1FZfs2a9pA=",
+      "dev": true
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "dev": true,
+      "requires": {
+        "async-limiter": "~1.0.0",
+        "safe-buffer": "~5.1.0",
+        "ultron": "~1.1.0"
+      }
+    },
+    "x-is-string": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
+      "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=",
+      "dev": true
+    },
+    "xmlbuilder": {
+      "version": "10.1.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
+      "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
+      "dev": true
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs-parser": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+      "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^4.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        }
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
+      "dev": true
+    },
+    "zip-stream": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz",
+      "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=",
+      "dev": true,
+      "requires": {
+        "archiver-utils": "^1.3.0",
+        "compress-commons": "^1.2.0",
+        "lodash": "^4.8.0",
+        "readable-stream": "^2.0.0"
+      }
+    }
+  }
+}
index 8fec026..9f14c78 100644 (file)
@@ -18,7 +18,7 @@
     "grunt-contrib-watch": "1.1.0",
     "grunt-eslint": "21.0.0",
     "grunt-karma": "3.0.2",
-    "grunt-stylelint": "0.10.1",
+    "grunt-stylelint": "0.11.0",
     "grunt-svgmin": "5.0.0",
     "jpeg-js": "0.3.5",
     "karma": "3.1.4",
@@ -28,7 +28,7 @@
     "karma-qunit": "2.1.0",
     "postcss-less": "2.0.0",
     "qunit": "2.9.1",
-    "stylelint-config-wikimedia": "0.5.0",
+    "stylelint-config-wikimedia": "0.6.0",
     "wdio-junit-reporter": "0.2.0",
     "wdio-mediawiki": "file:tests/selenium/wdio-mediawiki",
     "wdio-mocha-framework": "0.6.4",
index ecdd43f..b90ead4 100644 (file)
@@ -615,11 +615,6 @@ return [
                'dependencies' => 'jquery.effects.core',
                'group' => 'jquery.ui',
        ],
-       'jquery.effects.bounce' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-bounce.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
        'jquery.effects.clip' => [
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-clip.js',
                'dependencies' => 'jquery.effects.core',
@@ -630,31 +625,11 @@ return [
                'dependencies' => 'jquery.effects.core',
                'group' => 'jquery.ui',
        ],
-       'jquery.effects.explode' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-explode.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
-       'jquery.effects.fade' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-fade.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
-       'jquery.effects.fold' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-fold.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
        'jquery.effects.highlight' => [
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-highlight.js',
                'dependencies' => 'jquery.effects.core',
                'group' => 'jquery.ui',
        ],
-       'jquery.effects.pulsate' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-pulsate.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
        'jquery.effects.scale' => [
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-scale.js',
                'dependencies' => 'jquery.effects.core',
@@ -665,16 +640,6 @@ return [
                'dependencies' => 'jquery.effects.core',
                'group' => 'jquery.ui',
        ],
-       'jquery.effects.slide' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-slide.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
-       'jquery.effects.transfer' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.effect-transfer.js',
-               'dependencies' => 'jquery.effects.core',
-               'group' => 'jquery.ui',
-       ],
 
        /* Moment.js */
 
@@ -2932,12 +2897,10 @@ return [
                'dependencies' => [
                        'oojs',
                        'oojs-ui-core.styles',
+                       'oojs-ui-core.icons',
                        'oojs-ui.styles.indicators',
                        'oojs-ui.styles.textures',
                        'mediawiki.language',
-                       'oojs-ui.styles.icons-content',
-                       'oojs-ui.styles.icons-alerts',
-                       'oojs-ui.styles.icons-interactions',
                ],
                'messages' => [
                        'ooui-field-help',
@@ -2954,6 +2917,11 @@ return [
                'themeStyles' => 'core',
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'oojs-ui-core.icons' => [
+               'class' => ResourceLoaderOOUIIconPackModule::class,
+               'icons' => [ 'add', 'alert', 'notice', 'error', 'check', 'close', 'info', 'search', 'subtract' ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        // Additional widgets and layouts module.
        'oojs-ui-widgets' => [
                'class' => ResourceLoaderOOUIFileModule::class,
@@ -2961,11 +2929,7 @@ return [
                'themeStyles' => 'widgets',
                'dependencies' => [
                        'oojs-ui-core',
-                       'oojs-ui.styles.icons-interactions',
-                       'oojs-ui.styles.icons-content',
-                       'oojs-ui.styles.icons-editing-advanced',
-                       'oojs-ui.styles.icons-movement',
-                       'oojs-ui.styles.icons-moderation',
+                       'oojs-ui-widgets.icons',
                ],
                'messages' => [
                        'ooui-item-remove',
@@ -2987,6 +2951,12 @@ return [
                'themeStyles' => 'widgets',
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'oojs-ui-widgets.icons' => [
+               'class' => ResourceLoaderOOUIIconPackModule::class,
+               // Do not repeat icons already used in 'oojs-ui-core.icons'
+               'icons' => [ 'attachment', 'collapse', 'expand', 'trash', 'upload' ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        // Toolbar and tools module.
        'oojs-ui-toolbars' => [
                'class' => ResourceLoaderOOUIFileModule::class,
@@ -2994,7 +2964,7 @@ return [
                'themeStyles' => 'toolbars',
                'dependencies' => [
                        'oojs-ui-core',
-                       'oojs-ui.styles.icons-movement',
+                       'oojs-ui-toolbars.icons',
                ],
                'messages' => [
                        'ooui-toolbar-more',
@@ -3003,6 +2973,12 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'oojs-ui-toolbars.icons' => [
+               'class' => ResourceLoaderOOUIIconPackModule::class,
+               // Do not repeat icons already used in 'oojs-ui-core.icons': 'check'
+               'icons' => [ 'collapse', 'expand' ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        // Windows and dialogs module.
        'oojs-ui-windows' => [
                'class' => ResourceLoaderOOUIFileModule::class,
@@ -3010,7 +2986,7 @@ return [
                'themeStyles' => 'windows',
                'dependencies' => [
                        'oojs-ui-core',
-                       'oojs-ui.styles.icons-movement',
+                       'oojs-ui-windows.icons',
                ],
                'messages' => [
                        'ooui-dialog-message-accept',
@@ -3022,6 +2998,12 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'oojs-ui-windows.icons' => [
+               'class' => ResourceLoaderOOUIIconPackModule::class,
+               // Do not repeat icons already used in 'oojs-ui-core.icons': 'close'
+               'icons' => [ 'previous' ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
 
        'oojs-ui.styles.indicators' => [
                'class' => ResourceLoaderOOUIImageModule::class,
index 9f34e77..d737cbe 100644 (file)
@@ -121,7 +121,28 @@ jquery:
   integrity: sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=
   dest: jquery.js
 
-# TODO: jquery.chosen
+jquery.chosen:
+  type: multi-file
+  files:
+    LICENSE:
+      src: https://raw.githubusercontent.com/harvesthq/chosen/v1.8.2/LICENSE.md
+      integrity: sha384-hxUqOVbJZTd9clMlf9yV18PjyKQ2rUOCXLgFNYlV/blpyeCyiUCpmVjAmNP0yc8M
+    README.md:
+      src: https://raw.githubusercontent.com/harvesthq/chosen/v1.8.2/README.md
+      integrity: sha384-ps8fQiOF1anPibj6QMNii4OcAbZNcy+dkmdJUZzqBgmfjaPth9YDe0TRIk89lfID
+    # Following files taken from CDN because they're built, and don't exist in the repo
+    chosen-sprite.png:
+      src: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.2/chosen-sprite.png
+      integrity: sha384-QL0lDMjIhfcd5uzKEIPehkhx7l0gHWxFo1taNsY2hdDuYdGAadNhiwKueQ91R8KW
+    chosen-sprite@2x.png:
+      src: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.2/chosen-sprite%402x.png
+      integrity: sha384-MSDzP+ofFO+lRrCZQn3dztHS/GdR8Ai907bxrRZeuGSi87G8XffEKTxxM99GTvr1
+    chosen.css:
+      src: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.2/chosen.css
+      integrity: sha384-VeNz/jFhcqEG5UB40sPZW8Bg8sdtbtXW1038aqBPAZy/z/6j1XsSQjRUJ7NEM3nE
+    chosen.jquery.js:
+      src: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.2/chosen.jquery.js
+      integrity: sha384-EzfvMGW4mwDo/InJrmR/UvtxTUUYUA0cfybfS8aqPG1ItoAQYYYDImWl1gaBzMfQ
 
 jquery.client:
   type: tar
index 5b21256..a3aa89e 100644 (file)
@@ -45,4 +45,3 @@ We welcome all to participate in making Chosen the best software it can be. The
 - Design and CSS by [Matthew Lettini](http://matthewlettini.com/)
 - Repository maintained by [@pfiller](http://github.com/pfiller), [@kenearley](http://github.com/kenearley), [@stof](http://github.com/stof), [@koenpunt](http://github.com/koenpunt), and [@tjschuck](http://github.com/tjschuck).
 - Chosen includes [contributions by many fine folks](https://github.com/harvesthq/chosen/contributors).
-
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-bounce.js b/resources/lib/jquery.ui/jquery.ui.effect-bounce.js
deleted file mode 100644 (file)
index ab1977e..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*!
- * jQuery UI Effects Bounce 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/bounce-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.bounce = function( o, done ) {
-       var el = $( this ),
-               props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
-
-               // defaults:
-               mode = $.effects.setMode( el, o.mode || "effect" ),
-               hide = mode === "hide",
-               show = mode === "show",
-               direction = o.direction || "up",
-               distance = o.distance,
-               times = o.times || 5,
-
-               // number of internal animations
-               anims = times * 2 + ( show || hide ? 1 : 0 ),
-               speed = o.duration / anims,
-               easing = o.easing,
-
-               // utility:
-               ref = ( direction === "up" || direction === "down" ) ? "top" : "left",
-               motion = ( direction === "up" || direction === "left" ),
-               i,
-               upAnim,
-               downAnim,
-
-               // we will need to re-assemble the queue to stack our animations in place
-               queue = el.queue(),
-               queuelen = queue.length;
-
-       // Avoid touching opacity to prevent clearType and PNG issues in IE
-       if ( show || hide ) {
-               props.push( "opacity" );
-       }
-
-       $.effects.save( el, props );
-       el.show();
-       $.effects.createWrapper( el ); // Create Wrapper
-
-       // default distance for the BIGGEST bounce is the outer Distance / 3
-       if ( !distance ) {
-               distance = el[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3;
-       }
-
-       if ( show ) {
-               downAnim = { opacity: 1 };
-               downAnim[ ref ] = 0;
-
-               // if we are showing, force opacity 0 and set the initial position
-               // then do the "first" animation
-               el.css( "opacity", 0 )
-                       .css( ref, motion ? -distance * 2 : distance * 2 )
-                       .animate( downAnim, speed, easing );
-       }
-
-       // start at the smallest distance if we are hiding
-       if ( hide ) {
-               distance = distance / Math.pow( 2, times - 1 );
-       }
-
-       downAnim = {};
-       downAnim[ ref ] = 0;
-       // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here
-       for ( i = 0; i < times; i++ ) {
-               upAnim = {};
-               upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance;
-
-               el.animate( upAnim, speed, easing )
-                       .animate( downAnim, speed, easing );
-
-               distance = hide ? distance * 2 : distance / 2;
-       }
-
-       // Last Bounce when Hiding
-       if ( hide ) {
-               upAnim = { opacity: 0 };
-               upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance;
-
-               el.animate( upAnim, speed, easing );
-       }
-
-       el.queue(function() {
-               if ( hide ) {
-                       el.hide();
-               }
-               $.effects.restore( el, props );
-               $.effects.removeWrapper( el );
-               done();
-       });
-
-       // inject all the animations we just queued to be first in line (after "inprogress")
-       if ( queuelen > 1) {
-               queue.splice.apply( queue,
-                       [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
-       }
-       el.dequeue();
-
-};
-
-})(jQuery);
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-explode.js b/resources/lib/jquery.ui/jquery.ui.effect-explode.js
deleted file mode 100644 (file)
index 98d5be5..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-/*!
- * jQuery UI Effects Explode 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/explode-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.explode = function( o, done ) {
-
-       var rows = o.pieces ? Math.round( Math.sqrt( o.pieces ) ) : 3,
-               cells = rows,
-               el = $( this ),
-               mode = $.effects.setMode( el, o.mode || "hide" ),
-               show = mode === "show",
-
-               // show and then visibility:hidden the element before calculating offset
-               offset = el.show().css( "visibility", "hidden" ).offset(),
-
-               // width and height of a piece
-               width = Math.ceil( el.outerWidth() / cells ),
-               height = Math.ceil( el.outerHeight() / rows ),
-               pieces = [],
-
-               // loop
-               i, j, left, top, mx, my;
-
-       // children animate complete:
-       function childComplete() {
-               pieces.push( this );
-               if ( pieces.length === rows * cells ) {
-                       animComplete();
-               }
-       }
-
-       // clone the element for each row and cell.
-       for( i = 0; i < rows ; i++ ) { // ===>
-               top = offset.top + i * height;
-               my = i - ( rows - 1 ) / 2 ;
-
-               for( j = 0; j < cells ; j++ ) { // |||
-                       left = offset.left + j * width;
-                       mx = j - ( cells - 1 ) / 2 ;
-
-                       // Create a clone of the now hidden main element that will be absolute positioned
-                       // within a wrapper div off the -left and -top equal to size of our pieces
-                       el
-                               .clone()
-                               .appendTo( "body" )
-                               .wrap( "<div></div>" )
-                               .css({
-                                       position: "absolute",
-                                       visibility: "visible",
-                                       left: -j * width,
-                                       top: -i * height
-                               })
-
-                       // select the wrapper - make it overflow: hidden and absolute positioned based on
-                       // where the original was located +left and +top equal to the size of pieces
-                               .parent()
-                               .addClass( "ui-effects-explode" )
-                               .css({
-                                       position: "absolute",
-                                       overflow: "hidden",
-                                       width: width,
-                                       height: height,
-                                       left: left + ( show ? mx * width : 0 ),
-                                       top: top + ( show ? my * height : 0 ),
-                                       opacity: show ? 0 : 1
-                               }).animate({
-                                       left: left + ( show ? 0 : mx * width ),
-                                       top: top + ( show ? 0 : my * height ),
-                                       opacity: show ? 1 : 0
-                               }, o.duration || 500, o.easing, childComplete );
-               }
-       }
-
-       function animComplete() {
-               el.css({
-                       visibility: "visible"
-               });
-               $( pieces ).remove();
-               if ( !show ) {
-                       el.hide();
-               }
-               done();
-       }
-};
-
-})(jQuery);
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-fold.js b/resources/lib/jquery.ui/jquery.ui.effect-fold.js
deleted file mode 100644 (file)
index 9452c5d..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*!
- * jQuery UI Effects Fold 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/fold-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.fold = function( o, done ) {
-
-       // Create element
-       var el = $( this ),
-               props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
-               mode = $.effects.setMode( el, o.mode || "hide" ),
-               show = mode === "show",
-               hide = mode === "hide",
-               size = o.size || 15,
-               percent = /([0-9]+)%/.exec( size ),
-               horizFirst = !!o.horizFirst,
-               widthFirst = show !== horizFirst,
-               ref = widthFirst ? [ "width", "height" ] : [ "height", "width" ],
-               duration = o.duration / 2,
-               wrapper, distance,
-               animation1 = {},
-               animation2 = {};
-
-       $.effects.save( el, props );
-       el.show();
-
-       // Create Wrapper
-       wrapper = $.effects.createWrapper( el ).css({
-               overflow: "hidden"
-       });
-       distance = widthFirst ?
-               [ wrapper.width(), wrapper.height() ] :
-               [ wrapper.height(), wrapper.width() ];
-
-       if ( percent ) {
-               size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ];
-       }
-       if ( show ) {
-               wrapper.css( horizFirst ? {
-                       height: 0,
-                       width: size
-               } : {
-                       height: size,
-                       width: 0
-               });
-       }
-
-       // Animation
-       animation1[ ref[ 0 ] ] = show ? distance[ 0 ] : size;
-       animation2[ ref[ 1 ] ] = show ? distance[ 1 ] : 0;
-
-       // Animate
-       wrapper
-               .animate( animation1, duration, o.easing )
-               .animate( animation2, duration, o.easing, function() {
-                       if ( hide ) {
-                               el.hide();
-                       }
-                       $.effects.restore( el, props );
-                       $.effects.removeWrapper( el );
-                       done();
-               });
-
-};
-
-})(jQuery);
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-pulsate.js b/resources/lib/jquery.ui/jquery.ui.effect-pulsate.js
deleted file mode 100644 (file)
index 20f84dd..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*!
- * jQuery UI Effects Pulsate 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/pulsate-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.pulsate = function( o, done ) {
-       var elem = $( this ),
-               mode = $.effects.setMode( elem, o.mode || "show" ),
-               show = mode === "show",
-               hide = mode === "hide",
-               showhide = ( show || mode === "hide" ),
-
-               // showing or hiding leaves of the "last" animation
-               anims = ( ( o.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ),
-               duration = o.duration / anims,
-               animateTo = 0,
-               queue = elem.queue(),
-               queuelen = queue.length,
-               i;
-
-       if ( show || !elem.is(":visible")) {
-               elem.css( "opacity", 0 ).show();
-               animateTo = 1;
-       }
-
-       // anims - 1 opacity "toggles"
-       for ( i = 1; i < anims; i++ ) {
-               elem.animate({
-                       opacity: animateTo
-               }, duration, o.easing );
-               animateTo = 1 - animateTo;
-       }
-
-       elem.animate({
-               opacity: animateTo
-       }, duration, o.easing);
-
-       elem.queue(function() {
-               if ( hide ) {
-                       elem.hide();
-               }
-               done();
-       });
-
-       // We just queued up "anims" animations, we need to put them next in the queue
-       if ( queuelen > 1 ) {
-               queue.splice.apply( queue,
-                       [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
-       }
-       elem.dequeue();
-};
-
-})(jQuery);
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-slide.js b/resources/lib/jquery.ui/jquery.ui.effect-slide.js
deleted file mode 100644 (file)
index 445ec48..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*!
- * jQuery UI Effects Slide 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/slide-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.slide = function( o, done ) {
-
-       // Create element
-       var el = $( this ),
-               props = [ "position", "top", "bottom", "left", "right", "width", "height" ],
-               mode = $.effects.setMode( el, o.mode || "show" ),
-               show = mode === "show",
-               direction = o.direction || "left",
-               ref = (direction === "up" || direction === "down") ? "top" : "left",
-               positiveMotion = (direction === "up" || direction === "left"),
-               distance,
-               animation = {};
-
-       // Adjust
-       $.effects.save( el, props );
-       el.show();
-       distance = o.distance || el[ ref === "top" ? "outerHeight" : "outerWidth" ]( true );
-
-       $.effects.createWrapper( el ).css({
-               overflow: "hidden"
-       });
-
-       if ( show ) {
-               el.css( ref, positiveMotion ? (isNaN(distance) ? "-" + distance : -distance) : distance );
-       }
-
-       // Animation
-       animation[ ref ] = ( show ?
-               ( positiveMotion ? "+=" : "-=") :
-               ( positiveMotion ? "-=" : "+=")) +
-               distance;
-
-       // Animate
-       el.animate( animation, {
-               queue: false,
-               duration: o.duration,
-               easing: o.easing,
-               complete: function() {
-                       if ( mode === "hide" ) {
-                               el.hide();
-                       }
-                       $.effects.restore( el, props );
-                       $.effects.removeWrapper( el );
-                       done();
-               }
-       });
-};
-
-})(jQuery);
diff --git a/resources/lib/jquery.ui/jquery.ui.effect-transfer.js b/resources/lib/jquery.ui/jquery.ui.effect-transfer.js
deleted file mode 100644 (file)
index f133c04..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*!
- * jQuery UI Effects Transfer 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/transfer-effect/
- *
- * Depends:
- *     jquery.ui.effect.js
- */
-(function( $, undefined ) {
-
-$.effects.effect.transfer = function( o, done ) {
-       var elem = $( this ),
-               target = $( o.to ),
-               targetFixed = target.css( "position" ) === "fixed",
-               body = $("body"),
-               fixTop = targetFixed ? body.scrollTop() : 0,
-               fixLeft = targetFixed ? body.scrollLeft() : 0,
-               endPosition = target.offset(),
-               animation = {
-                       top: endPosition.top - fixTop ,
-                       left: endPosition.left - fixLeft ,
-                       height: target.innerHeight(),
-                       width: target.innerWidth()
-               },
-               startPosition = elem.offset(),
-               transfer = $( '<div class="ui-effects-transfer"></div>' )
-                       .appendTo( document.body )
-                       .addClass( o.className )
-                       .css({
-                               top: startPosition.top - fixTop ,
-                               left: startPosition.left - fixLeft ,
-                               height: elem.innerHeight(),
-                               width: elem.innerWidth(),
-                               position: targetFixed ? "fixed" : "absolute"
-                       })
-                       .animate( animation, o.duration, o.easing, function() {
-                               transfer.remove();
-                               done();
-                       });
-};
-
-})(jQuery);
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 fd6f38c..ac89616 100644 (file)
@@ -43,9 +43,9 @@
                display: none;
        }
 
+       // table.: Where the tbody or thead is the first child of the collapsible table
        ol.mw-collapsible:not( @{exclude} ):before,
        ul.mw-collapsible:not( @{exclude} ):before,
-       // Where the tbody or thead is the first child of the collapsible table
        table.mw-collapsible:not( @{exclude} ) :first-child tr:first-child th:last-child:before,
        table.mw-collapsible:not( @{exclude} ) > caption:first-child:after,
        div.mw-collapsible:not( @{exclude} ):before {
        // Use the exclude selector to ensure animations do not break
        .mw-collapsed:not( @{exclude} ) {
                // Avoid FOUC/reflows on collapsed elements by making sure they are opened by default (T42812)
+               // > thead + tbody: 'https://www.mediawiki.org/wiki/Manual:Collapsible_elements/Demo/Simple#Collapsed_by_default'
                > p,
                > table,
-               > thead + tbody, // 'https://www.mediawiki.org/wiki/Manual:Collapsible_elements/Demo/Simple#Collapsed_by_default'
+               > thead + tbody,
                tr:not( :first-child ),
                .mw-collapsible-content {
                        display: none;
index f58d733..3083b0f 100644 (file)
@@ -10,8 +10,6 @@
  *
  *  $( '#textbox' ).suggestions();
  *
- * Uses jQuery.suggestions singleton internally.
- *
  * @class jQuery.plugin.suggestions
  */
 
@@ -80,7 +78,7 @@
  *
  * @param {string} [options.expandFrom=auto] Which direction to offset the suggestion box from.
  *  Values 'start' and 'end' translate to left and right respectively depending on the directionality
- *   of the current document, according to `$( 'html' ).css( 'direction' )`.
+ *   of the current document, according to `$( document.documentElement ).css( 'direction' )`.
  *   Valid values: "left", "right", "start", "end", and "auto".
  *
  * @param {boolean} [options.positionFromLeft] Sets `expandFrom=left`, for backwards
 
 ( function () {
 
-       var hasOwn = Object.hasOwnProperty;
+       /**
+        * Cancel any delayed maybeFetch() call and callback the context so
+        * they can cancel any async fetching if they use AJAX or something.
+        *
+        * @param {Object} context
+        */
+       function cancel( context ) {
+               if ( context.data.timerID !== null ) {
+                       clearTimeout( context.data.timerID );
+               }
+               if ( typeof context.config.cancel === 'function' ) {
+                       context.config.cancel.call( context.data.$textbox );
+               }
+       }
 
        /**
-        * Used by jQuery.plugin.suggestions.
+        * Hide the element with suggestions and clean up some state.
         *
-        * @class jQuery.suggestions
-        * @singleton
-        * @private
+        * @param {Object} context
         */
-       $.suggestions = {
-               /**
-                * Cancel any delayed maybeFetch() call and callback the context so
-                * they can cancel any async fetching if they use AJAX or something.
-                *
-                * @param {Object} context
-                */
-               cancel: function ( context ) {
-                       if ( context.data.timerID !== null ) {
-                               clearTimeout( context.data.timerID );
-                       }
-                       if ( typeof context.config.cancel === 'function' ) {
-                               context.config.cancel.call( context.data.$textbox );
+       function hide( context ) {
+               // Remove any highlights, including on "special" items
+               context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' );
+               // Hide the container
+               context.data.$container.hide();
+       }
+
+       /**
+        * Restore the text the user originally typed in the textbox, before it
+        * was overwritten by highlight(). This restores the value the currently
+        * displayed suggestions are based on, rather than the value just before
+        * highlight() overwrote it; the former is arguably slightly more sensible.
+        *
+        * @param {Object} context
+        */
+       function restore( context ) {
+               context.data.$textbox.val( context.data.prevText );
+       }
+
+       /**
+        * @param {Object} context
+        */
+       function special( context ) {
+               // Allow custom rendering - but otherwise don't do any rendering
+               if ( typeof context.config.special.render === 'function' ) {
+                       // Wait for the browser to update the value
+                       setTimeout( function () {
+                               // Render special
+                               var $special = context.data.$container.find( '.suggestions-special' );
+                               context.config.special.render.call( $special, context.data.$textbox.val(), context );
+                       }, 1 );
+               }
+       }
+
+       /**
+        * Ask the user-specified callback for new suggestions. Any previous delayed
+        * call to this function still pending will be canceled. If the value in the
+        * textbox is empty or hasn't changed since the last time suggestions were fetched,
+        * this function does nothing.
+        *
+        * @param {Object} context
+        * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time
+        */
+       function update( context, delayed ) {
+               function maybeFetch() {
+                       var val = context.data.$textbox.val(),
+                               cache = context.data.cache,
+                               cacheHit;
+
+                       if ( typeof context.config.update.before === 'function' ) {
+                               context.config.update.before.call( context.data.$textbox );
                        }
-               },
-
-               /**
-                * Hide the element with suggestions and clean up some state.
-                *
-                * @param {Object} context
-                */
-               hide: function ( context ) {
-                       // Remove any highlights, including on "special" items
-                       context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' );
-                       // Hide the container
-                       context.data.$container.hide();
-               },
-
-               /**
-                * Restore the text the user originally typed in the textbox, before it
-                * was overwritten by highlight(). This restores the value the currently
-                * displayed suggestions are based on, rather than the value just before
-                * highlight() overwrote it; the former is arguably slightly more sensible.
-                *
-                * @param {Object} context
-                */
-               restore: function ( context ) {
-                       context.data.$textbox.val( context.data.prevText );
-               },
-
-               /**
-                * Ask the user-specified callback for new suggestions. Any previous delayed
-                * call to this function still pending will be canceled. If the value in the
-                * textbox is empty or hasn't changed since the last time suggestions were fetched,
-                * this function does nothing.
-                *
-                * @param {Object} context
-                * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time
-                */
-               update: function ( context, delayed ) {
-                       function maybeFetch() {
-                               var val = context.data.$textbox.val(),
-                                       cache = context.data.cache,
-                                       cacheHit;
-
-                               if ( typeof context.config.update.before === 'function' ) {
-                                       context.config.update.before.call( context.data.$textbox );
-                               }
 
-                               // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
-                               // if the textbox is empty then clear the result div, but leave other settings intouched
-                               if ( val.length === 0 ) {
-                                       $.suggestions.hide( context );
-                                       context.data.prevText = '';
-                               } else if (
-                                       val !== context.data.prevText ||
-                                       !context.data.$container.is( ':visible' )
-                               ) {
-                                       context.data.prevText = val;
-                                       // Try cache first
-                                       if ( context.config.cache && hasOwn.call( cache, val ) ) {
-                                               if ( mw.now() - cache[ val ].timestamp < context.config.cacheMaxAge ) {
-                                                       context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions );
+                       // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
+                       // if the textbox is empty then clear the result div, but leave other settings intouched
+                       if ( val.length === 0 ) {
+                               hide( context );
+                               context.data.prevText = '';
+                       } else if (
+                               val !== context.data.prevText ||
+                               !context.data.$container.is( ':visible' )
+                       ) {
+                               context.data.prevText = val;
+                               // Try cache first
+                               if ( context.config.cache && val in cache ) {
+                                       if ( mw.now() - cache[ val ].timestamp < context.config.cacheMaxAge ) {
+                                               context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions );
+                                               if ( typeof context.config.update.after === 'function' ) {
+                                                       context.config.update.after.call( context.data.$textbox, cache[ val ].metadata );
+                                               }
+                                               cacheHit = true;
+                                       } else {
+                                               // Cache expired
+                                               delete cache[ val ];
+                                       }
+                               }
+                               if ( !cacheHit && typeof context.config.fetch === 'function' ) {
+                                       context.config.fetch.call(
+                                               context.data.$textbox,
+                                               val,
+                                               function ( suggestions, metadata ) {
+                                                       suggestions = suggestions.slice( 0, context.config.maxRows );
+                                                       context.data.$textbox.suggestions( 'suggestions', suggestions );
                                                        if ( typeof context.config.update.after === 'function' ) {
-                                                               context.config.update.after.call( context.data.$textbox, cache[ val ].metadata );
+                                                               context.config.update.after.call( context.data.$textbox, metadata );
                                                        }
-                                                       cacheHit = true;
+                                                       if ( context.config.cache ) {
+                                                               cache[ val ] = {
+                                                                       suggestions: suggestions,
+                                                                       metadata: metadata,
+                                                                       timestamp: mw.now()
+                                                               };
+                                                       }
+                                               },
+                                               context.config.maxRows
+                                       );
+                               }
+                       }
+
+                       // Always update special rendering
+                       special( context );
+               }
+
+               // Cancels any delayed maybeFetch call, and invokes context.config.cancel.
+               cancel( context );
+
+               if ( delayed ) {
+                       // To avoid many started/aborted requests while typing, we're gonna take a short
+                       // break before trying to fetch data.
+                       context.data.timerID = setTimeout( maybeFetch, context.config.delay );
+               } else {
+                       maybeFetch();
+               }
+       }
+
+       /**
+        * Highlight a result in the results table
+        *
+        * @param {Object} context
+        * @param {jQuery|string} result `<tr>` to highlight, or 'prev' or 'next'
+        * @param {boolean} updateTextbox If true, put the suggestion in the textbox
+        */
+       function highlight( context, result, updateTextbox ) {
+               var selected = context.data.$container.find( '.suggestions-result-current' );
+               if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) {
+                       if ( result === 'prev' ) {
+                               if ( selected.hasClass( 'suggestions-special' ) ) {
+                                       result = context.data.$container.find( '.suggestions-result:last' );
+                               } else {
+                                       result = selected.prev();
+                                       if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
+                                               // there is something in the DOM between selected element and the wrapper, bypass it
+                                               result = selected.parents( '.suggestions-results > *' ).prev().find( '.suggestions-result' ).eq( 0 );
+                                       }
+
+                                       if ( selected.length === 0 ) {
+                                               // we are at the beginning, so lets jump to the last item
+                                               if ( context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
+                                                       result = context.data.$container.find( '.suggestions-special' );
                                                } else {
-                                                       // Cache expired
-                                                       delete cache[ val ];
+                                                       result = context.data.$container.find( '.suggestions-results .suggestions-result:last' );
                                                }
                                        }
-                                       if ( !cacheHit && typeof context.config.fetch === 'function' ) {
-                                               context.config.fetch.call(
-                                                       context.data.$textbox,
-                                                       val,
-                                                       function ( suggestions, metadata ) {
-                                                               suggestions = suggestions.slice( 0, context.config.maxRows );
-                                                               context.data.$textbox.suggestions( 'suggestions', suggestions );
-                                                               if ( typeof context.config.update.after === 'function' ) {
-                                                                       context.config.update.after.call( context.data.$textbox, metadata );
-                                                               }
-                                                               if ( context.config.cache ) {
-                                                                       cache[ val ] = {
-                                                                               suggestions: suggestions,
-                                                                               metadata: metadata,
-                                                                               timestamp: mw.now()
-                                                                       };
-                                                               }
-                                                       },
-                                                       context.config.maxRows
-                                               );
-                                       }
                                }
+                       } else if ( result === 'next' ) {
+                               if ( selected.length === 0 ) {
+                                       // No item selected, go to the first one
+                                       result = context.data.$container.find( '.suggestions-results .suggestions-result:first' );
+                                       if ( result.length === 0 && context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
+                                               // No suggestion exists, go to the special one directly
+                                               result = context.data.$container.find( '.suggestions-special' );
+                                       }
+                               } else {
+                                       result = selected.next();
+                                       if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
+                                               // there is something in the DOM between selected element and the wrapper, bypass it
+                                               result = selected.parents( '.suggestions-results > *' ).next().find( '.suggestions-result' ).eq( 0 );
+                                       }
 
-                               // Always update special rendering
-                               $.suggestions.special( context );
+                                       if ( selected.hasClass( 'suggestions-special' ) ) {
+                                               result = $( [] );
+                                       } else if (
+                                               result.length === 0 &&
+                                               context.data.$container.find( '.suggestions-special' ).html() !== ''
+                                       ) {
+                                               // We were at the last item, jump to the specials!
+                                               result = context.data.$container.find( '.suggestions-special' );
+                                       }
+                               }
                        }
-
-                       // Cancels any delayed maybeFetch call, and invokes context.config.cancel.
-                       $.suggestions.cancel( context );
-
-                       if ( delayed ) {
-                               // To avoid many started/aborted requests while typing, we're gonna take a short
-                               // break before trying to fetch data.
-                               context.data.timerID = setTimeout( maybeFetch, context.config.delay );
+                       selected.removeClass( 'suggestions-result-current' );
+                       result.addClass( 'suggestions-result-current' );
+               }
+               if ( updateTextbox ) {
+                       if ( result.length === 0 || result.is( '.suggestions-special' ) ) {
+                               restore( context );
                        } else {
-                               maybeFetch();
-                       }
-               },
-
-               /**
-                * @param {Object} context
-                */
-               special: function ( context ) {
-                       // Allow custom rendering - but otherwise don't do any rendering
-                       if ( typeof context.config.special.render === 'function' ) {
-                               // Wait for the browser to update the value
-                               setTimeout( function () {
-                                       // Render special
-                                       var $special = context.data.$container.find( '.suggestions-special' );
-                                       context.config.special.render.call( $special, context.data.$textbox.val(), context );
-                               }, 1 );
+                               context.data.$textbox.val( result.data( 'text' ) );
+                               // .val() doesn't call any event handlers, so
+                               // let the world know what happened
+                               context.data.$textbox.trigger( 'change' );
                        }
-               },
-
-               /**
-                * Sets the value of a property, and updates the widget accordingly
-                *
-                * @param {Object} context
-                * @param {string} property Name of property
-                * @param {Mixed} value Value to set property with
-                */
-               configure: function ( context, property, value ) {
-                       var newCSS,
-                               $result, $results, $spanForWidth, childrenWidth,
-                               regionIsFixed, regionPosition,
-                               i, expWidth, maxWidth, text;
-
-                       // Validate creation using fallback values
-                       switch ( property ) {
-                               case 'fetch':
-                               case 'cancel':
-                               case 'special':
-                               case 'result':
-                               case 'update':
-                               case '$region':
-                               case 'expandFrom':
-                                       context.config[ property ] = value;
-                                       break;
-                               case 'suggestions':
-                                       context.config[ property ] = value;
-                                       // Update suggestions
-                                       if ( context.data !== undefined ) {
-                                               if ( context.data.$textbox.val().length === 0 ) {
-                                                       // Hide the div when no suggestion exist
-                                                       $.suggestions.hide( context );
-                                               } else {
-                                                       // Rebuild the suggestions list
-                                                       context.data.$container.show();
-                                                       // Update the size and position of the list
-                                                       regionIsFixed = ( function () {
-                                                               var $el = context.config.$region;
-                                                               do {
-                                                                       if ( $el.css( 'position' ) === 'fixed' ) {
-                                                                               return true;
-                                                                       }
-                                                                       $el = $( $el[ 0 ].offsetParent );
-                                                               } while ( $el.length );
-                                                               return false;
-                                                       }() );
-                                                       regionPosition = regionIsFixed ?
-                                                               context.config.$region[ 0 ].getBoundingClientRect() :
-                                                               context.config.$region.offset();
-                                                       newCSS = {
-                                                               position: regionIsFixed ? 'fixed' : 'absolute',
-                                                               top: regionPosition.top + context.config.$region.outerHeight(),
-                                                               bottom: 'auto',
-                                                               width: context.config.$region.outerWidth(),
-                                                               height: 'auto'
-                                                       };
-
-                                                       // Process expandFrom, after this it is set to left or right.
-                                                       context.config.expandFrom = ( function ( expandFrom ) {
-                                                               var regionWidth, docWidth, regionCenter, docCenter,
-                                                                       docDir = $( document.documentElement ).css( 'direction' ),
-                                                                       $region = context.config.$region;
-
-                                                               // Backwards compatible
-                                                               if ( context.config.positionFromLeft ) {
-                                                                       expandFrom = 'left';
-
-                                                               // Catch invalid values, default to 'auto'
-                                                               } else if ( [ 'left', 'right', 'start', 'end', 'auto' ].indexOf( expandFrom ) === -1 ) {
-                                                                       expandFrom = 'auto';
+                       context.data.$textbox.trigger( 'change' );
+               }
+       }
+
+       /**
+        * Sets the value of a property, and updates the widget accordingly
+        *
+        * @param {Object} context
+        * @param {string} property Name of property
+        * @param {Mixed} value Value to set property with
+        */
+       function configure( context, property, value ) {
+               var newCSS,
+                       $result, $results, $spanForWidth, childrenWidth,
+                       regionIsFixed, regionPosition,
+                       i, expWidth, maxWidth, text;
+
+               // Validate creation using fallback values
+               switch ( property ) {
+                       case 'fetch':
+                       case 'cancel':
+                       case 'special':
+                       case 'result':
+                       case 'update':
+                       case '$region':
+                       case 'expandFrom':
+                               context.config[ property ] = value;
+                               break;
+                       case 'suggestions':
+                               context.config[ property ] = value;
+                               // Update suggestions
+                               if ( context.data !== undefined ) {
+                                       if ( context.data.$textbox.val().length === 0 ) {
+                                               // Hide the div when no suggestion exist
+                                               hide( context );
+                                       } else {
+                                               // Rebuild the suggestions list
+                                               context.data.$container.show();
+                                               // Update the size and position of the list
+                                               regionIsFixed = ( function () {
+                                                       var $el = context.config.$region;
+                                                       do {
+                                                               if ( $el.css( 'position' ) === 'fixed' ) {
+                                                                       return true;
                                                                }
+                                                               $el = $( $el[ 0 ].offsetParent );
+                                                       } while ( $el.length );
+                                                       return false;
+                                               }() );
+                                               regionPosition = regionIsFixed ?
+                                                       context.config.$region[ 0 ].getBoundingClientRect() :
+                                                       context.config.$region.offset();
+                                               newCSS = {
+                                                       position: regionIsFixed ? 'fixed' : 'absolute',
+                                                       top: regionPosition.top + context.config.$region.outerHeight(),
+                                                       bottom: 'auto',
+                                                       width: context.config.$region.outerWidth(),
+                                                       height: 'auto'
+                                               };
+
+                                               // Process expandFrom, after this it is set to left or right.
+                                               context.config.expandFrom = ( function ( expandFrom ) {
+                                                       var regionWidth, docWidth, regionCenter, docCenter,
+                                                               isRTL = $( document.documentElement ).css( 'direction' ) === 'rtl',
+                                                               $region = context.config.$region;
+
+                                                       // Backwards compatible
+                                                       if ( context.config.positionFromLeft ) {
+                                                               expandFrom = 'left';
+
+                                                       // Catch invalid values, default to 'auto'
+                                                       } else if ( [ 'left', 'right', 'start', 'end', 'auto' ].indexOf( expandFrom ) === -1 ) {
+                                                               expandFrom = 'auto';
+                                                       }
 
-                                                               if ( expandFrom === 'auto' ) {
-                                                                       if ( $region.data( 'searchsuggest-expand-dir' ) ) {
-                                                                               // If the markup explicitly contains a direction, use it.
-                                                                               expandFrom = $region.data( 'searchsuggest-expand-dir' );
+                                                       if ( expandFrom === 'auto' ) {
+                                                               if ( $region.data( 'searchsuggest-expand-dir' ) ) {
+                                                                       // If the markup explicitly contains a direction, use it.
+                                                                       expandFrom = $region.data( 'searchsuggest-expand-dir' );
+                                                               } else {
+                                                                       regionWidth = $region.outerWidth();
+                                                                       docWidth = $( document ).width();
+                                                                       if ( regionWidth > ( 0.85 * docWidth ) ) {
+                                                                               // If the input size takes up more than 85% of the document horizontally
+                                                                               // expand the suggestions to the writing direction's native end.
+                                                                               expandFrom = 'start';
                                                                        } else {
-                                                                               regionWidth = $region.outerWidth();
-                                                                               docWidth = $( document ).width();
-                                                                               if ( regionWidth > ( 0.85 * docWidth ) ) {
-                                                                                       // If the input size takes up more than 85% of the document horizontally
-                                                                                       // expand the suggestions to the writing direction's native end.
+                                                                               // Calculate the center points of the input and document
+                                                                               regionCenter = regionPosition.left + regionWidth / 2;
+                                                                               docCenter = docWidth / 2;
+                                                                               if ( Math.abs( regionCenter - docCenter ) < ( 0.10 * docCenter ) ) {
+                                                                                       // If the input's center is within 10% of the document center
+                                                                                       // use the writing direction's native end.
                                                                                        expandFrom = 'start';
                                                                                } else {
-                                                                                       // Calculate the center points of the input and document
-                                                                                       regionCenter = regionPosition.left + regionWidth / 2;
-                                                                                       docCenter = docWidth / 2;
-                                                                                       if ( Math.abs( regionCenter - docCenter ) < ( 0.10 * docCenter ) ) {
-                                                                                               // If the input's center is within 10% of the document center
-                                                                                               // use the writing direction's native end.
-                                                                                               expandFrom = 'start';
-                                                                                       } else {
-                                                                                               // Otherwise expand the input from the closest side of the page,
-                                                                                               // towards the side of the page with the most free open space
-                                                                                               expandFrom = regionCenter > docCenter ? 'right' : 'left';
-                                                                                       }
+                                                                                       // Otherwise expand the input from the closest side of the page,
+                                                                                       // towards the side of the page with the most free open space
+                                                                                       expandFrom = regionCenter > docCenter ? 'right' : 'left';
                                                                                }
                                                                        }
                                                                }
+                                                       }
 
-                                                               if ( expandFrom === 'start' ) {
-                                                                       expandFrom = docDir === 'rtl' ? 'right' : 'left';
+                                                       if ( expandFrom === 'start' ) {
+                                                               expandFrom = isRTL ? 'right' : 'left';
 
-                                                               } else if ( expandFrom === 'end' ) {
-                                                                       expandFrom = docDir === 'rtl' ? 'left' : 'right';
-                                                               }
+                                                       } else if ( expandFrom === 'end' ) {
+                                                               expandFrom = isRTL ? 'left' : 'right';
+                                                       }
 
-                                                               return expandFrom;
+                                                       return expandFrom;
 
-                                                       }( context.config.expandFrom ) );
+                                               }( context.config.expandFrom ) );
 
-                                                       if ( context.config.expandFrom === 'left' ) {
-                                                               // Expand from left
-                                                               newCSS.left = regionPosition.left;
-                                                               newCSS.right = 'auto';
+                                               if ( context.config.expandFrom === 'left' ) {
+                                                       // Expand from left
+                                                       newCSS.left = regionPosition.left;
+                                                       newCSS.right = 'auto';
+                                               } else {
+                                                       // Expand from right
+                                                       newCSS.left = 'auto';
+                                                       newCSS.right = document.documentElement.clientWidth -
+                                                               ( regionPosition.left + context.config.$region.outerWidth() );
+                                               }
+
+                                               context.data.$container.css( newCSS );
+                                               $results = context.data.$container.children( '.suggestions-results' );
+                                               $results.empty();
+                                               expWidth = -1;
+                                               for ( i = 0; i < context.config.suggestions.length; i++ ) {
+                                                       text = context.config.suggestions[ i ];
+                                                       $result = $( '<div>' )
+                                                               .addClass( 'suggestions-result' )
+                                                               .attr( 'rel', i )
+                                                               .data( 'text', context.config.suggestions[ i ] )
+                                                               .on( 'mousemove', function () {
+                                                                       context.data.selectedWithMouse = true;
+                                                                       highlight(
+                                                                               context,
+                                                                               $( this ).closest( '.suggestions-results .suggestions-result' ),
+                                                                               false
+                                                                       );
+                                                               } )
+                                                               .appendTo( $results );
+                                                       // Allow custom rendering
+                                                       if ( typeof context.config.result.render === 'function' ) {
+                                                               context.config.result.render.call( $result, context.config.suggestions[ i ], context );
                                                        } else {
-                                                               // Expand from right
-                                                               newCSS.left = 'auto';
-                                                               newCSS.right = $( 'body' ).width() - ( regionPosition.left + context.config.$region.outerWidth() );
+                                                               $result.text( text );
                                                        }
 
-                                                       context.data.$container.css( newCSS );
-                                                       $results = context.data.$container.children( '.suggestions-results' );
-                                                       $results.empty();
-                                                       expWidth = -1;
-                                                       for ( i = 0; i < context.config.suggestions.length; i++ ) {
-                                                               text = context.config.suggestions[ i ];
-                                                               $result = $( '<div>' )
-                                                                       .addClass( 'suggestions-result' )
-                                                                       .attr( 'rel', i )
-                                                                       .data( 'text', context.config.suggestions[ i ] )
-                                                                       .on( 'mousemove', function () {
-                                                                               context.data.selectedWithMouse = true;
-                                                                               $.suggestions.highlight(
-                                                                                       context,
-                                                                                       $( this ).closest( '.suggestions-results .suggestions-result' ),
-                                                                                       false
-                                                                               );
-                                                                       } )
-                                                                       .appendTo( $results );
-                                                               // Allow custom rendering
-                                                               if ( typeof context.config.result.render === 'function' ) {
-                                                                       context.config.result.render.call( $result, context.config.suggestions[ i ], context );
-                                                               } else {
-                                                                       $result.text( text );
-                                                               }
-
-                                                               if ( context.config.highlightInput ) {
-                                                                       $result.highlightText( context.data.prevText, { method: 'prefixHighlight' } );
-                                                               }
-
-                                                               // Widen results box if needed (new width is only calculated here, applied later).
-
-                                                               // The monstrosity below accomplishes two things:
-                                                               // * Wraps the text contents in a DOM element, so that we can know its width. There is
-                                                               //   no way to directly access the width of a text node, and we can't use the parent
-                                                               //   node width as it has text-overflow: ellipsis; and overflow: hidden; applied to
-                                                               //   it, which trims it to a smaller width.
-                                                               // * Temporarily applies position: absolute; to the wrapper to pull it out of normal
-                                                               //   document flow. Otherwise the CSS text-overflow: ellipsis; and overflow: hidden;
-                                                               //   rules would cause some browsers (at least all versions of IE from 6 to 11) to
-                                                               //   still report the "trimmed" width. This should not be done in regular CSS
-                                                               //   stylesheets as we don't want this rule to apply to other <span> elements, like
-                                                               //   the ones generated by jquery.highlightText.
-                                                               $spanForWidth = $result.wrapInner( '<span>' ).children();
-                                                               childrenWidth = $spanForWidth.css( 'position', 'absolute' ).outerWidth();
-                                                               $spanForWidth.contents().unwrap();
-
-                                                               if ( childrenWidth > $result.width() && childrenWidth > expWidth ) {
-                                                                       // factor in any padding, margin, or border space on the parent
-                                                                       expWidth = childrenWidth + ( context.data.$container.width() - $result.width() );
-                                                               }
+                                                       if ( context.config.highlightInput ) {
+                                                               $result.highlightText( context.data.prevText, { method: 'prefixHighlight' } );
                                                        }
 
-                                                       // Apply new width for results box, if any
-                                                       if ( expWidth > context.data.$container.width() ) {
-                                                               maxWidth = context.config.maxExpandFactor * context.data.$textbox.width();
-                                                               context.data.$container.width( Math.min( expWidth, maxWidth ) );
+                                                       // Widen results box if needed (new width is only calculated here, applied later).
+
+                                                       // The monstrosity below accomplishes two things:
+                                                       // * Wraps the text contents in a DOM element, so that we can know its width. There is
+                                                       //   no way to directly access the width of a text node, and we can't use the parent
+                                                       //   node width as it has text-overflow: ellipsis; and overflow: hidden; applied to
+                                                       //   it, which trims it to a smaller width.
+                                                       // * Temporarily applies position: absolute; to the wrapper to pull it out of normal
+                                                       //   document flow. Otherwise the CSS text-overflow: ellipsis; and overflow: hidden;
+                                                       //   rules would cause some browsers (at least all versions of IE from 6 to 11) to
+                                                       //   still report the "trimmed" width. This should not be done in regular CSS
+                                                       //   stylesheets as we don't want this rule to apply to other <span> elements, like
+                                                       //   the ones generated by jquery.highlightText.
+                                                       $spanForWidth = $result.wrapInner( '<span>' ).children();
+                                                       childrenWidth = $spanForWidth.css( 'position', 'absolute' ).outerWidth();
+                                                       $spanForWidth.contents().unwrap();
+
+                                                       if ( childrenWidth > $result.width() && childrenWidth > expWidth ) {
+                                                               // factor in any padding, margin, or border space on the parent
+                                                               expWidth = childrenWidth + ( context.data.$container.width() - $result.width() );
                                                        }
                                                }
-                                       }
-                                       break;
-                               case 'maxRows':
-                                       context.config[ property ] = Math.max( 1, Math.min( 100, value ) );
-                                       break;
-                               case 'delay':
-                                       context.config[ property ] = Math.max( 0, Math.min( 1200, value ) );
-                                       break;
-                               case 'cacheMaxAge':
-                                       context.config[ property ] = Math.max( 1, value );
-                                       break;
-                               case 'maxExpandFactor':
-                                       context.config[ property ] = Math.max( 1, value );
-                                       break;
-                               case 'cache':
-                               case 'submitOnClick':
-                               case 'positionFromLeft':
-                               case 'highlightInput':
-                                       context.config[ property ] = !!value;
-                                       break;
-                       }
-               },
-
-               /**
-                * Highlight a result in the results table
-                *
-                * @param {Object} context
-                * @param {jQuery|string} result `<tr>` to highlight, or 'prev' or 'next'
-                * @param {boolean} updateTextbox If true, put the suggestion in the textbox
-                */
-               highlight: function ( context, result, updateTextbox ) {
-                       var selected = context.data.$container.find( '.suggestions-result-current' );
-                       if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) {
-                               if ( result === 'prev' ) {
-                                       if ( selected.hasClass( 'suggestions-special' ) ) {
-                                               result = context.data.$container.find( '.suggestions-result:last' );
-                                       } else {
-                                               result = selected.prev();
-                                               if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
-                                                       // there is something in the DOM between selected element and the wrapper, bypass it
-                                                       result = selected.parents( '.suggestions-results > *' ).prev().find( '.suggestions-result' ).eq( 0 );
-                                               }
 
-                                               if ( selected.length === 0 ) {
-                                                       // we are at the beginning, so lets jump to the last item
-                                                       if ( context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
-                                                               result = context.data.$container.find( '.suggestions-special' );
-                                                       } else {
-                                                               result = context.data.$container.find( '.suggestions-results .suggestions-result:last' );
-                                                       }
-                                               }
-                                       }
-                               } else if ( result === 'next' ) {
-                                       if ( selected.length === 0 ) {
-                                               // No item selected, go to the first one
-                                               result = context.data.$container.find( '.suggestions-results .suggestions-result:first' );
-                                               if ( result.length === 0 && context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
-                                                       // No suggestion exists, go to the special one directly
-                                                       result = context.data.$container.find( '.suggestions-special' );
-                                               }
-                                       } else {
-                                               result = selected.next();
-                                               if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
-                                                       // there is something in the DOM between selected element and the wrapper, bypass it
-                                                       result = selected.parents( '.suggestions-results > *' ).next().find( '.suggestions-result' ).eq( 0 );
-                                               }
-
-                                               if ( selected.hasClass( 'suggestions-special' ) ) {
-                                                       result = $( [] );
-                                               } else if (
-                                                       result.length === 0 &&
-                                                       context.data.$container.find( '.suggestions-special' ).html() !== ''
-                                               ) {
-                                                       // We were at the last item, jump to the specials!
-                                                       result = context.data.$container.find( '.suggestions-special' );
+                                               // Apply new width for results box, if any
+                                               if ( expWidth > context.data.$container.width() ) {
+                                                       maxWidth = context.config.maxExpandFactor * context.data.$textbox.width();
+                                                       context.data.$container.width( Math.min( expWidth, maxWidth ) );
                                                }
                                        }
                                }
-                               selected.removeClass( 'suggestions-result-current' );
-                               result.addClass( 'suggestions-result-current' );
-                       }
-                       if ( updateTextbox ) {
-                               if ( result.length === 0 || result.is( '.suggestions-special' ) ) {
-                                       $.suggestions.restore( context );
+                               break;
+                       case 'maxRows':
+                               context.config[ property ] = Math.max( 1, Math.min( 100, value ) );
+                               break;
+                       case 'delay':
+                               context.config[ property ] = Math.max( 0, Math.min( 1200, value ) );
+                               break;
+                       case 'cacheMaxAge':
+                               context.config[ property ] = Math.max( 1, value );
+                               break;
+                       case 'maxExpandFactor':
+                               context.config[ property ] = Math.max( 1, value );
+                               break;
+                       case 'cache':
+                       case 'submitOnClick':
+                       case 'positionFromLeft':
+                       case 'highlightInput':
+                               context.config[ property ] = !!value;
+                               break;
+               }
+       }
+
+       /**
+        * Respond to keypress event
+        *
+        * @param {jQuery.Event} e
+        * @param {Object} context
+        * @param {number} key Code of key pressed
+        */
+       function keypress( e, context, key ) {
+               var selected,
+                       wasVisible = context.data.$container.is( ':visible' ),
+                       preventDefault = false;
+
+               switch ( key ) {
+                       // Arrow down
+                       case 40:
+                               if ( wasVisible ) {
+                                       highlight( context, 'next', true );
+                                       context.data.selectedWithMouse = false;
                                } else {
-                                       context.data.$textbox.val( result.data( 'text' ) );
-                                       // .val() doesn't call any event handlers, so
-                                       // let the world know what happened
-                                       context.data.$textbox.trigger( 'change' );
+                                       update( context, false );
+                               }
+                               preventDefault = true;
+                               break;
+                       // Arrow up
+                       case 38:
+                               if ( wasVisible ) {
+                                       highlight( context, 'prev', true );
+                                       context.data.selectedWithMouse = false;
                                }
+                               preventDefault = wasVisible;
+                               break;
+                       // Escape
+                       case 27:
+                               hide( context );
+                               restore( context );
+                               cancel( context );
                                context.data.$textbox.trigger( 'change' );
-                       }
-               },
-
-               /**
-                * Respond to keypress event
-                *
-                * @param {jQuery.Event} e
-                * @param {Object} context
-                * @param {number} key Code of key pressed
-                */
-               keypress: function ( e, context, key ) {
-                       var selected,
-                               wasVisible = context.data.$container.is( ':visible' ),
-                               preventDefault = false;
-
-                       switch ( key ) {
-                               // Arrow down
-                               case 40:
-                                       if ( wasVisible ) {
-                                               $.suggestions.highlight( context, 'next', true );
-                                               context.data.selectedWithMouse = false;
-                                       } else {
-                                               $.suggestions.update( context, false );
-                                       }
-                                       preventDefault = true;
-                                       break;
-                               // Arrow up
-                               case 38:
-                                       if ( wasVisible ) {
-                                               $.suggestions.highlight( context, 'prev', true );
-                                               context.data.selectedWithMouse = false;
-                                       }
-                                       preventDefault = wasVisible;
-                                       break;
-                               // Escape
-                               case 27:
-                                       $.suggestions.hide( context );
-                                       $.suggestions.restore( context );
-                                       $.suggestions.cancel( context );
-                                       context.data.$textbox.trigger( 'change' );
-                                       preventDefault = wasVisible;
-                                       break;
-                               // Enter
-                               case 13:
-                                       preventDefault = wasVisible;
-                                       selected = context.data.$container.find( '.suggestions-result-current' );
-                                       $.suggestions.hide( context );
-                                       if ( selected.length === 0 || context.data.selectedWithMouse ) {
-                                               // If nothing is selected or if something was selected with the mouse
-                                               // cancel any current requests and allow the form to be submitted
-                                               // (simply don't prevent default behavior).
-                                               $.suggestions.cancel( context );
-                                               preventDefault = false;
-                                       } else if ( selected.is( '.suggestions-special' ) ) {
-                                               if ( typeof context.config.special.select === 'function' ) {
-                                                       // Allow the callback to decide whether to prevent default or not
-                                                       if ( context.config.special.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
-                                                               preventDefault = false;
-                                                       }
+                               preventDefault = wasVisible;
+                               break;
+                       // Enter
+                       case 13:
+                               preventDefault = wasVisible;
+                               selected = context.data.$container.find( '.suggestions-result-current' );
+                               hide( context );
+                               if ( selected.length === 0 || context.data.selectedWithMouse ) {
+                                       // If nothing is selected or if something was selected with the mouse
+                                       // cancel any current requests and allow the form to be submitted
+                                       // (simply don't prevent default behavior).
+                                       cancel( context );
+                                       preventDefault = false;
+                               } else if ( selected.is( '.suggestions-special' ) ) {
+                                       if ( typeof context.config.special.select === 'function' ) {
+                                               // Allow the callback to decide whether to prevent default or not
+                                               if ( context.config.special.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
+                                                       preventDefault = false;
                                                }
-                                       } else {
-                                               if ( typeof context.config.result.select === 'function' ) {
-                                                       // Allow the callback to decide whether to prevent default or not
-                                                       if ( context.config.result.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
-                                                               preventDefault = false;
-                                                       }
+                                       }
+                               } else {
+                                       if ( typeof context.config.result.select === 'function' ) {
+                                               // Allow the callback to decide whether to prevent default or not
+                                               if ( context.config.result.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
+                                                       preventDefault = false;
                                                }
                                        }
-                                       break;
-                               default:
-                                       $.suggestions.update( context, true );
-                                       break;
-                       }
-                       if ( preventDefault ) {
-                               e.preventDefault();
-                               e.stopPropagation();
-                       }
+                               }
+                               break;
+                       default:
+                               update( context, true );
+                               break;
                }
-       };
+               if ( preventDefault ) {
+                       e.preventDefault();
+                       e.stopPropagation();
+               }
+       }
 
        // See file header for method documentation
        $.fn.suggestions = function () {
                                if ( typeof args[ 0 ] === 'object' ) {
                                        // Apply set of properties
                                        for ( key in args[ 0 ] ) {
-                                               $.suggestions.configure( context, key, args[ 0 ][ key ] );
+                                               configure( context, key, args[ 0 ][ key ] );
                                        }
                                } else if ( typeof args[ 0 ] === 'string' ) {
                                        if ( args.length > 1 ) {
                                                // Set property values
-                                               $.suggestions.configure( context, args[ 0 ], args[ 1 ] );
+                                               configure( context, args[ 0 ], args[ 1 ] );
                                        }
                                }
                        }
                                        prevText: null,
 
                                        // Cache of fetched suggestions
-                                       cache: {},
+                                       cache: Object.create( null ),
 
                                        // Number of results visible without scrolling
                                        visibleResults: 0,
                                                                if ( $result.get( 0 ) !== $other.get( 0 ) ) {
                                                                        return;
                                                                }
-                                                               $.suggestions.highlight( context, $result, true );
+                                                               highlight( context, $result, true );
                                                                if ( typeof context.config.result.select === 'function' ) {
                                                                        context.config.result.select.call( $result, context.data.$textbox, 'mouse' );
                                                                }
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (T64858).
                                                                        setTimeout( function () {
-                                                                               $.suggestions.hide( context );
+                                                                               hide( context );
                                                                        } );
                                                                }
                                                                // Always bring focus to the textbox, as that's probably where the user expects it
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (T64858).
                                                                        setTimeout( function () {
-                                                                               $.suggestions.hide( context );
+                                                                               hide( context );
                                                                        } );
                                                                }
                                                                // Always bring focus to the textbox, as that's probably where the user expects it
                                                        } )
                                                        .on( 'mousemove', function ( e ) {
                                                                context.data.selectedWithMouse = true;
-                                                               $.suggestions.highlight(
+                                                               highlight(
                                                                        context, $( e.target ).closest( '.suggestions-special' ), false
                                                                );
                                                        } )
                                        )
-                                       .appendTo( $( 'body' ) );
+                                       .appendTo( document.body );
 
                                $( this )
                                        // Stop browser autocomplete from interfering
                                        } )
                                        .on( 'keypress', function ( e ) {
                                                context.data.keypressedCount++;
-                                               $.suggestions.keypress( e, context, context.data.keypressed );
+                                               keypress( e, context, context.data.keypressed );
                                        } )
                                        .on( 'keyup', function ( e ) {
                                                // The keypress event is fired when a key is pressed down and that key normally
                                                        e.which === context.data.keypressed &&
                                                        allowed.indexOf( e.which ) !== -1
                                                ) {
-                                                       $.suggestions.keypress( e, context, context.data.keypressed );
+                                                       keypress( e, context, context.data.keypressed );
                                                }
                                        } )
                                        .on( 'blur', function () {
                                                if ( context.data.mouseDownOn.length > 0 ) {
                                                        return;
                                                }
-                                               $.suggestions.hide( context );
-                                               $.suggestions.cancel( context );
+                                               hide( context );
+                                               cancel( context );
                                        } );
                                // Load suggestions if the value is changed because there are already
                                // typed characters before the JavaScript is loaded.
-                               if ( this.value !== this.defaultValue ) {
-                                       $.suggestions.update( context, false );
+                               if ( $( this ).is( ':focus' ) && this.value !== this.defaultValue ) {
+                                       update( context, false );
                                }
                        }
 
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 900dab2..259febc 100644 (file)
@@ -507,7 +507,7 @@ Title.makeTitle = function ( namespace, title ) {
  * @return {mw.Title|null} A valid Title object or null if the input cannot be turned into a valid title
  */
 Title.newFromUserInput = function ( title, defaultNamespaceOrOptions, options ) {
-       var namespace, m, id, ext, parts,
+       var namespace, m, id, ext, lastDot,
                defaultNamespace;
 
        // defaultNamespace is optional; check whether options moves up
@@ -553,35 +553,27 @@ Title.newFromUserInput = function ( title, defaultNamespaceOrOptions, options )
                namespace === NS_MEDIA ||
                ( options.forUploading && ( namespace === NS_FILE ) )
        ) {
-
                title = sanitize( title, [ 'generalRule', 'fileRule' ] );
 
                // Operate on the file extension
                // Although it is possible having spaces between the name and the ".ext" this isn't nice for
                // operating systems hiding file extensions -> strip them later on
-               parts = title.split( '.' );
-
-               if ( parts.length > 1 ) {
-
-                       // Get the last part, which is supposed to be the file extension
-                       ext = parts.pop();
+               lastDot = title.lastIndexOf( '.' );
 
-                       // Remove whitespace of the name part (that W/O extension)
-                       title = parts.join( '.' ).trim();
-
-                       // Cut, if too long and append file extension
-                       title = trimFileNameToByteLength( title, ext );
+               // No or empty file extension
+               if ( lastDot === -1 || lastDot >= title.length - 1 ) {
+                       return null;
+               }
 
-               } else {
+               // Get the last part, which is supposed to be the file extension
+               ext = title.slice( lastDot + 1 );
 
-                       // Missing file extension
-                       title = parts.join( '.' ).trim();
+               // Remove whitespace of the name part (that without extension)
+               title = title.slice( 0, lastDot ).trim();
 
-                       // Name has no file extension and a fallback wasn't provided either
-                       return null;
-               }
+               // Cut, if too long and append file extension
+               title = trimFileNameToByteLength( title, ext );
        } else {
-
                title = sanitize( title, [ 'generalRule' ] );
 
                // Cut titles exceeding the TITLE_MAX_BYTES byte size limit
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 cc206b7..70e5102 100644 (file)
@@ -124,7 +124,7 @@ pre,
        background: #fff;
        color: #000;
        border: 1pt dashed #000;
-       padding: 1em 0;
+       padding: 1em;
        font-size: 8pt;
        white-space: pre-wrap;
        word-wrap: break-word;
index 630e3a6..4eba81d 100644 (file)
@@ -65,8 +65,6 @@
                        api.postWithToken( 'csrf', {
                                action: 'logout'
                        } ).done( function () {
-                               // Horrible hack until deprecation of logoutToken in GET is done
-                               returnUrl = returnUrl.replace( /logoutToken=.+?($|&)/g, 'logoutToken=%2B%5C' );
                                window.location = returnUrl;
                        } ).fail( function ( e ) {
                                mw.notify(
index d3fce46..dff7881 100644 (file)
 }
 
 @-webkit-keyframes rcfiltersBouncedelay {
+       // 50% equals 800ms
        0%,
-       50%, // equals 800ms
+       50%,
        100% {
                -webkit-transform: scale( 0.625 );
        }
 
-       20% { // equals 320ms
+       // equals 320ms
+       20% {
                opacity: 0.87;
                -webkit-transform: scale( 1 );
        }
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 861111a..3b643a5 100644 (file)
@@ -60,6 +60,7 @@ $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",
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 486b16d..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.
@@ -2380,13 +2384,20 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * @param string $text Content of the page
         * @param string $summary Optional summary string for the revision
         * @param int $defaultNs Optional namespace id
+        * @param User|null $user If null, static::getTestSysop()->getUser() is used.
         * @return Status Object as returned by WikiPage::doEditContent()
         * @throws MWException If this test cases's needsDB() method doesn't return true.
         *         Test cases can use "@group Database" to enable database test support,
         *         or list the tables under testing in $this->tablesUsed, or override the
         *         needsDB() method.
         */
-       protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
+       protected function editPage(
+               $pageName,
+               $text,
+               $summary = '',
+               $defaultNs = NS_MAIN,
+               User $user = null
+       ) {
                if ( !$this->needsDB() ) {
                        throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
                                ' method should return true. Use @group Database or $this->tablesUsed.' );
@@ -2395,7 +2406,13 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                $title = Title::newFromText( $pageName, $defaultNs );
                $page = WikiPage::factory( $title );
 
-               return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
+               return $page->doEditContent(
+                       ContentHandler::makeContent( $text, $title ),
+                       $summary,
+                       0,
+                       false,
+                       $user
+               );
        }
 
        /**
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 e9a8a1f..3e4531c 100644 (file)
@@ -2,7 +2,6 @@
 
 use MediaWiki\MediaWikiServices;
 use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
 
 abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
        // Version hash for a blank file module.
@@ -34,7 +33,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
                        'only' => 'scripts',
                        'safemode' => null,
                ];
-               $resourceLoader = $rl ?: new ResourceLoader();
+               $resourceLoader = $rl ?: new ResourceLoader( MediaWikiServices::getInstance()->getMainConfig() );
                $request = new FauxRequest( [
                                'debug' => $options['debug'],
                                'lang' => $options['lang'],
@@ -57,16 +56,23 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
                        // For ResourceLoader::inDebugMode since it doesn't have context
                        'ResourceLoaderDebug' => true,
 
-                       // Avoid influence from wgInvalidateCacheOnLocalSettingsChange
-                       'CacheEpoch' => '20140101000000',
-
-                       // For wfScript()
+                       // For ResourceLoaderStartUpModule and ResourceLoader::__construct()
                        'ScriptPath' => '/w',
                        'Script' => '/w/index.php',
                        'LoadScript' => '/w/load.php',
+
+                       // For ResourceLoader::register() - TODO: Inject somehow T32956
+                       'ResourceModuleSkinStyles' => [],
+
+                       // For ResourceLoader::respond() - TODO: Inject somehow T32956
+                       'UseFileCache' => false,
                ];
        }
 
+       public static function getMinimalConfig() {
+               return new HashConfig( self::getSettings() );
+       }
+
        protected function setUp() {
                parent::setUp();
 
@@ -154,12 +160,13 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
 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 ) {
@@ -171,14 +178,8 @@ class ResourceLoaderFileModuleTestModule extends ResourceLoaderFileModule {
 }
 
 class EmptyResourceLoader extends ResourceLoader {
-       // TODO: This won't be needed once ResourceLoader is empty by default
-       // and default registrations are done from ServiceWiring instead.
        public function __construct( Config $config = null, LoggerInterface $logger = null ) {
-               $this->setLogger( $logger ?: new NullLogger() );
-               $this->config = $config ?: MediaWikiServices::getInstance()->getMainConfig();
-               // Source "local" is required by StartupModule
-               $this->addSource( 'local', $this->config->get( 'LoadScript' ) );
-               $this->setMessageBlobStore( new MessageBlobStore( $this, $this->getLogger() ) );
+               parent::__construct( $config ?: ResourceLoaderTestCase::getMinimalConfig(), $logger );
        }
 
        public function getErrors() {
diff --git a/tests/phpunit/data/upload/jpeg-a-href-in-metadata.jpg b/tests/phpunit/data/upload/jpeg-a-href-in-metadata.jpg
new file mode 100644 (file)
index 0000000..5438736
Binary files /dev/null and b/tests/phpunit/data/upload/jpeg-a-href-in-metadata.jpg differ
diff --git a/tests/phpunit/data/upload/png-embedded-breaks-ie5.png b/tests/phpunit/data/upload/png-embedded-breaks-ie5.png
new file mode 100644 (file)
index 0000000..0af03fc
Binary files /dev/null and b/tests/phpunit/data/upload/png-embedded-breaks-ie5.png differ
diff --git a/tests/phpunit/data/upload/png-plain.png b/tests/phpunit/data/upload/png-plain.png
new file mode 100644 (file)
index 0000000..83e9130
Binary files /dev/null and b/tests/phpunit/data/upload/png-plain.png differ
index de70f26..40c45dc 100644 (file)
@@ -18,15 +18,6 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                'actor',
        ];
 
-       /**
-        * Create an ActorMigration for a particular stage
-        * @param int $stage
-        * @return ActorMigration
-        */
-       protected function makeMigration( $stage ) {
-               return new ActorMigration( $stage );
-       }
-
        /**
         * @dataProvider provideConstructor
         * @param int $stage
@@ -81,7 +72,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
         * @param array $expect
         */
        public function testGetJoin( $stage, $key, $expect ) {
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $result = $m->getJoin( $key );
                $this->assertEquals( $expect, $result );
        }
@@ -260,7 +251,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        $users = reset( $users );
                }
 
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $result = $m->getWhere( $this->db, $key, $users, $useId );
                $this->assertEquals( $expect, $result );
        }
@@ -510,7 +501,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                $extraFields['ipb_address'] = __CLASS__ . "#{$stageNames[$writeStage]}";
                        }
 
-                       $w = $this->makeMigration( $writeStage );
+                       $w = new ActorMigration( $writeStage );
                        $usesTemp = $key === 'rev_user';
 
                        if ( $usesTemp ) {
@@ -543,7 +534,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        }
 
                        foreach ( $possibleReadStages as $readStage ) {
-                               $r = $this->makeMigration( $readStage );
+                               $r = new ActorMigration( $readStage );
 
                                $queryInfo = $r->getJoin( $key );
                                $row = $this->db->selectRow(
@@ -615,7 +606,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
         * @expectedExceptionMessage Must use getInsertValuesWithTempTable() for rev_user
         */
        public function testInsertWrong( $stage ) {
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $m->getInsertValues( $this->db, 'rev_user', $this->getTestUser()->getUser() );
        }
 
@@ -626,7 +617,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
         * @expectedExceptionMessage Must use getInsertValues() for rc_user
         */
        public function testInsertWithTempTableWrong( $stage ) {
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() );
        }
 
@@ -639,7 +630,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                $wrap->formerTempTables += [ 'rc_user' => '1.30' ];
 
                $this->hideDeprecated( 'ActorMigration::getInsertValuesWithTempTable for rc_user' );
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                list( $fields, $callback )
                        = $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() );
                $this->assertTrue( is_callable( $callback ) );
@@ -652,7 +643,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
         * @expectedExceptionMessage $extra[rev_timestamp] is not provided
         */
        public function testInsertWithTempTableCallbackMissingFields( $stage ) {
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                list( $fields, $callback )
                        = $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $this->getTestUser()->getUser() );
                $callback( 1, [] );
@@ -677,7 +668,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
                list( $cFields, $cCallback ) = MediaWikiServices::getInstance()->getCommentStore()
                        ->insertWithTempTable( $this->db, 'rev_comment', '' );
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                list( $fields, $callback ) =
                        $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity );
                $extraFields = [
@@ -701,7 +692,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        (int)$row->rev_actor
                );
 
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $fields = $m->getInsertValues( $this->db, 'dummy_user', $userIdentity );
                if ( $stage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $this->assertSame( $user->getId(), $fields['dummy_user'] );
@@ -730,7 +721,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
         * @param string $isNotAnon
         */
        public function testIsAnon( $stage, $isAnon, $isNotAnon ) {
-               $m = $this->makeMigration( $stage );
+               $m = new ActorMigration( $stage );
                $this->assertSame( $isAnon, $m->isAnon( 'foo' ) );
                $this->assertSame( $isNotAnon, $m->isNotAnon( 'foo' ) );
        }
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..f073f6e 100644 (file)
@@ -2715,14 +2715,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 +2747,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 +2819,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 +2833,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 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 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 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 af49ecf..32c7571 100644 (file)
@@ -30,10 +30,6 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
                return $row;
        }
 
-       private function getTitleArrayFromResult( $resultWrapper ) {
-               return new TitleArrayFromResult( $resultWrapper );
-       }
-
        /**
         * @covers TitleArrayFromResult::__construct
         */
@@ -41,7 +37,7 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
                $row = false;
                $resultWrapper = $this->getMockResultWrapper( $row );
 
-               $object = $this->getTitleArrayFromResult( $resultWrapper );
+               $object = new TitleArrayFromResult( $resultWrapper );
 
                $this->assertEquals( $resultWrapper, $object->res );
                $this->assertSame( 0, $object->key );
@@ -57,7 +53,7 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
                $row = $this->getRowWithTitle( $namespace, $title );
                $resultWrapper = $this->getMockResultWrapper( $row );
 
-               $object = $this->getTitleArrayFromResult( $resultWrapper );
+               $object = new TitleArrayFromResult( $resultWrapper );
 
                $this->assertEquals( $resultWrapper, $object->res );
                $this->assertSame( 0, $object->key );
@@ -79,7 +75,7 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
         * @covers TitleArrayFromResult::count
         */
        public function testCountWithVaryingValues( $numRows ) {
-               $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper(
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper(
                        $this->getRowWithTitle(),
                        $numRows
                ) );
@@ -93,7 +89,7 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
                $namespace = 0;
                $title = 'foo';
                $row = $this->getRowWithTitle( $namespace, $title );
-               $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper( $row ) );
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $row ) );
                $this->assertInstanceOf( Title::class, $object->current() );
                $this->assertEquals( $namespace, $object->current->mNamespace );
                $this->assertEquals( $title, $object->current->mTextform );
@@ -111,7 +107,7 @@ class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
         * @covers TitleArrayFromResult::valid
         */
        public function testValid( $input, $expected ) {
-               $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $input ) );
                $this->assertEquals( $expected, $object->valid() );
        }
 
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 f20a061..6bbdd3b 100644 (file)
@@ -25,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 aec25c1..39a5534 100644 (file)
@@ -29,6 +29,7 @@ class BlockManagerTest extends MediaWikiTestCase {
                        'wgEnableDnsBlacklist' => true,
                        'wgProxyList' => [],
                        'wgProxyWhitelist' => [],
+                       'wgSecretKey' => false,
                        'wgSoftBlockRanges' => [],
                ];
        }
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 7fc070c..169e4bf 100644 (file)
@@ -26,6 +26,7 @@ use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\Rdbms\LoadMonitorNull;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @group Database
@@ -165,7 +166,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 +178,19 @@ class LoadBalancerTest extends MediaWikiTestCase {
                                'load' => 0,
                                'flags' => $flags
                        ],
-                       [ // emulated replica
+                       // Main replica DBs
+                       1 => [
+                               'host' => $wgDBserver,
+                               'dbname' => $wgDBname,
+                               'tablePrefix' => $this->dbPrefix(),
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'load' => 100,
+                               'flags' => $flags
+                       ],
+                       2 => [
                                'host' => $wgDBserver,
                                'dbname' => $wgDBname,
                                'tablePrefix' => $this->dbPrefix(),
@@ -186,6 +200,66 @@ class LoadBalancerTest extends MediaWikiTestCase {
                                '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
                        ]
                ];
 
@@ -488,4 +562,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 80238ec..0132efc 100644 (file)
@@ -11,10 +11,6 @@ use MediaWiki\MediaWikiServices;
  */
 class ImportTest extends MediaWikiLangTestCase {
 
-       private function getDataSource( $xml ) {
-               return new ImportStringSource( $xml );
-       }
-
        /**
         * @covers WikiImporter
         * @dataProvider getUnknownTagsXML
@@ -23,7 +19,7 @@ class ImportTest extends MediaWikiLangTestCase {
         * @param string $title
         */
        public function testUnknownXMLTags( $xml, $text, $title ) {
-               $source = $this->getDataSource( $xml );
+               $source = new ImportStringSource( $xml );
 
                $importer = new WikiImporter(
                        $source,
@@ -82,7 +78,7 @@ EOF
         * @param string|null $redirectTitle
         */
        public function testHandlePageContainsRedirect( $xml, $redirectTitle ) {
-               $source = $this->getDataSource( $xml );
+               $source = new ImportStringSource( $xml );
 
                $redirect = null;
                $callback = function ( Title $title, ForeignTitle $foreignTitle, $revCount,
@@ -168,7 +164,7 @@ EOF
         * @param array|null $namespaces
         */
        public function testSiteInfoContainsNamespaces( $xml, $namespaces ) {
-               $source = $this->getDataSource( $xml );
+               $source = new ImportStringSource( $xml );
 
                $importNamespaces = null;
                $callback = function ( array $siteinfo, $innerImporter ) use ( &$importNamespaces ) {
@@ -253,7 +249,7 @@ EOF
                $n = ( $assign ? 1 : 0 ) + ( $create ? 2 : 0 );
 
                // phpcs:disable Generic.Files.LineLength
-               $source = $this->getDataSource( <<<EOF
+               $source = new ImportStringSource( <<<EOF
 <mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en">
   <page>
     <title>TestImportPage</title>
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' ) );
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 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 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 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 );
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 c210061..e178e96 100644 (file)
@@ -16,7 +16,10 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
                                'skin' => 'fallback',
                                'target' => 'test',
                ] );
-               return new ResourceLoaderContext( new ResourceLoader(), $request );
+               return new ResourceLoaderContext(
+                       new ResourceLoader( ResourceLoaderTestCase::getMinimalConfig() ),
+                       $request
+               );
        }
 
        public function testChangeModules() {
@@ -30,6 +33,7 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
        public function testChangeLanguageAndDirection() {
                $derived = new DerivativeResourceLoaderContext( self::makeContext() );
                $this->assertSame( $derived->getLanguage(), 'qqx', 'inherit from parent' );
+               $this->assertSame( $derived->getDirection(), 'ltr', 'inherit from parent' );
 
                $derived->setLanguage( 'nl' );
                $this->assertSame( $derived->getLanguage(), 'nl' );
index 9afa232..e094d92 100644 (file)
@@ -25,7 +25,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        public function testBlobCreation() {
                $module = $this->makeModule( [ 'mainpage' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
 
                $blobStore = $this->makeBlobStore( null, $rl );
@@ -36,7 +36,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        public function testBlobCreation_empty() {
                $module = $this->makeModule( [] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
 
                $blobStore = $this->makeBlobStore( null, $rl );
@@ -47,7 +47,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        public function testBlobCreation_unknownMessage() {
                $module = $this->makeModule( [ 'i-dont-exist', 'mainpage', 'i-dont-exist2' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( null, $rl );
 
@@ -59,7 +59,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        public function testMessageCachingAndPurging() {
                $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
 
@@ -104,7 +104,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        public function testPurgeEverything() {
                $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                // Advance this new WANObjectCache instance to a normal state.
@@ -138,7 +138,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        public function testValidateAgainstModuleRegistry() {
                // Arrange version 1 of a module
                $module = $this->makeModule( [ 'foo' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
@@ -157,7 +157,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                // must always match the set of message keys required by the module.
                // We do not receive purges for this because no messages were changed.
                $module = $this->makeModule( [ 'foo', 'bar' ] );
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->exactly( 2 ) )
index 206160c..03a3e24 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"></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;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;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"></script>';
                // phpcs:enable
 
                $this->assertSame( $expected, (string)$client->getHeadHtml() );
@@ -228,50 +228,50 @@ Deprecation message.' ]
                                'modules' => [ 'test.scripts.raw' ],
                                '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>',
+                               'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts"></script>',
                        ],
                        [
                                'context' => [],
                                'modules' => [ 'test.scripts.raw' ],
                                '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>',
+                               'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&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
index 60cd4a8..c3d5ec1 100644 (file)
@@ -15,6 +15,8 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
                return new EmptyResourceLoader( new HashConfig( [
                        'ResourceLoaderDebug' => false,
                        'LoadScript' => '/w/load.php',
+                       // For ResourceLoader::register()
+                       'ResourceModuleSkinStyles' => [],
                ] ) );
        }
 
@@ -45,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() {
@@ -74,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..1585cbc 100644 (file)
@@ -347,7 +347,6 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                $module = new ResourceLoaderFileTestModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'styles.less' ],
-               ], [
                        'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
                ] );
                $module->setName( 'test.less' );
@@ -355,27 +354,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 +533,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                                        'main' => 'init.js'
                                ]
                        ],
-                       [
+                       'package file with callback' => [
                                $base + [
                                        'packageFiles' => [
                                                [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
@@ -498,6 +580,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 +616,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                                ],
                                false
                        ],
-                       [
+                       'package file with invalid callback' => [
                                $base + [
                                        'packageFiles' => [
                                                [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
@@ -559,7 +669,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 85a47de..1171ebc 100644 (file)
@@ -34,6 +34,42 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                $this->assertSame( 1, $ranHook, 'Hook was called' );
        }
 
+       public static function provideInvalidModuleName() {
+               return [
+                       'name with 300 chars' => [ str_repeat( 'x', 300 ) ],
+                       'name with bang' => [ 'this!that' ],
+                       'name with comma' => [ 'this,that' ],
+                       'name with pipe' => [ 'this|that' ],
+               ];
+       }
+
+       public static function provideValidModuleName() {
+               return [
+                       'empty string' => [ '' ],
+                       'simple name' => [ 'this.and-that2' ],
+                       'name with 100 chars' => [ str_repeat( 'x', 100 ) ],
+                       'name with hash' => [ 'this#that' ],
+                       'name with slash' => [ 'this/that' ],
+                       'name with at' => [ 'this@that' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInvalidModuleName
+        * @covers ResourceLoader
+        */
+       public function testIsValidModuleName_invalid( $name ) {
+               $this->assertFalse( ResourceLoader::isValidModuleName( $name ) );
+       }
+
+       /**
+        * @dataProvider provideValidModuleName
+        * @covers ResourceLoader
+        */
+       public function testIsValidModuleName_valid( $name ) {
+               $this->assertTrue( ResourceLoader::isValidModuleName( $name ) );
+       }
+
        /**
         * @covers ResourceLoader::register
         * @covers ResourceLoader::getModule
@@ -60,6 +96,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
 
        /**
         * @covers ResourceLoader::register
+        * @group medium
         */
        public function testRegisterEmptyString() {
                $module = new ResourceLoaderTestModule();
@@ -70,6 +107,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
 
        /**
         * @covers ResourceLoader::register
+        * @group medium
         */
        public function testRegisterInvalidName() {
                $resourceLoader = new EmptyResourceLoader();
@@ -111,7 +149,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
                $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
                $this->assertEquals(
-                       [ 'test.foo', 'test.bar' ],
+                       [ 'startup', 'test.foo', 'test.bar' ],
                        $resourceLoader->getModuleNames()
                );
        }
@@ -318,7 +356,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
         * @covers ResourceLoader::getSources
         */
        public function testAddSource( $name, $info, $expected ) {
-               $rl = new ResourceLoader;
+               $rl = new EmptyResourceLoader;
                $rl->addSource( $name, $info );
                if ( is_array( $expected ) ) {
                        foreach ( $expected as $source ) {
@@ -333,7 +371,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
         * @covers ResourceLoader::addSource
         */
        public function testAddSourceDupe() {
-               $rl = new ResourceLoader;
+               $rl = new EmptyResourceLoader;
                $this->setExpectedException(
                        MWException::class, 'ResourceLoader duplicate source addition error'
                );
@@ -345,7 +383,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
         * @covers ResourceLoader::addSource
         */
        public function testAddSourceInvalid() {
-               $rl = new ResourceLoader;
+               $rl = new EmptyResourceLoader;
                $this->setExpectedException( MWException::class, 'with no "loadScript" key' );
                $rl->addSource( 'foo',  [ 'x' => 'https://example.org/w/load.php' ] );
        }
@@ -623,7 +661,7 @@ END
         * @covers ResourceLoader::getLoadScript
         */
        public function testGetLoadScript() {
-               $rl = new ResourceLoader();
+               $rl = new EmptyResourceLoader();
                $sources = self::fakeSources();
                $rl->addSource( $sources );
                foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
@@ -881,12 +919,13 @@ END
         * @covers ResourceLoader::makeModuleResponse
         */
        public function testMakeModuleResponseStartupError() {
-               $rl = new EmptyResourceLoader();
+               // This is an integration test that uses a lot of MediaWiki state,
+               // provide the full Config object here.
+               $rl = new EmptyResourceLoader( MediaWikiServices::getInstance()->getMainConfig() );
                $rl->register( [
                        'foo' => self::getSimpleModuleMock( 'foo();' ),
                        'ferry' => self::getFailFerryMock(),
                        'bar' => self::getSimpleModuleMock( 'bar();' ),
-                       'startup' => [ 'class' => ResourceLoaderStartUpModule::class ],
                ] );
                $context = $this->getResourceLoaderContext(
                        [
@@ -897,7 +936,7 @@ END
                );
 
                $this->assertEquals(
-                       [ 'foo', 'ferry', 'bar', 'startup' ],
+                       [ 'startup', 'foo', 'ferry', 'bar' ],
                        $rl->getModuleNames(),
                        'getModuleNames'
                );
index 48c3d17..a1fdf8a 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Session;
 
+use Wikimedia\AtEase\AtEase;
 use Config;
 use MediaWikiTestCase;
 use User;
@@ -900,7 +901,7 @@ class SessionBackendTest extends MediaWikiTestCase {
                $manager->globalSessionRequest = $request;
 
                session_id( self::SESSIONID );
-               \Wikimedia\quietCall( 'session_start' );
+               AtEase::quietCall( 'session_start' );
                $_SESSION['foo'] = __METHOD__;
                $backend->resetId();
                $this->assertNotEquals( self::SESSIONID, $backend->getId() );
@@ -938,7 +939,7 @@ class SessionBackendTest extends MediaWikiTestCase {
                $manager->globalSessionRequest = $request;
 
                session_id( self::SESSIONID . 'x' );
-               \Wikimedia\quietCall( 'session_start' );
+               AtEase::quietCall( 'session_start' );
                $backend->unpersist();
                $this->assertSame( self::SESSIONID . 'x', session_id() );
                session_write_close();
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 58c69e3..cafc846 100644 (file)
@@ -585,6 +585,42 @@ class UploadBaseTest extends MediaWikiTestCase {
                        [ '<?xml version="1.0" encoding="WINDOWS-1252"?><svg></svg>', false ],
                ];
        }
+
+       /**
+        * @covers UploadBase::detectScript
+        * @dataProvider provideDetectScript
+        */
+       public function testDetectScript( $filename, $mime, $extension, $expected, $message ) {
+               $result = $this->upload->detectScript( $filename, $mime, $extension );
+               $this->assertSame( $expected, $result, $message );
+       }
+
+       public static function provideDetectScript() {
+               global $IP;
+               return [
+                       [
+                               "$IP/tests/phpunit/data/upload/png-plain.png",
+                               'image/png',
+                               'png',
+                               false,
+                               'PNG with no suspicious things in it, should pass.'
+                       ],
+                       [
+                               "$IP/tests/phpunit/data/upload/png-embedded-breaks-ie5.png",
+                               'image/png',
+                               'png',
+                               true,
+                               'PNG with embedded data that IE5/6 interprets as HTML; should be rejected.'
+                       ],
+                       [
+                               "$IP/tests/phpunit/data/upload/jpeg-a-href-in-metadata.jpg",
+                               'image/jpeg',
+                               'jpeg',
+                               false,
+                               'JPEG with innocuous HTML in metadata from a flickr photo; should pass (T27707).'
+                       ],
+               ];
+       }
 }
 
 class UploadTestHandler extends UploadBase {
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 beaacec..4cbfe46 100644 (file)
@@ -27,10 +27,6 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
                return $row;
        }
 
-       private function getUserArrayFromResult( $resultWrapper ) {
-               return new UserArrayFromResult( $resultWrapper );
-       }
-
        /**
         * @covers UserArrayFromResult::__construct
         */
@@ -38,7 +34,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
                $row = false;
                $resultWrapper = $this->getMockResultWrapper( $row );
 
-               $object = $this->getUserArrayFromResult( $resultWrapper );
+               $object = new UserArrayFromResult( $resultWrapper );
 
                $this->assertEquals( $resultWrapper, $object->res );
                $this->assertSame( 0, $object->key );
@@ -53,7 +49,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
                $row = $this->getRowWithUsername( $username );
                $resultWrapper = $this->getMockResultWrapper( $row );
 
-               $object = $this->getUserArrayFromResult( $resultWrapper );
+               $object = new UserArrayFromResult( $resultWrapper );
 
                $this->assertEquals( $resultWrapper, $object->res );
                $this->assertSame( 0, $object->key );
@@ -74,7 +70,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
         * @covers UserArrayFromResult::count
         */
        public function testCountWithVaryingValues( $numRows ) {
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper(
+               $object = new UserArrayFromResult( $this->getMockResultWrapper(
                        $this->getRowWithUsername(),
                        $numRows
                ) );
@@ -87,7 +83,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
        public function testCurrentAfterConstruction() {
                $username = 'addshore';
                $userRow = $this->getRowWithUsername( $username );
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
                $this->assertInstanceOf( User::class, $object->current() );
                $this->assertEquals( $username, $object->current()->mName );
        }
@@ -104,7 +100,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase {
         * @covers UserArrayFromResult::valid
         */
        public function testValid( $input, $expected ) {
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $input ) );
                $this->assertEquals( $expected, $object->valid() );
        }
 
index aa6ae48..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
         */
@@ -626,8 +636,10 @@ class UserTest extends MediaWikiTestCase {
                $cookies = $request1->response()->getCookies();
                $this->assertArrayHasKey( 'wmsitetitleBlockID', $cookies );
                $this->assertEquals( $expiryFiveHours, $cookies['wmsitetitleBlockID']['expire'] );
-               $cookieValue = DatabaseBlock::getIdFromCookieValue( $cookies['wmsitetitleBlockID']['value'] );
-               $this->assertEquals( $block->getId(), $cookieValue );
+               $cookieId = MediaWikiServices::getInstance()->getBlockManager()->getIdFromCookieValue(
+                       $cookies['wmsitetitleBlockID']['value']
+               );
+               $this->assertEquals( $block->getId(), $cookieId );
 
                // 2. Create a new request, set the cookies, and see if the (anon) user is blocked.
                $request2 = new FauxRequest();
@@ -777,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() );
@@ -807,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() );
        }
@@ -1314,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
@@ -1470,7 +1503,7 @@ class UserTest extends MediaWikiTestCase {
 
                // get user
                $user = User::newFromSession( $request );
-               $user->trackBlockWithCookie();
+               MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $user );
 
                // test cookie was set
                $cookies = $request->response()->getCookies();
@@ -1506,7 +1539,7 @@ class UserTest extends MediaWikiTestCase {
 
                // get user
                $user = User::newFromSession( $request );
-               $user->trackBlockWithCookie();
+               MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $user );
 
                // test cookie was not set
                $cookies = $request->response()->getCookies();
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 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 4f95fbb..4329867 100644 (file)
@@ -4,7 +4,6 @@
  * Checks that all API query modules, core and extensions, have unique prefixes.
  *
  * @group API
- * @coversNothing
  */
 class ApiPrefixUniquenessTest extends MediaWikiTestCase {
 
index 0d10a20..6b64b40 100644 (file)
@@ -11,7 +11,6 @@ use Wikimedia\TestingAccessWrapper;
  * - do not have inconsistencies in the parameter definitions
  *
  * @group API
- * @coversNothing
  */
 class ApiStructureTest extends MediaWikiTestCase {
 
index 2ae6a78..37babce 100644 (file)
@@ -1,8 +1,5 @@
 <?php
 
-/**
- * @coversNothing
- */
 class AutoLoaderStructureTest extends MediaWikiTestCase {
        /**
         * Assert that there were no classes loaded that are not registered with the AutoLoader.
@@ -49,7 +46,9 @@ class AutoLoaderStructureTest extends MediaWikiTestCase {
                        // Check that the expected class name (based on the filename) is the
                        // same as the one we found.
                        // Strip directory prefix from front of filename, and .php extension
-                       $abbrFileName = substr( substr( $file, strlen( $dir ) ), 0, -4 );
+                       $dirNameLength = strlen( realpath( $dir ) ) + 1; // +1 for the trailing slash
+                       $fileBaseName = substr( $file, $dirNameLength );
+                       $abbrFileName = substr( $fileBaseName, 0, -4 );
                        $expectedClassName = $prefix . str_replace( '/', '\\', $abbrFileName );
 
                        $this->assertSame(
index 57b063d..2a6575a 100644 (file)
@@ -35,9 +35,6 @@ class AvailableRightsTest extends PHPUnit\Framework\TestCase {
                return $rights;
        }
 
-       /**
-        * @coversNothing
-        */
        public function testAvailableRights() {
                $missingRights = array_diff(
                        $this->getAllVisibleRights(),
@@ -69,8 +66,6 @@ class AvailableRightsTest extends PHPUnit\Framework\TestCase {
         * Test, if for all rights a right- message exist,
         * which is used on Special:ListGroupRights as help text
         * Extensions and core
-        *
-        * @coversNothing
         */
        public function testAllRightsWithMessage() {
                $this->checkMessagesExist( 'right-' );
index c75a9d0..c8bcd60 100644 (file)
@@ -32,7 +32,6 @@ class ContentHandlerSanityTest extends MediaWikiTestCase {
        }
 
        /**
-        * @coversNothing
         * @dataProvider provideHandlers
         * @param ContentHandler $handler
         */
index 9c0a73d..b0c1c8f 100644 (file)
@@ -5,7 +5,6 @@ use Wikimedia\Rdbms\Database;
 
 /**
  * @group Database
- * @coversNothing
  */
 class DatabaseIntegrationTest extends MediaWikiTestCase {
        /**
index dea8f5a..60c97cc 100644 (file)
@@ -19,8 +19,6 @@
 /**
  * Validates all loaded extensions and skins using the ExtensionRegistry
  * against the extension.json schema in the docs/ folder.
- *
- * @coversNothing
  */
 class ExtensionJsonValidationTest extends PHPUnit\Framework\TestCase {
 
index 60ce575..d7f865d 100644 (file)
@@ -1,8 +1,5 @@
 <?php
 
-/**
- * @coversNothing
- */
 class PasswordPolicyStructureTest extends MediaWikiTestCase {
 
        public function provideChecks() {
index f41ab3a..4c34208 100644 (file)
@@ -14,7 +14,6 @@ use Wikimedia\TestingAccessWrapper;
  * @copyright © 2012, Niklas Laxström
  * @copyright © 2012, Santhosh Thottingal
  * @copyright © 2012, Timo Tijhof
- * @coversNothing
  */
 class ResourcesTest extends MediaWikiTestCase {
 
index 026b903..3fa31fe 100644 (file)
@@ -11,7 +11,6 @@ use MediaWiki\MediaWikiServices;
  *
  * @since 1.32
  * @author Addshore
- * @coversNothing
  */
 class SpecialPageFatalTest extends MediaWikiTestCase {
        public function provideSpecialPages() {
index 97bed4c..412ee99 100644 (file)
@@ -9,7 +9,6 @@ class StructureTest extends MediaWikiTestCase {
         * Verify all files that appear to be tests have file names ending in
         * Test.  If the file names do not end in Test, they will not be run.
         * @group medium
-        * @coversNothing
         */
        public function testUnitTestFileNamesEndWithTest() {
                // realpath() also normalizes directory separator on windows for prefix compares
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 a85e41a..30d993e 100644 (file)
@@ -84,7 +84,7 @@ class GenerateJqueryMsgData extends Maintenance {
 
        public function __construct() {
                parent::__construct();
-               $this->mDescription = 'Create a specification for message parsing ini JSON format';
+               $this->addDescription( 'Create a specification for message parsing ini JSON format' );
                // add any other options here
        }
 
index fca1f7d..a3f3cc8 100644 (file)
                                        title: 'File:Foo.JPEG  ',
                                        expected: 'File:Foo.JPEG',
                                        description: 'Page in File-namespace with trailing whitespace'
+                               },
+                               {
+                                       title: 'File:Foo',
+                                       description: 'File name without file extension'
+                               },
+                               {
+                                       title: 'File:Foo.',
+                                       description: 'File name with empty file extension'
                                }
                        ];
 
index 2a79467..9beabfc 100644 (file)
@@ -41,7 +41,7 @@ describe( 'Rollback with confirmation', function () {
                assert.strictEqual( HistoryPage.rollbackConfirmableNo.getText(), 'Cancel' );
        } );
 
-       it( 'should offer a way to cancel rollbacks', function () {
+       it.skip( 'should offer a way to cancel rollbacks', function () {
                HistoryPage.rollback.click();
 
                HistoryPage.rollbackConfirmableNo.waitForVisible( 5000 );
@@ -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 );
 
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;