Merge "API to fetch data about stashed images"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 15 Jan 2016 17:02:02 +0000 (17:02 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 15 Jan 2016 17:02:02 +0000 (17:02 +0000)
307 files changed:
RELEASE-NOTES-1.27
autoload.php
composer.json
docs/hooks.txt
includes/AjaxDispatcher.php
includes/Block.php
includes/DefaultSettings.php
includes/DerivativeRequest.php
includes/FauxRequest.php
includes/GlobalFunctions.php
includes/MediaWiki.php
includes/OutputPage.php
includes/PHPVersionCheck.php
includes/PageProps.php [new file with mode: 0644]
includes/Setup.php
includes/Title.php
includes/WebRequest.php
includes/WebResponse.php
includes/ZhConversion.php
includes/actions/Action.php
includes/actions/DeleteAction.php
includes/actions/EditAction.php
includes/actions/FormAction.php
includes/actions/InfoAction.php
includes/actions/MarkpatrolledAction.php
includes/actions/ProtectAction.php
includes/actions/PurgeAction.php
includes/actions/RawAction.php
includes/actions/RevertAction.php
includes/actions/RevisiondeleteAction.php
includes/actions/RollbackAction.php
includes/actions/SubmitAction.php
includes/actions/UnprotectAction.php
includes/actions/UnwatchAction.php
includes/actions/WatchAction.php
includes/api/ApiCreateAccount.php
includes/api/ApiLogin.php
includes/api/ApiLogout.php
includes/api/ApiMain.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryPageProps.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryWatchlist.php
includes/api/i18n/bg.json [new file with mode: 0644]
includes/api/i18n/gl.json
includes/api/i18n/nl.json
includes/api/i18n/ru.json
includes/context/RequestContext.php
includes/db/Database.php
includes/db/DatabaseMysqlBase.php
includes/db/loadbalancer/LBFactory.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php
includes/db/loadbalancer/LBFactorySingle.php
includes/db/loadbalancer/LoadBalancer.php
includes/db/loadbalancer/LoadMonitorMySQL.php
includes/deferred/AtomicSectionUpdate.php [new file with mode: 0644]
includes/filerepo/FileRepo.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignAPIFile.php
includes/filerepo/file/ForeignDBFile.php
includes/filerepo/file/LocalFile.php
includes/installer/Installer.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/installer/i18n/bg.json
includes/installer/i18n/fa.json
includes/installer/i18n/gl.json
includes/installer/i18n/it.json
includes/installer/i18n/nl.json
includes/jobqueue/JobQueueMemory.php [new file with mode: 0644]
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/UploadFromUrlJob.php
includes/objectcache/ObjectCacheSessionHandler.php [deleted file]
includes/page/Article.php
includes/page/WikiPage.php
includes/parser/CoreParserFunctions.php
includes/session/BotPasswordSessionProvider.php [new file with mode: 0644]
includes/session/CookieSessionProvider.php [new file with mode: 0644]
includes/session/ImmutableSessionProviderWithCookie.php [new file with mode: 0644]
includes/session/PHPSessionHandler.php [new file with mode: 0644]
includes/session/Session.php [new file with mode: 0644]
includes/session/SessionBackend.php [new file with mode: 0644]
includes/session/SessionId.php [new file with mode: 0644]
includes/session/SessionInfo.php [new file with mode: 0644]
includes/session/SessionManager.php [new file with mode: 0644]
includes/session/SessionManagerInterface.php [new file with mode: 0644]
includes/session/SessionProvider.php [new file with mode: 0644]
includes/session/SessionProviderInterface.php [new file with mode: 0644]
includes/session/UserInfo.php [new file with mode: 0644]
includes/specialpage/SpecialPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialBlock.php
includes/specials/SpecialBotPasswords.php [new file with mode: 0644]
includes/specials/SpecialChangeContentModel.php
includes/specials/SpecialChangeEmail.php
includes/specials/SpecialChangePassword.php
includes/specials/SpecialConfirmemail.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialEditTags.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialEmailInvalidate.php
includes/specials/SpecialEmailuser.php
includes/specials/SpecialImport.php
includes/specials/SpecialListgrants.php [new file with mode: 0644]
includes/specials/SpecialLockdb.php
includes/specials/SpecialMergeHistory.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialPageLanguage.php
includes/specials/SpecialPasswordReset.php
includes/specials/SpecialPreferences.php
includes/specials/SpecialResetTokens.php
includes/specials/SpecialRevisiondelete.php
includes/specials/SpecialRunJobs.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUnlockdb.php
includes/specials/SpecialUpload.php
includes/specials/SpecialUploadStash.php
includes/specials/SpecialUserlogin.php
includes/specials/SpecialUserlogout.php
includes/specials/SpecialUserrights.php
includes/upload/UploadFromUrl.php
includes/user/BotPassword.php [new file with mode: 0644]
includes/user/CentralIdLookup.php
includes/user/User.php
includes/utils/MWGrants.php [new file with mode: 0644]
includes/utils/MWRestrictions.php [new file with mode: 0644]
languages/i18n/ar.json
languages/i18n/ast.json
languages/i18n/az.json
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/bn.json
languages/i18n/br.json
languages/i18n/bs.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/de.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/gu.json
languages/i18n/he.json
languages/i18n/hi.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/id.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/ka.json
languages/i18n/ko.json
languages/i18n/krc.json
languages/i18n/ksh.json
languages/i18n/la.json
languages/i18n/lb.json
languages/i18n/lki.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mr.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/pl.json
languages/i18n/ps.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sa.json
languages/i18n/sl.json
languages/i18n/so.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/yi.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesEn.php
maintenance/archives/patch-bot_passwords.sql [new file with mode: 0644]
maintenance/language/zhtable/simp2trad.manual
maintenance/language/zhtable/toCN.manual
maintenance/language/zhtable/toHK.manual
maintenance/language/zhtable/toTW.manual
maintenance/language/zhtable/toTrad.manual
maintenance/language/zhtable/tradphrases.manual
maintenance/language/zhtable/tradphrases_exclude.manual
maintenance/postgres/archives/patch-bot_passwords.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/tables.sql
resources/lib/oojs-ui/i18n/be-tarask.json
resources/lib/oojs-ui/i18n/ka.json
resources/lib/oojs-ui/i18n/kk-cyrl.json
resources/lib/oojs-ui/i18n/lki.json
resources/lib/oojs-ui/i18n/vep.json [new file with mode: 0644]
resources/lib/oojs-ui/i18n/war.json [new file with mode: 0644]
resources/lib/oojs-ui/oojs-ui-apex-noimages.css
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-mediawiki-noimages.css
resources/lib/oojs-ui/oojs-ui-mediawiki.js
resources/lib/oojs-ui/oojs-ui.js
resources/lib/oojs-ui/themes/apex/icons-content.json [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/icons-editing-advanced.json
resources/lib/oojs-ui/themes/apex/icons-editing-styling.json
resources/lib/oojs-ui/themes/apex/icons.json
resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/language.png [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/language.svg [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.png [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.svg [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.png [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.svg [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.png [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.svg [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.png [deleted file]
resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/icons-content.json
resources/lib/oojs-ui/themes/mediawiki/icons-editing-advanced.json
resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json
resources/lib/oojs-ui/themes/mediawiki/icons.json
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/language.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.svg [deleted file]
tests/TestsAutoLoader.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/PagePropsTest.php [new file with mode: 0644]
tests/phpunit/includes/TestLogger.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/api/ApiTestCaseUpload.php
tests/phpunit/includes/context/RequestContextTest.php
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php [new file with mode: 0644]
tests/phpunit/includes/page/WikiPageTest.php
tests/phpunit/includes/session/BotPasswordSessionProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/session/CookieSessionProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php [new file with mode: 0644]
tests/phpunit/includes/session/PHPSessionHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionBackendTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionIdTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionInfoTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionManagerTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/session/SessionTest.php [new file with mode: 0644]
tests/phpunit/includes/session/TestBagOStuff.php [new file with mode: 0644]
tests/phpunit/includes/session/TestUtils.php [new file with mode: 0644]
tests/phpunit/includes/session/UserInfoTest.php [new file with mode: 0644]
tests/phpunit/includes/upload/UploadFromUrlTest.php
tests/phpunit/includes/user/BotPasswordTest.php [new file with mode: 0644]
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/utils/MWGrantsTest.php [new file with mode: 0644]
tests/phpunit/includes/utils/MWRestrictionsTest.php [new file with mode: 0644]
tests/phpunit/mocks/session/DummySessionBackend.php [new file with mode: 0644]
tests/phpunit/mocks/session/DummySessionProvider.php [new file with mode: 0644]
tests/phpunit/phpunit.php

index 32f2939..d1952c8 100644 (file)
@@ -62,6 +62,30 @@ production.
   $wgSharedDB and $wgSharedTables are properly set even on the "central" wiki
   that all others are sharing from and that $wgLocalDatabases is set to the
   full list of sharing wikis on all those wikis.
+* Massive overhaul to session handling:
+** $wgSessionsInObjectCache is no longer supported and must be true, due to
+   MediaWiki\Session\SessionManager. $wgSessionHandler is similarly no longer
+   used.
+** ObjectCacheSessionHandler is removed, replaced with
+   MediaWiki\Session\PhpSessionHandler.
+** PHP session handling in general ($_SESSION, session_id(), and so on) is
+   deprecated. Use MediaWiki\Session\SessionManager instead. A new config
+   variable, $wgPHPSessionHandling, is available to cause use of $_SESSION to
+   issue a deprecation warning or to cause most PHP session handling to throw
+   exceptions.
+** Deprecated UserSetCookies hook. Session-handling extensions should generally
+   be creating a custom subclass of CookieSessionProvider. Other extensions
+   messing with cookies can no longer count on user data being saved in cookies
+   versus other methods.
+** Deprecated UserLoadFromSession hook, extensions should create a
+   MediaWiki\Session\SessionProvider.
+** The User cannot be loaded from session until after Setup.php completes.
+   Attempts to do so will be ignored and the User will remain unloaded.
+* MediaWiki will now auto-create users as necessary, removing the need for
+  extensions to do so. An 'autocreateaccount' right is added to allow
+  auto-creation when 'createaccount' is not granted to all users.
+* Deprecated AuthPluginAutoCreate hook in favor of LocalUserCreated.
+* Most cookie-handling methods in User are deprecated.
 
 === New features in 1.27 ===
 * $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
@@ -106,6 +130,20 @@ production.
 * It is now possible to patrol file uploads (both for new files and new versions
   of existing files). Special:NewFiles has gained an option to filter by patrol
   status. This functionality can be disabled using $wgUseFilePatrol.
+* MediaWiki\Session infrastructure allows for easier use of session mechanisms
+  other than the usual cookies.
+** SessionMetadata and SessionCheckInfo hooks allow for setting and checking
+   custom session metadata.
+* Added MWGrants and associated configuration settings $wgGrantPermissions and
+  $wgGrantPermissionGroups to hold configuration for authentication features
+  such as OAuth that want to allow restricting the user rights a user may make
+  use of.
+** If you're already using the OAuth extension, these new variables are
+   identical to (and will replace) $wgMWOAuthGrantPermissions and
+   $wgMWOAuthGrantPermissionGroups.
+* Added MWRestrictions as a class to check restrictions on a WebRequest, e.g.
+  to assert that the request comes from a particular IP range.
+* Added bot passwords, a rights-restricted login mechanism for API-using bots.
 
 === External library changes in 1.27 ===
 
@@ -119,6 +157,7 @@ production.
 * Added wikimedia/cldr-plural-rule-parser v1.0.0.
 * Added wikimedia/relpath v1.0.3.
 * Added wikimedia/running-stat v1.1.0.
+* Added wikimedia/php-session-serializer v1.0.3.
 
 ==== Removed and replaced external libraries ====
 
@@ -141,6 +180,9 @@ production.
 * The following response properties from action=login are deprecated, and may
   be removed in the future: lgtoken, cookieprefix, sessionid. Clients should
   handle cookies to properly manage session state.
+* action=login transparently allows login using bot passwords. Clients should
+  merely need to change the username and password used after setting up a bot
+  password.
 
 === Action API internal changes in 1.27 ===
 * ApiQueryORM removed.
@@ -239,11 +281,14 @@ changes to languages because of Phabricator reports.
 * LanguageConverter::armourMath() was removed (deprecated since 1.22).
 * FakeConverter::armourMath() was removed (deprecated since 1.22).
 * The unused jquery.validate ResourceLoader module was removed.
+* FileRepo::getRootUrl() was removed (deprecated since 1.20).
+* User::generateToken() was removed (deprecated since 1.20).
+* WikiPage::getRawText() was removed (deprecated since 1.21).
 
 == Compatibility ==
 
 MediaWiki 1.27 requires PHP 5.3.3 or later. There is experimental support for
-HHVM 3.3.0.
+HHVM 3.6.5 or later.
 
 MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but
 support for them is somewhat less mature. There is experimental support for
index 02af18c..1a7c724 100644 (file)
@@ -141,6 +141,7 @@ $wgAutoloadLocalClasses = array(
        'Article' => __DIR__ . '/includes/page/Article.php',
        'AssembleUploadChunksJob' => __DIR__ . '/includes/jobqueue/jobs/AssembleUploadChunksJob.php',
        'AtomFeed' => __DIR__ . '/includes/Feed.php',
+       'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
        'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
        'AuthPlugin' => __DIR__ . '/includes/AuthPlugin.php',
        'AuthPluginUser' => __DIR__ . '/includes/AuthPlugin.php',
@@ -180,6 +181,7 @@ $wgAutoloadLocalClasses = array(
        'BlockListPager' => __DIR__ . '/includes/specials/SpecialBlockList.php',
        'BlockLogFormatter' => __DIR__ . '/includes/logging/BlockLogFormatter.php',
        'BmpHandler' => __DIR__ . '/includes/media/BMP.php',
+       'BotPassword' => __DIR__ . '/includes/user/BotPassword.php',
        'BrokenRedirectsPage' => __DIR__ . '/includes/specials/SpecialBrokenRedirects.php',
        'BufferingStatsdDataFactory' => __DIR__ . '/includes/libs/BufferingStatsdDataFactory.php',
        'CLIParser' => __DIR__ . '/maintenance/parse.php',
@@ -611,6 +613,7 @@ $wgAutoloadLocalClasses = array(
        'JobQueueError' => __DIR__ . '/includes/jobqueue/JobQueue.php',
        'JobQueueFederated' => __DIR__ . '/includes/jobqueue/JobQueueFederated.php',
        'JobQueueGroup' => __DIR__ . '/includes/jobqueue/JobQueueGroup.php',
+       'JobQueueMemory' => __DIR__ . '/includes/jobqueue/JobQueueMemory.php',
        'JobQueueRedis' => __DIR__ . '/includes/jobqueue/JobQueueRedis.php',
        'JobRunner' => __DIR__ . '/includes/jobqueue/JobRunner.php',
        'JobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
@@ -731,11 +734,13 @@ $wgAutoloadLocalClasses = array(
        'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php',
        'MWException' => __DIR__ . '/includes/exception/MWException.php',
        'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php',
+       'MWGrants' => __DIR__ . '/includes/utils/MWGrants.php',
        'MWHttpRequest' => __DIR__ . '/includes/HttpFunctions.php',
        'MWMemcached' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
        'MWMessagePack' => __DIR__ . '/includes/libs/MWMessagePack.php',
        'MWNamespace' => __DIR__ . '/includes/MWNamespace.php',
        'MWOldPassword' => __DIR__ . '/includes/password/MWOldPassword.php',
+       'MWRestrictions' => __DIR__ . '/includes/utils/MWRestrictions.php',
        'MWSaltedPassword' => __DIR__ . '/includes/password/MWSaltedPassword.php',
        'MWTidy' => __DIR__ . '/includes/parser/MWTidy.php',
        'MWTimestamp' => __DIR__ . '/includes/MWTimestamp.php',
@@ -779,6 +784,19 @@ $wgAutoloadLocalClasses = array(
        'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
        'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
        'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
+       'MediaWiki\\Session\\BotPasswordSessionProvider' => __DIR__ . '/includes/session/BotPasswordSessionProvider.php',
+       'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
+       'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
+       'MediaWiki\\Session\\PHPSessionHandler' => __DIR__ . '/includes/session/PHPSessionHandler.php',
+       'MediaWiki\\Session\\Session' => __DIR__ . '/includes/session/Session.php',
+       'MediaWiki\\Session\\SessionBackend' => __DIR__ . '/includes/session/SessionBackend.php',
+       'MediaWiki\\Session\\SessionId' => __DIR__ . '/includes/session/SessionId.php',
+       'MediaWiki\\Session\\SessionInfo' => __DIR__ . '/includes/session/SessionInfo.php',
+       'MediaWiki\\Session\\SessionManager' => __DIR__ . '/includes/session/SessionManager.php',
+       'MediaWiki\\Session\\SessionManagerInterface' => __DIR__ . '/includes/session/SessionManagerInterface.php',
+       'MediaWiki\\Session\\SessionProvider' => __DIR__ . '/includes/session/SessionProvider.php',
+       'MediaWiki\\Session\\SessionProviderInterface' => __DIR__ . '/includes/session/SessionProviderInterface.php',
+       'MediaWiki\\Session\\UserInfo' => __DIR__ . '/includes/session/UserInfo.php',
        'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
        'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php',
        'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php',
@@ -863,7 +881,6 @@ $wgAutoloadLocalClasses = array(
        'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
        'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
        'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php',
-       'ObjectCacheSessionHandler' => __DIR__ . '/includes/objectcache/ObjectCacheSessionHandler.php',
        'ObjectFactory' => __DIR__ . '/includes/libs/ObjectFactory.php',
        'ObjectFileCache' => __DIR__ . '/includes/cache/ObjectFileCache.php',
        'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php',
@@ -906,6 +923,7 @@ $wgAutoloadLocalClasses = array(
        'PageExists' => __DIR__ . '/maintenance/pageExists.php',
        'PageLangLogFormatter' => __DIR__ . '/includes/logging/PageLangLogFormatter.php',
        'PageLinkRenderer' => __DIR__ . '/includes/title/PageLinkRenderer.php',
+       'PageProps' => __DIR__ . '/includes/PageProps.php',
        'PageQueryPage' => __DIR__ . '/includes/specialpage/PageQueryPage.php',
        'Pager' => __DIR__ . '/includes/pager/Pager.php',
        'ParameterizedPassword' => __DIR__ . '/includes/password/ParameterizedPassword.php',
@@ -1147,6 +1165,7 @@ $wgAutoloadLocalClasses = array(
        'SpecialBlock' => __DIR__ . '/includes/specials/SpecialBlock.php',
        'SpecialBlockList' => __DIR__ . '/includes/specials/SpecialBlockList.php',
        'SpecialBookSources' => __DIR__ . '/includes/specials/SpecialBooksources.php',
+       'SpecialBotPasswords' => __DIR__ . '/includes/specials/SpecialBotPasswords.php',
        'SpecialCachedPage' => __DIR__ . '/includes/specials/SpecialCachedPage.php',
        'SpecialCategories' => __DIR__ . '/includes/specials/SpecialCategories.php',
        'SpecialChangeContentModel' => __DIR__ . '/includes/specials/SpecialChangeContentModel.php',
@@ -1167,6 +1186,7 @@ $wgAutoloadLocalClasses = array(
        'SpecialListAdmins' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'SpecialListBots' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'SpecialListFiles' => __DIR__ . '/includes/specials/SpecialListfiles.php',
+       'SpecialListGrants' => __DIR__ . '/includes/specials/SpecialListgrants.php',
        'SpecialListGroupRights' => __DIR__ . '/includes/specials/SpecialListgrouprights.php',
        'SpecialListUsers' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'SpecialLockdb' => __DIR__ . '/includes/specials/SpecialLockdb.php',
index 89ad8a6..94de20e 100644 (file)
@@ -21,7 +21,7 @@
                "ext-iconv": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.14.1",
+               "oojs/oojs-ui": "0.15.0",
                "oyejorge/less.php": "1.7.0.9",
                "php": ">=5.3.3",
                "psr/log": "1.0.0",
@@ -31,6 +31,7 @@
                "wikimedia/cldr-plural-rule-parser": "1.0.0",
                "wikimedia/composer-merge-plugin": "1.3.0",
                "wikimedia/ip-set": "1.0.1",
+               "wikimedia/php-session-serializer": "1.0.3",
                "wikimedia/relpath": "1.0.3",
                "wikimedia/running-stat": "1.1.0",
                "wikimedia/utfnormal": "1.0.3",
index 135a113..5e50cec 100644 (file)
@@ -741,8 +741,9 @@ viewing.
 redirect was followed.
 &$article: target article (object)
 
-'AuthPluginAutoCreate': Called when creating a local account for an user logged
-in from an external authentication method.
+'AuthPluginAutoCreate': DEPRECATED! Use the 'LocalUserCreated' hook instead.
+Called when creating a local account for an user logged in from an external
+authentication method.
 $user: User object created locally
 
 'AuthPluginSetup': Update or replace authentication plugin object ($wgAuth).
@@ -2574,6 +2575,20 @@ $targetUser: the user whom to send watchlist email notification
 $title: the page title
 $enotif: EmailNotification object
 
+'SessionCheckInfo': Validate a MediaWiki\Session\SessionInfo as it's being
+loaded from storage. Return false to prevent it from being used.
+&$reason: String rejection reason to be logged
+$info: MediaWiki\Session\SessionInfo being validated
+$request: WebRequest being loaded from
+$metadata: Array|false Metadata array for the MediaWiki\Session\Session
+$data: Array|false Data array for the MediaWiki\Session\Session
+
+'SessionMetadata': Add metadata to a session being saved.
+$backend: MediaWiki\Session\SessionBackend being saved.
+&$metadata: Array Metadata to be stored. Add new keys here.
+$requests: Array of WebRequests potentially being saved to. Generally 0-1 real
+  request and 0+ FauxRequests.
+
 'SetupAfterCache': Called in Setup.php, after cache objects are set
 
 'ShortPagesQuery': Allow extensions to modify the query used by
@@ -3292,8 +3307,9 @@ $name: user name
 $user: user object
 &$s: database query object
 
-'UserLoadFromSession': Called to authenticate users on external/environmental
-means; occurs before session is loaded.
+'UserLoadFromSession': DEPRECATED! Create a MediaWiki\Session\SessionProvider instead.
+Called to authenticate users on external/environmental means; occurs before
+session is loaded.
 $user: user object being loaded
 &$result: set this to a boolean value to abort the normal authentication
   process
@@ -3384,9 +3400,13 @@ $user: User object
 'UserSaveSettings': Called when saving user settings.
 $user: User object
 
-'UserSetCookies': Called when setting user cookies.
+'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
+handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
+instead. Otherwise, you can no longer count on user data being saved to cookies
+versus some other mechanism.
+Called when setting user cookies.
 $user: User object
-&$session: session array, will be added to $_SESSION
+&$session: session array, will be added to the session
 &$cookies: cookies array mapping cookie name to its value
 
 'UserSetEmail': Called when changing user email address.
index 96892d7..c2ea582 100644 (file)
@@ -135,6 +135,9 @@ class AjaxDispatcher {
                                                $result = new AjaxResponse( $result );
                                        }
 
+                                       // Make sure DB commit succeeds before sending a response
+                                       wfGetLBFactory()->commitMasterChanges( __METHOD__ );
+
                                        $result->sendHeaders();
                                        $result->printText();
 
index 7b287dc..c017c6c 100644 (file)
@@ -1021,12 +1021,17 @@ class Block {
                        return;
                }
 
-               $method = __METHOD__;
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
-                       $dbw->delete( 'ipblocks',
-                               array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), $method );
-               } );
+               DeferredUpdates::addUpdate( new AtomicSectionUpdate(
+                       wfGetDB( DB_MASTER ),
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) {
+                               $dbw->delete(
+                                       'ipblocks',
+                                       array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+                                       $fname
+                               );
+                       }
+               ) );
        }
 
        /**
index d46bc03..c9b0e36 100644 (file)
@@ -2181,7 +2181,7 @@ $wgMessageCacheType = CACHE_ANYTHING;
 $wgParserCacheType = CACHE_ANYTHING;
 
 /**
- * The cache type for storing session data. Used if $wgSessionsInObjectCache is true.
+ * The cache type for storing session data.
  *
  * For available types see $wgMainCacheType.
  */
@@ -2316,30 +2316,29 @@ $wgParserCacheExpireTime = 86400;
  *
  * @deprecated since 1.20; Use $wgSessionsInObjectCache
  */
-$wgSessionsInMemcached = false;
+$wgSessionsInMemcached = true;
 
 /**
- * Store sessions in an object cache, configured by $wgSessionCacheType. This
- * can be useful to improve performance, or to avoid the locking behavior of
- * PHP's default session handler, which tends to prevent multiple requests for
- * the same user from acting concurrently.
+ * @deprecated since 1.27, session data is always stored in object cache.
  */
-$wgSessionsInObjectCache = false;
+$wgSessionsInObjectCache = true;
 
 /**
- * The expiry time to use for session storage when $wgSessionsInObjectCache is
- * enabled, in seconds.
+ * The expiry time to use for session storage, in seconds.
  */
 $wgObjectCacheSessionExpiry = 3600;
 
 /**
- * This is used for setting php's session.save_handler. In practice, you will
- * almost never need to change this ever. Other options might be 'user' or
- * 'session_mysql.' Setting to null skips setting this entirely (which might be
- * useful if you're doing cross-application sessions, see bug 11381)
+ * @deprecated since 1.27, MediaWiki\\Session\\SessionManager doesn't use PHP session storage.
  */
 $wgSessionHandler = null;
 
+/**
+ * Whether to use PHP session handling ($_SESSION and session_*() functions)
+ * @var string 'enable', 'warn', or 'disable'
+ */
+$wgPHPSessionHandling = 'enable';
+
 /**
  * If enabled, will send MemCached debugging information to $wgDebugLogFile
  */
@@ -4660,6 +4659,30 @@ $wgUserrightsInterwikiDelimiter = '@';
  */
 $wgSecureLogin = false;
 
+/**
+ * MediaWiki\Session\SessionProvider configuration.
+ *
+ * Value is an array of ObjectFactory specifications for the SessionProviders
+ * to be used. Keys in the array are ignored. Order is not significant.
+ *
+ * @since 1.27
+ */
+$wgSessionProviders = array(
+       'MediaWiki\\Session\\CookieSessionProvider' => array(
+               'class' => 'MediaWiki\\Session\\CookieSessionProvider',
+               'args' => array( array(
+                       'priority' => 30,
+                       'callUserSetCookiesHook' => true,
+               ) ),
+       ),
+       'MediaWiki\\Session\\BotPasswordSessionProvider' => array(
+               'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider',
+               'args' => array( array(
+                       'priority' => 40,
+               ) ),
+       ),
+);
+
 /** @} */ # end user accounts }
 
 /************************************************************************//**
@@ -5362,6 +5385,161 @@ $wgQueryPageDefaultLimit = 50;
  */
 $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
 
+/**
+ * @var Array Map of (grant => right => boolean)
+ * Users authorize consumers (like Apps) to act on their behalf but only with
+ * a subset of the user's normal account rights (signed off on by the user).
+ * The possible rights to grant to a consumer are bundled into groups called
+ * "grants". Each grant defines some rights it lets consumers inherit from the
+ * account they may act on behalf of. Note that a user granting a right does
+ * nothing if that user does not actually have that right to begin with.
+ * @since 1.27
+ */
+$wgGrantPermissions = array();
+
+// @TODO: clean up grants
+// @TODO: auto-include read/editsemiprotected rights?
+
+$wgGrantPermissions['basic']['autoconfirmed'] = true;
+$wgGrantPermissions['basic']['autopatrol'] = true;
+$wgGrantPermissions['basic']['autoreview'] = true;
+$wgGrantPermissions['basic']['editsemiprotected'] = true;
+$wgGrantPermissions['basic']['ipblock-exempt'] = true;
+$wgGrantPermissions['basic']['nominornewtalk'] = true;
+$wgGrantPermissions['basic']['patrolmarks'] = true;
+$wgGrantPermissions['basic']['purge'] = true;
+$wgGrantPermissions['basic']['read'] = true;
+$wgGrantPermissions['basic']['skipcaptcha'] = true;
+$wgGrantPermissions['basic']['torunblocked'] = true;
+$wgGrantPermissions['basic']['writeapi'] = true;
+
+$wgGrantPermissions['highvolume']['bot'] = true;
+$wgGrantPermissions['highvolume']['apihighlimits'] = true;
+$wgGrantPermissions['highvolume']['noratelimit'] = true;
+$wgGrantPermissions['highvolume']['markbotedits'] = true;
+
+$wgGrantPermissions['editpage']['edit'] = true;
+$wgGrantPermissions['editpage']['minoredit'] = true;
+
+$wgGrantPermissions['editprotected'] = $wgGrantPermissions['editpage'];
+$wgGrantPermissions['editprotected']['editprotected'] = true;
+
+$wgGrantPermissions['editmycssjs'] = $wgGrantPermissions['editpage'];
+$wgGrantPermissions['editmycssjs']['editmyusercss'] = true;
+$wgGrantPermissions['editmycssjs']['editmyuserjs'] = true;
+
+$wgGrantPermissions['editmyoptions']['editmyoptions'] = true;
+
+$wgGrantPermissions['editinterface'] = $wgGrantPermissions['editpage'];
+$wgGrantPermissions['editinterface']['editinterface'] = true;
+$wgGrantPermissions['editinterface']['editusercss'] = true;
+$wgGrantPermissions['editinterface']['edituserjs'] = true;
+
+$wgGrantPermissions['createeditmovepage'] = $wgGrantPermissions['editpage'];
+$wgGrantPermissions['createeditmovepage']['createpage'] = true;
+$wgGrantPermissions['createeditmovepage']['createtalk'] = true;
+$wgGrantPermissions['createeditmovepage']['move'] = true;
+$wgGrantPermissions['createeditmovepage']['move-rootuserpages'] = true;
+$wgGrantPermissions['createeditmovepage']['move-subpages'] = true;
+
+$wgGrantPermissions['uploadfile']['upload'] = true;
+$wgGrantPermissions['uploadfile']['reupload-own'] = true;
+
+$wgGrantPermissions['uploadeditmovefile'] = $wgGrantPermissions['uploadfile'];
+$wgGrantPermissions['uploadeditmovefile']['reupload'] = true;
+$wgGrantPermissions['uploadeditmovefile']['reupload-shared'] = true;
+$wgGrantPermissions['uploadeditmovefile']['upload_by_url'] = true;
+$wgGrantPermissions['uploadeditmovefile']['movefile'] = true;
+$wgGrantPermissions['uploadeditmovefile']['suppressredirect'] = true;
+
+$wgGrantPermissions['patrol']['patrol'] = true;
+
+$wgGrantPermissions['rollback']['rollback'] = true;
+
+$wgGrantPermissions['blockusers']['block'] = true;
+$wgGrantPermissions['blockusers']['blockemail'] = true;
+
+$wgGrantPermissions['viewdeleted']['browsearchive'] = true;
+$wgGrantPermissions['viewdeleted']['deletedhistory'] = true;
+$wgGrantPermissions['viewdeleted']['deletedtext'] = true;
+
+$wgGrantPermissions['delete'] = $wgGrantPermissions['editpage'] +
+       $wgGrantPermissions['viewdeleted'];
+$wgGrantPermissions['delete']['delete'] = true;
+$wgGrantPermissions['delete']['bigdelete'] = true;
+$wgGrantPermissions['delete']['deletelogentry'] = true;
+$wgGrantPermissions['delete']['deleterevision'] = true;
+$wgGrantPermissions['delete']['undelete'] = true;
+
+$wgGrantPermissions['protect'] = $wgGrantPermissions['editprotected'];
+$wgGrantPermissions['protect']['protect'] = true;
+
+$wgGrantPermissions['viewmywatchlist']['viewmywatchlist'] = true;
+
+$wgGrantPermissions['editmywatchlist']['editmywatchlist'] = true;
+
+$wgGrantPermissions['sendemail']['sendemail'] = true;
+
+$wgGrantPermissions['createaccount']['createaccount'] = true;
+
+/**
+ * @var Array Map of grants to their UI grouping
+ * @since 1.27
+ */
+$wgGrantPermissionGroups = array(
+       // Hidden grants are implicitly present
+       'basic'            => 'hidden',
+
+       'editpage'            => 'page-interaction',
+       'createeditmovepage'  => 'page-interaction',
+       'editprotected'       => 'page-interaction',
+       'patrol'              => 'page-interaction',
+
+       'uploadfile'          => 'file-interaction',
+       'uploadeditmovefile'  => 'file-interaction',
+
+       'sendemail'           => 'email',
+
+       'viewmywatchlist'     => 'watchlist-interaction',
+       'editviewmywatchlist' => 'watchlist-interaction',
+
+       'editmycssjs'         => 'customization',
+       'editmyoptions'       => 'customization',
+
+       'editinterface'       => 'administration',
+       'rollback'            => 'administration',
+       'blockusers'          => 'administration',
+       'delete'              => 'administration',
+       'viewdeleted'         => 'administration',
+       'protect'             => 'administration',
+       'createaccount'       => 'administration',
+
+       'highvolume'          => 'high-volume',
+);
+
+/**
+ * @var bool Whether to enable bot passwords
+ * @since 1.27
+ */
+$wgEnableBotPasswords = true;
+
+/**
+ * Cluster for the bot_passwords table
+ * @var string|bool If false, the normal cluster will be used
+ * @since 1.27
+ */
+$wgBotPasswordsCluster = false;
+
+/**
+ * Database name for the bot_passwords table
+ *
+ * To use a database with a table prefix, set this variable to
+ * "{$database}-{$prefix}".
+ * @var string|bool If false, the normal database will be used
+ * @since 1.27
+ */
+$wgBotPasswordsDatabase = false;
+
 /** @} */ # end of user rights settings
 
 /************************************************************************//**
index dda1358..4c149ae 100644 (file)
@@ -61,6 +61,10 @@ class DerivativeRequest extends FauxRequest {
                return $this->base->getAllHeaders();
        }
 
+       public function getSession() {
+               return $this->base->getSession();
+       }
+
        public function getSessionData( $key ) {
                return $this->base->getSessionData( $key );
        }
index 888f853..f049d2e 100644 (file)
@@ -23,6 +23,8 @@
  * @file
  */
 
+use MediaWiki\Session\SessionManager;
+
 /**
  * WebRequest clone which takes values from a provided array.
  *
@@ -30,7 +32,6 @@
  */
 class FauxRequest extends WebRequest {
        private $wasPosted = false;
-       private $session = array();
        private $requestUrl;
        protected $cookies = array();
 
@@ -38,7 +39,8 @@ class FauxRequest extends WebRequest {
         * @param array $data Array of *non*-urlencoded key => value pairs, the
         *   fake GET/POST values
         * @param bool $wasPosted Whether to treat the data as POST
-        * @param array|null $session Session array or null
+        * @param MediaWiki\\Session\\Session|array|null $session Session, session
+        *  data array, or null
         * @param string $protocol 'http' or 'https'
         * @throws MWException
         */
@@ -53,8 +55,16 @@ class FauxRequest extends WebRequest {
                        throw new MWException( "FauxRequest() got bogus data" );
                }
                $this->wasPosted = $wasPosted;
-               if ( $session ) {
-                       $this->session = $session;
+               if ( $session instanceof MediaWiki\Session\Session ) {
+                       $this->sessionId = $session->getSessionId();
+               } elseif ( is_array( $session ) ) {
+                       $mwsession = SessionManager::singleton()->getEmptySession( $this );
+                       $this->sessionId = $mwsession->getSessionId();
+                       foreach ( $session as $key => $value ) {
+                               $mwsession->set( $key, $value );
+                       }
+               } elseif ( $session !== null ) {
+                       throw new MWException( "FauxRequest() got bogus session" );
                }
                $this->protocol = $protocol;
        }
@@ -140,10 +150,6 @@ class FauxRequest extends WebRequest {
                }
        }
 
-       public function checkSessionCookie() {
-               return false;
-       }
-
        /**
         * @since 1.25
         */
@@ -186,31 +192,15 @@ class FauxRequest extends WebRequest {
        }
 
        /**
-        * @param string $key
         * @return array|null
         */
-       public function getSessionData( $key ) {
-               if ( isset( $this->session[$key] ) ) {
-                       return $this->session[$key];
+       public function getSessionArray() {
+               if ( $this->sessionId !== null ) {
+                       return iterator_to_array( $this->getSession() );
                }
                return null;
        }
 
-       /**
-        * @param string $key
-        * @param array $data
-        */
-       public function setSessionData( $key, $data ) {
-               $this->session[$key] = $data;
-       }
-
-       /**
-        * @return array|mixed|null
-        */
-       public function getSessionArray() {
-               return $this->session;
-       }
-
        /**
         * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
         * @return string
index 26fb223..eda636a 100644 (file)
@@ -26,6 +26,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 
 use Liuggio\StatsdClient\Sender\SocketSender;
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
 
 // Hide compatibility functions from Doxygen
 /// @cond
@@ -3007,9 +3008,12 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1,
 /**
  * Check if there is sufficient entropy in php's built-in session generation
  *
+ * @deprecated since 1.27, PHP's session generation isn't used with
+ *  MediaWiki\\Session\\SessionManager
  * @return bool True = there is sufficient entropy
  */
 function wfCheckEntropy() {
+       wfDeprecated( __FUNCTION__, '1.27' );
        return (
                        ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
                        || ini_get( 'session.entropy_file' )
@@ -3018,83 +3022,65 @@ function wfCheckEntropy() {
 }
 
 /**
- * Override session_id before session startup if php's built-in
- * session generation code is not secure.
+ * @deprecated since 1.27, PHP's session generation isn't used with
+ *  MediaWiki\\Session\\SessionManager
  */
 function wfFixSessionID() {
-       // If the cookie or session id is already set we already have a session and should abort
-       if ( isset( $_COOKIE[session_name()] ) || session_id() ) {
-               return;
-       }
-
-       // PHP's built-in session entropy is enabled if:
-       // - entropy_file is set or you're on Windows with php 5.3.3+
-       // - AND entropy_length is > 0
-       // We treat it as disabled if it doesn't have an entropy length of at least 32
-       $entropyEnabled = wfCheckEntropy();
-
-       // If built-in entropy is not enabled or not sufficient override PHP's
-       // built in session id generation code
-       if ( !$entropyEnabled ) {
-               wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, " .
-                       "overriding session id generation using our cryptrand source.\n" );
-               session_id( MWCryptRand::generateHex( 32 ) );
-       }
+       wfDeprecated( __FUNCTION__, '1.27' );
 }
 
 /**
- * Reset the session_id
+ * Reset the session id
  *
+ * @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead
  * @since 1.22
  */
 function wfResetSessionID() {
-       global $wgCookieSecure;
-       $oldSessionId = session_id();
-       $cookieParams = session_get_cookie_params();
-       if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) {
-               session_regenerate_id( false );
-       } else {
-               $tmp = $_SESSION;
-               session_destroy();
-               wfSetupSession( MWCryptRand::generateHex( 32 ) );
-               $_SESSION = $tmp;
+       wfDeprecated( __FUNCTION__, '1.27' );
+       $session = SessionManager::getGlobalSession();
+       $delay = $session->delaySave();
+
+       $session->resetId();
+
+       // Make sure a session is started, since that's what the old
+       // wfResetSessionID() did.
+       if ( session_id() !== $session->getId() ) {
+               wfSetupSession( $session->getId() );
        }
-       $newSessionId = session_id();
+
+       ScopedCallback::consume( $delay );
 }
 
 /**
  * Initialise php session
  *
- * @param bool $sessionId
+ * @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead.
+ *  Generally, "using" SessionManager will be calling ->getSessionById() or
+ *  ::getGlobalSession() (depending on whether you were passing $sessionId
+ *  here), then calling $session->persist().
+ * @param bool|string $sessionId
  */
 function wfSetupSession( $sessionId = false ) {
-       global $wgSessionsInObjectCache, $wgSessionHandler;
-       global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly;
+       wfDeprecated( __FUNCTION__, '1.27' );
 
-       if ( $wgSessionsInObjectCache ) {
-               ObjectCacheSessionHandler::install();
-       } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) {
-               # Only set this if $wgSessionHandler isn't null and session.save_handler
-               # hasn't already been set to the desired value (that causes errors)
-               ini_set( 'session.save_handler', $wgSessionHandler );
+       // If they're calling this, they probably want our session management even
+       // if NO_SESSION was set for Setup.php.
+       if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
+               MediaWiki\Session\PHPSessionHandler::install( SessionManager::singleton() );
        }
 
-       session_set_cookie_params(
-               0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly );
-       session_cache_limiter( 'private, must-revalidate' );
        if ( $sessionId ) {
                session_id( $sessionId );
-       } else {
-               wfFixSessionID();
        }
 
-       MediaWiki\suppressWarnings();
-       session_start();
-       MediaWiki\restoreWarnings();
+       $session = SessionManager::getGlobalSession();
+       $session->persist();
 
-       if ( $wgSessionsInObjectCache ) {
-               ObjectCacheSessionHandler::renewCurrentSession();
+       if ( session_id() !== $session->getId() ) {
+               session_id( $session->getId() );
        }
+
+       MediaWiki\quietCall( 'session_start' );
 }
 
 /**
index 3b5a1b1..ab02ba7 100644 (file)
@@ -458,7 +458,6 @@ class MediaWiki {
         * @param Title $requestTitle The original title, before any redirects were applied
         */
        private function performAction( Page $page, Title $requestTitle ) {
-
                $request = $this->context->getRequest();
                $output = $this->context->getOutput();
                $title = $this->context->getTitle();
@@ -471,10 +470,16 @@ class MediaWiki {
                }
 
                $act = $this->getAction();
-
                $action = Action::factory( $act, $page, $this->context );
 
                if ( $action instanceof Action ) {
+                       // Narrow DB query expectations for this HTTP request
+                       $trxLimits = $this->config->get( 'TrxProfilerLimits' );
+                       $trxProfiler = Profiler::instance()->getTransactionProfiler();
+                       if ( $request->wasPosted() && !$action->doesWrites() ) {
+                               $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
+                       }
+
                        # Let CDN cache things if we can purge them.
                        if ( $this->config->get( 'UseSquid' ) &&
                                in_array(
@@ -494,7 +499,6 @@ class MediaWiki {
                        $output->setStatusCode( 404 );
                        $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
                }
-
        }
 
        /**
@@ -567,7 +571,9 @@ class MediaWiki {
                $request = $context->getRequest();
                if ( $request->wasPosted() && $factory->hasOrMadeRecentMasterChanges() ) {
                        $expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
-                       $request->response()->setCookie( 'UseDC', 'master', $expires, array( 'prefix' => '' ) );
+                       $options = array( 'prefix' => '' );
+                       $request->response()->setCookie( 'UseDC', 'master', $expires, $options );
+                       $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
                }
 
                // Avoid letting a few seconds of slave lag cause a month of stale data
@@ -623,7 +629,7 @@ class MediaWiki {
        }
 
        private function main() {
-               global $wgTitle, $wgTrxProfilerLimits;
+               global $wgTitle;
 
                $request = $this->context->getRequest();
 
@@ -647,13 +653,14 @@ class MediaWiki {
                $action = $this->getAction();
                $wgTitle = $title;
 
+               // Set DB query expectations for this HTTP request
+               $trxLimits = $this->config->get( 'TrxProfilerLimits' );
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
                $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
-
                if ( $request->wasPosted() ) {
-                       $trxProfiler->setExpectations( $wgTrxProfilerLimits['POST'], __METHOD__ );
+                       $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
                } else {
-                       $trxProfiler->setExpectations( $wgTrxProfilerLimits['GET'], __METHOD__ );
+                       $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
                }
 
                // If the user has forceHTTPS set to true, or if the user
@@ -664,8 +671,10 @@ class MediaWiki {
                if (
                        $request->getProtocol() == 'http' &&
                        (
+                               $request->getSession()->shouldForceHTTPS() ||
+                               // Check the cookie manually, for paranoia
                                $request->getCookie( 'forceHTTPS', '' ) ||
-                               // check for prefixed version for currently logged in users
+                               // check for prefixed version that was used for a time in older MW versions
                                $request->getCookie( 'forceHTTPS' ) ||
                                // Avoid checking the user and groups unless it's enabled.
                                (
index 97165b4..93ba702 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
 use WrappedString\WrappedString;
 
 /**
@@ -1977,11 +1978,9 @@ class OutputPage extends ContextSource {
                if ( $cookies === null ) {
                        $config = $this->getConfig();
                        $cookies = array_merge(
+                               SessionManager::singleton()->getVaryCookies(),
                                array(
-                                       $config->get( 'CookiePrefix' ) . 'Token',
-                                       $config->get( 'CookiePrefix' ) . 'LoggedOut',
-                                       "forceHTTPS",
-                                       session_name()
+                                       'forceHTTPS',
                                ),
                                $config->get( 'CacheVaryCookies' )
                        );
@@ -2033,6 +2032,9 @@ class OutputPage extends ContextSource {
         * @return string
         */
        public function getVaryHeader() {
+               foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
+                       $this->addVaryHeader( $header, $options );
+               }
                return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
        }
 
@@ -2050,6 +2052,10 @@ class OutputPage extends ContextSource {
                }
                $this->addVaryHeader( 'Cookie', $cookiesOption );
 
+               foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
+                       $this->addVaryHeader( $header, $options );
+               }
+
                $headers = array();
                foreach ( $this->mVaryHeader as $header => $option ) {
                        $newheader = $header;
@@ -2173,8 +2179,8 @@ class OutputPage extends ContextSource {
 
                if ( $this->mEnableClientCache ) {
                        if (
-                               $config->get( 'UseSquid' ) && session_id() == '' && !$this->isPrintable() &&
-                               $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
+                               $config->get( 'UseSquid' ) && !SessionManager::getGlobalSession()->isPersistent() &&
+                               !$this->isPrintable() && $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
                        ) {
                                if ( $config->get( 'UseESI' ) ) {
                                        # We'll purge the proxy cache explicitly, but require end user agents
index 41e88c2..eaab9c8 100644 (file)
@@ -162,7 +162,7 @@ function wfPHPVersionError( $type, $mwVersion, $minimumVersionPHP, $phpVersion )
 
        $longHtml = <<<HTML
                        Please consider <a href="http://www.php.net/downloads.php">upgrading your copy of PHP</a>.
-                       PHP versions less than 5.3.0 are no longer supported by the PHP Group and will not receive
+                       PHP versions less than 5.5.0 are no longer supported by the PHP Group and will not receive
                        security or bugfix updates.
                </p>
                <p>
diff --git a/includes/PageProps.php b/includes/PageProps.php
new file mode 100644 (file)
index 0000000..0a3a324
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+/**
+ * Access to properties of a page.
+ *
+ * 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
+ */
+
+/**
+ * Gives access to properties of a page.
+ *
+ * @since 1.27
+ *
+ */
+class PageProps {
+
+       /**
+        * @var PageProps
+        */
+       private static $instance;
+
+       /**
+        * @return PageProps
+        */
+       public static function getInstance() {
+               if ( self::$instance === null ) {
+                       self::$instance = new self();
+               }
+               return self::$instance;
+       }
+
+       /** Cache parameters */
+       const CACHE_TTL = 10; // integer; TTL in seconds
+       const CACHE_SIZE = 100; // integer; max cached pages
+
+       /** Property cache */
+       private $cache = null;
+
+       /**
+        * Create a PageProps object
+        */
+       private function __construct() {
+               $this->cache = new ProcessCacheLRU( self::CACHE_SIZE );
+       }
+
+       /**
+        * Given one or more Titles and the name of a property, returns an
+        * associative array mapping page ID to property value. Pages in the
+        * provided set of Titles that do not have a value for the given
+        * property will not appear in the returned array. If a single Title
+        * is provided, it does not need to be passed in an array, but an array
+        * will always be returned. An empty array will be returned if no
+        * matching properties were found.
+        *
+        * @param array|Title $titles
+        * @param string $propertyName
+        *
+        * @return array associative array mapping page ID to property value
+        *
+        */
+       public function getProperty( $titles, $propertyName ) {
+               $values = array();
+               $goodIDs = $this->getGoodIDs( $titles );
+               $queryIDs = array();
+               foreach ( $goodIDs as $pageID ) {
+                       $propertyValue = $this->getCachedProperty( $pageID, $propertyName );
+                       if ( $propertyValue === false ) {
+                               $queryIDs[] = $pageID;
+                       } else {
+                               $values[$pageID] = $propertyValue;
+                       }
+               }
+
+               if ( $queryIDs != array() ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $result = $dbr->select(
+                               'page_props',
+                               array(
+                                       'pp_page',
+                                       'pp_value'
+                               ),
+                               array(
+                                       'pp_page' => $queryIDs,
+                                       'pp_propname' => $propertyName
+                               ),
+                               __METHOD__
+                       );
+
+                       foreach ( $result as $row ) {
+                               $pageID = $row->pp_page;
+                               $propertyValue = $row->pp_value;
+                               $this->cacheProperty( $pageID, $propertyName, $propertyValue );
+                               $values[$pageID] = $propertyValue;
+                       }
+               }
+
+               return $values;
+       }
+
+       /**
+        * Get all page property values.
+        * Given one or more Titles, returns an associative array mapping page
+        * ID to an associative array mapping property names to property
+        * values. Pages in the provided set of Titles that do not have any
+        * properties will not appear in the returned array. If a single Title
+        * is provided, it does not need to be passed in an array, but an array
+        * will always be returned. An empty array will be returned if no
+        * matching properties were found.
+        *
+        * @param array|Title $titles
+        *
+        * @return array associative array mapping page ID to property value array
+        *
+        */
+       public function getProperties( $titles ) {
+               $values = array();
+               $goodIDs = $this->getGoodIDs( $titles );
+               $queryIDs = array();
+               foreach ( $goodIDs as $pageID ) {
+                       $pageProperties = $this->getCachedProperties( $pageID );
+                       if ( $pageProperties === false ) {
+                               $queryIDs[] = $pageID;
+                       } else {
+                               $values[$pageID] = $pageProperties;
+                       }
+               }
+
+               if ( $queryIDs != array() ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $result = $dbr->select(
+                               'page_props',
+                               array(
+                                       'pp_page',
+                                       'pp_propname',
+                                       'pp_value'
+                               ),
+                               array(
+                                       'pp_page' => $queryIDs,
+                               ),
+                               __METHOD__
+                       );
+
+                       $currentPageID = 0;
+                       $pageProperties = array();
+                       foreach ( $result as $row ) {
+                               $pageID = $row->pp_page;
+                               if ( $currentPageID != $pageID ) {
+                                       if ( $pageProperties != array() ) {
+                                               $this->cacheProperties( $currentPageID, $pageProperties );
+                                               $values[$currentPageID] = $pageProperties;
+                                       }
+                                       $currentPageID = $pageID;
+                                       $pageProperties = array();
+                               }
+                               $pageProperties[$row->pp_propname] = $row->pp_value;
+                       }
+                       if ( $pageProperties != array() ) {
+                               $this->cacheProperties( $pageID, $pageProperties );
+                               $values[$pageID] = $pageProperties;
+                       }
+               }
+
+               return $values;
+       }
+
+       /**
+        * @param array|Title $titles
+        *
+        * @return array array of good page IDs
+        *
+        */
+       private function getGoodIDs( $titles ) {
+               $result = array();
+               if ( is_array( $titles ) ) {
+                       foreach ( $titles as $title ) {
+                               $pageID = $title->getArticleID();
+                               if ( $pageID > 0 ) {
+                                       $result[] = $pageID;
+                               }
+                       }
+               } else {
+                       $pageID = $titles->getArticleID();
+                       if ( $pageID > 0 ) {
+                               $result[] = $pageID;
+                       }
+               }
+               return $result;
+       }
+
+       /**
+        * Get a property from the cache.
+        *
+        * @param int $pageID page ID of page being queried
+        * @param string $propertyName name of property being queried
+        *
+        * @return string|bool property value array or false if not found
+        *
+        */
+       private function getCachedProperty( $pageID, $propertyName ) {
+               if ( $this->cache->has( $pageID, $propertyName, self::CACHE_TTL ) ) {
+                       return $this->cache->get( $pageID, $propertyName );
+               }
+               if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
+                       $pageProperties = $this->cache->get( 0, $pageID );
+                       if ( isset( $pageProperties[$propertyName] ) ) {
+                               return $pageProperties[$propertyName];
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Get properties from the cache.
+        *
+        * @param int $pageID page ID of page being queried
+        *
+        * @return string|bool property value array or false if not found
+        *
+        */
+       private function getCachedProperties( $pageID ) {
+               if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
+                       return $this->cache->get( 0, $pageID );
+               }
+               return false;
+       }
+
+       /**
+        * Save a property to the cache.
+        *
+        * @param int $pageID page ID of page being cached
+        * @param string $propertyName name of property being cached
+        * @param mixed $propertyValue value of property
+        *
+        */
+       private function cacheProperty( $pageID, $propertyName, $propertyValue ) {
+               $this->cache->set( $pageID, $propertyName, $propertyValue );
+       }
+
+       /**
+        * Save properties to the cache.
+        *
+        * @param int $pageID page ID of page being cached
+        * @param array $pageProperties associative array of page properties to be cached
+        *
+        */
+       private function cacheProperties( $pageID, $pageProperties ) {
+               $this->cache->clear( $pageID );
+               $this->cache->set( 0, $pageID, $pageProperties );
+       }
+}
index 2723258..b7d0f42 100644 (file)
@@ -497,10 +497,25 @@ if ( $wgMaximalPasswordLength !== false ) {
        $wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength;
 }
 
-// Backwards compatibility with deprecated alias
-// Must be before call to wfSetupSession()
-if ( $wgSessionsInMemcached ) {
-       $wgSessionsInObjectCache = true;
+// Backwards compatibility warning
+if ( !$wgSessionsInObjectCache && !$wgSessionsInMemcached ) {
+       wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' );
+       if ( $wgSessionHandler ) {
+               wfDeprecated( '$wgSessionsHandler', '1.27' );
+       }
+       $cacheType = get_class( ObjectCache::getInstance( $wgSessionCacheType ) );
+       wfDebugLog(
+               "Session data will be stored in \"$cacheType\" cache with " .
+                       "expiry $wgObjectCacheSessionExpiry seconds"
+       );
+}
+$wgSessionsInObjectCache = true;
+
+if ( $wgPHPSessionHandling !== 'enable' &&
+       $wgPHPSessionHandling !== 'warn' &&
+       $wgPHPSessionHandling !== 'disable'
+) {
+       $wgPHPSessionHandling = 'warn';
 }
 
 Profiler::instance()->scopedProfileOut( $ps_default );
@@ -655,20 +670,6 @@ Profiler::instance()->scopedProfileOut( $ps_memcached );
 // Most of the config is out, some might want to run hooks here.
 Hooks::run( 'SetupAfterCache' );
 
-$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
-
-if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
-       // If session.auto_start is there, we can't touch session name
-       if ( !wfIniGetBool( 'session.auto_start' ) ) {
-               session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
-       }
-
-       if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) {
-               wfSetupSession();
-       }
-}
-
-Profiler::instance()->scopedProfileOut( $ps_session );
 $ps_globals = Profiler::instance()->scopedProfileIn( $fname . '-globals' );
 
 /**
@@ -681,6 +682,56 @@ $wgContLang->initContLang();
 // Now that variant lists may be available...
 $wgRequest->interpolateTitle();
 
+if ( !is_object( $wgAuth ) ) {
+       $wgAuth = new AuthPlugin;
+       Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
+}
+
+// Set up the session
+$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
+if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
+       // If session.auto_start is there, we can't touch session name
+       if ( $wgPHPSessionHandling !== 'disable' && !wfIniGetBool( 'session.auto_start' ) ) {
+               session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
+       }
+
+       // Create the SessionManager singleton and set up our session handler
+       MediaWiki\Session\PHPSessionHandler::install(
+               MediaWiki\Session\SessionManager::singleton()
+       );
+
+       // Initialize the session
+       try {
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+       } catch ( OverflowException $ex ) {
+               if ( isset( $ex->sessionInfos ) && count( $ex->sessionInfos ) >= 2 ) {
+                       // The exception is because the request had multiple possible
+                       // sessions tied for top priority. Report this to the user.
+                       $list = array();
+                       foreach ( $ex->sessionInfos as $info ) {
+                               $list[] = $info->getProvider()->describe( $wgContLang );
+                       }
+                       $list = $wgContLang->listToText( $list );
+                       throw new HttpError( 400,
+                               Message::newFromKey( 'sessionmanager-tie', $list )->inLanguage( $wgContLang )->plain()
+                       );
+               }
+
+               // Not the one we want, rethrow
+               throw $ex;
+       }
+
+       $session->renew();
+       if ( MediaWiki\Session\PHPSessionHandler::isEnabled() &&
+               ( $session->isPersistent() || $session->shouldRememberUser() )
+       ) {
+               // Start the PHP-session for backwards compatibility
+               session_id( $session->getId() );
+               MediaWiki\quietCall( 'session_start' );
+       }
+}
+Profiler::instance()->scopedProfileOut( $ps_session );
+
 /**
  * @var User $wgUser
  */
@@ -701,11 +752,6 @@ $wgOut = RequestContext::getMain()->getOutput(); // BackCompat
  */
 $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
 
-if ( !is_object( $wgAuth ) ) {
-       $wgAuth = new AuthPlugin;
-       Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
-}
-
 /**
  * @var Title $wgTitle
  */
@@ -737,6 +783,16 @@ foreach ( $wgExtensionFunctions as $func ) {
        Profiler::instance()->scopedProfileOut( $ps_ext_func );
 }
 
+// If the session user has a 0 id but a valid name, that means we need to
+// autocreate it.
+$sessionUser = MediaWiki\Session\SessionManager::getGlobalSession()->getUser();
+if ( $sessionUser->getId() === 0 && User::isValidUserName( $sessionUser->getName() ) ) {
+       $ps_autocreate = Profiler::instance()->scopedProfileIn( $fname . '-autocreate' );
+       MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser );
+       Profiler::instance()->scopedProfileOut( $ps_autocreate );
+}
+unset( $sessionUser );
+
 wfDebug( "Fully initialised\n" );
 $wgFullyInitialised = true;
 
index 56ba4c7..5b86ed6 100644 (file)
@@ -3020,20 +3020,22 @@ class Title {
                        return;
                }
 
-               $method = __METHOD__;
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
-                       $dbw->delete(
-                               'page_restrictions',
-                               array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
-                               $method
-                       );
-                       $dbw->delete(
-                               'protected_titles',
-                               array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
-                               $method
-                       );
-               } );
+               DeferredUpdates::addUpdate( new AtomicSectionUpdate(
+                       wfGetDB( DB_MASTER ),
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) {
+                               $dbw->delete(
+                                       'page_restrictions',
+                                       array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+                                       $fname
+                               );
+                               $dbw->delete(
+                                       'protected_titles',
+                                       array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
+                                       $fname
+                               );
+                       }
+               ) );
        }
 
        /**
index 7b76592..7306105 100644 (file)
@@ -23,6 +23,8 @@
  * @file
  */
 
+use MediaWiki\Session\SessionManager;
+
 /**
  * The WebRequest class encapsulates getting at data passed in the
  * URL or via a POSTed form stripping illegal input characters and
@@ -63,6 +65,13 @@ class WebRequest {
         */
        protected $protocol;
 
+       /**
+        * @var \\MediaWiki\\Session\\SessionId|null Session ID to use for this
+        *  request. We can't save the session directly due to reference cycles not
+        *  working too well (slow GC in Zend and never collected in HHVM).
+        */
+       protected $sessionId = null;
+
        public function __construct() {
                $this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
                        ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
@@ -638,18 +647,44 @@ class WebRequest {
        }
 
        /**
-        * Returns true if there is a session cookie set.
+        * Return the session for this request
+        * @since 1.27
+        * @note For performance, keep the session locally if you will be making
+        *  much use of it instead of calling this method repeatedly.
+        * @return MediaWiki\\Session\\Session
+        */
+       public function getSession() {
+               if ( $this->sessionId !== null ) {
+                       return SessionManager::singleton()->getSessionById( (string)$this->sessionId, false, $this );
+               }
+
+               $session = SessionManager::singleton()->getSessionForRequest( $this );
+               $this->sessionId = $session->getSessionId();
+               return $session;
+       }
+
+       /**
+        * Set the session for this request
+        * @since 1.27
+        * @private For use by MediaWiki\\Session classes only
+        * @param MediaWiki\\Session\\SessionId $sessionId
+        */
+       public function setSessionId( MediaWiki\Session\SessionId $sessionId ) {
+               $this->sessionId = $sessionId;
+       }
+
+       /**
+        * Returns true if the request has a persistent session.
         * This does not necessarily mean that the user is logged in!
         *
-        * If you want to check for an open session, use session_id()
-        * instead; that will also tell you if the session was opened
-        * during the current request (in which case the cookie will
-        * be sent back to the client at the end of the script run).
-        *
+        * @deprecated since 1.27, use
+        *  \\MediaWiki\\Session\\SessionManager::singleton()->getPersistedSessionId()
+        *  instead.
         * @return bool
         */
        public function checkSessionCookie() {
-               return isset( $_COOKIE[session_name()] );
+               wfDeprecated( __METHOD__, '1.27' );
+               return SessionManager::singleton()->getPersistedSessionId( $this ) !== null;
        }
 
        /**
@@ -907,26 +942,25 @@ class WebRequest {
        }
 
        /**
-        * Get data from $_SESSION
+        * Get data from the session
         *
-        * @param string $key Name of key in $_SESSION
+        * @note Prefer $this->getSession() instead if making multiple calls.
+        * @param string $key Name of key in the session
         * @return mixed
         */
        public function getSessionData( $key ) {
-               if ( !isset( $_SESSION[$key] ) ) {
-                       return null;
-               }
-               return $_SESSION[$key];
+               return $this->getSession()->get( $key );
        }
 
        /**
         * Set session data
         *
-        * @param string $key Name of key in $_SESSION
+        * @note Prefer $this->getSession() instead if making multiple calls.
+        * @param string $key Name of key in the session
         * @param mixed $data
         */
        public function setSessionData( $key, $data ) {
-               $_SESSION[$key] = $data;
+               return $this->getSession()->set( $key, $data );
        }
 
        /**
index 26fb20f..f14cf22 100644 (file)
  */
 class WebResponse {
 
+       /** @var array Used to record set cookies, because PHP's setcookie() will
+        * happily send an identical Set-Cookie to the client.
+        */
+       protected static $setCookies = array();
+
        /**
         * Output an HTTP header, wrapper for PHP's header()
         * @param string $string Header to output
@@ -62,6 +67,15 @@ class WebResponse {
                HttpStatus::header( $code );
        }
 
+       /**
+        * Test if headers have been sent
+        * @since 1.27
+        * @return bool
+        */
+       public function headersSent() {
+               return headers_sent();
+       }
+
        /**
         * Set the browser cookie
         * @param string $name The name of the cookie.
@@ -115,25 +129,26 @@ class WebResponse {
                $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
 
                if ( Hooks::run( 'WebResponseSetCookie', array( &$name, &$value, &$expire, $options ) ) ) {
-                       wfDebugLog( 'cookie',
-                               $func . ': "' . implode( '", "',
-                                       array(
-                                               $options['prefix'] . $name,
-                                               $value,
-                                               $expire,
-                                               $options['path'],
-                                               $options['domain'],
-                                               $options['secure'],
-                                               $options['httpOnly'] ) ) . '"' );
-
-                       call_user_func( $func,
-                               $options['prefix'] . $name,
-                               $value,
-                               $expire,
-                               $options['path'],
-                               $options['domain'],
-                               $options['secure'],
-                               $options['httpOnly'] );
+                       $cookie = $options['prefix'] . $name;
+                       $data = array(
+                               (string)$cookie,
+                               (string)$value,
+                               (int)$expire,
+                               (string)$options['path'],
+                               (string)$options['domain'],
+                               (bool)$options['secure'],
+                               (bool)$options['httpOnly'],
+                       );
+                       if ( !isset( self::$setCookies[$cookie] ) ||
+                               self::$setCookies[$cookie] !== array( $func, $data )
+                       ) {
+                               wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
+                               if ( call_user_func_array( $func, $data ) ) {
+                                       self::$setCookies[$cookie] = array( $func, $data );
+                               }
+                       } else {
+                               wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
+                       }
                }
        }
 
@@ -156,7 +171,7 @@ class WebResponse {
  */
 class FauxResponse extends WebResponse {
        private $headers;
-       private $cookies;
+       private $cookies = array();
        private $code;
 
        /**
@@ -192,6 +207,10 @@ class FauxResponse extends WebResponse {
                $this->code = intval( $code );
        }
 
+       public function headersSent() {
+               return false;
+       }
+
        /**
         * @param string $key The name of the header to get (case insensitive).
         * @return string|null The header value (if set); null otherwise.
index 9e7d12c..6c768ff 100644 (file)
@@ -294,6 +294,7 @@ $zh2Hant = array(
 '卖' => '賣',
 '卢' => '盧',
 '卤' => '鹵',
+'卧' => '臥',
 '卫' => '衛',
 '却' => '卻',
 '厂' => '廠',
@@ -3424,6 +3425,7 @@ $zh2Hant = array(
 '干股' => '乾股',
 '干肥' => '乾肥',
 '干脆' => '乾脆',
+'干脆面' => '乾脆麵',
 '干花' => '乾花',
 '干刍' => '乾芻',
 '干苔' => '乾苔',
@@ -3564,6 +3566,7 @@ $zh2Hant = array(
 '于慧' => '于慧',
 '于成龍' => '于成龍',
 '于成龙' => '于成龍',
+'于承惠' => '于承惠',
 '于振' => '于振',
 '于振武' => '于振武',
 '于敏' => '于敏',
@@ -3644,6 +3647,7 @@ $zh2Hant = array(
 '于赠' => '于贈',
 '于越' => '于越',
 '于軍' => '于軍',
+'于逸堯' => '于逸堯',
 '于道泉' => '于道泉',
 '于远伟' => '于遠偉',
 '于遠偉' => '于遠偉',
@@ -3829,6 +3833,8 @@ $zh2Hant = array(
 '信托' => '信託',
 '修杰楷' => '修杰楷',
 '修杰麟' => '修杰麟',
+'修筑前' => '修築前',
+'修筑后' => '修築後',
 '修胡刀' => '修鬍刀',
 '俯冲' => '俯衝',
 '个月里' => '個月裡',
@@ -4112,7 +4118,6 @@ $zh2Hant = array(
 '削面' => '削麵',
 '克剥' => '剋剝',
 '克扣' => '剋扣',
-'克星' => '剋星',
 '克期' => '剋期',
 '克死' => '剋死',
 '克薄' => '剋薄',
@@ -4235,6 +4240,7 @@ $zh2Hant = array(
 '去山里' => '去山裡',
 '参数只' => '參數只',
 '参数里' => '參數裡',
+'反反复复' => '反反覆覆',
 '反应制得' => '反應製得',
 '反朴' => '反樸',
 '反冲' => '反衝',
@@ -4736,14 +4742,10 @@ $zh2Hant = array(
 '委托书' => '委託書',
 '奸夫' => '姦夫',
 '奸妇' => '姦婦',
-'奸宄' => '姦宄',
 '奸情' => '姦情',
 '奸杀' => '姦殺',
 '奸污' => '姦污',
 '奸淫' => '姦淫',
-'奸猾' => '姦猾',
-'奸细' => '姦細',
-'奸邪' => '姦邪',
 '威棱' => '威稜',
 '婢仆' => '婢僕',
 '嫁祸于' => '嫁禍於',
@@ -4758,6 +4760,7 @@ $zh2Hant = array(
 '子里' => '子裡',
 '子里甲' => '子里甲',
 '字汇' => '字彙',
+'字母后' => '字母後',
 '字码表' => '字碼表',
 '字里行间' => '字裡行間',
 '存折' => '存摺',
@@ -5056,8 +5059,11 @@ $zh2Hant = array(
 '廢后' => '廢后',
 '广征' => '廣徵',
 '广舍' => '廣捨',
+'广播里' => '廣播裡',
 '延历' => '延曆',
 '建于' => '建於',
+'建筑前' => '建築前',
+'建筑后' => '建築後',
 '弄干' => '弄乾',
 '弄丑' => '弄醜',
 '弄脏胸' => '弄髒胸',
@@ -5131,6 +5137,7 @@ $zh2Hant = array(
 '影后' => '影后',
 '影相吊' => '影相弔',
 '役于' => '役於',
+'往复式' => '往復式',
 '往日无仇' => '往日無讎',
 '往里' => '往裡',
 '待复' => '待覆',
@@ -5405,7 +5412,6 @@ $zh2Hant = array(
 '欲令智昏' => '慾令智昏',
 '欲壑难填' => '慾壑難填',
 '欲念' => '慾念',
-'欲望' => '慾望',
 '欲海' => '慾海',
 '欲火' => '慾火',
 '欲障' => '慾障',
@@ -5472,6 +5478,7 @@ $zh2Hant = array(
 '手表面' => '手表面',
 '手里剑' => '手裏劍',
 '手里' => '手裡',
+'手游' => '手遊',
 '手表' => '手錶',
 '手链' => '手鍊',
 '手松' => '手鬆',
@@ -6227,6 +6234,7 @@ $zh2Hant = array(
 '水来汤里去' => '水來湯裡去',
 '水准' => '水準',
 '水无怜奈' => '水無怜奈',
+'水表示' => '水表示',
 '水表面' => '水表面',
 '水里' => '水裡',
 '水里商工' => '水里商工',
@@ -6499,6 +6507,7 @@ $zh2Hant = array(
 '沈丹客运' => '瀋丹客運',
 '沈丹线' => '瀋丹線',
 '沈丹铁路' => '瀋丹鐵路',
+'沈丹高' => '瀋丹高',
 '沈北' => '瀋北',
 '沈吉' => '瀋吉',
 '沈大线' => '瀋大線',
@@ -6714,7 +6723,6 @@ $zh2Hant = array(
 '发松' => '發鬆',
 '发面' => '發麵',
 '白干儿' => '白乾兒',
-'白子里' => '白子里',
 '白术' => '白朮',
 '白朴' => '白樸',
 '白净面皮' => '白淨面皮',
@@ -6735,6 +6743,7 @@ $zh2Hant = array(
 '百只足夠' => '百只足夠',
 '百周后' => '百周後',
 '百天后' => '百天後',
+'百子里' => '百子里',
 '百年' => '百年',
 '百拙千丑' => '百拙千醜',
 '百科里' => '百科裡',
@@ -6970,6 +6979,7 @@ $zh2Hant = array(
 '窗明几净' => '窗明几淨',
 '窗帘' => '窗簾',
 '窝里' => '窩裡',
+'窝里斗' => '窩裡鬥',
 '穷于' => '窮於',
 '穷追不舍' => '窮追不捨',
 '穷发' => '窮髮',
@@ -7280,6 +7290,7 @@ $zh2Hant = array(
 '聚药雄蕊' => '聚葯雄蕊',
 '闻风后' => '聞風後',
 '联系' => '聯繫',
+'声母后' => '聲母後',
 '听于' => '聽於',
 '肉干' => '肉乾',
 '肉欲' => '肉慾',
@@ -7970,11 +7981,13 @@ $zh2Hant = array(
 '警报钟' => '警報鐘',
 '警示钟' => '警示鐘',
 '警钟' => '警鐘',
+'译制' => '譯製',
 '译注' => '譯註',
 '护发' => '護髮',
 '变征' => '變徵',
 '变丑' => '變醜',
 '仇隙' => '讎隙',
+'赞一个' => '讚一個',
 '赞不绝口' => '讚不絕口',
 '赞佩' => '讚佩',
 '赞呗' => '讚唄',
@@ -8593,7 +8606,6 @@ $zh2Hant = array(
 '防御' => '防禦',
 '防范' => '防範',
 '防锈' => '防鏽',
-'防台' => '防颱',
 '阻于' => '阻於',
 '阿里' => '阿里',
 '附于' => '附於',
@@ -8683,6 +8695,7 @@ $zh2Hant = array(
 '电码表' => '電碼表',
 '电冲' => '電衝',
 '电视台风' => '電視台風',
+'电视里' => '電視裡',
 '电表' => '電錶',
 '电钟' => '電鐘',
 '震栗' => '震慄',
@@ -9345,7 +9358,6 @@ $zh2Hant = array(
 '齿危发秀' => '齒危髮秀',
 '齿落发白' => '齒落髮白',
 '齿发' => '齒髮',
-'出儿' => '齣兒',
 '龙岩' => '龍巖',
 '龙卷' => '龍捲',
 '龙眼干' => '龍眼乾',
@@ -9354,6 +9366,10 @@ $zh2Hant = array(
 '龙斗虎伤' => '龍鬥虎傷',
 '龜山庄' => '龜山庄',
 '龟鉴' => '龜鑑',
+',并力' => ',並力',
+',并力攻' => ',并力攻',
+',并力討' => ',并力討',
+',并力讨' => ',并力討',
 ',个中' => ',箇中',
 );
 
@@ -13874,6 +13890,8 @@ $zh2TW = array(
 '傅里叶' => '傅立葉',
 '光盘' => '光碟',
 '光驱' => '光碟機',
+'开普勒' => '克卜勒',
+'開普勒' => '克卜勒',
 '克罗地亚' => '克羅埃西亞',
 '克羅地亞' => '克羅埃西亞',
 '克里斯托弗' => '克里斯多福',
@@ -13896,6 +13914,7 @@ $zh2TW = array(
 '包豪斯' => '包浩斯',
 '北朝鲜' => '北韓',
 '局域网' => '區域網',
+'局域网络' => '區域網路',
 '十杆' => '十桿',
 '特立尼达和托巴哥' => '千里達托貝哥',
 '特立尼達和多巴哥' => '千里達托貝哥',
@@ -14100,6 +14119,8 @@ $zh2TW = array(
 '摩根士丹利' => '摩根史坦利',
 '台球' => '撞球',
 '攻打' => '攻打',
+'数字化' => '數位化',
+'數碼化' => '數位化',
 '数字技术' => '數位技術',
 '數碼技術' => '數位技術',
 '数字照相机' => '數位照相機',
@@ -14150,6 +14171,7 @@ $zh2TW = array(
 '撒切尔' => '柴契爾',
 '格林納達' => '格瑞那達',
 '格林纳达' => '格瑞那達',
+'桃金娘' => '桃金孃',
 '台式电脑' => '桌上型電腦',
 '乒乓' => '桌球',
 '乒乓球' => '桌球',
@@ -14291,6 +14313,7 @@ $zh2TW = array(
 '互联网' => '網際網路',
 '互联网络' => '網際網路',
 '互聯網' => '網際網路',
+'互聯網絡' => '網際網路',
 '因特网' => '網際網路',
 '系着' => '繫著',
 '卢瓦尔' => '羅亞爾',
@@ -14311,7 +14334,6 @@ $zh2TW = array(
 '聖馬力諾' => '聖馬利諾',
 '肯尼亚' => '肯亞',
 '氨基酸' => '胺基酸',
-'卧' => '臥',
 '自由泳' => '自由式',
 '三藩市' => '舊金山',
 '艾森豪威尔' => '艾森豪',
@@ -14403,8 +14425,6 @@ $zh2TW = array(
 '普密蓬' => '蒲美蓬',
 '布隆迪' => '蒲隆地',
 '圭亚那' => '蓋亞那',
-'开曼群岛' => '蓋曼群島',
-'開曼群島' => '蓋曼群島',
 '肖斯塔科维奇' => '蕭士塔高維奇',
 '蕭士達高維契' => '蕭士塔高維奇',
 '肖邦' => '蕭邦',
@@ -14429,7 +14449,6 @@ $zh2TW = array(
 '埃塞俄比亚' => '衣索比亞',
 '埃塞俄比亞' => '衣索比亞',
 '克隆人' => '複製人',
-'囯际象棋' => '西洋棋',
 '国际象棋' => '西洋棋',
 '國際象棋' => '西洋棋',
 '赫梯' => '西臺',
@@ -14454,7 +14473,9 @@ $zh2TW = array(
 '信息时代' => '資訊時代',
 '信息论' => '資訊理論',
 '乔布斯' => '賈伯斯',
+'本·拉登' => '賓·拉登',
 '宾西法尼亚' => '賓夕法尼亞',
+'本拉登' => '賓拉登',
 '利比里亚' => '賴比瑞亞',
 '利比里亞' => '賴比瑞亞',
 '莱索托' => '賴索托',
@@ -15486,6 +15507,7 @@ $zh2HK = array(
 '坎城' => '康城',
 '戛纳' => '康城',
 '庙里' => '廟裏',
+'广播里' => '廣播裏',
 '強占' => '強佔',
 '强占' => '強佔',
 '约翰斯顿岛' => '強斯頓環礁',
@@ -15889,6 +15911,8 @@ $zh2HK = array(
 '数字照相机' => '数碼照相機',
 '數位照相機' => '数碼照相機',
 '數著' => '數着',
+'数字化' => '數碼化',
+'數位化' => '數碼化',
 '数字技术' => '數碼技術',
 '數位技術' => '數碼技術',
 '數位相機' => '數碼相機',
@@ -15994,7 +16018,9 @@ $zh2HK = array(
 '朝著述' => '朝著述',
 '朝著錄' => '朝著錄',
 '板球' => '木球',
+'賓·拉登' => '本·拉登',
 '班傑明' => '本傑明',
+'賓拉登' => '本拉登',
 '本著' => '本着',
 '本著作' => '本著作',
 '本著名' => '本著名',
@@ -16559,6 +16585,7 @@ $zh2HK = array(
 '穿著述' => '穿著述',
 '穿著錄' => '穿著錄',
 '窝里' => '窩裏',
+'窝里斗' => '窩裏鬥',
 '立著' => '立着',
 '立著《' => '立著《',
 '立著作' => '立著作',
@@ -17292,7 +17319,7 @@ $zh2HK = array(
 '閉著者' => '閉著者',
 '閉著述' => '閉著述',
 '閉著錄' => '閉著錄',
-'蓋曼群島' => '開曼群島',
+'克卜勒' => '開普勒',
 '開著' => '開着',
 '開著作' => '開著作',
 '開著名' => '開著名',
@@ -17409,6 +17436,7 @@ $zh2HK = array(
 '电梯里' => '電梯裏',
 '电脑程序' => '電腦程式',
 '计算机程序' => '電腦程式',
+'电视里' => '電視裏',
 '霄裡' => '霄裡',
 '荷姆茲' => '霍爾木茲',
 '雾里' => '霧裏',
@@ -18186,7 +18214,6 @@ $zh2CN = array(
 '因著稱' => '因著称',
 '因著者' => '因著者',
 '因著述' => '因著述',
-'西洋棋' => '囯际象棋',
 '困著' => '困着',
 '困著書' => '困著书',
 '困著作' => '困著作',
@@ -18205,6 +18232,7 @@ $zh2CN = array(
 '圍著述' => '围著述',
 '韌體' => '固件',
 '固著' => '固着',
+'西洋棋' => '国际象棋',
 '土魯斯' => '图卢兹',
 '吐瓦魯' => '图瓦卢',
 '原子筆' => '圆珠笔',
@@ -18340,6 +18368,7 @@ $zh2CN = array(
 '奈及利亞' => '尼日利亚',
 '尼日爾' => '尼日尔',
 '區域網' => '局域网',
+'區域網路' => '局域网络',
 '螢幕' => '屏幕',
 '展著' => '展着',
 '展著書' => '展著书',
@@ -18403,7 +18432,6 @@ $zh2CN = array(
 '應著述' => '应著述',
 '建帳' => '建账',
 '克卜勒' => '开普勒',
-'蓋曼群島' => '开曼群岛',
 '開著' => '开着',
 '開著書' => '开著书',
 '開著作' => '开著作',
@@ -18805,8 +18833,8 @@ $zh2CN = array(
 '散布著' => '散布着',
 '數位訊號' => '数字信号',
 '數碼訊號' => '数字信号',
+'數位化' => '数字化',
 '數位技術' => '数字技术',
-'數碼技術' => '数字技术',
 '數位電視' => '数字电视',
 '數碼電視' => '数字电视',
 '資料庫' => '数据库',
@@ -18913,7 +18941,9 @@ $zh2CN = array(
 '朝著稱' => '朝著称',
 '朝著者' => '朝著者',
 '朝著述' => '朝著述',
+'賓·拉登' => '本·拉登',
 '本份' => '本分',
+'賓拉登' => '本拉登',
 '本本份份' => '本本分分',
 '班傑明' => '本杰明',
 '本著' => '本着',
index 6ddc596..97d7132 100644 (file)
@@ -417,4 +417,13 @@ abstract class Action {
                        wfTransactionalTimeLimit();
                }
        }
+
+       /**
+        * Indicates whether this action may perform database writes
+        * @return bool
+        * @since 1.27
+        */
+       public function doesWrites() {
+               return false;
+       }
 }
index 841a94d..cb57d3e 100644 (file)
@@ -53,4 +53,8 @@ class DeleteAction extends FormlessAction {
                $this->addHelpLink( 'Help:Sysop deleting and undeleting' );
                $this->page->delete();
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index eb53f19..643d1c4 100644 (file)
@@ -58,4 +58,8 @@ class EditAction extends FormlessAction {
                        $editor->edit();
                }
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index aa201d7..55b9b99 100644 (file)
@@ -127,4 +127,8 @@ abstract class FormAction extends Action {
                        $this->onSuccess();
                }
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 7389ae2..6f33db7 100644 (file)
@@ -203,18 +203,10 @@ class InfoAction extends FormlessAction {
 
                $pageCounts = $this->pageCounts( $this->page );
 
-               // Get page properties
-               $dbr = wfGetDB( DB_SLAVE );
-               $result = $dbr->select(
-                       'page_props',
-                       array( 'pp_propname', 'pp_value' ),
-                       array( 'pp_page' => $id ),
-                       __METHOD__
-               );
-
                $pageProperties = array();
-               foreach ( $result as $row ) {
-                       $pageProperties[$row->pp_propname] = $row->pp_value;
+               $props = PageProps::getInstance()->getProperties( $title );
+               if ( isset( $props[$id] ) ) {
+                       $pageProperties = $props[$id];
                }
 
                // Basic information
index 4016f67..b8a7616 100644 (file)
@@ -63,8 +63,14 @@ class MarkpatrolledAction extends FormlessAction {
                }
 
                # It would be nice to see where the user had actually come from, but for now just guess
-               $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
-               $return = SpecialPage::getTitleFor( $returnto );
+               if ( $rc->getAttribute( 'rc_type' ) == RC_NEW ) {
+                       $returnTo = 'Newpages';
+               } elseif ( $rc->getAttribute( 'rc_log_type' ) == 'upload' ) {
+                       $returnTo = 'Newfiles';
+               } else {
+                       $returnTo = 'Recentchanges';
+               }
+               $return = SpecialPage::getTitleFor( $returnTo );
 
                if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) {
                        $this->getOutput()->setPageTitle( $this->msg( 'markedaspatrollederror' ) );
@@ -83,4 +89,8 @@ class MarkpatrolledAction extends FormlessAction {
                $this->getOutput()->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() );
                $this->getOutput()->returnToMain( null, $return );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 48909cf..126daa0 100644 (file)
@@ -51,4 +51,8 @@ class ProtectAction extends FormlessAction {
 
                $this->page->protect();
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 7e77846..183c3b9 100644 (file)
@@ -97,4 +97,8 @@ class PurgeAction extends FormAction {
        public function onSuccess() {
                $this->getOutput()->redirect( $this->getTitle()->getFullURL( $this->redirectParams ) );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 69cd7aa..b371848 100644 (file)
@@ -83,7 +83,8 @@ class RawAction extends FormlessAction {
                $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
                // Output may contain user-specific data;
                // vary generated content for open sessions on private wikis
-               $privateCache = !User::isEveryoneAllowed( 'read' ) && ( $smaxage == 0 || session_id() != '' );
+               $privateCache = !User::isEveryoneAllowed( 'read' ) &&
+                       ( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
                // Don't accidentally cache cookies if user is logged in (T55032)
                $privateCache = $privateCache || $this->getUser()->isLoggedIn();
                $mode = $privateCache ? 'private' : 'public';
index 4885a31..8a54c39 100644 (file)
@@ -151,4 +151,8 @@ class RevertAction extends FormAction {
        protected function getDescription() {
                return OutputPage::buildBacklinkSubtitle( $this->getTitle() );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index dbcb848..7df42b3 100644 (file)
@@ -58,4 +58,8 @@ class RevisiondeleteAction extends FormlessAction {
                $special->getContext()->setTitle( $special->getPageTitle() );
                $special->run( '' );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 0404856..cc94fd3 100644 (file)
@@ -126,4 +126,8 @@ class RollbackAction extends FormlessAction {
        protected function getDescription() {
                return '';
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index fae49f6..8990b75 100644 (file)
@@ -32,10 +32,8 @@ class SubmitAction extends EditAction {
        }
 
        public function show() {
-               if ( session_id() === '' ) {
-                       // Send a cookie so anons get talk message notifications
-                       wfSetupSession();
-               }
+               // Send a cookie so anons get talk message notifications
+               MediaWiki\Session\SessionManager::getGlobalSession()->persist();
 
                parent::show();
        }
index 559cfaf..0757e88 100644 (file)
@@ -39,4 +39,8 @@ class UnprotectAction extends ProtectAction {
        public function show() {
                $this->page->unprotect();
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 0a8628d..f8f1dc1 100644 (file)
@@ -52,4 +52,8 @@ class UnwatchAction extends WatchAction {
        public function onSuccess() {
                $this->getOutput()->addWikiMsg( 'removedwatchtext', $this->getTitle()->getPrefixedText() );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 30b83d7..547fd9b 100644 (file)
@@ -178,4 +178,8 @@ class WatchAction extends FormAction {
        public static function getUnwatchToken( Title $title, User $user, $action = 'unwatch' ) {
                return self::getWatchToken( $title, $user, $action );
        }
+
+       public function doesWrites() {
+               return true;
+       }
 }
index 1368bda..a044be2 100644 (file)
@@ -59,10 +59,8 @@ class ApiCreateAccount extends ApiBase {
 
                $params = $this->extractRequestParams();
 
-               // Init session if necessary
-               if ( session_id() == '' ) {
-                       wfSetupSession();
-               }
+               // Make sure session is persisted
+               MediaWiki\Session\SessionManager::getGlobalSession()->persist();
 
                if ( $params['mailpassword'] && !$params['email'] ) {
                        $this->dieUsageMsg( 'noemail' );
index eb376d3..860e3b2 100644 (file)
@@ -24,6 +24,7 @@
  *
  * @file
  */
+
 use MediaWiki\Logger\LoggerFactory;
 
 /**
@@ -62,26 +63,72 @@ class ApiLogin extends ApiBase {
 
                $result = array();
 
-               // Init session if necessary
-               if ( session_id() == '' ) {
-                       wfSetupSession();
+               // Make sure session is persisted
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+               $session->persist();
+
+               // Make sure it's possible to log in
+               if ( !$session->canSetUser() ) {
+                       $this->getResult()->addValue( null, 'login', array(
+                               'result' => 'Aborted',
+                               'reason' => 'Cannot log in when using ' .
+                                       $session->getProvider()->describe( Language::factory( 'en' ) ),
+                       ) );
+
+                       return;
                }
 
+               $authRes = false;
                $context = new DerivativeContext( $this->getContext() );
-               $context->setRequest( new DerivativeRequest(
-                       $this->getContext()->getRequest(),
-                       array(
-                               'wpName' => $params['name'],
-                               'wpPassword' => $params['password'],
-                               'wpDomain' => $params['domain'],
-                               'wpLoginToken' => $params['token'],
-                               'wpRemember' => ''
-                       )
-               ) );
-               $loginForm = new LoginForm();
-               $loginForm->setContext( $context );
+               $loginType = 'N/A';
+
+               // Check login token
+               $token = LoginForm::getLoginToken();
+               if ( !$token ) {
+                       LoginForm::setLoginToken();
+                       $authRes = LoginForm::NEED_TOKEN;
+               } elseif ( !$params['token'] ) {
+                       $authRes = LoginForm::NEED_TOKEN;
+               } elseif ( $token !== $params['token'] ) {
+                       $authRes = LoginForm::WRONG_TOKEN;
+               }
+
+               // Try bot passwords
+               if ( $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) &&
+                       strpos( $params['name'], BotPassword::getSeparator() ) !== false
+               ) {
+                       $status = BotPassword::login(
+                               $params['name'], $params['password'], $this->getRequest()
+                       );
+                       if ( $status->isOk() ) {
+                               $session = $status->getValue();
+                               $authRes = LoginForm::SUCCESS;
+                               $loginType = 'BotPassword';
+                       } else {
+                               LoggerFactory::getInstance( 'authmanager' )->info(
+                                       'BotPassword login failed: ' . $status->getWikiText()
+                               );
+                       }
+               }
+
+               // Normal login
+               if ( $authRes === false ) {
+                       $context->setRequest( new DerivativeRequest(
+                               $this->getContext()->getRequest(),
+                               array(
+                                       'wpName' => $params['name'],
+                                       'wpPassword' => $params['password'],
+                                       'wpDomain' => $params['domain'],
+                                       'wpLoginToken' => $params['token'],
+                                       'wpRemember' => ''
+                               )
+                       ) );
+                       $loginForm = new LoginForm();
+                       $loginForm->setContext( $context );
+                       $authRes = $loginForm->authenticateUserData();
+                       $loginType = 'LoginForm';
+               }
 
-               $authRes = $loginForm->authenticateUserData();
                switch ( $authRes ) {
                        case LoginForm::SUCCESS:
                                $user = $context->getUser();
@@ -107,16 +154,16 @@ class ApiLogin extends ApiBase {
                                // SessionManager/AuthManager are *really* going to break it.
                                $result['lgtoken'] = $user->getToken();
                                $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
-                               $result['sessionid'] = session_id();
+                               $result['sessionid'] = $session->getId();
                                break;
 
                        case LoginForm::NEED_TOKEN:
                                $result['result'] = 'NeedToken';
-                               $result['token'] = $loginForm->getLoginToken();
+                               $result['token'] = LoginForm::getLoginToken();
 
                                // @todo: See above about deprecation
                                $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
-                               $result['sessionid'] = session_id();
+                               $result['sessionid'] = $session->getId();
                                break;
 
                        case LoginForm::WRONG_TOKEN:
@@ -187,6 +234,7 @@ class ApiLogin extends ApiBase {
                LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', array(
                        'event' => 'login',
                        'successful' => $authRes === LoginForm::SUCCESS,
+                       'loginType' => $loginType,
                        'status' => LoginForm::$statusCodes[$authRes],
                ) );
        }
index bf0ca9c..b40f5a3 100644 (file)
 class ApiLogout extends ApiBase {
 
        public function execute() {
+               // Make sure it's possible to log out
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+               if ( !$session->canSetUser() ) {
+                       $this->dieUsage(
+                               'Cannot log out when using ' .
+                                       $session->getProvider()->describe( Language::factory( 'en' ) ),
+                               'cannotlogout'
+                       );
+               }
+
                $user = $this->getUser();
                $oldName = $user->getName();
                $user->logout();
index ef9f901..6ddc28a 100644 (file)
@@ -769,7 +769,7 @@ class ApiMain extends ApiBase {
                                        return;
                                }
                                // Logged out, send normal public headers below
-                       } elseif ( session_id() != '' ) {
+                       } elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
                                // Logged in or otherwise has session (e.g. anonymous users who have edited)
                                // Mark request private
                                $response->header( "Cache-Control: $privateCache" );
index 8dbd812..267cc6d 100644 (file)
@@ -515,6 +515,11 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        }
                        $vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT );
                        $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl(), PROTO_CURRENT );
+
+                       $shortDescriptionUrl = $file->getDescriptionShortUrl();
+                       if ( $shortDescriptionUrl !== null ) {
+                               $vals['descriptionshorturl'] = wfExpandUrl( $shortDescriptionUrl, PROTO_CURRENT );
+                       }
                }
 
                if ( $sha1 ) {
index 1f992f8..c2a8df7 100644 (file)
@@ -40,63 +40,44 @@ class ApiQueryPageProps extends ApiQueryBase {
        public function execute() {
                # Only operate on existing pages
                $pages = $this->getPageSet()->getGoodTitles();
-               if ( !count( $pages ) ) {
-                       # Nothing to do
-                       return;
-               }
 
                $this->params = $this->extractRequestParams();
-
-               $this->addTables( 'page_props' );
-               $this->addFields( array( 'pp_page', 'pp_propname', 'pp_value' ) );
-               $this->addWhereFld( 'pp_page', array_keys( $pages ) );
-
                if ( $this->params['continue'] ) {
-                       $this->addWhere( 'pp_page >=' . intval( $this->params['continue'] ) );
-               }
-
-               if ( $this->params['prop'] ) {
-                       $this->addWhereFld( 'pp_propname', $this->params['prop'] );
+                       $continueValue = intval( $this->params['continue'] );
+                       $filteredPages = array();
+                       foreach ( $pages as $id => $page ) {
+                               if ( $id >= $continueValue ) {
+                                       $filteredPages[$id] = $page;
+                               }
+                       }
+                       $pages = $filteredPages;
                }
 
-               # Force a sort order to ensure that properties are grouped by page
-               # But only if pp_page is not constant in the WHERE clause.
-               if ( count( $pages ) > 1 ) {
-                       $this->addOption( 'ORDER BY', 'pp_page' );
+               if ( !count( $pages ) ) {
+                       # Nothing to do
+                       return;
                }
 
-               $res = $this->select( __METHOD__ );
-               $currentPage = 0; # Id of the page currently processed
+               $pageProps = PageProps::getInstance();
                $props = array();
                $result = $this->getResult();
-
-               foreach ( $res as $row ) {
-                       if ( $currentPage != $row->pp_page ) {
-                               # Different page than previous row, so add the properties to
-                               # the result and save the new page id
-
-                               if ( $currentPage ) {
-                                       if ( !$this->addPageProps( $result, $currentPage, $props ) ) {
-                                               # addPageProps() indicated that the result did not fit
-                                               # so stop adding data. Reset props so that it doesn't
-                                               # get added again after loop exit
-
-                                               $props = array();
-                                               break;
+               if ( $this->params['prop'] ) {
+                       $propnames = $this->params['prop'];
+                       $properties = array();
+                       foreach ( $propnames as $propname ) {
+                               $values = $pageProps->getProperty( $pages, $propname );
+                               foreach ( $values as $page => $value ) {
+                                       if ( !isset( $properties[$page] ) ) {
+                                               $properties[$page] = array();
                                        }
-
-                                       $props = array();
+                                       $properties[$page][$propname] = $value;
                                }
-
-                               $currentPage = $row->pp_page;
                        }
-
-                       $props[$row->pp_propname] = $row->pp_value;
+               } else {
+                       $properties = $pageProps->getProperties( $pages );
                }
-
-               if ( count( $props ) ) {
-                       # Add any remaining properties to the results
-                       $this->addPageProps( $result, $currentPage, $props );
+               foreach ( $properties as $page => $props ) {
+                       $this->addPageProps( $result, $page, $props );
                }
        }
 
index 5426fb8..49d4c8c 100644 (file)
@@ -289,7 +289,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        $this->addFieldsIf( 'rc_user_text', $this->fld_user );
                        $this->addFieldsIf( array( 'rc_minor', 'rc_type', 'rc_bot' ), $this->fld_flags );
                        $this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes );
-                       $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
+                       $this->addFieldsIf( array( 'rc_patrolled', 'rc_log_type' ), $this->fld_patrolled );
                        $this->addFieldsIf(
                                array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ),
                                $this->fld_loginfo
index 75fc33e..ffbd75a 100644 (file)
@@ -105,7 +105,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
                        $this->addFieldsIf( 'rc_user_text', $this->fld_user );
                        $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
-                       $this->addFieldsIf( 'rc_patrolled', $this->fld_patrol );
+                       $this->addFieldsIf( array( 'rc_patrolled', 'rc_log_type' ), $this->fld_patrol );
                        $this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes );
                        $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp );
                        $this->addFieldsIf(
diff --git a/includes/api/i18n/bg.json b/includes/api/i18n/bg.json
new file mode 100644 (file)
index 0000000..7ab62b5
--- /dev/null
@@ -0,0 +1,46 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Vodnokon4e"
+               ]
+       },
+       "apihelp-main-param-action": "Кое действие да се извърши.",
+       "apihelp-block-description": "Блокиране на потребител.",
+       "apihelp-block-param-user": "Потребителско име, IP адрес или диапазон от IP адреси, които искате да блокирате.",
+       "apihelp-block-param-reason": "Причина за блокиране.",
+       "apihelp-block-param-nocreate": "Забрана за създаване на потребителски сметки.",
+       "apihelp-createaccount-description": "Създаване на нова потребителска сметка.",
+       "apihelp-createaccount-param-name": "Потребителско име.",
+       "apihelp-createaccount-param-email": "Адрес на електронна поща на потребителя (незадължително).",
+       "apihelp-createaccount-param-realname": "Истинско име на потребителя (незадължително).",
+       "apihelp-delete-description": "Изтриване на страница.",
+       "apihelp-edit-description": "Създаване и редактиране на страници.",
+       "apihelp-edit-param-text": "Съдържание на страница.",
+       "apihelp-edit-param-minor": "Малка промяна.",
+       "apihelp-edit-param-notminor": "Значителна промяна.",
+       "apihelp-edit-param-bot": "Отбелязване на редакцията като бот.",
+       "apihelp-emailuser-description": "Изпращане на е-писмо до потребител.",
+       "apihelp-emailuser-param-target": "Получател на имейла.",
+       "apihelp-emailuser-param-subject": "Заглавие на тема.",
+       "apihelp-emailuser-param-text": "Съдържание на писмото.",
+       "apihelp-emailuser-param-ccme": "Изпращане на копие от това писмо до мен.",
+       "apihelp-expandtemplates-param-title": "Заглавие на страница.",
+       "apihelp-feedrecentchanges-param-hideminor": "Скриване на малки редакции.",
+       "apihelp-feedrecentchanges-param-hidebots": "Скриване на промени, направени от ботове.",
+       "apihelp-feedrecentchanges-param-hideanons": "Скриване на промени, направени от анонимни потребители.",
+       "apihelp-feedrecentchanges-param-hideliu": "Скриване на промени, направени от регистрирани потребители.",
+       "apihelp-feedrecentchanges-param-hidepatrolled": "Скриване на проверени промени.",
+       "apihelp-feedrecentchanges-param-hidemyself": "Скриване на промените, направени от настоящия потребител.",
+       "apihelp-feedrecentchanges-param-tagfilter": "Филтриране по етикети.",
+       "apihelp-feedrecentchanges-example-simple": "Показване на последни промени.",
+       "apihelp-feedrecentchanges-example-30days": "Показване на последните промени в рамките на 30 дни.",
+       "apihelp-login-param-name": "Потребителско име.",
+       "apihelp-login-param-password": "Парола.",
+       "apihelp-move-description": "Преместване на страница.",
+       "apihelp-move-param-reason": "Причина за преименуването.",
+       "apihelp-move-param-movetalk": "Преименуване на беседата, ако има такава.",
+       "apihelp-move-param-movesubpages": "Преименуване на подстраници, ако е приложимо.",
+       "apihelp-move-param-noredirect": "Не създавай пренасочване.",
+       "apihelp-move-param-ignorewarnings": "Пренебрегване на всякакви предупреждения.",
+       "apihelp-protect-example-protect": "Защита на страница."
+}
index a878f3b..479ae30 100644 (file)
        "apihelp-edit-param-appendtext": "Engadir este texto no final da páxina. Ignorar $1text.\n\nUse $1section=new para engadir unha nova sección, máis que este parámetro.",
        "apihelp-edit-param-undo": "Desfacer esta revisión. Ignorar $1text, $1prependtext e $1appendtext.",
        "apihelp-edit-param-undoafter": "Desfacer tódalas revisións dende $1undo ata esta. Se non está definido, só desfacer unha revisión.",
-       "apihelp-edit-param-redirect": "Resolver redireccións automáticamente",
+       "apihelp-edit-param-redirect": "Resolver redireccións automaticamente",
        "apihelp-edit-param-contentformat": "Formato de serialización de contido utilizado para o texto de entrada.",
        "apihelp-edit-param-contentmodel": "Modelo de contido para o novo contido.",
        "apihelp-edit-param-token": "O identificador debería enviarse empre como o último parámetro, ou polo menos despois do parámetro $1text.",
        "apihelp-feedrecentchanges-param-target": "Mostrar só os cambios nas páxinas ligadas a esta.",
        "apihelp-feedrecentchanges-param-showlinkedto": "Mostrar os cambios nas páxinas ligadas coa páxina seleccionada.",
        "apihelp-feedrecentchanges-param-categories": "Só mostrar cambios en páxinas pertencentes a todas estas categorías.",
+       "apihelp-feedrecentchanges-param-categories_any": "Só mostrar cambios en páxinas pertencentes a calquera das categorías.",
        "apihelp-feedrecentchanges-example-simple": "Mostrar os cambios recentes",
        "apihelp-feedrecentchanges-example-30days": "Mostrar os cambios recentes limitados a 30 días",
        "apihelp-feedwatchlist-description": "Devolve o fluxo dunha lista de vixiancia.",
index 2eb761d..71260d9 100644 (file)
@@ -10,7 +10,8 @@
                        "Macofe",
                        "SPQRobin",
                        "HanV",
-                       "Rangekill"
+                       "Rangekill",
+                       "Robin van der Vliet"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentatie]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> Alle functies die op deze pagina worden weergegeven horen te werken. Aan de API wordt actief gewerkt, en deze kan gewijzigd worden. Abonneer u op  de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over aanpassingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:API:Errors_and_warnings|API: Errors and warnings]] voor meer informatie.",
        "apihelp-main-param-servedby": "Voeg de hostnaam van de server die de aanvraag heeft afgehandeld toe aan het antwoord.",
        "apihelp-main-param-curtimestamp": "Huidige tijd aan het antwoord toevoegen.",
        "apihelp-block-description": "Gebruiker blokkeren.",
+       "apihelp-block-param-user": "Gebruikersnaam, IP-adres of IP-range om te blokkeren.",
        "apihelp-block-param-reason": "Reden voor blokkade.",
+       "apihelp-block-param-nocreate": "Voorkom registeren van accounts.",
        "apihelp-block-param-autoblock": "Blokkeer automatisch het laatst gebruikte IP-adres en ieder volgend IP-adres van waaruit ze proberen aan te melden.",
        "apihelp-block-param-reblock": "De huidige blokkade aanpassen als de gebruiker al geblokkeerd is.",
        "apihelp-createaccount-param-name": "Gebruikersnaam.",
+       "apihelp-createaccount-param-email": "E-mailadres van de gebruikers (optioneel).",
+       "apihelp-createaccount-param-realname": "Echte naam van de gebruiker (optioneel).",
        "apihelp-delete-description": "Verwijder een pagina.",
        "apihelp-delete-example-simple": "Verwijder <kbd>Main Page</kbd>.",
        "apihelp-delete-example-reason": "Verwijder <kbd>Main Page</kbd> met als reden <kbd>Preparing for move</kbd>.",
        "apihelp-disabled-description": "Deze module is uitgeschakeld.",
+       "apihelp-edit-description": "Aanmaken en bewerken van pagina's.",
+       "apihelp-edit-param-sectiontitle": "De titel van de nieuwe sectie.",
        "apihelp-edit-param-text": "Pagina-inhoud.",
        "apihelp-edit-param-minor": "Kleine bewerking.",
        "apihelp-edit-param-notminor": "Geen kleine bewerking.",
        "apihelp-expandtemplates-param-title": "Paginanaam.",
        "apihelp-feedcontributions-param-year": "Van jaar (en eerder).",
        "apihelp-feedcontributions-param-month": "Van maand (en eerder).",
+       "apihelp-feedrecentchanges-param-hideminor": "Kleine wijzigingen verbergen.",
+       "apihelp-feedrecentchanges-param-hidebots": "Wijzigingen gedaan door bots verbergen.",
+       "apihelp-feedrecentchanges-param-hideanons": "Wijzigingen gedaan door anonieme gebruikers verbergen.",
+       "apihelp-feedrecentchanges-param-hideliu": "Wijzigingen gedaan door geregistreerde gebruikers verbergen.",
+       "apihelp-feedrecentchanges-param-hidepatrolled": "Wijzigingen gemarkeerd als gecontroleerd verbergen.",
+       "apihelp-feedrecentchanges-param-hidemyself": "Wijzigingen door de huidige gebruiker verbergen.",
+       "apihelp-feedrecentchanges-param-hidecategorization": "Wijzigingen in categorielidmaatschap verbergen.",
        "apihelp-feedrecentchanges-param-tagfilter": "Filteren op label.",
+       "apihelp-feedrecentchanges-example-simple": "Recente wijzigingen weergeven.",
+       "apihelp-feedrecentchanges-example-30days": "Recente wijzigingen van de afgelopen 30 dagen weergeven.",
        "apihelp-import-param-namespace": "Importeren in deze naamruimte. Can niet samen gebruikt worden met <var>$1rootpage</var>.",
        "apihelp-import-param-rootpage": "Importeren als subpagina van deze pagina. Kan niet samen met <var>$1namespace</var> gebruikt worden.",
        "apihelp-login-param-name": "Gebruikersnaam.",
@@ -60,6 +76,8 @@
        "apihelp-logout-example-logout": "Meldt de huidige gebruiker af.",
        "apihelp-managetags-param-tag": "Label om aan te maken, te activeren of te deactiveren. Voor het aanmaken van een label, mag het niet bestaan. Voor het verwijderen van een label, moet het bestaan. Voor het activeren van een label, moet het bestaan en mag het niet gebruikt worden door een uitbreiding. Voor het deactiveren van een label, moet het gebruikt worden en handmatig gedefinieerd zijn.",
        "apihelp-move-description": "Pagina hernoemen.",
+       "apihelp-move-param-reason": "Reden voor de naamswijziging.",
+       "apihelp-move-param-noredirect": "Geen doorverwijzing achterlaten.",
        "apihelp-move-param-watch": "Pagina en de omleiding toevoegen aan de volglijst van de huidige gebruiker.",
        "apihelp-move-param-unwatch": "Verwijder de pagina en de doorverwijzing van de volglijst van de huidige gebruiker.",
        "apihelp-move-param-watchlist": "De pagina onvoorwaardelijk toevoegen aan of verwijderen van de volglijst van de huidige gebruiker, gebruik voorkeuren of wijzig het volgen niet.",
        "apihelp-parse-example-page": "Een pagina parseren.",
        "apihelp-parse-example-text": "Wikitext parseren.",
        "apihelp-parse-example-summary": "Een samenvatting parseren.",
+       "apihelp-patrol-example-revid": "Een versie markeren als gecontroleerd.",
+       "apihelp-protect-param-reason": "Reden voor opheffen van de beveiliging.",
        "apihelp-protect-example-protect": "Een pagina beveiligen",
        "apihelp-query+alldeletedrevisions-param-tag": "Alleen versies weergeven met dit label.",
        "apihelp-query+allmessages-param-enableparser": "Stel in om de parser in te schakelen, zorgt voor het voorverwerken van de wikitekst van een bericht (vervangen van magische woorden, de afhandeling van sjablonen, enzovoort).",
index 766da01..508a4c4 100644 (file)
        "apihelp-login-example-login": "Войти",
        "apihelp-logout-description": "Выйти и очистить данные сессии.",
        "apihelp-move-description": "Переместить страницу.",
+       "apihelp-move-param-to": "Заголовок, в который следует переименовать страницу.",
        "apihelp-move-param-reason": "Причина переименования.",
        "apihelp-move-param-movetalk": "Переименовать страницу обсуждения, если она есть.",
        "apihelp-move-param-movesubpages": "Переименовать подстраницы, если это применимо.",
        "apihelp-move-param-noredirect": "Не создавать перенаправление.",
+       "apihelp-move-param-watch": "Добавить страницу и перенаправление в список наблюдения текущего пользователя.",
+       "apihelp-move-param-unwatch": "Удалить страницу и перенаправление из списка наблюдения текущего пользователя.",
        "apihelp-move-param-ignorewarnings": "Игнорировать предупреждения",
        "apihelp-opensearch-param-search": "Строка поиска.",
        "apihelp-opensearch-param-limit": "Максимальное число возвращаемых результатов.",
        "apihelp-php-description": "Выходные данные в сериализованном формате PHP.",
        "apihelp-phpfm-description": "Выходные данные в сериализованном формате PHP (pretty-print in HTML).",
        "apihelp-xml-description": "Выходные данные в формате XML.",
-       "apihelp-yaml-description": "Выходные данные в формате yaml.",
        "api-format-title": "Результат MediaWiki API",
        "api-pageset-param-titles": "Список заголовков для работы.",
        "api-pageset-param-pageids": "Список страниц идентификаторов для работы.",
        "api-help-param-type-boolean": "Тип: двоичный ([[Special:ApiHelp/main#main/datatypes|details]])",
        "api-help-param-type-timestamp": "Тип: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatypes|allowed formats]])",
        "api-help-param-type-user": "Тип: {{PLURAL:$1|1=user name|2=list of user names}}",
-       "api-help-param-list": "{{PLURAL:$1|1=One value|2=Values (separate with <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Одно из следующих значений|2=Значения (разделённые <kbd>{{!}}</kbd>)}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Должен быть пустым|может быть пустым, или $2}}",
        "api-help-param-limit": "Не более чем $1 разрешено.",
        "api-help-param-limit2": "Разрешено не более чем $1 ($2 для ботов).",
index 36c644a..16f11ee 100644 (file)
@@ -513,7 +513,7 @@ class RequestContext implements IContextSource, MutableContext {
                return array(
                        'ip' => $this->getRequest()->getIP(),
                        'headers' => $this->getRequest()->getAllHeaders(),
-                       'sessionId' => session_id(),
+                       'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
                        'userId' => $this->getUser()->getId()
                );
        }
@@ -541,7 +541,9 @@ class RequestContext implements IContextSource, MutableContext {
         * @since 1.21
         */
        public static function importScopedSession( array $params ) {
-               if ( session_id() != '' && strlen( $params['sessionId'] ) ) {
+               if ( strlen( $params['sessionId'] ) &&
+                       MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
+               ) {
                        // Sanity check to avoid sending random cookies for the wrong users.
                        // This method should only called by CLI scripts or by HTTP job runners.
                        throw new MWException( "Sessions can only be imported when none is active." );
@@ -563,23 +565,37 @@ class RequestContext implements IContextSource, MutableContext {
                        global $wgRequest, $wgUser;
 
                        $context = RequestContext::getMain();
+
                        // Commit and close any current session
-                       session_write_close(); // persist
-                       session_id( '' ); // detach
-                       $_SESSION = array(); // clear in-memory array
-                       // Remove any user IP or agent information
-                       $context->setRequest( new FauxRequest() );
+                       if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
+                               session_write_close(); // persist
+                               session_id( '' ); // detach
+                               $_SESSION = array(); // clear in-memory array
+                       }
+
+                       // Get new session, if applicable
+                       $session = null;
+                       if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
+                               $session = MediaWiki\Session\SessionManager::singleton()
+                                       ->getSessionById( $params['sessionId'] );
+                       }
+
+                       // Remove any user IP or agent information, and attach the request
+                       // with the new session.
+                       $context->setRequest( new FauxRequest( array(), false, $session ) );
                        $wgRequest = $context->getRequest(); // b/c
+
                        // Now that all private information is detached from the user, it should
                        // be safe to load the new user. If errors occur or an exception is thrown
                        // and caught (leaving the main context in a mixed state), there is no risk
                        // of the User object being attached to the wrong IP, headers, or session.
                        $context->setUser( $user );
                        $wgUser = $context->getUser(); // b/c
-                       if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
-                               wfSetupSession( $params['sessionId'] ); // sets $_SESSION
+                       if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
+                               session_id( $session->getId() );
+                               MediaWiki\quietCall( 'session_start' );
                        }
-                       $request = new FauxRequest( array(), false, $_SESSION );
+                       $request = new FauxRequest( array(), false, $session );
                        $request->setIP( $params['ip'] );
                        foreach ( $params['headers'] as $name => $value ) {
                                $request->setHeader( $name, $value );
index 37c9d6f..c58e9bd 100644 (file)
@@ -282,9 +282,19 @@ abstract class DatabaseBase implements IDatabase {
         * @return TransactionProfiler
         */
        protected function getTransactionProfiler() {
-               return $this->trxProfiler
-                       ? $this->trxProfiler
-                       : Profiler::instance()->getTransactionProfiler();
+               if ( !$this->trxProfiler ) {
+                       $this->trxProfiler = new TransactionProfiler();
+               }
+
+               return $this->trxProfiler;
+       }
+
+       /**
+        * @param TransactionProfiler $profiler
+        * @since 1.27
+        */
+       public function setTransactionProfiler( TransactionProfiler $profiler ) {
+               $this->trxProfiler = $profiler;
        }
 
        /**
@@ -2675,14 +2685,10 @@ abstract class DatabaseBase implements IDatabase {
                        if ( !$this->mTrxLevel ) {
                                wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
                                return; // nothing to do
-                       } elseif ( $this->mTrxAutomatic ) {
-                               wfWarn( "$fname: Explicit rollback of implicit transaction. Something may be out of sync!" );
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
                                return; // nothing to do
-                       } elseif ( !$this->mTrxAutomatic ) {
-                               wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" );
                        }
                }
 
index 5bcfd66..3a8f737 100644 (file)
@@ -654,6 +654,13 @@ abstract class DatabaseMysqlBase extends Database {
        protected function getLagFromPtHeartbeat() {
                $masterInfo = $this->getMasterServerInfo();
                if ( !$masterInfo ) {
+                       wfLogDBError(
+                               "Unable to query master of {db_server} for server ID",
+                               $this->getLogContext( array(
+                                       'method' => __METHOD__
+                               ) )
+                       );
+
                        return false; // could not get master server ID
                }
 
@@ -666,6 +673,13 @@ abstract class DatabaseMysqlBase extends Database {
                        return max( $nowUnix - $timeUnix, 0.0 );
                }
 
+               wfLogDBError(
+                       "Unable to find pt-heartbeat row for {db_server}",
+                       $this->getLogContext( array(
+                               'method' => __METHOD__
+                       ) )
+               );
+
                return false;
        }
 
index 4aed718..4a13522 100644 (file)
@@ -28,6 +28,8 @@
 abstract class LBFactory {
        /** @var ChronologyProtector */
        protected $chronProt;
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
 
        /** @var LBFactory */
        private static $instance;
@@ -47,6 +49,7 @@ abstract class LBFactory {
                }
 
                $this->chronProt = $this->newChronologyProtector();
+               $this->trxProfiler = Profiler::instance()->getTransactionProfiler();
        }
 
        /**
index e25499c..39be996 100644 (file)
@@ -300,7 +300,8 @@ class LBFactoryMulti extends LBFactory {
                return new LoadBalancer( array(
                        'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
                        'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $readOnlyReason
+                       'readOnlyReason' => $readOnlyReason,
+                       'trxProfiler' => $this->trxProfiler
                ) );
        }
 
index 3349e1f..53b0310 100644 (file)
@@ -87,7 +87,8 @@ class LBFactorySimple extends LBFactory {
                return new LoadBalancer( array(
                        'servers' => $servers,
                        'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $this->readOnlyReason
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'trxProfiler' => $this->trxProfiler
                ) );
        }
 
@@ -120,7 +121,8 @@ class LBFactorySimple extends LBFactory {
                return new LoadBalancer( array(
                        'servers' => $wgExternalServers[$cluster],
                        'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $this->readOnlyReason
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'trxProfiler' => $this->trxProfiler
                ) );
        }
 
index 5a6cfa7..cbe9517 100644 (file)
@@ -35,8 +35,10 @@ class LBFactorySingle extends LBFactory {
        public function __construct( array $conf ) {
                parent::__construct( $conf );
 
-               $conf['readOnlyReason'] = $this->readOnlyReason;
-               $this->lb = new LoadBalancerSingle( $conf );
+               $this->lb = new LoadBalancerSingle( array(
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'trxProfiler' => $this->trxProfiler
+               ) + $conf );
        }
 
        /**
@@ -103,7 +105,8 @@ class LoadBalancerSingle extends LoadBalancer {
                                        'dbname' => $this->db->getDBname(),
                                        'load' => 1,
                                )
-                       )
+                       ),
+                       'trxProfiler' => $this->trxProfiler
                ) );
 
                if ( isset( $params['readOnlyReason'] ) ) {
index 5d1b745..3fe9638 100644 (file)
@@ -40,9 +40,9 @@ class LoadBalancer {
        private $mAllowLagged;
        /** @var integer Seconds to spend waiting on slave lag to resolve */
        private $mWaitTimeout;
-
        /** @var array LBFactory information */
        private $mParentInfo;
+
        /** @var string The LoadMonitor subclass name */
        private $mLoadMonitorClass;
        /** @var LoadMonitor */
@@ -67,6 +67,9 @@ class LoadBalancer {
        /** @var integer Total connections opened */
        private $connsOpened = 0;
 
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
+
        /** @var integer Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
        /** @var integer Default 'max lag' when unspecified */
@@ -127,6 +130,12 @@ class LoadBalancer {
                }
 
                $this->srvCache = ObjectCache::getLocalServerInstance();
+
+               if ( isset( $params['trxProfiler'] ) ) {
+                       $this->trxProfiler = $params['trxProfiler'];
+               } else {
+                       $this->trxProfiler = new TransactionProfiler();
+               }
        }
 
        /**
@@ -181,11 +190,13 @@ class LoadBalancer {
                                if ( isset( $this->mServers[$i]['max lag'] ) ) {
                                        $maxServerLag = min( $maxServerLag, $this->mServers[$i]['max lag'] );
                                }
+
+                               $host = $this->getServerName( $i );
                                if ( $lag === false ) {
-                                       wfDebugLog( 'replication', "Server #$i is not replicating" );
+                                       wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
                                        unset( $loads[$i] );
                                } elseif ( $lag > $maxServerLag ) {
-                                       wfDebugLog( 'replication', "Server #$i is excessively lagged ($lag seconds)" );
+                                       wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
                                        unset( $loads[$i] );
                                }
                        }
@@ -670,9 +681,7 @@ class LoadBalancer {
         *
         * @param int $i Server index
         * @param string|bool $wiki Wiki ID, or false for the current wiki
-        * @return DatabaseBase
-        *
-        * @access private
+        * @return DatabaseBase|bool Returns false on errors
         */
        public function openConnection( $i, $wiki = false ) {
                if ( $wiki !== false ) {
@@ -840,6 +849,7 @@ class LoadBalancer {
                $db->setLazyMasterHandle(
                        $this->getLazyConnectionRef( DB_MASTER, array(), $db->getWikiID() )
                );
+               $db->setTransactionProfiler( $this->trxProfiler );
 
                return $db;
        }
index 31f6163..e251501 100644 (file)
@@ -88,18 +88,33 @@ class LoadMonitorMySQL implements LoadMonitor {
 
                $lagTimes = array();
                foreach ( $serverIndexes as $i ) {
-                       if ( $i == 0 ) { # Master
-                               $lagTimes[$i] = 0;
+                       if ( $i == $this->parent->getWriterIndex() ) {
+                               $lagTimes[$i] = 0; // master always has no lag
                                continue;
                        }
+
                        $conn = $this->parent->getAnyOpenConnection( $i );
-                       if ( $conn !== false ) {
-                               $lagTimes[$i] = $conn->getLag();
+                       if ( $conn ) {
+                               $close = false; // already open
+                       } else {
+                               $conn = $this->parent->openConnection( $i, $wiki );
+                               $close = true; // new connection
+                       }
+
+                       if ( !$conn ) {
+                               $lagTimes[$i] = false;
+                               $host = $this->parent->getServerName( $i );
+                               wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is unreachable" );
                                continue;
                        }
-                       $conn = $this->parent->openConnection( $i, $wiki );
-                       if ( $conn !== false ) {
-                               $lagTimes[$i] = $conn->getLag();
+
+                       $lagTimes[$i] = $conn->getLag();
+                       if ( $lagTimes[$i] === false ) {
+                               $host = $this->parent->getServerName( $i );
+                               wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is not replicating?" );
+                       }
+
+                       if ( $close ) {
                                # Close the connection to avoid sleeper connections piling up.
                                # Note that the caller will pick one of these DBs and reconnect,
                                # which is slightly inefficient, but this only matters for the lag
@@ -124,7 +139,10 @@ class LoadMonitorMySQL implements LoadMonitor {
        }
 
        private function getLagTimeCacheKey() {
+               $writerIndex = $this->parent->getWriterIndex();
                // Lag is per-server, not per-DB, so key on the master DB name
-               return $this->srvCache->makeGlobalKey( 'lag-times', $this->parent->getServerName( 0 ) );
+               return $this->srvCache->makeGlobalKey(
+                       'lag-times', $this->parent->getServerName( $writerIndex )
+               );
        }
 }
diff --git a/includes/deferred/AtomicSectionUpdate.php b/includes/deferred/AtomicSectionUpdate.php
new file mode 100644 (file)
index 0000000..a9921b3
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
+ * @since 1.27
+ */
+class AtomicSectionUpdate implements DeferrableUpdate {
+       /** @var IDatabase */
+       private $dbw;
+       /** @var string */
+       private $fname;
+       /** @var Closure|callable */
+       private $callback;
+
+       /**
+        * @param IDatabase $dbw
+        * @param string $fname Caller name (usually __METHOD__)
+        * @param callable $callback
+        * @throws InvalidArgumentException
+        * @see IDatabase::doAtomicSection()
+        */
+       public function __construct( IDatabase $dbw, $fname, $callback ) {
+               $this->dbw = $dbw;
+               $this->fname = $fname;
+               if ( !is_callable( $callback ) ) {
+                       throw new InvalidArgumentException( 'Not a valid callback/closure!' );
+               }
+               $this->callback = $callback;
+       }
+
+       public function doUpdate() {
+               $this->dbw->doAtomicSection( $this->fname, $this->callback );
+       }
+}
index 15007af..8ba2745 100644 (file)
@@ -603,16 +603,6 @@ class FileRepo {
                return array();
        }
 
-       /**
-        * Get the public root URL of the repository
-        *
-        * @deprecated since 1.20
-        * @return string
-        */
-       public function getRootUrl() {
-               return $this->getZoneUrl( 'public' );
-       }
-
        /**
         * Get the URL of thumb.php
         *
index debdeb5..a8d37a1 100644 (file)
@@ -218,7 +218,11 @@ class ForeignAPIRepo extends FileRepo {
                if ( $data && isset( $data['query']['pages'] ) ) {
                        foreach ( $data['query']['pages'] as $info ) {
                                if ( isset( $info['imageinfo'][0] ) ) {
-                                       return $info['imageinfo'][0];
+                                       $return = $info['imageinfo'][0];
+                                       if ( isset( $info['pageid'] ) ) {
+                                               $return['pageid'] = $info['pageid'];
+                                       }
+                                       return $return;
                                }
                        }
                }
index 72f12d1..8a3900e 100644 (file)
@@ -354,6 +354,16 @@ abstract class File implements IDBAccessObject {
                return $this->url;
        }
 
+       /*
+        * Get short description URL for a files based on the page ID
+        *
+        * @return string|null
+        * @since 1.27
+        */
+       public function getDescriptionShortUrl() {
+               return null;
+       }
+
        /**
         * Return a fully-qualified URL to the file.
         * Upload URL paths _may or may not_ be fully qualified, so
index 26ea38b..43cb5a5 100644 (file)
@@ -218,6 +218,25 @@ class ForeignAPIFile extends File {
                return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
        }
 
+       /**
+        * Get short description URL for a file based on the foreign API response,
+        * or if unavailable, the short URL is constructed from the foreign page ID.
+        *
+        * @return null|string
+        * @since 1.27
+        */
+       public function getDescriptionShortUrl() {
+               if ( isset( $this->mInfo['descriptionshorturl'] ) ) {
+                       return $this->mInfo['descriptionshorturl'];
+               } elseif ( isset( $this->mInfo['pageid'] ) ) {
+                       $url = $this->repo->makeUrl( array( 'curid' => $this->mInfo['pageid'] ) );
+                       if ( $url !== false ) {
+                               return $url;
+                       }
+               }
+               return null;
+       }
+
        /**
         * @param string $type
         * @return int|null|string
index 561ead7..611ae10 100644 (file)
@@ -127,4 +127,28 @@ class ForeignDBFile extends LocalFile {
                // Restore remote behavior
                return File::getDescriptionText( $lang );
        }
+
+       /**
+        * Get short description URL for a file based on the page ID.
+        *
+        * @return string
+        * @throws DBUnexpectedError
+        * @since 1.27
+        */
+       public function getDescriptionShortUrl() {
+               $dbr = $this->repo->getSlaveDB();
+               $pageId = $dbr->selectField( 'page', 'page_id', array(
+                       'page_namespace' => NS_FILE,
+                       'page_title' => $this->title->getDBkey()
+               ) );
+
+               if ( $pageId !== false ) {
+                       $url = $this->repo->makeUrl( array( 'curid' => $pageId ) );
+                       if ( $url !== false ) {
+                               return $url;
+                       }
+               }
+               return null;
+       }
+
 }
index b7d6f98..9e214f6 100644 (file)
@@ -757,6 +757,25 @@ class LocalFile extends File {
                }
        }
 
+       /**
+        * Get short description URL for a file based on the page ID.
+        *
+        * @return string|null
+        * @throws MWException
+        * @since 1.27
+        */
+       public function getDescriptionShortUrl() {
+               $pageId = $this->title->getArticleID();
+
+               if ( $pageId !== null ) {
+                       $url = $this->repo->makeUrl( array( 'curid' => $pageId ) );
+                       if ( $url !== false ) {
+                               return $url;
+                       }
+               }
+               return null;
+       }
+
        /**
         * Get handler-specific metadata
         * @return string
index ea700c7..de84199 100644 (file)
@@ -169,6 +169,7 @@ abstract class Installer {
                'wgEnotifUserTalk',
                'wgEnotifWatchlist',
                'wgEmailAuthentication',
+               'wgDBname',
                'wgDBtype',
                'wgDiff3',
                'wgImageMagickConvertCommand',
index add600d..4813bea 100644 (file)
@@ -278,6 +278,7 @@ class MysqlUpdater extends DatabaseUpdater {
                        // 1.27
                        array( 'dropTable', 'msg_resource_links' ),
                        array( 'dropTable', 'msg_resource' ),
+                       array( 'addTable', 'bot_passwords', 'patch-bot_passwords.sql' ),
                );
        }
 
index 7880557..21d5dbc 100644 (file)
@@ -89,6 +89,7 @@ class PostgresUpdater extends DatabaseUpdater {
                        array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
                        array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql' ),
                        array( 'addTable', 'sites', 'patch-sites.sql' ),
+                       array( 'addTable', 'bot_passwords', 'patch-bot_passwords.sql' ),
 
                        # Needed before new field
                        array( 'convertArchive2' ),
index 5279c2d..ba223c4 100644 (file)
@@ -147,6 +147,7 @@ class SqliteUpdater extends DatabaseUpdater {
                        // 1.27
                        array( 'dropTable', 'msg_resource_links' ),
                        array( 'dropTable', 'msg_resource' ),
+                       array( 'addTable', 'bot_passwords', 'patch-bot_passwords.sql' ),
                );
        }
 
index 27a5449..f455ced 100644 (file)
        "config-oracle-def-ts": "Таблично пространство по подразбиране:",
        "config-oracle-temp-ts": "Временно таблично пространство:",
        "config-type-mysql": "MySQL (или съвместима)",
-       "config-type-mssql": "Microsoft SQL Сървър",
+       "config-type-mssql": "Microsoft SQL сървър",
        "config-support-info": "МедияУики поддържа следните системи за бази от данни:\n\n$1\n\nАко не виждате желаната за използване система в списъка по-долу, следвайте инструкциите за активиране на поддръжка по-горе.",
        "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] е най-важна за МедияУики и се поддържа най-добре. МедияУики работи също така с [{{int:version-db-mariadb-url}} MariaDB] и [{{int:version-db-percona-url}} Percona Server], които са съвместими с MySQL.\n([http://www.php.net/manual/bg/mysqli.installation.php Как се компилира PHP с поддръжка на MySQL])",
        "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] е популярна система за управление на бази от данни, алтернатива на MySQL. Възможно е все още да има грешки, затова не се препоръчва да се използва в общодостъпна среда.([http://www.php.net/manual/bg/pgsql.installation.php Как се компилира PHP с поддръжка на PostgreSQL])",
        "config-header-postgres": "Настройки за PostgreSQL",
        "config-header-sqlite": "Настройки за SQLite",
        "config-header-oracle": "Настройки за Oracle",
-       "config-header-mssql": "Настройки за Microsoft SQL Сървър",
+       "config-header-mssql": "Настройки за Microsoft SQL сървър",
        "config-invalid-db-type": "Невалиден тип база от данни",
        "config-missing-db-name": "Необходимо е да се въведе стойност за „{{int:config-db-name}}“.",
        "config-missing-db-host": "Необходимо е да се въведе стойност за „{{int:config-db-host}}“.",
        "config-ns-invalid": "Посоченото именно пространство \"<nowiki>$1</nowiki>\" е невалидно.\nНеобходимо е да бъде посочено друго.",
        "config-ns-conflict": "Посоченото именно пространство \"<nowiki>$1</nowiki>\" е в конфликт с използваното по подразбиране именно пространство MediaWiki.\nНеобходимо е да се посочи друго именно пространство.",
        "config-admin-box": "Администраторска сметка",
-       "config-admin-name": "Ð\9fотребителско име:",
+       "config-admin-name": "Ð\92аÑ\88еÑ\82о Ð¿отребителско име:",
        "config-admin-password": "Парола:",
        "config-admin-password-confirm": "Парола (повторно):",
        "config-admin-help": "Въвежда се предпочитаното потребителско име, например \"Иванчо Иванчев\".\nТова ще е потребителското име, което администраторът ще използва за влизане в уикито.",
        "config-upload-help": "Качването на файлове е възможно да доведе до пробели със сигурността на сървъра.\nПовече информация по темата има в [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security раздела за сигурност] в Наръчника.\n\nЗа позволяване качването на файлове, необходимо е уебсървърът да може да записва в поддиректорията на МедияУики <code>images</code>.\nСлед като това условие е изпълнено, функционалността може да бъде активирана.",
        "config-upload-deleted": "Директория за изтритите файлове:",
        "config-upload-deleted-help": "Избиране на директория, в която ще се складират изтритите файлове.\nВ най-добрия случай тя не трябва да е достъпна през уеб.",
-       "config-logo": "Адрес на логото:",
+       "config-logo": "URL адрес на логото:",
        "config-logo-help": "Обликът по подразбиране на МедияУики вклчва място с размери 135х160 пиксела за лого над страничното меню.\nАко има наличен файл с подходящ размер, неговият адрес може да бъде посочен тук.\n\nМоже да се използва <code>$wgStylePath</code> или <code>$wgScriptPath</code> ако логото е относително към тези пътища.\n\nАко не е необходимо лого, полето може да се остави празно.",
        "config-instantcommons": "Включване на Instant Commons",
        "config-instantcommons-help": "[//www.mediawiki.org/wiki/InstantCommons Instant Commons] е функционалност, която позволява на уикитата да използват картинки, звуци и друга медиа от сайта на Уикимедия [//commons.wikimedia.org/ Общомедия].\nЗа да е възможно това, МедияУики изисква достъп до Интернет.\n\nПовече информация за тази функционалност, както и инструкции за настройване за други уикита, различни от Общомедия, е налична в [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos наръчника].",
index c2889bf..c4fc0cd 100644 (file)
@@ -80,6 +80,7 @@
        "config-apc": "[http://www.php.net/apc APC] نصب شده‌است.",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] نصب شده‌است.",
        "config-no-cache": "'''هشدار:''' [http://www.php.net/apc APC],[http://xcache.lighttpd.net/ XCache] یا [http://www.iis.net/download/WinCacheForPhp WinCache] را نتوانست پیدا کند.\nذخیره شی فعال نیست.",
+       "config-no-cache-apcu": "<strong>هشدار:</strong> پیوند [http://www.php.net/apcu APCu]، [http://xcache.lighttpd.net/ XCache] یا [http://www.iis.net/download/WinCacheForPhp WinCache] یافت نشد. ذخیره شی فعال نیست.",
        "config-mod-security": "'''هشدار:''' وب سرور شما [http://modsecurity.org/ mod_security] فعال است.اگر اشتباه پیکربندی شده‌‌ باشد،می تواند باعث ایجاد مشکلاتی برای مدیاویکی یا دیگر نرم‌افزاری شود که به کاربران اجازه می‌دهد پیام دلخواه ارسال کنند.\nبه [http://modsecurity.org/documentation/ mod_security documentation] مراجعه کنید یا اگر با خطاهای اتفاقی مواجه شدید با پشتیبانی میزبان خود در تماس باشید.",
        "config-diff3-bad": "جی‌ان‌یو دیف۳ پیدا نشد.",
        "config-git": "کنترل نسخهٔ نرم‌افزار گیت پیدا شد: <code>$1</code>.",
index 643a370..9458a8f 100644 (file)
@@ -74,6 +74,7 @@
        "config-apc": "[http://www.php.net/apc APC] está instalado",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
        "config-no-cache": "<strong>Atención:</strong> Non se puido atopar [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nA caché de obxectos está desactivada.",
+       "config-no-cache-apcu": "<strong>Advertencia:</strong> Non se puido atopar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nA caché de obxectos non está activada.",
        "config-mod-security": "<strong>Atención:</strong> O seu servidor web ten o [http://modsecurity.org/ mod_security] activado. Se estivese mal configurado, pode causar problemas a MediaWiki ou calquera outro software que permita aos usuarios publicar contidos arbitrarios.\nOlle a [http://modsecurity.org/documentation/ documentación do mod_security] ou póñase en contacto co soporte do seu servidor se atopa erros aleatorios.",
        "config-diff3-bad": "GNU diff3 non se atopou.",
        "config-git": "Atopouse o software de control da versión de Git: <code>$1</code>.",
index 7ed7067..572f3af 100644 (file)
@@ -85,6 +85,7 @@
        "config-apc": "[http://www.php.net/apc APC] è installato",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] è installato",
        "config-no-cache": "'''Attenzione:''' [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] non sono stati trovati.\nLa caching degli oggetti non è attivata.",
+       "config-no-cache-apcu": "'''Attenzione:''' [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] non sono stati trovati.\nLa caching degli oggetti non è attivata.",
        "config-mod-security": "<strong>Attenzione:</strong> Il tuo server web ha il [http://modsecurity.org/ mod_security] abilitato. Se non correttamente configurato, può creare problemi a MediaWiki o ad altro software che permette agli utenti di pubblicare contenuto.\nFai riferimento alla [http://modsecurity.org/documentation/ documentazione sul mod_security] o contatta il supporto tecnico del tuo provider di hosting se si verificano errori.",
        "config-diff3-bad": "GNU diff3 non trovato.",
        "config-git": "Trovato software di controllo della versione Git: <code>$1</code>.",
index b7b11c3..734b747 100644 (file)
                ]
        },
        "config-desc": "Het installatieprogramma voor MediaWiki",
-       "config-title": "Installatie MediaWiki $1",
+       "config-title": "Installatie van MediaWiki $1",
        "config-information": "Gegevens",
        "config-localsettings-upgrade": "Er is een bestaand instellingenbestand <code>LocalSettings.php</code> gevonden.\nVoer de waarde van <code>$wgUpgradeKey</code> in in onderstaande invoerveld om deze installatie bij te werken.\nDe instelling is terug te vinden in <code>LocalSettings.php</code>.",
        "config-localsettings-cli-upgrade": "Het bestand <code>LocalSettings.php</code> is al aanwezig.\nVoer <code>update.php</code> uit om deze installatie bij te werken.",
        "config-localsettings-key": "Upgradesleutel:",
-       "config-localsettings-badkey": "De sleutel die u hebt opgegeven is onjuist",
+       "config-localsettings-badkey": "De sleutel die u hebt opgegeven is onjuist.",
        "config-upgrade-key-missing": "Er is een bestaande installatie van MediaWiki aangetroffen.\nPlaats de volgende regel onderaan uw <code>LocalSettings.php</code> om deze installatie bij te werken:\n\n$1",
        "config-localsettings-incomplete": "De bestaande inhoud van <code>LocalSettings.php</code> lijkt incompleet.\nDe variabele $1 is niet ingesteld.\nWijzig <code>LocalSettings.php</code> zodat deze variabele is ingesteld en klik op \"{{int:Config-continue}}\".",
        "config-localsettings-connection-error": "Er is een fout opgetreden tijdens het verbinden met de database met de instellingen uit <code>LocalSettings.php</code>. Los het probleem met de instellingen op en probeer het daarna opnieuw.\n\n$1",
        "config-nofile": "Het bestand \"$1\" is niet gevonden. Is het verwijderd?",
        "config-extension-link": "Weet u dat u [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions uitbreidingen] kunt gebruiken voor uw wiki?\nU kunt [//www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category uitbreidingen op categorie] bekijken of ga naar de [//www.mediawiki.org/wiki/Extension_Matrix uitbreidingenmatrix] om de volledige lijst met uitbreidingen te bekijken.",
        "mainpagetext": "'''De installatie van MediaWiki is geslaagd.'''",
-       "mainpagedocfooter": "Raadpleeg de [//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Contents handleiding] voor informatie over het gebruik van de wikisoftware.\n\n== Meer hulp over MediaWiki ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lijst met instellingen]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen (FAQ)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Maak MediaWiki beschikbaar in uw taal]"
+       "mainpagedocfooter": "Raadpleeg de [//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Contents handleiding] voor informatie over het gebruik van de wikisoftware.\n\n== Meer hulp over MediaWiki ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lijst met instellingen]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen (FAQ)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Maak MediaWiki beschikbaar in uw taal]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Leer hoe u spam kunt voorkomen op uw wiki]"
 }
diff --git a/includes/jobqueue/JobQueueMemory.php b/includes/jobqueue/JobQueueMemory.php
new file mode 100644 (file)
index 0000000..d9b30c7
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+/**
+ * PHP memory-backed job queue code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to handle job queues stored in PHP memory for testing
+ *
+ * JobQueueGroup does not remember every queue instance, so statically track it here
+ *
+ * @ingroup JobQueue
+ * @since 1.27
+ */
+class JobQueueMemory extends JobQueue {
+       /** @var array[] */
+       protected static $data = array();
+
+       protected function doBatchPush( array $jobs, $flags ) {
+               $unclaimed =& $this->getQueueData( 'unclaimed', array() );
+
+               /** @var IJobSpecification[] $jobs */
+               foreach ( $jobs as $job ) {
+                       if ( $job->ignoreDuplicates() ) {
+                               $sha1 = Wikimedia\base_convert(
+                                       sha1( serialize( $job->getDeduplicationInfo() ) ),
+                                       16, 36, 31
+                               );
+                               if ( !isset( $unclaimed[$sha1] ) ) {
+                                       $unclaimed[$sha1] = $job;
+                               }
+                       } else {
+                               $unclaimed[] = $job;
+                       }
+               }
+       }
+
+       protected function supportedOrders() {
+               return array( 'random', 'timestamp', 'fifo' );
+       }
+
+       protected function optimalOrder() {
+               return 'fifo';
+       }
+
+       protected function doIsEmpty() {
+               return ( $this->doGetSize() == 0 );
+       }
+
+       protected function doGetSize() {
+               $unclaimed = $this->getQueueData( 'unclaimed' );
+
+               return $unclaimed ? count( $unclaimed ) : 0;
+       }
+
+       protected function doGetAcquiredCount() {
+               $claimed = $this->getQueueData( 'claimed' );
+
+               return $claimed ? count( $claimed ) : 0;
+       }
+
+       protected function doPop() {
+               if ( $this->doGetSize() == 0 ) {
+                       return false;
+               }
+
+               $unclaimed =& $this->getQueueData( 'unclaimed' );
+               $claimed =& $this->getQueueData( 'claimed', array() );
+
+               if ( $this->order === 'random' ) {
+                       $key = array_rand( $unclaimed );
+               } else {
+                       reset( $unclaimed );
+                       $key = key( $unclaimed );
+               }
+
+               $spec = $unclaimed[$key];
+               unset( $unclaimed[$key] );
+               $claimed[] = $spec;
+
+               $job = $this->jobFromSpecInternal( $spec );
+
+               end( $claimed );
+               $job->metadata['claimId'] = key( $claimed );
+
+               return $job;
+       }
+
+       protected function doAck( Job $job ) {
+               if ( $this->getAcquiredCount() == 0 ) {
+                       return;
+               }
+
+               $claimed =& $this->getQueueData( 'claimed' );
+               unset( $claimed[$job->metadata['claimId']] );
+       }
+
+       protected function doDelete() {
+               if ( isset( self::$data[$this->type][$this->wiki] ) ) {
+                       unset( self::$data[$this->type][$this->wiki] );
+                       if ( !self::$data[$this->type] ) {
+                               unset( self::$data[$this->type] );
+                       }
+               }
+       }
+
+       public function getAllQueuedJobs() {
+               $unclaimed = $this->getQueueData( 'unclaimed' );
+               if ( !$unclaimed ) {
+                       return new ArrayIterator( array() );
+               }
+
+               $that = $this;
+               return new MappedIterator(
+                       $unclaimed,
+                       function ( $value ) use ( $that ) {
+                               $that->jobFromSpecInternal( $value );
+                       }
+               );
+       }
+
+       public function getAllAcquiredJobs() {
+               $claimed = $this->getQueueData( 'claimed' );
+               if ( !$claimed ) {
+                       return new ArrayIterator( array() );
+               }
+
+               $that = $this;
+               return new MappedIterator(
+                       $claimed,
+                       function ( $value ) use ( $that ) {
+                               $that->jobFromSpecInternal( $value );
+                       }
+               );
+       }
+
+       public function jobFromSpecInternal( IJobSpecification $spec ) {
+               return Job::factory( $spec->getType(), $spec->getTitle(), $spec->getParams() );
+       }
+
+       private function &getQueueData( $field, $init = null ) {
+               if ( !isset( self::$data[$this->type][$this->wiki][$field] ) ) {
+                       if ( $init !== null ) {
+                               self::$data[$this->type][$this->wiki][$field] = $init;
+                       } else {
+                               return $init;
+                       }
+               }
+
+               return self::$data[$this->type][$this->wiki][$field];
+       }
+}
index 6d2ce0e..4ab9f5a 100644 (file)
@@ -40,6 +40,10 @@ class JobRunner implements LoggerAwareInterface {
         */
        protected $logger;
 
+       const MAX_ALLOWED_LAG = 3; // abort if more than this much DB lag is present
+       const LAG_CHECK_PERIOD = 1.0; // check slave lag this many seconds
+       const ERROR_BACKOFF_TTL = 1; // seconds to back off a queue due to errors
+
        /**
         * @param callable $debug Optional debug output handler
         */
@@ -98,47 +102,42 @@ class JobRunner implements LoggerAwareInterface {
                $maxTime = isset( $options['maxTime'] ) ? $options['maxTime'] : false;
                $noThrottle = isset( $options['throttle'] ) && !$options['throttle'];
 
+               // Bail if job type is invalid
                if ( $type !== false && !isset( $wgJobClasses[$type] ) ) {
                        $response['reached'] = 'none-possible';
                        return $response;
                }
-
-               // Bail out if in read-only mode
+               // Bail out if DB is in read-only mode
                if ( wfReadOnly() ) {
                        $response['reached'] = 'read-only';
                        return $response;
                }
-
-               // Catch huge single updates that lead to slave lag
-               $trxProfiler = Profiler::instance()->getTransactionProfiler();
-               $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
-               $trxProfiler->setExpectations( $wgTrxProfilerLimits['JobRunner'], __METHOD__ );
-
                // Bail out if there is too much DB lag.
                // This check should not block as we want to try other wiki queues.
-               $maxAllowedLag = 3;
                list( , $maxLag ) = wfGetLB( wfWikiID() )->getMaxLag();
-               if ( $maxLag >= $maxAllowedLag ) {
+               if ( $maxLag >= self::MAX_ALLOWED_LAG ) {
                        $response['reached'] = 'slave-lag-limit';
                        return $response;
                }
 
-               $group = JobQueueGroup::singleton();
-
                // Flush any pending DB writes for sanity
                wfGetLBFactory()->commitAll( __METHOD__ );
 
+               // Catch huge single updates that lead to slave lag
+               $trxProfiler = Profiler::instance()->getTransactionProfiler();
+               $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
+               $trxProfiler->setExpectations( $wgTrxProfilerLimits['JobRunner'], __METHOD__ );
+
                // Some jobs types should not run until a certain timestamp
                $backoffs = array(); // map of (type => UNIX expiry)
                $backoffDeltas = array(); // map of (type => seconds)
                $wait = 'wait'; // block to read backoffs the first time
 
+               $group = JobQueueGroup::singleton();
                $stats = RequestContext::getMain()->getStats();
                $jobsPopped = 0;
                $timeMsTotal = 0;
-               $flags = JobQueueGroup::USE_CACHE;
                $startTime = microtime( true ); // time since jobs started running
-               $checkLagPeriod = 1.0; // check slave lag this many seconds
                $lastCheckTime = 1; // timestamp of last slave check
                do {
                        // Sync the persistent backoffs with concurrent runners
@@ -147,7 +146,11 @@ class JobRunner implements LoggerAwareInterface {
                        $wait = 'nowait'; // less important now
 
                        if ( $type === false ) {
-                               $job = $group->pop( JobQueueGroup::TYPE_DEFAULT, $flags, $blacklist );
+                               $job = $group->pop(
+                                       JobQueueGroup::TYPE_DEFAULT,
+                                       JobQueueGroup::USE_CACHE,
+                                       $blacklist
+                               );
                        } elseif ( in_array( $type, $blacklist ) ) {
                                $job = false; // requested queue in backoff state
                        } else {
@@ -155,6 +158,7 @@ class JobRunner implements LoggerAwareInterface {
                        }
 
                        if ( $job ) { // found a job
+                               ++$jobsPopped;
                                $popTime = time();
                                $jType = $job->getType();
 
@@ -169,80 +173,26 @@ class JobRunner implements LoggerAwareInterface {
                                        $backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
                                }
 
-                               $msg = $job->toString() . " STARTING";
-                               $this->logger->debug( $msg );
-                               $this->debugCallback( $msg );
-
-                               // Run the job...
-                               $jobStartTime = microtime( true );
-                               try {
-                                       ++$jobsPopped;
-                                       $status = $job->run();
-                                       $error = $job->getLastError();
-                                       $this->commitMasterChanges( $job );
-
-                                       DeferredUpdates::doUpdates();
-                                       $this->commitMasterChanges( $job );
-                               } catch ( Exception $e ) {
-                                       MWExceptionHandler::rollbackMasterChangesAndLog( $e );
-                                       $status = false;
-                                       $error = get_class( $e ) . ': ' . $e->getMessage();
-                                       MWExceptionHandler::logException( $e );
-                               }
-                               // Commit all outstanding connections that are in a transaction
-                               // to get a fresh repeatable read snapshot on every connection.
-                               // Note that jobs are still responsible for handling slave lag.
-                               wfGetLBFactory()->commitAll( __METHOD__ );
-                               // Clear out title cache data from prior snapshots
-                               LinkCache::singleton()->clear();
-                               $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
-                               $timeMsTotal += $timeMs;
-
-                               // Record how long jobs wait before getting popped
-                               $readyTs = $job->getReadyTimestamp();
-                               if ( $readyTs ) {
-                                       $pickupDelay = max( 0, $popTime - $readyTs );
-                                       $stats->timing( 'jobqueue.pickup_delay.all', 1000 * $pickupDelay );
-                                       $stats->timing( "jobqueue.pickup_delay.$jType", 1000 * $pickupDelay );
-                               }
-                               // Record root job age for jobs being run
-                               $root = $job->getRootJobParams();
-                               if ( $root['rootJobTimestamp'] ) {
-                                       $age = max( 0, $popTime - wfTimestamp( TS_UNIX, $root['rootJobTimestamp'] ) );
-                                       $stats->timing( "jobqueue.pickup_root_age.$jType", 1000 * $age );
-                               }
-                               // Track the execution time for jobs
-                               $stats->timing( "jobqueue.run.$jType", $timeMs );
-
-                               // Mark the job as done on success or when the job cannot be retried
-                               if ( $status !== false || !$job->allowRetries() ) {
-                                       $group->ack( $job ); // done
+                               $info = $this->executeJob( $job, $stats, $popTime );
+                               if ( $info['status'] !== false || !$job->allowRetries() ) {
+                                       $group->ack( $job ); // succeeded or job cannot be retried
                                }
 
                                // Back off of certain jobs for a while (for throttling and for errors)
-                               if ( $status === false && mt_rand( 0, 49 ) == 0 ) {
-                                       $ttw = max( $ttw, 30 ); // too many errors
+                               if ( $info['status'] === false && mt_rand( 0, 49 ) == 0 ) {
+                                       $ttw = max( $ttw, self::ERROR_BACKOFF_TTL ); // too many errors
                                        $backoffDeltas[$jType] = isset( $backoffDeltas[$jType] )
                                                ? $backoffDeltas[$jType] + $ttw
                                                : $ttw;
                                }
 
-                               if ( $status === false ) {
-                                       $msg = $job->toString() . " t=$timeMs error={$error}";
-                                       $this->logger->error( $msg );
-                                       $this->debugCallback( $msg );
-                               } else {
-                                       $msg = $job->toString() . " t=$timeMs good";
-                                       $this->logger->info( $msg );
-                                       $this->debugCallback( $msg );
-                               }
-
                                $response['jobs'][] = array(
                                        'type'   => $jType,
-                                       'status' => ( $status === false ) ? 'failed' : 'ok',
-                                       'error'  => $error,
-                                       'time'   => $timeMs
+                                       'status' => ( $info['status'] === false ) ? 'failed' : 'ok',
+                                       'error'  => $info['error'],
+                                       'time'   => $info['timeMs']
                                );
+                               $timeMsTotal += $info['timeMs'];
 
                                // Break out if we hit the job count or wall time limits...
                                if ( $maxJobs && $jobsPopped >= $maxJobs ) {
@@ -257,8 +207,8 @@ class JobRunner implements LoggerAwareInterface {
                                // This only waits for so long before exiting and letting
                                // other wikis in the farm (on different masters) get a chance.
                                $timePassed = microtime( true ) - $lastCheckTime;
-                               if ( $timePassed >= $checkLagPeriod || $timePassed < 0 ) {
-                                       if ( !wfWaitForSlaves( $lastCheckTime, false, '*', $maxAllowedLag ) ) {
+                               if ( $timePassed >= self::LAG_CHECK_PERIOD || $timePassed < 0 ) {
+                                       if ( !wfWaitForSlaves( $lastCheckTime, false, '*', self::MAX_ALLOWED_LAG ) ) {
                                                $response['reached'] = 'slave-lag-limit';
                                                break;
                                        }
@@ -288,6 +238,85 @@ class JobRunner implements LoggerAwareInterface {
                return $response;
        }
 
+       /**
+        * @param Job $job
+        * @param BufferingStatsdDataFactory $stats
+        * @param float $popTime
+        * @return array Map of status/error/timeMs
+        */
+       private function executeJob( Job $job, $stats, $popTime ) {
+               $jType = $job->getType();
+               $msg = $job->toString() . " STARTING";
+               $this->logger->debug( $msg );
+               $this->debugCallback( $msg );
+
+               // Run the job...
+               $rssStart = $this->getMaxRssKb();
+               $jobStartTime = microtime( true );
+               try {
+                       $status = $job->run();
+                       $error = $job->getLastError();
+                       $this->commitMasterChanges( $job );
+
+                       DeferredUpdates::doUpdates();
+                       $this->commitMasterChanges( $job );
+               } catch ( Exception $e ) {
+                       MWExceptionHandler::rollbackMasterChangesAndLog( $e );
+                       $status = false;
+                       $error = get_class( $e ) . ': ' . $e->getMessage();
+                       MWExceptionHandler::logException( $e );
+               }
+               // Commit all outstanding connections that are in a transaction
+               // to get a fresh repeatable read snapshot on every connection.
+               // Note that jobs are still responsible for handling slave lag.
+               wfGetLBFactory()->commitAll( __METHOD__ );
+               // Clear out title cache data from prior snapshots
+               LinkCache::singleton()->clear();
+               $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
+               $rssEnd = $this->getMaxRssKb();
+
+               // Record how long jobs wait before getting popped
+               $readyTs = $job->getReadyTimestamp();
+               if ( $readyTs ) {
+                       $pickupDelay = max( 0, $popTime - $readyTs );
+                       $stats->timing( 'jobqueue.pickup_delay.all', 1000 * $pickupDelay );
+                       $stats->timing( "jobqueue.pickup_delay.$jType", 1000 * $pickupDelay );
+               }
+               // Record root job age for jobs being run
+               $root = $job->getRootJobParams();
+               if ( $root['rootJobTimestamp'] ) {
+                       $age = max( 0, $popTime - wfTimestamp( TS_UNIX, $root['rootJobTimestamp'] ) );
+                       $stats->timing( "jobqueue.pickup_root_age.$jType", 1000 * $age );
+               }
+               // Track the execution time for jobs
+               $stats->timing( "jobqueue.run.$jType", $timeMs );
+               // Track RSS increases for jobs (in case of memory leaks)
+               if ( $rssStart && $rssEnd ) {
+                       $stats->increment( "jobqueue.rss_delta.$jType", $rssEnd - $rssStart );
+               }
+
+               if ( $status === false ) {
+                       $msg = $job->toString() . " t=$timeMs error={$error}";
+                       $this->logger->error( $msg );
+                       $this->debugCallback( $msg );
+               } else {
+                       $msg = $job->toString() . " t=$timeMs good";
+                       $this->logger->info( $msg );
+                       $this->debugCallback( $msg );
+               }
+
+               return array( 'status' => $status, 'error' => $error, 'timeMs' => $timeMs );
+       }
+
+       /**
+        * @return int|null Max memory RSS in kilobytes
+        */
+       private function getMaxRssKb() {
+               $info = wfGetRusage() ?: array();
+               // see http://linux.die.net/man/2/getrusage
+               return isset( $info['ru_maxrss'] ) ? (int)$info['ru_maxrss'] : null;
+       }
+
        /**
         * @param Job $job
         * @return int Seconds for this runner to avoid doing more jobs of this type
index ade4810..28e3c40 100644 (file)
@@ -93,10 +93,10 @@ class UploadFromUrlJob extends Job {
                                                        $this->params['url'] )->text()
                                        );
                                } else {
-                                       wfSetupSession( $this->params['sessionId'] );
-                                       $this->storeResultInSession( 'Warning',
+                                       $session = MediaWiki\Session\SessionManager::singleton()
+                                               ->getSessionById( $this->params['sessionId'] );
+                                       $this->storeResultInSession( $session, 'Warning',
                                                'warnings', $warnings );
-                                       session_write_close();
                                }
 
                                return true;
@@ -139,15 +139,15 @@ class UploadFromUrlJob extends Job {
                                        )->text() );
                        }
                } else {
-                       wfSetupSession( $this->params['sessionId'] );
+                       $session = MediaWiki\Session\SessionManager::singleton()
+                               ->getSessionById( $this->params['sessionId'] );
                        if ( $status->isOk() ) {
-                               $this->storeResultInSession( 'Success',
+                               $this->storeResultInSession( $session, 'Success',
                                        'filename', $this->upload->getLocalFile()->getName() );
                        } else {
-                               $this->storeResultInSession( 'Failure',
+                               $this->storeResultInSession( $session, 'Failure',
                                        'errors', $status->getErrorsArray() );
                        }
-                       session_write_close();
                }
        }
 
@@ -155,33 +155,55 @@ class UploadFromUrlJob extends Job {
         * Store a result in the session data. Note that the caller is responsible
         * for appropriate session_start and session_write_close calls.
         *
+        * @param MediaWiki\\Session\\Session $session Session to store result into
         * @param string $result The result (Success|Warning|Failure)
         * @param string $dataKey The key of the extra data
         * @param mixed $dataValue The extra data itself
         */
-       protected function storeResultInSession( $result, $dataKey, $dataValue ) {
-               $session =& self::getSessionData( $this->params['sessionKey'] );
-               $session['result'] = $result;
-               $session[$dataKey] = $dataValue;
+       protected function storeResultInSession(
+               MediaWiki\Session\Session $session, $result, $dataKey, $dataValue
+       ) {
+               $data = self::getSessionData( $session, $this->params['sessionKey'] );
+               $data['result'] = $result;
+               $data[$dataKey] = $dataValue;
+               self::setSessionData( $session, $this->params['sessionKey'], $data );
        }
 
        /**
         * Initialize the session data. Sets the initial result to queued.
         */
        public function initializeSessionData() {
-               $session =& self::getSessionData( $this->params['sessionKey'] );
-               $session['result'] = 'Queued';
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+               $data = self::getSessionData( $session, $this->params['sessionKey'] );
+               $data['result'] = 'Queued';
+               self::setSessionData( $session, $this->params['sessionKey'], $data );
        }
 
        /**
+        * @param MediaWiki\\Session\\Session $session
         * @param string $key
         * @return mixed
         */
-       public static function &getSessionData( $key ) {
-               if ( !isset( $_SESSION[self::SESSION_KEYNAME][$key] ) ) {
-                       $_SESSION[self::SESSION_KEYNAME][$key] = array();
+       public static function getSessionData( MediaWiki\Session\Session $session, $key ) {
+               $data = $session->get( self::SESSION_KEYNAME );
+               if ( !is_array( $data ) || !isset( $data[$key] ) ) {
+                       self::setSessionData( $session, $key, array() );
+                       return array();
                }
+               return $data[$key];
+       }
 
-               return $_SESSION[self::SESSION_KEYNAME][$key];
+       /**
+        * @param MediaWiki\\Session\\Session $session
+        * @param string $key
+        * @param mixed $value
+        */
+       public static function setSessionData( MediaWiki\Session\Session $session, $key, $value ) {
+               $data = $session->get( self::SESSION_KEYNAME, array() );
+               if ( !is_array( $data ) ) {
+                       $data = array();
+               }
+               $data[$key] = $value;
+               $session->set( self::SESSION_KEYNAME, $data );
        }
 }
diff --git a/includes/objectcache/ObjectCacheSessionHandler.php b/includes/objectcache/ObjectCacheSessionHandler.php
deleted file mode 100644 (file)
index cc85074..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-/**
- * Session storage in object cache.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (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 Cache
- */
-
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * Session storage in object cache.
- * Used if $wgSessionsInObjectCache is true.
- *
- * @ingroup Cache
- */
-class ObjectCacheSessionHandler {
-       /** @var array Map of (session ID => SHA-1 of the data) */
-       protected static $hashCache = array();
-
-       /**
-        * Install a session handler for the current web request
-        */
-       static function install() {
-               session_set_save_handler(
-                       array( __CLASS__, 'open' ),
-                       array( __CLASS__, 'close' ),
-                       array( __CLASS__, 'read' ),
-                       array( __CLASS__, 'write' ),
-                       array( __CLASS__, 'destroy' ),
-                       array( __CLASS__, 'gc' ) );
-
-               // It's necessary to register a shutdown function to call session_write_close(),
-               // because by the time the request shutdown function for the session module is
-               // called, the BagOStuff has already been destroyed. Shutdown functions registered
-               // this way are called before object destruction.
-               register_shutdown_function( array( __CLASS__, 'handleShutdown' ) );
-       }
-
-       /**
-        * Get the cache storage object to use for session storage
-        * @return BagOStuff
-        */
-       protected static function getCache() {
-               global $wgSessionCacheType;
-
-               return ObjectCache::getInstance( $wgSessionCacheType );
-       }
-
-       /**
-        * Get a cache key for the given session id.
-        *
-        * @param string $id Session id
-        * @return string Cache key
-        */
-       protected static function getKey( $id ) {
-               return wfMemcKey( 'session', $id );
-       }
-
-       /**
-        * @param mixed $data
-        * @return string
-        */
-       protected static function getHash( $data ) {
-               return sha1( serialize( $data ) );
-       }
-
-       /**
-        * Callback when opening a session.
-        *
-        * @param string $save_path Path used to store session files, unused
-        * @param string $session_name Session name
-        * @return bool Success
-        */
-       static function open( $save_path, $session_name ) {
-               return true;
-       }
-
-       /**
-        * Callback when closing a session.
-        * NOP.
-        *
-        * @return bool Success
-        */
-       static function close() {
-               return true;
-       }
-
-       /**
-        * Callback when reading session data.
-        *
-        * @param string $id Session id
-        * @return mixed Session data
-        */
-       static function read( $id ) {
-               $stime = microtime( true );
-               $data = self::getCache()->get( self::getKey( $id ) );
-               $real = microtime( true ) - $stime;
-
-               RequestContext::getMain()->getStats()->timing( "session.read", 1000 * $real );
-
-               self::$hashCache = array( $id => self::getHash( $data ) );
-
-               return ( $data === false ) ? '' : $data;
-       }
-
-       /**
-        * Callback when writing session data.
-        *
-        * @param string $id Session id
-        * @param string $data Session data
-        * @return bool Success
-        */
-       static function write( $id, $data ) {
-               global $wgObjectCacheSessionExpiry;
-
-               // Only issue a write if anything changed (PHP 5.6 already does this)
-               if ( !isset( self::$hashCache[$id] )
-                       || self::getHash( $data ) !== self::$hashCache[$id]
-               ) {
-                       $stime = microtime( true );
-                       self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry );
-                       $real = microtime( true ) - $stime;
-
-                       RequestContext::getMain()->getStats()->timing( "session.write", 1000 * $real );
-               }
-
-               return true;
-       }
-
-       /**
-        * Callback to destroy a session when calling session_destroy().
-        *
-        * @param string $id Session id
-        * @return bool Success
-        */
-       static function destroy( $id ) {
-               $stime = microtime( true );
-               self::getCache()->delete( self::getKey( $id ) );
-               $real = microtime( true ) - $stime;
-
-               RequestContext::getMain()->getStats()->timing( "session.destroy", 1000 * $real );
-
-               return true;
-       }
-
-       /**
-        * Callback to execute garbage collection.
-        * NOP: Object caches perform garbage collection implicitly
-        *
-        * @param int $maxlifetime Maximum session life time
-        * @return bool Success
-        */
-       static function gc( $maxlifetime ) {
-               return true;
-       }
-
-       /**
-        * Shutdown function.
-        * See the comment inside ObjectCacheSessionHandler::install for rationale.
-        */
-       static function handleShutdown() {
-               session_write_close();
-       }
-
-       /**
-        * Pre-emptive session renewal function
-        */
-       static function renewCurrentSession() {
-               global $wgObjectCacheSessionExpiry;
-
-               // Once a session is at half TTL, renew it
-               $window = $wgObjectCacheSessionExpiry / 2;
-               $logger = LoggerFactory::getInstance( 'SessionHandler' );
-
-               $now = microtime( true );
-               // Session are only written in object stores when $_SESSION changes,
-               // which also renews the TTL ($wgObjectCacheSessionExpiry). If a user
-               // is active but not causing session data changes, it may suddenly
-               // expire as they view a form, blocking the first submission.
-               // Make a dummy change every so often to avoid this.
-               if ( !isset( $_SESSION['wsExpiresUnix'] ) ) {
-                       $_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
-
-                       $logger->info( "Set expiry for session " . session_id(), array() );
-               } elseif ( ( $now + $window ) > $_SESSION['wsExpiresUnix'] ) {
-                       $_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
-
-                       $logger->info( "Renewed session " . session_id(), array() );
-               }
-       }
-}
index 22e24ae..c9af075 100644 (file)
@@ -1229,7 +1229,7 @@ class Article implements Page {
         */
        public static function purgePatrolFooterCache( $articleID ) {
                $cache = ObjectCache::getMainWANInstance();
-               $cache->touchCheckKey( wfMemcKey( 'unpatrollable-page', $articleID ) );
+               $cache->delete( wfMemcKey( 'unpatrollable-page', $articleID ) );
        }
 
        /**
index e3e6e15..6f4c296 100644 (file)
@@ -687,18 +687,6 @@ class WikiPage implements Page, IDBAccessObject {
                return false;
        }
 
-       /**
-        * Get the text of the current revision. No side-effects...
-        *
-        * @return string|bool The text of the current revision. False on failure
-        * @deprecated since 1.21, getContent() should be used instead.
-        */
-       public function getRawText() {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
-
-               return $this->getText( Revision::RAW );
-       }
-
        /**
         * @return string MW timestamp of last article revision
         */
@@ -1172,11 +1160,11 @@ class WikiPage implements Page, IDBAccessObject {
         * @return bool|int The newly created page_id key; false if the title already existed
         */
        public function insertOn( $dbw, $pageId = null ) {
-               $pageId = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
+               $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
                $dbw->insert(
                        'page',
                        array(
-                               'page_id'           => $pageId,
+                               'page_id'           => $pageIdForInsert,
                                'page_namespace'    => $this->mTitle->getNamespace(),
                                'page_title'        => $this->mTitle->getDBkey(),
                                'page_restrictions' => '',
@@ -1192,7 +1180,7 @@ class WikiPage implements Page, IDBAccessObject {
                );
 
                if ( $dbw->affectedRows() > 0 ) {
-                       $newid = $dbw->insertId();
+                       $newid = $pageId ?: $dbw->insertId();
                        $this->mId = $newid;
                        $this->mTitle->resetArticleID( $newid );
 
index d25d11a..91b6080 100644 (file)
@@ -813,7 +813,7 @@ class CoreParserFunctions {
                        $titleObject = $parser->mTitle;
                }
                if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) {
-                       $expiry = $parser->mTitle->getRestrictionExpiry( strtolower( $type ) );
+                       $expiry = $titleObject->getRestrictionExpiry( strtolower( $type ) );
                        // getRestrictionExpiry() returns false on invalid type; trying to
                        // match protectionlevel() function that returns empty string instead
                        if ( $expiry === false ) {
diff --git a/includes/session/BotPasswordSessionProvider.php b/includes/session/BotPasswordSessionProvider.php
new file mode 100644 (file)
index 0000000..d9c60c7
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+/**
+ * Session provider for bot passwords
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use BotPassword;
+use User;
+use WebRequest;
+
+/**
+ * Session provider for bot passwords
+ * @since 1.27
+ */
+class BotPasswordSessionProvider extends ImmutableSessionProviderWithCookie {
+
+       /**
+        * @param array $params Keys include:
+        *  - priority: (required) Set the priority
+        *  - sessionCookieName: Session cookie name. Default is '_BPsession'.
+        *  - sessionCookieOptions: Options to pass to WebResponse::setCookie().
+        */
+       public function __construct( array $params = array() ) {
+               if ( !isset( $params['sessionCookieName'] ) ) {
+                       $params['sessionCookieName'] = '_BPsession';
+               }
+               parent::__construct( $params );
+
+               if ( !isset( $params['priority'] ) ) {
+                       throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
+               }
+               if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
+                       $params['priority'] > SessionInfo::MAX_PRIORITY
+               ) {
+                       throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
+               }
+
+               $this->priority = $params['priority'];
+       }
+
+       public function provideSessionInfo( WebRequest $request ) {
+               // Only relevant for the API
+               if ( !defined( 'MW_API' ) ) {
+                       return null;
+               }
+
+               // Enabled?
+               if ( !$this->config->get( 'EnableBotPasswords' ) ) {
+                       return null;
+               }
+
+               // Have a session ID?
+               $id = $this->getSessionIdFromCookie( $request );
+               if ( $id === null ) {
+                       return null;
+               }
+
+               return new SessionInfo( $this->priority, array(
+                       'provider' => $this,
+                       'id' => $id,
+                       'persisted' => true
+               ) );
+       }
+
+       public function newSessionInfo( $id = null ) {
+               // We don't activate by default
+               return null;
+       }
+
+       /**
+        * Create a new session for a request
+        * @param User $user
+        * @param BotPassword $bp
+        * @param WebRequest $request
+        * @return Session
+        */
+       public function newSessionForRequest( User $user, BotPassword $bp, WebRequest $request ) {
+               $id = $this->getSessionIdFromCookie( $request );
+               $info = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $this,
+                       'id' => $id,
+                       'userInfo' => UserInfo::newFromUser( $user, true ),
+                       'persisted' => $id !== null,
+                       'metadata' => array(
+                               'centralId' => $bp->getUserCentralId(),
+                               'appId' => $bp->getAppId(),
+                               'token' => $bp->getToken(),
+                               'rights' => \MWGrants::getGrantRights( $bp->getGrants() ),
+                       ),
+               ) );
+               $session = $this->getManager()->getSessionFromInfo( $info, $request );
+               $session->persist();
+               return $session;
+       }
+
+       public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
+               $missingKeys = array_diff(
+                       array( 'centralId', 'appId', 'token' ),
+                       array_keys( $metadata )
+               );
+               if ( $missingKeys ) {
+                       $this->logger->info( "Session $info: Missing metadata: " . join( ', ', $missingKeys ) );
+                       return false;
+               }
+
+               $bp = BotPassword::newFromCentralId( $metadata['centralId'], $metadata['appId'] );
+               if ( !$bp ) {
+                       $this->logger->info(
+                               "Session $info: No BotPassword for {$metadata['centralId']} {$metadata['appId']}"
+                       );
+                       return false;
+               }
+
+               if ( !hash_equals( $metadata['token'], $bp->getToken() ) ) {
+                       $this->logger->info( "Session $info: BotPassword token check failed" );
+                       return false;
+               }
+
+               $status = $bp->getRestrictions()->check( $request );
+               if ( !$status->isOk() ) {
+                       $this->logger->info( "Session $info: Restrictions check failed", $status->getValue() );
+                       return false;
+               }
+
+               // Update saved rights
+               $metadata['rights'] = \MWGrants::getGrantRights( $bp->getGrants() );
+
+               return true;
+       }
+
+       public function preventSessionsForUser( $username ) {
+               BotPassword::removeAllPasswordsForUser( $username );
+       }
+
+       public function getAllowedUserRights( SessionBackend $backend ) {
+               if ( $backend->getProvider() !== $this ) {
+                       throw new InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
+               }
+               $data = $backend->getProviderMetadata();
+               if ( $data ) {
+                       return $data['rights'];
+               }
+
+               // Should never happen
+               $this->logger->debug( __METHOD__ . ': No provider metadata, returning no rights allowed' );
+               return array();
+       }
+}
diff --git a/includes/session/CookieSessionProvider.php b/includes/session/CookieSessionProvider.php
new file mode 100644 (file)
index 0000000..f92a519
--- /dev/null
@@ -0,0 +1,324 @@
+<?php
+/**
+ * MediaWiki cookie-based session provider interface
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Config;
+use User;
+use WebRequest;
+
+/**
+ * A CookieSessionProvider persists sessions using cookies
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+class CookieSessionProvider extends SessionProvider {
+
+       protected $params = array();
+       protected $cookieOptions = array();
+
+       /**
+        * @param array $params Keys include:
+        *  - priority: (required) Priority of the returned sessions
+        *  - callUserSetCookiesHook: Whether to call the deprecated hook
+        *  - sessionName: Session cookie name. Doesn't honor 'prefix'. Defaults to
+        *    $wgSessionName, or $wgCookiePrefix . '_session' if that is unset.
+        *  - cookieOptions: Options to pass to WebRequest::setCookie():
+        *    - prefix: Cookie prefix, defaults to $wgCookiePrefix
+        *    - path: Cookie path, defaults to $wgCookiePath
+        *    - domain: Cookie domain, defaults to $wgCookieDomain
+        *    - secure: Cookie secure flag, defaults to $wgCookieSecure
+        *    - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
+        */
+       public function __construct( $params = array() ) {
+               parent::__construct();
+
+               $params += array(
+                       'cookieOptions' => array(),
+                       // @codeCoverageIgnoreStart
+               );
+               // @codeCoverageIgnoreEnd
+
+               if ( !isset( $params['priority'] ) ) {
+                       throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
+               }
+               if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
+                       $params['priority'] > SessionInfo::MAX_PRIORITY
+               ) {
+                       throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
+               }
+
+               if ( !is_array( $params['cookieOptions'] ) ) {
+                       throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
+               }
+
+               $this->priority = $params['priority'];
+               $this->cookieOptions = $params['cookieOptions'];
+               $this->params = $params;
+               unset( $this->params['priority'] );
+               unset( $this->params['cookieOptions'] );
+       }
+
+       public function setConfig( Config $config ) {
+               parent::setConfig( $config );
+
+               // @codeCoverageIgnoreStart
+               $this->params += array(
+                       // @codeCoverageIgnoreEnd
+                       'callUserSetCookiesHook' => false,
+                       'sessionName' =>
+                               $config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session',
+               );
+
+               // @codeCoverageIgnoreStart
+               $this->cookieOptions += array(
+                       // @codeCoverageIgnoreEnd
+                       'prefix' => $config->get( 'CookiePrefix' ),
+                       'path' => $config->get( 'CookiePath' ),
+                       'domain' => $config->get( 'CookieDomain' ),
+                       'secure' => $config->get( 'CookieSecure' ),
+                       'httpOnly' => $config->get( 'CookieHttpOnly' ),
+               );
+       }
+
+       public function provideSessionInfo( WebRequest $request ) {
+               $info = array(
+                       'id' => $request->getCookie( $this->params['sessionName'], '' )
+               );
+               if ( !SessionManager::validateSessionId( $info['id'] ) ) {
+                       unset( $info['id'] );
+               }
+
+               list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
+               if ( $userId !== null ) {
+                       try {
+                               $userInfo = UserInfo::newFromId( $userId );
+                       } catch ( \InvalidArgumentException $ex ) {
+                               return null;
+                       }
+
+                       // Sanity check
+                       if ( $userName !== null && $userInfo->getName() !== $userName ) {
+                               return null;
+                       }
+
+                       if ( $token !== null ) {
+                               if ( !hash_equals( $userInfo->getToken(), $token ) ) {
+                                       return null;
+                               }
+                               $info['userInfo'] = $userInfo->verified();
+                       } elseif ( isset( $info['id'] ) ) { // No point if no session ID
+                               $info['userInfo'] = $userInfo;
+                       }
+               }
+
+               if ( !$info ) {
+                       return null;
+               }
+
+               $info += array(
+                       'provider' => $this,
+                       'persisted' => isset( $info['id'] ),
+                       'forceHTTPS' => $request->getCookie( 'forceHTTPS', '', false )
+               );
+
+               return new SessionInfo( $this->priority, $info );
+       }
+
+       public function persistsSessionId() {
+               return true;
+       }
+
+       public function canChangeUser() {
+               return true;
+       }
+
+       public function persistSession( SessionBackend $session, WebRequest $request ) {
+               $response = $request->response();
+               if ( $response->headersSent() ) {
+                       // Can't do anything now
+                       $this->logger->debug( __METHOD__ . ': Headers already sent' );
+                       return;
+               }
+
+               $user = $session->getUser();
+
+               $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
+               $sessionData = $this->sessionDataToExport( $user );
+
+               // Legacy hook
+               if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
+                       \Hooks::run( 'UserSetCookies', array( $user, &$sessionData, &$cookies ) );
+               }
+
+               $options = $this->cookieOptions;
+               if ( $session->shouldForceHTTPS() || $user->requiresHTTPS() ) {
+                       $response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
+                               array( 'prefix' => '', 'secure' => false ) + $options );
+                       $options['secure'] = true;
+               }
+
+               $response->setCookie( $this->params['sessionName'], $session->getId(), null,
+                       array( 'prefix' => '' ) + $options
+               );
+
+               $extendedCookies = $this->config->get( 'ExtendedLoginCookies' );
+               $extendedExpiry = $this->config->get( 'ExtendedLoginCookieExpiration' );
+
+               foreach ( $cookies as $key => $value ) {
+                       if ( $value === false ) {
+                               $response->clearCookie( $key, $options );
+                       } else {
+                               if ( $extendedExpiry !== null && in_array( $key, $extendedCookies ) ) {
+                                       $expiry = time() + (int)$extendedExpiry;
+                               } else {
+                                       $expiry = 0; // Default cookie expiration
+                               }
+                               $response->setCookie( $key, (string)$value, $expiry, $options );
+                       }
+               }
+
+               $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
+
+               if ( $sessionData ) {
+                       $session->addData( $sessionData );
+               }
+       }
+
+       public function unpersistSession( WebRequest $request ) {
+               $response = $request->response();
+               if ( $response->headersSent() ) {
+                       // Can't do anything now
+                       $this->logger->debug( __METHOD__ . ': Headers already sent' );
+                       return;
+               }
+
+               $cookies = array(
+                       'UserID' => false,
+                       'Token' => false,
+               );
+
+               $response->clearCookie(
+                       $this->params['sessionName'], array( 'prefix' => '' ) + $this->cookieOptions
+               );
+
+               foreach ( $cookies as $key => $value ) {
+                       $response->clearCookie( $key, $this->cookieOptions );
+               }
+
+               $response->clearCookie( 'forceHTTPS',
+                       array( 'prefix' => '', 'secure' => false ) + $this->cookieOptions );
+       }
+
+       /**
+        * Set the "logged out" cookie
+        * @param int $loggedOut timestamp
+        * @param WebRequest $request
+        */
+       protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
+               if ( $loggedOut + 86400 > time() &&
+                       $loggedOut !== (int)$request->getCookie( 'LoggedOut', $this->cookieOptions['prefix'] )
+               ) {
+                       $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
+                               $this->cookieOptions );
+               }
+       }
+
+       public function getVaryCookies() {
+               return array(
+                       // Vary on token and session because those are the real authn
+                       // determiners. UserID and UserName don't matter without those.
+                       $this->cookieOptions['prefix'] . 'Token',
+                       $this->cookieOptions['prefix'] . 'LoggedOut',
+                       $this->params['sessionName'],
+                       'forceHTTPS',
+               );
+       }
+
+       public function suggestLoginUsername( WebRequest $request ) {
+                $name = $request->getCookie( 'UserName', $this->cookieOptions['prefix'] );
+                if ( $name !== null ) {
+                        $name = User::getCanonicalName( $name, 'usable' );
+                }
+                return $name === false ? null : $name;
+       }
+
+       /**
+        * Fetch the user identity from cookies
+        * @return array (int|null $id, string|null $token)
+        */
+       protected function getUserInfoFromCookies( $request ) {
+               $prefix = $this->cookieOptions['prefix'];
+               return array(
+                       $request->getCookie( 'UserID', $prefix ),
+                       $request->getCookie( 'UserName', $prefix ),
+                       $request->getCookie( 'Token', $prefix ),
+               );
+       }
+
+       /**
+        * Return the data to store in cookies
+        * @param User $user
+        * @param bool $remember
+        * @return array $cookies Set value false to unset the cookie
+        */
+       protected function cookieDataToExport( $user, $remember ) {
+               if ( $user->isAnon() ) {
+                       return array(
+                               'UserID' => false,
+                               'Token' => false,
+                       );
+               } else {
+                       return array(
+                               'UserID' => $user->getId(),
+                               'UserName' => $user->getName(),
+                               'Token' => $remember ? (string)$user->getToken() : false,
+                       );
+               }
+       }
+
+       /**
+        * Return extra data to store in the session
+        * @param User $user
+        * @return array $session
+        */
+       protected function sessionDataToExport( $user ) {
+               // If we're calling the legacy hook, we should populate $session
+               // like User::setCookies() did.
+               if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
+                       return array(
+                               'wsUserID' => $user->getId(),
+                               'wsToken' => $user->getToken(),
+                               'wsUserName' => $user->getName(),
+                       );
+               }
+
+               return array();
+       }
+
+       public function whyNoSession() {
+               return wfMessage( 'sessionprovider-nocookies' );
+       }
+
+}
diff --git a/includes/session/ImmutableSessionProviderWithCookie.php b/includes/session/ImmutableSessionProviderWithCookie.php
new file mode 100644 (file)
index 0000000..98f7e5c
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+/**
+ * MediaWiki session provider base class
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use WebRequest;
+
+/**
+ * An ImmutableSessionProviderWithCookie doesn't persist the user, but
+ * optionally can use a cookie to support multiple IDs per session.
+ *
+ * As mentioned in the documentation for SessionProvider, many methods that are
+ * technically "cannot persist ID" could be turned into "can persist ID but
+ * not changing User" using a session cookie. This class implements such an
+ * optional session cookie.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
+
+       /** @var string|null */
+       protected $sessionCookieName = null;
+       protected $sessionCookieOptions = array();
+
+       /**
+        * @param array $params Keys include:
+        *  - sessionCookieName: Session cookie name, if multiple sessions per
+        *    client are to be supported.
+        *  - sessionCookieOptions: Options to pass to WebResponse::setCookie().
+        */
+       public function __construct( $params = array() ) {
+               parent::__construct();
+
+               if ( isset( $params['sessionCookieName'] ) ) {
+                       if ( !is_string( $params['sessionCookieName'] ) ) {
+                               throw new \InvalidArgumentException( 'sessionCookieName must be a string' );
+                       }
+                       $this->sessionCookieName = $params['sessionCookieName'];
+               }
+               if ( isset( $params['sessionCookieOptions'] ) ) {
+                       if ( !is_array( $params['sessionCookieOptions'] ) ) {
+                               throw new \InvalidArgumentException( 'sessionCookieOptions must be an array' );
+                       }
+                       $this->sessionCookieOptions = $params['sessionCookieOptions'];
+               }
+       }
+
+       /**
+        * Get the session ID from the cookie, if any.
+        *
+        * Only call this if $this->sessionCookieName !== null. If
+        * sessionCookieName is null, do some logic (probably involving a call to
+        * $this->hashToSessionId()) to create the single session ID corresponding
+        * to this WebRequest instead of calling this method.
+        *
+        * @param WebRequest $request
+        * @return string|null
+        */
+       protected function getSessionIdFromCookie( WebRequest $request ) {
+               if ( $this->sessionCookieName === null ) {
+                       throw new \BadMethodCallException(
+                               __METHOD__ . ' may not be called when $this->sessionCookieName === null'
+                       );
+               }
+
+               $prefix = isset( $this->sessionCookieOptions['prefix'] )
+                       ? $this->sessionCookieOptions['prefix']
+                       : $this->config->get( 'CookiePrefix' );
+               $id = $request->getCookie( $this->sessionCookieName, $prefix );
+               return SessionManager::validateSessionId( $id ) ? $id : null;
+       }
+
+       public function persistsSessionId() {
+               return $this->sessionCookieName !== null;
+       }
+
+       public function canChangeUser() {
+               return false;
+       }
+
+       public function persistSession( SessionBackend $session, WebRequest $request ) {
+               if ( $this->sessionCookieName === null ) {
+                       return;
+               }
+
+               $response = $request->response();
+               if ( $response->headersSent() ) {
+                       // Can't do anything now
+                       $this->logger->debug( __METHOD__ . ': Headers already sent' );
+                       return;
+               }
+
+               $options = $this->sessionCookieOptions;
+               if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
+                       $response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
+                               array( 'prefix' => '', 'secure' => false ) + $options );
+                       $options['secure'] = true;
+               }
+
+               $response->setCookie( $this->sessionCookieName, $session->getId(), null, $options );
+       }
+
+       public function unpersistSession( WebRequest $request ) {
+               if ( $this->sessionCookieName === null ) {
+                       return;
+               }
+
+               $response = $request->response();
+               if ( $response->headersSent() ) {
+                       // Can't do anything now
+                       $this->logger->debug( __METHOD__ . ': Headers already sent' );
+                       return;
+               }
+
+               $response->clearCookie( $this->sessionCookieName, $this->sessionCookieOptions );
+       }
+
+       public function getVaryCookies() {
+               if ( $this->sessionCookieName === null ) {
+                       return array();
+               }
+
+               $prefix = isset( $this->sessionCookieOptions['prefix'] )
+                       ? $this->sessionCookieOptions['prefix']
+                       : $this->config->get( 'CookiePrefix' );
+               return array( $prefix . $this->sessionCookieName );
+       }
+
+       public function whyNoSession() {
+               return wfMessage( 'sessionprovider-nocookies' );
+       }
+}
diff --git a/includes/session/PHPSessionHandler.php b/includes/session/PHPSessionHandler.php
new file mode 100644 (file)
index 0000000..c59cc96
--- /dev/null
@@ -0,0 +1,369 @@
+<?php
+/**
+ * Session storage in object cache.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerInterface;
+use BagOStuff;
+
+/**
+ * Adapter for PHP's session handling
+ * @todo Once we drop support for PHP < 5.4, use SessionHandlerInterface
+ *  (should just be a matter of adding "implements SessionHandlerInterface" and
+ *  changing the session_set_save_handler() call).
+ * @ingroup Session
+ * @since 1.27
+ */
+class PHPSessionHandler {
+       /** @var PHPSessionHandler */
+       protected static $instance = null;
+
+       /** @var bool Whether PHP session handling is enabled */
+       protected $enable = false;
+       protected $warn = true;
+
+       /** @var SessionManager|null */
+       protected $manager;
+
+       /** @var BagOStuff|null */
+       protected $store;
+
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var array Track original session fields for later modification check */
+       protected $sessionFieldCache = array();
+
+       protected function __construct( SessionManager $manager ) {
+               $this->setEnableFlags(
+                       \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
+               );
+               $manager->setupPHPSessionHandler( $this );
+       }
+
+       /**
+        * Set $this->enable and $this->warn
+        *
+        * Separate just because there doesn't seem to be a good way to test it
+        * otherwise.
+        *
+        * @param string $PHPSessionHandling See $wgPHPSessionHandling
+        */
+       private function setEnableFlags( $PHPSessionHandling ) {
+               switch ( $PHPSessionHandling ) {
+                       case 'enable':
+                               $this->enable = true;
+                               $this->warn = false;
+                               break;
+
+                       case 'warn':
+                               $this->enable = true;
+                               $this->warn = true;
+                               break;
+
+                       case 'disable':
+                               $this->enable = false;
+                               $this->warn = false;
+                               break;
+               }
+       }
+
+       /**
+        * Test whether the handler is installed
+        * @return bool
+        */
+       public static function isInstalled() {
+               return (bool)self::$instance;
+       }
+
+       /**
+        * Test whether the handler is installed and enabled
+        * @return bool
+        */
+       public static function isEnabled() {
+               return self::$instance && self::$instance->enable;
+       }
+
+       /**
+        * Install a session handler for the current web request
+        * @param SessionManager $manager
+        */
+       public static function install( SessionManager $manager ) {
+               if ( self::$instance ) {
+                       $manager->setupPHPSessionHandler( self::$instance );
+                       return;
+               }
+
+               self::$instance = new self( $manager );
+
+               // Close any auto-started session, before we replace it
+               session_write_close();
+
+               // Tell PHP not to mess with cookies itself
+               ini_set( 'session.use_cookies', 0 );
+               ini_set( 'session.use_trans_sid', 0 );
+
+               // Also set a sane serialization handler
+               \Wikimedia\PhpSessionSerializer::setSerializeHandler();
+
+               session_set_save_handler(
+                       array( self::$instance, 'open' ),
+                       array( self::$instance, 'close' ),
+                       array( self::$instance, 'read' ),
+                       array( self::$instance, 'write' ),
+                       array( self::$instance, 'destroy' ),
+                       array( self::$instance, 'gc' )
+               );
+
+               // It's necessary to register a shutdown function to call session_write_close(),
+               // because by the time the request shutdown function for the session module is
+               // called, other needed objects may have already been destroyed. Shutdown functions
+               // registered this way are called before object destruction.
+               register_shutdown_function( array( self::$instance, 'handleShutdown' ) );
+       }
+
+       /**
+        * Set the manager, store, and logger
+        * @private Use self::install().
+        * @param SessionManager $manager
+        * @param BagOStuff $store
+        * @param LoggerInterface $store
+        */
+       public function setManager(
+               SessionManager $manager, BagOStuff $store, LoggerInterface $logger
+       ) {
+               if ( $this->manager !== $manager ) {
+                       // Close any existing session before we change stores
+                       if ( $this->manager ) {
+                               session_write_close();
+                       }
+                       $this->manager = $manager;
+                       $this->store = $store;
+                       $this->logger = $logger;
+                       \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
+               }
+       }
+
+       /**
+        * Initialize the session (handler)
+        * @private For internal use only
+        * @param string $save_path Path used to store session files (ignored)
+        * @param string $session_name Session name (ignored)
+        * @return bool Success
+        */
+       public function open( $save_path, $session_name ) {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               if ( !$this->enable ) {
+                       throw new \BadMethodCallException( 'Attempt to use PHP session management' );
+               }
+               return true;
+       }
+
+       /**
+        * Close the session (handler)
+        * @private For internal use only
+        * @return bool Success
+        */
+       public function close() {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               $this->sessionFieldCache = array();
+               return true;
+       }
+
+       /**
+        * Read session data
+        * @private For internal use only
+        * @param string $id Session id
+        * @return string Session data
+        */
+       public function read( $id ) {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               if ( !$this->enable ) {
+                       throw new \BadMethodCallException( 'Attempt to use PHP session management' );
+               }
+
+               $session = $this->manager->getSessionById( $id, true );
+               if ( !$session ) {
+                       return '';
+               }
+               $session->persist();
+
+               $data = iterator_to_array( $session );
+               $this->sessionFieldCache[$id] = $data;
+               return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
+       }
+
+       /**
+        * Write session data
+        * @private For internal use only
+        * @param string $id Session id
+        * @param string $dataStr Session data. Not that you should ever call this
+        *   directly, but note that this has the same issues with code injection
+        *   via user-controlled data as does PHP's unserialize function.
+        * @return bool Success
+        */
+       public function write( $id, $dataStr ) {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               if ( !$this->enable ) {
+                       throw new \BadMethodCallException( 'Attempt to use PHP session management' );
+               }
+
+               $session = $this->manager->getSessionById( $id );
+
+               // First, decode the string PHP handed us
+               $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
+               if ( $data === null ) {
+                       // @codeCoverageIgnoreStart
+                       return false;
+                       // @codeCoverageIgnoreEnd
+               }
+
+               // Now merge the data into the Session object.
+               $changed = false;
+               $cache = isset( $this->sessionFieldCache[$id] ) ? $this->sessionFieldCache[$id] : array();
+               foreach ( $data as $key => $value ) {
+                       if ( !isset( $cache[$key] ) ) {
+                               if ( $session->exists( $key ) ) {
+                                       // New in both, so ignore and log
+                                       $this->logger->warning(
+                                               __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
+                                       );
+                               } else {
+                                       // New in $_SESSION, keep it
+                                       $session->set( $key, $value );
+                                       $changed = true;
+                               }
+                       } elseif ( $cache[$key] === $value ) {
+                               // Unchanged in $_SESSION, so ignore it
+                       } elseif ( !$session->exists( $key ) ) {
+                               // Deleted in Session, keep but log
+                               $this->logger->warning(
+                                       __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
+                               );
+                               $session->set( $key, $value );
+                               $changed = true;
+                       } elseif ( $cache[$key] === $session->get( $key ) ) {
+                               // Unchanged in Session, so keep it
+                               $session->set( $key, $value );
+                               $changed = true;
+                       } else {
+                               // Changed in both, so ignore and log
+                               $this->logger->warning(
+                                       __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
+                               );
+                       }
+               }
+               // Anything deleted in $_SESSION and unchanged in Session should be deleted too
+               // (but not if $_SESSION can't represent it at all)
+               \Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
+               foreach ( $cache as $key => $value ) {
+                       if ( !isset( $data[$key] ) && $session->exists( $key ) &&
+                               \Wikimedia\PhpSessionSerializer::encode( array( $key => true ) )
+                       ) {
+                               if ( $cache[$key] === $session->get( $key ) ) {
+                                       // Unchanged in Session, delete it
+                                       $session->remove( $key );
+                                       $changed = true;
+                               } else {
+                                       // Changed in Session, ignore deletion and log
+                                       $this->logger->warning(
+                                               __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
+                                       );
+                               }
+                       }
+               }
+               \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
+
+               // Save and update cache if anything changed
+               if ( $changed ) {
+                       if ( $this->warn ) {
+                               wfDeprecated( '$_SESSION', '1.27' );
+                               $this->logger->warning( 'Something wrote to $_SESSION!' );
+                       }
+
+                       $session->save();
+                       $this->sessionFieldCache[$id] = iterator_to_array( $session );
+               }
+
+               $session->persist();
+
+               return true;
+       }
+
+       /**
+        * Destroy a session
+        * @private For internal use only
+        * @param string $id Session id
+        * @return bool Success
+        */
+       public function destroy( $id ) {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               if ( !$this->enable ) {
+                       throw new \BadMethodCallException( 'Attempt to use PHP session management' );
+               }
+               $session = $this->manager->getSessionById( $id, true );
+               if ( $session ) {
+                       $session->clear();
+               }
+               return true;
+       }
+
+       /**
+        * Execute garbage collection.
+        * @private For internal use only
+        * @param int $maxlifetime Maximum session life time (ignored)
+        * @return bool Success
+        */
+       public function gc( $maxlifetime ) {
+               if ( self::$instance !== $this ) {
+                       throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
+               }
+               $before = date( 'YmdHis', time() );
+               $this->store->deleteObjectsExpiringBefore( $before );
+               return true;
+       }
+
+       /**
+        * Shutdown function.
+        *
+        * See the comment inside self::install for rationale.
+        * @codeCoverageIgnore
+        * @private For internal use only
+        */
+       public function handleShutdown() {
+               if ( $this->enable ) {
+                       session_write_close();
+               }
+       }
+
+}
diff --git a/includes/session/Session.php b/includes/session/Session.php
new file mode 100644 (file)
index 0000000..840baa7
--- /dev/null
@@ -0,0 +1,372 @@
+<?php
+/**
+ * MediaWiki session
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use User;
+use WebRequest;
+
+/**
+ * Manages data for an an authenticated session
+ *
+ * A Session represents the fact that the current HTTP request is part of a
+ * session. There are two broad types of Sessions, based on whether they
+ * return true or false from self::canSetUser():
+ * * When true (mutable), the Session identifies multiple requests as part of
+ *   a session generically, with no tie to a particular user.
+ * * When false (immutable), the Session identifies multiple requests as part
+ *   of a session by identifying and authenticating the request itself as
+ *   belonging to a particular user.
+ *
+ * The Session object also serves as a replacement for PHP's $_SESSION,
+ * managing access to per-session data.
+ *
+ * @todo Once we drop support for PHP 5.3.3, implementing ArrayAccess would be nice.
+ * @ingroup Session
+ * @since 1.27
+ */
+final class Session implements \Countable, \Iterator {
+       /** @var SessionBackend Session backend */
+       private $backend;
+
+       /** @var int Session index */
+       private $index;
+
+       /**
+        * @param SessionBackend $backend
+        * @param int $index
+        */
+       public function __construct( SessionBackend $backend, $index ) {
+               $this->backend = $backend;
+               $this->index = $index;
+       }
+
+       public function __destruct() {
+               $this->backend->deregisterSession( $this->index );
+       }
+
+       /**
+        * Returns the session ID
+        * @return string
+        */
+       public function getId() {
+               return $this->backend->getId();
+       }
+
+       /**
+        * Returns the SessionId object
+        * @private For internal use by WebRequest
+        * @return SessionId
+        */
+       public function getSessionId() {
+               return $this->backend->getSessionId();
+       }
+
+       /**
+        * Changes the session ID
+        * @return string New ID (might be the same as the old)
+        */
+       public function resetId() {
+               return $this->backend->resetId();
+       }
+
+       /**
+        * Fetch the SessionProvider for this session
+        * @return SessionProviderInterface
+        */
+       public function getProvider() {
+               return $this->backend->getProvider();
+       }
+
+       /**
+        * Indicate whether this session is persisted across requests
+        *
+        * For example, if cookies are set.
+        *
+        * @return bool
+        */
+       public function isPersistent() {
+               return $this->backend->isPersistent();
+       }
+
+       /**
+        * Make this session persisted across requests
+        *
+        * If the session is already persistent, equivalent to calling
+        * $this->renew().
+        */
+       public function persist() {
+               $this->backend->persist();
+       }
+
+       /**
+        * Indicate whether the user should be remembered independently of the
+        * session ID.
+        * @return bool
+        */
+       public function shouldRememberUser() {
+               return $this->backend->shouldRememberUser();
+       }
+
+       /**
+        * Set whether the user should be remembered independently of the session
+        * ID.
+        * @param bool $remember
+        */
+       public function setRememberUser( $remember ) {
+               $this->backend->setRememberUser( $remember );
+       }
+
+       /**
+        * Returns the request associated with this session
+        * @return WebRequest
+        */
+       public function getRequest() {
+               return $this->backend->getRequest( $this->index );
+       }
+
+       /**
+        * Returns the authenticated user for this session
+        * @return User
+        */
+       public function getUser() {
+               return $this->backend->getUser();
+       }
+
+       /**
+        * Fetch the rights allowed the user when this session is active.
+        * @return null|string[] Allowed user rights, or null to allow all.
+        */
+       public function getAllowedUserRights() {
+               return $this->backend->getAllowedUserRights();
+       }
+
+       /**
+        * Indicate whether the session user info can be changed
+        * @return bool
+        */
+       public function canSetUser() {
+               return $this->backend->canSetUser();
+       }
+
+       /**
+        * Set a new user for this session
+        * @note This should only be called when the user has been authenticated
+        * @param User $user User to set on the session.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        */
+       public function setUser( $user ) {
+               $this->backend->setUser( $user );
+       }
+
+       /**
+        * Get a suggested username for the login form
+        * @return string|null
+        */
+       public function suggestLoginUsername() {
+               return $this->backend->suggestLoginUsername( $this->index );
+       }
+
+       /**
+        * Whether HTTPS should be forced
+        * @return bool
+        */
+       public function shouldForceHTTPS() {
+               return $this->backend->shouldForceHTTPS();
+       }
+
+       /**
+        * Set whether HTTPS should be forced
+        * @param bool $force
+        */
+       public function setForceHTTPS( $force ) {
+               $this->backend->setForceHTTPS( $force );
+       }
+
+       /**
+        * Fetch the "logged out" timestamp
+        * @return int
+        */
+       public function getLoggedOutTimestamp() {
+               return $this->backend->getLoggedOutTimestamp();
+       }
+
+       /**
+        * Set the "logged out" timestamp
+        * @param int $ts
+        */
+       public function setLoggedOutTimestamp( $ts ) {
+               $this->backend->setLoggedOutTimestamp( $ts );
+       }
+
+       /**
+        * Fetch provider metadata
+        * @protected For use by SessionProvider subclasses only
+        * @return mixed
+        */
+       public function getProviderMetadata() {
+               return $this->backend->getProviderMetadata();
+       }
+
+       /**
+        * Delete all session data and clear the user (if possible)
+        */
+       public function clear() {
+               $data = &$this->backend->getData();
+               if ( $data ) {
+                       $data = array();
+                       $this->backend->dirty();
+               }
+               if ( $this->backend->canSetUser() ) {
+                       $this->backend->setUser( new User );
+               }
+               $this->backend->save();
+       }
+
+       /**
+        * Renew the session
+        *
+        * Resets the TTL in the backend store if the session is near expiring, and
+        * re-persists the session to any active WebRequests if persistent.
+        */
+       public function renew() {
+               $this->backend->renew();
+       }
+
+       /**
+        * Fetch a copy of this session attached to an alternative WebRequest
+        *
+        * Actions on the copy will affect this session too, and vice versa.
+        *
+        * @param WebRequest $request Any existing session associated with this
+        *  WebRequest object will be overwritten.
+        * @return Session
+        */
+       public function sessionWithRequest( WebRequest $request ) {
+               $request->setSessionId( $this->backend->getSessionId() );
+               return $this->backend->getSession( $request );
+       }
+
+       /**
+        * Fetch a value from the session
+        * @param string|int $key
+        * @param mixed $default
+        * @return mixed
+        */
+       public function get( $key, $default = null ) {
+               $data = &$this->backend->getData();
+               return array_key_exists( $key, $data ) ? $data[$key] : $default;
+       }
+
+       /**
+        * Test if a value exists in the session
+        * @param string|int $key
+        * @return bool
+        */
+       public function exists( $key ) {
+               $data = &$this->backend->getData();
+               return array_key_exists( $key, $data );
+       }
+
+       /**
+        * Set a value in the session
+        * @param string|int $key
+        * @param mixed $value
+        */
+       public function set( $key, $value ) {
+               $data = &$this->backend->getData();
+               if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
+                       $data[$key] = $value;
+                       $this->backend->dirty();
+               }
+       }
+
+       /**
+        * Remove a value from the session
+        * @param string|int $key
+        */
+       public function remove( $key ) {
+               $data = &$this->backend->getData();
+               if ( array_key_exists( $key, $data ) ) {
+                       unset( $data[$key] );
+                       $this->backend->dirty();
+               }
+       }
+
+       /**
+        * Delay automatic saving while multiple updates are being made
+        *
+        * Calls to save() or clear() will not be delayed.
+        *
+        * @return \ScopedCallback When this goes out of scope, a save will be triggered
+        */
+       public function delaySave() {
+               return $this->backend->delaySave();
+       }
+
+       /**
+        * Save the session
+        */
+       public function save() {
+               $this->backend->save();
+       }
+
+       /**
+        * @name Interface methods
+        * @{
+        */
+
+       public function count() {
+               $data = &$this->backend->getData();
+               return count( $data );
+       }
+
+       public function current() {
+               $data = &$this->backend->getData();
+               return current( $data );
+       }
+
+       public function key() {
+               $data = &$this->backend->getData();
+               return key( $data );
+       }
+
+       public function next() {
+               $data = &$this->backend->getData();
+               next( $data );
+       }
+
+       public function rewind() {
+               $data = &$this->backend->getData();
+               reset( $data );
+       }
+
+       public function valid() {
+               $data = &$this->backend->getData();
+               return key( $data ) !== null;
+       }
+
+       /**@}*/
+
+}
diff --git a/includes/session/SessionBackend.php b/includes/session/SessionBackend.php
new file mode 100644 (file)
index 0000000..5743b12
--- /dev/null
@@ -0,0 +1,632 @@
+<?php
+/**
+ * MediaWiki session backend
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use BagOStuff;
+use Psr\Log\LoggerInterface;
+use User;
+use WebRequest;
+
+/**
+ * This is the actual workhorse for Session.
+ *
+ * Most code does not need to use this class, you want \\MediaWiki\\Session\\Session.
+ * The exceptions are SessionProviders and SessionMetadata hook functions,
+ * which get an instance of this class rather than Session.
+ *
+ * The reasons for this split are:
+ * 1. A session can be attached to multiple requests, but we want the Session
+ *    object to have some features that correspond to just one of those
+ *    requests.
+ * 2. We want reasonable garbage collection behavior, but we also want the
+ *    SessionManager to hold a reference to every active session so it can be
+ *    saved when the request ends.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+final class SessionBackend {
+       /** @var SessionId */
+       private $id;
+
+       private $persist = false;
+       private $remember = false;
+       private $forceHTTPS = false;
+
+       /** @var array|null */
+       private $data = null;
+
+       private $forcePersist = false;
+       private $metaDirty = false;
+       private $dataDirty = false;
+
+       /** @var string Used to detect subarray modifications */
+       private $dataHash = null;
+
+       /** @var BagOStuff */
+       private $store;
+
+       /** @var LoggerInterface */
+       private $logger;
+
+       /** @var int */
+       private $lifetime;
+
+       /** @var User */
+       private $user;
+
+       private $curIndex = 0;
+
+       /** @var WebRequest[] Session requests */
+       private $requests = array();
+
+       /** @var SessionProvider provider */
+       private $provider;
+
+       /** @var array|null provider-specified metadata */
+       private $providerMetadata = null;
+
+       private $expires = 0;
+       private $loggedOut = 0;
+       private $delaySave = 0;
+
+       private $usePhpSessionHandling = true;
+       private $checkPHPSessionRecursionGuard = false;
+
+       /**
+        * @param SessionId $id Session ID object
+        * @param SessionInfo $info Session info to populate from
+        * @param BagOStuff $store Backend data store
+        * @param LoggerInterface $logger
+        * @param int $lifetime Session data lifetime in seconds
+        */
+       public function __construct(
+               SessionId $id, SessionInfo $info, BagOStuff $store, LoggerInterface $logger, $lifetime
+       ) {
+               $phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
+               $this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
+
+               if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
+                       throw new \InvalidArgumentException(
+                               "Refusing to create session for unverified user {$info->getUserInfo()}"
+                       );
+               }
+               if ( $info->getProvider() === null ) {
+                       throw new \InvalidArgumentException( 'Cannot create session without a provider' );
+               }
+               if ( $info->getId() !== $id->getId() ) {
+                       throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
+               }
+
+               $this->id = $id;
+               $this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
+               $this->store = $store;
+               $this->logger = $logger;
+               $this->lifetime = $lifetime;
+               $this->provider = $info->getProvider();
+               $this->persist = $info->wasPersisted();
+               $this->remember = $info->wasRemembered();
+               $this->forceHTTPS = $info->forceHTTPS();
+               $this->providerMetadata = $info->getProviderMetadata();
+
+               $blob = $store->get( wfMemcKey( 'MWSession', (string)$this->id ) );
+               if ( !is_array( $blob ) ||
+                       !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
+                       !isset( $blob['data'] ) || !is_array( $blob['data'] )
+               ) {
+                       $this->data = array();
+                       $this->dataDirty = true;
+                       $this->metaDirty = true;
+                       $this->logger->debug( "SessionBackend $this->id is unsaved, marking dirty in constructor" );
+               } else {
+                       $this->data = $blob['data'];
+                       if ( isset( $blob['metadata']['loggedOut'] ) ) {
+                               $this->loggedOut = (int)$blob['metadata']['loggedOut'];
+                       }
+                       if ( isset( $blob['metadata']['expires'] ) ) {
+                               $this->expires = (int)$blob['metadata']['expires'];
+                       } else {
+                               $this->metaDirty = true;
+                               $this->logger->debug(
+                                       "SessionBackend $this->id metadata dirty due to missing expiration timestamp"
+                               );
+                       }
+               }
+               $this->dataHash = md5( serialize( $this->data ) );
+       }
+
+       /**
+        * Return a new Session for this backend
+        * @param WebRequest $request
+        * @return Session
+        */
+       public function getSession( WebRequest $request ) {
+               $index = ++$this->curIndex;
+               $this->requests[$index] = $request;
+               $session = new Session( $this, $index );
+               return $session;
+       }
+
+       /**
+        * Deregister a Session
+        * @private For use by \\MediaWiki\\Session\\Session::__destruct() only
+        * @param int $index
+        */
+       public function deregisterSession( $index ) {
+               unset( $this->requests[$index] );
+               if ( !count( $this->requests ) ) {
+                       $this->save( true );
+                       $this->provider->getManager()->deregisterSessionBackend( $this );
+               }
+       }
+
+       /**
+        * Returns the session ID.
+        * @return string
+        */
+       public function getId() {
+               return (string)$this->id;
+       }
+
+       /**
+        * Fetch the SessionId object
+        * @private For internal use by WebRequest
+        * @return SessionId
+        */
+       public function getSessionId() {
+               return $this->id;
+       }
+
+       /**
+        * Changes the session ID
+        * @return string New ID (might be the same as the old)
+        */
+       public function resetId() {
+               if ( $this->provider->persistsSessionId() ) {
+                       $oldId = (string)$this->id;
+                       $restart = $this->usePhpSessionHandling && $oldId === session_id() &&
+                               PHPSessionHandler::isEnabled();
+
+                       if ( $restart ) {
+                               // If this session is the one behind PHP's $_SESSION, we need
+                               // to close then reopen it.
+                               session_write_close();
+                       }
+
+                       $this->provider->getManager()->changeBackendId( $this );
+                       $this->provider->sessionIdWasReset( $this, $oldId );
+                       $this->metaDirty = true;
+                       $this->logger->debug(
+                               "SessionBackend $this->id metadata dirty due to ID reset (formerly $oldId)"
+                       );
+
+                       if ( $restart ) {
+                               session_id( (string)$this->id );
+                               \MediaWiki\quietCall( 'session_start' );
+                       }
+
+                       $this->autosave();
+
+                       // Delete the data for the old session ID now
+                       $this->store->delete( wfMemcKey( 'MWSession', $oldId ) );
+               }
+       }
+
+       /**
+        * Fetch the SessionProvider for this session
+        * @return SessionProviderInterface
+        */
+       public function getProvider() {
+               return $this->provider;
+       }
+
+       /**
+        * Indicate whether this session is persisted across requests
+        *
+        * For example, if cookies are set.
+        *
+        * @return bool
+        */
+       public function isPersistent() {
+               return $this->persist;
+       }
+
+       /**
+        * Make this session persisted across requests
+        *
+        * If the session is already persistent, equivalent to calling
+        * $this->renew().
+        */
+       public function persist() {
+               if ( !$this->persist ) {
+                       $this->persist = true;
+                       $this->forcePersist = true;
+                       $this->logger->debug( "SessionBackend $this->id force-persist due to persist()" );
+                       $this->autosave();
+               } else {
+                       $this->renew();
+               }
+       }
+
+       /**
+        * Indicate whether the user should be remembered independently of the
+        * session ID.
+        * @return bool
+        */
+       public function shouldRememberUser() {
+               return $this->remember;
+       }
+
+       /**
+        * Set whether the user should be remembered independently of the session
+        * ID.
+        * @param bool $remember
+        */
+       public function setRememberUser( $remember ) {
+               if ( $this->remember !== (bool)$remember ) {
+                       $this->remember = (bool)$remember;
+                       $this->metaDirty = true;
+                       $this->logger->debug( "SessionBackend $this->id metadata dirty due to remember-user change" );
+                       $this->autosave();
+               }
+       }
+
+       /**
+        * Returns the request associated with a Session
+        * @param int $index Session index
+        * @return WebRequest
+        */
+       public function getRequest( $index ) {
+               if ( !isset( $this->requests[$index] ) ) {
+                       throw new \InvalidArgumentException( 'Invalid session index' );
+               }
+               return $this->requests[$index];
+       }
+
+       /**
+        * Returns the authenticated user for this session
+        * @return User
+        */
+       public function getUser() {
+               return $this->user;
+       }
+
+       /**
+        * Fetch the rights allowed the user when this session is active.
+        * @return null|string[] Allowed user rights, or null to allow all.
+        */
+       public function getAllowedUserRights() {
+               return $this->provider->getAllowedUserRights( $this );
+       }
+
+       /**
+        * Indicate whether the session user info can be changed
+        * @return bool
+        */
+       public function canSetUser() {
+               return $this->provider->canChangeUser();
+       }
+
+       /**
+        * Set a new user for this session
+        * @note This should only be called when the user has been authenticated via a login process
+        * @param User $user User to set on the session.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        */
+       public function setUser( $user ) {
+               if ( !$this->canSetUser() ) {
+                       throw new \BadMethodCallException(
+                               'Cannot set user on this session; check $session->canSetUser() first'
+                       );
+               }
+
+               $this->user = $user;
+               $this->metaDirty = true;
+               $this->logger->debug( "SessionBackend $this->id metadata dirty due to user change" );
+               $this->autosave();
+       }
+
+       /**
+        * Get a suggested username for the login form
+        * @param int $index Session index
+        * @return string|null
+        */
+       public function suggestLoginUsername( $index ) {
+               if ( !isset( $this->requests[$index] ) ) {
+                       throw new \InvalidArgumentException( 'Invalid session index' );
+               }
+               return $this->provider->suggestLoginUsername( $this->requests[$index] );
+       }
+
+       /**
+        * Whether HTTPS should be forced
+        * @return bool
+        */
+       public function shouldForceHTTPS() {
+               return $this->forceHTTPS;
+       }
+
+       /**
+        * Set whether HTTPS should be forced
+        * @param bool $force
+        */
+       public function setForceHTTPS( $force ) {
+               if ( $this->forceHTTPS !== (bool)$force ) {
+                       $this->forceHTTPS = (bool)$force;
+                       $this->metaDirty = true;
+                       $this->logger->debug( "SessionBackend $this->id metadata dirty due to force-HTTPS change" );
+                       $this->autosave();
+               }
+       }
+
+       /**
+        * Fetch the "logged out" timestamp
+        * @return int
+        */
+       public function getLoggedOutTimestamp() {
+               return $this->loggedOut;
+       }
+
+       /**
+        * Set the "logged out" timestamp
+        * @param int $ts
+        */
+       public function setLoggedOutTimestamp( $ts = null ) {
+               $ts = (int)$ts;
+               if ( $this->loggedOut !== $ts ) {
+                       $this->loggedOut = $ts;
+                       $this->metaDirty = true;
+                       $this->logger->debug(
+                               "SessionBackend $this->id metadata dirty due to logged-out-timestamp change"
+                       );
+                       $this->autosave();
+               }
+       }
+
+       /**
+        * Fetch provider metadata
+        * @protected For use by SessionProvider subclasses only
+        * @return mixed
+        */
+       public function getProviderMetadata() {
+               return $this->providerMetadata;
+       }
+
+       /**
+        * Fetch the session data array
+        *
+        * Note the caller is responsible for calling $this->dirty() if anything in
+        * the array is changed.
+        *
+        * @private For use by \\MediaWiki\\Session\\Session only.
+        * @return array
+        */
+       public function &getData() {
+               return $this->data;
+       }
+
+       /**
+        * Add data to the session.
+        *
+        * Overwrites any existing data under the same keys.
+        *
+        * @param array $newData Key-value pairs to add to the session
+        */
+       public function addData( array $newData ) {
+               $data = &$this->getData();
+               foreach ( $newData as $key => $value ) {
+                       if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
+                               $data[$key] = $value;
+                               $this->dataDirty = true;
+                               $this->logger->debug(
+                                       "SessionBackend $this->id data dirty due to addData(): " . wfGetAllCallers( 5 )
+                               );
+                       }
+               }
+       }
+
+       /**
+        * Mark data as dirty
+        * @private For use by \\MediaWiki\\Session\\Session only.
+        */
+       public function dirty() {
+               $this->dataDirty = true;
+               $this->logger->debug(
+                       "SessionBackend $this->id data dirty due to dirty(): " . wfGetAllCallers( 5 )
+               );
+       }
+
+       /**
+        * Renew the session by resaving everything
+        *
+        * Resets the TTL in the backend store if the session is near expiring, and
+        * re-persists the session to any active WebRequests if persistent.
+        */
+       public function renew() {
+               if ( time() + $this->lifetime / 2 > $this->expires ) {
+                       $this->metaDirty = true;
+                       $this->logger->debug(
+                               "SessionBackend $this->id metadata dirty for renew(): " . wfGetAllCallers( 5 )
+                       );
+                       if ( $this->persist ) {
+                               $this->forcePersist = true;
+                               $this->logger->debug(
+                                       "SessionBackend $this->id force-persist for renew(): " . wfGetAllCallers( 5 )
+                               );
+                       }
+               }
+               $this->autosave();
+       }
+
+       /**
+        * Delay automatic saving while multiple updates are being made
+        *
+        * Calls to save() will not be delayed.
+        *
+        * @return \ScopedCallback When this goes out of scope, a save will be triggered
+        */
+       public function delaySave() {
+               $that = $this;
+               $this->delaySave++;
+               $ref = &$this->delaySave;
+               return new \ScopedCallback( function () use ( $that, &$ref ) {
+                       if ( --$ref <= 0 ) {
+                               $ref = 0;
+                               $that->save();
+                       }
+               } );
+       }
+
+       /**
+        * Save and persist session data, unless delayed
+        */
+       private function autosave() {
+               if ( $this->delaySave <= 0 ) {
+                       $this->save();
+               }
+       }
+
+       /**
+        * Save and persist session data
+        * @param bool $closing Whether the session is being closed
+        */
+       public function save( $closing = false ) {
+               if ( $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
+                       $this->logger->debug(
+                               "SessionBackend $this->id not saving, " .
+                                       "user {$this->user} was passed to SessionManager::preventSessionsForUser"
+                       );
+                       return;
+               }
+
+               // Ensure the user has a token
+               // @codeCoverageIgnoreStart
+               $anon = $this->user->isAnon();
+               if ( !$anon && !$this->user->getToken() ) {
+                       $this->logger->debug(
+                               "SessionBackend $this->id creating token for user {$this->user} on save"
+                       );
+                       $this->user->setToken();
+                       if ( !wfReadOnly() ) {
+                               $this->user->saveSettings();
+                       }
+                       $this->metaDirty = true;
+               }
+               // @codeCoverageIgnoreEnd
+
+               if ( !$this->metaDirty && !$this->dataDirty &&
+                       $this->dataHash !== md5( serialize( $this->data ) )
+               ) {
+                       $this->logger->debug( "SessionBackend $this->id data dirty due to hash mismatch, " .
+                               "$this->dataHash !== " . md5( serialize( $this->data ) ) );
+                       $this->dataDirty = true;
+               }
+
+               if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
+                       return;
+               }
+
+               $this->logger->debug( "SessionBackend $this->id save: " .
+                       'dataDirty=' . (int)$this->dataDirty . ' ' .
+                       'metaDirty=' . (int)$this->metaDirty . ' ' .
+                       'forcePersist=' . (int)$this->forcePersist
+               );
+
+               // Persist to the provider, if flagged
+               if ( $this->persist && ( $this->metaDirty || $this->forcePersist ) ) {
+                       foreach ( $this->requests as $request ) {
+                               $request->setSessionId( $this->getSessionId() );
+                               $this->provider->persistSession( $this, $request );
+                       }
+                       if ( !$closing ) {
+                               $this->checkPHPSession();
+                       }
+               }
+
+               $this->forcePersist = false;
+
+               if ( !$this->metaDirty && !$this->dataDirty ) {
+                       return;
+               }
+
+               // Save session data to store, if necessary
+               $metadata = $origMetadata = array(
+                       'provider' => (string)$this->provider,
+                       'providerMetadata' => $this->providerMetadata,
+                       'userId' => $anon ? 0 : $this->user->getId(),
+                       'userName' => $anon ? null : $this->user->getName(),
+                       'userToken' => $anon ? null : $this->user->getToken(),
+                       'remember' => !$anon && $this->remember,
+                       'forceHTTPS' => $this->forceHTTPS,
+                       'expires' => time() + $this->lifetime,
+                       'loggedOut' => $this->loggedOut,
+               );
+
+               \Hooks::run( 'SessionMetadata', array( $this, &$metadata, $this->requests ) );
+
+               foreach ( $origMetadata as $k => $v ) {
+                       if ( $metadata[$k] !== $v ) {
+                               throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
+                       }
+               }
+
+               $this->store->set(
+                       wfMemcKey( 'MWSession', (string)$this->id ),
+                       array(
+                               'data' => $this->data,
+                               'metadata' => $metadata,
+                       ),
+                       $metadata['expires']
+               );
+
+               $this->metaDirty = false;
+               $this->dataDirty = false;
+               $this->dataHash = md5( serialize( $this->data ) );
+               $this->expires = $metadata['expires'];
+       }
+
+       /**
+        * For backwards compatibility, open the PHP session when the global
+        * session is persisted
+        */
+       private function checkPHPSession() {
+               if ( !$this->checkPHPSessionRecursionGuard ) {
+                       $this->checkPHPSessionRecursionGuard = true;
+                       $ref = &$this->checkPHPSessionRecursionGuard;
+                       $reset = new \ScopedCallback( function () use ( &$ref ) {
+                               $ref = false;
+                       } );
+
+                       if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
+                               SessionManager::getGlobalSession()->getId() === (string)$this->id
+                       ) {
+                               $this->logger->debug( "SessionBackend $this->id: Taking over PHP session" );
+                               session_id( (string)$this->id );
+                               \MediaWiki\quietCall( 'session_start' );
+                       }
+               }
+       }
+
+}
diff --git a/includes/session/SessionId.php b/includes/session/SessionId.php
new file mode 100644 (file)
index 0000000..0669100
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/**
+ * MediaWiki session ID holder
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+/**
+ * Value object holding the session ID in a manner that can be globally
+ * updated.
+ *
+ * This class exists because we want WebRequest to refer to the session, but it
+ * can't hold the Session itself due to issues with circular references and it
+ * can't just hold the ID as a string because we need to be able to update the
+ * ID when SessionBackend::resetId() is called.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+final class SessionId {
+       /** @var string */
+       private $id;
+
+       /**
+        * @param string $id
+        */
+       public function __construct( $id ) {
+               $this->id = $id;
+       }
+
+       /**
+        * Get the ID
+        * @return string
+        */
+       public function getId() {
+               return $this->id;
+       }
+
+       /**
+        * Set the ID
+        * @private For use by \\MediaWiki\\Session\\SessionManager only
+        * @param string $id
+        */
+       public function setId( $id ) {
+               $this->id = $id;
+       }
+
+       public function __toString() {
+               return $this->id;
+       }
+
+}
diff --git a/includes/session/SessionInfo.php b/includes/session/SessionInfo.php
new file mode 100644 (file)
index 0000000..9fe2cdf
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+/**
+ * MediaWiki session info
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerInterface;
+use BagOStuff;
+use WebRequest;
+
+/**
+ * Value object returned by SessionProvider
+ *
+ * This holds the data necessary to construct a Session.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+class SessionInfo {
+       /** Minimum allowed priority */
+       const MIN_PRIORITY = 1;
+
+       /** Maximum allowed priority */
+       const MAX_PRIORITY = 100;
+
+       /** @var SessionProvider|null */
+       private $provider;
+
+       /** @var string */
+       private $id;
+
+       /** @var int */
+       private $priority;
+
+       /** @var UserInfo|null */
+       private $userInfo = null;
+
+       private $persisted = false;
+       private $remembered = false;
+       private $forceHTTPS = false;
+       private $idIsSafe = false;
+
+       /** @var array|null */
+       private $providerMetadata = null;
+
+       /**
+        * @param int $priority Session priority
+        * @param array $data
+        *  - provider: (SessionProvider|null) If not given, the provider will be
+        *    determined from the saved session data.
+        *  - id: (string|null) Session ID
+        *  - userInfo: (UserInfo|null) User known from the request. If
+        *    $provider->canChangeUser() is false, a verified user
+        *    must be provided.
+        *  - persisted: (bool) Whether this session was persisted
+        *  - remembered: (bool) Whether the verified user was remembered.
+        *    Defaults to true.
+        *  - forceHTTPS: (bool) Whether to force HTTPS for this session
+        *  - metadata: (array) Provider metadata, to be returned by
+        *    Session::getProviderMetadata().
+        *  - idIsSafe: (bool) Set true if the 'id' did not come from the user.
+        *    Generally you'll use this from SessionProvider::newEmptySession(),
+        *    and not from any other method.
+        *  - copyFrom: (SessionInfo) SessionInfo to copy other data items from.
+        */
+       public function __construct( $priority, array $data ) {
+               if ( $priority < self::MIN_PRIORITY || $priority > self::MAX_PRIORITY ) {
+                       throw new \InvalidArgumentException( 'Invalid priority' );
+               }
+
+               if ( isset( $data['copyFrom'] ) ) {
+                       $from = $data['copyFrom'];
+                       if ( !$from instanceof SessionInfo ) {
+                               throw new \InvalidArgumentException( 'Invalid copyFrom' );
+                       }
+                       $data += array(
+                               'provider' => $from->provider,
+                               'id' => $from->id,
+                               'userInfo' => $from->userInfo,
+                               'persisted' => $from->persisted,
+                               'remembered' => $from->remembered,
+                               'forceHTTPS' => $from->forceHTTPS,
+                               'metadata' => $from->providerMetadata,
+                               'idIsSafe' => $from->idIsSafe,
+                               // @codeCoverageIgnoreStart
+                       );
+                       // @codeCoverageIgnoreEnd
+               } else {
+                       $data += array(
+                               'provider' => null,
+                               'id' => null,
+                               'userInfo' => null,
+                               'persisted' => false,
+                               'remembered' => true,
+                               'forceHTTPS' => false,
+                               'metadata' => null,
+                               'idIsSafe' => false,
+                               // @codeCoverageIgnoreStart
+                       );
+                       // @codeCoverageIgnoreEnd
+               }
+
+               if ( $data['id'] !== null && !SessionManager::validateSessionId( $data['id'] ) ) {
+                       throw new \InvalidArgumentException( 'Invalid session ID' );
+               }
+
+               if ( $data['userInfo'] !== null && !$data['userInfo'] instanceof UserInfo ) {
+                       throw new \InvalidArgumentException( 'Invalid userInfo' );
+               }
+
+               if ( !$data['provider'] && $data['id'] === null ) {
+                       throw new \InvalidArgumentException(
+                               'Must supply an ID when no provider is given'
+                       );
+               }
+
+               if ( $data['metadata'] !== null && !is_array( $data['metadata'] ) ) {
+                       throw new \InvalidArgumentException( 'Invalid metadata' );
+               }
+
+               $this->provider = $data['provider'];
+               if ( $data['id'] !== null ) {
+                       $this->id = $data['id'];
+                       $this->idIsSafe = $data['idIsSafe'];
+               } else {
+                       $this->id = $this->provider->getManager()->generateSessionId();
+                       $this->idIsSafe = true;
+               }
+               $this->priority = (int)$priority;
+               $this->userInfo = $data['userInfo'];
+               $this->persisted = (bool)$data['persisted'];
+               if ( $data['provider'] !== null ) {
+                       if ( $this->userInfo !== null && !$this->userInfo->isAnon() && $this->userInfo->isVerified() ) {
+                               $this->remembered = (bool)$data['remembered'];
+                       }
+                       $this->providerMetadata = $data['metadata'];
+               }
+               $this->forceHTTPS = (bool)$data['forceHTTPS'];
+       }
+
+       /**
+        * Return the provider
+        * @return SessionProvider|null
+        */
+       final public function getProvider() {
+               return $this->provider;
+       }
+
+       /**
+        * Return the session ID
+        * @return string
+        */
+       final public function getId() {
+               return $this->id;
+       }
+
+       /**
+        * Indicate whether the ID is "safe"
+        *
+        * The ID is safe in the following cases:
+        * - The ID was randomly generated by the constructor.
+        * - The ID was found in the backend data store.
+        * - $this->getProvider()->persistsSessionId() is false.
+        * - The constructor was explicitly told it's safe using the 'idIsSafe'
+        *   parameter.
+        *
+        * @return bool
+        */
+       final public function isIdSafe() {
+               return $this->idIsSafe;
+       }
+
+       /**
+        * Return the priority
+        * @return int
+        */
+       final public function getPriority() {
+               return $this->priority;
+       }
+
+       /**
+        * Return the user
+        * @return UserInfo|null
+        */
+       final public function getUserInfo() {
+               return $this->userInfo;
+       }
+
+       /**
+        * Return whether the session is persisted
+        *
+        * i.e. a session ID was given to the constuctor
+        *
+        * @return bool
+        */
+       final public function wasPersisted() {
+               return $this->persisted;
+       }
+
+       /**
+        * Return provider metadata
+        * @return array|null
+        */
+       final public function getProviderMetadata() {
+               return $this->providerMetadata;
+       }
+
+       /**
+        * Return whether the user was remembered
+        *
+        * For providers that can persist the user separately from the session,
+        * the human using it may not actually *want* that to be done. For example,
+        * a cookie-based provider can set cookies that are longer-lived than the
+        * backend session data, but on a public terminal the human likely doesn't
+        * want those cookies set.
+        *
+        * This is false unless a non-anonymous verified user was passed to
+        * the SessionInfo constructor by the provider, and the provider didn't
+        * pass false for the 'remembered' data item.
+        *
+        * @return bool
+        */
+       final public function wasRemembered() {
+               return $this->remembered;
+       }
+
+       /**
+        * Whether this session should only be used over HTTPS
+        * @return bool
+        */
+       final public function forceHTTPS() {
+               return $this->forceHTTPS;
+       }
+
+       public function __toString() {
+               return '[' . $this->getPriority() . ']' .
+                       ( $this->getProvider() ?: 'null' ) .
+                       ( $this->userInfo ?: '<null>' ) . $this->getId();
+       }
+
+       /**
+        * Compare two SessionInfo objects by priority
+        * @param SessionInfo $a
+        * @param SessionInfo $b
+        * @return int Negative if $a < $b, positive if $a > $b, zero if equal
+        */
+       public static function compare( $a, $b ) {
+               return $a->getPriority() - $b->getPriority();
+       }
+
+}
diff --git a/includes/session/SessionManager.php b/includes/session/SessionManager.php
new file mode 100644 (file)
index 0000000..1c8686c
--- /dev/null
@@ -0,0 +1,997 @@
+<?php
+/**
+ * MediaWiki\Session entry point
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerInterface;
+use BagOStuff;
+use Config;
+use FauxRequest;
+use Language;
+use Message;
+use User;
+use WebRequest;
+
+/**
+ * This serves as the entry point to the MediaWiki session handling system.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+final class SessionManager implements SessionManagerInterface {
+       /** @var SessionManager|null */
+       private static $instance = null;
+
+       /** @var Session|null */
+       private static $globalSession = null;
+
+       /** @var WebRequest|null */
+       private static $globalSessionRequest = null;
+
+       /** @var LoggerInterface */
+       private $logger;
+
+       /** @var Config */
+       private $config;
+
+       /** @var BagOStuff|null */
+       private $store;
+
+       /** @var SessionProvider[] */
+       private $sessionProviders = null;
+
+       /** @var string[] */
+       private $varyCookies = null;
+
+       /** @var array */
+       private $varyHeaders = null;
+
+       /** @var SessionBackend[] */
+       private $allSessionBackends = array();
+
+       /** @var SessionId[] */
+       private $allSessionIds = array();
+
+       /** @var string[] */
+       private $preventUsers = array();
+
+       /**
+        * Get the global SessionManager
+        * @return SessionManagerInterface
+        *  (really a SessionManager, but this is to make IDEs less confused)
+        */
+       public static function singleton() {
+               if ( self::$instance === null ) {
+                       self::$instance = new self();
+               }
+               return self::$instance;
+       }
+
+       /**
+        * Get the "global" session
+        *
+        * If PHP's session_id() has been set, returns that session. Otherwise
+        * returns the session for RequestContext::getMain()->getRequest().
+        *
+        * @return Session
+        */
+       public static function getGlobalSession() {
+               if ( !PHPSessionHandler::isEnabled() ) {
+                       $id = '';
+               } else {
+                       $id = session_id();
+               }
+
+               $request = \RequestContext::getMain()->getRequest();
+               if (
+                       !self::$globalSession // No global session is set up yet
+                       || self::$globalSessionRequest !== $request // The global WebRequest changed
+                       || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
+               ) {
+                       self::$globalSessionRequest = $request;
+                       if ( $id === '' ) {
+                               // session_id() wasn't used, so fetch the Session from the WebRequest.
+                               // We use $request->getSession() instead of $singleton->getSessionForRequest()
+                               // because doing the latter would require a public
+                               // "$request->getSessionId()" method that would confuse end
+                               // users by returning SessionId|null where they'd expect it to
+                               // be short for $request->getSession()->getId(), and would
+                               // wind up being a duplicate of the code in
+                               // $request->getSession() anyway.
+                               self::$globalSession = $request->getSession();
+                       } else {
+                               // Someone used session_id(), so we need to follow suit.
+                               // Note this overwrites whatever session might already be
+                               // associated with $request with the one for $id.
+                               self::$globalSession = self::singleton()->getSessionById( $id, false, $request );
+                       }
+               }
+               return self::$globalSession;
+       }
+
+       /**
+        * @param array $options
+        *  - config: Config to fetch configuration from. Defaults to the default 'main' config.
+        *  - logger: LoggerInterface to use for logging. Defaults to the 'session' channel.
+        *  - store: BagOStuff to store session data in.
+        */
+       public function __construct( $options = array() ) {
+               if ( isset( $options['config'] ) ) {
+                       $this->config = $options['config'];
+                       if ( !$this->config instanceof Config ) {
+                               throw new \InvalidArgumentException(
+                                       '$options[\'config\'] must be an instance of Config'
+                               );
+                       }
+               } else {
+                       $this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+               }
+
+               if ( isset( $options['logger'] ) ) {
+                       if ( !$options['logger'] instanceof LoggerInterface ) {
+                               throw new \InvalidArgumentException(
+                                       '$options[\'logger\'] must be an instance of LoggerInterface'
+                               );
+                       }
+                       $this->setLogger( $options['logger'] );
+               } else {
+                       $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
+               }
+
+               if ( isset( $options['store'] ) ) {
+                       if ( !$options['store'] instanceof BagOStuff ) {
+                               throw new \InvalidArgumentException(
+                                       '$options[\'store\'] must be an instance of BagOStuff'
+                               );
+                       }
+                       $this->store = $options['store'];
+               } else {
+                       $this->store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
+                       $this->store->setLogger( $this->logger );
+               }
+
+               register_shutdown_function( array( $this, 'shutdown' ) );
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       public function getPersistedSessionId( WebRequest $request ) {
+               $info = $this->getSessionInfoForRequest( $request );
+               if ( $info && $info->wasPersisted() ) {
+                       return $info->getId();
+               } else {
+                       return null;
+               }
+       }
+
+       public function getSessionForRequest( WebRequest $request ) {
+               $info = $this->getSessionInfoForRequest( $request );
+
+               if ( !$info ) {
+                       $session = $this->getEmptySession( $request );
+               } else {
+                       $session = $this->getSessionFromInfo( $info, $request );
+               }
+               return $session;
+       }
+
+       public function getSessionById( $id, $noEmpty = false, WebRequest $request = null ) {
+               if ( !self::validateSessionId( $id ) ) {
+                       throw new \InvalidArgumentException( 'Invalid session ID' );
+               }
+               if ( !$request ) {
+                       $request = new FauxRequest;
+               }
+
+               $session = null;
+
+               // Test this here to provide a better log message for the common case
+               // of "no such ID"
+               $key = wfMemcKey( 'MWSession', $id );
+               if ( is_array( $this->store->get( $key ) ) ) {
+                       $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
+                       if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
+                               $session = $this->getSessionFromInfo( $info, $request );
+                       }
+               }
+
+               if ( !$noEmpty && $session === null ) {
+                       $ex = null;
+                       try {
+                               $session = $this->getEmptySessionInternal( $request, $id );
+                       } catch ( \Exception $ex ) {
+                               $this->logger->error( __METHOD__ . ': failed to create empty session: ' .
+                                       $ex->getMessage() );
+                               $session = null;
+                       }
+                       if ( $session === null ) {
+                               throw new \UnexpectedValueException(
+                                       'Can neither load the session nor create an empty session', 0, $ex
+                               );
+                       }
+               }
+
+               return $session;
+       }
+
+       public function getEmptySession( WebRequest $request = null ) {
+               return $this->getEmptySessionInternal( $request );
+       }
+
+       /**
+        * @see SessionManagerInterface::getEmptySession
+        * @param WebRequest|null $request
+        * @param string|null $id ID to force on the new session
+        * @return Session
+        */
+       private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
+               if ( $id !== null ) {
+                       if ( !self::validateSessionId( $id ) ) {
+                               throw new \InvalidArgumentException( 'Invalid session ID' );
+                       }
+
+                       $key = wfMemcKey( 'MWSession', $id );
+                       if ( is_array( $this->store->get( $key ) ) ) {
+                               throw new \InvalidArgumentException( 'Session ID already exists' );
+                       }
+               }
+               if ( !$request ) {
+                       $request = new FauxRequest;
+               }
+
+               $infos = array();
+               foreach ( $this->getProviders() as $provider ) {
+                       $info = $provider->newSessionInfo( $id );
+                       if ( !$info ) {
+                               continue;
+                       }
+                       if ( $info->getProvider() !== $provider ) {
+                               throw new \UnexpectedValueException(
+                                       "$provider returned an empty session info for a different provider: $info"
+                               );
+                       }
+                       if ( $id !== null && $info->getId() !== $id ) {
+                               throw new \UnexpectedValueException(
+                                       "$provider returned empty session info with a wrong id: " .
+                                               $info->getId() . ' != ' . $id
+                               );
+                       }
+                       if ( !$info->isIdSafe() ) {
+                               throw new \UnexpectedValueException(
+                                       "$provider returned empty session info with id flagged unsafe"
+                               );
+                       }
+                       $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
+                       if ( $compare > 0 ) {
+                               continue;
+                       }
+                       if ( $compare === 0 ) {
+                               $infos[] = $info;
+                       } else {
+                               $infos = array( $info );
+                       }
+               }
+
+               // Make sure there's exactly one
+               if ( count( $infos ) > 1 ) {
+                       throw new \UnexpectedValueException(
+                               'Multiple empty sessions tied for top priority: ' . join( ', ', $infos )
+                       );
+               } elseif ( count( $infos ) < 1 ) {
+                       throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
+               }
+
+               return $this->getSessionFromInfo( $infos[0], $request );
+       }
+
+       public function getVaryHeaders() {
+               if ( $this->varyHeaders === null ) {
+                       $headers = array();
+                       foreach ( $this->getProviders() as $provider ) {
+                               foreach ( $provider->getVaryHeaders() as $header => $options ) {
+                                       if ( !isset( $headers[$header] ) ) {
+                                               $headers[$header] = array();
+                                       }
+                                       if ( is_array( $options ) ) {
+                                               $headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
+                                       }
+                               }
+                       }
+                       $this->varyHeaders = $headers;
+               }
+               return $this->varyHeaders;
+       }
+
+       public function getVaryCookies() {
+               if ( $this->varyCookies === null ) {
+                       $cookies = array();
+                       foreach ( $this->getProviders() as $provider ) {
+                               $cookies = array_merge( $cookies, $provider->getVaryCookies() );
+                       }
+                       $this->varyCookies = array_values( array_unique( $cookies ) );
+               }
+               return $this->varyCookies;
+       }
+
+       /**
+        * Validate a session ID
+        * @param string $id
+        * @return bool
+        */
+       public static function validateSessionId( $id ) {
+               return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
+       }
+
+       /**
+        * @name Internal methods
+        * @{
+        */
+
+       /**
+        * Auto-create the given user, if necessary
+        * @private Don't call this yourself. Let Setup.php do it for you at the right time.
+        * @note This more properly belongs in AuthManager, but we need it now.
+        *  When AuthManager comes, this will be deprecated and will pass-through
+        *  to the corresponding AuthManager method.
+        * @param User $user User to auto-create
+        * @return bool Success
+        */
+       public static function autoCreateUser( User $user ) {
+               global $wgAuth;
+
+               $logger = self::singleton()->logger;
+
+               // Much of this code is based on that in CentralAuth
+
+               // Try the local user from the slave DB
+               $localId = User::idFromName( $user->getName() );
+
+               // Fetch the user ID from the master, so that we don't try to create the user
+               // when they already exist, due to replication lag
+               // @codeCoverageIgnoreStart
+               if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
+                       $localId = User::idFromName( $user->getName(), User::READ_LATEST );
+               }
+               // @codeCoverageIgnoreEnd
+
+               if ( $localId ) {
+                       // User exists after all.
+                       $user->setId( $localId );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Denied by AuthPlugin? But ignore AuthPlugin itself.
+               if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
+                       $logger->debug( __METHOD__ . ': denied by AuthPlugin' );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Wiki is read-only?
+               if ( wfReadOnly() ) {
+                       $logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               $userName = $user->getName();
+
+               // Check the session, if we tried to create this user already there's
+               // no point in retrying.
+               $session = self::getGlobalSession();
+               $reason = $session->get( 'MWSession::AutoCreateBlacklist' );
+               if ( $reason ) {
+                       $logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Is the IP user able to create accounts?
+               $anon = new User;
+               if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
+                       || $anon->isBlockedFromCreateAccount()
+               ) {
+                       // Blacklist the user to avoid repeated DB queries subsequently
+                       $logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
+                       $session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
+                       $session->persist();
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Check for validity of username
+               if ( !User::isCreatableName( $userName ) ) {
+                       $logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
+                       $session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
+                       $session->persist();
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Give other extensions a chance to stop auto creation.
+               $user->loadDefaults( $userName );
+               $abortMessage = '';
+               if ( !\Hooks::run( 'AbortAutoAccount', array( $user, &$abortMessage ) ) ) {
+                       // In this case we have no way to return the message to the user,
+                       // but we can log it.
+                       $logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
+                       $session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
+                       $session->persist();
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Make sure the name has not been changed
+               if ( $user->getName() !== $userName ) {
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       throw new \UnexpectedValueException(
+                               'AbortAutoAccount hook tried to change the user name'
+                       );
+               }
+
+               // Ignore warnings about master connections/writes...hard to avoid here
+               \Profiler::instance()->getTransactionProfiler()->resetExpectations();
+
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
+               if ( $cache->get( $backoffKey ) ) {
+                       $logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return false;
+               }
+
+               // Checks passed, create the user...
+               $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
+               $logger->info( __METHOD__ . ": creating new user ($userName) - from: $from" );
+
+               try {
+                       // Insert the user into the local DB master
+                       $status = $user->addToDatabase();
+                       if ( !$status->isOK() ) {
+                               // @codeCoverageIgnoreStart
+                               $logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText() );
+                               $user->setId( 0 );
+                               $user->loadFromId();
+                               return false;
+                               // @codeCoverageIgnoreEnd
+                       }
+               } catch ( \Exception $ex ) {
+                       // @codeCoverageIgnoreStart
+                       $logger->error( __METHOD__ . ': failed with exception ' . $ex->getMessage() );
+                       // Do not keep throwing errors for a while
+                       $cache->set( $backoffKey, 1, 600 );
+                       // Bubble up error; which should normally trigger DB rollbacks
+                       throw $ex;
+                       // @codeCoverageIgnoreEnd
+               }
+
+               # Notify hooks (e.g. Newuserlog)
+               \Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
+               \Hooks::run( 'LocalUserCreated', array( $user, true ) );
+
+               # Update user count
+               \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+
+               # Watch user's userpage and talk page
+               $user->addWatch( $user->getUserPage(), \WatchedItem::IGNORE_USER_RIGHTS );
+
+               return true;
+       }
+
+       /**
+        * Prevent future sessions for the user
+        *
+        * The intention is that the named account will never again be usable for
+        * normal login (i.e. there is no way to undo the prevention of access).
+        *
+        * @private For use from \\User::newSystemUser only
+        * @param string $username
+        */
+       public function preventSessionsForUser( $username ) {
+               $this->preventUsers[$username] = true;
+
+               // Reset the user's token to kill existing sessions
+               $user = User::newFromName( $username );
+               if ( $user && $user->getToken() ) {
+                       $user->setToken( true );
+                       $user->saveSettings();
+               }
+
+               // Instruct the session providers to kill any other sessions too.
+               foreach ( $this->getProviders() as $provider ) {
+                       $provider->preventSessionsForUser( $username );
+               }
+       }
+
+       /**
+        * Test if a user is prevented
+        * @private For use from SessionBackend only
+        * @param string $username
+        * @return bool
+        */
+       public function isUserSessionPrevented( $username ) {
+               return !empty( $this->preventUsers[$username] );
+       }
+
+       /**
+        * Get the available SessionProviders
+        * @return SessionProvider[]
+        */
+       protected function getProviders() {
+               if ( $this->sessionProviders === null ) {
+                       $this->sessionProviders = array();
+                       foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
+                               $provider = \ObjectFactory::getObjectFromSpec( $spec );
+                               $provider->setLogger( $this->logger );
+                               $provider->setConfig( $this->config );
+                               $provider->setManager( $this );
+                               if ( isset( $this->sessionProviders[(string)$provider] ) ) {
+                                       throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
+                               }
+                               $this->sessionProviders[(string)$provider] = $provider;
+                       }
+               }
+               return $this->sessionProviders;
+       }
+
+       /**
+        * Get a session provider by name
+        *
+        * Generally, this will only be used by internal implementation of some
+        * special session-providing mechanism. General purpose code, if it needs
+        * to access a SessionProvider at all, will use Session::getProvider().
+        *
+        * @param string $name
+        * @return SessionProvider|null
+        */
+       public function getProvider( $name ) {
+               $providers = $this->getProviders();
+               return isset( $providers[$name] ) ? $providers[$name] : null;
+       }
+
+       /**
+        * Save all active sessions on shutdown
+        * @private For internal use with register_shutdown_function()
+        */
+       public function shutdown() {
+               if ( $this->allSessionBackends ) {
+                       $this->logger->debug( 'Saving all sessions on shutdown' );
+                       if ( session_id() !== '' ) {
+                               // @codeCoverageIgnoreStart
+                               session_write_close();
+                       }
+                       // @codeCoverageIgnoreEnd
+                       foreach ( $this->allSessionBackends as $backend ) {
+                               $backend->save( true );
+                       }
+               }
+       }
+
+       /**
+        * Fetch the SessionInfo(s) for a request
+        * @param WebRequest $request
+        * @return SessionInfo|null
+        */
+       private function getSessionInfoForRequest( WebRequest $request ) {
+               // Call all providers to fetch "the" session
+               $infos = array();
+               foreach ( $this->getProviders() as $provider ) {
+                       $info = $provider->provideSessionInfo( $request );
+                       if ( !$info ) {
+                               continue;
+                       }
+                       if ( $info->getProvider() !== $provider ) {
+                               throw new \UnexpectedValueException(
+                                       "$provider returned session info for a different provider: $info"
+                               );
+                       }
+                       $infos[] = $info;
+               }
+
+               // Sort the SessionInfos. Then find the first one that can be
+               // successfully loaded, and then all the ones after it with the same
+               // priority.
+               usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
+               $retInfos = array();
+               while ( $infos ) {
+                       $info = array_pop( $infos );
+                       if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
+                               $retInfos[] = $info;
+                               while ( $infos ) {
+                                       $info = array_pop( $infos );
+                                       if ( SessionInfo::compare( $retInfos[0], $info ) ) {
+                                               // We hit a lower priority, stop checking.
+                                               break;
+                                       }
+                                       if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
+                                               // This is going to error out below, but we want to
+                                               // provide a complete list.
+                                               $retInfos[] = $info;
+                                       }
+                               }
+                       }
+               }
+
+               if ( count( $retInfos ) > 1 ) {
+                       $ex = new \OverflowException(
+                               'Multiple sessions for this request tied for top priority: ' . join( ', ', $retInfos )
+                       );
+                       $ex->sessionInfos = $retInfos;
+                       throw $ex;
+               }
+
+               return $retInfos ? $retInfos[0] : null;
+       }
+
+       /**
+        * Load and verify the session info against the store
+        *
+        * @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance
+        * @param WebRequest $request
+        * @return bool Whether the session info matches the stored data (if any)
+        */
+       private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
+               $blob = $this->store->get( wfMemcKey( 'MWSession', $info->getId() ) );
+
+               $newParams = array();
+
+               if ( $blob !== false ) {
+                       // Sanity check: blob must be an array, if it's saved at all
+                       if ( !is_array( $blob ) ) {
+                               $this->logger->warning( "Session $info: Bad data" );
+                               return false;
+                       }
+
+                       // Sanity check: blob has data and metadata arrays
+                       if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
+                               !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
+                       ) {
+                               $this->logger->warning( "Session $info: Bad data structure" );
+                               return false;
+                       }
+
+                       $data = $blob['data'];
+                       $metadata = $blob['metadata'];
+
+                       // Sanity check: metadata must be an array and must contain certain
+                       // keys, if it's saved at all
+                       if ( !array_key_exists( 'userId', $metadata ) ||
+                               !array_key_exists( 'userName', $metadata ) ||
+                               !array_key_exists( 'userToken', $metadata ) ||
+                               !array_key_exists( 'provider', $metadata )
+                       ) {
+                               $this->logger->warning( "Session $info: Bad metadata" );
+                               return false;
+                       }
+
+                       // First, load the provider from metadata, or validate it against the metadata.
+                       $provider = $info->getProvider();
+                       if ( $provider === null ) {
+                               $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
+                               if ( !$provider ) {
+                                       $this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
+                                       return false;
+                               }
+                       } elseif ( $metadata['provider'] !== (string)$provider ) {
+                               $this->logger->warning( "Session $info: Wrong provider, " .
+                                       $metadata['provider'] . ' !== ' . $provider );
+                               return false;
+                       }
+
+                       // Load provider metadata from metadata, or validate it against the metadata
+                       $providerMetadata = $info->getProviderMetadata();
+                       if ( isset( $metadata['providerMetadata'] ) ) {
+                               if ( $providerMetadata === null ) {
+                                       $newParams['metadata'] = $metadata['providerMetadata'];
+                               } else {
+                                       try {
+                                               $newProviderMetadata = $provider->mergeMetadata(
+                                                       $metadata['providerMetadata'], $providerMetadata
+                                               );
+                                               if ( $newProviderMetadata !== $providerMetadata ) {
+                                                       $newParams['metadata'] = $newProviderMetadata;
+                                               }
+                                       } catch ( \UnexpectedValueException $ex ) {
+                                               $this->logger->warning( "Session $info: Metadata merge failed: " . $ex->getMessage() );
+                                               return false;
+                                       }
+                               }
+                       }
+
+                       // Next, load the user from metadata, or validate it against the metadata.
+                       $userInfo = $info->getUserInfo();
+                       if ( !$userInfo ) {
+                               // For loading, id is preferred to name.
+                               try {
+                                       if ( $metadata['userId'] ) {
+                                               $userInfo = UserInfo::newFromId( $metadata['userId'] );
+                                       } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
+                                               $userInfo = UserInfo::newFromName( $metadata['userName'] );
+                                       } else {
+                                               $userInfo = UserInfo::newAnonymous();
+                                       }
+                               } catch ( \InvalidArgumentException $ex ) {
+                                       $this->logger->error( "Session $info: " . $ex->getMessage() );
+                                       return false;
+                               }
+                               $newParams['userInfo'] = $userInfo;
+                       } else {
+                               // User validation passes if user ID matches, or if there
+                               // is no saved ID and the names match.
+                               if ( $metadata['userId'] ) {
+                                       if ( $metadata['userId'] !== $userInfo->getId() ) {
+                                               $this->logger->warning( "Session $info: User ID mismatch, " .
+                                                       $metadata['userId'] . ' !== ' . $userInfo->getId() );
+                                               return false;
+                                       }
+
+                                       // If the user was renamed, probably best to fail here.
+                                       if ( $metadata['userName'] !== null &&
+                                               $userInfo->getName() !== $metadata['userName']
+                                       ) {
+                                               $this->logger->warning( "Session $info: User ID matched but name didn't (rename?), " .
+                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               return false;
+                                       }
+
+                               } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
+                                       if ( $metadata['userName'] !== $userInfo->getName() ) {
+                                               $this->logger->warning( "Session $info: User name mismatch, " .
+                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               return false;
+                                       }
+                               } elseif ( !$userInfo->isAnon() ) {
+                                       // Metadata specifies an anonymous user, but the passed-in
+                                       // user isn't anonymous.
+                                       $this->logger->warning(
+                                               "Session $info: Metadata has an anonymous user, " .
+                                                       'but a non-anon user was provided'
+                                       );
+                                       return false;
+                               }
+                       }
+
+                       // And if we have a token in the metadata, it must match the loaded/provided user.
+                       if ( $metadata['userToken'] !== null &&
+                               $userInfo->getToken() !== $metadata['userToken']
+                       ) {
+                               $this->logger->warning( "Session $info: User token mismatch" );
+                               return false;
+                       }
+                       if ( !$userInfo->isVerified() ) {
+                               $newParams['userInfo'] = $userInfo->verified();
+                       }
+
+                       if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
+                               $newParams['remembered'] = true;
+                       }
+                       if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
+                               $newParams['forceHTTPS'] = true;
+                       }
+
+                       if ( !$info->isIdSafe() ) {
+                               $newParams['idIsSafe'] = true;
+                       }
+               } else {
+                       // No metadata, so we can't load the provider if one wasn't given.
+                       if ( $info->getProvider() === null ) {
+                               $this->logger->warning( "Session $info: Null provider and no metadata" );
+                               return false;
+                       }
+
+                       // If no user was provided and no metadata, it must be anon.
+                       if ( !$info->getUserInfo() ) {
+                               if ( $info->getProvider()->canChangeUser() ) {
+                                       $newParams['userInfo'] = UserInfo::newAnonymous();
+                               } else {
+                                       $this->logger->info(
+                                               "Session $info: No user provided and provider cannot set user"
+                                       );
+                                       return false;
+                               }
+                       } elseif ( !$info->getUserInfo()->isVerified() ) {
+                               $this->logger->warning(
+                                       "Session $info: Unverified user provided and no metadata to auth it"
+                               );
+                               return false;
+                       }
+
+                       $data = false;
+                       $metadata = false;
+
+                       if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
+                               // The ID doesn't come from the user, so it should be safe
+                               // (and if not, nothing we can do about it anyway)
+                               $newParams['idIsSafe'] = true;
+                       }
+               }
+
+               // Construct the replacement SessionInfo, if necessary
+               if ( $newParams ) {
+                       $newParams['copyFrom'] = $info;
+                       $info = new SessionInfo( $info->getPriority(), $newParams );
+               }
+
+               // Allow the provider to check the loaded SessionInfo
+               $providerMetadata = $info->getProviderMetadata();
+               if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
+                       return false;
+               }
+               if ( $providerMetadata !== $info->getProviderMetadata() ) {
+                       $info = new SessionInfo( $info->getPriority(), array(
+                               'metadata' => $providerMetadata,
+                               'copyFrom' => $info,
+                       ) );
+               }
+
+               // Give hooks a chance to abort. Combined with the SessionMetadata
+               // hook, this can allow for tying a session to an IP address or the
+               // like.
+               $reason = 'Hook aborted';
+               if ( !\Hooks::run(
+                       'SessionCheckInfo',
+                       array( &$reason, $info, $request, $metadata, $data )
+               ) ) {
+                       $this->logger->warning( "Session $info: $reason" );
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Create a session corresponding to the passed SessionInfo
+        * @private For use by a SessionProvider that needs to specially create its
+        *  own session.
+        * @param SessionInfo $info
+        * @param WebRequest $request
+        * @return Session
+        */
+       public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
+               $id = $info->getId();
+
+               if ( !isset( $this->allSessionBackends[$id] ) ) {
+                       if ( !isset( $this->allSessionIds[$id] ) ) {
+                               $this->allSessionIds[$id] = new SessionId( $id );
+                       }
+                       $backend = new SessionBackend(
+                               $this->allSessionIds[$id],
+                               $info,
+                               $this->store,
+                               $this->logger,
+                               $this->config->get( 'ObjectCacheSessionExpiry' )
+                       );
+                       $this->allSessionBackends[$id] = $backend;
+                       $delay = $backend->delaySave();
+               } else {
+                       $backend = $this->allSessionBackends[$id];
+                       $delay = $backend->delaySave();
+                       if ( $info->wasPersisted() ) {
+                               $backend->persist();
+                       }
+                       if ( $info->wasRemembered() ) {
+                               $backend->setRememberUser( true );
+                       }
+               }
+
+               $request->setSessionId( $backend->getSessionId() );
+               $session = $backend->getSession( $request );
+
+               if ( !$info->isIdSafe() ) {
+                       $session->resetId();
+               }
+
+               \ScopedCallback::consume( $delay );
+               return $session;
+       }
+
+       /**
+        * Deregister a SessionBackend
+        * @private For use from \\MediaWiki\\Session\\SessionBackend only
+        * @param SessionBackend $backend
+        */
+       public function deregisterSessionBackend( SessionBackend $backend ) {
+               $id = $backend->getId();
+               if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
+                       $this->allSessionBackends[$id] !== $backend ||
+                       $this->allSessionIds[$id] !== $backend->getSessionId()
+               ) {
+                       throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
+               }
+
+               unset( $this->allSessionBackends[$id] );
+               // Explicitly do not unset $this->allSessionIds[$id]
+       }
+
+       /**
+        * Change a SessionBackend's ID
+        * @private For use from \\MediaWiki\\Session\\SessionBackend only
+        * @param SessionBackend $backend
+        */
+       public function changeBackendId( SessionBackend $backend ) {
+               $sessionId = $backend->getSessionId();
+               $oldId = (string)$sessionId;
+               if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
+                       $this->allSessionBackends[$oldId] !== $backend ||
+                       $this->allSessionIds[$oldId] !== $sessionId
+               ) {
+                       throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
+               }
+
+               $newId = $this->generateSessionId();
+
+               unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
+               $sessionId->setId( $newId );
+               $this->allSessionBackends[$newId] = $backend;
+               $this->allSessionIds[$newId] = $sessionId;
+       }
+
+       /**
+        * Generate a new random session ID
+        * @return string
+        */
+       public function generateSessionId() {
+               do {
+                       $id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
+                       $key = wfMemcKey( 'MWSession', $id );
+               } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
+               return $id;
+       }
+
+       /**
+        * Call setters on a PHPSessionHandler
+        * @private Use PhpSessionHandler::install()
+        * @param PHPSessionHandler $handler
+        */
+       public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
+               $handler->setManager( $this, $this->store, $this->logger );
+       }
+
+       /**
+        * Reset the internal caching for unit testing
+        */
+       public static function resetCache() {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       // @codeCoverageIgnoreStart
+                       throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
+                       // @codeCoverageIgnoreEnd
+               }
+
+               self::$globalSession = null;
+               self::$globalSessionRequest = null;
+       }
+
+       /**@}*/
+
+}
diff --git a/includes/session/SessionManagerInterface.php b/includes/session/SessionManagerInterface.php
new file mode 100644 (file)
index 0000000..67d6f5d
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * MediaWiki\Session entry point interface
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerAwareInterface;
+use WebRequest;
+
+/**
+ * This exists to make IDEs happy, so they don't see the
+ * internal-but-required-to-be-public methods on SessionManager.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+interface SessionManagerInterface extends LoggerAwareInterface {
+       /**
+        * Fetch the persisted session ID in a request.
+        *
+        * Note this is not the same thing as whether the session associated with
+        * the request is currently persistent, as the session might have been
+        * first made persistent during this request.
+        *
+        * @param WebRequest $request
+        * @return string|null
+        * @throws \\OverflowException if there are multiple sessions tied for top
+        *  priority in the request. Exception has a property "sessionInfos"
+        *  holding the SessionInfo objects for the sessions involved.
+        */
+       public function getPersistedSessionId( WebRequest $request );
+
+       /**
+        * Fetch the session for a request
+        *
+        * @note You probably want to use $request->getSession() instead. It's more
+        *  efficient and doesn't break FauxRequests or sessions that were changed
+        *  by $this->getSessionById() or $this->getEmptySession().
+        * @param WebRequest $request Any existing associated session will be reset
+        *  to the session corresponding to the data in the request itself.
+        * @return Session
+        * @throws \\OverflowException if there are multiple sessions tied for top
+        *  priority in the request. Exception has a property "sessionInfos"
+        *  holding the SessionInfo objects for the sessions involved.
+        */
+       public function getSessionForRequest( WebRequest $request );
+
+       /**
+        * Fetch a session by ID
+        * @param string $id
+        * @param bool $noEmpty Don't return an empty session
+        * @param WebRequest|null $request Corresponding request. Any existing
+        *  session associated with this WebRequest object will be overwritten.
+        * @return Session|null
+        */
+       public function getSessionById( $id, $noEmpty = false, WebRequest $request = null );
+
+       /**
+        * Fetch a new, empty session
+        *
+        * The first provider configured that is able to provide an empty session
+        * will be used.
+        *
+        * @param WebRequest|null $request Corresponding request. Any existing
+        *  session associated with this WebRequest object will be overwritten.
+        * @return Session
+        */
+       public function getEmptySession( WebRequest $request = null );
+
+       /**
+        * Return the HTTP headers that need varying on.
+        *
+        * The return value is such that someone could theoretically do this:
+        * @code
+        *  foreach ( $provider->getVaryHeaders() as $header => $options ) {
+        *      $outputPage->addVaryHeader( $header, $options );
+        *  }
+        * @endcode
+        *
+        * @return array
+        */
+       public function getVaryHeaders();
+
+       /**
+        * Return the list of cookies that need varying on.
+        * @return string[]
+        */
+       public function getVaryCookies();
+
+}
diff --git a/includes/session/SessionProvider.php b/includes/session/SessionProvider.php
new file mode 100644 (file)
index 0000000..0fd3a71
--- /dev/null
@@ -0,0 +1,487 @@
+<?php
+/**
+ * MediaWiki session provider base class
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Config;
+use Language;
+use WebRequest;
+
+/**
+ * A SessionProvider provides SessionInfo and support for Session
+ *
+ * A SessionProvider is responsible for taking a WebRequest and determining
+ * the authenticated session that it's a part of. It does this by returning an
+ * SessionInfo object with basic information about the session it thinks is
+ * associated with the request, namely the session ID and possibly the
+ * authenticated user the session belongs to.
+ *
+ * The SessionProvider also provides for updating the WebResponse with
+ * information necessary to provide the client with data that the client will
+ * send with later requests, and for populating the Vary and Key headers with
+ * the data necessary to correctly vary the cache on these client requests.
+ *
+ * An important part of the latter is indicating whether it even *can* tell the
+ * client to include such data in future requests, via the persistsSessionId()
+ * and canChangeUser() methods. The cases are (in order of decreasing
+ * commonness):
+ *  - Cannot persist ID, no changing User: The request identifies and
+ *    authenticates a particular local user, and the client cannot be
+ *    instructed to include an arbitrary session ID with future requests. For
+ *    example, OAuth or SSL certificate auth.
+ *  - Can persist ID and can change User: The client can be instructed to
+ *    return at least one piece of arbitrary data, that being the session ID.
+ *    The user identity might also be given to the client, otherwise it's saved
+ *    in the session data. For example, cookie-based sessions.
+ *  - Can persist ID but no changing User: The request uniquely identifies and
+ *    authenticates a local user, and the client can be instructed to return an
+ *    arbitrary session ID with future requests. For example, HTTP Digest
+ *    authentication might somehow use the 'opaque' field as a session ID
+ *    (although getting MediaWiki to return 401 responses without breaking
+ *    other stuff might be a challenge).
+ *  - Cannot persist ID but can change User: I can't think of a way this
+ *    would make sense.
+ *
+ * Note that many methods that are technically "cannot persist ID" could be
+ * turned into "can persist ID but not changing User" using a session cookie,
+ * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
+ * session cookie names should be used for different providers to avoid
+ * collisions.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
+
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var Config */
+       protected $config;
+
+       /** @var SessionManager */
+       protected $manager;
+
+       /** @var int Session priority. Used for the default newSessionInfo(), but
+        * could be used by subclasses too.
+        */
+       protected $priority;
+
+       /**
+        * @note To fully initialize a SessionProvider, the setLogger(),
+        *  setConfig(), and setManager() methods must be called (and should be
+        *  called in that order). Failure to do so is liable to cause things to
+        *  fail unexpectedly.
+        */
+       public function __construct() {
+               $this->priority = SessionInfo::MIN_PRIORITY + 10;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Set configuration
+        * @param Config $config
+        */
+       public function setConfig( Config $config ) {
+               $this->config = $config;
+       }
+
+       /**
+        * Set the session manager
+        * @param SessionManager $manager
+        */
+       public function setManager( SessionManager $manager ) {
+               $this->manager = $manager;
+       }
+
+       /**
+        * Get the session manager
+        * @return SessionManager
+        */
+       public function getManager() {
+               return $this->manager;
+       }
+
+       /**
+        * Provide session info for a request
+        *
+        * If no session exists for the request, return null. Otherwise return an
+        * SessionInfo object identifying the session.
+        *
+        * If multiple SessionProviders provide sessions, the one with highest
+        * priority wins. In case of a tie, an exception is thrown.
+        * SessionProviders are encouraged to make priorities user-configurable
+        * unless only max-priority makes sense.
+        *
+        * @warning This will be called early in the MediaWiki setup process,
+        *  before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
+        *  pieces of the main RequestContext are set up! If you try to use these,
+        *  things *will* break.
+        * @note The SessionProvider must not attempt to auto-create users.
+        *  MediaWiki will do this later (when it's safe) if the chosen session has
+        *  a user with a valid name but no ID.
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param WebRequest $request
+        * @return SessionInfo|null
+        */
+       abstract public function provideSessionInfo( WebRequest $request );
+
+       /**
+        * Provide session info for a new, empty session
+        *
+        * Return null if such a session cannot be created. This base
+        * implementation assumes that it only makes sense if a session ID can be
+        * persisted and changing users is allowed.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param string|null $id ID to force for the new session
+        * @return SessionInfo|null
+        *  If non-null, must return true for $info->isIdSafe(); pass true for
+        *  $data['idIsSafe'] to ensure this.
+        */
+       public function newSessionInfo( $id = null ) {
+               if ( $this->canChangeUser() && $this->persistsSessionId() ) {
+                       return new SessionInfo( $this->priority, array(
+                               'id' => $id,
+                               'provider' => $this,
+                               'persisted' => false,
+                               'idIsSafe' => true,
+                       ) );
+               }
+               return null;
+       }
+
+       /**
+        * Merge saved session provider metadata
+        *
+        * The default implementation checks that anything in both arrays is
+        * identical, then returns $providedMetadata.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param array $savedMetadata Saved provider metadata
+        * @param array $providedMetadata Provided provider metadata
+        * @return array Resulting metadata
+        * @throws \UnexpectedValueException If the metadata cannot be merged
+        */
+       public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
+               foreach ( $providedMetadata as $k => $v ) {
+                       if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
+                               throw new \UnexpectedValueException( "Key \"$k\" changed" );
+                       }
+               }
+               return $providedMetadata;
+       }
+
+       /**
+        * Validate a loaded SessionInfo and refresh provider metadata
+        *
+        * This is similar in purpose to the 'SessionCheckInfo' hook, and also
+        * allows for updating the provider metadata. On failure, the provider is
+        * expected to write an appropriate message to its logger.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param SessionInfo $info
+        * @param WebRequest $request
+        * @param array|null &$metadata Provider metadata, may be altered.
+        * @return bool Return false to reject the SessionInfo after all.
+        */
+       public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
+               return true;
+       }
+
+       /**
+        * Indicate whether self::persistSession() can save arbitrary session IDs
+        *
+        * If false, any session passed to self::persistSession() will have an ID
+        * that was originally provided by self::provideSessionInfo().
+        *
+        * If true, the provider may be passed sessions with arbitrary session IDs,
+        * and will be expected to manipulate the request in such a way that future
+        * requests will cause self::provideSessionInfo() to provide a SessionInfo
+        * with that ID.
+        *
+        * For example, a session provider for OAuth would function by matching the
+        * OAuth headers to a particular user, and then would use self::hashToSessionId()
+        * to turn the user and OAuth client ID (and maybe also the user token and
+        * client secret) into a session ID, and therefore can't easily assign that
+        * user+client a different ID. Similarly, a session provider for SSL client
+        * certificates would function by matching the certificate to a particular
+        * user, and then would use self::hashToSessionId() to turn the user and
+        * certificate fingerprint into a session ID, and therefore can't easily
+        * assign a different ID either. On the other hand, a provider that saves
+        * the session ID into a cookie can easily just set the cookie to a
+        * different value.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionBackend only
+        * @return bool
+        */
+       abstract public function persistsSessionId();
+
+       /**
+        * Indicate whether the user associated with the request can be changed
+        *
+        * If false, any session passed to self::persistSession() will have a user
+        * that was originally provided by self::provideSessionInfo(). Further,
+        * self::provideSessionInfo() may only provide sessions that have a user
+        * already set.
+        *
+        * If true, the provider may be passed sessions with arbitrary users, and
+        * will be expected to manipulate the request in such a way that future
+        * requests will cause self::provideSessionInfo() to provide a SessionInfo
+        * with that ID. This can be as simple as not passing any 'userInfo' into
+        * SessionInfo's constructor, in which case SessionInfo will load the user
+        * from the saved session's metadata.
+        *
+        * For example, a session provider for OAuth or SSL client certificates
+        * would function by matching the OAuth headers or certificate to a
+        * particular user, and thus would return false here since it can't
+        * arbitrarily assign those OAuth credentials or that certificate to a
+        * different user. A session provider that shoves information into cookies,
+        * on the other hand, could easily do so.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionBackend only
+        * @return bool
+        */
+       abstract public function canChangeUser();
+
+       /**
+        * Notification that the session ID was reset
+        *
+        * No need to persist here, persistSession() will be called if appropriate.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionBackend only
+        * @param SessionBackend $session Session to persist
+        * @param string $oldId Old session ID
+        * @codeCoverageIgnore
+        */
+       public function sessionIdWasReset( SessionBackend $session, $oldId ) {
+       }
+
+       /**
+        * Persist a session into a request/response
+        *
+        * For example, you might set cookies for the session's ID, user ID, user
+        * name, and user token on the passed request.
+        *
+        * To correctly persist a user independently of the session ID, the
+        * provider should persist both the user ID (or name, but preferably the
+        * ID) and the user token. When reading the data from the request, it
+        * should construct a User object from the ID/name and then verify that the
+        * User object's token matches the token included in the request. Should
+        * the tokens not match, an anonymous user *must* be passed to
+        * SessionInfo::__construct().
+        *
+        * When persisting a user independently of the session ID,
+        * $session->shouldRememberUser() should be checked first. If this returns
+        * false, the user token *must not* be saved to cookies. The user name
+        * and/or ID may be persisted, and should be used to construct an
+        * unverified UserInfo to pass to SessionInfo::__construct().
+        *
+        * A backend that cannot persist sesison ID or user info should implement
+        * this as a no-op.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionBackend only
+        * @param SessionBackend $session Session to persist
+        * @param WebRequest $request Request into which to persist the session
+        */
+       abstract public function persistSession( SessionBackend $session, WebRequest $request );
+
+       /**
+        * Remove any persisted session from a request/response
+        *
+        * For example, blank and expire any cookies set by self::persistSession().
+        *
+        * A backend that cannot persist sesison ID or user info should implement
+        * this as a no-op.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param WebRequest $request Request from which to remove any session data
+        */
+       abstract public function unpersistSession( WebRequest $request );
+
+       /**
+        * Prevent future sessions for the user
+        *
+        * If the provider is capable of returning a SessionInfo with a verified
+        * UserInfo for the named user in some manner other than by validating
+        * against $user->getToken(), steps must be taken to prevent that from
+        * occurring in the future. This might add the username to a blacklist, or
+        * it might just delete whatever authentication credentials would allow
+        * such a session in the first place (e.g. remove all OAuth grants or
+        * delete record of the SSL client certificate).
+        *
+        * The intention is that the named account will never again be usable for
+        * normal login (i.e. there is no way to undo the prevention of access).
+        *
+        * Note that the passed user name might not exist locally (i.e.
+        * User::idFromName( $username ) === 0); the name should still be
+        * prevented, if applicable.
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @param string $username
+        */
+       public function preventSessionsForUser( $username ) {
+               if ( !$this->canChangeUser() ) {
+                       throw new \BadMethodCallException(
+                               __METHOD__ . ' must be implmented when canChangeUser() is false'
+                       );
+               }
+       }
+
+       /**
+        * Return the HTTP headers that need varying on.
+        *
+        * The return value is such that someone could theoretically do this:
+        * @code
+        *  foreach ( $provider->getVaryHeaders() as $header => $options ) {
+        *      $outputPage->addVaryHeader( $header, $options );
+        *  }
+        * @endcode
+        *
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @return array
+        */
+       public function getVaryHeaders() {
+               return array();
+       }
+
+       /**
+        * Return the list of cookies that need varying on.
+        * @protected For use by \\MediaWiki\\Session\\SessionManager only
+        * @return string[]
+        */
+       public function getVaryCookies() {
+               return array();
+       }
+
+       /**
+        * Get a suggested username for the login form
+        * @protected For use by \\MediaWiki\\Session\\SessionBackend only
+        * @param WebRequest $request
+        * @return string|null
+        */
+       public function suggestLoginUsername( WebRequest $request ) {
+               return null;
+       }
+
+       /**
+        * Fetch the rights allowed the user when the specified session is active.
+        * @param SessionBackend $backend
+        * @return null|string[] Allowed user rights, or null to allow all.
+        */
+       public function getAllowedUserRights( SessionBackend $backend ) {
+               if ( $backend->getProvider() !== $this ) {
+                       // Not that this should ever happen...
+                       throw new \InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
+               }
+
+               return null;
+       }
+
+       /**
+        * @note Only override this if it makes sense to instantiate multiple
+        *  instances of the provider. Value returned must be unique across
+        *  configured providers. If you override this, you'll likely need to
+        *  override self::describeMessage() as well.
+        * @return string
+        */
+       public function __toString() {
+               return get_class( $this );
+       }
+
+       /**
+        * Return a Message identifying this session type
+        *
+        * This default implementation takes the class name, lowercases it,
+        * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
+        * determine the message key. For example, MediaWiki\\Session\\CookieSessionProvider
+        * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
+        *
+        * @note If self::__toString() is overridden, this will likely need to be
+        *  overridden as well.
+        * @warning This will be called early during MediaWiki startup. Do not
+        *  use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
+        *  RequestContext from this method!
+        * @return Message
+        */
+       protected function describeMessage() {
+               return wfMessage(
+                       'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) )
+               );
+       }
+
+       public function describe( Language $lang ) {
+               $msg = $this->describeMessage();
+               $msg->inLanguage( $lang );
+               if ( $msg->isDisabled() ) {
+                       $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
+               }
+               return $msg->plain();
+       }
+
+       public function whyNoSession() {
+               return null;
+       }
+
+       /**
+        * Hash data as a session ID
+        *
+        * Generally this will only be used when self::persistsSessionId() is false and
+        * the provider has to base the session ID on the verified user's identity
+        * or other static data.
+        *
+        * @param string $data
+        * @param string|null $key Defaults to $this->config->get( 'SecretKey' )
+        * @return string
+        */
+       final protected function hashToSessionId( $data, $key = null ) {
+               if ( !is_string( $data ) ) {
+                       throw new \InvalidArgumentException(
+                               '$data must be a string, ' . gettype( $data ) . ' was passed'
+                       );
+               }
+               if ( $key !== null && !is_string( $key ) ) {
+                       throw new \InvalidArgumentException(
+                               '$key must be a string or null, ' . gettype( $key ) . ' was passed'
+                       );
+               }
+
+               $hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
+               if ( strlen( $hash ) < 32 ) {
+                       // Should never happen, even md5 is 128 bits
+                       // @codeCoverageIgnoreStart
+                       throw new \UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
+                       // @codeCoverageIgnoreEnd
+               }
+               if ( strlen( $hash ) >= 40 ) {
+                       $hash = wfBaseConvert( $hash, 16, 32, 32 );
+               }
+               return substr( $hash, -32 );
+       }
+
+}
diff --git a/includes/session/SessionProviderInterface.php b/includes/session/SessionProviderInterface.php
new file mode 100644 (file)
index 0000000..02ae23d
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * MediaWiki\Session\Provider interface
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use Language;
+
+/**
+ * This exists to make IDEs happy, so they don't see the
+ * internal-but-required-to-be-public methods on SessionProvider.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+interface SessionProviderInterface {
+
+       /**
+        * Return an identifier for this session type
+        *
+        * @param Language $lang Language to use.
+        * @return string
+        */
+       public function describe( Language $lang );
+
+       /**
+        * Return a Message for why sessions might not be being persisted.
+        *
+        * For example, "check whether you're blocking our cookies".
+        *
+        * @return Message|null
+        */
+       public function whyNoSession();
+
+}
diff --git a/includes/session/UserInfo.php b/includes/session/UserInfo.php
new file mode 100644 (file)
index 0000000..e844bb6
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+/**
+ * MediaWiki session user info
+ *
+ * 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 Session
+ */
+
+namespace MediaWiki\Session;
+
+use User;
+
+/**
+ * Object holding data about a session's user
+ *
+ * In general, this class exists for two purposes:
+ * - User doesn't distinguish between "anonymous user" and "non-anonymous user
+ *   that doesn't exist locally", while we do need to.
+ * - We also need the "verified" property described below; tracking it via
+ *   another data item to SessionInfo's constructor makes things much more
+ *   confusing.
+ *
+ * A UserInfo may be "verified". This indicates that the creator knows that the
+ * request really comes from that user, whether that's by validating OAuth
+ * credentials, SSL client certificates, or by having both the user ID and
+ * token available from cookies.
+ *
+ * An "unverified" UserInfo should be used when it's not possible to
+ * authenticate the user, e.g. the user ID cookie is set but the user Token
+ * cookie isn't. If the Token is available but doesn't match, don't return a
+ * UserInfo at all.
+ *
+ * @ingroup Session
+ * @since 1.27
+ */
+final class UserInfo {
+       private $verified = false;
+
+       /** @var User|null */
+       private $user = null;
+
+       private function __construct( User $user = null, $verified ) {
+               if ( $user && $user->isAnon() && !User::isUsableName( $user->getName() ) ) {
+                       $this->verified = true;
+                       $this->user = null;
+               } else {
+                       $this->verified = $verified;
+                       $this->user = $user;
+               }
+       }
+
+       /**
+        * Create an instance for an anonymous (i.e. not logged in) user
+        *
+        * Logged-out users are always "verified".
+        *
+        * @return UserInfo
+        */
+       public static function newAnonymous() {
+               return new self( null, true );
+       }
+
+       /**
+        * Create an instance for a logged-in user by ID
+        * @param int $id User ID
+        * @param bool $verified True if the user is verified
+        * @return UserInfo
+        */
+       public static function newFromId( $id, $verified = false ) {
+               $user = User::newFromId( $id );
+
+               // Ensure the ID actually exists
+               $user->load();
+               if ( $user->isAnon() ) {
+                       throw new \InvalidArgumentException( 'Invalid ID' );
+               }
+
+               return new self( $user, $verified );
+       }
+
+       /**
+        * Create an instance for a logged-in user by name
+        * @param string $name User name (need not exist locally)
+        * @param bool $verified True if the user is verified
+        * @return UserInfo
+        */
+       public static function newFromName( $name, $verified = false ) {
+               $user = User::newFromName( $name, 'usable' );
+               if ( !$user ) {
+                       throw new \InvalidArgumentException( 'Invalid user name' );
+               }
+               return new self( $user, $verified );
+       }
+
+       /**
+        * Create an instance from an existing User object
+        * @param User $user (need not exist locally)
+        * @param bool $verified True if the user is verified
+        * @return UserInfo
+        */
+       public static function newFromUser( User $user, $verified = false ) {
+               return new self( $user, $verified );
+       }
+
+       /**
+        * Return whether this is an anonymous user
+        * @return bool
+        */
+       public function isAnon() {
+               return $this->user === null;
+       }
+
+       /**
+        * Return whether this represents a verified user
+        * @return bool
+        */
+       public function isVerified() {
+               return $this->verified;
+       }
+
+       /**
+        * Return the user ID
+        * @note Do not use this to test for anonymous users!
+        * @return int
+        */
+       public function getId() {
+               return $this->user === null ? 0 : $this->user->getId();
+       }
+
+       /**
+        * Return the user name
+        * @return string|null
+        */
+       public function getName() {
+               return $this->user === null ? null : $this->user->getName();
+       }
+
+       /**
+        * Return the user token
+        * @return string|null
+        */
+       public function getToken() {
+               return $this->user === null || $this->user->getId() === 0 ? null : $this->user->getToken( true );
+       }
+
+       /**
+        * Return a User object
+        * @return User
+        */
+       public function getUser() {
+               return $this->user === null ? new User : $this->user;
+       }
+
+       /**
+        * Return a verified version of this object
+        * @return UserInfo
+        */
+       public function verified() {
+               return $this->verified ? $this : new self( $this->user, true );
+       }
+
+       public function __toString() {
+               if ( $this->user === null ) {
+                       return '<anon>';
+               }
+               return '<' .
+                       ( $this->verified ? '+' : '-' ) . ':' .
+                       $this->getId() . ':' . $this->getName() .
+                       '>';
+       }
+
+}
index 65a4eb9..0417146 100644 (file)
@@ -675,6 +675,16 @@ class SpecialPage {
                return $group;
        }
 
+       /**
+        * Indicates whether this special page may perform database writes
+        *
+        * @return bool
+        * @since 1.27
+        */
+       public function doesWrites() {
+               return false;
+       }
+
        /**
         * Under which header this special page is listed in Special:SpecialPages
         * See messages 'specialpages-group-*' for valid names
index 47b4fc8..2bb92bc 100644 (file)
@@ -90,12 +90,14 @@ class SpecialPageFactory {
                'Unblock' => 'SpecialUnblock',
                'BlockList' => 'SpecialBlockList',
                'ChangePassword' => 'SpecialChangePassword',
+               'BotPasswords' => 'SpecialBotPasswords',
                'PasswordReset' => 'SpecialPasswordReset',
                'DeletedContributions' => 'DeletedContributionsPage',
                'Preferences' => 'SpecialPreferences',
                'ResetTokens' => 'SpecialResetTokens',
                'Contributions' => 'SpecialContributions',
                'Listgrouprights' => 'SpecialListGroupRights',
+               'Listgrants' => 'SpecialListGrants',
                'Listusers' => 'SpecialListUsers',
                'Listadmins' => 'SpecialListAdmins',
                'Listbots' => 'SpecialListBots',
@@ -498,7 +500,6 @@ class SpecialPageFactory {
         * @return bool
         */
        public static function executePath( Title &$title, IContextSource &$context, $including = false ) {
-
                // @todo FIXME: Redirects broken due to this call
                $bits = explode( '/', $title->getDBkey(), 2 );
                $name = $bits[0];
@@ -507,8 +508,8 @@ class SpecialPageFactory {
                } else {
                        $par = $bits[1];
                }
+
                $page = self::getPage( $name );
-               // Nonexistent?
                if ( !$page ) {
                        $context->getOutput()->setArticleRelated( false );
                        $context->getOutput()->setRobotPolicy( 'noindex,nofollow' );
@@ -523,6 +524,15 @@ class SpecialPageFactory {
                        return false;
                }
 
+               if ( !$including ) {
+                       // Narrow DB query expectations for this HTTP request
+                       $trxLimits = $context->getConfig()->get( 'TrxProfilerLimits' );
+                       $trxProfiler = Profiler::instance()->getTransactionProfiler();
+                       if ( $context->getRequest()->wasPosted() && !$page->doesWrites() ) {
+                               $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
+                       }
+               }
+
                // Page exists, set the context
                $page->setContext( $context );
 
index 226d633..d198ea1 100644 (file)
@@ -51,6 +51,10 @@ class SpecialBlock extends FormSpecialPage {
                parent::__construct( 'Block', 'block' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Checks that the user can unblock themselves if they are trying to do so
         *
@@ -646,12 +650,25 @@ class SpecialBlock extends FormSpecialPage {
                        return array( 'badipaddress' );
                }
 
-               if ( ( strlen( $data['Expiry'] ) == 0 ) || ( strlen( $data['Expiry'] ) > 50 )
-                       || !self::parseExpiryInput( $data['Expiry'] )
+               $expiryTime = self::parseExpiryInput( $data['Expiry'] );
+
+               if (
+                       // an expiry time is needed
+                       ( strlen( $data['Expiry'] ) == 0 ) ||
+                       // can't be a larger string as 50 (it should be a time format in any way)
+                       ( strlen( $data['Expiry'] ) > 50 ) ||
+                       // check, if the time could be parsed
+                       !$expiryTime
                ) {
                        return array( 'ipb_expiry_invalid' );
                }
 
+               // an expiry time should be in the future, not in the
+               // past (wouldn't make any sense) - bug T123069
+               if ( $expiryTime < wfTimestampNow() ) {
+                       return array( 'ipb_expiry_old' );
+               }
+
                if ( !isset( $data['DisableEmail'] ) ) {
                        $data['DisableEmail'] = false;
                }
@@ -695,7 +712,7 @@ class SpecialBlock extends FormSpecialPage {
                $block->setBlocker( $performer );
                # Truncate reason for whole multibyte characters
                $block->mReason = $wgContLang->truncate( $data['Reason'][0], 255 );
-               $block->mExpiry = self::parseExpiryInput( $data['Expiry'] );
+               $block->mExpiry = $expiryTime;
                $block->prevents( 'createaccount', $data['CreateAccount'] );
                $block->prevents( 'editownusertalk', ( !$wgBlockAllowsUTEdit || $data['DisableUTEdit'] ) );
                $block->prevents( 'sendemail', $data['DisableEmail'] );
diff --git a/includes/specials/SpecialBotPasswords.php b/includes/specials/SpecialBotPasswords.php
new file mode 100644 (file)
index 0000000..93c36ab
--- /dev/null
@@ -0,0 +1,357 @@
+<?php
+/**
+ * Implements Special:BotPasswords
+ *
+ * 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 SpecialPage
+ */
+
+/**
+ * Let users manage bot passwords
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialBotPasswords extends FormSpecialPage {
+
+       /** @var int Central user ID */
+       private $userId = 0;
+
+       /** @var BotPassword|null Bot password being edited, if any */
+       private $botPassword = null;
+
+       /** @var string Operation being performed: create, update, delete */
+       private $operation = null;
+
+       /** @var string New password set, for communication between onSubmit() and onSuccess() */
+       private $password = null;
+
+       public function __construct() {
+               parent::__construct( 'BotPasswords', 'editmyprivateinfo' );
+       }
+
+       /**
+        * @return bool
+        */
+       public function isListed() {
+               return $this->getConfig()->get( 'EnableBotPasswords' );
+       }
+
+       /**
+        * Main execution point
+        * @param string|null $par
+        */
+       function execute( $par ) {
+               $this->getOutput()->disallowUserJs();
+               $this->requireLogin();
+
+               $par = trim( $par );
+               if ( strlen( $par ) === 0 ) {
+                       $par = null;
+               } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) {
+                       throw new ErrorPageError( 'botpasswords', 'botpasswords-bad-appid',
+                               array( htmlspecialchars( $par ) ) );
+               }
+
+               parent::execute( $par );
+       }
+
+       protected function checkExecutePermissions( User $user ) {
+               parent::checkExecutePermissions( $user );
+
+               if ( !$this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                       throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
+               }
+
+               $this->userId = CentralIdLookup::factory()->centralIdFromLocalUser( $this->getUser() );
+               if ( !$this->userId ) {
+                       throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
+               }
+       }
+
+       protected function getFormFields() {
+               $that = $this;
+               $user = $this->getUser();
+               $request = $this->getRequest();
+
+               $fields = array();
+
+               if ( $this->par !== null ) {
+                       $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par );
+                       if ( !$this->botPassword ) {
+                               $this->botPassword = BotPassword::newUnsaved( array(
+                                       'centralId' => $this->userId,
+                                       'appId' => $this->par,
+                               ) );
+                       }
+
+                       $sep = BotPassword::getSeparator();
+                       $fields[] = array(
+                               'type' => 'info',
+                               'label-message' => 'username',
+                               'default' => $this->getUser()->getName() . $sep . $this->par
+                       );
+
+                       if ( $this->botPassword->isSaved() ) {
+                               $fields['resetPassword'] = array(
+                                       'type' => 'check',
+                                       'label-message' => 'botpasswords-label-resetpassword',
+                               );
+                       }
+
+                       $lang = $this->getLanguage();
+                       $showGrants = MWGrants::getValidGrants();
+                       $fields['grants'] = array(
+                               'type' => 'checkmatrix',
+                               'label-message' => 'botpasswords-label-grants',
+                               'help-message' => 'botpasswords-help-grants',
+                               'columns' => array(
+                                       $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
+                               ),
+                               'rows' => array_combine(
+                                       array_map( 'MWGrants::getGrantsLink', $showGrants ),
+                                       $showGrants
+                               ),
+                               'default' => array_map(
+                                       function( $g ) {
+                                               return "grant-$g";
+                                       },
+                                       $this->botPassword->getGrants()
+                               ),
+                               'tooltips' => array_combine(
+                                       array_map( 'MWGrants::getGrantsLink', $showGrants ),
+                                       array_map(
+                                               function( $rights ) use ( $lang ) {
+                                                       return $lang->semicolonList( array_map( 'User::getRightDescription', $rights ) );
+                                               },
+                                               array_intersect_key( MWGrants::getRightsByGrant(), array_flip( $showGrants ) )
+                                       )
+                               ),
+                               'force-options-on' => array_map(
+                                       function( $g ) {
+                                               return "grant-$g";
+                                       },
+                                       MWGrants::getHiddenGrants()
+                               ),
+                       );
+
+                       $fields['restrictions'] = array(
+                               'type' => 'textarea',
+                               'label-message' => 'botpasswords-label-restrictions',
+                               'required' => true,
+                               'default' => $this->botPassword->getRestrictions()->toJson( true ),
+                               'rows' => 5,
+                               'validation-callback' => function ( $v ) {
+                                       try {
+                                               MWRestrictions::newFromJson( $v );
+                                               return true;
+                                       } catch ( InvalidArgumentException $ex ) {
+                                               return $ex->getMessage();
+                                       }
+                               },
+                       );
+
+               } else {
+                       $dbr = BotPassword::getDB( DB_SLAVE );
+                       $res = $dbr->select(
+                               'bot_passwords',
+                               array( 'bp_app_id' ),
+                               array( 'bp_user' => $this->userId ),
+                               __METHOD__
+                       );
+                       foreach ( $res as $row ) {
+                               $fields[] = array(
+                                       'section' => 'existing',
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'default' => Linker::link(
+                                               $this->getPageTitle( $row->bp_app_id ),
+                                               htmlspecialchars( $row->bp_app_id ),
+                                               array(),
+                                               array(),
+                                               array( 'known' )
+                                       ),
+                               );
+                       }
+
+                       $fields['appId'] = array(
+                               'section' => 'createnew',
+                               'type' => 'textwithbutton',
+                               'label-message' => 'botpasswords-label-appid',
+                               'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
+                               'required' => true,
+                               'size' => BotPassword::APPID_MAXLENGTH,
+                               'maxlength' => BotPassword::APPID_MAXLENGTH,
+                               'validation-callback' => function ( $v ) {
+                                       $v = trim( $v );
+                                       return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
+                               },
+                       );
+
+                       $fields[] = array(
+                               'type' => 'hidden',
+                               'default' => 'new',
+                               'name' => 'op',
+                       );
+               }
+
+               return $fields;
+       }
+
+       protected function alterForm( HTMLForm $form ) {
+               $form->setId( 'mw-botpasswords-form' );
+               $form->setTableId( 'mw-botpasswords-table' );
+               $form->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
+               $form->suppressDefaultSubmit();
+
+               if ( $this->par !== null ) {
+                       if ( $this->botPassword->isSaved() ) {
+                               $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
+                               $form->addButton( array(
+                                       'name' => 'op',
+                                       'value' => 'update',
+                                       'label-message' => 'botpasswords-label-update',
+                                       'flags' => array( 'primary', 'progressive' ),
+                               ) );
+                               $form->addButton( array(
+                                       'name' => 'op',
+                                       'value' => 'delete',
+                                       'label-message' => 'botpasswords-label-delete',
+                                       'flags' => array( 'destructive' ),
+                               ) );
+                       } else {
+                               $form->setWrapperLegendMsg( 'botpasswords-createnew' );
+                               $form->addButton( array(
+                                       'name' => 'op',
+                                       'value' => 'create',
+                                       'label-message' => 'botpasswords-label-create',
+                                       'flags' => array( 'primary', 'constructive' ),
+                               ) );
+                       }
+
+                       $form->addButton( array(
+                               'name' => 'op',
+                               'value' => 'cancel',
+                               'label-message' => 'botpasswords-label-cancel'
+                       ) );
+               }
+       }
+
+       public function onSubmit( array $data ) {
+               $op = $this->getRequest()->getVal( 'op', '' );
+
+               switch ( $op ) {
+                       case 'new':
+                               $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
+                               return false;
+
+                       case 'create':
+                               $this->operation = 'insert';
+                               return $this->save( $data );
+
+                       case 'update':
+                               $this->operation = 'update';
+                               return $this->save( $data );
+
+                       case 'delete':
+                               $this->operation = 'delete';
+                               $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
+                               if ( $bp ) {
+                                       $bp->delete();
+                               }
+                               return Status::newGood();
+
+                       case 'cancel':
+                               $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
+                               return false;
+               }
+
+               return false;
+       }
+
+       private function save( array $data ) {
+               $bp = BotPassword::newUnsaved( array(
+                       'centralId' => $this->userId,
+                       'appId' => $this->par,
+                       'restrictions' => MWRestrictions::newFromJson( $data['restrictions'] ),
+                       'grants' => array_merge(
+                               MWGrants::getHiddenGrants(),
+                               preg_replace( '/^grant-/', '', $data['grants'] )
+                       )
+               ) );
+
+               if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
+                       $this->password = PasswordFactory::generateRandomPasswordString(
+                               max( 32, $this->getConfig()->get( 'MinimalPasswordLength' ) )
+                       );
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $password = $passwordFactory->newFromPlaintext( $this->password );
+               } else {
+                       $password = null;
+               }
+
+               if ( $bp->save( $this->operation, $password ) ) {
+                       return Status::newGood();
+               } else {
+                       // Messages: botpasswords-insert-failed, botpasswords-update-failed
+                       return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
+               }
+       }
+
+       public function onSuccess() {
+               $out = $this->getOutput();
+
+               switch ( $this->operation ) {
+                       case 'insert':
+                               $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
+                               $out->addWikiMsg( 'botpasswords-created-body', $this->par );
+                               break;
+
+                       case 'update':
+                               $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
+                               $out->addWikiMsg( 'botpasswords-updated-body', $this->par );
+                               break;
+
+                       case 'delete':
+                               $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
+                               $out->addWikiMsg( 'botpasswords-deleted-body', $this->par );
+                               $this->password = null;
+                               break;
+               }
+
+               if ( $this->password !== null ) {
+                       $sep = BotPassword::getSeparator();
+                       $out->addWikiMsg(
+                               'botpasswords-newpassword',
+                               htmlspecialchars( $this->getUser()->getName() . $sep . $this->par ),
+                               htmlspecialchars( $this->password )
+                       );
+                       $this->password = null;
+               }
+
+               $out->addReturnTo( $this->getPageTitle() );
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       protected function getDisplayFormat() {
+               return 'ooui';
+       }
+}
index 4812c9d..a9a7f97 100644 (file)
@@ -6,6 +6,10 @@ class SpecialChangeContentModel extends FormSpecialPage {
                parent::__construct( 'ChangeContentModel', 'editcontentmodel' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * @var Title|null
         */
index 51b08f9..989bae9 100644 (file)
@@ -36,6 +36,10 @@ class SpecialChangeEmail extends FormSpecialPage {
                parent::__construct( 'ChangeEmail', 'editmyprivateinfo' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * @return bool
         */
index 371ad19..8656798 100644 (file)
@@ -41,6 +41,10 @@ class SpecialChangePassword extends FormSpecialPage {
                $this->listed( false );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Main execution point
         * @param string|null $par
index 37d3636..5ed33e0 100644 (file)
@@ -34,6 +34,10 @@ class EmailConfirmation extends UnlistedSpecialPage {
                parent::__construct( 'Confirmemail', 'editmyprivateinfo' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Main execution point
         *
index 30e3833..f8c6832 100644 (file)
@@ -37,6 +37,10 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial {
                );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        // No reason to hide this link on Special:Specialpages
        public function isListed() {
                return true;
index 97b04c2..916ba6b 100644 (file)
@@ -55,6 +55,10 @@ class SpecialEditTags extends UnlistedSpecialPage {
                parent::__construct( 'EditTags', 'changetags' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par ) {
                $this->checkPermissions();
                $this->checkReadOnly();
index 952ae0e..13ee8b3 100644 (file)
@@ -53,6 +53,10 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                parent::__construct( 'EditWatchlist', 'editmywatchlist' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Main execution point
         *
index 30f9d2e..b5c66ff 100644 (file)
@@ -32,6 +32,10 @@ class EmailInvalidation extends UnlistedSpecialPage {
                parent::__construct( 'Invalidateemail', 'editmyprivateinfo' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        function execute( $code ) {
                // Ignore things like master queries/connections on GET requests.
                // It's very convenient to just allow formless link usage.
index 618e700..c036d3d 100644 (file)
@@ -38,6 +38,10 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                parent::__construct( 'Emailuser' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function getDescription() {
                $target = self::getTarget( $this->mTarget );
                if ( !$target instanceof User ) {
index 5ca90ed..97e093b 100644 (file)
@@ -51,6 +51,10 @@ class SpecialImport extends SpecialPage {
                parent::__construct( 'Import', 'import' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Execute
         * @param string|null $par
diff --git a/includes/specials/SpecialListgrants.php b/includes/specials/SpecialListgrants.php
new file mode 100644 (file)
index 0000000..c5eea3f
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Implements Special:Listgrants
+ *
+ * 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 SpecialPage
+ */
+
+/**
+ * This special page lists all defined rights grants and the associated rights.
+ * See also @ref $wgGrantPermissions and @ref $wgGrantPermissionGroups.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialListGrants extends SpecialPage {
+       function __construct() {
+               parent::__construct( 'Listgrants' );
+       }
+
+       /**
+        * Show the special page
+        * @param string|null $par
+        */
+       public function execute( $par ) {
+               $this->setHeaders();
+               $this->outputHeader();
+
+               $out = $this->getOutput();
+               $out->addModuleStyles( 'mediawiki.special' );
+
+               $out->addHTML(
+                       \Html::openElement( 'table',
+                               array( 'class' => 'wikitable mw-listgrouprights-table' ) ) .
+                               '<tr>' .
+                               \Html::element( 'th', null, $this->msg( 'listgrants-grant' )->text() ) .
+                               \Html::element( 'th', null, $this->msg( 'listgrants-rights' )->text() ) .
+                               '</tr>'
+               );
+
+               foreach ( $this->getConfig()->get( 'GrantPermissions' ) as $grant => $rights ) {
+                       $descs = array();
+                       $rights = array_filter( $rights ); // remove ones with 'false'
+                       foreach ( $rights as $permission => $granted ) {
+                               $descs[] = $this->msg(
+                                       'listgrouprights-right-display',
+                                       \User::getRightDescription( $permission ),
+                                       '<span class="mw-listgrants-right-name">' . $permission . '</span>'
+                               )->parse();
+                       }
+                       if ( !count( $descs ) ) {
+                               $grantCellHtml = '';
+                       } else {
+                               sort( $descs );
+                               $grantCellHtml = '<ul><li>' . implode( "</li>\n<li>", $descs ) . '</li></ul>';
+                       }
+
+                       $id = \Sanitizer::escapeId( $grant );
+                       $out->addHTML( \Html::rawElement( 'tr', array( 'id' => $id ),
+                               "<td>" . $this->msg( "grant-$grant" )->escaped() . "</td>" .
+                               "<td>" . $grantCellHtml . '</td>'
+                       ) );
+               }
+
+               $out->addHTML( \Html::closeElement( 'table' ) );
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+}
index a276197..0d495a0 100644 (file)
@@ -33,6 +33,10 @@ class SpecialLockdb extends FormSpecialPage {
                parent::__construct( 'Lockdb', 'siteadmin' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function requiresWrite() {
                return false;
        }
index f11ed9a..2607330 100644 (file)
@@ -68,6 +68,10 @@ class SpecialMergeHistory extends SpecialPage {
                parent::__construct( 'MergeHistory', 'mergehistory' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * @return void
         */
index 0c0d929..4bdad79 100644 (file)
@@ -62,6 +62,10 @@ class MovePageForm extends UnlistedSpecialPage {
                parent::__construct( 'Movepage' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par ) {
                $this->useTransactionalTimeLimit();
 
index 7509bbc..69a9d48 100644 (file)
@@ -38,6 +38,10 @@ class SpecialPageLanguage extends FormSpecialPage {
                parent::__construct( 'PageLanguage', 'pagelang' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        protected function preText() {
                $this->getOutput()->addModules( 'mediawiki.special.pageLanguage' );
        }
index 292b575..21ce1e1 100644 (file)
@@ -51,6 +51,10 @@ class SpecialPasswordReset extends FormSpecialPage {
                parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function userCanExecute( User $user ) {
                return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
        }
index 3fa5fd5..965a36e 100644 (file)
@@ -31,6 +31,10 @@ class SpecialPreferences extends SpecialPage {
                parent::__construct( 'Preferences' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par ) {
                $this->setHeaders();
                $this->outputHeader();
index cba5a44..38e977b 100644 (file)
@@ -34,6 +34,10 @@ class SpecialResetTokens extends FormSpecialPage {
                parent::__construct( 'ResetTokens' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Returns the token information list for this page after running
         * the hook and filtering out disabled preferences.
index 6dcbcb1..44e44ba 100644 (file)
@@ -109,6 +109,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
                parent::__construct( 'Revisiondelete', 'deletedhistory' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par ) {
                $this->useTransactionalTimeLimit();
 
index 4217553..eeaf2d3 100644 (file)
@@ -34,6 +34,10 @@ class SpecialRunJobs extends UnlistedSpecialPage {
                parent::__construct( 'RunJobs' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par = '' ) {
                $this->getOutput()->disable();
 
index f776832..cf807ef 100644 (file)
@@ -36,6 +36,10 @@ class SpecialUnblock extends SpecialPage {
                parent::__construct( 'Unblock', 'block' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function execute( $par ) {
                $this->checkPermissions();
                $this->checkReadOnly();
index 744a090..f99a52d 100644 (file)
@@ -51,6 +51,10 @@ class PageArchive {
                $this->config = $config;
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * List all deleted pages recorded in the archive table. Returns result
         * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
index dc03a4a..b73e3c5 100644 (file)
@@ -32,6 +32,10 @@ class SpecialUnlockdb extends FormSpecialPage {
                parent::__construct( 'Unlockdb', 'siteadmin' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function requiresWrite() {
                return false;
        }
index b4470f5..5b3c43e 100644 (file)
@@ -38,6 +38,10 @@ class SpecialUpload extends SpecialPage {
                parent::__construct( 'Upload', 'upload' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /** Misc variables **/
 
        /** @var WebRequest|FauxRequest The request this form is supposed to handle */
index eb34008..1cec116 100644 (file)
@@ -52,6 +52,10 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                parent::__construct( 'UploadStash', 'upload' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Execute page -- can output a file directly or show a listing of them.
         *
index fec1e3a..24e1675 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup SpecialPage
  */
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
 
 /**
  * Implements Special:UserLogin
@@ -132,6 +133,10 @@ class LoginForm extends SpecialPage {
                $wgUseMediaWikiUIEverywhere = true;
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        /**
         * Returns an array of all valid error messages.
         *
@@ -263,9 +268,9 @@ class LoginForm extends SpecialPage {
         * @param string|null $subPage
         */
        public function execute( $subPage ) {
-               if ( session_id() == '' ) {
-                       wfSetupSession();
-               }
+               // Make sure session is persisted
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+               $session->persist();
 
                $this->load();
 
@@ -276,6 +281,17 @@ class LoginForm extends SpecialPage {
                }
                $this->setHeaders();
 
+               // Make sure it's possible to log in
+               if ( $this->mType !== 'signup' && !$session->canSetUser() ) {
+                       throw new ErrorPageError(
+                               'cannotloginnow-title',
+                               'cannotloginnow-text',
+                               array(
+                                       $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
+                               )
+                       );
+               }
+
                /**
                 * In the case where the user is already logged in, and was redirected to
                 * the login form from a page that requires login, do not show the login
@@ -1376,7 +1392,7 @@ class LoginForm extends SpecialPage {
                        if ( $user->isLoggedIn() ) {
                                $this->mUsername = $user->getName();
                        } else {
-                               $this->mUsername = $this->getRequest()->getCookie( 'UserName' );
+                               $this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
                        }
                }
 
@@ -1552,7 +1568,8 @@ class LoginForm extends SpecialPage {
        function hasSessionCookie() {
                global $wgDisableCookieCheck;
 
-               return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie();
+               return $wgDisableCookieCheck ||
+                       SessionManager::singleton()->getPersistedSessionId( $this->getRequest() ) !== null;
        }
 
        /**
@@ -1571,7 +1588,7 @@ class LoginForm extends SpecialPage {
        public static function setLoginToken() {
                global $wgRequest;
                // Generate a token directly instead of using $user->getEditToken()
-               // because the latter reuses $_SESSION['wsEditToken']
+               // because the latter reuses wsEditToken in the session
                $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32 ) );
        }
 
@@ -1617,7 +1634,7 @@ class LoginForm extends SpecialPage {
                        $wgCookieSecure = false;
                }
 
-               wfResetSessionID();
+               MediaWiki\Session\SessionManager::getGlobalSession()->resetId();
        }
 
        /**
index 080dc11..6e34690 100644 (file)
@@ -31,6 +31,10 @@ class SpecialUserlogout extends UnlistedSpecialPage {
                parent::__construct( 'Userlogout' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        function execute( $par ) {
                /**
                 * Some satellite ISPs use broken precaching schemes that log people out straight after
@@ -44,6 +48,18 @@ class SpecialUserlogout extends UnlistedSpecialPage {
                $this->setHeaders();
                $this->outputHeader();
 
+               // Make sure it's possible to log out
+               $session = MediaWiki\Session\SessionManager::getGlobalSession();
+               if ( !$session->canSetUser() ) {
+                       throw new ErrorPageError(
+                               'cannotlogoutnow-title',
+                               'cannotlogoutnow-text',
+                               array(
+                                       $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
+                               )
+                       );
+               }
+
                $user = $this->getUser();
                $oldName = $user->getName();
                $user->logout();
index cf94e50..01b1f8e 100644 (file)
@@ -41,6 +41,10 @@ class UserrightsPage extends SpecialPage {
                parent::__construct( 'Userrights' );
        }
 
+       public function doesWrites() {
+               return true;
+       }
+
        public function isRestricted() {
                return true;
        }
index f897a79..23a4962 100644 (file)
@@ -390,7 +390,7 @@ class UploadFromUrl extends UploadBase {
                        'userName' => $user->getName(),
                        'leaveMessage' => $this->mAsync == 'async-leavemessage',
                        'ignoreWarnings' => $this->mIgnoreWarnings,
-                       'sessionId' => session_id(),
+                       'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
                        'sessionKey' => $sessionKey,
                ) );
                $job->initializeSessionData();
diff --git a/includes/user/BotPassword.php b/includes/user/BotPassword.php
new file mode 100644 (file)
index 0000000..286538b
--- /dev/null
@@ -0,0 +1,429 @@
+<?php
+/**
+ * Utility class for bot passwords
+ *
+ * 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
+ */
+
+use MediaWiki\Session\BotPasswordSessionProvider;
+use MediaWiki\Session\SessionInfo;
+
+/**
+ * Utility class for bot passwords
+ * @since 1.27
+ */
+class BotPassword implements IDBAccessObject {
+
+       const APPID_MAXLENGTH = 32;
+
+       /** @var bool */
+       private $isSaved;
+
+       /** @var int */
+       private $centralId;
+
+       /** @var string */
+       private $appId;
+
+       /** @var string */
+       private $token;
+
+       /** @var MWRestrictions */
+       private $restrictions;
+
+       /** @var string[] */
+       private $grants;
+
+       /** @var int */
+       private $flags = self::READ_NORMAL;
+
+       /**
+        * @param object $row bot_passwords database row
+        * @param bool $isSaved Whether the bot password was read from the database
+        * @param int $flags IDBAccessObject read flags
+        */
+       protected function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
+               $this->isSaved = $isSaved;
+               $this->flags = $flags;
+
+               $this->centralId = (int)$row->bp_user;
+               $this->appId = $row->bp_app_id;
+               $this->token = $row->bp_token;
+               $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
+               $this->grants = FormatJson::decode( $row->bp_grants );
+       }
+
+       /**
+        * Get a database connection for the bot passwords database
+        * @param int $db Index of the connection to get, e.g. DB_MASTER or DB_SLAVE.
+        * @return DatabaseBase
+        */
+       public static function getDB( $db ) {
+               global $wgBotPasswordsCluster, $wgBotPasswordsDatabase;
+
+               $lb = $wgBotPasswordsCluster
+                       ? wfGetLBFactory()->getExternalLB( $wgBotPasswordsCluster )
+                       : wfGetLB( $wgBotPasswordsDatabase );
+               return $lb->getConnectionRef( $db, array(), $wgBotPasswordsDatabase );
+       }
+
+       /**
+        * Load a BotPassword from the database
+        * @param User $user
+        * @param string $appId
+        * @param int $flags IDBAccessObject read flags
+        * @return BotPassword|null
+        */
+       public static function newFromUser( User $user, $appId, $flags = self::READ_NORMAL ) {
+               $centralId = CentralIdLookup::factory()->centralIdFromLocalUser(
+                       $user, CentralIdLookup::AUDIENCE_RAW, $flags
+               );
+               return $centralId ? self::newFromCentralId( $centralId, $appId, $flags ) : null;
+       }
+
+       /**
+        * Load a BotPassword from the database
+        * @param int $centralId from CentralIdLookup
+        * @param string $appId
+        * @param int $flags IDBAccessObject read flags
+        * @return BotPassword|null
+        */
+       public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
+               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+               $db = self::getDB( $index );
+               $row = $db->selectRow(
+                       'bot_passwords',
+                       array( 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ),
+                       array( 'bp_user' => $centralId, 'bp_app_id' => $appId ),
+                       __METHOD__,
+                       $options
+               );
+               return $row ? new self( $row, true, $flags ) : null;
+       }
+
+       /**
+        * Create an unsaved BotPassword
+        * @param array $data Data to use to create the bot password. Keys are:
+        *  - user: (User) User object to create the password for. Overrides username and centralId.
+        *  - username: (string) Username to create the password for. Overrides centralId.
+        *  - centralId: (int) User central ID to create the password for.
+        *  - appId: (string) App ID for the password.
+        *  - restrictions: (MWRestrictions, optional) Restrictions.
+        *  - grants: (string[], optional) Grants.
+        * @param int $flags IDBAccessObject read flags
+        * @return BotPassword|null
+        */
+       public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
+               $row = (object)array(
+                       'bp_user' => 0,
+                       'bp_app_id' => isset( $data['appId'] ) ? trim( $data['appId'] ) : '',
+                       'bp_token' => '**unsaved**',
+                       'bp_restrictions' => isset( $data['restrictions'] )
+                               ? $data['restrictions']
+                               : MWRestrictions::newDefault(),
+                       'bp_grants' => isset( $data['grants'] ) ? $data['grants'] : array(),
+               );
+
+               if (
+                       $row->bp_app_id === '' || strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
+                       !$row->bp_restrictions instanceof MWRestrictions ||
+                       !is_array( $row->bp_grants )
+               ) {
+                       return null;
+               }
+
+               $row->bp_restrictions = $row->bp_restrictions->toJson();
+               $row->bp_grants = FormatJson::encode( $row->bp_grants );
+
+               if ( isset( $data['user'] ) ) {
+                       if ( !$data['user'] instanceof User ) {
+                               return null;
+                       }
+                       $row->bp_user = CentralIdLookup::factory()->centralIdFromLocalUser(
+                               $data['user'], CentralIdLookup::AUDIENCE_RAW, $flags
+                       );
+               } elseif ( isset( $data['username'] ) ) {
+                       $row->bp_user = CentralIdLookup::factory()->centralIdFromName(
+                               $data['username'], CentralIdLookup::AUDIENCE_RAW, $flags
+                       );
+               } elseif ( isset( $data['centralId'] ) ) {
+                       $row->bp_user = $data['centralId'];
+               }
+               if ( !$row->bp_user ) {
+                       return null;
+               }
+
+               return new self( $row, false, $flags );
+       }
+
+       /**
+        * Indicate whether this is known to be saved
+        * @return bool
+        */
+       public function isSaved() {
+               return $this->isSaved;
+       }
+
+       /**
+        * Get the central user ID
+        * @return int
+        */
+       public function getUserCentralId() {
+               return $this->centralId;
+       }
+
+       /**
+        * Get the app ID
+        * @return string
+        */
+       public function getAppId() {
+               return $this->appId;
+       }
+
+       /**
+        * Get the token
+        * @return string
+        */
+       public function getToken() {
+               return $this->token;
+       }
+
+       /**
+        * Get the restrictions
+        * @return MWRestrictions
+        */
+       public function getRestrictions() {
+               return $this->restrictions;
+       }
+
+       /**
+        * Get the grants
+        * @return string[]
+        */
+       public function getGrants() {
+               return $this->grants;
+       }
+
+       /**
+        * Get the separator for combined user name + app ID
+        * @return string
+        */
+       public static function getSeparator() {
+               global $wgUserrightsInterwikiDelimiter;
+               return $wgUserrightsInterwikiDelimiter;
+       }
+
+       /**
+        * Get the password
+        * @return Password
+        */
+       protected function getPassword() {
+               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
+               $db = self::getDB( $index );
+               $password = $db->selectField(
+                       'bot_passwords',
+                       'bp_password',
+                       array( 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ),
+                       __METHOD__,
+                       $options
+               );
+               if ( $password === false ) {
+                       return PasswordFactory::newInvalidPassword();
+               }
+
+               $passwordFactory = new \PasswordFactory();
+               $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+               try {
+                       return $passwordFactory->newFromCiphertext( $password );
+               } catch ( PasswordError $ex ) {
+                       return PasswordFactory::newInvalidPassword();
+               }
+       }
+
+       /**
+        * Save the BotPassword to the database
+        * @param string $operation 'update' or 'insert'
+        * @param Password|null $password Password to set.
+        * @return bool Success
+        */
+       public function save( $operation, Password $password = null ) {
+               $conds = array(
+                       'bp_user' => $this->centralId,
+                       'bp_app_id' => $this->appId,
+               );
+               $fields = array(
+                       'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ),
+                       'bp_restrictions' => $this->restrictions->toJson(),
+                       'bp_grants' => FormatJson::encode( $this->grants ),
+               );
+
+               if ( $password !== null ) {
+                       $fields['bp_password'] = $password->toString();
+               } elseif ( $operation === 'insert' ) {
+                       $fields['bp_password'] = PasswordFactory::newInvalidPassword()->toString();
+               }
+
+               $dbw = self::getDB( DB_MASTER );
+               switch ( $operation ) {
+                       case 'insert':
+                               $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, array( 'IGNORE' ) );
+                               break;
+
+                       case 'update':
+                               $dbw->update( 'bot_passwords', $fields, $conds, __METHOD__ );
+                               break;
+
+                       default:
+                               return false;
+               }
+               $ok = (bool)$dbw->affectedRows();
+               if ( $ok ) {
+                       $this->token = $dbw->selectField( 'bot_passwords', 'bp_token', $conds, __METHOD__ );
+                       $this->isSaved = true;
+               }
+               return $ok;
+       }
+
+       /**
+        * Delete the BotPassword from the database
+        * @return bool Success
+        */
+       public function delete() {
+               $conds = array(
+                       'bp_user' => $this->centralId,
+                       'bp_app_id' => $this->appId,
+               );
+               $dbw = self::getDB( DB_MASTER );
+               $dbw->delete( 'bot_passwords', $conds, __METHOD__ );
+               $ok = (bool)$dbw->affectedRows();
+               if ( $ok ) {
+                       $this->token = '**unsaved**';
+                       $this->isSaved = false;
+               }
+               return $ok;
+       }
+
+       /**
+        * Invalidate all passwords for a user, by name
+        * @param string $username User name
+        * @return bool Whether any passwords were invalidated
+        */
+       public static function invalidateAllPasswordsForUser( $username ) {
+               $centralId = CentralIdLookup::factory()->centralIdFromName(
+                       $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
+               );
+               return $centralId && self::invalidateAllPasswordsForCentralId( $centralId );
+       }
+
+       /**
+        * Invalidate all passwords for a user, by central ID
+        * @param int $centralId
+        * @return bool Whether any passwords were invalidated
+        */
+       public static function invalidateAllPasswordsForCentralId( $centralId ) {
+               $dbw = self::getDB( DB_MASTER );
+               $dbw->update(
+                       'bot_passwords',
+                       array( 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ),
+                       array( 'bp_user' => $centralId ),
+                       __METHOD__
+               );
+               return (bool)$dbw->affectedRows();
+       }
+
+       /**
+        * Remove all passwords for a user, by name
+        * @param string $username User name
+        * @return bool Whether any passwords were removed
+        */
+       public static function removeAllPasswordsForUser( $username ) {
+               $centralId = CentralIdLookup::factory()->centralIdFromName(
+                       $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
+               );
+               return $centralId && self::removeAllPasswordsForCentralId( $centralId );
+       }
+
+       /**
+        * Remove all passwords for a user, by central ID
+        * @param int $centralId
+        * @return bool Whether any passwords were removed
+        */
+       public static function removeAllPasswordsForCentralId( $centralId ) {
+               $dbw = self::getDB( DB_MASTER );
+               $dbw->delete(
+                       'bot_passwords',
+                       array( 'bp_user' => $centralId ),
+                       __METHOD__
+               );
+               return (bool)$dbw->affectedRows();
+       }
+
+       /**
+        * Try to log the user in
+        * @param string $username Combined user name and app ID
+        * @param string $password Supplied password
+        * @param WebRequest $request
+        * @return Status On success, the good status's value is the new Session object
+        */
+       public static function login( $username, $password, WebRequest $request ) {
+               global $wgEnableBotPasswords;
+
+               if ( !$wgEnableBotPasswords ) {
+                       return Status::newFatal( 'botpasswords-disabled' );
+               }
+
+               $manager = MediaWiki\Session\SessionManager::singleton();
+               $provider = $manager->getProvider(
+                       'MediaWiki\\Session\\BotPasswordSessionProvider'
+               );
+               if ( !$provider ) {
+                       return Status::newFatal( 'botpasswords-no-provider' );
+               }
+
+               // Split name into name+appId
+               $sep = self::getSeparator();
+               if ( strpos( $username, $sep ) === false ) {
+                       return Status::newFatal( 'botpasswords-invalid-name', $sep );
+               }
+               list( $name, $appId ) = explode( $sep, $username, 2 );
+
+               // Find the named user
+               $user = User::newFromName( $name );
+               if ( !$user || $user->isAnon() ) {
+                       return Status::newFatal( 'nosuchuser', $name );
+               }
+
+               // Get the bot password
+               $bp = self::newFromUser( $user, $appId );
+               if ( !$bp ) {
+                       return Status::newFatal( 'botpasswords-not-exist', $name, $appId );
+               }
+
+               // Check restrictions
+               $status = $bp->getRestrictions()->check( $request );
+               if ( !$status->isOk() ) {
+                       return Status::newFatal( 'botpasswords-restriction-failed' );
+               }
+
+               // Check the password
+               if ( !$bp->getPassword()->equals( $password ) ) {
+                       return Status::newFatal( 'wrongpassword' );
+               }
+
+               // Ok! Create the session.
+               return Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) );
+       }
+}
index 638a3e2..4c2b5b7 100644 (file)
@@ -62,6 +62,16 @@ abstract class CentralIdLookup implements IDBAccessObject {
                return self::$instances[$providerId];
        }
 
+       /**
+        * Reset internal cache for unit testing
+        */
+       public static function resetCache() {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
+               }
+               self::$instances = array();
+       }
+
        final public function getProviderId() {
                return $this->providerId;
        }
index b406bac..6a6b594 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\Session\SessionManager;
+
 /**
  * String Some punctuation to prevent editing from broken text-mangling proxies.
  * @ingroup Constants
@@ -99,6 +101,7 @@ class User implements IDBAccessObject {
                'apihighlimits',
                'applychangetags',
                'autoconfirmed',
+               'autocreateaccount',
                'autopatrol',
                'bigdelete',
                'block',
@@ -227,7 +230,7 @@ class User implements IDBAccessObject {
         *  - 'defaults'   anonymous user initialised from class defaults
         *  - 'name'       initialise from mName
         *  - 'id'         initialise from mId
-        *  - 'session'    log in from cookies or session if possible
+        *  - 'session'    log in from session if possible
         *
         * Use the User::newFrom*() family of functions to set this.
         */
@@ -311,14 +314,26 @@ class User implements IDBAccessObject {
         * @param integer $flags User::READ_* constant bitfield
         */
        public function load( $flags = self::READ_NORMAL ) {
+               global $wgFullyInitialised;
+
                if ( $this->mLoadedItems === true ) {
                        return;
                }
 
                // Set it now to avoid infinite recursion in accessors
+               $oldLoadedItems = $this->mLoadedItems;
                $this->mLoadedItems = true;
                $this->queryFlagsUsed = $flags;
 
+               // If this is called too early, things are likely to break.
+               if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( 'User::loadFromSession called before the end of Setup.php' );
+                       $this->loadDefaults();
+                       $this->mLoadedItems = $oldLoadedItems;
+                       return;
+               }
+
                switch ( $this->mFrom ) {
                        case 'defaults':
                                $this->loadDefaults();
@@ -541,8 +556,8 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Create a new user object using data from session or cookies. If the
-        * login credentials are invalid, the result is an anonymous user.
+        * Create a new user object using data from session. If the login
+        * credentials are invalid, the result is an anonymous user.
         *
         * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
         * @return User
@@ -662,6 +677,8 @@ class User implements IDBAccessObject {
                        $user->saveSettings();
                }
 
+               SessionManager::singleton()->preventSessionsForUser( $user->getName() );
+
                return $user;
        }
 
@@ -1069,8 +1086,9 @@ class User implements IDBAccessObject {
                $this->mOptionOverrides = null;
                $this->mOptionsLoaded = false;
 
-               $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
-               if ( $loggedOut !== null ) {
+               $request = $this->getRequest();
+               $loggedOut = $request ? $request->getSession()->getLoggedOutTimestamp() : 0;
+               if ( $loggedOut !== 0 ) {
                        $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
                } else {
                        $this->mTouched = '1'; # Allow any pages to be cached
@@ -1115,84 +1133,32 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Load user data from the session or login cookie.
+        * Load user data from the session.
         *
         * @return bool True if the user is logged in, false otherwise.
         */
        private function loadFromSession() {
+               // Deprecated hook
                $result = null;
-               Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
+               Hooks::run( 'UserLoadFromSession', array( $this, &$result ), '1.27' );
                if ( $result !== null ) {
                        return $result;
                }
 
-               $request = $this->getRequest();
-
-               $cookieId = $request->getCookie( 'UserID' );
-               $sessId = $request->getSessionData( 'wsUserID' );
-
-               if ( $cookieId !== null ) {
-                       $sId = intval( $cookieId );
-                       if ( $sessId !== null && $cookieId != $sessId ) {
-                               wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
-                                       cookie user ID ($sId) don't match!" );
-                               return false;
-                       }
-                       $request->setSessionData( 'wsUserID', $sId );
-               } elseif ( $sessId !== null && $sessId != 0 ) {
-                       $sId = $sessId;
-               } else {
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsUserName' ) !== null ) {
-                       $sName = $request->getSessionData( 'wsUserName' );
-               } elseif ( $request->getCookie( 'UserName' ) !== null ) {
-                       $sName = $request->getCookie( 'UserName' );
-                       $request->setSessionData( 'wsUserName', $sName );
-               } else {
-                       return false;
-               }
-
-               $proposedUser = User::newFromId( $sId );
-               if ( !$proposedUser->isLoggedIn() ) {
-                       // Not a valid ID
-                       return false;
-               }
-
-               global $wgBlockDisablesLogin;
-               if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
-                       // User blocked and we've disabled blocked user logins
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsToken' ) ) {
-                       $passwordCorrect =
-                               ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
-                       $from = 'session';
-               } elseif ( $request->getCookie( 'Token' ) ) {
-                       # Get the token from DB/cache and clean it up to remove garbage padding.
-                       # This deals with historical problems with bugs and the default column value.
-                       $token = rtrim( $proposedUser->getToken( false ) ); // correct token
-                       // Make comparison in constant time (bug 61346)
-                       $passwordCorrect = strlen( $token )
-                               && hash_equals( $token, $request->getCookie( 'Token' ) );
-                       $from = 'cookie';
-               } else {
-                       // No session or persistent login cookie
-                       return false;
-               }
-
-               if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
-                       $this->loadFromUserObject( $proposedUser );
-                       $request->setSessionData( 'wsToken', $this->mToken );
-                       wfDebug( "User: logged in from $from\n" );
+               // MediaWiki\Session\Session already did the necessary authentication of the user
+               // returned here, so just use it if applicable.
+               $session = $this->getRequest()->getSession();
+               $user = $session->getUser();
+               if ( $user->isLoggedIn() ) {
+                       $this->loadFromUserObject( $user );
+                       // Other code expects these to be set in the session, so set them.
+                       $session->set( 'wsUserID', $this->getId() );
+                       $session->set( 'wsUserName', $this->getName() );
+                       $session->set( 'wsToken', $this->mToken );
                        return true;
-               } else {
-                       // Invalid credentials
-                       wfDebug( "User: can't log in from $from, invalid credentials\n" );
-                       return false;
                }
+
+               return false;
        }
 
        /**
@@ -2442,6 +2408,9 @@ class User implements IDBAccessObject {
                        ),
                        __METHOD__
                );
+
+               // When the main password is changed, invalidate all bot passwords too
+               BotPassword::invalidateAllPasswordsForUser( $this->getName() );
        }
 
        /**
@@ -3015,6 +2984,12 @@ class User implements IDBAccessObject {
        public function getRights() {
                if ( is_null( $this->mRights ) ) {
                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+
+                       $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
+                       if ( $allowedRights !== null ) {
+                               $this->mRights = array_intersect( $this->mRights, $allowedRights );
+                       }
+
                        Hooks::run( 'UserGetRights', array( $this, &$this->mRights ) );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
@@ -3276,13 +3251,6 @@ class User implements IDBAccessObject {
                if ( $action === '' ) {
                        return true; // In the spirit of DWIM
                }
-               // Patrolling may not be enabled
-               if ( $action === 'patrol' || $action === 'autopatrol' ) {
-                       global $wgUseRCPatrol, $wgUseNPPatrol;
-                       if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
-                               return false;
-                       }
-               }
                // Use strict parameter to avoid matching numeric 0 accidentally inserted
                // by misconfiguration: 0 == 'foo'
                return in_array( $action, $this->getRights(), true );
@@ -3502,6 +3470,7 @@ class User implements IDBAccessObject {
        /**
         * Set a cookie on the user's client. Wrapper for
         * WebResponse::setCookie
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to set
         * @param string $value Value to set
         * @param int $exp Expiration time, as a UNIX time value;
@@ -3517,6 +3486,7 @@ class User implements IDBAccessObject {
        protected function setCookie(
                $name, $value, $exp = 0, $secure = null, $params = array(), $request = null
        ) {
+               wfDeprecated( __METHOD__, '1.27' );
                if ( $request === null ) {
                        $request = $this->getRequest();
                }
@@ -3526,6 +3496,7 @@ class User implements IDBAccessObject {
 
        /**
         * Clear a cookie on the user's client
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to clear
         * @param bool $secure
         *  true: Force setting the secure attribute when setting the cookie
@@ -3534,6 +3505,7 @@ class User implements IDBAccessObject {
         * @param array $params Array of options sent passed to WebResponse::setcookie()
         */
        protected function clearCookie( $name, $secure = null, $params = array() ) {
+               wfDeprecated( __METHOD__, '1.27' );
                $this->setCookie( $name, '', time() - 86400, $secure, $params );
        }
 
@@ -3544,6 +3516,7 @@ class User implements IDBAccessObject {
         *
         * @see User::setCookie
         *
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to set
         * @param string $value Value to set
         * @param bool $secure
@@ -3554,6 +3527,8 @@ class User implements IDBAccessObject {
        protected function setExtendedLoginCookie( $name, $value, $secure ) {
                global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
 
+               wfDeprecated( __METHOD__, '1.27' );
+
                $exp = time();
                $exp += $wgExtendedLoginCookieExpiration !== null
                        ? $wgExtendedLoginCookieExpiration
@@ -3563,7 +3538,7 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Set the default cookies for this session on the user's client.
+        * Persist this user's session (e.g. set cookies)
         *
         * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
         *        is passed.
@@ -3571,72 +3546,36 @@ class User implements IDBAccessObject {
         * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
         */
        public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
-               global $wgExtendedLoginCookies;
-
-               if ( $request === null ) {
-                       $request = $this->getRequest();
-               }
-
                $this->load();
                if ( 0 == $this->mId ) {
                        return;
                }
-               if ( !$this->mToken ) {
-                       // When token is empty or NULL generate a new one and then save it to the database
-                       // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
-                       // Simply by setting every cell in the user_token column to NULL and letting them be
-                       // regenerated as users log back into the wiki.
-                       $this->setToken();
-                       if ( !wfReadOnly() ) {
-                               $this->saveSettings();
-                       }
-               }
-               $session = array(
-                       'wsUserID' => $this->mId,
-                       'wsToken' => $this->mToken,
-                       'wsUserName' => $this->getName()
-               );
-               $cookies = array(
-                       'UserID' => $this->mId,
-                       'UserName' => $this->getName(),
-               );
-               if ( $rememberMe ) {
-                       $cookies['Token'] = $this->mToken;
-               } else {
-                       $cookies['Token'] = false;
-               }
-
-               Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
 
-               foreach ( $session as $name => $value ) {
-                       $request->setSessionData( $name, $value );
+               $session = $this->getRequest()->getSession();
+               if ( $request && $session->getRequest() !== $request ) {
+                       $session = $session->sessionWithRequest( $request );
                }
-               foreach ( $cookies as $name => $value ) {
-                       if ( $value === false ) {
-                               $this->clearCookie( $name );
-                       } elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
-                               $this->setExtendedLoginCookie( $name, $value, $secure );
-                       } else {
-                               $this->setCookie( $name, $value, 0, $secure, array(), $request );
+               $delay = $session->delaySave();
+
+               if ( !$session->getUser()->equals( $this ) ) {
+                       if ( !$session->canSetUser() ) {
+                               \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                                       ->warning( __METHOD__ .
+                                               ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
+                                       );
+                               return;
                        }
+                       $session->setUser( $this );
                }
 
-               /**
-                * If wpStickHTTPS was selected, also set an insecure cookie that
-                * will cause the site to redirect the user to HTTPS, if they access
-                * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
-                * as the one set by centralauth (bug 53538). Also set it to session, or
-                * standard time setting, based on if rememberme was set.
-                */
-               if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
-                       $this->setCookie(
-                               'forceHTTPS',
-                               'true',
-                               $rememberMe ? 0 : null,
-                               false,
-                               array( 'prefix' => '' ) // no prefix
-                       );
+               $session->setRememberUser( $rememberMe );
+               if ( $secure !== null ) {
+                       $session->setForceHTTPS( $secure );
                }
+
+               $session->persist();
+
+               ScopedCallback::consume( $delay );
        }
 
        /**
@@ -3649,20 +3588,29 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Clear the user's cookies and session, and reset the instance cache.
+        * Clear the user's session, and reset the instance cache.
         * @see logout()
         */
        public function doLogout() {
-               $this->clearInstanceCache( 'defaults' );
-
-               $this->getRequest()->setSessionData( 'wsUserID', 0 );
-
-               $this->clearCookie( 'UserID' );
-               $this->clearCookie( 'Token' );
-               $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
-
-               // Remember when user logged out, to prevent seeing cached pages
-               $this->setCookie( 'LoggedOut', time(), time() + 86400 );
+               $session = $this->getRequest()->getSession();
+               if ( !$session->canSetUser() ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
+               } elseif ( !$session->getUser()->equals( $this ) ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( __METHOD__ .
+                                       ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
+                               );
+                       // But we still may as well make this user object anon
+                       $this->clearInstanceCache( 'defaults' );
+               } else {
+                       $this->clearInstanceCache( 'defaults' );
+                       $delay = $session->delaySave();
+                       $session->setLoggedOutTimestamp( time() );
+                       $session->setUser( new User );
+                       $session->set( 'wsUserID', 0 ); // Other code expects this
+                       ScopedCallback::consume( $delay );
+               }
        }
 
        /**
@@ -4160,17 +4108,6 @@ class User implements IDBAccessObject {
                );
        }
 
-       /**
-        * Generate a looking random token for various uses.
-        *
-        * @return string The new random token
-        * @deprecated since 1.20: Use MWCryptRand for secure purposes or
-        *   wfRandomString for pseudo-randomness.
-        */
-       public static function generateToken() {
-               return MWCryptRand::generateHex( 32 );
-       }
-
        /**
         * Get the embedded timestamp from a token.
         * @param string $val Input token
@@ -4570,7 +4507,14 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Check if all users have the given permission
+        * Check if all users may be assumed to have the given permission
+        *
+        * We generally assume so if the right is granted to '*' and isn't revoked
+        * on any group. It doesn't attempt to take grants or other extension
+        * limitations on rights into account in the general case, though, as that
+        * would require it to always return false and defeat the purpose.
+        * Specifically, session-based rights restrictions (such as OAuth or bot
+        * passwords) are applied based on the current session.
         *
         * @since 1.22
         * @param string $right Right to check
@@ -4599,7 +4543,14 @@ class User implements IDBAccessObject {
                        }
                }
 
-               // Allow extensions (e.g. OAuth) to say false
+               // Remove any rights that aren't allowed to the global-session user
+               $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
+               if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
+                       $cache[$right] = false;
+                       return false;
+               }
+
+               // Allow extensions to say false
                if ( !Hooks::run( 'UserIsEveryoneAllowed', array( $right ) ) ) {
                        $cache[$right] = false;
                        return false;
diff --git a/includes/utils/MWGrants.php b/includes/utils/MWGrants.php
new file mode 100644 (file)
index 0000000..b9b51d5
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/**
+ * Functions and constants to deal with grants
+ *
+ * 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
+ */
+
+/**
+ * A collection of public static functions to deal with grants.
+ */
+class MWGrants {
+
+       /**
+        * List all known grants.
+        * @return array
+        */
+       public static function getValidGrants() {
+               global $wgGrantPermissions;
+
+               return array_keys( $wgGrantPermissions );
+       }
+
+       /**
+        * Map all grants to corresponding user rights.
+        * @return array grant => array of rights
+        */
+       public static function getRightsByGrant() {
+               global $wgGrantPermissions;
+
+               $res = array();
+               foreach ( $wgGrantPermissions as $grant => $rights ) {
+                       $res[$grant] = array_keys( array_filter( $rights ) );
+               }
+               return $res;
+       }
+
+       /**
+        * Fetch the display name of the grant
+        * @param string $grant
+        * @param Language|string|null $lang
+        * @return string Grant description
+        */
+       public static function grantName( $grant, $lang = null ) {
+               // Give grep a chance to find the usages:
+               // grant-blockusers, grant-createeditmovepage, grant-delete,
+               // grant-editinterface, grant-editmycssjs, grant-editmywatchlist,
+               // grant-editpage, grant-editprotected, grant-highvolume,
+               // grant-oversight, grant-patrol, grant-protect, grant-rollback,
+               // grant-sendemail, grant-uploadeditmovefile, grant-uploadfile,
+               // grant-basic, grant-viewdeleted, grant-viewmywatchlist,
+               // grant-createaccount
+               $msg = wfMessage( "grant-$grant" );
+               if ( $lang !== null ) {
+                       if ( is_string( $lang ) ) {
+                               $lang = Language::factory( $lang );
+                       }
+                       $msg->inLanguage( $lang );
+               }
+               if ( !$msg->exists() ) {
+                       $msg = wfMessage( 'grant-generic', $grant );
+                       if ( $lang ) {
+                               $msg->inLanguage( $lang );
+                       }
+               }
+               return $msg->text();
+       }
+
+       /**
+        * Fetch the display names for the grants.
+        * @param string[] $grants
+        * @param Language|string|null $lang
+        * @return string[] Corresponding grant descriptions
+        */
+       public static function grantNames( array $grants, $lang = null ) {
+               if ( $lang !== null ) {
+                       if ( is_string( $lang ) ) {
+                               $lang = Language::factory( $lang );
+                       }
+               }
+
+               $ret = array();
+               foreach ( $grants as $grant ) {
+                       $ret[] = self::grantName( $grant, $lang );
+               }
+               return $ret;
+       }
+
+       /**
+        * Fetch the rights allowed by a set of grants.
+        * @param string[]|string $grants
+        * @return string[]
+        */
+       public static function getGrantRights( $grants ) {
+               global $wgGrantPermissions;
+
+               $rights = array();
+               foreach ( (array)$grants as $grant ) {
+                       if ( isset( $wgGrantPermissions[$grant] ) ) {
+                               $rights = array_merge( $rights, array_keys( array_filter( $wgGrantPermissions[$grant] ) ) );
+                       }
+               }
+               return array_unique( $rights );
+       }
+
+       /**
+        * Test that all grants in the list are known.
+        * @param string[] $grants
+        * @return bool
+        */
+       public static function grantsAreValid( array $grants ) {
+               return array_diff( $grants, self::getValidGrants() ) === array();
+       }
+
+       /**
+        * Divide the grants into groups.
+        * @param string[]|null $grantsFilter
+        * @return array Map of (group => (grant list))
+        */
+       public static function getGrantGroups( $grantsFilter = null ) {
+               global $wgGrantPermissions, $wgGrantPermissionGroups;
+
+               if ( is_array( $grantsFilter ) ) {
+                       $grantsFilter = array_flip( $grantsFilter );
+               }
+
+               $groups = array();
+               foreach ( $wgGrantPermissions as $grant => $rights ) {
+                       if ( $grantsFilter !== null && !isset( $grantsFilter[$grant] ) ) {
+                               continue;
+                       }
+                       if ( isset( $wgGrantPermissionGroups[$grant] ) ) {
+                               $groups[$wgGrantPermissionGroups[$grant]][] = $grant;
+                       } else {
+                               $groups['other'][] = $grant;
+                       }
+               }
+
+               return $groups;
+       }
+
+       /**
+        * Get the list of grants that are hidden and should always be granted
+        * @return string[]
+        */
+       public static function getHiddenGrants() {
+               global $wgGrantPermissionGroups;
+
+               $grants = array();
+               foreach ( $wgGrantPermissionGroups as $grant => $group ) {
+                       if ( $group === 'hidden' ) {
+                               $grants[] = $grant;
+                       }
+               }
+               return $grants;
+       }
+
+       /**
+        * Generate a link to Special:ListGrants for a particular grant name.
+        *
+        * This should be used to link end users to a full description of what
+        * rights they are giving when they authorize a grant.
+        *
+        * @param string $grant the grant name
+        * @param Language|string|null $lang
+        * @return string (proto-relative) HTML link
+        */
+       public static function getGrantsLink( $grant, $lang = null ) {
+               return \Linker::linkKnown(
+                       \SpecialPage::getTitleFor( 'Listgrants', false, $grant ),
+                       htmlspecialchars( self::grantName( $grant, $lang ) )
+               );
+       }
+
+       /**
+        * Generate wikitext to display a list of grants
+        * @param string[]|null $grantsFilter If non-null, only display these grants.
+        * @param Language|string|null $lang
+        * @return string Wikitext
+        */
+       public static function getGrantsWikiText( $grantsFilter, $lang = null ) {
+               global $wgContLang;
+
+               if ( is_string( $lang ) ) {
+                       $lang = Language::factory( $lang );
+               } elseif ( $lang === null ) {
+                       $lang = $wgContLang;
+               }
+
+               $s = '';
+               foreach ( self::getGrantGroups( $grantsFilter ) as $group => $grants ) {
+                       if ( $group === 'hidden' ) {
+                               continue; // implicitly granted
+                       }
+                       $s .= "*<span class=\"mw-grantgroup\">" .
+                               wfMessage( "grant-group-$group" )->inLanguage( $lang )->text() . "</span>\n";
+                       $s .= ":" . $lang->semicolonList( self::grantNames( $grants, $lang ) ) . "\n";
+               }
+               return "$s\n";
+       }
+
+}
diff --git a/includes/utils/MWRestrictions.php b/includes/utils/MWRestrictions.php
new file mode 100644 (file)
index 0000000..3b4dc11
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+/**
+ * A class to check request restrictions expressed as a JSON object
+ *
+ * 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
+ */
+
+/**
+ * A class to check request restrictions expressed as a JSON object
+ */
+class MWRestrictions {
+
+       private $ipAddresses = array( '0.0.0.0/0', '::/0' );
+
+       /**
+        * @param array $restrictions
+        */
+       protected function __construct( array $restrictions = null ) {
+               if ( $restrictions !== null ) {
+                       $this->loadFromArray( $restrictions );
+               }
+       }
+
+       /**
+        * @return MWRestrictions
+        */
+       public static function newDefault() {
+               return new self();
+       }
+
+       /**
+        * @param array $restrictions
+        * @return MWRestrictions
+        */
+       public static function newFromArray( array $restrictions ) {
+               return new self( $restrictions );
+       }
+
+       /**
+        * @param string $json JSON representation of the restrictions
+        * @return MWRestrictions
+        */
+       public static function newFromJson( $json ) {
+               $restrictions = FormatJson::decode( $json, true );
+               if ( !is_array( $restrictions ) ) {
+                       throw new InvalidArgumentException( 'Invalid restrictions JSON' );
+               }
+               return new self( $restrictions );
+       }
+
+       private function loadFromArray( array $restrictions ) {
+               static $validKeys = array( 'IPAddresses' );
+               static $neededKeys = array( 'IPAddresses' );
+
+               $keys = array_keys( $restrictions );
+               $invalidKeys = array_diff( $keys, $validKeys );
+               if ( $invalidKeys ) {
+                       throw new InvalidArgumentException(
+                               'Array contains invalid keys: ' . join( ', ', $invalidKeys )
+                       );
+               }
+               $missingKeys = array_diff( $neededKeys, $keys );
+               if ( $missingKeys ) {
+                       throw new InvalidArgumentException(
+                               'Array is missing required keys: ' . join( ', ', $missingKeys )
+                       );
+               }
+
+               if ( !is_array( $restrictions['IPAddresses'] ) ) {
+                       throw new InvalidArgumentException( 'IPAddresses is not an array' );
+               }
+               foreach ( $restrictions['IPAddresses'] as $ip ) {
+                       if ( !\IP::isIPAddress( $ip ) ) {
+                               throw new InvalidArgumentException( "Invalid IP address: $ip" );
+                       }
+               }
+               $this->ipAddresses = $restrictions['IPAddresses'];
+       }
+
+       /**
+        * Return the restrictions as an array
+        * @return array
+        */
+       public function toArray() {
+               return array(
+                       'IPAddresses' => $this->ipAddresses,
+               );
+       }
+
+       /**
+        * Return the restrictions as a JSON string
+        * @param bool|string $pretty Pretty-print the JSON output, see FormatJson::encode
+        * @return string
+        */
+       public function toJson( $pretty = false ) {
+               return FormatJson::encode( $this->toArray(), $pretty, FormatJson::ALL_OK );
+       }
+
+       public function __toString() {
+               return $this->toJson();
+       }
+
+       /**
+        * Test against the passed WebRequest
+        * @param WebRequest $request
+        * @return Status
+        */
+       public function check( WebRequest $request ) {
+               $ok = array(
+                       'ip' => $this->checkIP( $request->getIP() ),
+               );
+               $status = Status::newGood();
+               $status->setResult( $ok === array_filter( $ok ), $ok );
+               return $status;
+       }
+
+       /**
+        * Test an IP address
+        * @param string $ip
+        * @return bool
+        */
+       public function checkIP( $ip ) {
+               foreach ( $this->ipAddresses as $range ) {
+                       if ( \IP::isInRange( $ip, $range ) ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+}
index af06431..cad807c 100644 (file)
        "right-passwordreset": "عرض رسائل إعادة ضبط كلمات السر",
        "right-managechangetags": "إنشاء وحذف [[Special:Tags|الوسوم]] من قاعدة البيانات",
        "right-applychangetags": "تطبيق [[Special:Tags|الوسوم]]  مع التغييرات التي أجريتها.",
+       "grant-generic": "\"$1\" حزمة الصلاحيات",
        "newuserlogpage": "سجل إنشاء المستخدمين",
        "newuserlogpagetext": "هذا سجل بعمليات إنشاء المستخدمين.",
        "rightslog": "سجل صلاحيات المستخدمين",
index 3520f64..5945ddf 100644 (file)
        "october-date": "$1 d'ochobre",
        "november-date": "$1 de payares",
        "december-date": "$1 d'avientu",
+       "period-am": "AM",
+       "period-pm": "PM",
        "pagecategories": "{{PLURAL:$1|Categoría|Categoríes}}",
        "category_header": "Páxines na categoría «$1»",
        "subcategories": "Subcategoríes",
        "virus-scanfailed": "fallu d'escanéu (códigu $1)",
        "virus-unknownscanner": "antivirus desconocíu:",
        "logouttext": "'''Zarró la sesión.'''\n\nTenga en cuenta que dalgunes páxines puen siguir apaeciendo como si inda tuviera la sesión aniciada, mentanto nun llimpie la caché del navegador.",
+       "cannotlogoutnow-title": "Nun puede zarrase sesión agora",
+       "cannotlogoutnow-text": "Nun puede zarrase sesión cuando s'usa $1.",
        "welcomeuser": "¡Bienllegáu, $1!",
        "welcomecreation-msg": "Creóse la to cuenta.\nNun t'escaezas de camudar les tos [[Special:Preferences|preferencies de {{SITENAME}}]].",
        "yourname": "Nome d'usuariu:",
        "remembermypassword": "Recordar la mio identificación nesti restolador (un máximu {{PLURAL:$1|d'un día|de $1 díes}})",
        "userlogin-remembermypassword": "Caltener abierta la sesión",
        "userlogin-signwithsecure": "Usar una conexón segura",
+       "cannotloginnow-title": "Nun puede aniciase sesión agora",
+       "cannotloginnow-text": "Nun puede aniciase sesión cuando s'usa $1.",
        "yourdomainname": "El to dominiu:",
        "password-change-forbidden": "Nun se pueden camudar les contraseñes nesta wiki.",
        "externaldberror": "O hebo un fallu d'autenticación de la base de datos o nun tienes permisu p'anovar la to cuenta esterna.",
        "resetpass_submit": "Configurar la contraseña y aniciar sesión",
        "changepassword-success": "¡Camudóse la contraseña correutamente!",
        "changepassword-throttled": "Ficisti demasiaos intentos d'aniciu de sesión recientes.\nPor favor espera $1 enantes d'intentalo otra vuelta.",
+       "botpasswords": "Contraseñes de bots",
+       "botpasswords-summary": "Les <em>contraseñes de bot</em> permiten l'accesu a una cuenta d'usuariu por aciu de la API sin usar les credenciales d'accesu de la cuenta principal. Los permisos d'usuariu disponibles al aniciar sesión con una contraseña de bot puen tar torgaos.\n\nSi nun sabes pa qué val esto, probablemente nun tendríes d'usalo. Naide tendría de pidite nunca que xeneres una d'estes y que-y la deas.",
+       "botpasswords-disabled": "Les contraseñes de bot tán desactivaes.",
+       "botpasswords-no-central-id": "Pa usar contraseñes de bot tienes d'aniciar sesión con una cuenta centralizada.",
+       "botpasswords-existing": "Contraseñes de bots esistentes",
+       "botpasswords-createnew": "Crear una contraseña nueva de bot",
+       "botpasswords-editexisting": "Editar una contraseña de bot esistiente",
+       "botpasswords-label-appid": "Nome del bot:",
+       "botpasswords-label-create": "Crear",
+       "botpasswords-label-update": "Anovar",
+       "botpasswords-label-cancel": "Encaboxar",
+       "botpasswords-label-delete": "Desaniciar",
+       "botpasswords-label-resetpassword": "Reestablecer la contraseña",
+       "botpasswords-label-grants": "Permisos aplicables:",
+       "botpasswords-help-grants": "Cada permisu da accesu a los permisos de usuario llistaos que yá tenga la cuenta. Mira la [[Special:ListGrants|tabla de permisos]] pa más información.",
+       "botpasswords-label-restrictions": "Torgues d'usu:",
+       "botpasswords-label-grants-column": "Permitío",
+       "botpasswords-bad-appid": "El nome del bot \"$1\" nun ye válidu.",
+       "botpasswords-insert-failed": "Nun pudo amestase'l nome de bot «$1». ¿Taba añadíu yá?",
+       "botpasswords-update-failed": "Nun pudo anovase'l nome de bot «$1». ¿Desaniciaríase?",
+       "botpasswords-created-title": "Creóse la contraseña de bot",
+       "botpasswords-created-body": "La contraseña de bot «$1» creóse correchamente.",
+       "botpasswords-updated-title": "Anovóse la contraseña de bot",
+       "botpasswords-updated-body": "La contraseña de bot «$1» anovóse correchamente.",
+       "botpasswords-deleted-title": "Desanicióse la contraseña de bot",
+       "botpasswords-deleted-body": "La contraseña de bot «$1» desanicióse.",
+       "botpasswords-newpassword": "La nueva contraseña p'aniciar sesión con strong>$1</strong> ye <strong>$2</strong>. <em>Por favor, rexistra esto pa referencies futures.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider nun ta disponible.",
+       "botpasswords-restriction-failed": "Hai torgues de contraseña de bot que torgaron esti aniciu de sesión.",
+       "botpasswords-invalid-name": "El nome d'usuariu especificáu nun contien el separador de contraseña de bot («$1»).",
+       "botpasswords-not-exist": "L'usuariu «$1» nun tien una contraseña de bot llamada «$2».",
        "resetpass_forbidden": "Nun puen camudase les contraseñes",
        "resetpass-no-info": "Tienes d'aniciar sesión pa entrar direutamente a esta páxina.",
        "resetpass-submit-loggedin": "Camudar la contraseña",
        "right-createpage": "Crear páxines (que nun seyan páxines d'alderique)",
        "right-createtalk": "Crear páxines d'alderique",
        "right-createaccount": "Crear cuentes nueves d'usuariu",
+       "right-autocreateaccount": "Aniciar sesión automáticamente con una cuenta d'usuariu esterna",
        "right-minoredit": "Marcar ediciones como menores",
        "right-move": "Treslladar páxines",
        "right-move-subpages": "Treslladar les páxines coles sos subpáxines",
        "right-managechangetags": "Crear y desaniciar [[Special:Tags|etiquetes]] dende la base de datos",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetes]] xunto colos cambios propios",
        "right-changetags": "Amestar y desaniciar [[Special:Tags|etiquetes]] arbitraries en revisiones individuales y entraes del rexistru",
+       "grant-generic": "Conxuntu de drechos \"$1\"",
+       "grant-group-page-interaction": "Interactuar con páxines",
+       "grant-group-file-interaction": "Interactuar con multimedia",
+       "grant-group-watchlist-interaction": "Interactuar cola to llista de vixilancia",
+       "grant-group-email": "Unviar correos",
+       "grant-group-high-volume": "Facer una actividá d'altu volume",
+       "grant-group-customization": "Personalización y preferencies",
+       "grant-group-administration": "Facer aiciones alministratives",
+       "grant-group-other": "Actividaes variaes",
+       "grant-blockusers": "Bloquiar y desbloquiar usuarios",
+       "grant-createaccount": "Crear cuentes",
+       "grant-createeditmovepage": "Crear, editar y mover páxines",
+       "grant-delete": "Desaniciar páxines, revisiones y entraes del rexistru",
+       "grant-editinterface": "Editar l'espaciu de nomes MediaWiki y los CSS/JavaScript d'usuariu",
+       "grant-editmycssjs": "Editar los CSS/JavaScript d'usuariu propios",
+       "grant-editmyoptions": "Editar les preferencies d'usuariu propies",
+       "grant-editmywatchlist": "Editar la llista de vixilancia propia",
+       "grant-editpage": "Editar páxines esistentes",
+       "grant-editprotected": "Editar páxines protexíes",
+       "grant-highvolume": "Ediciones de gran volume",
+       "grant-oversight": "Tapecer usuarios y desaniciar revisiones",
+       "grant-patrol": "Patrullar los cambios fechos nes páxines",
+       "grant-protect": "Protexer y desprotexer páxines",
+       "grant-rollback": "Desfacer los cambios fechos nes páxines",
+       "grant-sendemail": "Unviar corréu a otros usuarios",
+       "grant-uploadeditmovefile": "Xubir, trocar y mover ficheros",
+       "grant-uploadfile": "Xubir ficheros nuevos",
+       "grant-basic": "Permisos básicos",
+       "grant-viewdeleted": "Ver los ficheros y páxines desaniciaos",
+       "grant-viewmywatchlist": "Ver la to llista de siguimientu",
        "newuserlogpage": "Rexistru de creación d'usuarios",
        "newuserlogpagetext": "Esti ye un rexistru de creación d'usuarios.",
        "rightslog": "Rexistru de perfil d'usuariu",
        "action-createpage": "crear páxines",
        "action-createtalk": "crear páxines d'alderique",
        "action-createaccount": "crear esta cuenta d'usuariu",
+       "action-autocreateaccount": "crear automáticamente esta cuenta d'usuariu esterna",
        "action-history": "ver l'historial d'esta páxina",
        "action-minoredit": "marcar esta edición como menor",
        "action-move": "treslladar esta páxina",
        "listgrouprights-namespaceprotection-header": "Torgues d'espaciu de nomes",
        "listgrouprights-namespaceprotection-namespace": "Espaciu de nomes",
        "listgrouprights-namespaceprotection-restrictedto": "Permisu(os) d'edición del usuariu",
+       "listgrants": "Permisos",
+       "listgrants-summary": "Esta ye la llista d'autorizaciones colos accesos asociaos a los permisos de usuario. los usuarios puen autorizar a les aplicaciones qu'usen la so cuenta, pero con permisos llendaos basándose nos permisos que'l usuariu diera a l'aplicación. Sicasí, una aplicación qu'actua nel nome d'un usuariu nun pué usar realmente permisos que nun tenga'l propiu usuariu.\nPué haber [[{{MediaWiki:Listgrouprights-helppage}}|más información]] tocante a los permisos individuales.",
+       "listgrants-grant": "Permisu",
+       "listgrants-rights": "Permisos",
        "trackingcategories": "Categoríes de siguimientu",
        "trackingcategories-summary": "Esta páxina llista les categoríes de siguimientu que rellena automáticamente'l software de MediaWiki. Puen camudase los nomes alterando los mensaxes del sistema correspondientes nel espaciu de nomes {{ns:8}}.",
        "trackingcategories-msg": "Categoría de siguimientu",
        "javascripttest-pagetext-frameworks": "Escueyi un de los siguientes entornos de pruebes: $1",
        "javascripttest-pagetext-skins": "Escueyi una apariencia pa executar les pruebes:",
        "javascripttest-qunit-intro": "Ver la [$1 documentación de les pruebes] en mediawiki.org.",
-       "tooltip-pt-userpage": "La to páxina d'usuariu",
+       "tooltip-pt-userpage": "La to páxina d'{{GENDER:|usuariu|usuaria}}",
        "tooltip-pt-anonuserpage": "La páxina d'usuariu de la IP cola que tas editando",
-       "tooltip-pt-mytalk": "La to páxina d'alderique",
+       "tooltip-pt-mytalk": "La {{GENDER:|to}} páxina d'alderique",
        "tooltip-pt-anontalk": "Alderique de les ediciones feches con esta direición IP",
-       "tooltip-pt-preferences": "Les tos preferencies",
+       "tooltip-pt-preferences": "Les {{GENDER:|tos}} preferencies",
        "tooltip-pt-watchlist": "Llista de les páxines nes que tas vixilando los cambios",
-       "tooltip-pt-mycontris": "Llista de les tos collaboraciones",
+       "tooltip-pt-mycontris": "Llista de les {{GENDER:|tos}} collaboraciones",
        "tooltip-pt-anoncontribs": "Una llista d'ediciones feches dende esta dirección IP",
        "tooltip-pt-login": "T'encamentamos que t'identifiques, anque nun ye obligatorio",
        "tooltip-pt-logout": "Salir",
        "tooltip-t-recentchangeslinked": "Cambios recientes nes páxines enllazaes dende esta",
        "tooltip-feed-rss": "Canal RSS pa esta páxina",
        "tooltip-feed-atom": "Canal Atom pa esta páxina",
-       "tooltip-t-contributions": "Llista de collaboraciones d'esti usuariu",
-       "tooltip-t-emailuser": "Unvia un corréu a esti usuariu",
+       "tooltip-t-contributions": "Llista de collaboraciones d'{{GENDER:$1|esti usuariu|esta usuaria}}",
+       "tooltip-t-emailuser": "Unvia un corréu a {{GENDER:$1|esti usuariu|esta usuaria}}",
        "tooltip-t-info": "Más información sobro esta páxina",
        "tooltip-t-upload": "Xubir ficheros",
        "tooltip-t-specialpages": "Llista de toles páxines especiales",
        "watchlisttools-edit": "Ver y editar la llista de siguimientu",
        "watchlisttools-raw": "Editar la llista de siguimientu (ensin formatu)",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|alderique]])",
+       "timezone-local": "Llocal",
        "duplicate-defaultsort": "Avisu: La clave d'ordenación predeterminada \"$2\" anula la clave d'ordenación anterior \"$1\".",
        "duplicate-displaytitle": "<strong>Avisu:</strong> El títulu a amosar \"$2\" anula el títulu anterior \"$1\".",
        "invalid-indicator-name": "<strong>Error:</strong> L'atributu <code>name</code> de los indicadores d'estáu de la páxina nun pue tar baleru.",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "la páxina inda nun esiste",
        "mw-widgets-titleinput-description-redirect": "redirixir a $1",
-       "api-error-blacklisted": "Escueyi un títulu distintu, más descriptivu."
+       "api-error-blacklisted": "Escueyi un títulu distintu, más descriptivu.",
+       "sessionmanager-tie": "Nun puen combinase dellos tipos de solicitú d'identificación: $1.",
+       "sessionprovider-generic": "sesiones $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sesiones basaes en cookies",
+       "sessionprovider-nocookies": "Les cookies puen tar desactivaes. Asegúrate de tener activaes les cookies y vuelve a principiar."
 }
index 3ae26e9..2564d6a 100644 (file)
@@ -46,7 +46,7 @@
        "tog-watchmoves": "Adlarını dəyişdiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
        "tog-watchdeletion": "Sildiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
        "tog-minordefault": "Standart olaraq bütün redaktələri kiçik redaktə kimi nişanla",
-       "tog-previewontop": "Sınaq göstərişi yazma sahəsindən əvvəl göstər",
+       "tog-previewontop": "Sınaq göstərişini redaktə pəncərəsindən əvvəl göstər",
        "tog-previewonfirst": "İlkin redaktədə sınaq göstərişi",
        "tog-enotifwatchlistpages": "İzləmə siyahısında olan məqalə redaktə olunsa, mənə e-məktub göndər",
        "tog-enotifusertalkpages": "Müzakirə səhifəm redaktə olunsa, mənə e-məktub göndər",
        "confirmable-no": "Xeyr",
        "thisisdeleted": "$1 bax və ya bərpa et?",
        "viewdeleted": "$1 göstərilsin?",
-       "restorelink": "{{PLURAL:$1|bir silinmiş redaktəyə|$1 silinmiş redaktəyə}}",
+       "restorelink": "$1 silinmiş redaktəyə",
        "feedlinks": "Kanal növü:",
        "feed-invalid": "Yanlış qeydiyyat kanalı növü.",
        "feed-unavailable": "Sindikasiya xətləri etibarsızdır",
        "laggedslavemode": "'''Xəbərdarlıq:''' Səhifə son əlavələri əks etdirməyə bilər.",
        "readonly": "Verilənlər bazası bloklanıb",
        "enterlockreason": "Bloklamanın səbəbini və nəzərdə tutulan müddətini qeyd edin",
-       "readonlytext": "Verilənlər bazası ehtimal ki, adi təmir işləri ilə əlaqədar müvəqqəti olaraq yeni məqalələr və dəyişikliklər üçün bağlanmışdır.\nVerilənlər bazasını bloklayan operatorun izahatı: $1",
+       "readonlytext": "Verilənlər bazası ehtimal ki, planlaşdırılmış təmir işləri ilə əlaqədar müvəqqəti olaraq yeni məqalələr və dəyişikliklər üçün bağlanmışdır və təmirdən sonra açılacaqdır.\nVerilənlər bazasını bloklayan idarəçi bunu belə izah edib: $1",
        "missing-article": "Məlumat bazası, tapılması istənən \"$1\" $2 adlı səhifəyə aid mətni tapa bilmədi.\n\nBu vəziyyət səhifənin, silinmiş bir səhifənin keçmiş versiyası olmasından qaynaqlana bilər.\n\nƏgər niyə bu deyilsə, proqramda bir səhv ilə qarşılaşmış ola bilərsiniz.\nXahiş edirik bunu bir [[Special:ListUsers/sysop|İdarəçilərə]], URL not edərək göndərin.",
        "missingarticle-rev": "(versiya №: $1)",
        "missingarticle-diff": "(fərq: $1, $2)",
        "createacct-reason": "Səbəb",
        "createacct-reason-ph": "Niyə başqa bir hesab yaradırsınız",
        "createacct-submit": "İstifadəçi hesabı yarat",
-       "createacct-another-submit": "Başqa bir istifadəçi hesabı yarat",
+       "createacct-another-submit": "İstifadəçi hesabı yarat",
        "createacct-benefit-heading": "{{SITENAME}} sizin kimi insanlar tərəfindən yaradılır.",
        "createacct-benefit-body1": "$1 {{PLURAL:$1|redaktə}}",
        "createacct-benefit-body2": "{{PLURAL:$1|səhifə|səhifə}}",
        "passwordreset-emailtitle": "{{SITENAME}} hesabın yaradılması",
        "passwordreset-emailelement": "İstifadəçi adı: \n$1\n\nMüvəqqəti parol: \n$2",
        "passwordreset-emailsentemail": "Xəbərdarlıq məktubu e-maillə göndərildi.",
-       "changeemail": "E-məktub ünvanını dəyiş",
+       "changeemail": "E-məktub ünvanını dəyiş və ya sil",
        "changeemail-oldemail": "Hazırkı e-poçt ünvanı:",
        "changeemail-newemail": "Yeni e-poçt ünvanı:",
        "changeemail-none": "(yoxdur)",
        "sig_tip": "İmza və vaxt",
        "hr_tip": "Horizontal cizgi",
        "summary": "Qısa məzmun:",
-       "subject": "Mövzu/başlıq:",
+       "subject": "Mövzu:",
        "minoredit": "Kiçik redaktə",
        "watchthis": "Bu səhifəni izlə",
        "savearticle": "Səhifəni qeyd et",
        "prefs-watchlist-token": "İzləmə siyahısı nişanı:",
        "prefs-misc": "Digər seçimlər",
        "prefs-resetpass": "Parolu dəyiş",
-       "prefs-changeemail": "E-poçtu dəyiş",
+       "prefs-changeemail": "E-poçtu dəyiş və ya sil",
        "prefs-setemail": "E-poçt ünvanının nizamlanması",
        "prefs-email": "E-mailin parametrləri",
        "prefs-rendering": "Görünüş",
        "group-bot": "Botlar",
        "group-sysop": "İdarəçilər",
        "group-bureaucrat": "Bürokratlar",
-       "group-suppress": "Müfəttişlər",
+       "group-suppress": "Gizlədənlər",
        "group-all": "(bütün)",
        "group-user-member": "{{GENDER:$1|istifadəçi}}",
        "group-autoconfirmed-member": "Avtotəsdiqlənmiş istifadəçilər",
        "group-bot-member": "{{GENDER:$1|bot}}",
        "group-sysop-member": "{{GENDER:$1|idarəçi}}",
        "group-bureaucrat-member": "{{GENDER:$1|bürokrat}}",
-       "group-suppress-member": "{{GENDER:$1|oversight}}",
+       "group-suppress-member": "{{GENDER:$1|gizlədən}}",
        "grouppage-user": "{{ns:project}}:İstifadəçilər",
        "grouppage-autoconfirmed": "{{ns:project}}:Avtotəsdiqlənmiş istifadəçilər",
        "grouppage-bot": "{{ns:project}}:Botlar",
        "grouppage-sysop": "{{ns:project}}:İdarəçilər",
        "grouppage-bureaucrat": "{{ns:project}}:Bürokratlar",
-       "grouppage-suppress": "{{ns:project}}:Müfəttişlər",
+       "grouppage-suppress": "{{ns:project}}:Gizlədənlər",
        "right-read": "Səhifələrin oxunması",
        "right-edit": "Səhifələrin redaktəsi",
        "right-createpage": "Səhifələr yaratmaq (müzakirə səhifələrindən əlavə səhifələr nəzərdə tutulur)",
        "right-userrights-interwiki": "Digər vikilərdəki istifadəçilərin istifadəçi hüquqlarını dəyişdir",
        "right-siteadmin": "Məlumatlar bazasının bloklanması və blokun götürülməsi",
        "right-sendemail": "Digər istifadəçilərə elektron poçt göndər",
+       "grant-editmywatchlist": "İzləmə siyahınızda redaktə",
        "newuserlogpage": "Yeni istifadəçilərin qeydiyyatı",
        "newuserlogpagetext": "Yeni qeydiyyatdan keçmiş istifadəçilərin siyahısı.",
        "rightslog": "İstifadəçi hüquqları qeydləri",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[$1 izləyən istifadəçi]",
        "rc_categories": "Kateqoriyalara limit qoy (\"|\" ilə ayır)",
-       "rc_categories_any": "Hər",
+       "rc_categories_any": "Seçilənlərdən hər hansı biri",
        "rc-change-size": "$1",
        "rc-change-size-new": "Dəyişiklikdən sonrakı ölçü: $1 bayt",
        "newsectionsummary": "/* $1 */ yeni bölmə",
        "reuploaddesc": "Return to the upload form.",
        "upload-tryagain": "Dəyşdirilmiş fayl izahını göndər",
        "uploadnologin": "Daxil olmamısınız",
-       "uploadnologintext": "Fayl yükləmək üçün [[Special:UserLogin|daxil olmalısınız]].",
+       "uploadnologintext": "Fayl yükləmək üçün siz sistemə $1.",
        "upload_directory_missing": "($1) yükləmə qaydası axtarılır və vebserverdə yaradılması qeyri-mümkündür.",
        "upload_directory_read_only": "\"$1\" kataloqunun arxivi veb-server yazıları üçün qapalıdır.",
        "uploaderror": "Yükləmə xətası",
        "upload-recreate-warning": "'''Diqqət: Bu adda fayl silinib, yaxud adı dəyişdirilib.'''\n\nBu səhifənin silinmə və addəyişmə jurnalı aşağıda göstərilmişdir:",
        "uploadtext": "Fayl yükləmək üçün aşağıdakı formadan istifadə edin.\nƏvvəllər yüklənmiş fayllara baxmaq üçün [[Special:FileList|yüklənmiş fayllar siyahısına]] keçin, həmçinin (təkrar) yüklənmiş fayllara [[Special:Log/upload|yükləmə jurnalında]], silinmiş fayllara [[Special:Log/delete|silinmə jurnalında]] baxa bilərsiniz.\n\nMəqaləyə fayl yerləşdirmək üçün aşağıdaki formalardan birini istifadə edin:\n* Faylın tam versiyasını yerləşdirmək üçün: '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code>''';\n* Faylın 200 pikselədək kiçildilmiş versiyasını mətndən solda, altında izahla yerləşdirmək üçün: '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|təsvir]]</nowiki></code>''';\n* Səhifədə faylın özünü göstərmədən ona birbaşa keçid yerləşdirmək üçün: '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>'''.",
-       "upload-permitted": "İcazə verilən fayl tipləri: $1.",
-       "upload-preferred": "İcazə verilən fayl tipləri: $1.",
-       "upload-prohibited": "İcazə verilməyən fayl tipləri: $1.",
+       "upload-permitted": "İcazə verilən fayl {{PLURAL:$2|tipi|tipləri}}: $1.",
+       "upload-preferred": "Üstünlük verilən fayl {{PLURAL:$2|tipi|tipləri}}: $1.",
+       "upload-prohibited": "Qadağan olunan fayl {{PLURAL:$2|tipi|tipləri}}: $1.",
        "uploadlogpage": "Yükləmə qeydləri",
        "uploadlogpagetext": "Aşağıda ən yeni yükləmə jurnal qeydləri verilmişdir.",
        "filename": "Fayl adı",
        "nopagetitle": "Belə hədəf səhifəsi yoxdur",
        "pager-newer-n": "{{PLURAL:$1|1 daha yeni|$1 daha yeni}}",
        "pager-older-n": "{{PLURAL:$1|1 daha köhnə|$1 daha köhnə}}",
-       "suppress": "Təftişçi",
+       "suppress": "Gizlət",
        "booksources": "Kitab mənbələri",
        "booksources-search-legend": "Kitab mənbələri axtar",
        "booksources-isbn": "ISBN:",
        "addwatch": "İzləmə siyahısına əlavə et",
        "addedwatchtext": "\"[[:$1]]\" səhifəsi [[Special:Watchlist|izlədiyiniz səhifələr]] siyahısına əlavə edildi. Bu səhifədə və əlaqəli müzakirə səhifəsindəki bütün dəyişikliklər orada göstəriləcək və səhifə asanlıqla seçiləbilmək üçün [[Special:RecentChanges|son dəyişikliklərdə]] qalın şriftlərlə görünəcəkdir. <p> Səhifəni izləmə siyahınızdan çıxarmaq üçün yan lövhədəki \"izləmə\" düyməsinə vurun.",
        "removewatch": "Bu səhifəni İzlədiyim səhifələr siyahısından çıxar",
-       "removedwatchtext": "\"[[:$1]]\" səhifəsi [[Special:Watchlist|izləmə siyahınızdan]] çıxarıldı.",
+       "removedwatchtext": "\"[[:$1]]\" səhifəsi sizin [[Special:Watchlist|izləmə siyahınızdan]] çıxarıldı.",
        "watch": "İzlə",
        "watchthispage": "Bu səhifəni izlə",
        "unwatch": "İzləmə",
        "notanarticle": "Səhifə boşdur",
        "notvisiblerev": "Başqa istifadıçinin son dəyişikliyi silinib",
        "watchlist-details": "İzləmə siyahınızda, müzakirə səhifələrini çıxmaq şərtilə, {{PLURAL:$1|$1 səhifə|$1 səhifə}} var.",
-       "wlheader-enotif": " E-məktubla bildiriş aktivdir.",
+       "wlheader-enotif": "E-məktubla bildiriş aktivdir.",
        "wlheader-showupdated": "Son ziyarətinizdən sonra edilən dəyişikliklər '''qalın şriftlərlə''' göstərilmişdir.",
        "wlnote": "Aşağıdakı {{PLURAL:$1|'''$1''' dəyişiklik|'''$1''' dəyişiklik}} son {{PLURAL:$2|saatda|'''$2''' saatda}} edilmişdir.",
-       "wlshowlast": "Bunları göstər: son $1 saatı $2 günü",
+       "wlshowlast": "Son $1 saatı $2 günü göstər",
        "watchlistall2": "hamısı",
        "wlshowhidemine": "mənimn redaktələrim",
        "watchlist-options": "İzləmə siyahısının nizamlamaları",
        "deletecomment": "Səbəb:",
        "deleteotherreason": "Digər/əlavə səbəb:",
        "deletereasonotherlist": "Digər səbəb",
-       "deletereason-dropdown": "*Əsas silmə səbəbi\n** Müəllif istəyi\n** Müəllif hüququ pozuntusu\n** Vandalizm",
+       "deletereason-dropdown": "*Əsas silmə səbəbləri\n** Spam\n** Vandalizm\n** Müəllif hüququ pozuntusu\n** Müəllif istəyi\n** Səhv yönləndirmə",
        "delete-edit-reasonlist": "Silmə səbəblərinin redaktəsi",
        "delete-toobig": "Bu səhifə $1-dən artıq redaktə ilə çox böyük redaktə tarixçəsinə malikdir.\n\"{{SITENAME}}\" saytının fəaliyyətində problemlər yaratmamaq üçün bu cür səhifələrin silinməsi qadağandır.",
        "rollback": "əvvəlki halına qaytar",
        "protect-locked-access": "Sizin hesabınızın mühafizə səviyyəsini dəyişməyə ixtiyarı yoxdur.\n'''$1''' səhifəsində hal-hazırda edə biləcəyiniz əməliyyatlar bunlardır:",
        "protect-cascadeon": "Bu səhifə mühafizəlidir, çünki bu səhifə {{PLURAL:$1|başqa bir|başqa bir}} səhifədən kaskad mühafizə edilmişdir. Siz bu səhifənin mühafizə səviyyəsini dəyişdirə bilərsiniz, bu kaskad mühafizəyə təsir etməyəcək.",
        "protect-default": "Bütün istifadəçilərə icazə ver",
-       "protect-fallback": "\"$1\" icazəsi tələb olunur",
-       "protect-level-autoconfirmed": "Yeni və anonim istifadəçiləri blokla",
+       "protect-fallback": "Yalnız \"$1\" hüquqları olan istifadəçilərə icazə verilir",
+       "protect-level-autoconfirmed": "Yalnız avtotəsdiqlənmiş istifadəçilərə icazə verilir",
        "protect-level-sysop": "Yalnız idarəçilərə icazə verilir",
        "protect-summary-cascade": "kaskad mühafizə",
        "protect-expiring": "$1 (UTC)- tarixində vaxtı bitir",
        "djvu_no_xml": "DjVu üçün XML faylı almaq mümkün deyil.",
        "thumbnail_image-missing": "Belə görünür ki, $1 faylı yoxdur",
        "import": "Səhifələri idxal et",
-       "importinterwiki": "Vikilərarası çıxarma",
+       "importinterwiki": "Başqa vikidən idxal et",
        "import-interwiki-history": "Səhifənin dəyişmə tarixçələrinin hamısını köçür",
        "import-interwiki-templates": "Bütün şablonlarla birlikdə",
        "import-interwiki-submit": "İdxal",
        "importlogpagetext": "Səhifələrin idarəçilər tərəfindən digər vikilərdən dəyişiklik tarixçəsi ilə birlikdə köçürülməsi",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|dəyişiklik|dəyişiklik}} idxal edildi.",
        "import-logentry-interwiki-detail": "$2 vikidən $1 {{PLURAL:$1|dəyişiklik|dəyişiklik}} idxal edildi",
-       "tooltip-pt-userpage": "İstifadəçi səhifəniz",
+       "tooltip-pt-userpage": "{{GENDER:|İstifadəçi}} səhifəniz",
        "tooltip-pt-anonuserpage": "The user page for the ip you",
-       "tooltip-pt-mytalk": "Danışıq səhifəm",
+       "tooltip-pt-mytalk": "{{GENDER:|Sizin}} müzakirə səhifəniz",
        "tooltip-pt-anontalk": "Bu IP ünvanından redaktə olunmuş danışıqlar",
-       "tooltip-pt-preferences": "Mənim nizamlamalarım",
+       "tooltip-pt-preferences": "{{GENDER:|Sizin}} nizamlamalarınız",
        "tooltip-pt-watchlist": "İzləməyə götürdüyüm səhifələr",
-       "tooltip-pt-mycontris": "Etdiyim dəyişikliklərin siyahısı",
+       "tooltip-pt-mycontris": "{{GENDER:|Sizin}} töhfələrinizin siyahısı",
        "tooltip-pt-login": "Daxil olmanız tövsiyə olunur, amma bu məcburi tələb deyil.",
        "tooltip-pt-logout": "Sistemdən çıx",
        "tooltip-ca-talk": "Məqalə haqqındə müzakirə edib, münasibətivi bildir",
        "tooltip-feed-rss": "Bu səhifə üçün RSS yayımı",
        "tooltip-feed-atom": "Bu səhifə üçün Atom yayımı",
        "tooltip-t-contributions": "Bu istifadəçinin redaktə etdiyi səhifələrin siyahısı",
-       "tooltip-t-emailuser": "Bu istifadəçiyə e-məktub yolla",
+       "tooltip-t-emailuser": "{{GENDER:$1|Bu istifadəçiyə}} e-məktub göndər",
        "tooltip-t-upload": "Yeni şəkil və ya multimedia faylı yüklə",
        "tooltip-t-specialpages": "Xüsusi səhifələrin siyahısı",
        "tooltip-t-print": "Səhifənin çap versiyası",
        "tooltip-ca-nstab-main": "Məqalənin məzmununu göstər",
        "tooltip-ca-nstab-user": "İstifadəçi səhifəsinə bax",
        "tooltip-ca-nstab-media": "Media-fayl",
-       "tooltip-ca-nstab-special": "Bu xüsusi səhifə olduğu üçün redaktə edilə bilməz",
+       "tooltip-ca-nstab-special": "Bu xidməti səhifədir və redaktə edilə bilməz",
        "tooltip-ca-nstab-project": "Layihə səhifəsinə bax",
        "tooltip-ca-nstab-image": "Faylın səhifəsinə bax",
        "tooltip-ca-nstab-mediawiki": "Sistem məlumatına bax",
        "creditspage": "Səhifə kreditleri",
        "spamprotectiontitle": "Spam qoruma süzgəci",
        "spambot_username": "MediaViki spam təmizləməsi",
-       "simpleantispam-label": "Anti-spam yoxlanılması.\nBuranı <strong>DOLDURMAYIN</strong>!",
+       "simpleantispam-label": "Anti-spam yoxlaması.\nBuranı <strong>doldurmayın</strong>!",
        "pageinfo-title": "\"$1\" üçün məlumat",
        "pageinfo-header-basic": "Əsas məlumatlar",
        "pageinfo-header-edits": "Redaktə tarixçəsi",
index 094f532..db2ef5e 100644 (file)
        "virus-scanfailed": "памылка сканаваньня (код $1)",
        "virus-unknownscanner": "невядомы антывірус:",
        "logouttext": "<strong>Вы выйшлі з сыстэмы.</strong>\n\nНекаторыя старонкі могуць яшчэ паказваць, нібы вы ў сыстэме. Каб гэтага пазьбегнуць, трэба ачысьціць кэш браўзэра.",
+       "cannotlogoutnow-title": "Цяпер немагчыма выйсьці",
+       "cannotlogoutnow-text": "Выхад з сыстэмы немагчымы пры выкарыстаньні $1.",
        "welcomeuser": "Вітаем, $1!",
        "welcomecreation-msg": "Ваш рахунак быў створаны.\nВы можаце зьмяніць Вашыя [[Special:Preferences|налады ў {{GRAMMAR:месны|{{SITENAME}}}}]], калі пажадаеце.",
        "yourname": "Імя ўдзельніка:",
        "remembermypassword": "Запомніць мяне на гэтым кампутары (ня больш за $1 {{PLURAL:$1|дзень|дні|дзён}})",
        "userlogin-remembermypassword": "Запомніць мяне",
        "userlogin-signwithsecure": "Скарыстацца бясьпечным злучэньнем",
+       "cannotloginnow-title": "Цяпер немагчыма ўвайсьці",
        "yourdomainname": "Ваш дамэн:",
        "password-change-forbidden": "Вы ня можаце зьмяняць паролі ў гэтай вікі.",
        "externaldberror": "Адбылася памылка аўтэнтыфікацыі з дапамогай вонкавай базы зьвестак, ці Вам не дазволена абнаўляць свой рахунак.",
        "resetpass_submit": "Захаваць пароль і ўвайсьці",
        "changepassword-success": "Ваш пароль быў пасьпяхова зьменены!",
        "changepassword-throttled": "Вы зрабілі зашмат спробаў увайсьці ў сыстэму.\nКалі ласка, пачакайце $1 перад наступнай спробай.",
+       "botpasswords-label-create": "Стварыць",
+       "botpasswords-label-update": "Абнавіць",
+       "botpasswords-label-cancel": "Скасаваць",
+       "botpasswords-label-delete": "Выдаліць",
+       "botpasswords-label-resetpassword": "Ачысьціць пароль",
        "resetpass_forbidden": "Пароль ня можа быць зьменены",
        "resetpass-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
        "resetpass-submit-loggedin": "Зьмяніць пароль",
        "right-managechangetags": "ствараць і выдаляць [[Special:Tags|меткі]] з базы зьвестак",
        "right-applychangetags": "дадаваць [[Special:Tags|меткі]] пры рэдагаваньні",
        "right-changetags": "дадаваць і выдаляць адвольныя [[Special:Tags|меткі]] да асобных вэрсіяў і запісаў у журнале падзеяў",
+       "grant-createaccount": "Стварыць рахункі",
+       "grant-createeditmovepage": "Ствараць, рэдагаваць і пераносіць старонкі",
+       "grant-delete": "Выдаляць старонкі, вэрсіі і запісы журналу",
+       "grant-editinterface": "Рэдагаваць прасторы назваў МэдыяВікі і CSS/JavaScript удзельніка",
+       "grant-editmycssjs": "Рэдагаваць Ваш CSS/JavaScript",
+       "grant-editmyoptions": "Рэдагаваць Вашыя налады ўдзельніка",
+       "grant-editmywatchlist": "Рэдагаваць ваш сьпіс назіраньня",
+       "grant-editpage": "Рэдагаваць існыя старонкі",
+       "grant-editprotected": "Рэдагаваць абароненыя старонкі",
        "newuserlogpage": "Журнал стварэньня рахункаў",
        "newuserlogpagetext": "Гэта журнал стварэньня рахункаў удзельнікаў і ўдзельніц.",
        "rightslog": "Журнал правоў удзельнікаў",
        "javascripttest-pagetext-frameworks": "Калі ласка, выберыце адну з прапанаваных бібліятэка тэставаньня: $1",
        "javascripttest-pagetext-skins": "Выберыце афармленьне для тэставаньня:",
        "javascripttest-qunit-intro": "Глядзіце [$1 дакумэнтацыю па тэставаньні] на mediawiki.org.",
-       "tooltip-pt-userpage": "Вашая ўласная старонка",
+       "tooltip-pt-userpage": "{{GENDER:|Вашая ўласная}} старонка",
        "tooltip-pt-anonuserpage": "Старонка ўдзельніка для IP-адрасу, зь якога Вы рэдагуеце",
-       "tooltip-pt-mytalk": "Ваша старонка гутарак",
+       "tooltip-pt-mytalk": "{{GENDER:|Вашая}} старонка гутарак",
        "tooltip-pt-anontalk": "Старонка гутарак пра рэдагаваньні, зробленыя з гэтага IP-адрасу",
-       "tooltip-pt-preferences": "Вашыя налады",
+       "tooltip-pt-preferences": "{{GENDER:|Вашыя}} налады",
        "tooltip-pt-watchlist": "Сьпіс старонак, за зьменамі якіх Вы назіраеце",
-       "tooltip-pt-mycontris": "Ваш унёсак",
+       "tooltip-pt-mycontris": "{{GENDER:|Ваш}} унёсак",
        "tooltip-pt-anoncontribs": "Сьпіс рэдагаваньняў, зробленых з гэтага IP-адрасу",
        "tooltip-pt-login": "Вас запрашаюць увайсьці, хаця гэта і неабавязкова.",
        "tooltip-pt-logout": "Выйсьці",
        "tooltip-t-recentchangeslinked": "Апошнія зьмены ў старонках, на якія спасылаецца гэтая старонка",
        "tooltip-feed-rss": "RSS-стужка для гэтай старонкі",
        "tooltip-feed-atom": "Atom-стужка для гэтай старонкі",
-       "tooltip-t-contributions": "Унёсак гэтага ўдзельніка/гэтай удзельніцы",
-       "tooltip-t-emailuser": "Даслаць ліст гэтаму ўдзельніку/гэтай удзельніцы па электроннай пошце",
+       "tooltip-t-contributions": "Унёсак {{GENDER:$1|гэтага ўдзельніка|гэтай удзельніцы}}",
+       "tooltip-t-emailuser": "Даслаць ліст {{GENDER:$1|гэтаму ўдзельніку|гэтай удзельніцы}} па электроннай пошце",
        "tooltip-t-info": "Болей інфармацыі пра гэтую старонку",
        "tooltip-t-upload": "Загрузіць файлы",
        "tooltip-t-specialpages": "Сьпіс усіх спэцыяльных старонак",
index 07bb46f..53e3cf4 100644 (file)
        "right-blockemail": "блокиране на потребители да изпращат писма по е-поща",
        "right-hideuser": "блокиране и скриване на потребителско име",
        "right-ipblock-exempt": "пренебрегване на блокирания по IP blocks, автоматични блокирания и блокирани IP интервали",
-       "right-proxyunbannable": "пÑ\80енебÑ\80егване на автоматичното блокиране на проксита",
+       "right-proxyunbannable": "Ð\97аобикалÑ\8fне на автоматичното блокиране на проксита",
        "right-unblockself": "Собствено отблокиране",
        "right-protect": "променяне на нивото на защита и редактиране на защитени страници",
        "right-editprotected": "редактиране на защитени страници (без каскадна защита)",
        "unblocked": "[[User:$1|$1]] беше отблокиран.",
        "unblocked-range": "$1 беше отблокиран",
        "unblocked-id": "Блок № $1 беше премахнат",
+       "unblocked-ip": "[[Special:Contributions/$1|$1]] е отблокиран.",
        "blocklist": "Блокирани потребители",
        "ipblocklist": "Блокирани потребители",
        "ipblocklist-legend": "Откриване на блокиран потребител",
        "export-download": "Съхраняване като файл",
        "export-templates": "Включване на шаблоните",
        "export-pagelinks": "Включване на свързаните страници с дълбочина до:",
+       "export-manual": "Добавяне на страниците ръчно:",
        "allmessages": "Системни съобщения",
        "allmessagesname": "Име",
        "allmessagesdefault": "Текст по подразбиране",
        "tooltip-pt-anontalk": "Дискусия относно редакциите от този адрес",
        "tooltip-pt-preferences": "Вашите настройки",
        "tooltip-pt-watchlist": "Списък на страници, чиито промени сте избрали да наблюдавате",
-       "tooltip-pt-mycontris": "Списък на вашите приноси",
+       "tooltip-pt-mycontris": "Списък на {{GENDER:|вашите}} приноси",
+       "tooltip-pt-anoncontribs": "Списък на промените, направени от този IP адрес",
        "tooltip-pt-login": "Насърчаваме Ви да влезете, въпреки че не е задължително.",
        "tooltip-pt-logout": "Излизане от {{SITENAME}}",
        "tooltip-pt-createaccount": "Насърчаваме Ви да си създадете сметка и да влезете, въпреки че не е задължително.",
        "tooltip-t-recentchangeslinked": "Последните промени на страници, сочени от тази страница",
        "tooltip-feed-rss": "RSS feed за страницата",
        "tooltip-feed-atom": "Atom feed за страницата",
-       "tooltip-t-contributions": "Показване на приносите на потребителя",
-       "tooltip-t-emailuser": "Изпращане на писмо до потребителя",
+       "tooltip-t-contributions": "Показване на приносите на {{GENDER:$1|потребителя}}",
+       "tooltip-t-emailuser": "Изпращане на писмо до {{GENDER:$1|потребителя}}",
        "tooltip-t-info": "Повече за тази страница",
        "tooltip-t-upload": "Качи файлове",
        "tooltip-t-specialpages": "Списък на всички специални страници",
        "version-libraries": "Инсталирани библиотеки",
        "version-libraries-library": "Библиотека",
        "version-libraries-version": "Версия",
+       "version-libraries-license": "Лиценз",
        "version-libraries-description": "Описание",
        "version-libraries-authors": "Автори",
        "redirect-submit": "Отваряне",
        "revdelete-uname-hid": "скрито потребителско име",
        "revdelete-restricted": "добавени ограничения за администраторите",
        "revdelete-unrestricted": "премахнати ограничения за администраторите",
+       "logentry-suppress-block": "$1 {{GENDER:$2|блокира}} {{GENDER:$4|$3}} за срок от $5 $6",
+       "logentry-suppress-reblock": "$1 {{GENDER:$2|промени}} настройките на блокиране на {{GENDER:$4|$3}} за срок от $5 $6",
        "logentry-move-move": "$1 {{GENDER:$2|премести}} страница „$3“ като „$4“",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|премести}} страницата „$3“ като „$4“ без пренасочване",
        "logentry-move-move_redir": "$1 {{GENDER:$2|премести}} страницата $3 като $4 (върху пренасочване)",
        "special-characters-title-endash": "средно тире",
        "special-characters-title-emdash": "дълго тире",
        "special-characters-title-minus": "знак минус",
+       "mw-widgets-dateinput-no-date": "Нищо не е избрано",
        "mw-widgets-dateinput-placeholder-day": "ГГГГ-ММ-ДД",
        "mw-widgets-dateinput-placeholder-month": "ГГГГ-ММ",
        "mw-widgets-titleinput-description-new-page": "страницата все още не съществува",
index 3c1156c..7c42165 100644 (file)
        "right-managechangetags": "ডাটাবেস থেকে [[Special:Tags|ট্যাগ]] তৈরি ও অপসারণ করুন",
        "right-applychangetags": "সম্পাদনার সাথে [[Special:Tags|ট্যাগ]] যুক্ত করুন",
        "right-changetags": "নির্দিষ্ট সংস্করণ এবং দীর্ঘ সম্পাদনাগুলোতে [[Special:Tags|ট্যাগ]] সংযোজন ও অপসারণ করুন",
+       "grant-group-email": "ইমেইল পাঠান",
+       "grant-createaccount": "অ্যাকাউন্ট তৈরি করুন",
+       "grant-editmyoptions": "আপনার ব্যবহারকারী পছন্দসমূহ সম্পাদনা করুন",
+       "grant-editmywatchlist": "আপনার নজরতালিকা সম্পাদনা করুন",
        "newuserlogpage": "ব্যবহারকারী সৃষ্টির লগ",
        "newuserlogpagetext": "এটি নতুন ব্যবহারকারী সৃষ্টির লগ",
        "rightslog": "ব্যবহারকারীর অধিকার লগ",
        "listgrouprights-removegroup-self-all": "নিজের অ্যাকাউন্ট থেকে সকল দল অপসারণ",
        "listgrouprights-namespaceprotection-header": "নামস্থান নিষেধাজ্ঞাসমূহ",
        "listgrouprights-namespaceprotection-namespace": "নামস্থান",
+       "listgrants-rights": "অধিকার",
        "trackingcategories": "বিষয়শ্রেণীসমূহ অনুসরণ করা হচ্ছে",
        "trackingcategories-msg": "বিষয়শ্রেণী অনুসরণ করা হচ্ছে",
        "trackingcategories-name": "বার্তা নাম",
index b00dab0..1fd444d 100644 (file)
        "right-override-export-depth": "Ezporzhiañ ar pajennoù en ur lakaat e-barzh ar pajennoù liammet betek un donder a 5 live",
        "right-sendemail": "Kas ur postel d'an implijerien all",
        "right-passwordreset": "Gwelet ar posteloù assevel gerioù-tremen",
+       "grant-group-email": "Kas ur postel",
+       "grant-createaccount": "Krouiñ kontoù",
+       "grant-createeditmovepage": "Krouiñ, aozañ ha dilec'hiañ pajennoù",
+       "grant-editmywatchlist": "Aozañ ho roll evezhiañ",
+       "grant-editpage": "Aoañ pajennoù a zo anezho dija",
+       "grant-editprotected": "Aozañ pajennoù gwarezet",
+       "grant-sendemail": "Kas ur postel d'an implijerien all",
+       "grant-viewdeleted": "Gwelet an titouroù dilamet",
+       "grant-viewmywatchlist": "Gwelet ho roll evezhiañ",
        "newuserlogpage": "Marilh ar c'hontoù krouet",
        "newuserlogpagetext": "Marilh krouiñ ar c'hontoù implijer.",
        "rightslog": "Marilh statud an implijerien",
        "listgrouprights-removegroup-self-all": "Gallout a ra tennañ kuit an holl strolladoù eus kont an-unan.",
        "listgrouprights-namespaceprotection-namespace": "Esaouenn anv",
        "listgrouprights-namespaceprotection-restrictedto": "Gwir(ioù) hag a aotre an implijer da aozañ",
+       "listgrants-rights": "Gwirioù",
        "trackingcategories": "Rummadoù evezhiañ",
        "trackingcategories-msg": "Rummad evezhiañ",
        "trackingcategories-name": "Anv ar gemennadenn",
index 623f260..6c9e7ba 100644 (file)
        "right-managechangetags": "Napravi i briši [[Special:Tags|oznake]] iz baze podataka",
        "right-applychangetags": "Primijeni [[Special:Tags|oznake]] na nečije izmjene",
        "right-changetags": "Dodavanje ili uklanjanje raznih [[Special:Tags|oznaka]] na pojedinačnim verzijama i unosima zapisnika",
+       "grant-createeditmovepage": "Pravljenje, uređivanje i premještanje stranica",
+       "grant-editmywatchlist": "Uređivanje Vašeg spiska praćenja",
+       "grant-editpage": "Uređivanje postojećih stranica",
+       "grant-editprotected": "Uređivanje zaštićenih stranica",
+       "grant-highvolume": "Uređivanja velikog opsega",
+       "grant-uploadeditmovefile": "Postavljanje, zamjena i premještanje datoteka",
+       "grant-uploadfile": "Postavljanje novih datoteka",
+       "grant-viewmywatchlist": "Pregled Vašeg spiska praćenja",
        "newuserlogpage": "Zapisnik novih korisnika",
        "newuserlogpagetext": "Ovo je zapisnik o registraciji novih korisnika.",
        "rightslog": "Zapisnik korisničkih prava",
        "listgrouprights-namespaceprotection-header": "Ograničenja imenskog prostora",
        "listgrouprights-namespaceprotection-namespace": "Imenski prostor",
        "listgrouprights-namespaceprotection-restrictedto": "Prava kojima se dozvoljava korisniku da uređuje",
+       "listgrants-summary": "Ovo je spisak OAuth dozvola s odgovarajućim pravima uz svaku dozvolu s desne strane. Korisnici aplikacijama mogu odobriti da koriste njihov korisnički račun ali uz ograničena prava u zavisnosti od tog koju dozvolu im korisnik omogući. Međutim, aplikacija koja se koristi korisničkim računom ne može koristiti prava koja korisnik ne posjeduje. Moguće je da postoje [[{{MediaWiki:Listgrouprights-helppage}}|dodatne informacije]] o pojedinim pravima.",
        "trackingcategories": "Praćenje kategorija",
        "trackingcategories-summary": "Ova stranica prikazuje prateće kategorije koje MediaWiki softver automatski popunjava. Njihovi nazivi se mogu promijeniti izmjenom odgovarajućih sistemskih poruka u imenskom prostoru {{ns:8}}.",
        "trackingcategories-msg": "Praćenje kategorije",
        "protect-cascadeon": "Ova stranica je trenutno zaštićena jer je uključena u {{PLURAL:$1|stranicu, koja ima|stranice, koje imaju}} uključenu prenosivu zaštitu.\nPromjene stepena zaštite ove stranice neće utjecati na prenosnu zaštitu.",
        "protect-default": "Dopušteno svim korisnicima",
        "protect-fallback": "Dozvolite samo korisnicima sa \"$1\" ovlastima/privilegijama",
-       "protect-level-autoconfirmed": "Dopušteno samo automatski potvrđenim korisnicima",
-       "protect-level-sysop": "Dopušteno samo administratorima",
+       "protect-level-autoconfirmed": "dopušteno samo automatski potvrđenim korisnicima",
+       "protect-level-sysop": "dopušteno samo administratorima",
        "protect-summary-desc": "[$1=$2] ($3)",
        "protect-summary-cascade": "prenosna zaštita",
        "protect-expiring": "ističe $1 (UTC)",
-       "protect-expiring-local": "ističe $1",
+       "protect-expiring-local": "ističe $2 u $3",
        "protect-expiry-indefinite": "neodređeno",
        "protect-cascade": "Zaštiti sve stranice koje su uključene u ovu (prenosiva zaštita)",
        "protect-cantedit": "Ne možete mijenjati nivo zaštite ove stranice, jer nemate prava da je uređujete.",
        "protect-edit-reasonlist": "Uredi razloge zaštićivanja",
        "protect-expiry-options": "1 sat:1 hour,1 dan:1 day,1 sedmica:1 week,2 sedmice:2 weeks,1 mjesec:1 month,3 mjeseca:3 months,6 mjeseci:6 months,1 godine:1 year,beskonačno:infinite",
        "restriction-type": "Dopuštenje:",
-       "restriction-level": "Stepen ograničenja:",
+       "restriction-level": "Nivo zaštite:",
        "minimum-size": "Najmanja veličina",
        "maximum-size": "Najveća veličina:",
        "pagesize": "(bajta)",
-       "restriction-edit": "Uređivanje",
-       "restriction-move": "Premještanje",
-       "restriction-create": "Napravi",
+       "restriction-edit": "uređivanje",
+       "restriction-move": "premještanje",
+       "restriction-create": "pravljenje",
        "restriction-upload": "Postavljanje",
        "restriction-level-sysop": "potpuno zaštićeno",
        "restriction-level-autoconfirmed": "poluzaštićeno",
        "undeleteviewlink": "pogledaj",
        "undeleteinvert": "Izmijeni odabir",
        "undeletecomment": "Razlog:",
-       "undeletedrevisions": "{{PLURAL:$1|$1 verzija vraćena|$1 verzije vraćene|$1 verzija vraćeno}}",
+       "undeletedrevisions": "{{PLURAL:$1|vraćena $1 verzija|vraćene $1 verzije|vraćeno $1 verzija}}",
        "undeletedrevisions-files": "{{PLURAL:$1|1 verzija|$1 verzije|$1 verzija}} i {{PLURAL:$2|1 datoteka|$2 datoteke|$2 datoteka}} vraćeno",
        "undeletedfiles": "{{PLURAL:$1|1 datoteka vraćena|$1 datoteke vraćene|$1 datoteka vraćeno}}",
        "cannotundelete": "Vraćanje nije uspjelo:\n$1",
        "logentry-move-move": "$1 {{GENDER:$2|premjestio|premjestila}} je stranicu $3 na $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|premjestio|premjestila}} je stranicu $3 na $4 bez ostavljanja preusmjerenja",
        "logentry-move-move_redir": "$1 {{GENDER:$2|premjestio|premjestila}} je stranicu $3 na $4 preko preusmjerenja",
-       "logentry-move-move_redir-noredirect": "$1 {{GENDER:$2|premjestio|premjestila}} je stranicu $3 na $4 preko preusmjeravanja bez ostavljanja preusmjeravanja",
+       "logentry-move-move_redir-noredirect": "$1 {{GENDER:$2|premjestio|premjestila}} je stranicu $3 na $4 preko preusmjerenja bez ostavljanja preusmjerenja",
        "logentry-patrol-patrol": "$1 {{GENDER:$2|označio|označila}} je izmjenu $4 stranice $3 pregledanom",
        "logentry-patrol-patrol-auto": "$1 automatski je {{GENDER:$2|označio|označila}} verziju $4 stranice $3 pregledanom",
        "logentry-newusers-newusers": "Korisnički račun $1 je {{GENDER:$2|napravljen}}",
index e1af1d5..93b6dec 100644 (file)
        "right-sendemail": "кхечу декъашхошка электронан хаамаш кхехьийта",
        "right-passwordreset": "пароль хийцарца электроннан хаамашка хьажар",
        "right-managechangetags": "Хаамийн базан чохь [[Special:Tags|билгалонаш]] кхолла а, дӀаяха а",
+       "grant-generic": "Бакъонийн гулам «$1»",
+       "grant-group-page-interaction": "АгӀонашца зӀе",
+       "grant-group-file-interaction": "Медиафайлашца зӀе",
+       "grant-group-email": "ДӀадахьийта кехат",
+       "grant-group-high-volume": "Дешдерг сиха дар",
+       "grant-blockusers": "Декъашхойн блоктохар а, блокдӀаяккхар а",
+       "grant-createaccount": "Декъашхочун дӀаяздарш кхоллар",
+       "grant-createeditmovepage": "АгӀонашна хийцам бар",
+       "grant-delete": "АгӀонаш а, нисдарш а, декъашхойн дӀаяздар а дӀадахар",
+       "grant-editinterface": "MediaWiki цӀерийн ана а, долара CSS/JavaScript нисъяр",
+       "grant-editmycssjs": "Долара CSS/JavaScript нисъяр",
+       "grant-editmyoptions": "Табе хьай гӀирс",
+       "grant-editmywatchlist": "Хьан тергаме могӀам табар",
+       "grant-editpage": "Йолуш йолу агӀонаш таяр",
+       "grant-editprotected": "Ларйина агӀонаш таяр",
+       "grant-highvolume": "Сиха тадарш дар",
+       "grant-patrol": "АгӀонийн хийцамаш патруль яр",
+       "grant-protect": "АгӀонаш ларъяр а, ларъяр дӀадаккхар а",
+       "grant-rollback": "АгӀонийн хийцамаш юхабахар",
+       "grant-sendemail": "Кхечу декъашхошка электронан хаамаш кхехьийта",
+       "grant-uploadeditmovefile": "Чуяхар а, хийцар а файлийн цӀерш хийцар а",
+       "grant-uploadfile": "Чуяха керла файлаш",
+       "grant-viewdeleted": "ДӀабаьхьна хаамашка хьажар",
+       "grant-viewmywatchlist": "Шен тергаме могӀаме хьажар",
        "newuserlogpage": "Декъашхой дӀабазбина тептар",
        "newuserlogpagetext": "Дукху хан йоцуш дӀабазбелла декъашхойн могӀам",
        "rightslog": "Декъашхочун бакъона тéптар",
        "listgrouprights-namespaceprotection-header": "ЦӀеран анан бехкам",
        "listgrouprights-namespaceprotection-namespace": "ЦӀерийн ана",
        "listgrouprights-namespaceprotection-restrictedto": "Декъашхочун хийцамаш бан таро хуьлуьйту бакъонаш",
+       "listgrants-grant": "Бакъо",
+       "listgrants-rights": "Бакъонаш",
        "trackingcategories": "Хьожуш йолу категореш",
        "trackingcategories-summary": "ХӀокху агӀонгахь ю хьожуш йолу категореш, MediaWikiс тӀеюзаш ю уьш. {{ns:8}} цӀерийн меттигера системин хаам хийцина цера цӀерш хийца йиш ю.",
        "trackingcategories-msg": "Категореш зер",
        "watchlistall2": "массо",
        "watchlist-hide": "Къайлаяккха",
        "watchlist-submit": "Гайта",
+       "wlshowtime": "Гаран хенан мур:",
        "wlshowhideminor": "жима нисдарш",
        "wlshowhidebots": "Боташ",
        "wlshowhideliu": "ДӀабазбелла декъашхой",
        "hours-abbrev": "$1 сахь.",
        "seconds": "{{PLURAL:$1|$1 секунд|$1 секунд}}",
        "minutes": "{{PLURAL:$1|$1 минот|$1 минот}}",
-       "hours": "{{PLURAL:$1|$1 сахьт|$1 сахьт}}",
-       "days": "{{PLURAL:$1|$1 де|$1 де}}",
+       "hours": "{{PLURAL:$1|$1 сахьт}}",
+       "days": "{{PLURAL:$1|$1 де}}",
        "weeks": "{{PLURAL:$1|$1 кӀира}}",
        "months": "{{PLURAL:$1|$1 бутт|$1 бутт}}",
        "years": "{{PLURAL:$1|$1 шо|$1 шо}}",
index 8e1bd3e..58ae809 100644 (file)
        "virus-scanfailed": "prověřování selhalo (kód $1)",
        "virus-unknownscanner": "neznámý antivirus:",
        "logouttext": "<strong>Nyní jste odhlášeni.</strong>\n\nNěkteré stránky se mohou i nadále zobrazovat, jako byste byli dosud přihlášeni, dokud nevymažete cache prohlížeče.",
+       "cannotlogoutnow-title": "Momentálně se nelze odhlásit",
+       "cannotlogoutnow-text": "Odhlášení není možné, když se používají $1.",
        "welcomeuser": "Vítejte, uživateli $1!",
        "welcomecreation-msg": "Váš účet byl vytvořen.\nNezapomeňte si upravit své [[Special:Preferences|nastavení {{grammar:2sg|{{SITENAME}}}}]].",
        "yourname": "Uživatelské jméno:",
        "remembermypassword": "Zapamatovat si mé přihlášení na tomto počítači (maximálně $1 {{PLURAL:$1|den|dny|dní}})",
        "userlogin-remembermypassword": "Přihlásit trvale",
        "userlogin-signwithsecure": "Používat zabezpečené připojení",
+       "cannotloginnow-title": "Momentálně se nelze přihlásit",
+       "cannotloginnow-text": "Přihlášení není možné, když se používají $1.",
        "yourdomainname": "Vaše doména",
        "password-change-forbidden": "Na této wiki nemůžete měnit hesla.",
        "externaldberror": "Buď nastala chyba externí autentizační databáze, nebo nemáte dovoleno měnit svůj externí účet.",
        "right-managechangetags": "Vytváření [[Special:Tags|značek]] a jejich mazání z databáze",
        "right-applychangetags": "Přidávání [[Special:Tags|značek]] k vlastním změnám",
        "right-changetags": "Přidávání libovolných [[Special:Tags|značek]] na jednotlivé revize a protokolovací záznamy a jejich odebírání",
+       "grant-generic": "Balíček oprávnění „$1“",
+       "grant-group-page-interaction": "Interakce se stránkami",
+       "grant-group-file-interaction": "Interakce se soubory",
+       "grant-group-watchlist-interaction": "Interakce s vaším seznamem sledovaných stránek",
+       "grant-group-email": "Rozesílání e-mailů",
+       "grant-group-high-volume": "Velkoobjemové činnosti",
+       "grant-group-customization": "Nastavení a přizpůsobení",
+       "grant-group-administration": "Provádění správcovských činností",
+       "grant-group-other": "Různé činnosti",
+       "grant-blockusers": "Blokovat a odblokovávat uživatele",
+       "grant-createaccount": "Zakládat účty",
+       "grant-createeditmovepage": "Vytvářet, editovat a přesouvat stránky",
+       "grant-delete": "Mazat stránky, revize a protokolovací záznamy",
+       "grant-editinterface": "Editovat jmenný prostor MediaWiki a uživatelské CSS/JavaScript",
+       "grant-editmycssjs": "Editovat váš uživatelský CSS/JavaScript",
+       "grant-editmyoptions": "Změna vašich uživatelských nastavení",
+       "grant-editmywatchlist": "Upravovat váš seznam sledovaných stránek",
+       "grant-editpage": "Editovat existující stránky",
+       "grant-editprotected": "Editovat zamčené stránky",
+       "grant-highvolume": "Hromadné editace",
+       "grant-oversight": "Skrývat uživatele a utajovat revize",
+       "grant-patrol": "Patrolovat změny stránek",
+       "grant-protect": "Zamykat a odemykat stránky",
+       "grant-rollback": "Vracet editace zpět",
+       "grant-sendemail": "Posílat e-maily ostatním uživatelům",
+       "grant-uploadeditmovefile": "Načítat, nahrazovat a přesouvat soubory",
+       "grant-uploadfile": "Načítat nové soubory",
+       "grant-basic": "Základní oprávnění",
+       "grant-viewdeleted": "Prohlížet si smazané soubory a stránky",
+       "grant-viewmywatchlist": "Prohlížet si váš seznam sledovaných stránek",
        "newuserlogpage": "Kniha nových uživatelů",
        "newuserlogpagetext": "Toto je záznam nově zaregistrovaných uživatelů.",
        "rightslog": "Kniha práv uživatelů",
        "action-createpage": "vytvářet stránky",
        "action-createtalk": "vytvářet diskusní stránky",
        "action-createaccount": "vytvořit tento uživatelský účet",
+       "action-autocreateaccount": "automaticky založit tento externí uživatelský účet",
        "action-history": "prohlížet si historii této stránky",
        "action-minoredit": "označit tuto editaci jako malou",
        "action-move": "přesunout tuto stránku",
        "listgrouprights-namespaceprotection-header": "Omezení jmenných prostorů",
        "listgrouprights-namespaceprotection-namespace": "Jmenný prostor",
        "listgrouprights-namespaceprotection-restrictedto": "Oprávnění umožňující uživateli editovat",
+       "listgrants-summary": "Následující seznam obsahuje oprávnění s odpovídajícím přístupem k uživatelským právům. Uživatelé mohou aplikace autorizovat k využití jejich účtu, ale s omezenými právy na základě oprávnění, která uživatel aplikaci přidělil. Aplikace konající jménem uživatele ale nemůže využít oprávnění, která uživatel nemá.\nK jednotlivým oprávněním mohou existovat [[{{MediaWiki:Listgrouprights-helppage}}|doplňující informace]].",
+       "listgrants-rights": "Oprávnění",
        "trackingcategories": "Sledovací kategorie",
        "trackingcategories-summary": "Tato stránka obsahuje seznam sledovacích kategorií, které automaticky přidává software MediaWiki. Jejich jména lze změnit úpravou příslušných systémových hlášení ve jmenném prostoru {{ns:8}}.",
        "trackingcategories-msg": "Sledovací kategorie",
        "revertpage": "Editace uživatele „[[Special:Contributions/$2|$2]]“ ([[User talk:$2|diskuse]]) vráceny do předchozího stavu, jehož autorem je „[[User:$1|$1]]“",
        "revertpage-nouser": "Editace skrytého uživatele vráceny do předchozího stavu, jehož {{GENDER:$1|autorem|autorkou}} je „[[User:$1|$1]]“",
        "rollback-success": "Editace uživatele $1 byly vráceny na poslední verzi od uživatele $2.",
-       "sessionfailure-title": "Chyba sezení",
+       "sessionfailure-title": "Chyba relace",
        "sessionfailure": "Zřejmě je nějaký problém s vaším přihlášením;\nvámi požadovaná činnost byla stornována jako prevence před neoprávněným přístupem.\nStiskněte tlačítko „zpět“, obnovte stránku, ze které jste přišli, a zkuste činnost znovu.",
        "changecontentmodel": "Změna modelu obsahu stránky",
        "changecontentmodel-legend": "Změnit model obsahu",
        "javascripttest-pagetext-frameworks": "Zvolte jednu z následujících testovacích knihoven: $1",
        "javascripttest-pagetext-skins": "Zvolte vzhled, pod kterým se mají testy spustit:",
        "javascripttest-qunit-intro": "Vizte [$1 dokumentaci testování] na mediawiki.org",
-       "tooltip-pt-userpage": "Vaše uživatelská stránka",
+       "tooltip-pt-userpage": "{{GENDER:|Vaše uživatelská}} stránka",
        "tooltip-pt-anonuserpage": "Uživatelská stránka pro IP adresu, ze které editujete",
-       "tooltip-pt-mytalk": "Vaše diskusní stránka",
+       "tooltip-pt-mytalk": "{{GENDER:|Vaše}} diskusní stránka",
        "tooltip-pt-anontalk": "Diskuse o editacích provedených z této IP adresy",
-       "tooltip-pt-preferences": "Vaše nastavení",
+       "tooltip-pt-preferences": "{{GENDER:|Vaše}} nastavení",
        "tooltip-pt-watchlist": "Seznam stránek, jejichž změny sledujete",
-       "tooltip-pt-mycontris": "Seznam vašich příspěvků",
+       "tooltip-pt-mycontris": "Seznam {{GENDER:|vašich}} příspěvků",
        "tooltip-pt-anoncontribs": "Seznam editací provedených z této IP adresy",
        "tooltip-pt-login": "Doporučujeme vám přihlásit se, ovšem není to povinné.",
        "tooltip-pt-logout": "Odhlásit se",
        "tooltip-t-recentchangeslinked": "Nedávné změny stránek, na které je odkazováno",
        "tooltip-feed-rss": "RSS kanál pro tuto stránku",
        "tooltip-feed-atom": "Atom kanál pro tuto stránku",
-       "tooltip-t-contributions": "Prohlédnout si seznam příspěvků tohoto uživatele",
-       "tooltip-t-emailuser": "Poslat e-mail tomuto uživateli",
+       "tooltip-t-contributions": "Seznam příspěvků {{GENDER:$1|tohoto uživatele|této uživatelky}}",
+       "tooltip-t-emailuser": "Poslat e-mail {{GENDER:$1|tomuto uživateli|této uživatelce}}",
        "tooltip-t-info": "Více informací o této stránce",
        "tooltip-t-upload": "Nahrát obrázky či jiná multimédia",
        "tooltip-t-specialpages": "Seznam všech speciálních stránek",
        "expand_templates_generate_xml": "Zobrazit syntaktický strom v XML",
        "expand_templates_generate_rawhtml": "Zobrazit surové HTML",
        "expand_templates_preview": "Náhled",
-       "expand_templates_preview_fail_html": "<em>Protože {{SITENAME}} má povolené syrové HTML a došlo ke ztrátě dat sezení, je náhled skryt kvůli ochraně před JavaScriptovými útoky.</em>\n\n<strong>Pokud to byl legitimní pokus o náhled, zkuste to znovu.</strong>\nPokud to stále nebude fungovat, zkuste se [[Special:UserLogout|odhlásit]] a znovu přihlásit.",
+       "expand_templates_preview_fail_html": "<em>Protože {{SITENAME}} má povolené syrové HTML a došlo ke ztrátě dat relace, je náhled skryt kvůli ochraně před JavaScriptovými útoky.</em>\n\n<strong>Pokud to byl legitimní pokus o náhled, zkuste to znovu.</strong>\nPokud to stále nebude fungovat, zkuste se [[Special:UserLogout|odhlásit]] a znovu přihlásit.",
        "expand_templates_preview_fail_html_anon": "<em>Protože {{SITENAME}} má povolené syrové HTML a vy nejste přihlášeni, je náhled skryt kvůli ochraně před JavaScriptovými útoky.</em>\n\n<strong>Pokud to byl legitimní pokus o náhled, [[Special:UserLogin|přihlaste se]] a zkuste to znovu.</strong>",
        "expand_templates_input_missing": "Musíte zadat alespoň nějaký vstupní text.",
        "pagelanguage": "Volba jazyka stránky",
        "mw-widgets-dateinput-placeholder-month": "RRRR-MM",
        "mw-widgets-titleinput-description-new-page": "stránka zatím neexistuje",
        "mw-widgets-titleinput-description-redirect": "přesměrování na $1",
-       "api-error-blacklisted": "Zvolte prosím jiný, popisný název."
+       "api-error-blacklisted": "Zvolte prosím jiný, popisný název.",
+       "sessionmanager-tie": "Nelze kombinovat několik typů autentizace požadavků: $1.",
+       "sessionprovider-generic": "relace pomocí $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "relace pomocí cookies",
+       "sessionprovider-nocookies": "Možná jsou zakázány cookies. Ujistěte se, že máte cookies povoleny a zkuste to znovu."
 }
index 4c84d06..7aa1a05 100644 (file)
        "virus-scanfailed": "Scan fehlgeschlagen (Code $1)",
        "virus-unknownscanner": "Unbekannter Virenscanner:",
        "logouttext": "<strong>Du bist nun abgemeldet.</strong>\n\nBeachte, dass einige Seiten noch anzeigen können, dass du angemeldet bist, solange du nicht deinen Browsercache geleert hast.",
+       "cannotlogoutnow-title": "Abmeldung nicht erfolgreich",
+       "cannotlogoutnow-text": "Eine Abmeldung ist mit Verwendung von $1 nicht möglich.",
        "welcomeuser": "Willkommen, $1!",
        "welcomecreation-msg": "Dein Benutzerkonto wurde erstellt.\nVergiss nicht, deine [[Special:Preferences|{{SITENAME}}-Einstellungen]] zu ändern.",
        "yourname": "Benutzername:",
        "remembermypassword": "Mit diesem Browser dauerhaft angemeldet bleiben (maximal $1 {{PLURAL:$1|Tag|Tage}})",
        "userlogin-remembermypassword": "Angemeldet bleiben",
        "userlogin-signwithsecure": "Sichere Verbindung verwenden",
+       "cannotloginnow-title": "Anmeldung nicht erfolgreich",
+       "cannotloginnow-text": "Eine Anmeldung ist mit Verwendung von $1 nicht möglich.",
        "yourdomainname": "Deine Domain:",
        "password-change-forbidden": "Du kannst auf diesem Wiki keine Passwörter ändern.",
        "externaldberror": "Entweder liegt ein Fehler bei der externen Authentifizierung vor oder du darfst dein externes Benutzerkonto nicht aktualisieren.",
        "resetpass_submit": "Passwort übermitteln und anmelden",
        "changepassword-success": "Dein Passwort wurde erfolgreich geändert!",
        "changepassword-throttled": "Du hast kürzlich zu viele Anmeldeversuche unternommen.\nBitte warte $1, bevor du es erneut versuchst.",
+       "botpasswords": "Botpasswörter",
+       "botpasswords-summary": "<em>Botpasswörter</em> erlauben Zugriff auf ein Benutzerkonto über die API, ohne die Hauptanmeldeinformationen des Benutzerkontos zu verwenden. Die verfügbaren Benutzerrechte bei der Anmeldung mit einem Botpasswort können beschränkt sein.\n\nWenn du nicht weist, warum du dies tun möchtest, solltest du dies wahrscheinlich nicht tun. Niemand soll dich jemals bitten, ein Passwort zu erzeugen und es an ihn zu übergeben.",
+       "botpasswords-disabled": "Botpasswörter sind deaktiviert.",
+       "botpasswords-no-central-id": "Um Botpasswörter zu verwenden, musst du bei einem zentralisierten Benutzerkonto angemeldet sein.",
+       "botpasswords-existing": "Vorhandene Botpasswörter",
+       "botpasswords-createnew": "Ein neues Botpasswort erstellen",
+       "botpasswords-editexisting": "Ein vorhandenes Botpasswort bearbeiten",
+       "botpasswords-label-appid": "Name des Bots:",
+       "botpasswords-label-create": "Erstellen",
+       "botpasswords-label-update": "Aktualisieren",
+       "botpasswords-label-cancel": "Abbrechen",
+       "botpasswords-label-delete": "Löschen",
+       "botpasswords-label-resetpassword": "Passwort zurücksetzen",
+       "botpasswords-label-grants": "Anwendbare Berechtigungen:",
+       "botpasswords-help-grants": "Jede Berechtigung gibt Zugriff auf gelistete Benutzerrechte, die ein Benutzerkonto bereits hat. Siehe die [[Special:ListGrants|Tabelle]] für weitere Informationen.",
+       "botpasswords-label-restrictions": "Verwendungsbeschränkungen:",
+       "botpasswords-label-grants-column": "Gewährt",
+       "botpasswords-bad-appid": "Der Botname „$1“ ist nicht gültig.",
+       "botpasswords-insert-failed": "Der Botname „$1“ konnte nicht hinzugefügt werden. Wurde er bereits hinzugefügt?",
+       "botpasswords-update-failed": "Der Botname „$1“ konnte nicht aktualisiert werden. Wurde er gelöscht?",
+       "botpasswords-created-title": "Botpasswort erstellt",
+       "botpasswords-created-body": "Das Botpasswort „$1“ wurde erfolgreich erstellt.",
+       "botpasswords-updated-title": "Botpasswort aktualisiert",
+       "botpasswords-updated-body": "Das Botpasswort „$1“ wurde erfolgreich aktualisiert.",
+       "botpasswords-deleted-title": "Botpasswort gelöscht",
+       "botpasswords-deleted-body": "Das Botpasswort „$1“ wurde gelöscht.",
+       "botpasswords-newpassword": "Das neue Passwort zur Anmeldung mit <strong>$1</strong> ist <strong>$2</strong>. <em>Bitte halte dies für die Zukunft fest.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider ist nicht verfügbar.",
+       "botpasswords-restriction-failed": "Beschränkungen des Botpassworts verhindern diese Anmeldung.",
+       "botpasswords-invalid-name": "Der angegebene Benutzername enthält keinen Botpassworttrenner („$1“).",
+       "botpasswords-not-exist": "Der Benutzer „$1“ hat kein Botpasswort mit dem Namen „$2“.",
        "resetpass_forbidden": "Das Passwort kann nicht geändert werden.",
        "resetpass-no-info": "Du musst dich anmelden, um auf diese Seite direkt zuzugreifen.",
        "resetpass-submit-loggedin": "Passwort ändern",
        "right-createpage": "Seiten erstellen (die keine Diskussionsseiten sind)",
        "right-createtalk": "Diskussionsseiten erstellen",
        "right-createaccount": "Benutzerkonto erstellen",
+       "right-autocreateaccount": "Automatische Anmeldung mit einem externen Benutzerkonto",
        "right-minoredit": "Bearbeitungen als klein markieren",
        "right-move": "Seiten verschieben",
        "right-move-subpages": "Seiten inklusive Unterseiten verschieben",
        "right-managechangetags": "[[Special:Tags|Markierungen]] erstellen und aus der Datenbank löschen",
        "right-applychangetags": "[[Special:Tags|Markierungen]] zusammen mit den Änderungen anwenden",
        "right-changetags": "Beliebige [[Special:Tags|Markierungen]] zu einzelnen Versionen und Logbucheinträgen hinzufügen und entfernen",
+       "grant-generic": "Rechtegruppe „$1“",
+       "grant-group-page-interaction": "Mit Seiten interagieren",
+       "grant-group-file-interaction": "Mit Medien interagieren",
+       "grant-group-watchlist-interaction": "Mit deiner Beobachtungsliste interagieren",
+       "grant-group-email": "E-Mail versenden",
+       "grant-group-high-volume": "Massenaktivitäten ausführen",
+       "grant-group-customization": "Anpassung und Einstellungen",
+       "grant-group-administration": "Administrative Aktionen ausführen",
+       "grant-group-other": "Verschiedene Aktivitäten",
+       "grant-blockusers": "Benutzer sperren und freigeben",
+       "grant-createaccount": "Benutzerkonten erstellen",
+       "grant-createeditmovepage": "Seiten erstellen, bearbeiten und verschieben",
+       "grant-delete": "Seiten, Versionen und Logbucheinträge löschen",
+       "grant-editinterface": "MediaWiki-Namensraum und Benutzer-CSS/JavaScript bearbeiten",
+       "grant-editmycssjs": "Dein Benutzer-CSS/JavaScript bearbeiten",
+       "grant-editmyoptions": "Deine Benutzereinstellungen bearbeiten",
+       "grant-editmywatchlist": "Deine Beobachtungsliste bearbeiten",
+       "grant-editpage": "Vorhandene Seiten bearbeiten",
+       "grant-editprotected": "Geschützte Seiten bearbeiten",
+       "grant-highvolume": "Massenbearbeitungen",
+       "grant-oversight": "Benutzer verstecken und Versionen unterdrücken",
+       "grant-patrol": "Änderungen an Seiten kontrollieren",
+       "grant-protect": "Seiten schützen und freigeben",
+       "grant-rollback": "Änderungen an Seiten zurücksetzen",
+       "grant-sendemail": "E-Mails an andere Benutzer versenden",
+       "grant-uploadeditmovefile": "Dateien hochladen, ersetzen und verschieben",
+       "grant-uploadfile": "Neue Dateien hochladen",
+       "grant-basic": "Basisrechte",
+       "grant-viewdeleted": "Gelöschte Dateien und Seiten ansehen",
+       "grant-viewmywatchlist": "Deine Beobachtungsliste ansehen",
        "newuserlogpage": "Neuanmeldungs-Logbuch",
        "newuserlogpagetext": "Dies ist ein Logbuch der neu erstellten Benutzerkonten.",
        "rightslog": "Rechte-Logbuch",
        "action-createpage": "Seiten zu erstellen",
        "action-createtalk": "Diskussionsseiten zu erstellen",
        "action-createaccount": "ein Benutzerkonto zu erstellen",
+       "action-autocreateaccount": "automatisch dieses externe Benutzerkonto zu erstellen",
        "action-history": "die Versionsgeschichte dieser Seite anzusehen",
        "action-minoredit": "diese Bearbeitung als klein zu markieren",
        "action-move": "die Seite zu verschieben",
        "listgrouprights-namespaceprotection-header": "Namensraumbeschränkungen",
        "listgrouprights-namespaceprotection-namespace": "Namensraum",
        "listgrouprights-namespaceprotection-restrictedto": "Rechte, die dem Benutzer die Bearbeitung erlauben",
+       "listgrants": "Berechtigungen",
+       "listgrants-summary": "Es folgt eine Liste mit Berechtigungen mit ihrem verknüpften Zugriff auf Benutzerrechte. Benutzer können Anwendungen autorisieren, um ihr Benutzerkonto zu verwenden, aber mit beschränkten Berechtigungen basierend auf den Rechten, die der Benutzer der Anwendung gegeben hat. Eine Anwendung agiert im Namen eines Benutzers, die keine Rechte verwenden kann, die der Benutzer nicht hat.\nEs gibt [[{{MediaWiki:Listgrouprights-helppage}}|zusätzliche Informationen]] über einzelne Rechte.",
+       "listgrants-grant": "Berechtigung",
+       "listgrants-rights": "Rechte",
        "trackingcategories": "Tracking-Kategorien",
        "trackingcategories-summary": "Diese Seite listet Tracking-Kategorien auf, die von der MediaWiki-Software automatisch gefüllt werden. Ihre Namen können durch Änderung der entsprechenden Systemnachrichten im {{ns:8}}-Namensraum angepasst werden.",
        "trackingcategories-msg": "Tracking-Kategorie",
        "block-log-flags-hiddenname": "Benutzername versteckt",
        "range_block_disabled": "Die Möglichkeit, ganze Adressräume zu sperren, ist nicht aktiviert.",
        "ipb_expiry_invalid": "Die eingegebene Dauer ist ungültig.",
+       "ipb_expiry_old": "Der Zeitpunkt des Ablaufs liegt in der Vergangenheit.",
        "ipb_expiry_temp": "Benutzernamens-Sperren mit der Verstecken-Option müssen permanent sein.",
        "ipb_hide_invalid": "Dieses Konto kann nicht unterdrückt werden, da es mehr als {{PLURAL:$1|eine Bearbeitung|$1 Bearbeitungen}} aufweist.",
        "ipb_already_blocked": "„$1“ ist bereits gesperrt",
        "javascripttest-pagetext-frameworks": "Bitte wähle eine der folgenden Prüfumgebungen aus: $1",
        "javascripttest-pagetext-skins": "Wähle eine Benutzeroberfläche zur Durchführung der Tests aus:",
        "javascripttest-qunit-intro": "Siehe die [$1 Dokumentation zu Tests] auf mediawiki.org",
-       "tooltip-pt-userpage": "Deine Benutzerseite",
+       "tooltip-pt-userpage": "{{GENDER:|Deine}} Benutzerseite",
        "tooltip-pt-anonuserpage": "Benutzerseite der IP-Adresse von der aus du Änderungen durchführst",
-       "tooltip-pt-mytalk": "Deine Diskussionsseite",
+       "tooltip-pt-mytalk": "{{GENDER:|Deine}} Diskussionsseite",
        "tooltip-pt-anontalk": "Diskussion über Änderungen von dieser IP-Adresse",
-       "tooltip-pt-preferences": "Deine Einstellungen",
+       "tooltip-pt-preferences": "{{GENDER:|Deine}} Einstellungen",
        "tooltip-pt-watchlist": "Liste der von dir beobachteten Seiten",
-       "tooltip-pt-mycontris": "Liste eigener Beiträge",
+       "tooltip-pt-mycontris": "Liste {{GENDER:|eigener}} Beiträge",
        "tooltip-pt-anoncontribs": "Eine Liste der Bearbeitungen, die von dieser IP-Adresse gemacht wurden",
        "tooltip-pt-login": "Sich anzumelden wird gerne gesehen, ist jedoch nicht zwingend erforderlich.",
        "tooltip-pt-logout": "Abmelden",
        "tooltip-t-recentchangeslinked": "Letzte Änderungen an Seiten, die von hier verlinkt sind",
        "tooltip-feed-rss": "RSS-Feed dieser Seite",
        "tooltip-feed-atom": "Atom-Feed dieser Seite",
-       "tooltip-t-contributions": "Liste der Beiträge dieses Benutzers ansehen",
-       "tooltip-t-emailuser": "Eine E-Mail an diesen Benutzer senden",
+       "tooltip-t-contributions": "Liste der Beiträge {{GENDER:$1|dieses Benutzers|dieser Benutzerin}} ansehen",
+       "tooltip-t-emailuser": "Eine E-Mail an {{GENDER:$1|diesen Benutzer|diese Benutzerin}} senden",
        "tooltip-t-info": "Weitere Informationen über diese Seite",
        "tooltip-t-upload": "Dateien hochladen",
        "tooltip-t-specialpages": "Liste aller Spezialseiten",
        "newimages-summary": "Diese Spezialseite zeigt die zuletzt hochgeladenen Dateien an.",
        "newimages-legend": "Filter",
        "newimages-label": "Dateiname (oder ein Teil davon):",
-       "newimages-showbots": "Uploads von Bots anzeigen",
-       "newimages-hidepatrolled": "Kontrollierte Uploads ausblenden",
+       "newimages-showbots": "Von Bots hochgeladene Dateien anzeigen",
+       "newimages-hidepatrolled": "Kontrollierte Dateien ausblenden",
        "noimages": "Keine Dateien gefunden.",
        "ilsubmit": "Suchen",
        "bydate": "nach Datum",
        "mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
        "mw-widgets-titleinput-description-new-page": "Seite ist noch nicht vorhanden",
        "mw-widgets-titleinput-description-redirect": "Weiterleitung nach $1",
-       "api-error-blacklisted": "Bitte einen anderen, aussagekräftigen Titel wählen."
+       "api-error-blacklisted": "Bitte einen anderen, aussagekräftigen Titel wählen.",
+       "sessionmanager-tie": "Mehrere Anfrageauthentifikationstypen konnten nicht kombiniert werden: $1.",
+       "sessionprovider-generic": "$1-Sitzungen",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "cookiebasierten Sitzungen",
+       "sessionprovider-nocookies": "Cookies sind eventuell deaktiviert. Stelle sicher, dass Cookies aktiviert sind und versuche es erneut."
 }
index 88c5d07..602c290 100644 (file)
        "virus-scanfailed": "scan failed (code $1)",
        "virus-unknownscanner": "unknown antivirus:",
        "logouttext": "<strong>You are now logged out.</strong>\n\nNote that some pages may continue to be displayed as if you were still logged in, until you clear your browser cache.",
+       "cannotlogoutnow-title": "Cannot log out now",
+       "cannotlogoutnow-text": "Logging out is not possible when using $1.",
        "welcomeuser": "Welcome, $1!",
        "welcomecreation-msg": "Your account has been created.\nYou can change your {{SITENAME}} [[Special:Preferences|preferences]] if you wish.",
        "yourname": "Username:",
        "remembermypassword": "Remember my login on this browser (for a maximum of $1 {{PLURAL:$1|day|days}})",
        "userlogin-remembermypassword": "Keep me logged in",
        "userlogin-signwithsecure": "Use secure connection",
+       "cannotloginnow-title": "Cannot log in now",
+       "cannotloginnow-text": "Logging in is not possible when using $1.",
        "yourdomainname": "Your domain:",
        "password-change-forbidden": "You cannot change passwords on this wiki.",
        "externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
        "resetpass_submit": "Set password and log in",
        "changepassword-success": "Your password has been changed successfully!",
        "changepassword-throttled": "You have made too many recent login attempts.\nPlease wait $1 before trying again.",
+       "botpasswords": "Bot passwords",
+       "botpasswords-summary": "<em>Bot passwords</em> allow access to a user account via the API without using the account's main login credentials. The user rights available when logged in with a bot password may be restricted.\n\nIf you don't know why you might want to do this, you should probably not do it. No one should ever ask you to generate one of these and give it to them.",
+       "botpasswords-disabled": "Bot passwords are disabled.",
+       "botpasswords-no-central-id": "To use bot passwords, you must be logged in to a centralized account.",
+       "botpasswords-existing": "Existing bot passwords",
+       "botpasswords-createnew": "Create a new bot password",
+       "botpasswords-editexisting": "Edit an existing bot password",
+       "botpasswords-label-appid": "Bot name:",
+       "botpasswords-label-create": "Create",
+       "botpasswords-label-update": "Update",
+       "botpasswords-label-cancel": "Cancel",
+       "botpasswords-label-delete": "Delete",
+       "botpasswords-label-resetpassword": "Reset the password",
+       "botpasswords-label-grants": "Applicable grants:",
+       "botpasswords-help-grants": "Each grant gives access to listed user rights that a user account already has. See the [[Special:ListGrants|table of grants]] for more information.",
+       "botpasswords-label-restrictions": "Usage restrictions:",
+       "botpasswords-label-grants-column": "Granted",
+       "botpasswords-bad-appid": "The bot name \"$1\" is not valid.",
+       "botpasswords-insert-failed": "Failed to add bot name \"$1\". Was it already added?",
+       "botpasswords-update-failed": "Failed to update bot name \"$1\". Was it deleted?",
+       "botpasswords-created-title": "Bot password created",
+       "botpasswords-created-body": "The bot password \"$1\" was created successfully.",
+       "botpasswords-updated-title": "Bot password updated",
+       "botpasswords-updated-body": "The bot password \"$1\" was updated successfully.",
+       "botpasswords-deleted-title": "Bot password deleted",
+       "botpasswords-deleted-body": "The bot password \"$1\" was deleted.",
+       "botpasswords-newpassword": "The new password to log in with <strong>$1</strong> is <strong>$2</strong>. <em>Please record this for future reference.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider is not available.",
+       "botpasswords-restriction-failed": "Bot password restrictions prevent this login.",
+       "botpasswords-invalid-name": "The username specified does not contain the bot password separator (\"$1\").",
+       "botpasswords-not-exist": "User \"$1\" does not have a bot password named \"$2\".",
        "resetpass_forbidden": "Passwords cannot be changed",
        "resetpass-no-info": "You must be logged in to access this page directly.",
        "resetpass-submit-loggedin": "Change password",
        "right-createpage": "Create pages (which are not discussion pages)",
        "right-createtalk": "Create discussion pages",
        "right-createaccount": "Create new user accounts",
+       "right-autocreateaccount": "Automatically log in with an external user account",
        "right-minoredit": "Mark edits as minor",
        "right-move": "Move pages",
        "right-move-subpages": "Move pages with their subpages",
        "right-managechangetags": "Create and delete [[Special:Tags|tags]] from the database",
        "right-applychangetags": "Apply [[Special:Tags|tags]] along with one's changes",
        "right-changetags": "Add and remove arbitrary [[Special:Tags|tags]] on individual revisions and log entries",
+       "grant-generic": "\"$1\" rights bundle",
+       "grant-group-page-interaction": "Interact with pages",
+       "grant-group-file-interaction": "Interact with media",
+       "grant-group-watchlist-interaction": "Interact with your watchlist",
+       "grant-group-email": "Send email",
+       "grant-group-high-volume": "Perform high volume activity",
+       "grant-group-customization": "Customization and preferences",
+       "grant-group-administration": "Perform administrative actions",
+       "grant-group-other": "Miscellaneous activity",
+       "grant-blockusers": "Block and unblock users",
+       "grant-createaccount": "Create accounts",
+       "grant-createeditmovepage": "Create, edit, and move pages",
+       "grant-delete": "Delete pages, revisions, and log entries",
+       "grant-editinterface": "Edit the MediaWiki namespace and user CSS/JavaScript",
+       "grant-editmycssjs": "Edit your user CSS/JavaScript",
+       "grant-editmyoptions": "Edit your user preferences",
+       "grant-editmywatchlist": "Edit your watchlist",
+       "grant-editpage": "Edit existing pages",
+       "grant-editprotected": "Edit protected pages",
+       "grant-highvolume": "High-volume editing",
+       "grant-oversight": "Hide users and suppress revisions",
+       "grant-patrol": "Patrol changes to pages",
+       "grant-protect": "Protect and unprotect pages",
+       "grant-rollback": "Rollback changes to pages",
+       "grant-sendemail": "Send email to other users",
+       "grant-uploadeditmovefile": "Upload, replace, and move files",
+       "grant-uploadfile": "Upload new files",
+       "grant-basic": "Basic rights",
+       "grant-viewdeleted": "View deleted files and pages",
+       "grant-viewmywatchlist": "View your watchlist",
        "newuserlogpage": "User creation log",
        "newuserlogpagetext": "This is a log of user creations.",
        "rightslog": "User rights log",
        "action-createpage": "create pages",
        "action-createtalk": "create discussion pages",
        "action-createaccount": "create this user account",
+       "action-autocreateaccount": "automatically create this external user account",
        "action-history": "view the history of this page",
        "action-minoredit": "mark this edit as minor",
        "action-move": "move this page",
        "listgrouprights-namespaceprotection-header": "Namespace restrictions",
        "listgrouprights-namespaceprotection-namespace": "Namespace",
        "listgrouprights-namespaceprotection-restrictedto": "Right(s) allowing user to edit",
+       "listgrants": "Grants",
+       "listgrants-summary": "The following is a list of grants with their associated access to user rights. Users can authorize applications to use their account, but with limited permissions based on the grants the user gave to the application. An application acting on behalf of a user cannot actually use rights that the user does not have however.\nThere may be [[{{MediaWiki:Listgrouprights-helppage}}|additional information]] about individual rights.",
+       "listgrants-grant": "Grant",
+       "listgrants-rights": "Rights",
        "trackingcategories": "Tracking categories",
        "trackingcategories-summary": "This page lists tracking categories which are automatically populated by the MediaWiki software. Their names can be changed by altering the relevant system messages in the {{ns:8}} namespace.",
        "trackingcategories-msg": "Tracking category",
        "block-log-flags-hiddenname": "username hidden",
        "range_block_disabled": "The administrator ability to create range blocks is disabled.",
        "ipb_expiry_invalid": "Expiry time invalid.",
+       "ipb_expiry_old": "Expiry time is in the past.",
        "ipb_expiry_temp": "Hidden username blocks must be permanent.",
        "ipb_hide_invalid": "Unable to suppress this account; it has more than {{PLURAL:$1|one edit|$1 edits}}.",
        "ipb_already_blocked": "\"$1\" is already blocked.",
        "mw-widgets-dateinput-placeholder-month": "YYYY-MM",
        "mw-widgets-titleinput-description-new-page": "page does not exist yet",
        "mw-widgets-titleinput-description-redirect": "redirect to $1",
-       "api-error-blacklisted": "Please choose a different, descriptive title."
+       "api-error-blacklisted": "Please choose a different, descriptive title.",
+       "sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
+       "sessionprovider-generic": "$1 sessions",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions",
+       "sessionprovider-nocookies": "Cookies may be disabled. Ensure you have cookies enabled and start again."
 }
index c7de3b0..6344dab 100644 (file)
        "virus-scanfailed": "ha fallado el análisis (código $1)",
        "virus-unknownscanner": "antivirus desconocido:",
        "logouttext": "<strong>Tu sesión ha finalizado.</strong>\n\nPuede que algunas páginas continúen mostrándose como si la sesión estuviera iniciada hasta que actualices la caché de tu navegador.",
+       "cannotlogoutnow-title": "No se puede cerrar sesión ahora",
+       "cannotlogoutnow-text": "No se puede cerrar sesión cuando se usa $1.",
        "welcomeuser": "¡Bienvenido/a, $1!",
        "welcomecreation-msg": "Se ha creado tu cuenta.\nSi lo deseas, puedes cambiar tus [[Special:Preferences|preferencias]] para {{SITENAME}}.",
        "yourname": "Usuario:",
        "remembermypassword": "Mantenerme conectado en este navegador (hasta $1 {{PLURAL:$1|día|días}})",
        "userlogin-remembermypassword": "Mantener mi sesión iniciada",
        "userlogin-signwithsecure": "Usar conexión segura",
+       "cannotloginnow-title": "No se puede iniciar sesión ahora",
        "yourdomainname": "Tu dominio:",
        "password-change-forbidden": "No puedes cambiar las contraseñas en este wiki.",
        "externaldberror": "Hubo un error de autenticación en la base de datos, o bien no tienes autorización para actualizar tu cuenta externa.",
        "resetpass_submit": "Establecer contraseña e iniciar sesión",
        "changepassword-success": "La contraseña se modificó correctamente.",
        "changepassword-throttled": "Has intentado acceder demasiadas veces recientemente.\nEspera $1 antes de intentarlo de nuevo.",
+       "botpasswords": "Contraseñas de bots",
+       "botpasswords-disabled": "Las contraseñas de bot están desactivadas.",
+       "botpasswords-existing": "Contraseñas de bots existentes",
+       "botpasswords-label-appid": "Nombre del bot:",
+       "botpasswords-label-create": "Crear",
+       "botpasswords-label-update": "Actualizar",
+       "botpasswords-label-cancel": "Cancelar",
+       "botpasswords-label-delete": "Borrar",
+       "botpasswords-label-resetpassword": "Restablecer la contraseña",
+       "botpasswords-label-restrictions": "Restricciones de uso:",
+       "botpasswords-label-grants-column": "Concedido",
+       "botpasswords-bad-appid": "El nombre del bot \"$1\" no es válido.",
+       "botpasswords-created-title": "Se creó la contraseña de bot",
+       "botpasswords-created-body": "La contraseña de bot \"$1\" se creó correctamente.",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider no está disponible.",
+       "botpasswords-invalid-name": "El nombre de usuario especificado no contiene el separador de contraseña de bot (\"$1\").",
        "resetpass_forbidden": "No se pueden cambiar las contraseñas",
        "resetpass-no-info": "Debes iniciar sesión para acceder directamente a esta página.",
        "resetpass-submit-loggedin": "Cambiar contraseña",
        "right-createpage": "Crear páginas que no sean de discusión",
        "right-createtalk": "Crear páginas de discusión",
        "right-createaccount": "Crear cuentas de usuario nuevas",
+       "right-autocreateaccount": "Iniciar sesión automáticamente con una cuenta de usuario externa",
        "right-minoredit": "Marcar ediciones como menores",
        "right-move": "Trasladar páginas",
        "right-move-subpages": "Trasladar páginas con sus subpáginas",
        "right-managechangetags": "Crear y eliminar [[Special:Tags|etiquetas]] en la base de datos",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetas]] junto con los cambios propios",
        "right-changetags": "Agregar y quitar [[Special:Tags|etiquetas]] arbitrarias a revisiones individuales y entradas del registro",
+       "grant-generic": "Paquete de derechos \"$1\"",
+       "grant-group-page-interaction": "Interactuar con páginas",
+       "grant-group-file-interaction": "Interactuar con multimedia",
+       "grant-group-watchlist-interaction": "Interactuar con tu lista de seguimiento",
+       "grant-group-email": "Enviar correo electrónico",
+       "grant-group-customization": "Personalización y preferencias",
+       "grant-group-administration": "Realizar acciones administrativas",
+       "grant-group-other": "Actividades diversas",
+       "grant-blockusers": "Bloquear y desbloquear usuarios",
+       "grant-createaccount": "Crear cuentas",
+       "grant-createeditmovepage": "Crear, editar y trasladar páginas",
+       "grant-delete": "Borrar páginas, revisiones y entradas del registro",
+       "grant-editinterface": "Editar el espacio de nombres MediaWiki y el CSS/JavaScript de usuarios",
+       "grant-editmycssjs": "Editar tu CSS o JavaScript",
+       "grant-editmyoptions": "Editar tus preferencias de usuario",
+       "grant-editmywatchlist": "Editar tu lista de seguimiento",
+       "grant-editpage": "Editar páginas existentes",
+       "grant-editprotected": "Editar páginas protegidas",
+       "grant-highvolume": "Gran cantidad de ediciones",
+       "grant-oversight": "Ocultar a los usuarios y suprimir las revisiones",
+       "grant-patrol": "Verificar cambios a páginas",
+       "grant-protect": "Proteger y desproteger páginas",
+       "grant-rollback": "Revertir cambios a páginas",
+       "grant-sendemail": "Enviar un correo electrónico a otros usuarios",
+       "grant-uploadeditmovefile": "Cargar, reemplazar y renombrar archivos",
+       "grant-uploadfile": "Subir archivos nuevos",
+       "grant-basic": "Permisos básicos",
+       "grant-viewdeleted": "Ver archivos y páginas eliminados",
+       "grant-viewmywatchlist": "Ver tu lista de seguimiento",
        "newuserlogpage": "Registro de creación de usuarios",
        "newuserlogpagetext": "Este es un registro de creación de usuarios.",
        "rightslog": "Registro de permisos de usuario",
        "action-createpage": "crear páginas",
        "action-createtalk": "crear páginas de discusión",
        "action-createaccount": "crear esta cuenta de usuario",
+       "action-autocreateaccount": "crear automáticamente esta cuenta de usuario externa",
        "action-history": "ver el historial de esta página",
        "action-minoredit": "marcar este cambio como menor",
        "action-move": "trasladar esta página",
        "listgrouprights-namespaceprotection-header": "Restricciones del espacio de nombres",
        "listgrouprights-namespaceprotection-namespace": "Espacio de nombres",
        "listgrouprights-namespaceprotection-restrictedto": "Derechos de usuario para editar",
+       "listgrants-rights": "Conceder",
        "trackingcategories": "Categorías de seguimiento",
        "trackingcategories-summary": "Esta página lista categorías de seguimiento que han sido generadas automáticamente por el software MediaWiki. Sus nombres pueden cambiarse editando su mensaje correspondiente en el espacio de nombres {{ns:8}}.",
        "trackingcategories-msg": "Categoría de seguimiento",
        "tooltip-t-recentchangeslinked": "Cambios recientes en las páginas que enlazan con esta",
        "tooltip-feed-rss": "Sindicación RSS de esta página",
        "tooltip-feed-atom": "Sindicación Atom de esta página",
-       "tooltip-t-contributions": "Lista de contribuciones de este usuario",
-       "tooltip-t-emailuser": "Enviar un mensaje de correo a este usuario",
+       "tooltip-t-contributions": "Lista de contribuciones de {{GENDER:$1|este usuario|esta usuaria}}",
+       "tooltip-t-emailuser": "Enviar un mensaje de correo a {{GENDER:$1|este usuario|esta usuaria}}",
        "tooltip-t-info": "Más información sobre esta página",
        "tooltip-t-upload": "Subir archivos",
        "tooltip-t-specialpages": "Lista de todas las páginas especiales",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "la página aún no existe",
        "mw-widgets-titleinput-description-redirect": "redirigir a $1",
-       "api-error-blacklisted": "Elige un título diferente, más descriptivo."
+       "api-error-blacklisted": "Elige un título diferente, más descriptivo.",
+       "sessionprovider-generic": "sesiones $1"
 }
index d334122..e2978d1 100644 (file)
        "right-managechangetags": "Koostada [[Special:Tags|märgiseid]] ja kustutada neid andmebaasist",
        "right-applychangetags": "Rakendada [[Special:Tags|märgiseid]] enda muudatuste suhtes",
        "right-changetags": "Lisada ja eemaldada käsitsi rakendatavaid [[Special:Tags|märgiseid]] üksikute redaktsioonide ja logisissekannete juures",
+       "grant-group-page-interaction": "Interaktsioon lehekülgedega",
+       "grant-group-file-interaction": "Interaktsioon meediafailidega",
+       "grant-group-watchlist-interaction": "Interaktsioon sinu jälgimisloendiga",
+       "grant-group-email": "E-kirja saatmine",
+       "grant-group-high-volume": "Suuremahuline tegevus",
+       "grant-group-customization": "Kohandamine ja eelistused",
+       "grant-group-administration": "Administraatori toimingud",
+       "grant-group-other": "Mitmesugused toimingud",
+       "grant-blockusers": "Kasutajate blokeerimine ja blokeeringute eemaldamine",
+       "grant-createaccount": "Kontode loomine",
+       "grant-createeditmovepage": "Lehekülgede alustamine, muutmine ja teisaldamine",
+       "grant-delete": "Lehekülgede, redaktsioonide ja logisissekannete kustutamine",
+       "grant-editinterface": "MediaWiki nimeruumi ning kasutaja CSSi ja JavaScripti redigeerimine",
+       "grant-editmycssjs": "Oma CSSi või JavaScripti muutmine",
+       "grant-editmyoptions": "Enda eelistuste muutmine",
+       "grant-editmywatchlist": "Oma jälgimisloendi muutmine",
+       "grant-editpage": "Olemasolevate lehekülgede redigeerimine",
+       "grant-editprotected": "Kaitstud lehekülgede redigeerimine",
+       "grant-highvolume": "Suuremahuline redigeerimine",
+       "grant-patrol": "Lehekülgede muudatuste kontroll",
+       "grant-protect": "Lehekülgede kaitsmine ja kaitse eemaldamine",
+       "grant-rollback": "Lehekülgede muudatuste tühistamine",
+       "grant-sendemail": "Kasutajatele e-kirjade saatmine",
+       "grant-uploadeditmovefile": "Failide üleslaadimine, asendamine ja teisaldamine",
+       "grant-uploadfile": "Uute failide üleslaadimine",
+       "grant-viewdeleted": "Kustutatud failide ja lehekülgede vaatamine",
+       "grant-viewmywatchlist": "Oma jälgimisloendi vaatamine",
        "newuserlogpage": "Konto loomise logi",
        "newuserlogpagetext": "Siin on logitud kasutajate registreerimine.",
        "rightslog": "Kasutajaõiguste logi",
        "listgrouprights-namespaceprotection-header": "Nimeruumipiirangud",
        "listgrouprights-namespaceprotection-namespace": "Nimeruum",
        "listgrouprights-namespaceprotection-restrictedto": "Redigeerimiseks vajalikud õigused",
+       "listgrants-summary": "See on OAuthi-volituste ja neile vastavate kasutajaõiguste loend. Kasutaja saab volitada rakenduse tarvituse enda nimel, aga vaid kasutaja valitud volituste piires. Rakenduse abil ei saa kasutaja nimel siiski kasutada õigusi, mida kasutajal pole.\nÜksikute õiguste kohta võib leiduda [[{{MediaWiki:Listgrouprights-helppage}}|lisateavet]].",
+       "listgrants-rights": "Volitus",
        "trackingcategories": "Süsteemikategooriad",
        "trackingcategories-summary": "Siin leheküljel on loetletud süsteemikategooriad, millesse MediaWiki tarkvara ise lehekülgi arvab. Nende kategooriate nimesid saab muuta, kui vahetada {{ns:8}}-nimeruumis vastavaid süsteemisõnumeid.",
        "trackingcategories-msg": "Süsteemikategooria",
index 4c31544..0ba79a6 100644 (file)
        "october-date": "$1 اکتبر",
        "november-date": "$1 نوامبر",
        "december-date": "$1 دسامبر",
+       "period-am": "صبح",
+       "period-pm": "عصر",
        "pagecategories": "{{PLURAL:$1|رده|رده‌ها}}",
        "category_header": "صفحه‌های ردهٔ «$1»",
        "subcategories": "زیررده‌ها",
        "right-managechangetags": "ایجاد و حذف [[Special:Tags|برچسب‌ها]] از پایگاه داده",
        "right-applychangetags": "تائید [[Special:Tags|برچسب]] بر روی تغییرات یک نفر",
        "right-changetags": "افزودن یا حذف [[Special:Tags|برچسب]] قراردادی بر روی نسخه یا سیاهه ورودی‌ها",
+       "grant-generic": "\" $1 \" حقوق بسته نرم‌افزاری",
+       "grant-group-page-interaction": "تعامل با صفحات",
+       "grant-group-file-interaction": "تعامل با رسانه",
+       "grant-group-watchlist-interaction": "تعامل با فهرست پیگیری‌های شما",
+       "grant-group-email": "ارسال ایمیل",
+       "grant-group-high-volume": "انجام فعالیت‌های حجم بالا",
+       "grant-group-customization": "سفارشی‌سازی و تنظیمات",
+       "grant-group-administration": "انجام اقدامات مدیریتی",
+       "grant-group-other": "فعالیت‌های متفرقه",
+       "grant-blockusers": "بستن و باز کردن کاربرها",
+       "grant-createaccount": "ایجاد حساب‌های کاربری",
+       "grant-createeditmovepage": "ایجاد، ویرایش و انتقال صفحات",
+       "grant-delete": "حذف صفحات، نسخه‌های ویرایش و سیاهه ورودی",
+       "grant-editinterface": "ویرایش CSS کاربر/جاوااسکریپت  و فضای نام مدیاویکی",
+       "grant-editmycssjs": "ویرایش  CSS /جاوااسکریپت  کاربری خودتان",
+       "grant-editmyoptions": "اولویت‌های کاربری خود را ویرایش کنید",
+       "grant-editmywatchlist": "ویرایش فهرست پی‌گیری‌هایتان",
+       "grant-editpage": "ویرایش صفحات موجود",
+       "grant-editprotected": "ویرایش صفحه حفاظت شده",
+       "grant-highvolume": "ویرایش با حجم بالا",
+       "grant-oversight": "پنهان کردن ویرایش‌ها",
+       "grant-patrol": "تغییرات گشت صفحات",
+       "grant-protect": "حفاظت و عدم حفاظت صفحات",
+       "grant-rollback": "واگردانی  تغییرات صفحات",
+       "grant-sendemail": "ارسال ایمیل به دیگر کاربران",
+       "grant-uploadeditmovefile": "بارگذاری، جایگزینی و انتقال پرونده‌ها",
+       "grant-uploadfile": "بازگذاری پرونده‌های جدید",
+       "grant-viewdeleted": "مشاهدهٔ اطلاعات حذف",
+       "grant-viewmywatchlist": "مشاهدۀ فهرست پیگیری‌هایتان",
        "newuserlogpage": "سیاههٔ ایجاد کاربر",
        "newuserlogpagetext": "این سیاهه‌ای از نام‌های کاربری تازه‌ساخته‌شده است.",
        "rightslog": "سیاههٔ اختیارات کاربر",
        "listgrouprights-namespaceprotection-header": "محدودیت فضای نام",
        "listgrouprights-namespaceprotection-namespace": "فضای نام",
        "listgrouprights-namespaceprotection-restrictedto": "دسترسی(های) مجاز کاربر برای ویرایش",
+       "listgrants-summary": "فهرست روبرو OAuthهای اعطاشده با دسترسی‌هایشان به دسترسی‌های کاربری است. کاربران می‌توانند برنامه‌ها را به حسابشان دسترسی دهند ولی با دسترسی محدود بر پایهٔ اعطاشده‌های کاربر به برنامه. یک برنامه از طرف کاربر عمل می‌کند و نمی‌تواند کاری را انجام دهد که کاربر بدان دسترسی ندارد.\nاینجا احتمالاً [[{{MediaWiki:Listgrouprights-helppage}}|اطلاعات بیشتری]] دربارهٔ دسترسی‌های منحصر به‌فرد وجود دارد.",
+       "listgrants-grant": "اعطا",
+       "listgrants-rights": "دسترسی‌ها",
        "trackingcategories": "رده‌های ردیابی",
        "trackingcategories-summary": "این صفحه فهرست رده‌هایی ردیابی است که به‌صورت خودکار توسط مدیاویکی پر می‌شوند است. نام‌هایشان می‌تواند پس از تغییر پیام‌های سامانه‌ای مرتبط در فضای نام {{ns:8}} تغییر یابد.",
        "trackingcategories-msg": "ردهٔ ردیابی",
        "hebrew-calendar-m11-gen": "آب",
        "hebrew-calendar-m12-gen": "ایلول",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|بحث]])",
+       "timezone-local": "محلی",
        "duplicate-defaultsort": "هشدار: ترتیب پیش‌فرض «$2» ترتیب پیش‌فرض قبلی «$1» را باطل می‌کند.",
        "duplicate-displaytitle": "<strong>هشدار:</strong> نمایش عنوان \" $2 \"باعث ابطال پیش نمایش عنوان\" $1 \" می‌شود.",
        "invalid-indicator-name": "<strong>خطا:</strong>ویژگی های شاخص‌های وضعیت صفحهٔ <code>name</code> نباید خالی باشند.",
index 6492700..570f754 100644 (file)
        "right-managechangetags": "Luoda ja poistaa [[Special:Tags|merkkauksia]] tietokannasta",
        "right-applychangetags": "Asettaa [[Special:Tags|merkkauksia]] omien muutosten yhteyteen",
        "right-changetags": "Lisätä ja poistaa satunnaisia [[Special:Tags|merkkauksia]] yksittäisissä sivuversioissa tai lokimerkinnöissä",
+       "grant-group-high-volume": "Suorita suuri määrä toimintoja",
+       "grant-group-customization": "Mukautus ja asetukset",
+       "grant-group-administration": "Suorita ylläpidon toimintoja",
+       "grant-group-other": "Sekalainen toiminta",
+       "grant-blockusers": "Estä käyttäjiä muokkaamasta ja poista estoja",
+       "grant-createaccount": "Luo käyttäjätunnuksia",
+       "grant-createeditmovepage": "Luo, muokkaa ja siirrä sivuja",
+       "grant-delete": "Poista sivuja, yksittäisiä versioita tai lokimerkintöjä",
+       "grant-editinterface": "Muokkaa järjestelmäviestejä ja käyttäjien CSS/JavaScript-sivuja",
+       "grant-editmycssjs": "Muokkaa käyttäjän omia CSS/JavaScript-sivuja",
+       "grant-editmyoptions": "Muokkaa käyttäjän omia asetuksia",
+       "grant-editmywatchlist": "Muokkaa omaa tarkkailulistaa",
+       "grant-editpage": "Muokkaa olemassa olevia sivuja",
+       "grant-editprotected": "Muokkaa suojattuja sivuja",
+       "grant-highvolume": "Paljon muokkauksia",
+       "grant-oversight": "Piilota käyttäjiä ja häivytä versioita",
+       "grant-patrol": "Partioi sivuihin tehtyjä muutoksia",
+       "grant-protect": "Suojaa sivuja tai poista suojaus",
+       "grant-rollback": "Palauta nopeasti sivuun tehdyt muutokset",
+       "grant-sendemail": "Lähetä sähköpostia toisille käyttäjille",
+       "grant-uploadeditmovefile": "Tallenna, korvaa ja siirrä tiedostoja",
+       "grant-uploadfile": "Tallenna uusia tiedostoja",
+       "grant-viewdeleted": "Näe poistettu sisältö",
+       "grant-viewmywatchlist": "Katso omaa tarkkailulistaa",
        "newuserlogpage": "Uudet käyttäjät",
        "newuserlogpagetext": "Tämä on loki luoduista käyttäjätunnuksista.",
        "rightslog": "Käyttöoikeusloki",
index a316142..e53db83 100644 (file)
        "virus-scanfailed": "Échec de la recherche (code $1)",
        "virus-unknownscanner": "antivirus inconnu :",
        "logouttext": "'''Vous êtes à présent déconnecté{{GENDER:||e}}.'''\n\nNotez que certaines pages peuvent être encore affichées comme si vous étiez toujours connecté, jusqu’à ce que vous effaciez le cache de votre navigateur.",
+       "cannotlogoutnow-title": "Impossible de se déconnecter maintenant",
+       "cannotlogoutnow-text": "La déconnexion n’est pas possible en utilisant $1.",
        "welcomeuser": "Bienvenue, $1&nbsp;!",
        "welcomecreation-msg": "Votre compte a été créé.\nN'oubliez pas de modifier [[Special:Preferences|vos préférences pour {{SITENAME}}]].",
        "yourname": "Nom d'utilisateur :",
        "remembermypassword": "Me reconnecter automatiquement lors des prochaines visites avec ce navigateur (au maximum $1&nbsp;{{PLURAL:$1|jour|jours}})",
        "userlogin-remembermypassword": "Garder ma session active",
        "userlogin-signwithsecure": "Utiliser une connexion sécurisée",
+       "cannotloginnow-title": "Impossible de se connecter maintenant",
+       "cannotloginnow-text": "La connexion n’est pas possible en utilisant $1.",
        "yourdomainname": "Votre domaine :",
        "password-change-forbidden": "Vous ne pouvez pas modifier les mots de passe sur ce wiki.",
        "externaldberror": "Une erreur s'est produite avec la base de données d'authentification externe, ou bien vous ne pouvez pas mettre à jour votre compte externe.",
        "resetpass_submit": "Changer le mot de passe et se connecter",
        "changepassword-success": "Votre mot de passe a été changé avec succès !",
        "changepassword-throttled": "Vous avez fait trop de tentatives de connexion récemment.\nVeuillez attendre $1 avant de réessayer.",
+       "botpasswords": "Mots de passe de robots",
+       "botpasswords-summary": "<em>Mots de passe de robots</em> permet d’accéder à un compte utilisateur via l’API sans utiliser les identifiants de connexion principaux. Les droits utilisateur disponibles en étant connecté avec un mot de passe robot peuvent être réduits.\n\nSi vous ne voyez pas pourquoi vous voudriez faire cela, c’est que vous n’en avez pas besoin. Personne ne devrait jamais vous demander d’en générer un et de le lui donner.",
+       "botpasswords-disabled": "Les mots de passe robots sont désactivés.",
+       "botpasswords-no-central-id": "Pour utiliser les mots de passe de robots, vous devez être connecté à un compte centralisé.",
+       "botpasswords-existing": "Mots de passe de robots existants",
+       "botpasswords-createnew": "Créer un nouveau mot de passe de robots",
+       "botpasswords-editexisting": "Modifier un mot de passe de robots existant",
+       "botpasswords-label-appid": "Nom du robot :",
+       "botpasswords-label-create": "Créer",
+       "botpasswords-label-update": "Mettre à jour",
+       "botpasswords-label-cancel": "Annuler",
+       "botpasswords-label-delete": "Supprimer",
+       "botpasswords-label-resetpassword": "Réinitialiser le mot de passe",
+       "botpasswords-label-grants": "Droits applicables :",
+       "botpasswords-help-grants": "Chaque droit donne accès aux droits utilisateurs listés qu’a déjà un compte. Voyez le [[Special:ListGrants|tableau des droits]] pour plus d’information.",
+       "botpasswords-label-restrictions": "Restrictions d’utilisation :",
+       "botpasswords-label-grants-column": "Accordé",
+       "botpasswords-bad-appid": "Le nom de robot « $1 » n’est pas valide.",
+       "botpasswords-insert-failed": "Échec de l’ajout du nom de robot « $1 ». A-t-il déjà été ajouté ?",
+       "botpasswords-update-failed": "Échec à la mise à jour du nom de robot « $1 ». A-t-il déjà été supprimé ?",
+       "botpasswords-created-title": "Mot de passe de robots créé",
+       "botpasswords-created-body": "Le mot de passe de robots « $1 » a bien été créé.",
+       "botpasswords-updated-title": "Mot de passe de robots mis à jour",
+       "botpasswords-updated-body": "Le mot de passe de robots « $1 » a bien été mis à jour.",
+       "botpasswords-deleted-title": "Mot de passe de robots supprimé",
+       "botpasswords-deleted-body": "Le mot de passe de robots « $1 » a été supprimé.",
+       "botpasswords-newpassword": "Le nouveau mot de passe pour se connecter avec <strong>$1</strong> est <strong>$2</strong>. <em>Veuillez l’enregistrer pour y faire référence ultérieurement.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider n’est pas disponible.",
+       "botpasswords-restriction-failed": "Les restrictions de mot de passe de robots empêchent cette connexion.",
+       "botpasswords-invalid-name": "Le nom d’utilisateur spécifié ne contient pas de séparateur de mot de passe de robots (« $1 »).",
+       "botpasswords-not-exist": "L’utilisateur « $1 » n’a pas de nom de mot de passe de robots appelé « $2 ».",
        "resetpass_forbidden": "Les mots de passe ne peuvent pas être changés",
        "resetpass-no-info": "Vous devez être connecté pour avoir accès à cette page.",
        "resetpass-submit-loggedin": "Changer de mot de passe",
        "right-createpage": "Créer des pages (qui ne sont pas des pages de discussion)",
        "right-createtalk": "Créer des pages de discussion",
        "right-createaccount": "Créer des comptes utilisateur",
+       "right-autocreateaccount": "Connexion automatique avec un compte utilisateur externe",
        "right-minoredit": "Marquer ses modifications comme mineures",
        "right-move": "Renommer des pages",
        "right-move-subpages": "Renommer des pages avec leurs sous-pages",
        "right-managechangetags": "Créer et supprimer des [[Spécial:Balises|balises]] de la base de données",
        "right-applychangetags": "Appliquer [[Special:Tags|les balises]] avec ses propres modifications",
        "right-changetags": "Ajouter et supprimer de façon arbitraire [[Special:Tags|des balises]] sur des révisions individuelles et des entrées de journal",
+       "grant-generic": "ensemble de droits « $1 »",
+       "grant-group-page-interaction": "Interagir avec des pages",
+       "grant-group-file-interaction": "Interagir avec des médias",
+       "grant-group-watchlist-interaction": "Interagir avec votre liste de suivi",
+       "grant-group-email": "Envoyer un courriel",
+       "grant-group-high-volume": "Effectuer une activité de fort volume",
+       "grant-group-customization": "Personnalisation et préférences",
+       "grant-group-administration": "Effectuer des actions administratives",
+       "grant-group-other": "Activités diverses",
+       "grant-blockusers": "Bloquer et débloquer des utilisateurs",
+       "grant-createaccount": "Créer des comptes",
+       "grant-createeditmovepage": "Créer, modifier et déplacer des pages",
+       "grant-delete": "Supprimer les pages, les révisions et les entrées du journal",
+       "grant-editinterface": "Modifier l’espace de noms MédiaWiki et le CSS/JavaScript utilisateur",
+       "grant-editmycssjs": "Modifier votre CSS/JavaScript utilisateur",
+       "grant-editmyoptions": "Modifier vos préférences utilisateur",
+       "grant-editmywatchlist": "Modifier votre liste de suivi",
+       "grant-editpage": "Modifier des pages existantes",
+       "grant-editprotected": "Modifier des pages protégées",
+       "grant-highvolume": "Modification de gros volumes",
+       "grant-oversight": "Masquer les utilisateurs et supprimer les révisions",
+       "grant-patrol": "Patrouiller les modifications aux pages",
+       "grant-protect": "Protéger et déprotéger des pages",
+       "grant-rollback": "Révoquer des modifications sur des pages",
+       "grant-sendemail": "Envoyer des courriels aux autres utilisateurs",
+       "grant-uploadeditmovefile": "Télécharger, remplacer et renommer des fichiers",
+       "grant-uploadfile": "Importer de nouveaux fichiers",
+       "grant-basic": "Droits de base",
+       "grant-viewdeleted": "Afficher les fichiers et pages supprimés",
+       "grant-viewmywatchlist": "Afficher votre liste de suivi",
        "newuserlogpage": "Journal des créations de comptes utilisateur",
        "newuserlogpagetext": "Cette page affiche l’historique des créations de comptes utilisateur.",
        "rightslog": "Journal des modifications de droits d’utilisateurs",
        "action-createpage": "créer des pages",
        "action-createtalk": "créer des pages de discussion",
        "action-createaccount": "créer ce compte utilisateur",
+       "action-autocreateaccount": "créer automatiquement ce compte utilisateur externe",
        "action-history": "afficher l’historique de cette page",
        "action-minoredit": "marquer cette modification comme mineure",
        "action-move": "renommer cette page",
        "listgrouprights-namespaceprotection-header": "Restrictions d'espace de noms",
        "listgrouprights-namespaceprotection-namespace": "Espace de noms",
        "listgrouprights-namespaceprotection-restrictedto": "Droit(s) permettant à l'utilisateur de modifier",
+       "listgrants": "Autorisations",
+       "listgrants-summary": "Voici une liste des droits avec leur accès associé aux droits utilisateur. Les utilisateurs peuvent autoriser les applications à utiliser leur compte, mais avec des droits limités d’après les droits que l’utilisateur a donnés à l’application. Un application agissant au nom d’un utilisateur ne peut toutefois pas, de fait, utiliser des droits que l’utilisateur ne possède pas.\nIl peut y avoir [[{{MediaWiki:Listgrouprights-helppage}}|plus d’information]] sur les droits individuels.",
+       "listgrants-grant": "Accorder",
+       "listgrants-rights": "Droits",
        "trackingcategories": "Catégories de suivi",
        "trackingcategories-summary": "Cette page liste les catégories de suivi qui sont remplies automatiquement par [[MediaWiki]]. Leurs noms peuvent être changés en modifiant les messages systèmes correspondants dans l’espace de noms {{ns:8}}.",
        "trackingcategories-msg": "Catégorie de suivi",
        "javascripttest-pagetext-frameworks": "Veuillez choisir l'une des structures de test suivantes : $1",
        "javascripttest-pagetext-skins": "Choisissez un habillage avec lequel lancer les tests :",
        "javascripttest-qunit-intro": "Voir [$1 la documentation de test] sur mediawiki.org.",
-       "tooltip-pt-userpage": "Votre page utilisateur",
+       "tooltip-pt-userpage": "{{GENDER:|Votre}} page utilisateur",
        "tooltip-pt-anonuserpage": "La page utilisateur de l'IP avec laquelle vous contribuez",
-       "tooltip-pt-mytalk": "Votre page de discussion",
+       "tooltip-pt-mytalk": "{{GENDER:|Votre}} page de discussion",
        "tooltip-pt-anontalk": "La page de discussion pour les contributions depuis cette adresse IP",
-       "tooltip-pt-preferences": "Vos préférences",
+       "tooltip-pt-preferences": "{{GENDER:|Vos}} préférences",
        "tooltip-pt-watchlist": "La liste des pages dont vous suivez les modifications",
-       "tooltip-pt-mycontris": "La liste de vos contributions",
+       "tooltip-pt-mycontris": "La liste de {{GENDER:|vos}} contributions",
        "tooltip-pt-anoncontribs": "Une liste des modifications effectuées depuis cette adresse IP",
        "tooltip-pt-login": "Il est recommandé de vous identifier ; ce n'est cependant pas obligatoire.",
        "tooltip-pt-logout": "Se déconnecter",
        "tooltip-t-recentchangeslinked": "Liste des modifications récentes des pages liées à celle-ci",
        "tooltip-feed-rss": "Flux RSS pour cette page",
        "tooltip-feed-atom": "Flux Atom pour cette page",
-       "tooltip-t-contributions": "Voir la liste des contributions de cet utilisateur",
-       "tooltip-t-emailuser": "Envoyer un courriel à cet utilisateur",
+       "tooltip-t-contributions": "Voir la liste des contributions de {{GENDER:$1|cet utilisateur|cette utilisatrice}}",
+       "tooltip-t-emailuser": "Envoyer un courriel à {{GENDER:$1|cet utilisateur|cette utilisatrice}}",
        "tooltip-t-info": "Plus d’information sur cette page",
        "tooltip-t-upload": "Envoyer une image ou fichier média sur le serveur",
        "tooltip-t-specialpages": "Liste de toutes les pages spéciales",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "la page n’existe pas encore",
        "mw-widgets-titleinput-description-redirect": "redirection vers $1",
-       "api-error-blacklisted": "Merci de choisir un autre titre descriptif."
+       "api-error-blacklisted": "Merci de choisir un autre titre descriptif.",
+       "sessionmanager-tie": "Impossible de combiner les demandes multiples de types d’authentification : $1.",
+       "sessionprovider-generic": "sessions $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sessions basées sur les cookies",
+       "sessionprovider-nocookies": "Les cookies peuvent être désactivés. Assurez-vous que vous avez activé les cookies et recommencez."
 }
index 93fd3a7..79a15c8 100644 (file)
@@ -56,7 +56,7 @@
        "tog-watchlisthidebots": "Agochar as edicións dos bots na lista de vixilancia",
        "tog-watchlisthideminor": "Agochar as edicións pequenas na lista de vixilancia",
        "tog-watchlisthideliu": "Agochar as edicións dos usuarios rexistrados na lista de vixilancia",
-       "tog-watchlistreloadautomatically": "Recargar a lista de vixilancia automáticamente cando se produce un cambio nun filtro (require JavaScript)",
+       "tog-watchlistreloadautomatically": "Recargar a lista de vixilancia automaticamente cando se produce un cambio nun filtro (require JavaScript)",
        "tog-watchlisthideanons": "Agochar as edicións dos usuarios anónimos na lista de vixilancia",
        "tog-watchlisthidepatrolled": "Agochar as edicións patrulladas na lista de vixilancia",
        "tog-watchlisthidecategorization": "Agochar a categorización das páxinas",
        "october-date": "$1 de outubro",
        "november-date": "$1 de novembro",
        "december-date": "$1 de decembro",
+       "period-am": "AM",
+       "period-pm": "PM",
        "pagecategories": "{{PLURAL:$1|Categoría|Categorías}}",
        "category_header": "Páxinas na categoría \"$1\"",
        "subcategories": "Subcategorías",
        "virus-scanfailed": "fallou o escaneado (código $1)",
        "virus-unknownscanner": "antivirus descoñecido:",
        "logouttext": "'''Agora está fóra do sistema.'''\n\nTeña en conta que algunhas páxinas poden continuar aparecendo como se aínda estivese dentro do sistema, ata que limpe a caché do seu navegador.",
+       "cannotlogoutnow-title": "Non se pode saír da sesión agora mesmo",
+       "cannotlogoutnow-text": "Non é posible saír da sesión cando se usa $1.",
        "welcomeuser": "Reciba a nosa benvida, $1!",
        "welcomecreation-msg": "A súa conta foi creada correctamente.\nNon esqueza personalizar as súas [[Special:Preferences|preferencias de {{SITENAME}}]].",
        "yourname": "Nome de usuario:",
        "remembermypassword": "Lembrar o meu contrasinal neste ordenador (ata $1 {{PLURAL:$1|día|días}})",
        "userlogin-remembermypassword": "Manter a miña conexión",
        "userlogin-signwithsecure": "Utilizar a conexión segura",
+       "cannotloginnow-title": "Non se pode iniciar a sesión agora mesmo",
+       "cannotloginnow-text": "Non é posible iniciar a sesión cando se usa $1.",
        "yourdomainname": "O seu dominio:",
        "password-change-forbidden": "Non pode mudar os contrasinais neste wiki.",
        "externaldberror": "Ou ben se produciu un erro da base de datos na autenticación externa ou ben non se lle permite actualizar a súa conta externa.",
        "resetpass_submit": "Establecer o contrasinal e acceder ao sistema",
        "changepassword-success": "O seu contrasinal modificouse correctamente!",
        "changepassword-throttled": "Fixo demasiados intentos de acceder ao sistema.\nPor favor, agarde $1 antes de probar outra vez.",
+       "botpasswords": "Contrasinais de Bot",
+       "botpasswords-summary": "Os <em>contrasinais de Bot</em> permiten acceder a unha conta de usuario por medio da API sen usar as crecenciais de acceso da conta principal. Os dereitos de usuario dispoñibles cando se accede ao sistema cun contrasinal de bot poden estar restrinxidos.",
+       "botpasswords-disabled": "Os contrasinais de bot non están habilitados.",
+       "botpasswords-no-central-id": "Para usar contrasinais de bot debes acceder ao sistema cunha conta centralizada.",
+       "botpasswords-existing": "Contrasinais de bot existentes",
+       "botpasswords-createnew": "Crear un novo contrasinal de bot",
+       "botpasswords-editexisting": "Editar un contrasinal de bot xa existente",
+       "botpasswords-label-appid": "Nome do bot:",
+       "botpasswords-label-create": "Crear",
+       "botpasswords-label-update": "Actualizar",
+       "botpasswords-label-cancel": "Cancelar",
+       "botpasswords-label-delete": "Borrar",
+       "botpasswords-label-resetpassword": "Restablecer o contrasinal",
+       "botpasswords-label-grants": "Permisos aplicables:",
+       "botpasswords-help-grants": "Cada permiso da acceso aos permisos de usuario listados que a conta xa teña. Vexa a [[Special:ListGrants|táboa de permisos]] para máis información.",
+       "botpasswords-label-restrictions": "Restriccións de uso:",
+       "botpasswords-label-grants-column": "Concedido",
+       "botpasswords-bad-appid": "O nome de bot \"$1\" non é válido.",
+       "botpasswords-insert-failed": "Erro ao engadir o nome de bot \"$1\". Revise se xa foi engadido previamente.",
+       "botpasswords-update-failed": "Erro ao actualizar o nome de bot \"$1\". Revise se foi borrado.",
+       "botpasswords-created-title": "Contrasinal de bot creado",
+       "botpasswords-created-body": "O contrasinal de bot \"$1\" creouse con éxito.",
+       "botpasswords-updated-title": "Contrasinal de bot actualizado",
+       "botpasswords-updated-body": "O contrasinal de bot \"$1\" actualizouse con éxito.",
+       "botpasswords-deleted-title": "Contrasinal de bot borrado",
+       "botpasswords-deleted-body": "O contrasinal de bot \"$1\" foi borrado.",
+       "botpasswords-newpassword": "O novo contrasinal para acceder con strong>$1</strong> é <strong>$2</strong>. <em>Por favor, rexistra isto para referencia futura.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider non está dispoñible.",
+       "botpasswords-restriction-failed": "Restricións de contrasinal de bots evitaron esta conexión.",
+       "botpasswords-invalid-name": "O nome de usuario especificado non contén o separador de contrasinal de bot (\"$1\").",
+       "botpasswords-not-exist": "O usuario \"$1\" non ten un contrasinal de bot de nome \"$2\".",
        "resetpass_forbidden": "Non se poden mudar os contrasinais",
        "resetpass-no-info": "Debe rexistrarse para acceder directamente a esta páxina.",
        "resetpass-submit-loggedin": "Cambiar o contrasinal",
        "italic_tip": "Texto en cursiva",
        "link_sample": "Título da ligazón",
        "link_tip": "Ligazón interna",
-       "extlink_sample": "http://www.example.com título da ligazón",
+       "extlink_sample": "http://www.exemplo.com título da ligazón",
        "extlink_tip": "Ligazón externa (lembre o prefixo http://)",
        "headline_sample": "Texto de cabeceira",
        "headline_tip": "Cabeceira de nivel 2",
        "nowiki_sample": "Insira aquí un texto sen formato",
        "nowiki_tip": "Ignorar o formato wiki",
        "image_sample": "Exemplo.jpg",
-       "image_tip": "Ficheiro embelecido",
+       "image_tip": "Ficheiro incorporado",
        "media_sample": "Exemplo.ogg",
        "media_tip": "Ligazón a un ficheiro",
        "sig_tip": "A súa sinatura con data e hora",
        "textmatches": "O texto da páxina coincide",
        "notextmatches": "Non se atopou o texto en ningunha páxina",
        "prevn": "{{PLURAL:$1|$1}} previas",
-       "nextn": "{{PLURAL:$1|$1}} seguintes",
+       "nextn": "{{PLURAL:$1|seguinte|$1 seguintes}}",
        "prev-page": "páxina anterior",
        "next-page": "páxina seguinte",
        "prevn-title": "{{PLURAL:$1|O resultado anterior|Os anteriores $1 resultados}}",
        "right-createpage": "Crear páxinas (que non son de conversa)",
        "right-createtalk": "Crear páxinas de conversa",
        "right-createaccount": "Crear novas contas de usuario",
+       "right-autocreateaccount": "Acceder ao sistema automaticamente cunha conta de usuario externa",
        "right-minoredit": "Marcar as edicións como pequenas",
        "right-move": "Mover páxinas",
        "right-move-subpages": "Mover páxinas coas súas subpáxinas",
        "right-managechangetags": "Crear e borrar [[Special:Tags|tags]] da base de datos",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetas]] xunto cos cambios propios",
        "right-changetags": "Engadir e quitar [[Special:Tags|etiquetas]] arbitrarias a revisións individuais e entradas do rexistro",
+       "grant-generic": "conxunto de dereitos \"$1\"",
+       "grant-group-page-interaction": "Interactuar con páxinas",
+       "grant-group-file-interaction": "Interactuar con ficheiros multimedia",
+       "grant-group-watchlist-interaction": "Interactuar coa súa lista de vixilancia",
+       "grant-group-email": "Enviar correos electrónicos",
+       "grant-group-high-volume": "Realizar actividades de alto volume",
+       "grant-group-customization": "Personalización e preferencias",
+       "grant-group-administration": "Realizar accións administrativas",
+       "grant-group-other": "Outras actividades",
+       "grant-blockusers": "Bloquear e desbloquear usuarios",
+       "grant-createaccount": "Crear contas",
+       "grant-createeditmovepage": "Crear, editar e mover páxinas",
+       "grant-delete": "Borrar páxinas, revisións e entradas de rexistro",
+       "grant-editinterface": "Editar o espazo de nomes MediaWiki e o CSS/JavaScript de usuario",
+       "grant-editmycssjs": "Editar o seu CSS/JavaScript de usuario",
+       "grant-editmyoptions": "Editar as súas preferencias de usuario",
+       "grant-editmywatchlist": "Editar a súa lista de vixilancia",
+       "grant-editpage": "Editar páxinas existentes",
+       "grant-editprotected": "Editar páxinas protexidas",
+       "grant-highvolume": "Edicións de gran volume",
+       "grant-oversight": "Agochar usuarios e eliminar revisións",
+       "grant-patrol": "Patrullar os cambios feitos nas páxinas",
+       "grant-protect": "Protexer e desprotexer páxinas",
+       "grant-rollback": "Reverter os cambios feitos nas páxinas",
+       "grant-sendemail": "Enviar correos electrónicos a outros usuarios",
+       "grant-uploadeditmovefile": "Cargar, substituír e mover ficheiros",
+       "grant-uploadfile": "Cargar ficheiros novos",
+       "grant-basic": "Dereitos básicos",
+       "grant-viewdeleted": "Ver ficheiros e páxinas eliminadas",
+       "grant-viewmywatchlist": "Ver a súa lista de vixilancia",
        "newuserlogpage": "Rexistro de creación de usuarios",
        "newuserlogpagetext": "Este é un rexistro de creación de contas de usuario.",
        "rightslog": "Rexistro de dereitos de usuario",
        "action-createpage": "crear páxinas",
        "action-createtalk": "crear páxinas de conversa",
        "action-createaccount": "crear esta conta de usuario",
+       "action-autocreateaccount": "crear automaticamente esta conta de usuario externa",
        "action-history": "ver o historial desta páxina",
        "action-minoredit": "marcar esta edición como pequena",
        "action-move": "mover esta páxina",
        "upload-form-label-select-file": "Seleccionar un ficheiro",
        "upload-form-label-infoform-title": "Detalles",
        "upload-form-label-infoform-name": "Nome",
+       "upload-form-label-infoform-name-tooltip": "Un título único descritivo para o ficheiro, que servirá como un nome de ficheiro. Pode usar unha linguaxe clara con espazos. Non inclúa a extensión do ficheiro.",
        "upload-form-label-infoform-description": "Descrición",
+       "upload-form-label-infoform-description-tooltip": "Describa brevemente todo o destacable acerca do traballo.\nPara unha foto, mencione as cousas principais que se representan, a ocasión ou o lugar.",
        "upload-form-label-usage-title": "Uso",
        "upload-form-label-usage-filename": "Nome do ficheiro",
        "foreign-structured-upload-form-label-own-work": "Isto é o meu propio traballo",
        "move": "Mover",
        "movethispage": "Mover esta páxina",
        "unusedimagestext": "Os seguintes ficheiros existen pero aínda non se incluíron en ningunha páxina.\nPor favor, teña en conta que outras páxinas web poden ligar cara a un ficheiro mediante un enderezo URL directo e por iso poden aparecer listados aquí, mesmo estando en uso.",
-       "unusedcategoriestext": "Existen as seguintes categorías, aínda que ningún artigo ou categoría as emprega.",
+       "unusedcategoriestext": "As seguintes categorías están creadas, aínda que ningún artigo ou categoría fai uso delas.",
        "notargettitle": "Sen obxectivo",
        "notargettext": "Non especificou a páxina ou o usuario no cal levar a cabo esta función.",
        "nopagetitle": "Non existe esa páxina",
        "listgrouprights-namespaceprotection-header": "Restricións dos espazos de nomes",
        "listgrouprights-namespaceprotection-namespace": "Espazo de nomes",
        "listgrouprights-namespaceprotection-restrictedto": "Dereito(s) que permite(n) ao usuario editar",
+       "listgrants": "Permisos",
+       "listgrants-summary": "Esta é unha lista de permisos cos seus accesos asoaciados aos permisos de usuario. Os usuarios poden autorizar aplicacións para usar a súa conta, pero con permisos limitados baseados nos permisos que o usuario dá á aplicación. Porén, unha aplicación que actúa no lugar do usuario non pode empregar permisos que o propio usuario non posúe.\nPode ver máis información sobre dereitos individuais [[{{MediaWiki:Listgrouprights-helppage}}|aquí]].",
+       "listgrants-grant": "Outorgar",
+       "listgrants-rights": "Dereitos",
        "trackingcategories": "Categorías de seguimento",
        "trackingcategories-summary": "Esta páxina lista as categorías de seguimento que o software de MediaWiki enche automaticamente. Pódense alterar os seus nomes modificando as correspondentes mensaxes do sistema no espazo de nomes \"{{ns:8}}\".",
        "trackingcategories-msg": "Categoría de seguimento",
        "unblock": "Desbloquear un usuario",
        "blockip": "Bloquear {{GENDER:$1|o usuario|a usuaria}}",
        "blockip-legend": "Bloquear un usuario",
-       "blockiptext": "Use o seguinte formulario para bloquear o acceso de escritura desde un enderezo IP ou para bloquear un usuario específico.\nIsto debería facerse só para previr vandalismo, e de acordo coa [[{{MediaWiki:Policy-url}}|política e normas]] vixentes.\nExplique a razón específica do bloqueo (por exemplo, citando as páxinas concretas que sufriron vandalismo).",
+       "blockiptext": "Use o seguinte formulario para bloquear o acceso de escritura desde un enderezo IP ou para bloquear un usuario específico.\nIsto debería facerse só para previr vandalismo, e de acordo coa [[{{MediaWiki:Policy-url}}|política e normas]] vixentes.\nExplique a razón específica do bloqueo abaixo (por exemplo, citando as páxinas concretas que sufriron vandalismo).\nPode bloquear intervalos IP coa sintaxe [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]; o intervalo máis grande permitido é /$1 para IPv4 e /$2 para IPv6.",
        "ipaddressorusername": "Enderezo IP ou nome de usuario:",
        "ipbexpiry": "Duración:",
        "ipbreason": "Motivo:",
        "move-page-legend": "Mover unha páxina",
        "movepagetext": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nPode actualizar automaticamente as redireccións que van dar ao título orixinal.\nSe escolle non facelo, asegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
        "movepagetext-noredirectfixer": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nAsegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
-       "movepagetalktext": "Se marca esta caixa, a páxina de conversa asociada trasladarase automáticamente ó título novo a menos que xa exista unha páxina de conversa non baleira alí.\n\nNeste caso, deberá trasladar ou fusionar manualmente a páxina se así o quere.",
+       "movepagetalktext": "Se marca esta caixa, a páxina de conversa asociada moverase automaticamente ó título novo a menos que xa exista unha páxina de conversa non baleira alí.\n\nNeste caso, deberá trasladar ou fusionar manualmente a páxina se así o desexa.",
        "moveuserpage-warning": "'''Aviso:''' Está a piques de mover unha páxina de usuario. Por favor, teña en conta que só se trasladará a páxina e que o usuario '''non''' será renomeado.",
        "movecategorypage-warning": "'''Aviso:''' Está a piques de mover unha páxina de categoría. Por favor, teña en conta que só se trasladará a páxina e que as páxinas presentes na categoría vella '''non''' serán recategorizadas na categoría nova.",
        "movenologintext": "Debe ser un usuario rexistrado e [[Special:UserLogin|acceder ao sistema]] para mover unha páxina.",
        "export-download": "Gardar como un ficheiro",
        "export-templates": "Incluír os modelos",
        "export-pagelinks": "Engadir as páxinas ligadas a unha profundidade de:",
+       "export-manual": "Engadir páxinas manualmente:",
        "allmessages": "Mensaxes do sistema",
        "allmessagesname": "Nome",
        "allmessagesdefault": "Texto predeterminado",
        "tooltip-pt-anonuserpage": "A páxina de usuario do enderezo IP desde o que está a editar",
        "tooltip-pt-mytalk": "A súa páxina de conversa",
        "tooltip-pt-anontalk": "Conversa acerca de edicións feitas desde este enderezo IP",
-       "tooltip-pt-preferences": "As miñas preferencias",
+       "tooltip-pt-preferences": "As as preferencias",
        "tooltip-pt-watchlist": "A lista de páxinas cuxas modificacións está a seguir",
        "tooltip-pt-mycontris": "Lista das súas contribucións",
        "tooltip-pt-anoncontribs": "Unha lista de modificacións feitas desde esta dirección IP",
        "tooltip-t-recentchangeslinked": "Cambios recentes nas páxinas ligadas desde esta",
        "tooltip-feed-rss": "Fonte de novas RSS desta páxina",
        "tooltip-feed-atom": "Fonte de novas Atom desta páxina",
-       "tooltip-t-contributions": "Ver a lista de contribucións {{GENDER:{{BASEPAGENAME}}|deste usuario|desta usuaria}}",
-       "tooltip-t-emailuser": "Enviarlle unha mensaxe a {{GENDER:{{BASEPAGENAME}}|este usuario|esta usuaria}} por correo electrónico",
+       "tooltip-t-contributions": "Lista de contribucións {{GENDER:$1|deste usuario|desta usuaria}}",
+       "tooltip-t-emailuser": "Enviarlle unha mensaxe de correo electrónico a {{GENDER:$1|este usuario|esta usuaria}}",
        "tooltip-t-info": "Máis información sobre esta páxina",
        "tooltip-t-upload": "Subir ficheiros",
        "tooltip-t-specialpages": "Lista de todas as páxinas especiais",
        "pageinfo-category-files": "Número de ficheiros",
        "markaspatrolleddiff": "Marcar como revisada",
        "markaspatrolledtext": "Marcar esta páxina como revisada",
+       "markaspatrolledtext-file": "Marcar esta versión de ficheiro como verificada",
        "markedaspatrolled": "Marcar como revisado",
        "markedaspatrolledtext": "A revisión seleccionada de \"[[:$1]]\" foi marcada como revisada.",
        "rcpatroldisabled": "A patrulla dos cambios recentes está desactivada",
        "newimages-legend": "Filtro",
        "newimages-label": "Nome do ficheiro (ou parte del):",
        "newimages-showbots": "Mostrar as cargas feitas por bots",
+       "newimages-hidepatrolled": "Ocultar as subas verificadas",
        "noimages": "Non hai imaxes para ver.",
        "ilsubmit": "Procurar",
        "bydate": "por data",
        "watchlisttools-edit": "Ver e editar a lista de vixilancia",
        "watchlisttools-raw": "Editar a lista de vixilancia simple",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|conversa]])",
+       "timezone-local": "Local",
        "duplicate-defaultsort": "<strong>Aviso:</strong> A clave de ordenación por defecto \"$2\" anula a clave de ordenación anterior por defecto \"$1\".",
        "duplicate-displaytitle": "'''Aviso:''' O título mostrado \"$2\" anula o título anterior \"$1\".",
        "invalid-indicator-name": "<strong>Erro:</strong> O atributo <code>name</code> dos indicadores do estado da páxina non pode estar baleiro.",
        "expand_templates_preview": "Vista previa",
        "expand_templates_preview_fail_html": "<em>Dado que o código HTML puro está activado en {{SITENAME}} e produciuse unha perda dos datos da sesión, a vista previa está oculta como precaución contra ataques mediante código JavaScript.</em>\n\n<strong>Se este é un intento lexítimo de acceso á vista previa, inténteo de novo.</strong>\nSe segue sen funcionar, probe a [[Special:UserLogout|saír]] e volver a entrar coa súa conta.",
        "expand_templates_preview_fail_html_anon": "<em>Dado que o código HTML puro está activado en {{SITENAME}} e produciuse unha perda dos datos da sesión, a vista previa está oculta como precaución contra ataques mediante código JavaScript.</em>\n\n<strong>Se este é un intento lexítimo de acceso á vista previa, probe a [[Special:UserLogout|saír]] e volver a entrar coa súa conta.</strong>",
+       "expand_templates_input_missing": "Necesita proporcionar polo menos algún texto de entrada.",
        "pagelanguage": "Selector de lingua da páxina",
        "pagelang-name": "Páxina",
        "pagelang-language": "Lingua",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "a páxina aínda non existe",
        "mw-widgets-titleinput-description-redirect": "redirección cara a $1",
-       "api-error-blacklisted": "Escolla un título diferente e descritivo."
+       "api-error-blacklisted": "Escolla un título diferente e descritivo.",
+       "sessionmanager-tie": "Non pode combinar peticións múltiples de tipos de autenticación: $1.",
+       "sessionprovider-generic": "sesións $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sesións baseadas nas cookies",
+       "sessionprovider-nocookies": "As cookies poden estar desactivadas. Asegúrese de que ten activas as cookies e comece de novo."
 }
index d67eb56..2626085 100644 (file)
        "diff-multi-sameuser": "(સમાન સભ્ય દ્વારા {{PLURAL:$1|કરાયેલ એક પુનરાવર્તન| કરાયેલા $1 પુનરાવર્તનો}} દર્શાવેલ નથી)",
        "diff-multi-manyusers": "{{PLURAL:$2|એક સભ્યએ કરેલું|$2 સભ્યોએ કરેલા}} ({{PLURAL:$1|વચગાળાનું એક પુનરાવર્તન દર્શાવ્યં|વચગાળાનાં $1 પુનરાવર્તનો દર્શાવ્યાં}} નથી.)",
        "searchresults": "પરિણામોમાં શોધો",
-       "searchresults-title": "પરિણામોમાં \"$1\" શોધો",
+       "searchresults-title": "\"$1\" માટેના પરિણામો",
        "titlematches": "પાનાનું શીર્ષક મળતું આવે છે",
        "textmatches": "પાનાના શબ્દો મળતાં આવે છે",
        "notextmatches": "આ શબ્દ કોઈ પાનામાં મળ્યો નથી",
        "javascripttest-pagetext-frameworks": "નીચેનામાંથી કોઈ એક ચકાસણી ફ્રેમવર્ક પસંદ કરો : $1",
        "javascripttest-pagetext-skins": "ચકાસણી કરવા માટેની સ્કીન પસંદ કરો",
        "javascripttest-qunit-intro": "mediawiki.org પર  [$1 testing documentation] તપાસ માહિતી જુઓ",
-       "tooltip-pt-userpage": "તમારૂં પાનું (તમારૂં 'મારા વિષે')",
+       "tooltip-pt-userpage": "{{GENDER:|તમારું સભ્ય}} પાનું",
        "tooltip-pt-anonuserpage": "IP સરનામું માટેના સભ્ય પાનામાં તમે ફેરફાર કરી રહ્યાં છો.",
-       "tooltip-pt-mytalk": "તમારૂં ચર્ચાનું પાનું",
+       "tooltip-pt-mytalk": "{{GENDER:|તમારૂં}} ચર્ચા પાનું",
        "tooltip-pt-anontalk": "આ IP સરનામા દ્વારા થયેલ ફેરફારની ચર્ચા",
-       "tooltip-pt-preferences": "તમારી પસંદગીઓ",
+       "tooltip-pt-preferences": "{{GENDER:|તમારી}} પસંદગીઓ",
        "tooltip-pt-watchlist": "તમે દેખરેખ રાખી રહ્યાં હોવ તેવા પાનાઓની યાદી",
-       "tooltip-pt-mycontris": "તમારા યોગદાનની યાદી",
+       "tooltip-pt-mycontris": "{{GENDER:|તમારા}} યોગદાનોની યાદી",
        "tooltip-pt-login": "આપને પ્રવેશ કરવા ભલામણ કરવામાં આવે છે, જોકે તે આવશ્યક નથી",
        "tooltip-pt-logout": "પ્રસ્થાન",
        "tooltip-pt-createaccount": "તમને ખાતું બનાવીને પ્રવેશ કરવા માટે આમંત્રણ છે; તેમ કરવું જોકે, આવશ્યક નથી",
        "tooltip-t-recentchangeslinked": "આ પાના પરની કડીઓ વાળા લેખોમાં તાજેતરમાં થયેલા ફેરફારો",
        "tooltip-feed-rss": "આ પાના માટે આર.એસ.એસ. ફીડ",
        "tooltip-feed-atom": "આ પાના માટે એટમ ફીડ",
-       "tooltip-t-contributions": "આ સભ્યનાં યોગદાનોની યાદી",
+       "tooltip-t-contributions": "{{GENDER:$1|આ સભ્ય}} વડે કરાયલ યોગદાનોની યાદી",
        "tooltip-t-emailuser": "આ સભ્યને ઇ-મેલ મોકલો",
        "tooltip-t-info": "આ પાનાં વિષે વધુ માહિતી",
        "tooltip-t-upload": "ફાઇલ ચડાવો",
index e13fb30..8788dba 100644 (file)
@@ -31,7 +31,8 @@
                        "Ldorfman",
                        "LaG roiL",
                        "Eraab",
-                       "Geagea"
+                       "Geagea",
+                       "פוילישער"
                ]
        },
        "tog-underline": "סימון קישורים בקו תחתי:",
        "virus-scanfailed": "הסריקה נכשלה (קוד: $1)",
        "virus-unknownscanner": "אנטי־וירוס בלתי ידוע:",
        "logouttext": "'''יצאתם זה עתה מהחשבון.'''\n\nשימו לב כי ייתכן שדפים אחדים ימשיכו להיות מוצגים כאילו אתם עדיין מחוברים לחשבון עד שתנקו את המטמון של הדפדפן שלכם.",
+       "cannotlogoutnow-title": "לא ניתן לצאת מהחשבון עכשיו",
+       "cannotlogoutnow-text": "היציאה אינה אפשרית בעת שימוש ב{{GRAMMAR:תחילית|$1}}.",
        "welcomeuser": "ברוך בואך, $1!",
        "welcomecreation-msg": "חשבונך נוצר.\nבאפשרותך להתאים את [[Special:Preferences|ההעדפות]] שלך ב{{grammar:תחילית|{{SITENAME}}}}.",
        "yourname": "שם משתמש:",
        "remembermypassword": "שמירת הכניסה שלי בדפדפן הזה ({{PLURAL:$1|ליום אחד|ליומיים|ל־$1 ימים}} לכל היותר)",
        "userlogin-remembermypassword": "לזכור שנכנסתי",
        "userlogin-signwithsecure": "שימוש בחיבור מאובטח",
+       "cannotloginnow-title": "לא ניתן להיכנס עכשיו",
+       "cannotloginnow-text": "הכניסה אינה אפשרית בעת שימוש ב{{GRAMMAR:תחילית|$1}}.",
        "yourdomainname": "המתחם שלך:",
        "password-change-forbidden": "אין באפשרותך לשנות סיסמאות באתר זה.",
        "externaldberror": "אירעה שגיאת אימות בבסיס הנתונים, או שאינך מורשה לעדכן את החשבון החיצוני שלך.",
        "resetpass_submit": "הגדרת הסיסמה וכניסה לחשבון",
        "changepassword-success": "סיסמתך שונתה בהצלחה!",
        "changepassword-throttled": "ביצעתם לאחרונה ניסיונות רבים מדי להיכנס לחשבון זה.\nאנא המתינו $1 לפני שתנסו שוב.",
+       "botpasswords": "ססמאות בוט",
+       "botpasswords-summary": "<em>ססמאות בוט</em> מאפשרות כנסה לחשבון משתמש דרך API ללא שימוש בנתוני ההזדהות הראשיים של החשבון. הרשאות המשתמש שזמינות למשתמש שנכנס עם ססמת בוט יכולות להיות מוגבלות.",
+       "botpasswords-disabled": "ססמאות בוט כבויות.",
+       "botpasswords-no-central-id": "כדי להשתמש בססמאות בוט, יש להיכנס לחשבון מרכזי.",
+       "botpasswords-existing": "ססמאות בוט קיימות",
+       "botpasswords-createnew": "יצירת ססמת בוט חדשה",
+       "botpasswords-editexisting": "עריכת ססמת בוט קיימת",
+       "botpasswords-label-appid": "שם הבוט:",
+       "botpasswords-label-create": "יצירה",
+       "botpasswords-label-update": "עדכון",
+       "botpasswords-label-cancel": "ביטול",
+       "botpasswords-label-delete": "מחיקה",
+       "botpasswords-label-resetpassword": "איפוס ססמה",
+       "botpasswords-label-grants": "זיכיונות מתאימים",
+       "botpasswords-help-grants": "כל זיכיון נותן גישה להרשאות משתמש רשומות שכבר יש לחשבון המשתמש. ר' את [[Special:ListGrants|טבלת הזיכיונות]] למידע נוסף.",
+       "botpasswords-label-restrictions": "הגבלות שימוש:",
+       "botpasswords-label-grants-column": "ניתן זיכיון",
+       "botpasswords-bad-appid": "שם הבטו \"$1\" אינו תקין.",
+       "botpasswords-insert-failed": "הוספת שם הבוט \"$1\" נכשלה. האם הוא כבר נוסף?",
+       "botpasswords-update-failed": "לא היה אפשר לעדכן את שם הבוט \"$1\". האם הוא נמחק?",
+       "botpasswords-created-title": "ססמת בוט נוצרה",
+       "botpasswords-created-body": "ססמת הבוט \"$1\" נוצרה בהצלחה.",
+       "botpasswords-updated-title": "ססמת הבוט עודכנה",
+       "botpasswords-updated-body": "ססמת הבוט \"$1\" עודכנה בהצלחה.",
+       "botpasswords-deleted-title": "ססמת הבוט נמחקה",
+       "botpasswords-deleted-body": "ססמת הבוט \"$1\" נמחקה.",
+       "botpasswords-newpassword": "הססמה החדשה לכניסה <strong>$1</strong> היא <strong>$2</strong>. נא לרשום את זה לעיון עתידי.",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider אינו זמין.",
+       "botpasswords-restriction-failed": "הגבלות של ססמת בוט מונעות את הכניסה הזאת.",
+       "botpasswords-invalid-name": "שם המשתמש שניתן אינו מכיל את פריד ססמאות הבוט (\"$1\").",
+       "botpasswords-not-exist": "למשתמש \"$1\" אין ססמת בוט בשם \"$2\".",
        "resetpass_forbidden": "לא ניתן לשנות סיסמאות.",
        "resetpass-no-info": "נדרשת כניסה לחשבון כדי לגשת לדף זה באופן ישיר.",
        "resetpass-submit-loggedin": "שינוי סיסמה",
        "right-createpage": "יצירת דפים שאינם דפי שיחה",
        "right-createtalk": "יצירת דפי שיחה",
        "right-createaccount": "יצירת חשבונות משתמש חדשים",
+       "right-autocreateaccount": "כניסה אוטומטית עם חשבון משתמש חיצוני",
        "right-minoredit": "סימון עריכות כמשניות",
        "right-move": "העברת דפים",
        "right-move-subpages": "העברת דפים עם דפי המשנה שלהם",
        "right-managechangetags": "יצירת ומחיקת [[Special:Tags|תגיות]] מבסיס הנתונים",
        "right-applychangetags": "החלת [[Special:Tags|תגיות]] יחד עם שינויים",
        "right-changetags": "הוספת והסרה של [[Special:Tags|תגיות]] כלשהן לגרסאות מסוימות ולרשומות יומן",
+       "grant-generic": "חבילת ההרשאות \"$1\"",
+       "grant-group-page-interaction": "פעילות הידודית בדפים",
+       "grant-group-file-interaction": "פעילות הידודית במדיה",
+       "grant-group-watchlist-interaction": "פעילות הידודית ברשימת המעקב שלך",
+       "grant-group-email": "שליחת דוא\"ל",
+       "grant-group-high-volume": "ביצוי פעולות מרובות",
+       "grant-group-customization": "התאמה אישית והעדפות",
+       "grant-group-administration": "ביצוע פעולות ניהול",
+       "grant-group-other": "פעילות שונה",
+       "grant-blockusers": "חסימת משתמשים ושחרורם",
+       "grant-createaccount": "יצירת חשבונות",
+       "grant-createeditmovepage": "יצירה, עריכה והעברה של דפים",
+       "grant-delete": "מחיקת דפים, גרסאות ועיולי יומן",
+       "grant-editinterface": "עריכת מרחב השם מדיה ויקי ו־CSS/JavaScript של משתמשים",
+       "grant-editmycssjs": "עריכת CSS/JavaScript שלך",
+       "grant-editmyoptions": "עריכת העדפות המשתמש שלך",
+       "grant-editmywatchlist": "עריכת רשימת המעקב שלך",
+       "grant-editpage": "עריכת דפים קיימים",
+       "grant-editprotected": "עריכת דפים מוגנים",
+       "grant-highvolume": "ביצוע עריכות מרובות",
+       "grant-oversight": "החבאת משתמשים והעלמת גרסאות",
+       "grant-patrol": "ניטור שינויים לדפים",
+       "grant-protect": "הפעלת הגנה על דפים והסרתה",
+       "grant-rollback": "שחזור שינויים בדפים",
+       "grant-sendemail": "שליחת דואר אלקטרוני למשתמשים אחרים",
+       "grant-uploadeditmovefile": "העלאת קבצים, החלפתם והעברתם.",
+       "grant-uploadfile": "העלאת קבצים חדשים",
+       "grant-basic": "הרשאות בסיסיות",
+       "grant-viewdeleted": "צפייה בקבצים ודפים שנמחקו",
+       "grant-viewmywatchlist": "צפייה ברשימת מעקב שלך",
        "newuserlogpage": "יומן רישום משתמשים",
        "newuserlogpagetext": "זהו יומן המכיל הרשמות של משתמשים.",
        "rightslog": "יומן תפקידים",
        "action-createpage": "ליצור דפים",
        "action-createtalk": "ליצור דפי שיחה",
        "action-createaccount": "ליצור את חשבון המשתמש הזה",
+       "action-autocreateaccount": "ליצור את חשבון המשתמש החיצוני הזה",
        "action-history": "לצפות בהיסטוריה של דף זה",
        "action-minoredit": "לסמן עריכה זו כמשנית",
        "action-move": "להעביר דף זה",
        "listgrouprights-namespaceprotection-header": "הגבלות על מרחבי שם",
        "listgrouprights-namespaceprotection-namespace": "מרחב השם",
        "listgrouprights-namespaceprotection-restrictedto": "ההרשאה או ההרשאות המאפשרות למשתמשים לערוך",
+       "listgrants": "זיכיונות",
+       "listgrants-summary": "להלן רשימת זיכיונות עם הגישות להרשאות משתמש שמשויכות אליהן. משתמשים יכולים לאשר ליישומים להשתמש בחשבון שלהם, אבל עם הרשאות מוגבלות בהתאם לזיכיון שהשמשתמש נתן ליישום. יישום שפועל בשמו של המשתמש אינו יכול להשתמש בהרשאות שאין למשתמש.\nייתכן שיש [[{{MediaWiki:Listgrouprights-helppage}}|מידע נוסף]] על הרשאות פרטניות.",
+       "listgrants-grant": "זיכיון",
+       "listgrants-rights": "הרשאות",
        "trackingcategories": "קטגוריות מעקב",
        "trackingcategories-summary": "דף זה כולל רשימה של קטגוריות מעקב, שנוצרות אוטומטית על־ידי תוכנת מדיה‏‏־ויקי. ניתן לשנות את שמותיהן על‏‏־ידי שינוי הודעות המערכת הרלוונטיות במרחב השם \"{{ns:8}}\".",
        "trackingcategories-msg": "קטגוריית מעקב",
        "javascripttest-pagetext-frameworks": "נא לבחור אחת מסביבות הבדיקות הבאות: $1",
        "javascripttest-pagetext-skins": "בחירת עיצוב שאיתו יורצו הבדיקות:",
        "javascripttest-qunit-intro": "ראו את [$1 תיעוד הבדיקות] באתר mediawiki.org.",
-       "tooltip-pt-userpage": "דף המשתמש שלך",
+       "tooltip-pt-userpage": "דף {{GENDER:|המשתמש|המשתמשת}} שלך",
        "tooltip-pt-anonuserpage": "דף המשתמש של משתמש אנונימי זה",
        "tooltip-pt-mytalk": "דף השיחה שלך",
        "tooltip-pt-anontalk": "שיחה על תרומות המשתמש האנונימי",
        "tooltip-t-recentchangeslinked": "השינויים האחרונים שבוצעו בדפים המקושרים מדף זה",
        "tooltip-feed-rss": "הזנת RSS עבור דף זה",
        "tooltip-feed-atom": "הזנת Atom עבור דף זה",
-       "tooltip-t-contributions": "תרומותיו של משתמש זה",
-       "tooltip-t-emailuser": "שליחת דואר אלקטרוני למשתמש זה",
+       "tooltip-t-contributions": "{{GENDER:$1|תרומותיו של משתמש זה|תרומותיה של משתמשת זו}}",
+       "tooltip-t-emailuser": "שליחת דואר אלקטרוני {{GENDER:$1|למשתמש זה|למשתמשת זו}}",
        "tooltip-t-info": "מידע נוסף על דף זה",
        "tooltip-t-upload": "העלאת קבצים",
        "tooltip-t-specialpages": "רשימת כל הדפים המיוחדים",
        "mw-widgets-dateinput-no-date": "לא נבחר תאריך",
        "mw-widgets-titleinput-description-new-page": "הדף עדיין לא קיים",
        "mw-widgets-titleinput-description-redirect": "הפניה ל{{GRAMMAR:תחילית|$1}}",
-       "api-error-blacklisted": "נא לבחור כותרת אחרת, המתארת טוב יותר את התוכן."
+       "api-error-blacklisted": "נא לבחור כותרת אחרת, המתארת טוב יותר את התוכן.",
+       "sessionmanager-tie": "לא ניתן לצרף מספר סוגי אימות זהות: $1.",
+       "sessionprovider-generic": "התחברויות של $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "התחברויות מבוססות־עוגיות",
+       "sessionprovider-nocookies": "אפשר לכבות עוגיות. יש לוודא שהעוגיות מופעלות ולהתחיל מחדש."
 }
index 82a42ee..c6f2924 100644 (file)
@@ -68,7 +68,8 @@
                        "Matma Rex",
                        "Angpradesh",
                        "Sfic",
-                       "Niharika29"
+                       "Niharika29",
+                       "जनक राज भट्ट"
                ]
        },
        "tog-underline": "कड़ियाँ अधोरेखन:",
        "hebrew-calendar-m11-gen": "एवी (Av)",
        "hebrew-calendar-m12-gen": "एलुल (Elul)",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|वार्ता]])",
+       "timezone-local": "स्थानीय",
        "duplicate-defaultsort": "'''Warning:''' पुरानी मूल क्रमांकन कुंजी \"$1\" के बजाय अब मूल क्रमांकन कुंजी \"$2\" होगी।",
        "duplicate-displaytitle": "<strong>चेतावनी:</strong> शीर्षक दिखाएँ \"$2\" पूर्व दिखाए गए शीर्षक \"$1\" पर छा रहा है।",
        "invalid-indicator-name": "<strong>त्रुटि:</strong> पृष्ठ स्थिति सांकेतक <code>नाम</code> गुण खाली नहीं रहना चाहिए।",
index 19062c2..3a75dfe 100644 (file)
        "right-override-export-depth": "Izvezi stranice uključujući i povezane stranice do dubine od 5",
        "right-sendemail": "Slanje e-pošte drugim suradnicima",
        "right-passwordreset": "Vidi poruku e-pošte o ponovnom postavljanju lozinke",
+       "grant-highvolume": "Uređivanja velikog opsega",
        "newuserlogpage": "Evidencija novih suradnika",
        "newuserlogpagetext": "Ispod je popis nedavno otvorenih suradničkih imena.",
        "rightslog": "Evidencija suradničkih prava",
index 5c205b4..6637f25 100644 (file)
@@ -41,7 +41,8 @@
                        "Macofe",
                        "Nyuszika7H",
                        "Matma Rex",
-                       "JulesWinnfield-hu"
+                       "JulesWinnfield-hu",
+                       "Bencoke"
                ]
        },
        "tog-underline": "Hivatkozások aláhúzása:",
        "october-date": "Október $1",
        "november-date": "November $1",
        "december-date": "December $1",
-       "pagecategories": "{{PLURAL:$1|Kategória|Kategória}}",
+       "pagecategories": "{{PLURAL:$1|Kategória|Kategóriák}}",
        "category_header": "A(z) „$1” kategóriába tartozó lapok",
        "subcategories": "Alkategóriák",
        "category-media-header": "A(z) „$1” kategóriába tartozó médiafájlok",
        "category-empty": "''Ebben a kategóriában pillanatnyilag egyetlen lap vagy médiafájl sem szerepel.''",
        "hidden-categories": "{{PLURAL:$1|Rejtett kategória|Rejtett kategóriák}}",
        "hidden-category-category": "Rejtett kategóriák",
-       "category-subcat-count": "''{{PLURAL:$2|Ennek a kategóriának csak egyetlen alkategóriája van.|Ez a kategória az alábbi {{PLURAL:$1|alkategóriával|$1 alkategóriával}} rendelkezik (összesen $2 alkategóriája van).}}''",
+       "category-subcat-count": "{{PLURAL:$2|Ennek a kategóriának csak egyetlen alkategóriája van.|Ez a kategória az alábbi {{PLURAL:$1|alkategóriával|$1 alkategóriával}} rendelkezik (összesen $2 alkategóriája van).}}",
        "category-subcat-count-limited": "Ebben a kategóriában {{PLURAL:$1|egy|$1}} alkategória található.",
-       "category-article-count": "{{PLURAL:$2|A kategóriában csak a következő lap található.|A következő $1 lap található a kategóriában, összesen $2 lapból.}}",
+       "category-article-count": "{{PLURAL:$2|A kategóriában csak a következő lap található.|A következő {{PLURAL:$2|lap|$1 lap}} található a kategóriában, összesen $2 lapból.}}",
        "category-article-count-limited": "Ebben a kategóriában a következő {{PLURAL:$1|lap|$1 lap}} található:",
        "category-file-count": "{{PLURAL:$2|Csak a következő fájl található ebben a kategóriában.|Az összesen $2 fájlból a következő $1-t listázza ez a kategórialap, a többi a további oldalakon található.}}",
        "category-file-count-limited": "Ebben a kategóriában {{PLURAL:$1|egy|$1}} fájl található.",
        "resetpass_submit": "Add meg a jelszót és jelentkezz be",
        "changepassword-success": "A jelszavad megváltoztatása sikeresen befejeződött!",
        "changepassword-throttled": "Túl sok hibás bejelentkezés.\nVárj $1, mielőtt újra próbálkozol.",
+       "botpasswords-label-delete": "Törlés",
        "resetpass_forbidden": "A jelszavak nem változtathatók meg",
        "resetpass-no-info": "Be kell jelentkezned, hogy közvetlenül elérd ezt a lapot.",
        "resetpass-submit-loggedin": "Jelszó megváltoztatása",
        "passwordreset-emailtext-user": "$1 felhasználó jelszó-visszaállítást kért a {{SITENAME}} ($4) oldalon felvett {{PLURAL:$3|fiókban|fiókokban}}. A következő felhasználói {{PLURAL:$3|fiók van|fiókok vannak}} hozzárendelve ehhez az e-mail címhez:\n\n$2\n\n{{PLURAL:$3|Ez az ideiglenes jelszó|Ezek az ideiglenes jelszavak}} $5 nap múlva {{PLURAL:$3|jár|járnak}} le. Jelentkezz be, és cseréld le a jelszavadat. Ha valaki más kérte az emlékeztetőt, vagy eszedbe jutott a régi jelszó, és nem akarod lecserélni a jelszavadat, hagyd figyelmen kívül ezt az üzenetet, és használd a régi jelszavadat.",
        "passwordreset-emailelement": "Felhasználónév: \n$1\n\nIdeiglenes jelszó: \n$2",
        "passwordreset-emailsentemail": "Ha ez az e-mail-cím van a fiókodhoz társítva, egy jelszó-visszaállító e-mailt küldünk.",
-       "passwordreset-emailsentusername": "Ha meg lett adva email-cím ehhez a felhasználónévhez, akkor egy visszaállító emailt küld a rendszer.",
+       "passwordreset-emailsentusername": "Ha ehhez a felhasználónévhez tartozik e-mail cím, akkor egy jelszó-visszaállító levelet küld a rendszer.",
        "passwordreset-emailsent-capture": "Az alább látható jelszó-visszaállító e-mail lett elküldve.",
        "passwordreset-emailerror-capture": "A jelszó-visszaállító e-mail generálása megtörtént, mint az alább látszik, de elküldése a {{GENDER:$2|szerkesztőnek}} nem sikerült: $1",
        "changeemail": "E-mail cím megváltoztatása vagy eltávolítása",
        "currentrev-asof": "A lap jelenlegi, $1-kori változata",
        "revisionasof": "A lap $1-kori változata",
        "revision-info": "A lap korábbi változatát látod, amilyen {{GENDER:$6|$2}} $1-kor történt szerkesztése után volt.$7",
-       "previousrevision": "←Régebbi változat",
+       "previousrevision": "← Régebbi változat",
        "nextrevision": "Újabb változat→",
        "currentrevisionlink": "Aktuális változat",
        "cur": "akt",
        "titlematches": "Címbeli egyezések",
        "textmatches": "Szövegbeli egyezések",
        "notextmatches": "Nincsenek szövegbeli egyezések",
-       "prevn": "előző {{PLURAL:$1|egy|$1}}",
-       "nextn": "következő {{PLURAL:$1|egy|$1}}",
+       "prevn": "előző $1",
+       "nextn": "következő $1",
        "prev-page": "előző oldal",
        "next-page": "következő oldal",
        "prevn-title": "Előző {{PLURAL:$1|egy|$1}} találat",
        "nextn-title": "Következő {{PLURAL:$1|egy|$1}} találat",
-       "shown-title": "{{PLURAL:$1|Egy|$1}} találat laponként",
+       "shown-title": "$1 találat megjelenítése laponként",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''A wikin már van „[[:$1]]” nevű lap'''",
        "searchmenu-new": "<strong>Hozd létre a(z) „[[:$1]]” nevű lapot ezen a wikin!</strong>\n{{PLURAL:$2|0=|Lásd még a keresés során talált lapot.|Lásd még a keresés eredményét.}}",
        "searchprofile-images-tooltip": "Fájlok keresése",
        "searchprofile-everything-tooltip": "Minden névtérben keres (a vitalapokat is beleértve)",
        "searchprofile-advanced-tooltip": "Keresés adott névterekben",
-       "search-result-size": "$1 ({{PLURAL:$2|egy|$2}} szó)",
+       "search-result-size": "$1 ($2 szó)",
        "search-result-category-size": "$1 oldal, $2 alkategória, $3 fájl",
        "search-redirect": "(átirányítva innen: $1)",
        "search-section": "($1 szakasz)",
        "prefs-help-signature": "A vitalapra írt hozzászólásaidat négy hullámvonallal (<nowiki>~~~~</nowiki>) írd alá. A lap mentésekor ez lecserélődik az aláírásodra és egy időbélyegre.",
        "badsig": "Érvénytelen aláírás; ellenőrizd a HTML-formázást.",
        "badsiglength": "Az aláírásod túl hosszú.\n{{PLURAL:$1|Egy|$1}} karakternél rövidebbnek kell lennie.",
-       "yourgender": "Biológiai nem:",
+       "yourgender": "Milyen neműként hivatkozzunk rád?",
        "gender-unknown": "Amikor rólad van szó, a szoftver semleges szavakat fog használni, ha lehetséges",
-       "gender-male": "Férfi",
-       "gender-female": "Nő",
+       "gender-male": "Férfiként",
+       "gender-female": "Nőként",
        "prefs-help-gender": "Nem kötelező: a szoftver használja a nemtől függő üzenetek megjelenítéséhez. Az információ mindenki számára látható.",
        "email": "E-mail",
        "prefs-help-realname": "A valódi név nem kötelező.\nHa megadod, akkor leszel feltüntetve a munkád szerzőjeként.",
        "right-managechangetags": "Adatbázis [[Special:Tags|címkék]] létrehozása és törlése",
        "right-applychangetags": "[[Special:Tags|címkék]] alkalmazása a változakra",
        "right-changetags": "Egyedi változtatásokon és napló bejegyzéseken tetszőleges [[Special:Tags|címkék]] hozzáadása és törlése",
+       "grant-generic": "„$1” jogosultságcsomag",
+       "grant-group-email": "E-mail küldése",
+       "grant-group-high-volume": "Nagy mennyiségű szerkesztés végrehajtása",
+       "grant-group-customization": "Személyre szabás és beállítások",
+       "grant-group-administration": "Adminisztratív műveletek végrehajtása",
+       "grant-blockusers": "felhasználók blokkolása és blokk feloldása",
+       "grant-createaccount": "fiókok létrehozása",
+       "grant-createeditmovepage": "Lapok készítése, szerkesztése és átnevezése",
+       "grant-delete": "lapok, lapváltozatok és naplóbejegyzések törlése",
+       "grant-editinterface": "MediaWiki-névtér és felhasználói CSS/JavaScript szerkesztése",
+       "grant-editmycssjs": "Felhasználói CSS-ed/JavaScripted szerkesztése",
+       "grant-editmyoptions": "Felhasználói beállításaid szerkesztése",
+       "grant-editmywatchlist": "figyelőlista szerkesztése",
+       "grant-editpage": "létező lapok szerkesztése",
+       "grant-editprotected": "védett lapok szerkesztése",
+       "grant-highvolume": "nagy mennyiségű szerkesztés",
+       "grant-oversight": "felhasználók és lapváltozatok elrejtése",
+       "grant-patrol": "szerkesztések ellenőrzése",
+       "grant-protect": "lapok védelme és védelem feloldása",
+       "grant-rollback": "szerkesztések gyors visszaállítása",
+       "grant-sendemail": "e-mail küldése más felhasználóknak",
+       "grant-uploadeditmovefile": "fájlok feltöltése, felülírása és átnevezése",
+       "grant-uploadfile": "fájlok feltöltése",
+       "grant-viewdeleted": "Törölt fájlok és lapok megtekintése",
+       "grant-viewmywatchlist": "figyelőlista megtekintése",
        "newuserlogpage": "Új szerkesztők naplója",
        "newuserlogpagetext": "Ez a napló az újonnan regisztrált szerkesztők listáját tartalmazza.",
        "rightslog": "Szerkesztői jogosultságok naplója",
        "recentchanges-label-bot": "Ezt a szerkesztést egy bot hajtotta végre",
        "recentchanges-label-unpatrolled": "Ezt a szerkesztést még nem ellenőrizték",
        "recentchanges-label-plusminus": "Az oldal mérete ennyi bájttal módosult",
-       "recentchanges-legend-heading": "Jelmagyarázat:",
+       "recentchanges-legend-heading": "'''Jelmagyarázat:'''",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (lásd még: [[Special:NewPages|új lapok listája]])",
        "recentchanges-submit": "Megjelenítés",
        "rcnotefrom": "Alább a <strong>$3 $4</strong> óta történt változtatások láthatóak (legfeljebb <b>$1</b> db).",
-       "rclistfrom": "$3 $2 után történt változtatások megtekintése",
+       "rclistfrom": "$3, $2 után történt változtatások megtekintése",
        "rcshowhideminor": "apró szerkesztések $1",
        "rcshowhideminor-show": "megjelenítése",
        "rcshowhideminor-hide": "elrejtése",
        "number_of_watching_users_pageview": "[Jelenleg {{PLURAL:$1|egy|$1}} felhasználó figyeli]",
        "rc_categories": "Szűkítés kategóriákra („|” jellel válaszd el őket)",
        "rc_categories_any": "Bármelyik",
-       "rc-change-size-new": "{{PLURAL:$1| egy bájt|$1 bájt}} módosítás után",
+       "rc-change-size-new": "$1 bájt módosítás után",
        "newsectionsummary": "/* $1 */ (új szakasz)",
        "rc-enhanced-expand": "Részletek megjelenítése",
        "rc-enhanced-hide": "Részletek elrejtése",
        "withoutinterwiki-legend": "Előtag",
        "withoutinterwiki-submit": "Megjelenítés",
        "fewestrevisions": "Legrövidebb laptörténetű lapok",
-       "nbytes": "{{PLURAL:$1|egy|$1}} bájt",
+       "nbytes": "$1 bájt",
        "ncategories": "$1 kategória",
        "ninterwikis": "{{PLURAL:$1|egy|$1}} interwiki",
        "nlinks": "{{PLURAL:$1|egy|$1}} hivatkozás",
        "allpagesto": "Lapok listázása a következő címig:",
        "allarticles": "Az összes lap listája",
        "allinnamespace": "Összes lap ($1 névtér)",
-       "allpagessubmit": "Keresés",
+       "allpagessubmit": "Menj",
        "allpagesprefix": "Lapok listázása, amik ezzel az előtaggal kezdődnek:",
        "allpagesbadtitle": "A megadott lapnév nyelvközi vagy wikiközi előtagot tartalmazott, vagy érvénytelen volt. Talán olyan karakter van benne, amit nem lehet lapnevekben használni.",
        "allpages-bad-ns": "A(z) {{SITENAME}} webhelyen nincs \"$1\" névtér.",
        "listgrouprights-namespaceprotection-header": "Névtér korlátozások",
        "listgrouprights-namespaceprotection-namespace": "Névtér",
        "listgrouprights-namespaceprotection-restrictedto": "A szerkesztéshez szükséges jogosultság(ok)",
+       "listgrants-summary": "Lenn láthatóak az OAuth által használt jogosultsági szintek, valamint az azokhoz tartozó jogok. A felhasználók engedélyezhetnek alkalmazásokat, hogy használják a fiókjukat, de csak korlátozott engedélyekkel, amelyek a felhasználó által engedélyezett jogosultsági szinteken alapulnak. Egy ilyen alkalmazás nem használhatja ezeket a jogokat, ha azokkal a felhasználó sem rendelkezik.\nLehetnek [[{{MediaWiki:Listgrouprights-helppage}}|további információk]] az egyes jogokról.",
+       "listgrants-grant": "Jogosultsági szint",
+       "listgrants-rights": "Jogok",
        "trackingcategories": "Nyomkövető kategóriák",
        "trackingcategories-summary": "Ez az oldal azokat a nyomkövető kategóriákat tartalmazza, amelyet a MediaWiki szoftver magától feltölt. Ezen neveket a megfelelő rendszer üzenet módosításával lehet megváltoztatni a {{ns:8}} névtérben.",
        "trackingcategories-msg": "Nyomkövető kategória",
        "watchlist-hide": "Elrejtés",
        "watchlist-submit": "Megjelenítés",
        "wlshowtime": "Időszak:",
-       "wlshowhideminor": "apró",
+       "wlshowhideminor": "apró szerkesztések",
        "wlshowhidebots": "botok",
-       "wlshowhideliu": "bejelentkezett",
-       "wlshowhideanons": "névtelen",
-       "wlshowhidemine": "saját",
+       "wlshowhideliu": "bejelentkezett felhasználók",
+       "wlshowhideanons": "névtelen felhasználók",
+       "wlshowhidemine": "saját szerkesztéseim",
        "watchlist-options": "A figyelőlista beállításai",
        "watching": "Figyelés...",
        "unwatching": "Figyelés befejezése...",
        "tooltip-invert": "Pipáld ki a dobozt, ha el szeretnéd rejteni a kiválasztott névterekben történt változtatásokat (és a kapcsolódó névterekben, amennyiben úgy van beállítva)",
        "tooltip-whatlinkshere-invert": "Pipáld ki a dobozt, ha el szeretnéd rejteni a kiválasztott névterekben található hivatkozásokat.",
        "namespace_association": "Kapcsolódó névtér",
-       "tooltip-namespace_association": "Pipáld ki ezt a dobozt, ha a kiválasztott névtérhez tartozó vita- vagy tárgynévteret is bele szeretnéd venni.",
+       "tooltip-namespace_association": "Pipáld ki ezt a dobozt, ha a kiválasztott névtérhez tartozó vita- vagy tartalmi névteret is bele szeretnéd venni.",
        "blanknamespace": "(Fő)",
        "contributions": "{{GENDER:$1|Szerkesztő}} közreműködései",
        "contributions-title": "$1 közreműködései",
        "allmessages-language": "Nyelv:",
        "allmessages-filter-submit": "Szűrés",
        "allmessages-filter-translate": "Fordítás",
-       "thumbnail-more": "A kép nagyítása",
+       "thumbnail-more": "Nagyítás",
        "filemissing": "A fájl nincs meg",
        "thumbnail_error": "Hiba a bélyegkép létrehozásakor: $1",
        "thumbnail_error_remote": "Hiba üzenet $1-tól: $2",
        "tooltip-n-currentevents": "Háttérinformáció az aktuális eseményekről",
        "tooltip-n-recentchanges": "A wikiben történt legutóbbi változtatások listája",
        "tooltip-n-randompage": "Egy véletlenszerűen kiválasztott lap betöltése",
-       "tooltip-n-help": "Ha bármi problémád van...",
+       "tooltip-n-help": "Ha bármi problémád van",
        "tooltip-t-whatlinkshere": "Az erre a lapra hivatkozó más lapok listája",
        "tooltip-t-recentchangeslinked": "Az erről a lapról hivatkozott lapok utolsó változtatásai",
        "tooltip-feed-rss": "A lap tartalma RSS hírcsatorna formájában",
        "tooltip-feed-atom": "A lap tartalma Atom hírcsatorna formájában",
-       "tooltip-t-contributions": "A felhasználó közreműködéseinek listája",
+       "tooltip-t-contributions": "A {{GENDER:$1|felhasználó}} közreműködéseinek listája",
        "tooltip-t-emailuser": "Írj levelet ennek a felhasználónak!",
        "tooltip-t-info": "További információk erről a lapról",
        "tooltip-t-upload": "Képek vagy egyéb fájlok feltöltése",
        "exif-urgency-low": "Alacsony ($1)",
        "exif-urgency-high": "Magas ($1)",
        "exif-urgency-other": "Egyedi prioritás ($1)",
-       "namespacesall": "Ã\96sszes",
+       "namespacesall": "Ãsszes",
        "monthsall": "mind",
        "confirmemail": "E-mail cím megerősítése",
        "confirmemail_noemail": "Nincs érvényes e-mail cím megadva a [[Special:Preferences|beállításaidnál]].",
        "log-name-pagelang": "Nyelvváltoztatások naplója",
        "log-description-pagelang": "Ebben a naplóban a lap nyelvének változásait követheted nyomon.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|megváltoztatta}} a(z) $3 lap nyelvét $4 nyelvről $5 nyelvre.",
-       "default-skin-not-found": "Upsz! A wiki alapértelmezett felülete, amely a <code dir=\"ltr\">$wgDefaultSkin</code> szerint <code>$1</code>, nem áll rendelkezésre.\n\nA telepítés az alábbi {{PLURAL:$4|felületet|felületeket}} tartalmazza.\nTovábbi információkat a felület konfigurálásáról és az alapértelmezett felület beállításáról  a [https://www.mediawiki.org/wiki/Manual:Skin_configuration Kézikönyv: Felület konfigurálása] helyen találsz.\n\n$2\n\n; Ha frissen telepítetted a MediaWikit:\n: Valószínűleg a git-ről telepítetted, vagy forrás kódból más módon. Ebben az esetben ez várható. Próbálj telepíteni felületeket a [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org felület könyvtárából], az alábbi módokon:\n:* Töltsd le a [https://www.mediawiki.org/wiki/Download tarball telepítőt], amely számos felületet és kiegészítést tartalmaz. Simán másold ki és beilleszt be a <code>skins/</code> könyvtárat belőle.\n:* Töltsd le az egyedi felület telepítő készleteket a [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] helyről.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Felület letöltése Git segítségével].\n: Ez nem ütközik a git repository-val, ha MediaWiki fejlesztő vagy.\n\n; Ha most frissítetted a MediaWikit:\n: MediaWiki 1.24 és újabb verziók már nem engedélyezik automatikusan a felületeket (lásd [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Kézikönyv: Felület automatikus felderítése]). Illeszd be az alábbi {{PLURAL:$5|sort|sorokat}} a  <code>LocalSettings.php</code> fájlba, ha engedélyezni akarod {{PLURAL:$5|a|az összes}} telepített felületet:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Ha most módosítottad a <code>LocalSettings.php</code> fájlt:\n: Ellenőrizd a felület neveket, mert lehet, hogy elírtad!",
+       "default-skin-not-found": "Upsz! A wiki alapértelmezett felülete, amely a <code dir=\"ltr\">$wgDefaultSkin</code> szerint <code>$1</code>, nem áll rendelkezésre.\n\nA telepítés az alábbi {{PLURAL:$4|felületet|felületeket}} tartalmazza.\nTovábbi információkat a felület konfigurálásáról és az alapértelmezett felület beállításáról a [https://www.mediawiki.org/wiki/Manual:Skin_configuration Kézikönyv: Felület konfigurálása] helyen találsz.\n\n$2\n\n; Ha frissen telepítetted a MediaWikit:\n: Valószínűleg a gitről telepítetted, vagy közvetlenül forráskódból más módon. Ebben az esetben ez várható. Próbálj telepíteni felületeket a [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org felületkönyvtárából], az alábbi módokon:\n:* Töltsd le a [https://www.mediawiki.org/wiki/Download tarball telepítőt], amely számos felületet és kiegészítést tartalmaz. Simán másold át belőle a <code>skins/</code> könyvtárat.\n:* Tölts le az egyedi felülettelepítő-készleteket a [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] helyről.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Felület letöltése Git segítségével].\n: Ez nem ütközik a git repository-val, ha MediaWiki fejlesztő vagy.\n\n; Ha most frissítetted a MediaWikit:\n: A MediaWiki 1.24 és újabb verziók már nem engedélyezik automatikusan a telepített felületeket (lásd [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Kézikönyv: Felület automatikus felderítése]). Illeszd be az alábbi {{PLURAL:$5|sort|sorokat}} a <code>LocalSettings.php</code> fájlba, ha engedélyezni akarod {{PLURAL:$5|a|az összes}} telepített felületet:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Ha most módosítottad a <code>LocalSettings.php</code> fájlt:\n: Ellenőrizd a felületneveket, mert lehet, hogy elírtad!",
        "default-skin-not-found-no-skins": "Upsz! A wiki alapértelmezett felülete, amely a <code dir=\"ltr\">$wgDefaultSkin</code> szerint <code>$1</code>, nem áll rendelkezésre.\n\nNincs telepített felület\n\n; Ha frissen telepítetted a MediaWikit:\n: Valószínűleg a git-ről telepítetted, vagy forrás kódból más módon. Ebben az esetben ez várható. Próbálj telepíteni felületeket a [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org felület könyvtárából], az alábbi módokon:\n:* Töltsd le a [https://www.mediawiki.org/wiki/Download tarball telepítőt], amely számos felületet és kiegészítést tartalmaz. Simán másold ki és beilleszt be a <code>skins/</code> könyvtárat belőle.\n:* Töltsd le az egyedi felület telepítő készleteket a [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] helyről.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Felület letöltése Git segítségével].\n: Ez nem ütközik a git repository-val, ha MediaWiki fejlesztő vagy.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (engedélyezve)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 ('''letiltva''')",
index 7fc28fe..db898ce 100644 (file)
        "right-sendemail": "Mengirim surel ke pengguna lain",
        "right-passwordreset": "Lihat surel pengaturulangan kata sandi",
        "right-managechangetags": "Membuat dan menghapus [[Special:Tags|tag]] dari basis data",
+       "grant-generic": "\"$1\" bundel hak akses",
+       "grant-group-page-interaction": "Berinteraksi dengan halaman",
+       "grant-group-file-interaction": "Berinteraksi dengan media",
+       "grant-group-watchlist-interaction": "Berinteraksi dengan daftar pantauan Anda",
+       "grant-group-email": "Mengirim surel",
+       "grant-group-high-volume": "Melakukan aktivitas yang amat banyak",
+       "grant-group-customization": "Kustomisasi dan preferensi",
+       "grant-group-administration": "Lakukan tindakan administratif",
+       "grant-group-other": "Aktivitas lain-lain",
+       "grant-blockusers": "Blokir dan buka pemblokiran pengguna",
+       "grant-createaccount": "Buat akun",
+       "grant-createeditmovepage": "Membuat, menyunting dan memindahkan halaman",
+       "grant-delete": "Menghapus halaman, revisi, dan log entri",
+       "grant-editinterface": "Menyunting ruang nama MediaWiki dan CSS/JavaScript pengguna",
+       "grant-editmycssjs": "Menyunting halaman CSS/JavaScript Anda",
+       "grant-editmyoptions": "Menyunting preferensi Anda",
+       "grant-editmywatchlist": "Menyunting daftar pantauan Anda",
+       "grant-editpage": "Menyunting halaman yang ada",
+       "grant-editprotected": "Menyunting halaman yang dilindungi",
+       "grant-highvolume": "Amat sering menyunting",
+       "grant-oversight": "Sembunyikan pengguna dan revisinya",
+       "grant-patrol": "Tandai halaman terpatroli",
+       "grant-protect": "Melindungi dan membuka perlindungan halaman",
+       "grant-rollback": "Membalikkan halamn",
+       "grant-sendemail": "Mengirim surel pada pengguna lain",
+       "grant-uploadeditmovefile": "Mengunggah, mengganti, dan memindahkan berkas",
+       "grant-uploadfile": "Mengunggah berkas baru",
+       "grant-viewdeleted": "Melihat informasi halaman yang dihapus",
+       "grant-viewmywatchlist": "Melihat daftar pantauan Anda",
        "newuserlogpage": "Log pengguna baru",
        "newuserlogpagetext": "Di bawah ini adalah log pendaftaran pengguna baru",
        "rightslog": "Log perubahan hak akses",
        "listgrouprights-namespaceprotection-header": "Batasan ruang nama",
        "listgrouprights-namespaceprotection-namespace": "Ruang nama",
        "listgrouprights-namespaceprotection-restrictedto": "Hak yang mengizinkan pengguna untuk menyunting",
+       "listgrants-grant": "Izin",
+       "listgrants-rights": "Hak",
        "trackingcategories": "Kategori pelacak",
        "trackingcategories-summary": "Halaman ini mendaftarkan kategori-kategori pelacak yang secara otomatis dibuat oleh perangkat lunak MediaWiki. Nama kategori ini dapat diubah dengan mengubah pesan sistem yang sesuai dalam ruang nama {{ns:8}}.",
        "trackingcategories-msg": "Kategori pelacak",
index 2e26bb7..fb5266d 100644 (file)
        "virus-scanfailed": "scansione fallita (codice $1)",
        "virus-unknownscanner": "antivirus sconosciuto:",
        "logouttext": "'''Logout effettuato.'''\n\nNota che alcune pagine potrebbero continuare ad apparire come se il logout non fosse avvenuto finché non viene pulita la cache del proprio browser.",
+       "cannotlogoutnow-title": "Impossibile uscire ora",
+       "cannotlogoutnow-text": "La disconnessione non è possibile quando si sta usando $1.",
        "welcomeuser": "Benvenuto, $1!",
        "welcomecreation-msg": "L'account è stato creato correttamente.\nNon dimenticare di personalizzare le [[Special:Preferences|preferenze di {{SITENAME}}]].",
        "yourname": "Nome utente:",
        "remembermypassword": "Ricorda la password su questo browser (per un massimo di $1 {{PLURAL:$1|giorno|giorni}})",
        "userlogin-remembermypassword": "Mantienimi collegato",
        "userlogin-signwithsecure": "Usa una connessione sicura",
+       "cannotloginnow-title": "Impossibile accedere ora",
+       "cannotloginnow-text": "L'accesso non è possibile quando si sta usando $1.",
        "yourdomainname": "Specificare il dominio",
        "password-change-forbidden": "Non è possibile modificare le password su questo wiki.",
        "externaldberror": "Si è verificato un errore con il server di autenticazione esterno, oppure non si dispone delle autorizzazioni necessarie per aggiornare il proprio accesso esterno.",
        "resetpass_submit": "Imposta la password e accedi al sito",
        "changepassword-success": "La password è stata modificata correttamente!",
        "changepassword-throttled": "Sono stati effettuati troppi tentativi di accesso in breve tempo.\nAttendi $1 e riprova in seguito.",
+       "botpasswords": "Password bot",
+       "botpasswords-summary": "<em>Password bot</em> consente l'accesso ad un'utenza tramite API senza usare le credenziali di accesso principali dell'utenza. I diritti utente disponibili quando si è effettuato l'accesso con una password bot possono essere limitati.\n\nSe non conosci il motivo per cui potresti farlo, allora probabilmente non dovresti farlo. Nessuno dovrebbe mai chiederti di generare uno di questi e darli a loro.",
+       "botpasswords-disabled": "Le password bot sono disabilitate.",
+       "botpasswords-no-central-id": "Per utilizzare una password bot, è necessario accedere ad un'utenza centralizzata.",
+       "botpasswords-existing": "Password bot esistenti",
+       "botpasswords-createnew": "Crea una nuova password bot",
+       "botpasswords-editexisting": "Modifica password bot esistenti",
+       "botpasswords-label-appid": "Nome bot:",
+       "botpasswords-label-create": "Crea",
+       "botpasswords-label-update": "Aggiorna",
+       "botpasswords-label-cancel": "Annulla",
+       "botpasswords-label-delete": "Cancella",
+       "botpasswords-label-resetpassword": "Reimposta la password",
+       "botpasswords-label-grants": "Assegnazioni applicabili:",
+       "botpasswords-help-grants": "Ogni assegnazione dà accesso ai diritti utente elencati che un'utenza ha già. Vedi la [[Special:ListGrants|tabella delle assegnazioni]] per ulteriori informazioni.",
+       "botpasswords-label-restrictions": "Restrizioni d'uso:",
+       "botpasswords-label-grants-column": "Assegnazioni",
+       "botpasswords-bad-appid": "Il nome bot \"$1\" non è valido.",
+       "botpasswords-insert-failed": "Impossibile aggiungere il nome bot \"$1\". È stato già aggiunto?",
+       "botpasswords-update-failed": "Impossibile aggiornare il nome bot \"$1\". È stato cancellato?",
+       "botpasswords-created-title": "Password bot creata",
+       "botpasswords-created-body": "La password bot \"$1\" è stata creata con successo.",
+       "botpasswords-updated-title": "Password bot aggiornata",
+       "botpasswords-updated-body": "La password bot \"$1\" è stata aggiornata con successo.",
+       "botpasswords-deleted-title": "Password bot cancellata",
+       "botpasswords-deleted-body": "La password bot \"$1\" è stata cancellata.",
+       "botpasswords-newpassword": "La nuova password per accedere con <strong>$1</strong> è <strong>$2</strong>. <em>Registratelo per consultazioni future.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider non è disponibile.",
+       "botpasswords-restriction-failed": "Le restrizioni di password bot impediscono questo accesso.",
+       "botpasswords-invalid-name": "Il nome utente indicato non contiene il separatore per password bot (\"$1\").",
+       "botpasswords-not-exist": "L'utente \"$1\" non dispone di una password bot chiamata \"$2\".",
        "resetpass_forbidden": "Non è possibile modificare le password",
        "resetpass-no-info": "Devi aver effettuato l'accesso per accedere a questa pagina direttamente.",
        "resetpass-submit-loggedin": "Cambia password",
        "right-createpage": "Crea pagine (escluse le pagine di discussione)",
        "right-createtalk": "Crea pagine di discussione",
        "right-createaccount": "Crea nuove utenze",
+       "right-autocreateaccount": "Accede automaticamente con un'utenza esterna",
        "right-minoredit": "Segna le modifiche come minori",
        "right-move": "Sposta le pagine",
        "right-move-subpages": "Sposta le pagine insieme alle relative sottopagine",
        "right-managechangetags": "Crea ed elimina dal database i [[Special:Tags|tag]]",
        "right-applychangetags": "Applica delle [[Special:Tags|etichette]] alle proprie modifiche",
        "right-changetags": "Aggiunge e rimuove specifiche [[Special:Tags|etichette]] su singole versioni o voci di registro",
+       "grant-generic": "Pacchetto diritti \"$1\"",
+       "grant-group-page-interaction": "Interagisce con le pagine",
+       "grant-group-file-interaction": "Interagisce con i file multimediali",
+       "grant-group-watchlist-interaction": "Interagisce con i tuoi osservati speciali",
+       "grant-group-email": "Invia email",
+       "grant-group-high-volume": "Esegue azioni massive",
+       "grant-group-customization": "Personalizzazione e preferenze",
+       "grant-group-administration": "Esegue azioni amministrative",
+       "grant-group-other": "Attività varie",
+       "grant-blockusers": "Blocca e sblocca utenti",
+       "grant-createaccount": "Crea un'utenza",
+       "grant-createeditmovepage": "Crea, modifica e sposta le pagine",
+       "grant-delete": "Cancella pagine, versioni, e voci di registro",
+       "grant-editinterface": "Modifica il namespace MediaWiki e i CSS/JavaScript degli utenti",
+       "grant-editmycssjs": "Modifica i CSS/JavaScript della tua utenza",
+       "grant-editmyoptions": "Modifica le preferenze della tua utenza",
+       "grant-editmywatchlist": "Modifica i tuoi osservati speciali",
+       "grant-editpage": "Modifica pagine esistenti",
+       "grant-editprotected": "Modifica pagine protette",
+       "grant-highvolume": "Modifiche massive",
+       "grant-oversight": "Nasconde utenti e sopprime le versioni",
+       "grant-patrol": "Segna le modifiche alle pagine come verificate",
+       "grant-protect": "Protegge e sprotegge pagine",
+       "grant-rollback": "Rollback delle modifiche alle pagine",
+       "grant-sendemail": "Invia email ad altri utenti",
+       "grant-uploadeditmovefile": "Carica, sostituisce e sposta i file",
+       "grant-uploadfile": "Carica nuovi file",
+       "grant-basic": "Diritti di base",
+       "grant-viewdeleted": "Vede i file e le pagine cancellati",
+       "grant-viewmywatchlist": "Vede i tuoi osservati speciali",
        "newuserlogpage": "Nuovi utenti",
        "newuserlogpagetext": "Di seguito sono elencate le utenze di nuova creazione.",
        "rightslog": "Diritti degli utenti",
        "action-createpage": "creare pagine",
        "action-createtalk": "creare pagine di discussione",
        "action-createaccount": "effettuare questa registrazione",
+       "action-autocreateaccount": "creare automaticamente questa utenza esterna",
        "action-history": "vedere la cronologia di questa pagina",
        "action-minoredit": "segnare questa modifica come minore",
        "action-move": "spostare questa pagina",
        "listgrouprights-namespaceprotection-header": "Restrizioni per namespace",
        "listgrouprights-namespaceprotection-namespace": "Namespace",
        "listgrouprights-namespaceprotection-restrictedto": "Diritto che consente all'utente di modificare",
+       "listgrants": "Assegnazioni",
+       "listgrants-summary": "Di seguito è riportato un elenco delle concessioni, con i loro diritti utente associati. Gli utenti possono autorizzare le applicazioni a utilizzare la propria utenza, ma con autorizzazioni limitate in base alle assegnazioni che l'utente ha dato all'applicazione. Tuttavia, un'applicazione che agisce per conto di un utente non può effettivamente utilizzare i diritti di cui l'utente non dispone.\nPuoi trovare [[{{MediaWiki:Listgrouprights-helppage}}|ulteriori informazioni]] sui diritti individuali.",
+       "listgrants-grant": "Assegnazioni",
+       "listgrants-rights": "Diritti",
        "trackingcategories": "Categorie di monitoraggio",
        "trackingcategories-summary": "Questa pagina elenca le categorie di monitoraggio che vengono popolate automaticamente dal software MediaWiki. I loro nomi possono essere cambiati modificando i relativi messaggi di sistema nel namespace {{ns:8}}.",
        "trackingcategories-msg": "Categoria di monitoraggio",
        "javascripttest-pagetext-frameworks": "Per cortesia, scegli uno dei seguenti framework per i test: $1",
        "javascripttest-pagetext-skins": "Scegli un tema con cui eseguire i test:",
        "javascripttest-qunit-intro": "Vedi su mediawiki.org la [$1 documentazione riguardante i test].",
-       "tooltip-pt-userpage": "La tua pagina utente",
+       "tooltip-pt-userpage": "La {{GENDER:|tua}} pagina utente",
        "tooltip-pt-anonuserpage": "La pagina utente di questo indirizzo IP",
-       "tooltip-pt-mytalk": "La tua pagina di discussione",
+       "tooltip-pt-mytalk": "La {{GENDER:|tua}} pagina di discussione",
        "tooltip-pt-anontalk": "Discussioni sulle modifiche fatte da questo indirizzo IP",
-       "tooltip-pt-preferences": "Le tue preferenze",
+       "tooltip-pt-preferences": "Le {{GENDER:|tue}} preferenze",
        "tooltip-pt-watchlist": "La lista delle pagine che stai tenendo sotto osservazione",
-       "tooltip-pt-mycontris": "La lista dei tuoi contributi",
+       "tooltip-pt-mycontris": "La lista dei {{GENDER:|tuoi}} contributi",
        "tooltip-pt-anoncontribs": "Un elenco delle modifiche fatte da questo indirizzo IP",
        "tooltip-pt-login": "Si consiglia di effettuare l'accesso, anche se non è obbligatorio",
        "tooltip-pt-logout": "Uscita (logout)",
        "tooltip-t-recentchangeslinked": "Elenco delle ultime modifiche alle pagine collegate a questa",
        "tooltip-feed-rss": "Feed RSS per questa pagina",
        "tooltip-feed-atom": "Feed Atom per questa pagina",
-       "tooltip-t-contributions": "Elenco dei contributi di questo utente",
-       "tooltip-t-emailuser": "Invia un messaggio email a questo utente",
+       "tooltip-t-contributions": "Elenco dei contributi di {{GENDER:$1|questo|questa}} utente",
+       "tooltip-t-emailuser": "Invia un messaggio email a {{GENDER:$1|questo|questa}} utente",
        "tooltip-t-info": "Ulteriori informazioni su questa pagina",
        "tooltip-t-upload": "Carica file multimediali",
        "tooltip-t-specialpages": "Elenco di tutte le pagine speciali",
        "expand_templates_generate_xml": "Mostra albero sintattico XML",
        "expand_templates_generate_rawhtml": "Mostra HTML",
        "expand_templates_preview": "Anteprima",
-       "expand_templates_preview_fail_html": "<em>Dato che {{SITENAME}} ha dell'HTML grezzo attivato e c'è stata una perdita dei dati della sessione, l'anteprima è nascosta per precauzione contro gli attacchi a JavaScript.</em>\n\n<strong>Se si tratta di un normale tentativo d'anteprima, riprova.</strong> \nSe comunque non dovesse funzionare, prova ad [[Special:UserLogout|uscire]] ed a rientrare.",
+       "expand_templates_preview_fail_html": "<em>Poiché {{SITENAME}} ha dell'HTML grezzo attivato e c'è stata una perdita dei dati della sessione, l'anteprima è nascosta come precauzione contro gli attacchi JavaScript.</em>\n\n<strong>Se si tratta di un normale tentativo d'anteprima, riprova.</strong> \nSe comunque non dovesse funzionare, prova ad [[Special:UserLogout|uscire]] ed a rientrare.",
        "expand_templates_preview_fail_html_anon": "<em>Poiché {{SITENAME}} ha dell'HTML grezzo attivato e non hai effettuato l'accesso, l'anteprima è nascosta come precauzione contro gli attacchi JavaScript.</em>\n\n<strong>Se si tratta di un normale tentativo d'anteprima, [[Special:UserLogin|entra]] e riprova.</strong>",
        "expand_templates_input_missing": "Devi inserire del testo come input.",
        "pagelanguage": "Seleziona lingua della pagina",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "questa pagina non esiste ancora",
        "mw-widgets-titleinput-description-redirect": "reindirizzamento a $1",
-       "api-error-blacklisted": "Per favore scegli un titolo diverso e descrittivo."
+       "api-error-blacklisted": "Per favore scegli un titolo diverso e descrittivo.",
+       "sessionmanager-tie": "Non è possibile combinare più tipi di richieste di autenticazione: $1.",
+       "sessionprovider-generic": "sessioni $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sessioni basate su cookie",
+       "sessionprovider-nocookies": "I cookie possono essere disattivati. Assicurati di avere i cookie abilitati e ha inizia nuovamente."
 }
index 94700cc..4bf10a1 100644 (file)
        "right-managechangetags": "[[Special:Tags|タグ]]のデータベースにおける作成および削除",
        "right-applychangetags": "自分の編集に[[Special:Tags|タグ]]を適用する",
        "right-changetags": "個々の版と記録項目の任意の[[Special:Tags|タグ]]の追加と削除",
+       "grant-group-email": "メールの送信",
+       "grant-group-customization": "カスタマイズと個人設定",
+       "grant-group-other": "その他の活動",
+       "grant-blockusers": "利用者をブロック/ブロック解除",
+       "grant-createaccount": "アカウントを作成",
+       "grant-createeditmovepage": "ページを作成、編集、および移動",
+       "grant-delete": "ページ、版、記録項目を削除",
+       "grant-editinterface": "MediaWiki 名前空間および利用者 CSS/JavaScript を編集",
+       "grant-editmycssjs": "あなた自身の利用者 CSS/JavaScript を編集",
+       "grant-editmyoptions": "あなたの個人設定を編集",
+       "grant-editmywatchlist": "自身のウォッチリストを編集",
+       "grant-editpage": "既存のページを編集",
+       "grant-editprotected": "保護されたページを編集",
+       "grant-oversight": "利用者名および版を秘匿",
+       "grant-patrol": "ページへの変更の巡回",
+       "grant-protect": "ページを保護および保護解除",
+       "grant-rollback": "ページヘの変更の巻き戻し",
+       "grant-sendemail": "他の利用者へのメールの送信",
+       "grant-uploadeditmovefile": "ファイルをアップロード/置き換え/移動",
+       "grant-uploadfile": "新しいファイルをアップロード",
+       "grant-viewdeleted": "削除されたファイルとページを閲覧",
+       "grant-viewmywatchlist": "自身のウォッチリストを閲覧",
        "newuserlogpage": "アカウント作成記録",
        "newuserlogpagetext": "以下はアカウント作成の記録です。",
        "rightslog": "利用者権限の変更記録",
        "listgrouprights-namespaceprotection-header": "名前空間ごとの制限",
        "listgrouprights-namespaceprotection-namespace": "名前空間",
        "listgrouprights-namespaceprotection-restrictedto": "編集を可能にする権限",
+       "listgrants-rights": "権限",
        "trackingcategories": "追跡用カテゴリ",
        "trackingcategories-summary": "このページでは、MediaWiki ソフトウェアが自動的に追加した追跡用カテゴリを列挙します。これらの名前は、{{ns:8}} 名前空間内の対応するシステム メッセージを修正することで変更できます。",
        "trackingcategories-msg": "追跡用カテゴリ",
        "tooltip-t-recentchangeslinked": "このページからリンクしているページの最近の更新",
        "tooltip-feed-rss": "このページのRSSフィード",
        "tooltip-feed-atom": "このページのAtomフィード",
-       "tooltip-t-contributions": "ã\81\93ã\81®å\88©ç\94¨è\80\85ã\81®投稿の一覧",
+       "tooltip-t-contributions": "ã\81\93ã\81®å\88©ç\94¨è\80\85ã\81«ã\82\88ã\82\8b投稿の一覧",
        "tooltip-t-emailuser": "この利用者にメールを送信する",
        "tooltip-t-info": "このページについての詳細情報",
        "tooltip-t-upload": "ファイルをアップロードする",
index fbab2fc..551baec 100644 (file)
        "content-model-css": "CSS",
        "content-json-empty-object": "ცარიელი ობიექტი",
        "content-json-empty-array": "ცარიელი ტაბლო",
+       "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=bar}}</nowiki></code>.",
        "expensive-parserfunction-warning": "ყურადღება. მოცემული გვერდი შეიცავს ძალიან ბევრ მძიმე ფუნქციას.\n\nგამოძახებათა რაოდენობა შეზღუდულია $2 დონეზე.ამ შემთხვევაში უნდა გაკეთდეს  $1 გამოძახება.",
        "right-managechangetags": "[[Special:Tags|tags]] შექმნა და წაშლა მონაცემთა ბაზიდან",
        "right-applychangetags": "[[Special:Tags|tags]] მიღება თქვენ ცვლილებებთან ერთად",
        "right-changetags": "თვითნებური [[Special:Tags|tags]] დამატება ან წაშლა ცალკეულ ცვლილებებსა და ჟურნალის ჩანაწერებში",
+       "grant-group-email": "ელ.ფოსტის გაგზავნა",
+       "grant-editmyoptions": "თქვენი საკუთარი კონფიგურაციის რედაქტირება",
+       "grant-editmywatchlist": "თქვენი კონტროლის სიის რედაქტირება",
        "newuserlogpage": "მომხმარებლის რეგისტრაციის ჟურნალი",
        "newuserlogpagetext": "ბოლო დროს დარეგისტრირებულ მომხმარებელთა სია",
        "rightslog": "მომხმარებლის უფლებების ჟურნალი",
index 0d5f011..4534257 100644 (file)
        "right-managechangetags": "데이터베이스에서 [[Special:Tags|태그]]를 만들거나 지우기",
        "right-applychangetags": "자신이 편집할 때 [[Special:Tags|태그]]를 적용하기",
        "right-changetags": "문서의 특정 판과 특정 기록 항목에 임의의 [[Special:Tags|태그]]를 추가하거나 제거하기",
+       "grant-generic": "\"$1\" 권한 번들",
+       "grant-group-page-interaction": "문서로 상호 작용",
+       "grant-group-file-interaction": "미디어로 상호 작용",
+       "grant-group-watchlist-interaction": "당신의 주시문서로 상호작용",
+       "grant-group-email": "이메일 보내기",
+       "grant-group-high-volume": "대량의 작업을 수행",
+       "grant-group-customization": "사용자 최적화 및 환경 설정",
+       "grant-group-administration": "관리 기능 수행",
+       "grant-group-other": "기타 활동",
+       "grant-blockusers": "사용자 차단 또는 차단 해제",
+       "grant-createaccount": "계정 만들기",
+       "grant-createeditmovepage": "문서 만들기, 편집 및 이동",
+       "grant-delete": "문서, 판 및 기록 항목 삭제",
+       "grant-editinterface": "미디어위키 이름공간과 사용자 CSS/JS 편집",
+       "grant-editmycssjs": "자신의 사용자 CSS/자바스크립트 편집하기",
+       "grant-editmyoptions": "사용자 환경 설정 편집하기",
+       "grant-editmywatchlist": "내 주시문서 목록 편집하기",
+       "grant-editpage": "기존 문서 편집하기",
+       "grant-editprotected": "보호된 문서 편집하기",
+       "grant-highvolume": "대용량 편집",
+       "grant-oversight": "사용자 숨기기와 판 억제",
+       "grant-patrol": "페이지 검토",
+       "grant-protect": "문서 보호 및 보호 해제",
+       "grant-rollback": "문서의 바뀜을 되돌리기",
+       "grant-sendemail": "다른 사용자에게 이메일 보내기",
+       "grant-uploadeditmovefile": "파일 올리기, 바꾸기, 이동",
+       "grant-uploadfile": "새 파일 올리기",
+       "grant-viewdeleted": "삭제된 파일과 문서 보기",
+       "grant-viewmywatchlist": "내 주시문서 목록 보기",
        "newuserlogpage": "사용자 만들기 기록",
        "newuserlogpagetext": "사용자가 만들어진 기록입니다.",
        "rightslog": "사용자 권한 기록",
        "listgrouprights-namespaceprotection-header": "이름공간 제한",
        "listgrouprights-namespaceprotection-namespace": "이름공간",
        "listgrouprights-namespaceprotection-restrictedto": "사용자가 편집할 수 있는 권한",
+       "listgrants-summary": "다음은 사용자 권한에 관련된 접근 권한을 통해 부여된 OAuth 부여 목록입니다. 사용자는 자신의 계정에 대해 권한을 부여 할 수 있지만, 사용자가 응용 프로그램에 부여한 권한 설정에 따라 제한이 있습니다. 사용자를 대신하여 동작하는 응용 프로그램은 사용자가 갖고 있지 않은 권한은 사용할 수 없습니다. \n각각의 권한에 대한 [[{{MediaWiki:Listgrouprights-helppage}}|추가 정보]]가 있습니다.",
+       "listgrants-grant": "부여",
+       "listgrants-rights": "권한",
        "trackingcategories": "추적용 분류",
        "trackingcategories-summary": "이 페이지는 미디어위키 소프트웨어에 의해 자동으로 만들어지는 추적용 분류를 나열합니다. 그들의 이름은 {{ns:8}} 이름공간에 관련된 시스템 메시지를 바꾸어서 바꿀 수 있습니다.",
        "trackingcategories-msg": "추적용 분류",
index 682f51d..7f7eb78 100644 (file)
        "createaccountreason": "Чурум:",
        "createacct-reason": "Чурум",
        "createacct-reason-ph": "Башха тергеу джазыуну нек къураусыз",
-       "createacct-captcha": "Къоркъуусузлукъну тинтиу",
-       "createacct-imgcaptcha-ph": "Башыракъдагъы текстни джаз",
        "createacct-submit": "Тергеу джазыуну къура",
        "createacct-another-submit": "Энтда бир аккаунт къурау",
        "createacct-benefit-heading": "{{SITENAME}} сизнича адамла бла къуралгъанды.",
        "passwordreset-email": "Электрон почтаны адреси:",
        "passwordreset-emailtitle": "{{SITENAME}} сайтдагъы тергеу джазыуну юсюнден билгиле",
        "passwordreset-emailelement": "Къошулуучуну аты: \n$1\n\nБолджаллы пароль: \n$2",
-       "passwordreset-emailsent": "Пароль бла e-mail ийилди.",
+       "passwordreset-emailsentemail": "Пароль бла e-mail ийилди.",
        "passwordreset-emailsent-capture": "Ийилген пароль эсгертиу e-mail тюбюрекде берилибди.",
        "passwordreset-emailerror-capture": "Пароль эсгертиу e-mail генерация этилди (тюбюрекде берилибди), аны {{GENDER:$2|къошулуучугъа}} ашырыу джетишимсиз болду, чурум: $1",
        "changeemail": "Электрон почтаны адресин ауушдур",
-       "changeemail-text": "Сизни e-mail адресигизни тюрлендирир ючюн бу форманы толтуругъуз. Тюрлениуню бегитир ючюн паролну джазаргъа керек боллукъду.",
+       "changeemail-header": "Электрон почтаны адресин ауушдуруу",
        "changeemail-no-info": "Бу бетни кёрюр ючюн сиз системагъа тергеу джазыуугъуз (аккаунтугъуз) бла кирирге керексиз.",
        "changeemail-oldemail": "Почтаны бусагъатдагъы адреси:",
        "changeemail-newemail": "Джангы email адрес:",
        "prefs-displaywatchlist": "Кёрюнюуню джарашдырыулары",
        "prefs-diffs": "Версияланы башхалыкълары",
        "prefs-help-prefershttps": "Бу джарашдырыу эндиги авторизацияны ётгенден сора сингдирилликди.",
-       "email-address-validity-valid": "E-mail адрес тюзге ушайды",
-       "email-address-validity-invalid": "Тюз e-mail адрес джазыгъыз!",
        "userrights": "Къошулуучуну хакъларына оноу этиу",
        "userrights-lookup-user": "Къошулуучуланы къауумуна оноу эт",
        "userrights-user-editname": "Къошулуучуну атын джазыгъыз:",
        "right-override-export-depth": "Бетлени, теренлиги 5-ге дери байламлы бетле бла бирге экспорт эт",
        "right-sendemail": "Башха къошулуучулагъа электрон почта джиберирге",
        "right-passwordreset": "пароль тюрлениуле бла e-mail'леге къарау",
+       "grant-group-email": "E-mail джибер",
+       "grant-createaccount": "Аккаунтла къурау",
        "newuserlogpage": "Къошулуучуланы регистрацияларыны журналы",
        "newuserlogpagetext": "Кёб болмай регистрация этген къошулуучуланы тизмеси.",
        "rightslog": "Къошулуучуну хакъларыны журналы",
        "listgrouprights-removegroup-self-all": "Кесини тергеу джазыуундан бютеу къауумланы къораталлыкъды",
        "listgrouprights-namespaceprotection-header": "Ат аламны чеклеулери",
        "listgrouprights-namespaceprotection-namespace": "Атла алам",
+       "listgrants-rights": "Онгла",
        "trackingcategories": "Марлаб туруучу категорияла",
        "trackingcategories-msg": "Марлаб туруучу категория",
        "trackingcategories-name": "Билдириуню аты",
        "wlheader-showupdated": "Ахыр кириуюгюзден сора бетни тюрлениулери '''къалын''' джазыу бла кёргюзюлгенди.",
        "wlnote": "Тюбюндеди кёргюзюлгенди: ахыр '''$2''' сагъатха этилген ахыр '''$1''' тюрлениу, $3 $4 заманнга дери.",
        "wlshowlast": "Арт $1 сагъат $2 кюннге  кёргюз",
+       "watchlistall2": "бютеу",
        "watchlist-options": "Кёзде тургъан тизмени джарашдырыулары",
        "watching": "Кёзде тургъан тизмеге къошуу...",
        "unwatching": "Кёзде тургъан тизмеден кетериу...",
        "movenosubpage": "Бу бетни тюб бети джокъду.",
        "movereason": "Чурум:",
        "revertmove": "ызына къайтыу",
-       "delete_and_move": "Кетер эмда атын тюрлендир",
        "delete_and_move_text": "== Кетериу керекди ==\n\"[[:$1]]\" атлы бет алайсызда джокъду. О бетни кетериб, атны тюрлендириуню андан ары бардырыргъа излеймисиз?",
        "delete_and_move_confirm": "Хоу, бетни кетер",
        "delete_and_move_reason": "«[[$1]]» бетни атын тюрлендирир ючюн кетерилди",
index 5c7ed50..cb628f5 100644 (file)
        "right-managechangetags": "[[Special:Tags|Kännzeijsche]] en de Dahtebangk aanlähje udder fottschmiiße",
        "right-applychangetags": "[[Special:Tags|Makehronge]] met de eije Änderonge zersamme verjävve",
        "right-changetags": "[[Special:Tags|Makehronge]] vun Väsjohne un Enndrähsche em Logbohch fott nämme un zohföhje",
+       "grant-group-page-interaction": "Met Sigge ömjonn",
+       "grant-group-file-interaction": "Met Mehdeje_Datteije ömjonn",
+       "grant-group-watchlist-interaction": "Met de eije Oppaßless ömjonn",
+       "grant-group-email": "<i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"„de eläktrohnesche Poß“\">e-mail</i> scheke",
+       "grant-group-high-volume": "Vill en koote Zigg donn",
+       "grant-group-customization": "Enschtällonge",
+       "grant-group-administration": "Verwallde",
+       "grant-group-other": "Söns jät donn",
+       "grant-blockusers": "Metmaacher schpaääre un esu en Schpärre ophävvve",
+       "grant-createaccount": "Zohjäng aanjähje",
+       "grant-createeditmovepage": "Sigge aanläje, änndere, un ömnänne",
+       "grant-delete": "Sigge, Väsjohne vun Sigge, un Endrähsch uss em Lohbohch fottschmiiße",
+       "grant-editinterface": "Em Appachtemang {{ns:MediaWiki}} un aan de Metmaacher iere <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"Cascading Style Sheet\">CSS</i>e un JavaSkrepte jäd änndere.",
+       "grant-editmycssjs": "Aan de eije <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"Cascading Style Sheet\">CSS</i>e un JavaSkrepte jäd änndere.",
+       "grant-editmyoptions": "Aan de eije Enschtälonge jäd änndere.",
+       "grant-editmywatchlist": "De eije Oppaßleß änndere.",
+       "grant-editpage": "Sigge ändere, di et ald jitt",
+       "grant-editprotected": "Jeschöz Sigge ändere",
+       "grant-highvolume": "Vill en einem Rötsch ändere",
+       "grant-oversight": "Metmaacher verschteische un Väsohne ongerdröke",
+       "grant-patrol": "Änderonge aan Sigge nohkike",
+       "grant-protect": "Sigge schöze un der Schoz wider ophävve",
+       "grant-rollback": "Änderonge aan Sigge retuhr maache",
+       "grant-sendemail": "<i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"„de eläktrohnesche Poß“\">e-mails</i> aan ander Metmaacher schecke",
+       "grant-uploadeditmovefile": "Datteije uhlahde, ußtuusche un ömbenänne",
+       "grant-uploadfile": "Neu datteije huhlahde",
+       "grant-viewdeleted": "Fottjeschmeße Dahte un Sigge belohre",
+       "grant-viewmywatchlist": "De eije Oppaßleß ze belooere",
        "newuserlogpage": "Logbohch för neu Metmaachere",
        "newuserlogpagetext": "He sin de Metmaacher opjelėßß, di sesh nöü aanjemäldt han.",
        "rightslog": "Logbohch för Änderonge aan Metmaacher-Rääschde",
        "listgrouprights-namespaceprotection-header": "Beschrängkonge för Appachtemangs",
        "listgrouprights-namespaceprotection-namespace": "Appachtemang",
        "listgrouprights-namespaceprotection-restrictedto": "Rääsch(de) zom Verändere",
+       "listgrants-rights": "Rääschte",
        "trackingcategories": "Saachjroppe för täschnsche Saache ze verfollje.",
        "trackingcategories-summary": "Op hee dä {{int:nstab-special}} sin Saachjroppe opjeleß, di automattesch vum Wikki jevöllt wähde. Dä iehr Nahme künne övver Veränderonge aan beschtemmpte Täxte em Appachtemang {{ns:8}} faßjelaat wäde.",
        "trackingcategories-msg": "Saachjropp för täschnsche Saache ze verfollje.",
        "export-download": "Als en <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"Extensible Markup Language\">XML</i>-Dattei affschpeischere",
        "export-templates": "De Schablohne met expochtehre, di de Sigge bruche",
        "export-pagelinks": "Donn de Sigge metnämme, wo vun heh Lengks drop jon, un vun do wigger, bes esu vill Schrette:",
+       "export-manual": "Donn Sigge vun Hand derbei.",
        "allmessages": "Aanzeije-Baustein, Täxte, un Nohreeschte vum Wiki-System",
        "allmessagesname": "Nahme",
        "allmessagesdefault": "Dä standaadmäßije Tex",
        "expand_templates_preview": "Vör-Aansich",
        "expand_templates_preview_fail_html": "<em>Weil et Wiki rüh <i xml:lang=\"en\" title=\"HyperText Markup Language\" lang=\"en\">HTML</i> zohlöht un de Sezongsdahte verschött jejange sin, dom_mer de {{int:preview}} uß Vörseesch nit aanzeije, öm Aanjreffe övver JavaSkrep zevör ze kumme.</em>\n\n<strong>Wann dat heh en Ohdenong es, bes esu johd un versöhg et norr_ens.</strong>\nwann dat nix hellef, versöhg ens [[Special:UserLogout|ußzelogge]] un neu enzelogge.",
        "expand_templates_preview_fail_html_anon": "<em>Weil et Wiki rüh <i xml:lang=\"en\" title=\"HyperText Markup Language\" lang=\"en\">HTML</i> zohlöht un Do nit ennjelogg bes, dom_mer de {{int:preview}} uß Vörseesch nit aanzeije, öm Aanjreffe övver JavaSkrep zevör ze kumme.</em>\n\n<strong>Wann dat heh en Ohdenong es, bes esu johd un donn [[Special:UserLogin|enlogge]] un versöhg et norr_ens.</strong>",
+       "expand_templates_input_missing": "Mer mß winnischsdrens jät Täx ennjävve.",
        "pagelanguage": "De Schprohch för di Sigg faßlääje",
        "pagelang-name": "Sigg",
        "pagelang-language": "De Schprohch",
index 2a8a480..635ad26 100644 (file)
        "yourlanguage": "Lingua:",
        "yourvariant": "Differentia linguae contentorum:",
        "yournick": "Subscriptio nova:",
-       "prefs-help-signature": "Commentationibus ad quanque disputationem factis ne subscribe nisi quater undulam (<nowiki>~</nowiki>) ponens! Quae quatuor undulae (<nowiki>~~~~</nowiki>) automatice in subscriptionem tuam et tempus subscribendi convertentur.",
+       "prefs-help-signature": "Disputationibus auctis ne subscripseris nisi quater undulam (<nowiki>~</nowiki>) ponens! Quae quatuor undulae (<nowiki>~~~~</nowiki>) automatice nomen tuum et diem adscribent.",
        "badsig": "Subscriptio cruda non est valida; scrutina affixa HTML.",
        "badsiglength": "Subscriptio tua nimis longa est.\n{{PLURAL:$1|Una littera est|$1 litterae sunt}} longitudo maxima.",
        "yourgender": "Sexus:",
        "email": "Litterae electronicae",
        "prefs-help-realname": "Nomen verum non necesse est.\nSi vis id dare, opera tua tibi ascribentur.",
        "prefs-help-email": "Non necesse est inscriptionem electronicam dare. Qua tamen data tessera tibi tribui poterit nova, si prioris oblitus eris.",
-       "prefs-help-email-others": "Praeterea, si libeat, aliis concedas tibi nuntia per nexum in pagina vel disputatione tua positum mittere electronicas.\nInscriptio tua electronica usoribus, qui tibi scribeant, non erit visibilis.",
+       "prefs-help-email-others": "Praeterea, si libeat, aliis concedas tibi nuntia per nexum in pagina vel disputatione tua positum mittere electronicas.\nInscriptio tua conlatoribus, qui tibi scribeant, latebit.",
        "prefs-help-email-required": "Inscriptio electronica necesse est.",
        "prefs-info": "Generalia",
        "prefs-i18n": "Sermonis delectus",
        "watchlistanontext": "Ad inspiciendum vel recensendum indicem paginarum observandarum necesse est nomen dare.",
        "watchnologin": "Nomen datum non est",
        "addedwatchtext": "Pagina \"[[:$1]]\" necnon disputatio pertinens abhinc [[Special:Watchlist|observabitur]].",
-       "removedwatchtext": "Pagina \"[[:$1]]\" necnon disputio eius ex [[Special:Watchlist|indice paginarum observandarum]] remota est.",
+       "removedwatchtext": "Pagina \"[[:$1]]\" necnon disputatio eius ex [[Special:Watchlist|indice paginarum observandarum]] remota est.",
        "watch": "Observare",
        "watchthispage": "Observare hanc paginam",
        "unwatch": "Non iam observare",
index 5d2bc45..ad57f53 100644 (file)
        "right-override-export-depth": "Säiten exportéieren inklusiv de verlinkte Säite bis zu enger Déift vu 5",
        "right-sendemail": "Anere Benotzer E-Maile schécken",
        "right-passwordreset": "Maile vum Zrécksetze vum Passwuert weisen",
+       "grant-group-page-interaction": "Mat Säiten interagéieren",
+       "grant-group-watchlist-interaction": "Mat Ärer Iwwerwaachungslëscht interagéieren",
+       "grant-group-email": "E-Mail schécken",
+       "grant-group-high-volume": "Massenaktivitéiten ausféieren",
+       "grant-group-customization": "Upassungen an Astellungen",
+       "grant-group-administration": "Administrativ Aktioune maachen",
+       "grant-group-other": "Verschidden Aktivitéiten",
+       "grant-blockusers": "Benotzer spären an hir Spär ophiewen",
+       "grant-createaccount": "Benotzerkonten opmaachen",
+       "grant-createeditmovepage": "Säiten uleeën, änneren a réckelen",
+       "grant-delete": "Säiten, Versiounen a Rubriken a Logbicher läschen",
+       "grant-editinterface": "MediaWiki-Nummraum a Benotzer CSS/JS änneren",
+       "grant-editmycssjs": "Ären eegene Benotzer CSS/JavaScript änneren",
+       "grant-editmyoptions": "Ännert Är Benotzerastellungen",
+       "grant-editmywatchlist": "Ännert Är Iwwerwaachungslëscht",
+       "grant-editpage": "Säiten déi et gëtt änneren",
+       "grant-editprotected": "Gespaart Säiten änneren",
+       "grant-oversight": "Benotzer verstoppen a Versioune läschen",
+       "grant-patrol": "Ännerungen op Säiten iwwerwaachen",
+       "grant-protect": "Säite spären an entspären",
+       "grant-rollback": "Ännerungen op Säiten zrécksetzen",
+       "grant-sendemail": "Anere Benotzer E-Maile schécken",
+       "grant-uploadeditmovefile": "Fichieren eroplueden, ersetzen a réckelen",
+       "grant-uploadfile": "Nei Fichieren eroplueden",
+       "grant-viewdeleted": "Geläscht Fichieren a Säite weisen",
+       "grant-viewmywatchlist": "Kuckt Är Iwwerwaachungslëscht",
        "newuserlogpage": "Logbuch vun den neien Umeldungen",
        "newuserlogpagetext": "Dëst ass d'Lescht vun de Benotzernimm déi ugeluecht goufen.",
        "rightslog": "Logbuch vun de Benotzerrechter",
        "listgrouprights-namespaceprotection-header": "Limitatioune vum Nummraum",
        "listgrouprights-namespaceprotection-namespace": "Nummraum",
        "listgrouprights-namespaceprotection-restrictedto": "Recht(er), déi dem Benotzer d'Änneren erlaben",
+       "listgrants-rights": "Rechter",
        "trackingcategories": "Tracking-Kategorien",
        "trackingcategories-msg": "Tracking-Kategorie",
        "trackingcategories-name": "Numm vum Message",
index c23fbbf..3ccfea3 100644 (file)
        "category-media-header": "\"پۀروۀندۀل ردهٔ \"$1",
        "category-empty": "<em>این رده در حال حاضر حاوی هیچ صفحه یا پرونده‌ای نیست.</em>",
        "hidden-categories": "{{PLURAL:$1|رده آشاریا|رده ل آشاریا}}",
-       "hidden-category-category": "رده‌های پنهان",
+       "hidden-category-category": "ڕزگەل آشاریا",
        "category-subcat-count": "{{PLURAL:$2|این رده تنها حاوی زیرردهٔ زیر است.|{{PLURAL:$1|این زیررده|این $1 زیررده}} در این رده قرار {{PLURAL:$1|دارد|دارند}}؛ این رده در کل حاوی $2 زیررده است.}}",
        "category-subcat-count-limited": "ئئ رده/ڕِزگة شامل {{PLURAL:$1|یک|$1}} ژئر رده زیر است.",
        "category-article-count": "{{PLURAL:$2|این رده فقط دارای صفحهٔ زیر است.|{{PLURAL:$1|این صفحه|این $1 صفحه}} در این رده قرار {{PLURAL:$1|دارد|دارند}}؛ این رده در کل حاوی $2 صفحه است.}}",
        "revdelete-hide-user": "نام کاربری/نشانی آی‌پی",
        "revdelete-hide-restricted": "فرونشانی اطلاعات برای مدیران به همراه دیگران",
        "revdelete-radio-same": "(بدون گؤەڕانن/تغییر )",
-       "revdelete-radio-set": "آشاریا/پنهان",
+       "revdelete-radio-set": "آشاریا",
        "revdelete-radio-unset": "دیارۀ-نمایان",
        "revdelete-suppress": "از دسترسی مدیران به داده نیز مانند سایر کاربران جلوگیری به عمل آید.",
        "revdelete-unsuppress": "حذف محدودیت‌ها در بازبینی‌های ترمیم‌شده",
        "right-managechangetags": "ایجاد و حذف [[Special:Tags|برچسب‌ها]] از پایگاه داده",
        "right-applychangetags": "تائید [[Special:Tags|برچسب]] بر روی تغییرات یک نفر",
        "right-changetags": "افزودن یا حذف [[Special:Tags|برچسب]] قراردادی بر روی نسخه یا سیاهه ورودی‌ها",
+       "grant-group-customization": "سفارشی‌سازی و تنظیمات",
        "newuserlogpage": "سیاههٔ ایجاد کاربر",
        "newuserlogpagetext": "این سیاهه‌ای از نام‌های کاربری تازه‌ساخته‌شده است.",
        "rightslog": "سیاههٔ اختیارات کاربر",
        "block-log-flags-noemail": "ایمیل بسته‌شد",
        "block-log-flags-nousertalk": "صفحهٔ بحث خود را نمی‌تواند ویرایش کند",
        "block-log-flags-angry-autoblock": "قطع دسترسی خودکار پیشرفته فعال شد",
-       "block-log-flags-hiddenname": "نام کاربری پنهان",
+       "block-log-flags-hiddenname": "نۆم کاربەری آشاریا",
        "range_block_disabled": "بستن یک بازه توسط مدیران غیر فعال است.",
        "ipb_expiry_invalid": "زمان سرآمدن نامعتبر.",
        "ipb_expiry_temp": "قطع دسترسی کاربرهای پهنان باید همیشگی باشد.",
        "logentry-suppress-revision-legacy": "$1 پیدایی نسخه‌های $3 را مخفیانه {{GENDER:$2|تغییر داد}}",
        "revdelete-content-hid": "نۆم جِک(محتوا)آشاریائە",
        "revdelete-summary-hid": "خلاصه ویرایش را پنهان کرد",
-       "revdelete-uname-hid": "نام کاربری را پنهان کرد",
+       "revdelete-uname-hid": "نۆم کاربەری شاردێآ",
        "revdelete-content-unhid": "نۆم جِک(محتوا)نەشاریاسآ(پنهان نؤیە)",
        "revdelete-summary-unhid": "خلاصه ویرایش را آشکار کرد",
        "revdelete-uname-unhid": "نام کاربری را آشکار کرد",
index 98a177f..d9d4f73 100644 (file)
        "right-managechangetags": "Создавање3 или бришење на [[Special:Tags|ознаки]] од базата",
        "right-applychangetags": "Задавање на [[Special:Tags|ознаки]] заедно со направените измени",
        "right-changetags": "Додавате и отстранување на произволни [[Special:Tags|ознаки]] во поединечни преработки и дневнички записи",
+       "grant-generic": "Група права „$1“",
+       "grant-group-page-interaction": "Опходување со страници",
+       "grant-group-file-interaction": "Опходување со слики и снимки",
+       "grant-group-watchlist-interaction": "Опходување со набљудуваните",
+       "grant-group-email": "Испраќање на е-пошта",
+       "grant-group-high-volume": "Вршење на активности од голем обем",
+       "grant-group-customization": "Прилагодувања и поставки",
+       "grant-group-administration": "Вршење на административни дејства",
+       "grant-group-other": "Разни активности",
+       "grant-blockusers": "Блокирање и одблокирање корисници",
+       "grant-createaccount": "Направи сметки",
+       "grant-createeditmovepage": "Создавање, уредување и преместување страници",
+       "grant-delete": "Бришење на страници, преработки и дневнички записи",
+       "grant-editinterface": "Измена на именскиот простор „МедијаВики“ и кориснички CSS/JS",
+       "grant-editmycssjs": "Уредување на ваш кориснички CSS/JavaScript",
+       "grant-editmyoptions": "Уредете ги вашите кориснички нагодувања",
+       "grant-editmywatchlist": "Уреди мои набљудувани",
+       "grant-editpage": "Измени постоечки страници",
+       "grant-editprotected": "Уредување на заштитени страници",
+       "grant-highvolume": "Високообемно уредување",
+       "grant-oversight": "Скривање на корисници и преработки",
+       "grant-patrol": "Патрола на измени во страници",
+       "grant-protect": "Заштита на незаштитени страници",
+       "grant-rollback": "Отповикување на измени во страници",
+       "grant-sendemail": "Испраќање на е-пошта до други корисници",
+       "grant-uploadeditmovefile": "Подигање, замена и преместување на податотеки",
+       "grant-uploadfile": "Подигни нови податотеки",
+       "grant-viewdeleted": "Преглед на избришани податотеки и страници",
+       "grant-viewmywatchlist": "Преглед на вашите набљудувања",
        "newuserlogpage": "Дневник на регистрирања на корисници",
        "newuserlogpagetext": "Ова е дневник на регистрирани корисници.",
        "rightslog": "Дневник на корисничките права",
        "listgrouprights-namespaceprotection-header": "Ограничувања за именски простори",
        "listgrouprights-namespaceprotection-namespace": "Именски простор",
        "listgrouprights-namespaceprotection-restrictedto": "Права што им овозможуваат на корисниците да уредуваат",
+       "listgrants-summary": "Ова е список на доделувања на OAuth, секое со своите права. Корисниците можат да овластуваат извршници што ќе ги користат сметки, но со ограничувања во дозволите што им се доделени. Покрај ова, извршникот што делува во име на корисникот е ограничен на нештата на кои има права самиот корисник.\nМоже да најдете [[{{MediaWiki:Listgrouprights-helppage}}|уште информации]] за поединечните права.",
+       "listgrants-grant": "Доделување",
+       "listgrants-rights": "Права",
        "trackingcategories": "Следечки категории",
        "trackingcategories-summary": "На страницава се наведени следечки категории што автоматски се пополнуваат од програмот на МедијаВики. Нивните називи можат да се сменат со измена на соодветните системски пораки во именскиот простор {{ns:8}}.",
        "trackingcategories-msg": "Следечка категорија",
index 6152032..7d8012f 100644 (file)
        "virus-scanfailed": "വൈറസ് സ്കാനിങ് പരാജയപ്പെട്ടു (code $1)",
        "virus-unknownscanner": "തിരിച്ചറിയാനാകാത്ത ആന്റിവൈറസ്:",
        "logouttext": "'''താങ്കൾ ഇപ്പോൾ {{SITENAME}} സംരംഭത്തിൽനിന്നും ലോഗൗട്ട് ചെയ്തിരിക്കുന്നു'''\n\nതാങ്കൾ വെബ് ബ്രൗസറിന്റെ ക്യാഷെ ശൂന്യമാക്കിയിട്ടില്ലെങ്കിൽ ചില താളുകളിൽ താങ്കൾ ലോഗിൻ ചെയ്തിരിക്കുന്നതായി കാണിക്കാൻ സാധ്യതയുണ്ട്.",
+       "cannotlogoutnow-title": "ഇപ്പോൾ ലോഗൗട്ട് ചെയ്യാനാവില്ല",
+       "cannotlogoutnow-text": "$1 ഉപയോഗിച്ചുകൊണ്ടിരിക്കെ പുറത്ത് കടക്കാൻ കഴിയില്ല.",
        "welcomeuser": "സ്വാഗതം, $1!",
        "welcomecreation-msg": "താങ്കളുടെ അംഗത്വം സൃഷ്ടിക്കപ്പെട്ടിരിക്കുന്നു.\nതാങ്കളുടെ [[Special:Preferences|{{SITENAME}} ക്രമീകരണങ്ങളിൽ]] മാറ്റം വരുത്താൻ മറക്കരുത്.",
        "yourname": "ഉപയോക്തൃനാമം:",
        "remembermypassword": "എന്റെ പ്രവേശനം ഈ ബ്രൗസറിൽ ({{PLURAL:$1|ഒരു ദിവസം|$1 ദിവസം}}) ഓർത്തുവെക്കുക",
        "userlogin-remembermypassword": "ഞാൻ പ്രവേശിച്ചതായിത്തന്നെ ഓർത്തിരിക്കുക",
        "userlogin-signwithsecure": "സുരക്ഷിത കണക്ഷൻ ഉപയോഗിക്കുക",
+       "cannotloginnow-title": "ഇപ്പോൾ പ്രവേശിക്കാൻ കഴിയില്ല",
+       "cannotloginnow-text": "$1 ഉപയോഗിച്ചുകൊണ്ടിരിക്കെ പ്രവേശിക്കാൻ കഴിയില്ല.",
        "yourdomainname": "താങ്കളുടെ ഡൊമെയിൻ:",
        "password-change-forbidden": "ഈ വിക്കിയിൽ രഹസ്യവാക്കുകൾ മാറ്റാനാവില്ല.",
        "externaldberror": "ഒന്നുകിൽ ഡേറ്റാബേസ് സാധൂകരണത്തിൽ പ്രശ്നം ഉണ്ടായിരുന്നു അല്ലെങ്കിൽ നവീകരിക്കുവാൻ താങ്കളുടെ ബാഹ്യ അംഗത്വം താങ്കളെ അനുവദിക്കുന്നില്ല.",
        "resetpass_submit": "രഹസ്യവാക്ക് സജ്ജീകരിച്ചശേഷം ലോഗിൻ ചെയ്യുക",
        "changepassword-success": "താങ്കളുടെ രഹസ്യവാക്ക് വിജയകരമായി മാറ്റിയിരിക്കുന്നു!",
        "changepassword-throttled": "കുറഞ്ഞ സമയത്തിനുള്ളിൽ താങ്കൾ നിരവധി തവണ പ്രവേശിക്കാൻ ശ്രമിച്ചിരിക്കുന്നു.\nവീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ദയവായി $1 കാത്തിരിക്കുക.",
+       "botpasswords": "യന്ത്രത്തിനുള്ള രഹസ്യവാക്കുകൾ",
+       "botpasswords-label-appid": "യന്ത്രത്തിന്റെ പേര്:",
+       "botpasswords-label-create": "സൃഷ്ടിക്കുക",
+       "botpasswords-label-update": "പുതുക്കുക",
+       "botpasswords-label-cancel": "റദ്ദാക്കുക",
+       "botpasswords-label-delete": "മായ്ക്കുക",
+       "botpasswords-label-resetpassword": "രഹസ്യവാക്ക് പുനഃക്രമീകരിക്കുക",
+       "botpasswords-label-grants": "ബാധകമായ അനുമതികൾ:",
+       "botpasswords-label-restrictions": "ഉപയോഗത്തിന്റെ പരിമിതപ്പെടുത്തലുകൾ:",
+       "botpasswords-label-grants-column": "അനുവദിച്ചിരിക്കുന്നവ",
        "resetpass_forbidden": "രഹസ്യവാക്കുകൾ മാറ്റുന്നത് അനുവദിക്കുന്നില്ല",
        "resetpass-no-info": "ഈ താൾ നേരിട്ടു കാണുന്നതിന് താങ്കൾ ലോഗിൻ ചെയ്തിരിക്കണം.",
        "resetpass-submit-loggedin": "രഹസ്യവാക്ക് മാറ്റുക",
        "right-createpage": "താളുകൾ സൃഷ്ടിക്കുക (സംവാദം താളുകൾ അല്ലാത്തവ)",
        "right-createtalk": "സംവാദ താളുകൾ സൃഷ്ടിക്കുക",
        "right-createaccount": "പുതിയ ഉപയോക്തൃ അംഗത്വങ്ങൾ സൃഷ്ടിക്കുക",
+       "right-autocreateaccount": "ബാഹ്യ ഉപയോക്തൃ അംഗത്വമുപയോഗിച്ച് സ്വയം പ്രവേശിക്കുക",
        "right-minoredit": "ചെറിയ തിരുത്തലായി രേഖപ്പെടുത്തുക",
        "right-move": "താളുകൾ നീക്കുക",
        "right-move-subpages": "താളുകൾ അവയുടെ ഉപതാളുകളോടുകൂടീ നീക്കുക",
        "right-managechangetags": "ഡേറ്റാബേസിൽ നിന്നുള്ള [[Special:Tags|ടാഗുകൾ]] സൃഷ്ടിക്കുക അല്ലെങ്കിൽ മായ്ക്കുക",
        "right-applychangetags": "മാറ്റങ്ങളോടൊപ്പം [[Special:Tags|ടാഗുകളും]] ബാധകമാക്കുക",
        "right-changetags": "ഒറ്റയൊറ്റ നാൾപ്പതിപ്പുകൾക്കും രേഖയിലെ ഉൾപ്പെടുത്തലുകൾക്കും ഐച്ഛിക [[Special:Tags|ടാഗുകൾ]] ചേർക്കുക അല്ലെങ്കിൽ നീക്കംചെയ്യുക",
+       "grant-generic": "\"$1\" അവകാശ സഞ്ചയം",
+       "grant-group-page-interaction": "താളുകളുമായി സമ്പർക്കം പുലർത്തുക",
+       "grant-group-file-interaction": "മീഡിയയുമായി സമ്പർക്കം പുലർത്തുക",
+       "grant-group-watchlist-interaction": "ശ്രദ്ധിക്കേണ്ട പട്ടികയുമായി സമ്പർക്കം പുലർത്തുക",
+       "grant-group-email": "ഇമെയിൽ അയയ്ക്കുക",
+       "grant-group-high-volume": "ഉയർന്ന തോതിലുള്ള പ്രവൃത്തികൾ നടത്തുക",
+       "grant-group-customization": "ഇച്ഛാനുസരണമാക്കലുകളും ക്രമീകരണങ്ങളും",
+       "grant-group-administration": "കാര്യനിർവ്വാഹക ജോലികൾ നടത്തുക",
+       "grant-group-other": "വിവിധ പ്രവൃത്തികൾ",
+       "grant-blockusers": "ഉപയോക്താക്കളെ തടയുക, തടയൽ നീക്കുക",
+       "grant-createaccount": "അംഗത്വങ്ങൾ സൃഷ്ടിക്കുക",
+       "grant-createeditmovepage": "താളുകൾ സൃഷ്ടിക്കുക, തിരുത്തുക, മാറ്റുക",
+       "grant-sendemail": "ഇമെയിൽ അയയ്ക്കുക",
        "newuserlogpage": "ഉപയോക്തൃ സൃഷ്ടിയുടെ രേഖ",
        "newuserlogpagetext": "പുതിയതായി അംഗത്വമെടുത്ത ഉപയോക്താക്കളുടെ പട്ടിക താഴെ കാണാം.",
        "rightslog": "ഉപയോക്തൃ അവകാശ രേഖ",
index 2044c3f..aeefcc4 100644 (file)
        "virus-scanfailed": "क्रमवीक्षण (स्कॅन) अयशस्वी (कोड $1)",
        "virus-unknownscanner": "अनोळखी ऍन्टीव्हायरस:",
        "logouttext": "'''तुम्ही आता सनोंद-निर्गमित(लॉग-आउट) झाला आहात.'''\n\n\nआपण स्वत:च्या न्याहाळकाची सय (कॅशे) रिकामी करत नाही तो पर्यंत, काही पाने आपण अजून प्रवेशित आहात, असे नुसतेच दाखवत राहू शकतील.",
+       "cannotlogoutnow-title": "आता सनोंद निर्गम करु शकत नाही",
+       "cannotlogoutnow-text": "$1 वापरत असतांना सनोंद निर्गम शक्य नाही.",
        "welcomeuser": "स्वागत, $1!",
        "welcomecreation-msg": "तुमचे खाते उघडण्यात आले आहे.\nआपला [[Special:Preferences|{{SITENAME}} पसंतीक्रम]] बदलण्यास विसरू नका.",
        "yourname": "सदस्यनाम:",
        "remembermypassword": "माझा सनोंदप्रवेश (लॉग-ईन) या न्याहाळकावर लक्षात ठेवा (जास्तीत जास्त $1 {{PLURAL:$1|दिवसासाठी|दिवसांसाठी}})",
        "userlogin-remembermypassword": "मला नोंदीकृतच(लॉग्ड-ईन) ठेवा",
        "userlogin-signwithsecure": "सुरक्षित अनुबंध(सेक्युअर कनेक्शन) वापरा",
+       "cannotloginnow-title": "आता सनोंद प्रवेश घेऊ शकत नाही",
+       "cannotloginnow-text": "$1 वापरत असतांना सनोंद प्रवेश करणे शक्य नाही.",
        "yourdomainname": "तुमचे क्षेत्र (डोमेन) :",
        "password-change-forbidden": "तुम्ही या विकिवर तुमचा परवलीचा शब्द बदलू शकत नाही.",
        "externaldberror": "विदागार ’खातरजमा’ (प्रमाणितीकरण) त्रुटी होती अथवा तुम्हाला तुमचे बाह्य खाते अद्ययावत  करण्याची परवानगी नाही.",
        "resetpass_submit": "परवलीचा शब्द टाका आणि सनोंद-प्रवेश करा",
        "changepassword-success": "तुमचा परवलीचा शब्द यशस्वीरित्या बदललेला आहे!",
        "changepassword-throttled": "तुम्ही नुकतेच सनोंद- प्रवेशासाठी अनेकानेक प्रयत्न केले आहेत.\nकृपया, पुन्हा प्रयत्न करण्याआधी $1 थोडी उसंत घ्या.",
+       "botpasswords": "सांगकाम्याचे परवलीचे शब्द",
+       "botpasswords-summary": "<em>सांगकाम्याचे परवलीचे शब्द</em>हे त्या खात्याची मुख्य सनोंद-प्रवेश अधिकारपत्रे न वापरता, एपीआय मार्फत, सदस्य खात्याच्या प्रवेशास पोहोच देतात.सांगकाम्याचा परवलीचा शब्द वापरुन सनोंद प्रवेश केलेल्यांचे उपलब्ध सदस्य अधिकार प्रतिबंधित असू शकतात.\n\nजर आपणास कळत नसेल आपण हे कां करीत आहोत,तर आपण ते बहुतेक करावयास नको.कोणीही आपणास असे कधीही सांगु नये कि यापैकी एखादे उत्पादित करा व त्यांना द्या.",
+       "botpasswords-disabled": "सांगकाम्यांचे परवलीचे शब्द अक्षम केले आहेत.",
+       "botpasswords-no-central-id": "सांगकाम्याचा परवलीचा शब्द वापरण्यास, आपण केंद्रीय खात्यात सनोंद प्रवेशित हवेत.",
+       "botpasswords-existing": "अस्तित्वात असलेला सांगकाम्याचा परवलीचा शब्द",
+       "botpasswords-createnew": "सांगकाम्याचा परवलीचा शब्द नविन तयार करा",
+       "botpasswords-editexisting": "संपादन व अस्तित्वात असलेला सांगकाम्याचा परवलीचा शब्द",
+       "botpasswords-label-appid": "सांगकाम्याचे नाव:",
+       "botpasswords-label-create": "तयार करा",
+       "botpasswords-label-update": "अद्यतन करा",
+       "botpasswords-label-cancel": "रद्द करा",
+       "botpasswords-label-delete": "वगळा",
+       "botpasswords-label-resetpassword": "परवलीच्या शब्दाची पुनर्स्थापना करा",
+       "botpasswords-label-grants": "लागू अनुदाने:",
+       "botpasswords-help-grants": "प्रत्येक अनुदान हे सदस्य खात्यास आधीच असलेल्या यादीकृत सदस्य अधिकारास पोहोच देते.अधिक माहितीसाठी [[Special:ListGrants|अनुदानांचा तक्ता]] हे बघा.",
+       "botpasswords-label-restrictions": "वापराचे प्रतिबंध:",
+       "botpasswords-label-grants-column": "मंजूर",
+       "botpasswords-bad-appid": "\"$1\" हे सांगकाम्याचे नाव वैध नाही.",
+       "botpasswords-insert-failed": "\"$1\" हे सांगकाम्याचे नाव जोडण्यात अयशस्वी. ते पूर्वीच जोडले होते काय?",
+       "botpasswords-update-failed": "\"$1\" हे सांगकाम्याचे नाव अद्यतन करण्यात अयशस्वी. ते वगळण्यात आले होते काय?",
+       "botpasswords-created-title": "सांगकाम्याचा परवलीचा शब्द तयार केल्या गेला.",
+       "botpasswords-created-body": "\"$1\" हा सांगकाम्याचा परवलीचा शब्द यशस्वीरित्या तयार केल्या गेला.",
+       "botpasswords-updated-title": "सांगकाम्याचा परवलीचा शब्द अद्यतन केला",
+       "botpasswords-updated-body": "\"$1\" हा सांगकाम्याचा परवलीचा शब्द यशस्वीरित्या अद्यतन केल्या गेला.",
+       "botpasswords-deleted-title": "सांगकाम्याचा परवलीचा शब्द वगळला",
+       "botpasswords-deleted-body": "सांगकाम्याचा परवलीचा शब्द \"$1\" वगळला.",
+       "botpasswords-newpassword": "<strong>$1</strong>द्वारे सनोंद प्रवेशास नविन परवलीचा शब्द <strong>$2</strong>आहे. <em>कृपया याचा भविष्यातील संदर्भासाठी वापर करा.</em>",
+       "botpasswords-no-provider": "सांगकाम्यापरवलीशब्दसत्रपुरवठादार उपलब्ध नाही.",
+       "botpasswords-restriction-failed": "सांगकाम्या परवलीच्या शब्दावर असलेले प्रतिबंध या सनोंद प्रवेशास अटकाव करीत आहेत.",
+       "botpasswords-invalid-name": "नमूद केलेल्या सदस्यनावात सांगकाम्याचा परवलीचा शब्द-विभाजक (\"$1\") नाही.",
+       "botpasswords-not-exist": "सदस्य \"$1\" चा \"$2\" नावाचा सांगकाम्या परवलीचा शब्द नाही.",
        "resetpass_forbidden": "परवलीचे शब्द बदलता येत नाहीत.",
        "resetpass-no-info": "या पानामध्ये थेट जाण्यासाठी तुम्हास  सनोंद-प्रवेशित असावयास हवे.",
        "resetpass-submit-loggedin": "परवलीचा शब्द बदला",
        "right-createpage": "पृष्ठे तयार करा (जी चर्चापानांव्यतिरिक्त आहेत)",
        "right-createtalk": "चर्चा पृष्ठे तयार करा",
        "right-createaccount": "नवीन सदस्य खाती तयार करा",
+       "right-autocreateaccount": "या बाह्य सदस्य खात्याद्वारे आपोआप सनोंद प्रवेश करा",
        "right-minoredit": "बदल किरकोळ म्हणून जतन करा",
        "right-move": "पानांचे स्थानांतरण करा",
        "right-move-subpages": "पाने उपपानांसकट स्थानांतरीत करा",
        "right-managechangetags": "डाटाबेस मधून [[Special:Tags|खूणपताका]] तयार करा किंवा  वगळा",
        "right-applychangetags": "कोणाच्याही बदलास [[Special:Tags|खूणपताका]] जोडा",
        "right-changetags": "वैयक्तिक आवृत्त्यांना व नोंद प्रवेष्ट्यांना, आहेतुक(arbitrary) [[Special:Tags|खूणपताका]] जोडा अथवा हटवा",
+       "grant-generic": "\"$1\" अधिकार गठ्ठा",
+       "grant-group-email": "विपत्र पाठवा",
+       "grant-blockusers": "सदस्यांना प्रतिबंधन/अप्रतिबंधित करा",
+       "grant-createaccount": "खाते तयार करा",
+       "grant-createeditmovepage": "पाने बनवा,संपादा व स्थानांतरण करा",
+       "grant-delete": "पाने, आवृत्त्या व नोंदी वगळा",
+       "grant-editinterface": "मिडियाविकि नामविश्व व सदस्यांची CSS/JS संपादा",
+       "grant-editmycssjs": "आपले सदस्य CSS/JavaScript संपादित करा",
+       "grant-editmyoptions": "आपला सदस्य पसंतीक्रम संपादा",
+       "grant-editmywatchlist": "आपली निरीक्षणयादी संपादित करा",
+       "grant-editpage": "अस्तित्वात असलेली पाने संपादा",
+       "grant-editprotected": "संपादनांपासून सुरक्षित असलेली पाने",
+       "grant-highvolume": "अत्त्युच्च-जागा घेणारे संपादन",
+       "grant-oversight": "सदस्य लपवा व आवृत्त्या दाबा",
+       "grant-patrol": "पानांच्या बदलांवर गस्त घाला",
+       "grant-protect": "पाने सुरक्षित किंवा असुरक्षित करा",
+       "grant-rollback": "पानाचे बदल परतवा",
+       "grant-sendemail": "इतर सदस्यांना विपत्र पाठवा",
+       "grant-basic": "मूळ अधिकार",
+       "grant-viewdeleted": "वगळलेल्या संचिका व पाने बघा",
+       "grant-viewmywatchlist": "आपली निरीक्षणसूची बघा",
        "newuserlogpage": "नवीन सदस्यांची नोंद",
        "newuserlogpagetext": "ही नवीन सदस्यांची नोंद यादी आहे.",
        "rightslog": "सदस्य आधिकार नोंद",
        "action-createpage": "लेख बनवा",
        "action-createtalk": "चर्चा पृष्ठे तयार करा",
        "action-createaccount": "हे सदस्यखाते तयार करा",
+       "action-autocreateaccount": "हे बाह्य सदस्य खाते आपोआप तयार करा",
        "action-history": "या पानाचा इतिहास बघा.",
        "action-minoredit": "हे संपादन 'किरकोळ' म्हणून खूण करा",
        "action-move": "हे पान स्थानांतरित करा",
        "foreign-structured-upload-form-label-own-work": "हे माझे स्वत:चे काम आहे",
        "foreign-structured-upload-form-label-infoform-categories": "वर्ग",
        "foreign-structured-upload-form-label-infoform-date": "दिनांक",
+       "foreign-structured-upload-form-3-label-yes": "होय",
+       "foreign-structured-upload-form-3-label-no": "नाही",
        "backend-fail-stream": "$1 या संचिकेचा स्त्रोत शोधता आला नाही.",
        "backend-fail-backup": "$1 या संचिकेची आधारप्रत बनविता आली नाही.",
        "backend-fail-notexists": "$1 ही संचिका अस्तित्वात नाही.",
        "listgrouprights-removegroup-self-all": "सर्व गट स्वतःच्या खात्यातून काढून टाका",
        "listgrouprights-namespaceprotection-header": "नामविश्व प्रतिबंध",
        "listgrouprights-namespaceprotection-namespace": "नामविश्व",
+       "listgrants": "अनुदाने",
+       "listgrants-grant": "अनुदान",
+       "listgrants-rights": "अधिकार",
        "trackingcategories": "वर्ग शोधत आहोत",
        "trackingcategories-summary": "या पानात ते रेखापथनातील वर्ग(tracking categories) आहेत, जे, मिडियाविकि संचेतनाद्वारे स्वयंचलितरित्या वसविण्यात (तयार करण्यात) आले आहेत. त्यांची नावे, {{ns:8}} नामविश्वातील संबंधित प्रणाली संदेशात फेरफार करुन, बदलविता येतात.",
        "trackingcategories-name": "संदेश नाम",
        "wlshowhideanons": "अनामिक सदस्य",
        "wlshowhidepatr": "पहारा केलेली संपादने",
        "wlshowhidemine": "माझी संपादने",
+       "wlshowhidecategorization": "पानांचे वर्गीकरण",
        "watchlist-options": "पहाऱ्याच्या सूचीचे पर्याय",
        "watching": "पहारा देत आहे...",
        "unwatching": "पहारा काढत आहे...",
        "javascripttest-pagetext-frameworks": "कृपया टेस्टिंग साठी पुढील पैकी व्यवस्था / पद्धत निवडावी: $1",
        "javascripttest-pagetext-skins": "टेस्ट करण्यासाठी योग्य ती स्कीन निवडावी",
        "javascripttest-qunit-intro": "mediawiki.org वर [$1 testing documentation] पहा",
-       "tooltip-pt-userpage": "तुमचे सदस्य पान",
+       "tooltip-pt-userpage": "{{GENDER:|आपले सदस्य}} पान",
        "tooltip-pt-anonuserpage": "तुम्ही ज्या अंकपत्त्यान्वये संपादित करत आहात त्याकरिता हे सदस्य पान",
-       "tooltip-pt-mytalk": "तुमचे चर्चा पान",
+       "tooltip-pt-mytalk": "{{GENDER:|आपले}} चर्चा पान",
        "tooltip-pt-anontalk": "या अंकपत्त्यापासून झालेल्या संपादनांबद्दल चर्चा",
-       "tooltip-pt-preferences": "तुमचा पसंतीक्रम",
+       "tooltip-pt-preferences": "{{GENDER:|आपला}} पसंतीक्रम",
        "tooltip-pt-watchlist": "तुम्ही पहारा दिलेल्या पानांची यादी",
-       "tooltip-pt-mycontris": "आपल्या योगदानांची यादी",
+       "tooltip-pt-mycontris": "{{GENDER:|आपल्या}} योगदानांची यादी",
        "tooltip-pt-anoncontribs": "या अंकपत्त्यावरुन झालेले संपादन",
        "tooltip-pt-login": "आपणांस सनोंद प्रवेशासाठी प्रोत्साहीत करण्यात येत आहे;अर्थातच, ते अनिवार्य नाही.",
        "tooltip-pt-logout": "सनोंद निर्गम",
        "tooltip-t-recentchangeslinked": "या पानास जोडलेल्या सर्व पानांवरील अलीकडील बदल",
        "tooltip-feed-rss": "या पानाकरिता आर.एस.एस. रसद",
        "tooltip-feed-atom": "या पानाकरिता अॅटम रसद",
-       "tooltip-t-contributions": "या सदस्याच्या योगदानांची यादी पहा",
-       "tooltip-t-emailuser": "या सदस्याला ई-मेल पाठवा",
+       "tooltip-t-contributions": "{{GENDER:$1|या सदस्याच्या}} योगदानांची यादी",
+       "tooltip-t-emailuser": "{{GENDER:$1|या सदस्याला}} विपत्र पाठवा",
        "tooltip-t-info": "या पानाबाबत अधिक माहिती",
        "tooltip-t-upload": "संचिकेचे अपभारण करा",
        "tooltip-t-specialpages": "सर्व विशेष पृष्ठांची यादी",
        "tags-activate": "सक्रीय करा",
        "tags-deactivate": "निष्क्रिय करा",
        "tags-hitcount": "$1 {{PLURAL:$1|बदल|बदल}}",
+       "tags-manage-blocked": "आपण प्रतिबंधित असतांना बदल खूणपताकांचे व्यवस्थापन करु शकत नाही.",
        "tags-create-heading": "नवीन बिल्ला तयार करा",
        "tags-create-tag-name": "खूणपताकेचे नाव:",
        "tags-create-reason": "कारण:",
        "tags-deactivate-reason": "कारण:",
        "tags-deactivate-not-allowed": "\"$1\" खूणपताकेस अक्रिय करणे शक्य नाही.",
        "tags-deactivate-submit": "निष्क्रिय करा",
+       "tags-apply-blocked": "आपण प्रतिबंधित असतांना आपल्या बदलांसह, बदल खूणपताकांना  लागू करु शकत नाही.",
+       "tags-update-blocked": "आपण प्रतिबंधित असतांना बदल खूणपताकांना जोडू अथवा हटवू शकत नाही.",
        "tags-edit-reason": "कारण:",
        "tags-edit-none-selected": "जोडण्यास किंवा हटविण्यास किमान एक खूणपताका निवडा.",
        "comparepages": "पानांची तुलना करा",
        "mw-widgets-dateinput-no-date": "कोणताही दिनांक निवडला नाही",
        "mw-widgets-titleinput-description-new-page": "अद्याप पान अस्तित्वात नाही",
        "mw-widgets-titleinput-description-redirect": "$1ला पुनर्निर्देशित करा",
-       "api-error-blacklisted": "कुपया वेगळे वर्णनात्मक शीर्षक निवडा"
+       "api-error-blacklisted": "कुपया वेगळे वर्णनात्मक शीर्षक निवडा",
+       "sessionmanager-tie": "हे एकत्रित करु शकत नाही,बहुविध विनंती अधिप्रमाणन प्रकार:$1",
+       "sessionprovider-generic": "$1 सत्रे",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "कुकी-आधारीत सत्रे",
+       "sessionprovider-nocookies": "कुकिज अक्षम असू शकतात. याची खात्री करा कि कुकिज सक्षम केल्या आहेत व पुन्हा सुरुवात करा."
 }
index 466e5cb..450fc64 100644 (file)
        "virus-scanfailed": "scanziona fallita (codece $1)",
        "virus-unknownscanner": "antivirus scanusciuto:",
        "logouttext": "'''Site asciùte.'''\n\nNota ca arcune paggene putessero cuntinuà ad cumparì comme se 'o logout nun fosse affettuato fin quanno nun sarrà pulezzata 'a cache d\"o proprio browser.",
+       "cannotlogoutnow-title": "Nun se pò ascì mò",
+       "cannotlogoutnow-text": "'A disconessione nun è possibbele quanno s'ausa $1.",
        "welcomeuser": "Bemmenuto, $1!",
        "welcomecreation-msg": "'O cunto vuosto è stato criato.\nMo' putite cagnà 'e [[Special:Preferences|preferenze 'e {{SITENAME}}]].",
        "yourname": "Nomme utente",
        "remembermypassword": "Allicuordate d\"a password (for a maximum of $1 {{PLURAL:$1|day|days}})",
        "userlogin-remembermypassword": "Mantienime cullegato",
        "userlogin-signwithsecure": "Usa na conessione sicura",
+       "cannotloginnow-title": "Nun se pò trasì mò",
+       "cannotloginnow-text": "'A connessione nun è possibbele quanno s'ausa $1.",
        "yourdomainname": "Spiecà 'o dumminio",
        "password-change-forbidden": "Nun se ponno cagnà 'e password ncopp'a sta wiki.",
        "externaldberror": "Ce sta n'errore ch' 'e server d'autenticazione esterno, o pure nun v'è permesso accedere all'aghiurnamento d' 'o cunto sterno vuosto.",
        "resetpass_submit": "Stabbelite 'a password e trasite",
        "changepassword-success": "'A password è stata cagnata currettamente!",
        "changepassword-throttled": "Songo state fatte troppe tentative 'a trasì.\nAspetta nu $1 apprimma 'e pruvà n'ata vota.",
+       "botpasswords": "Password bot",
+       "botpasswords-summary": "<em>Password bot</em> premmettesse 'e trasì a n'utente pe' bbìa d'API senza ausà 'e credenziale d'acciesso prencepale 'e ll'utenza. 'E deritte utente disponibbele quanno s'affettuasse l'accesso cu na password 'e bot se ponn'apprisentà lemmetate.\n\nSi nun canuscite 'o mutivo p' 'o quale 'o putite fà, allora può darse ca nun l'avissev'a ffà. Nisciuno avesse maje 'addimannà 'e crià una 'e chiste p' 'e ddà a chille.",
+       "botpasswords-disabled": "'E password 'e bot songo stutate.",
+       "botpasswords-no-central-id": "Pe' puté ausà na password bot, avit'a trasì dint'a nu cunto centralizzato.",
+       "botpasswords-existing": "Password bot esistente",
+       "botpasswords-createnew": "Crèa na password bot nòva",
+       "botpasswords-editexisting": "Cagna na password bot esistente",
+       "botpasswords-label-appid": "Nomme d' 'o bot:",
+       "botpasswords-label-create": "Crèa",
+       "botpasswords-label-update": "Agghiuorna",
+       "botpasswords-label-cancel": "Canciella",
+       "botpasswords-label-delete": "Scancèlla",
+       "botpasswords-label-resetpassword": "Riabbìa 'a password",
+       "botpasswords-label-grants": "Assegnaziune apprecabbele:",
+       "botpasswords-help-grants": "Ogne assegnazione dà acciesso a 'e deritte utente elencate ca n'utenza avesse già. Vedite 'a [[Special:ListGrants|tabbella 'e ll'assegnaziune]] pe' ne mòvere cchiù nfurmaziune.",
+       "botpasswords-label-restrictions": "Restriziune d'uso:",
+       "botpasswords-label-grants-column": "Assegnaziune date",
+       "botpasswords-bad-appid": "'O nomme bot \"$1\" nun è bbuono.",
+       "botpasswords-insert-failed": "Nun se pò azzeccà 'o nomme bot \"$1\". Fosse stato già azzeccato?",
+       "botpasswords-update-failed": "Se scassaje a carrecà 'o nomme bot \"$1\". È stato scancellato?",
+       "botpasswords-created-title": "Password bot criata",
+       "botpasswords-updated-title": "Password bot agghiurnata",
+       "botpasswords-deleted-title": "Password bot scancellata",
+       "botpasswords-deleted-body": "'A password bot \"$1\" è stata scancellata.",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider nun è disponibbele.",
+       "botpasswords-restriction-failed": "'E restriziune 'e password bot nun ve permettessero st'acciesso.",
        "resetpass_forbidden": "'E password nun se ponno cagnà",
        "resetpass-no-info": "Avite 'a trasì ('o login) pe ffà l'acciesso a sta paggena direttamente.",
        "resetpass-submit-loggedin": "Cagna password",
        "right-createpage": "Crìa paggene (ca nun songo paggene 'e chiacchiera)",
        "right-createtalk": "Crìa chiacchiere 'e paggena",
        "right-createaccount": "Crìa utenze nove",
+       "right-autocreateaccount": "Tràse automaticamente cu n'utenza 'e fore",
        "right-minoredit": "Nzigna 'e cagnamiente comme minure",
        "right-move": "Muove 'e paggene",
        "right-move-subpages": "Muove 'e paggene nzieme ê sottopaggene suje",
        "right-managechangetags": "Crìa e scancella 'e [[Special:Tags|tag]] d' 'o database",
        "right-applychangetags": "Appreca [[Special:Tags|tag]] pe' tramente ca se fanno 'e cagnamiente 'e coccheruno",
        "right-changetags": "Azzecca o lèva a caso 'e [[Special:Tags|tag]] dint'a verziune nnividuale e riggistre 'e log",
+       "grant-generic": "Pacco 'e deritte \"$1\"",
+       "grant-group-page-interaction": "Interagisce ch' 'e paggene",
+       "grant-group-file-interaction": "Intergische ch' 'e file 'e media",
+       "grant-group-watchlist-interaction": "Interagisce cu l'elenco 'e cuntrollo vuosto",
+       "grant-group-email": "Manna e-mail",
+       "grant-group-high-volume": "Secuta attività 'e volume massivo",
+       "grant-group-customization": "Personalizzaziona e preferenze",
+       "grant-group-administration": "Secuta aziune ammenistrative",
+       "grant-group-other": "Attività differénte",
+       "grant-blockusers": "Blocca e sblocca utente",
+       "grant-createaccount": "Crìa cunte",
        "newuserlogpage": "Riggistro 'e nuove utente",
        "newuserlogpagetext": "Chest'è nu riggistro 'e criazione d'utenze.",
        "rightslog": "Deritte 'e ll'utente",
index 006c20f..664c61b 100644 (file)
@@ -46,7 +46,8 @@
                        "Macofe",
                        "Kingu",
                        "Tarjeimo",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "SuperPotato"
                ]
        },
        "tog-underline": "Strek under lenker:",
@@ -81,6 +82,7 @@
        "tog-watchlisthidebots": "Skjul robotendringer fra overvåkningslisten",
        "tog-watchlisthideminor": "Skjul mindre endringer fra overvåkningslisten",
        "tog-watchlisthideliu": "Skjul endringer av innloggede brukere fra overvåkningslisten",
+       "tog-watchlistreloadautomatically": "Oppdater oversiktslisten automatisk når et filter er endret (JavaScript kreves)",
        "tog-watchlisthideanons": "Skjul endringer av anonyme brukere fra overvåkningslisten",
        "tog-watchlisthidepatrolled": "Skjul patruljerte endringer fra overvåkningslisten",
        "tog-watchlisthidecategorization": "Skjul kategorisering av sider",
        "virus-scanfailed": "skanning mislyktes (kode $1)",
        "virus-unknownscanner": "ukjent antivirusprogram:",
        "logouttext": "'''Du er nå logget ut.'''\n\nVær oppmerksom på at noen sider kan fortsette å dukke opp som om du fortsatt var innlogget, helt til du nullstiller nettleserens mellomlager (cache).",
+       "cannotlogoutnow-title": "Kan ikke logge ut nå",
+       "cannotlogoutnow-text": "Å logge ut er ikke mulig ved bruk av $1.",
        "welcomeuser": "Velkommen $1!",
        "welcomecreation-msg": "Kontoen din har blitt opprettet.\nIkke glem å endre [[Special:Preferences|innstillingene dine]] på {{SITENAME}}.",
        "yourname": "Brukernavn:",
        "remembermypassword": "Husk meg på denne datamaskinen (i maks $1 {{PLURAL:$1|dag|dager}})",
        "userlogin-remembermypassword": "Hold meg innlogget",
        "userlogin-signwithsecure": "Logg inn med sikker tjener",
+       "cannotloginnow-title": "Kan ikke logge inn nå",
+       "cannotloginnow-text": "Å logge inn er ikke mulig ved bruk av $1.",
        "yourdomainname": "Ditt domene",
        "password-change-forbidden": "Du kan ikke endre passord på denne wikien.",
        "externaldberror": "Det var en ekstern autentifiseringsfeil, eller du kan ikke oppdatere din eksterne konto.",
        "wrongpasswordempty": "Du oppga ikke noe passord. Prøv igjen.",
        "passwordtooshort": "Passord må ha minst {{PLURAL:$1|ett tegn|$1 tegn}}.",
        "passwordtoolong": "Passord kan ikke overskride {{PLURAL:$1|1 character|$1 characters}}.",
+       "passwordtoopopular": "Nylig valgt passord kan ikke brukes. Vennligst bruk et mer unikt passord.",
        "password-name-match": "Passord og brukernavn kan ikke være det samme.",
        "password-login-forbidden": "Bruken av dette brukernavnet og passordet er forbudt.",
        "mailmypassword": "Tilbakestill passord",
        "right-managechangetags": "Opprette og slette [[Special:Tags|tagger]] fra databasen",
        "right-applychangetags": "Legg til [[Special:Tags|merker]] sammen med ens endringer",
        "right-changetags": "Legg til og fjern vilkårlige [[Special:Tags|merker]] på individuelle revisjoner og loggposter",
+       "grant-generic": "Rettighetspakken «$1»",
+       "grant-group-page-interaction": "Interagere med sider",
+       "grant-group-file-interaction": "Interagere med media",
+       "grant-group-watchlist-interaction": "Interagere med overvåkningslisten din",
+       "grant-group-email": "Sende e-post",
+       "grant-group-high-volume": "Utføre høyvolumaktivitet",
+       "grant-group-customization": "Tilpasninger og innstillinger",
+       "grant-group-administration": "Utføre administrative handlinger",
+       "grant-group-other": "Andre ting",
+       "grant-blockusers": "Blokkere og avblokkere brukere",
+       "grant-createaccount": "Opprette kontoer",
+       "grant-createeditmovepage": "Opprette, redigere eller flytte sider",
+       "grant-delete": "Slette sider, revisjoner og logginnlegg",
+       "grant-editinterface": "Redigere i MediaWiki-navnerommet og CSS/JavaScript i brukernavnerommet",
+       "grant-editmycssjs": "Redigere din bruker-CSS/JavaScript",
+       "grant-editmyoptions": "Rediger dine brukerinnstillinger",
+       "grant-editmywatchlist": "Redigere overvåkningslisten din",
+       "grant-editpage": "Redigere eksisterende sider",
+       "grant-editprotected": "Redigere beskyttede sider",
+       "grant-highvolume": "Høyvolumredigering",
+       "grant-oversight": "Skjule brukere og undertrykke revisjoner",
+       "grant-patrol": "Patruljere sideendringer",
+       "grant-protect": "Beskytte og avbeskytte sider",
+       "grant-rollback": "Tilbakestille sideendringer",
+       "grant-sendemail": "Sende e-post til andre brukere",
+       "grant-uploadeditmovefile": "Laste opp, erstatte, og flytte filer",
+       "grant-uploadfile": "Laste opp nye filer",
+       "grant-viewdeleted": "Vise slettede filer og sider",
+       "grant-viewmywatchlist": "Vise overvåkningslisten din",
        "newuserlogpage": "Brukeropprettelseslogg",
        "newuserlogpagetext": "Dette er en logg over brukeropprettelser.",
        "rightslog": "Brukerrettighetslogg",
        "listgrouprights-namespaceprotection-header": "Navneromsbegrensinger",
        "listgrouprights-namespaceprotection-namespace": "Navnerom",
        "listgrouprights-namespaceprotection-restrictedto": "Rettighet(er) som tillater at brukeren redigerer",
+       "listgrants-summary": "Følgende er en liste over OAuth-tildelinger og hvilke brukerrettigheter de gir tilgang til. Brukere kan autorisere applikasjoner til å bruke kontoen deres, med rettigheter begrenset til de gitt av tildelingene brukeren har godkjent. En applikasjon som handler på vegne av en bruker kan imidlertid aldri benytte seg av rettigheter brukeren ikke selv har.\nDet kan finnes [[{{MediaWiki:Listgrouprights-helppage}}|ytterligere informasjon]] om de ulike rettighetene.",
+       "listgrants-rights": "Tildeling",
        "trackingcategories": "Sporingskategori",
        "trackingcategories-summary": "Denne siden lister sporingskategorier som er automatisk befolket av Mediawiki-programvaren. Navnene deres kan endres ved å redigere de tilhørende systembeskjedene i {{ns:8}}-navnerommet.",
        "trackingcategories-msg": "Sporingskategori",
index 04ff6de..73ab92b 100644 (file)
        "right-managechangetags": "डाटाबेसबाट [[Special:Tags|tags]] बनाउने र हटाउने",
        "right-applychangetags": "एकको परिवर्तन सहित [[Special:Tags|tags]] लागु गर्ने",
        "right-changetags": "जोड्ने र हटाउने स्वतन्त्र [[Special:Tags|ट्याग]] व्यक्तिगत अवतरणहरू र लग इन्ट्रीहरूमा",
+       "grant-createeditmovepage": "पृष्ठहरूमा परिवर्तन गर्नुहोस्",
+       "grant-editmycssjs": "तपाईंको प्रयोगकर्ता CSS/JavaScript सम्पादन गर्नुहोस्",
+       "grant-editmyoptions": "तपाईंको प्रयोगकर्ता अभिरूचीहरूलाई सम्पादन गर्नुहोस्",
+       "grant-viewdeleted": "मेटाइएका फाइल तथा पृष्ठहरू हेर्ने",
        "newuserlogpage": "प्रयोगकर्ता श्रृजना लग",
        "newuserlogpagetext": "यो प्रयोगकर्ता सिर्जनाको लग हो ।",
        "rightslog": "प्रयोगकर्ता अधिकार लग",
        "pagelang-language": "भाषा",
        "pagelang-use-default": "पूर्वनिर्धारित भाषा प्रयोग गर्ने",
        "pagelang-select-lang": "भाषा छान्ने",
+       "pagelang-submit": "बुझाउने",
        "right-pagelang": "पृष्ठको भाषा परिवर्तन गर्ने",
        "action-pagelang": "यस पृष्ठको भाषा परिवर्तन गर्ने",
        "log-name-pagelang": "लगको भाषा परिवर्तन गर्ने",
        "mediastatistics-header-text": "पाठ",
        "mediastatistics-header-executable": "कार्यान्वयन गर्न मिल्नेहरू",
        "mediastatistics-header-archive": "संकुचित ढाँचाहरू",
+       "mediastatistics-header-total": "सबै फाइलहरु",
        "json-warn-trailing-comma": "$1 पछाडी रहेको छ {{PLURAL:$1|कोमा को|कोमाहरूको}} जेएसओएनबाट हटाइयो",
        "json-error-unknown": "जेएसओएन मा समस्या छ । समस्याः $1",
        "json-error-depth": "स्ट्याकको अधिकतम गहिराई बढी सकेको छ",
index 74df170..64de448 100644 (file)
        "category_header": "Pagina’s in categorie \"$1\"",
        "subcategories": "Ondercategorieën",
        "category-media-header": "Media in categorie \"$1\"",
-       "category-empty": "''Deze categorie bevat geen pagina’s of media.''",
+       "category-empty": "<em>Deze categorie bevat geen pagina’s of media.</em>",
        "hidden-categories": "Verborgen {{PLURAL:$1|categorie|categorieën}}",
        "hidden-category-category": "Verborgen categorieën",
        "category-subcat-count": "{{PLURAL:$2|Deze categorie bevat de volgende ondercategorie.|Deze categorie bevat de volgende {{PLURAL:$1|ondercategorie|$1 ondercategorieën}}, van een totaal van $2.}}",
        "laggedslavemode": "<strong>Waarschuwing:</strong> in deze pagina zijn recente wijzigingen mogelijk nog niet verwerkt.",
        "readonly": "Database geblokkeerd",
        "enterlockreason": "Geef een reden op voor de blokkade en geef op wanneer die waarschijnlijk wordt opgeheven",
-       "readonlytext": "De database is geblokkeerd voor bewerkingen, waarschijnlijk voor regulier databaseonderhoud. Na afronding wordt de functionaliteit hersteld.\n\nDe beheerder heeft de volgende reden opgegeven: $1",
+       "readonlytext": "De database is geblokkeerd voor bewerkingen, waarschijnlijk voor regulier databaseonderhoud. Na afronding wordt de functionaliteit hersteld.\n\nDe systeembeheerder heeft de volgende reden opgegeven: $1",
        "missing-article": "In de database is geen inhoud aangetroffen voor de pagina \"$1\" die er wel zou moeten zijn ($2).\n\nDit kan voorkomen als u een verouderde koppeling naar het verschil tussen twee versies van een pagina volgt of een versie opvraagt die is verwijderd.\n\nAls dit niet het geval is, hebt u wellicht een fout in de software gevonden.\nMaak hiervan melding bij een [[Special:ListUsers/sysop|beheerder]] van {{SITENAME}} en vermeld daarbij de URL van deze pagina.",
        "missingarticle-rev": "(versienummer: $1)",
        "missingarticle-diff": "(Wijziging: $1, $2)",
        "virus-scanfailed": "scannen is mislukt (code $1)",
        "virus-unknownscanner": "onbekend antivirusprogramma:",
        "logouttext": "<strong>U bent nu afgemeld.</strong>\n\nSommige pagina's kunnen blijven weergegeven alsof u nog aangemeld bent, totdat u uw browsercache leegt.",
+       "cannotlogoutnow-title": "Niet mogelijk om nu uit te loggen",
+       "cannotlogoutnow-text": "Uitloggen is niet mogelijk bij het gebruik van $1.",
        "welcomeuser": "Welkom, $1!",
        "welcomecreation-msg": "Uw account is aangemaakt.\nIndien gewenst kunt u uw [[Special:Preferences|voorkeuren]] voor {{SITENAME}} aanpassen.",
        "yourname": "Gebruikersnaam:",
        "remembermypassword": "Aanmeldgegevens onthouden (maximaal $1 {{PLURAL:$1|dag|dagen}})",
        "userlogin-remembermypassword": "Aangemeld blijven",
        "userlogin-signwithsecure": "Beveiligde verbinding gebruiken",
+       "cannotloginnow-title": "Niet mogelijk om nu in te loggen",
+       "cannotloginnow-text": "Inloggen is niet mogelijk bij het gebruik van $1.",
        "yourdomainname": "Uw domein:",
        "password-change-forbidden": "U kunt uw wachtwoord niet wijzigen in deze wiki.",
        "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming uw externe gebruiker bij te werken.",
        "resetpass_submit": "Wachtwoord instellen en aanmelden",
        "changepassword-success": "Uw wachtwoord is gewijzigd.",
        "changepassword-throttled": "U heeft recentelijk te veel mislukte aanmeldpogingen gedaan.\nWacht alstublieft $1 voordat u het opnieuw probeert.",
+       "botpasswords": "Botwachtwoorden",
+       "botpasswords-disabled": "Botwachtwoorden zijn uitgeschakeld.",
+       "botpasswords-no-central-id": "Om botwachtwoorden te gebruiken, moet u ingelogd zijn met een gecentraliseerd account",
+       "botpasswords-existing": "Bestaande botwachtwoorden",
+       "botpasswords-createnew": "Een nieuw botwachtwoord aanmaken",
+       "botpasswords-label-appid": "Naam van bot:",
+       "botpasswords-label-create": "Aanmaken",
+       "botpasswords-label-update": "Bijwerken",
+       "botpasswords-label-cancel": "Annuleren",
+       "botpasswords-label-delete": "Verwijderen",
+       "botpasswords-label-resetpassword": "Het wachtwoord opnieuw instellen",
+       "botpasswords-label-restrictions": "Gebruiksbeperkingen:",
+       "botpasswords-label-grants-column": "Toegewezen",
+       "botpasswords-bad-appid": "De botnaam \"$1\" is niet geldig.",
+       "botpasswords-insert-failed": "Toevoegen van botnaam \"$1\" mislukt. Is deze misschien al toegevoegd?",
+       "botpasswords-update-failed": "Bijwerken van botnaam \"$1\" mislukt. Is deze misschien verwijderd?",
+       "botpasswords-created-title": "Botwachtwoord aangemaakt",
+       "botpasswords-created-body": "Het botwachtwoord \"$1\" is succesvol aangemaakt.",
+       "botpasswords-updated-title": "Botwachtwoord bijgewerkt",
+       "botpasswords-updated-body": "Het botwachtwoord \"$1\" is succesvol bijgewerkt.",
+       "botpasswords-deleted-title": "Botwachtwoord verwijderd",
+       "botpasswords-deleted-body": "Het botwachtwoord \"$1\" is verwijderd.",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider is niet beschikbaar.",
+       "botpasswords-restriction-failed": "Botwachtwoord-beperkingen maken het aanmelden onmogelijk.",
+       "botpasswords-not-exist": "Gebruiker \"$1\" heeft geen botwachtwoord genaamd \"$2\"",
        "resetpass_forbidden": "Wachtwoorden kunnen niet gewijzigd worden",
        "resetpass-no-info": "U dient aangemeld zijn voordat u deze pagina kunt gebruiken.",
        "resetpass-submit-loggedin": "Wachtwoord wijzigen",
        "passwordreset-text-many": "{{PLURAL:$1|Vul een van de gegevensvelden in om per e-mail een tijdelijk wachtwoord te ontvangen.}}",
        "passwordreset-disabled": "Het is in deze wiki niet mogelijk uw wachtwoord opnieuw in te stellen.",
        "passwordreset-emaildisabled": "E-mailmogelijkheden zijn uitgeschakeld op deze wiki.",
-       "passwordreset-username": "Gebruiker:",
+       "passwordreset-username": "Gebruikersnaam:",
        "passwordreset-domain": "Domein:",
        "passwordreset-capture": "De resulterende e-mail bekijken?",
        "passwordreset-capture-help": "Als u dit vakje aanvinkt, wordt de e-mail (met het tijdelijke wachtwoord) naar de gebruiker verzonden en ook aan u weergegeven.",
        "passwordreset-emailtext-ip": "Iemand, waarschijnlijk u, heeft vanaf het IP-adres $1 een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. De volgende {{PLURAL:$3|gebruiker is|gebruikers zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}. Meld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
        "passwordreset-emailtext-user": "Gebruiker $1 op de site {{SITENAME}} heeft een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. De volgende {{PLURAL:$3|gebruiker is|gebruikers zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}.\nMeld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
        "passwordreset-emailelement": "Gebruikersnaam: \n$1\n\nTijdelijk wachtwoord: \n$2",
-       "passwordreset-emailsentemail": "Als dit een geregistreerd e-mailadres is voor uw account, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
+       "passwordreset-emailsentemail": "Als dit een e-mailadres is dat gekoppeld is aan uw account, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
        "passwordreset-emailsentusername": "Als er een e-mailadres geregistreerd is voor die gebruikersnaam, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
        "passwordreset-emailsent-capture": "Er is een e-mail voor het opnieuw instellen van een wachtwoord verzonden. Deze wordt hieronder weergegeven.",
        "passwordreset-emailerror-capture": "Er is een e-mail voor het opnieuw instellen van een wachtwoord aangemaakt. Deze wordt hieronder weergegeven. Het verzenden naar de {{GENDER:$2|gebruiker}} is mislukt om de volgende reden: $1",
        "loginreqtitle": "Aanmelden verplicht",
        "loginreqlink": "aanmelden",
        "loginreqpagetext": "U moet zich $1 om andere pagina's te kunnen bekijken.",
-       "accmailtitle": "Wachtwoord verzonden.",
+       "accmailtitle": "Wachtwoord verzonden",
        "accmailtext": "Een willekeurig gegenereerd wachtwoord voor [[User talk:$1|$1]] is verzonden naar $2. Het kan worden gewijzigd op de pagina \"[[Special:ChangePassword|wachtwoord wijzigen]]\" na het aanmelden.",
        "newarticle": "(Nieuw)",
        "newarticletext": "Deze pagina bestaat niet.\nTyp in het onderstaande veld om de pagina aan te maken (meer informatie staat op de [$1 hulppagina]).\nGebruik de knop <strong>Terug</strong> in uw browser als u hier per ongeluk terecht bent gekomen.",
        "sitejspreview": "'''Dit is alleen een voorvertoning van de JavaScriptcode.'''\n'''Deze is nog niet opgeslagen!'''",
        "userinvalidcssjstitle": "'''Waarschuwing:''' er is geen uiterlijk \"$1\".\nUw eigen .css- en .js-pagina's beginnen met een kleine letter, bijvoorbeeld {{ns:user}}:Naam/vector.css in plaats van {{ns:user}}:Naam/Vector.css.",
        "updated": "(Bijgewerkt)",
-       "note": "'''Opmerking:'''",
+       "note": "<strong>Opmerking:</strong>",
        "previewnote": "'''Let op: dit is een controlepagina.'''\nUw tekst is niet opgeslagen!",
        "continue-editing": "Naar het bewerkingsvenster gaan",
        "previewconflict": "Deze voorvertoning geeft aan hoe de tekst in het bovenste veld eruit ziet als u deze opslaat.",
        "copyrightwarning2": "Al uw bijdragen aan {{SITENAME}} kunnen bewerkt, gewijzigd of verwijderd worden door andere gebruikers.\nAls u niet wilt dat uw teksten rigoureus aangepast worden door anderen, plaats ze hier dan niet.<br />\nU belooft ook dat u de oorspronkelijke auteur bent van dit materiaal of dat u het hebt gekopieerd uit een bron in het publieke domein of een soortgelijke vrije bron (zie $1 voor details).\n'''Gebruik geen materiaal dat beschermd wordt door auteursrecht, tenzij u daarvoor toestemming hebt!'''",
        "editpage-cannot-use-custom-model": "Het inhoudsmodel van deze pagina kan niet worden gewijzigd.",
        "longpageerror": "'''Fout: de tekst die u hebt toegevoegd is {{PLURAL:$1|één kilobyte|$1 kilobyte}} groot, wat groter is dan het maximum van {{PLURAL:$2|één kilobyte|$2 kilobyte}}.'''\nOpslaan is niet mogelijk.",
-       "readonlywarning": "'''Waarschuwing: u kunt deze bewerking nu niet opslaan omdat de database is geblokkeerd voor bewerkingen wegens onderhoudswerkzaamheden.'''\nHet is misschien verstandig om uw tekst tijdelijk in een tekstbestand op te slaan om dit te bewaren voor wanneer de blokkering van de database opgeheven is.\n\nEen beheerder heeft de database geblokkeerd om de volgende reden: $1",
+       "readonlywarning": "<strong>Waarschuwing: u kunt deze bewerking nu niet opslaan omdat de database is geblokkeerd voor bewerkingen wegens onderhoudswerkzaamheden.</strong>\nHet is misschien verstandig om uw tekst tijdelijk in een tekstbestand op te slaan om dit te bewaren voor wanneer de blokkering van de database opgeheven is.\n\nDe systeembeheerder heeft de database geblokkeerd om de volgende reden: $1",
        "protectedpagewarning": "'''Waarschuwing: deze beveiligde pagina kan alleen door gebruikers met beheerdersrechten bewerkt worden.'''\nDe laatste logboekregel staat hieronder:",
        "semiprotectedpagewarning": "'''Let op:''' deze pagina is beveiligd en kan alleen door geregistreerde gebruikers bewerkt worden.\nDe laatste logboekregel staat hieronder:",
        "cascadeprotectedwarning": "<strong>Waarschuwing:</strong> deze pagina is beveiligd en kan alleen door beheerders bewerkt worden, omdat ze is opgenomen in de volgende {{PLURAL:$1|pagina|pagina's}} die beveiligd {{PLURAL:$1|is|zijn}} met de cascade-optie:",
        "edit-gone-missing": "De pagina is niet bijgewerkt.\nDeze lijkt verwijderd te zijn.",
        "edit-conflict": "Bewerkingsconflict.",
        "edit-no-change": "Uw bewerking is genegeerd, omdat er geen wijziging aan de tekst is gemaakt.",
-       "postedit-confirmation-created": "De pagina is gemaakt.",
+       "postedit-confirmation-created": "De pagina is aangemaakt.",
        "postedit-confirmation-restored": "De pagina is hersteld.",
-       "postedit-confirmation-saved": "Uw bewerking is opgeslagen",
+       "postedit-confirmation-saved": "Uw bewerking is opgeslagen.",
        "edit-already-exists": "De pagina is niet aangemaakt.\nDeze bestaat al.",
        "defaultmessagetext": "Standaardinhoud",
        "content-failed-to-parse": "Het was niet mogelijk de inhoud van het MIME-type $2 voor het model $1 te verwerken: $3.",
        "revdelete-unsuppress": "Beperkingen op teruggeplaatste wijzigingen verwijderen",
        "revdelete-log": "Reden:",
        "revdelete-submit": "Toepassen op de geselecteerde {{PLURAL:$1|bewerking|bewerkingen}}",
-       "revdelete-success": "'''De zichtbaarheid van de wijziging is bijgewerkt.'''",
-       "revdelete-failure": "'''De zichtbaarheid van de wijziging kon niet bijgewerkt worden:'''\n$1",
+       "revdelete-success": "Zichtbaarheid van de wijziging is succesvol bijgewerkt.",
+       "revdelete-failure": "Zichtbaarheid van de wijziging kon niet bijgewerkt worden:\n$1",
        "logdelete-success": "De zichtbaarheid van de gebeurtenis is ingesteld.",
-       "logdelete-failure": "'''De zichtbaarheid van de logboekregel kon niet ingesteld worden:'''\n$1",
-       "revdel-restore": "Zichtbaarheid wijzigen",
+       "logdelete-failure": "Zichtbaarheid van de logboekregel kon niet ingesteld worden:\n$1",
+       "revdel-restore": "zichtbaarheid wijzigen",
        "pagehist": "Geschiedenis",
        "deletedhist": "verwijderde geschiedenis",
        "revdelete-hide-current": "Er is een fout opgetreden bij het verbergen van het object van $1 om $2 uur: dit is de huidige versie.\nDeze versie kan niet verborgen worden.",
        "revdelete-otherreason": "Andere reden:",
        "revdelete-reasonotherlist": "Andere reden",
        "revdelete-edit-reasonlist": "Redenen voor verwijderen bewerken",
-       "revdelete-offender": "Auteur versie:",
+       "revdelete-offender": "Auteur van versie:",
        "suppressionlog": "Verbergingslogboek",
        "suppressionlogtext": "De onderstaande lijst bevat de verwijderingen en blokkades die voor beheerders verborgen zijn.\nIn de [[Special:BlockList|blokkadelijst]] zijn de huidige blokkades te bekijken.",
        "mergehistory": "Geschiedenis van pagina's samenvoegen",
        "prefs-common-css-js": "Gedeelde CSS/JavaScript voor elke vormgeving:",
        "prefs-reset-intro": "Gebruik deze functie om uw voorkeuren te herstellen naar de standaardinstellingen.\nDeze handeling kan niet ongedaan gemaakt worden.",
        "prefs-emailconfirm-label": "E-mailbevestiging:",
-       "youremail": "Uw e-mailadres:",
+       "youremail": "E-mailadres:",
        "username": "{{GENDER:$1|Gebruikersnaam}}:",
        "prefs-memberingroups": "{{GENDER:$2|Lid}} van {{PLURAL:$1|groep|groepen}}:",
        "prefs-registration": "Registratiedatum:",
-       "yourrealname": "Uw echte naam:",
+       "yourrealname": "Echte naam:",
        "yourlanguage": "Taal:",
        "yourvariant": "Taalvariant voor inhoud:",
        "prefs-help-variant": "Uw voorkeursvariant of -spelling om de inhoudspagina's van deze wiki in weer te geven.",
        "right-createpage": "Pagina's aanmaken",
        "right-createtalk": "Overlegpagina's aanmaken",
        "right-createaccount": "Nieuwe gebruikers aanmaken",
+       "right-autocreateaccount": "Automatisch inloggen met een extern gebruikersaccount",
        "right-minoredit": "Bewerkingen als klein markeren",
        "right-move": "Pagina's hernoemen",
        "right-move-subpages": "Pagina's inclusief subpagina's verplaatsen",
        "right-managechangetags": "[[Special:Tags|Labels]] aan de database toevoegen of eruit verwijderen",
        "right-applychangetags": "[[Special:Tags|Labels]] aan bewerkingen toewijzen",
        "right-changetags": "Willekeurige [[Special:Tags|labels]] toevoegen aan en verwijderen van versies en logboekregels",
+       "grant-generic": "Rechtengroep \"$1\"",
+       "grant-group-page-interaction": "Werken met pagina's",
+       "grant-group-file-interaction": "Werken met media",
+       "grant-group-watchlist-interaction": "Werken met uw volglijst",
+       "grant-group-email": "E-mail verzenden",
+       "grant-group-high-volume": "Activiteiten met hoog volume uitvoeren",
+       "grant-group-customization": "Aanpassingen en voorkeuren",
+       "grant-group-administration": "Beheerdershandelingen uitvoeren",
+       "grant-group-other": "Diverse handelingen",
+       "grant-blockusers": "Gebruikers (de)blokkeren",
+       "grant-createaccount": "Gebruikers aanmaken",
+       "grant-createeditmovepage": "Pagina's aanmaken, bewerken en hernoemen",
+       "grant-delete": "Pagina's, wijzigingen en vermeldingen in het logboek verwijderen",
+       "grant-editinterface": "De naamruimte MediaWiki en CSS en JavaScript van gebruikers bewerken",
+       "grant-editmycssjs": "Uw eigen CSS/JavaScript bewerken",
+       "grant-editmyoptions": "Eigen voorkeuren instellen",
+       "grant-editmywatchlist": "Uw eigen volglijst bewerken",
+       "grant-editpage": "Bestaande pagina's bewerken",
+       "grant-editprotected": "Beveiligde pagina's bewerken",
+       "grant-highvolume": "Veel bewerkingen in korte tijd maken",
+       "grant-oversight": "Gebruikers en versies verbergen",
+       "grant-patrol": "Wijzigingen aan pagina's controleren",
+       "grant-protect": "Pagina's beveiligen en beveiliging opheffen",
+       "grant-rollback": "Wijzigingen aan pagina's terugdraaien",
+       "grant-sendemail": "E-mail verzenden aan andere gebruikers",
+       "grant-uploadeditmovefile": "Bestanden uploaden, vervangen en hernoemen",
+       "grant-uploadfile": "Nieuwe bestanden uploaden",
+       "grant-basic": "Basisrechten",
+       "grant-viewdeleted": "Verwijderde bestanden en pagina's bekijken",
+       "grant-viewmywatchlist": "Uw volglijst bekijken",
        "newuserlogpage": "Logboek nieuwe gebruikers",
        "newuserlogpagetext": "Hieronder staan de nieuw ingeschreven gebruikers",
        "rightslog": "Gebruikersrechtenlogboek",
        "action-createpage": "pagina's aan te maken",
        "action-createtalk": "overlegpagina's aan te maken",
        "action-createaccount": "deze gebruiker aan te maken",
+       "action-autocreateaccount": "dit externe gebruikersaccount automatisch aanmaken",
        "action-history": "de geschiedenis van deze pagina te bekijken",
        "action-minoredit": "deze bewerking als klein te markeren",
        "action-move": "deze pagina te hernoemen",
        "upload-form-label-select-file": "Selecteer bestand",
        "upload-form-label-infoform-title": "Details",
        "upload-form-label-infoform-name": "Naam",
+       "upload-form-label-infoform-name-tooltip": "Een korte beschrijvende naam voor het bestand, die als de bestandsnaam wordt gebruikt. U kunt platte tekst met spaties gebruiken. Neem de bestandsextensie niet op.",
        "upload-form-label-infoform-description": "Beschrijving",
        "upload-form-label-usage-title": "Gebruik",
        "upload-form-label-usage-filename": "Bestandsnaam",
        "protectedpages-performer": "Beveiligd door",
        "protectedpages-params": "Beveiligingsopties",
        "protectedpages-reason": "Reden",
+       "protectedpages-submit": "Pagina's weergeven",
        "protectedpages-unknown-timestamp": "Onbekend",
        "protectedpages-unknown-performer": "Onbekende gebruiker",
        "protectedtitles": "Beveiligde paginanamen",
        "protectedtitles-summary": "Deze pagina bevat een lijst met pagina's die niet mogen worden aangemaakt. Zie [[{{#special:ProtectedPages}}|{{int:protectedpages}}]] voor de lijst met beveiligde pagina's.",
        "protectedtitlesempty": "Er zijn geen paginanamen beveiligd die aan deze voorwaarden voldoen.",
+       "protectedtitles-submit": "Paginanamen weergeven",
        "listusers": "Gebruikerslijst",
        "listusers-editsonly": "Alleen gebruikers met bewerkingen weergeven",
        "listusers-creationsort": "Sorteren op registratiedatum",
        "listgrouprights-namespaceprotection-header": "Naamruimtebeperkingen",
        "listgrouprights-namespaceprotection-namespace": "Naamruimte",
        "listgrouprights-namespaceprotection-restrictedto": "Recht(en) waardoor gebruiker kan bewerken",
+       "listgrants": "Toestemmingen",
+       "listgrants-summary": "Hieronder staat een lijst met toestemmingen en de bijbehorende gebruikersrechten. Gebruikers kunnen toepassingen machtigen voor toegang tot hun gebruikers, maar met beperkte rechten gebaseerd op de toestemmingen die de gebruiker aan de toepassing heeft gegeven. Een toepassing die namens een gebruiker handelt, kan nooit rechten gebruiken die een gebruiker niet heeft.\nEr zijn mogelijk [[{{MediaWiki:Listgrouprights-helppage}}|aanvullende  gegevens]] over individuele rechten.",
+       "listgrants-grant": "Toestemming",
+       "listgrants-rights": "Rechten",
        "trackingcategories": "Volgcategorieën",
        "trackingcategories-summary": "Op deze pagina worden volgcategorieën weergegeven die automatisch worden gevuld door de MediaWikisoftware. De namen van de categorieën kunnen gewijzigd worden door de bijbehorende systeemberichten in de naamruimte \"{{ns:8}}\" bij te werken.",
        "trackingcategories-msg": "Volgcategorie",
        "wlshowhideanons": "anonieme gebruikers",
        "wlshowhidepatr": "gecontroleerde bewerkingen",
        "wlshowhidemine": "mijn bewerkingen",
+       "wlshowhidecategorization": "categorisatie van pagina's",
        "watchlist-options": "Opties voor volglijst",
        "watching": "Bezig met plaatsen op volglijst…",
        "unwatching": "Bezig met verwijderen van volglijst…",
        "unblock": "Gebruiker deblokkeren",
        "blockip": "{{GENDER:$1|Gebruiker}} blokkeren",
        "blockip-legend": "Gebruiker blokkeren",
-       "blockiptext": "Gebruik het onderstaande formulier om schrijftoegang voor een gebruiker of IP-adres in te trekken.\nDoe dit alleen als bescherming tegen vandalisme en in overeenstemming met het [[{{MediaWiki:Policy-url}}|beleid]].\nGeef hieronder een reden op (bijvoorbeeld welke pagina's gevandaliseerd zijn).",
+       "blockiptext": "Gebruik het onderstaande formulier om schrijftoegang voor een gebruiker of IP-adres in te trekken.\nDoe dit alleen als bescherming tegen vandalisme en in overeenstemming met het [[{{MediaWiki:Policy-url}}|beleid]].\nGeef hieronder een reden op (bijvoorbeeld welke pagina's gevandaliseerd zijn).\nU kunt IP-ranges blokkeren door gebruik te maken van de [https://nl.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]-syntax, tot een maximum range van /$1 voor IPv4 en /$2 voor IPv6.",
        "ipaddressorusername": "IP-adres of gebruikersnaam:",
        "ipbexpiry": "Vervalt (maak een keuze):",
        "ipbreason": "Reden:",
        "export-download": "Als bestand opslaan",
        "export-templates": "Sjablonen toevoegen",
        "export-pagelinks": "Pagina's waarnaar verwezen wordt toevoegen tot een diepte van:",
+       "export-manual": "Pagina's handmatig toevoegen:",
        "allmessages": "Systeemteksten",
        "allmessagesname": "Naam",
        "allmessagesdefault": "Standaardinhoud",
        "javascripttest-pagetext-frameworks": "Kies een van de volgende testframeworks: $1",
        "javascripttest-pagetext-skins": "Kies een vormgeving om de tests mee uit te voeren:",
        "javascripttest-qunit-intro": "Zie de [$1 testdocumentatie] op mediawiki.org.",
-       "tooltip-pt-userpage": "Uw gebruikerspagina",
+       "tooltip-pt-userpage": "{{GENDER:|Uw}} gebruikerspagina",
        "tooltip-pt-anonuserpage": "Gebruikerspagina voor uw IP-adres",
-       "tooltip-pt-mytalk": "Uw overlegpagina",
+       "tooltip-pt-mytalk": "{{GENDER:|Uw}} overlegpagina",
        "tooltip-pt-anontalk": "Overlegpagina van de anonieme gebruiker van dit IP-adres",
-       "tooltip-pt-preferences": "Mijn voorkeuren",
+       "tooltip-pt-preferences": "{{GENDER:|Uw}} voorkeuren",
        "tooltip-pt-watchlist": "Overzicht van pagina's die u volgt",
-       "tooltip-pt-mycontris": "Overzicht van uw bijdragen",
+       "tooltip-pt-mycontris": "Overzicht van {{GENDER:|uw}} bijdragen",
        "tooltip-pt-anoncontribs": "Een lijst van bewerkingen gemaakt door dit IP-adres",
        "tooltip-pt-login": "U wordt van harte uitgenodigd om aan te melden, maar dit is niet verplicht",
        "tooltip-pt-logout": "Afmelden",
        "tooltip-t-recentchangeslinked": "Recente wijzigingen in pagina's waar deze pagina naar verwijst",
        "tooltip-feed-rss": "RSS-feed voor deze pagina",
        "tooltip-feed-atom": "Atom-feed voor deze pagina",
-       "tooltip-t-contributions": "Een lijst met bijdragen van deze gebruiker",
-       "tooltip-t-emailuser": "Een e-mail naar deze gebruiker verzenden",
+       "tooltip-t-contributions": "Een lijst met bijdragen van {{GENDER:$1|deze gebruiker}}",
+       "tooltip-t-emailuser": "Een e-mail naar {{GENDER:$1|deze gebruiker}} verzenden",
        "tooltip-t-info": "Meer informatie over deze pagina",
        "tooltip-t-upload": "Bestanden uploaden",
        "tooltip-t-specialpages": "Lijst met alle speciale pagina's",
        "pageinfo-category-files": "Aantal bestanden",
        "markaspatrolleddiff": "Als gecontroleerd markeren",
        "markaspatrolledtext": "Deze pagina als gecontroleerd markeren",
+       "markaspatrolledtext-file": "Deze bestandsversie als gecontroleerd markeren",
        "markedaspatrolled": "Gemarkeerd als gecontroleerd",
        "markedaspatrolledtext": "De geselecteerde bewerking van [[:$1]] is gemarkeerd als gecontroleerd.",
        "rcpatroldisabled": "De controlemogelijkheid op recente wijzigingen is uitgeschakeld.",
        "newimages-legend": "Bestandsnaam",
        "newimages-label": "Bestandsnaam (of deel daarvan):",
        "newimages-showbots": "Uploads door bots weergeven",
+       "newimages-hidepatrolled": "Gecontroleerde uploads verbergen",
        "noimages": "Er is niets te zien.",
        "ilsubmit": "Zoeken",
        "bydate": "op datum",
        "exif-compression-4": "CCITT Groep 4 faxcodering",
        "exif-copyrighted-true": "Auteursrechtelijk beschermd",
        "exif-copyrighted-false": "Auteursrechtelijke status niet ingesteld",
+       "exif-photometricinterpretation-1": "Zwart-wit (zwart is 0)",
        "exif-unknowndate": "Datum onbekend",
        "exif-orientation-1": "Normaal",
        "exif-orientation-2": "Horizontaal gespiegeld",
        "tags-deactivate": "deactiveren",
        "tags-hitcount": "$1 {{PLURAL:$1|wijziging|wijzigingen}}",
        "tags-manage-no-permission": "U hebt geen rechten om labels te beheren.",
+       "tags-manage-blocked": "U kunt geen labels beheren wanneer u geblokkeerd bent.",
        "tags-create-heading": "Een nieuw label aanmaken",
        "tags-create-explanation": "Standaard worden nieuw aangemaakte labels beschikbaar gesteld voor gebruik door gebruikers en bots.",
        "tags-create-tag-name": "Labelnaam:",
        "tags-deactivate-not-allowed": "Het is niet mogelijk het label \"$1\" te deactiveren.",
        "tags-deactivate-submit": "Deactiveren",
        "tags-apply-no-permission": "U hebt geen rechten om wijzigingslabels toe te voegen aan uw wijzigingen.",
+       "tags-apply-blocked": "U kunt geen wijzigen aanbrengen aan labels wanneer u geblokkeerd bent.",
        "tags-apply-not-allowed-one": "Het label \"$1\" mag niet handmatig toegevoegd worden.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|Het volgende label mag|De volgende labels mogen}} niet handmatig toegevoegd worden: $1",
        "tags-update-no-permission": "U hebt geen rechten om wijzigingslabels toe te voegen aan of te verwijderen van versies of logboekregels.",
+       "tags-update-blocked": "U kunt geen labels toevoegen of verwijderen wanneer u geblokkeerd bent.",
        "tags-update-add-not-allowed-one": "Het label \"$1\" mag niet handmatig toegevoegd worden.",
        "tags-update-add-not-allowed-multi": " {{PLURAL:$2|Het label kan|De labels kunnen}} niet handmatig toegevoegd worden: $1",
        "tags-update-remove-not-allowed-one": "Het label \"$1\" mag niet verwijderd worden.",
        "expand_templates_preview": "Voorvertoning",
        "expand_templates_preview_fail_html": "<em>Omdat voor {{SITENAME}} ruwe HTML is ingeschakeld, en er sessiegegevens verloren zijn gegaan, is de voorvertoning verborgen als voorzorgmaatregel tegen JavaScriptaanvallen.</em>\n\n<strong>Als dit een legitieme poging is voor het weergeven van een voorvertoning, probeer het dan opnieuw.</strong>\nAls het dan nog steeds niet werkt, probeer dan [[Special:UserLogout|af te melden]] en opnieuw aan te melden.",
        "expand_templates_preview_fail_html_anon": "<em>Omdat in {{SITENAME}} ruwe HTML is ingeschakeld en u niet bent aangemeld, is de voorvertoning verborgen als voorzorgsmaatregel tegen de JavaScriptaanvallen.</em>\n\n<strong>Als dit een legitieme poging is voor het maken van een voorvertoning, [[Special:UserLogin|meld u dan aan]] en probeer het opnieuw.</strong>",
+       "expand_templates_input_missing": "U moet wel iets invullen.",
        "pagelanguage": "Paginataal kiezen",
        "pagelang-name": "Pagina",
        "pagelang-language": "Taal",
        "mediastatistics": "Mediastatistieken",
        "mediastatistics-summary": "Statistieken over geüploade bestandstypen. Dit overzicht bevat alleen de meest recente versie van een bestand. Oude of verwijderde versies worden niet meegeteld.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%)",
+       "mediastatistics-bytespertype": "Totale bestandsgrootte van deze sectie: {{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%).",
+       "mediastatistics-allbytes": "Totale bestandsgrootte van alle bestanden: {{PLURAL:$1|$1 byte|$1 bytes}} ($2).",
        "mediastatistics-table-mimetype": "MIME-type",
        "mediastatistics-table-extensions": "Mogelijke extensies",
        "mediastatistics-table-count": "Aantal bestanden",
        "mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
        "mw-widgets-titleinput-description-new-page": "pagina bestaat nog niet",
        "mw-widgets-titleinput-description-redirect": "doorverwijzing naar $1",
-       "api-error-blacklisted": "Kies een andere, beschrijvende naam."
+       "api-error-blacklisted": "Kies een andere, beschrijvende naam.",
+       "sessionprovider-generic": "$1 sessies",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sessies gebaseerd op cookies",
+       "sessionprovider-nocookies": "Cookies kunnen uitgeschakeld zijn. Zorg ervoor dat u cookies hebt ingeschakeld en probeer het opnieuw."
 }
index f88f127..10893a1 100644 (file)
        "resetpass_submit": "Ustaw hasło i zaloguj się",
        "changepassword-success": "Twoje hasło zostało pomyślnie zmienione!",
        "changepassword-throttled": "Ostatnio zbyt wiele razy próbowałeś zalogować się na to konto.\nOdczekaj $1, zanim ponowisz próbę.",
+       "botpasswords-label-appid": "Nazwa bota:",
+       "botpasswords-label-create": "Utwórz",
+       "botpasswords-label-update": "Aktualizuj",
+       "botpasswords-label-cancel": "Anuluj",
+       "botpasswords-label-delete": "Usuń",
+       "botpasswords-label-resetpassword": "Zresetuj hasło",
        "resetpass_forbidden": "Hasła nie mogą zostać zmienione",
        "resetpass-no-info": "Musisz być zalogowany, by uzyskać bezpośredni dostęp do tej strony.",
        "resetpass-submit-loggedin": "Zmień hasło",
        "right-managechangetags": "Tworzenie i usuwanie [[Special:Tags|znaczników]] z bazy danych",
        "right-applychangetags": "Wprowadzanie [[Special:Tags|znaczników]] wraz z własnymi zmianami",
        "right-changetags": "Dodawanie i usuwanie dowolnych [[Special:Tags|znaczników]] z poszczególnych wersji i wpisów w rejestrze",
+       "grant-group-page-interaction": "Interakcja ze stronami",
+       "grant-group-file-interaction": "Interakcja z plikami multimedialnymi",
+       "grant-group-watchlist-interaction": "Interakcja z listą obserwowanych",
+       "grant-group-email": "Wyślij e‐mail",
+       "grant-group-high-volume": "Czynności na dużą skalę",
+       "grant-group-customization": "Dostosowywanie i preferencje",
+       "grant-group-administration": "Czynności administracyjne",
+       "grant-group-other": "Różne czynności",
+       "grant-blockusers": "Blokowanie i odblokowywanie użytkowników",
+       "grant-createaccount": "Utwórz konta",
+       "grant-createeditmovepage": "Tworzenie, edycja i przenoszenie stron",
+       "grant-delete": "Usuwanie stron, usuwanie określonych wersji stron i usuwanie wpisów rejestru",
+       "grant-editinterface": "Edycja przestrzeni nazw MediaWiki oraz CSS/JavaScript użytkownika",
+       "grant-editmycssjs": "Edycja swoich plików CSS/JavaScript",
+       "grant-editmyoptions": "Edycja swoich preferencji",
+       "grant-editmywatchlist": "Edytuj listę obserwowanych",
+       "grant-editpage": "Edytowanie istniejących stron",
+       "grant-editprotected": "Edycja stron zabezpieczonych",
+       "grant-highvolume": "Masowe edytowanie",
+       "grant-protect": "Zabezpieczanie i odbezpieczanie stron",
+       "grant-rollback": "Wycofywanie zmian na stronach",
+       "grant-sendemail": "Wysyłanie e‐maili do innych użytkowników",
+       "grant-uploadeditmovefile": "Przesyłanie, zamiana i przenoszenie plików",
+       "grant-uploadfile": "Przesyłanie nowych plików",
+       "grant-basic": "Podstawowe uprawnienia",
+       "grant-viewdeleted": "Wyświetlanie usuniętych plików i stron",
+       "grant-viewmywatchlist": "Zobacz listę obserwowanych",
        "newuserlogpage": "Nowi użytkownicy",
        "newuserlogpagetext": "To jest rejestr ostatnio utworzonych kont użytkowników",
        "rightslog": "Uprawnienia",
        "listgrouprights-namespaceprotection-header": "Ograniczenia przestrzeni nazw",
        "listgrouprights-namespaceprotection-namespace": "Przestrzeń nazw",
        "listgrouprights-namespaceprotection-restrictedto": "Uprawnienia pozwalające użytkownikom na edytowanie",
+       "listgrants-rights": "Uprawnienie",
        "trackingcategories": "Śledzenie kategorii",
        "trackingcategories-summary": "Ta strona zawiera listę kategorii monitorujących wypełnianych automatycznie przez oprogramowanie MediaWiki. Nazwy kategorii można zmienić edytując odpowiednie komunikaty systemowe znajdujące się w przestrzeni nazw {{ns:8}}.",
        "trackingcategories-msg": "Śledzenie kategorii",
        "tooltip-t-recentchangeslinked": "Ostatnie zmiany w stronach, do których ta strona linkuje",
        "tooltip-feed-rss": "Kanał RSS dla tej strony",
        "tooltip-feed-atom": "Kanał Atom dla tej strony",
-       "tooltip-t-contributions": "Pokaż listę edycji tego użytkownika",
-       "tooltip-t-emailuser": "Wyślij e‐mail do tego użytkownika",
+       "tooltip-t-contributions": "Pokaż listę edycji {{GENDER:$1|tego użytkownika|tej użytkowniczki}}",
+       "tooltip-t-emailuser": "Wyślij e‐mail do {{GENDER:$1|tego użytkownika|tej użytkowniczki}}",
        "tooltip-t-info": "Więcej informacji na temat tej strony",
        "tooltip-t-upload": "Prześlij pliki",
        "tooltip-t-specialpages": "Lista wszystkich specjalnych stron",
index 2937564..c0f7213 100644 (file)
        "resetpass_submit": "پټنوم مو وټاکۍ او بيا غونډال ته ورننوځۍ",
        "changepassword-success": "ستاسې پټنوم په برياليتوب سره بدل شو!",
        "changepassword-throttled": "تاسې څو واره هڅه کړې چې غونډال ته ورننوځۍ.\nلطفاً د بيا هڅې نه مخکې $1 شېبې تم شۍ.",
+       "botpasswords-label-appid": "روباټ نوم:",
+       "botpasswords-label-create": "جوړول",
+       "botpasswords-label-update": "اوسمهالول",
+       "botpasswords-label-cancel": "ناگارل",
+       "botpasswords-label-delete": "ړنگول",
        "resetpass_forbidden": "پټنومونه مو نه شي بدلېدلای",
        "resetpass-no-info": "دې مخ ته د لاسرسي لپاره بايد غونډال کې ورننوځۍ.",
        "resetpass-submit-loggedin": "پټنوم بدلول",
        "right-siteadmin": "توکبنسټ کولپول او پرانيستل",
        "right-sendemail": "نورو کارنانو ته برېښليک لېږل",
        "right-passwordreset": "د پټنوم بياپرځايولو برېښليکونه کتل",
+       "grant-blockusers": "په کارنانو بنديز لگول او بنديز ليرې کول",
+       "grant-delete": "مخونه، بياکتنې، او يادښتليکونه ړنگول",
+       "grant-basic": "بنسټيزې رښتې",
        "newuserlogpage": "د کارن-نوم د جوړېدو يادښت",
        "newuserlogpagetext": "دا د کارن-نوم د جوړېدو يادښت دی",
        "rightslog": "د کارن رښتو يادښت",
        "javascripttest": "د جاوا سکرېپټ آزمېښت",
        "javascripttest-pagetext-unknownaction": "ناڅرگنده کړنه \"$1\".",
        "javascripttest-pagetext-skins": "د آزمېښتونو د پلي کولو لپاره يوه پوښۍ وټاکئ:",
-       "tooltip-pt-userpage": "ستاسې کارن مخ",
-       "tooltip-pt-mytalk": "ستاسې د خبرواترو مخ",
-       "tooltip-pt-preferences": "زما غوره توبونه",
+       "tooltip-pt-userpage": "{{GENDER:|ستاسې کارن}} مخ",
+       "tooltip-pt-mytalk": "{{GENDER:|ستاسې}} د خبرواترو مخ",
+       "tooltip-pt-preferences": "{{GENDER:|ستاسې}} غوره توبونه",
        "tooltip-pt-watchlist": "د هغه مخونو لړليک چې تاسې يې د بدلون لپاره څاری",
-       "tooltip-pt-mycontris": "ستاسې د ونډو لړليک",
+       "tooltip-pt-mycontris": "{{GENDER:|ستاسې}} د ونډو يو لړليک",
        "tooltip-pt-login": "تاسې ته په غونډال کې د ننوتلو سپارښتنه کوو، که څه هم چې دا يو اړين کار نه دی.",
        "tooltip-pt-logout": "وتل",
        "tooltip-pt-createaccount": "تاسې ته د يو گڼون د جوړولو او بيا غونډال کې ورننوتلو سپارښتنه کېږي؛ که څه هم چې دا يو اړين کار نه دی",
        "tooltip-t-recentchangeslinked": "له دې مخ سره د تړل شويو مخونو وروستي بدلونونه",
        "tooltip-feed-rss": "د همدې مخ د آر اس اس کتنه",
        "tooltip-feed-atom": "د دې مخ د اټوم کتنې",
-       "tooltip-t-contributions": "د دې کارن د ونډو لړليک کتل",
-       "tooltip-t-emailuser": "دې کارن ته يو برېښليک لېږل",
+       "tooltip-t-contributions": "{{GENDER:$1|د دې کارن}} د ونډو يو لړليک",
+       "tooltip-t-emailuser": "{{GENDER:$1|دې کارن}} ته يو برېښليک لېږل",
        "tooltip-t-info": "د دې مخ اړونده نور مالومات",
        "tooltip-t-upload": "دوتنې پورته کول",
        "tooltip-t-specialpages": "د ټولو ځانگړو پاڼو لړليک",
        "exif-giffilecomment": "د GIF دوتنې تبصره",
        "exif-copyrighted-true": "په رښتو سمبال",
        "exif-copyrighted-false": "د خپراوي د رښتو دريځ نه دی ټاکل شوی",
+       "exif-photometricinterpretation-1": "تور او سپين (تور 0 دی)",
        "exif-unknowndate": "ناڅرگنده نېټه",
        "exif-orientation-1": "نورمال",
        "exif-componentsconfiguration-0": "نشته دی",
        "hebrew-calendar-m6": "آدار",
        "hebrew-calendar-m10-gen": "تموز",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|خبرې اترې]])",
+       "timezone-local": "سيمه ايز",
        "duplicate-defaultsort": "'''گواښنه:'''د \"$2\" تلواليزه اوډون تڼۍ تر دې پخوا ټاکلې تلواليزه اوډون تڼۍ \"$1\" پر ځای چارنه کېږي.",
        "version": "بڼه",
        "version-extensions": "لگېدلي شاتاړي",
index b7556e0..8b260c8 100644 (file)
        "right-managechangetags": "Criar e apagar [[Special:Tags|tags]] na base de dados",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetas]] juntamente com as alterações de alguém",
        "right-changetags": "Adicionar e remover [[Special:Tags|etiquetas]] arbitrárias em revisões e ''logs'' individuais",
+       "grant-group-page-interaction": "Interagir com páginas",
+       "grant-group-watchlist-interaction": "Interagir com sua lista de páginas vigiadas",
+       "grant-group-email": "Enviar e-mail",
+       "grant-group-high-volume": "Realizar grande volume de atividades",
+       "grant-group-customization": "Personalização e preferências",
+       "grant-group-administration": "Realizar ações administrativas",
+       "grant-group-other": "Atividade diversa",
+       "grant-blockusers": "Bloquear e desbloquear usuários",
+       "grant-createaccount": "Criar contas",
+       "grant-createeditmovepage": "Fazer alterações nas páginas",
+       "grant-delete": "Excluir páginas, revisões e entradas de registro",
+       "grant-editmyoptions": "Editar suas preferências de usuário",
+       "grant-editmywatchlist": "Editar sua lista de páginas vigiadas",
+       "grant-editpage": "Editar páginas existentes",
+       "grant-editprotected": "Editar páginas protegidas",
+       "grant-highvolume": "Edição de grandes volumes",
+       "grant-oversight": "Ocultar usuários e revisões suprimidas",
+       "grant-protect": "Proteger e desproteger páginas",
+       "grant-rollback": "Reverter alterações nas páginas",
+       "grant-sendemail": "Enviar e-mail a outros usuários",
+       "grant-uploadeditmovefile": "Enviar, substituir e mover arquivos",
+       "grant-uploadfile": "Enviar novos arquivos",
+       "grant-viewdeleted": "Ver páginas e arquivos excluídos",
+       "grant-viewmywatchlist": "Veja sua lista de páginas vigiadas",
        "newuserlogpage": "Registro de criação de usuários",
        "newuserlogpagetext": "Este é um registro de novas contas de usuário",
        "rightslog": "Registro de privilégios de usuários",
index 7075c2d..057420c 100644 (file)
        "right-managechangetags": "Criar e eliminar [[Special:Tags|etiquetas]] da base de dados",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetas]] juntamente com as alterações",
        "right-changetags": "Adicionar ou remover [[Special:Tags|etiquetas]] arbitrárias em revisões e entradas de registo individuais",
+       "grant-generic": "Pacote de direitos \"$1\"",
+       "grant-group-page-interaction": "Interagir com páginas",
+       "grant-group-file-interaction": "Interagir com multimédia",
+       "grant-group-watchlist-interaction": "Interagir com a sua lista de vigiados",
+       "grant-group-email": "Enviar correio electrónico",
+       "grant-group-high-volume": "Realizar actividades em grande quantidade",
+       "grant-group-customization": "Personalização e preferências",
+       "grant-group-administration": "Executar acções administrativas",
+       "grant-group-other": "Actividade diversa",
+       "grant-blockusers": "Bloquear e desbloquear utilizadores",
+       "grant-createaccount": "Criar contas",
+       "grant-createeditmovepage": "Criar, editar e mover páginas",
+       "grant-delete": "Eliminar páginas, revisões e entradas de registo",
+       "grant-editinterface": "Editar o domínio MediaWiki e o CSS/JavaScript do utilizador",
+       "grant-editmycssjs": "Editar o seu CSS/JavaScript personalizado",
+       "grant-editmyoptions": "Editar as suas preferências de utilizador",
+       "grant-editmywatchlist": "Editar a sua lista de vigiados",
+       "grant-editpage": "Editar páginas existentes",
+       "grant-editprotected": "Editar páginas protegidas",
+       "grant-highvolume": "Alta quantidade de edições",
+       "grant-oversight": "Ocultar utilizadores e edições suprimidas",
+       "grant-patrol": "Patrulhar alterações a páginas",
+       "grant-protect": "Proteger e desproteger páginas",
+       "grant-rollback": "Reverter alterações a páginas",
+       "grant-sendemail": "Enviar correio electrónico a outros utilizadores",
+       "grant-uploadeditmovefile": "Carregar, substituir e mover ficheiros",
+       "grant-uploadfile": "Carregar novos ficheiros",
+       "grant-viewdeleted": "Ver páginas e ficheiros eliminados",
+       "grant-viewmywatchlist": "Ver a sua lista de vigiados",
        "newuserlogpage": "Registo de criação de utilizadores",
        "newuserlogpagetext": "Este é um registo de novas contas de utilizador",
        "rightslog": "Registo de privilégios de utilizadores",
        "listgrouprights-namespaceprotection-header": "Restrições do domínio",
        "listgrouprights-namespaceprotection-namespace": "Domínio",
        "listgrouprights-namespaceprotection-restrictedto": "Direito(s) do utilizador para editar",
+       "listgrants-rights": "Conceder",
        "trackingcategories": "Categorias de monitorização",
        "trackingcategories-summary": "Esta página lista as categorias monitoradas que foram geradas automaticamente pelo software MediaWiki. Os seus nomes podem ser alterados ao editar sua mensagem correspondente no domínio {{ns:8}}.",
        "trackingcategories-msg": "Categoria monitorada",
        "wlheader-showupdated": "As páginas modificadas desde a última vez que as visitou aparecem destacadas a '''negrito'''.",
        "wlnote": "A seguir {{PLURAL:$1|está a última alteração ocorrida|estão as últimas <strong>$1</strong> alterações ocorridas}} {{PLURAL:$2|na última hora|nas últimas <strong>$2</strong> horas}} até $3, $4.",
        "wlshowlast": "Ver últimas $1 horas $2 dias",
-       "watchlistall2": "todas",
+       "watchlistall2": "desde sempre",
        "watchlist-hide": "Ocultar",
        "watchlist-submit": "Mostrar",
        "wlshowtime": "Período de tempo a mostrar:",
        "export-download": "Gravar em ficheiro",
        "export-templates": "Incluir predefinições",
        "export-pagelinks": "Incluir páginas ligadas, até uma profundidade de:",
+       "export-manual": "Adicionar páginas manualmente:",
        "allmessages": "Mensagens de sistema",
        "allmessagesname": "Nome",
        "allmessagesdefault": "Texto padrão",
        "pageinfo-category-files": "Número de ficheiros",
        "markaspatrolleddiff": "Marcar como patrulhada",
        "markaspatrolledtext": "Marcar esta página como patrulhada",
+       "markaspatrolledtext-file": "Marcar esta versão do ficheiro como patrulhada",
        "markedaspatrolled": "Marcada como patrulhada",
        "markedaspatrolledtext": "A edição selecionada de [[:$1]] foi marcada como patrulhada.",
        "rcpatroldisabled": "Edições patrulhadas nas Mudanças Recentes desativadas",
        "newimages-legend": "Filtrar",
        "newimages-label": "Nome de ficheiro (ou parte dele):",
        "newimages-showbots": "Mostrar carregamentos feitos por robôs",
+       "newimages-hidepatrolled": "Ocultar carregamentos patrulhados",
        "noimages": "Nada para ver.",
        "ilsubmit": "Pesquisar",
        "bydate": "por data",
        "watchlisttools-edit": "Ver e editar a lista de páginas vigiadas",
        "watchlisttools-raw": "Editar a lista de páginas vigiadas em forma de texto",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussão]])",
+       "timezone-local": "Local",
        "duplicate-defaultsort": "<strong>Aviso:</strong> A chave de ordenação padrão \"$2\" sobrepõe-se à anterior \"$1\".",
        "duplicate-displaytitle": "<strong>Aviso:</strong> O título em exibição \"$2\" anula o título anteriormente em exibição \"$1\".",
        "invalid-indicator-name": "<strong>Erro:</strong> O atributo <code>name</code>, da página de estados, não deve estar em branco.",
        "pagelang-language": "Idioma",
        "pagelang-use-default": "Usar idioma pré-definido",
        "pagelang-select-lang": "Escolher o idioma",
+       "pagelang-submit": "Submeter",
        "right-pagelang": "Alterar o idioma da página",
        "action-pagelang": "alterar o idioma da página",
        "log-name-pagelang": "Alterar registo de idioma",
        "mediastatistics": "Estatísticas multimédia",
        "mediastatistics-summary": "Estatísticas sobre os tipos de ficheiros carregados. Inclui apenas a versão mais recente do ficheiro. Versões antigas ou eliminadas são excluídas.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%)",
+       "mediastatistics-bytespertype": "Tamanho total de ficheiro para este secção: {{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%).",
+       "mediastatistics-allbytes": "Tamanho total de ficheiro para todos os ficheiros: {{PLURAL:$1|$1 byte|$1 bytes}} ($2).",
        "mediastatistics-table-mimetype": "Tipo MIME",
        "mediastatistics-table-extensions": "Extensões possíveis",
        "mediastatistics-table-count": "Número de ficheiros",
index 092d14e..8654523 100644 (file)
        "virus-scanfailed": "Used as error message. \"scan\" stands for \"virus scan\". Parameters:\n* $1 - exit code of virus scanner",
        "virus-unknownscanner": "Used as error message. This message is followed by the virus scanner name.",
        "logouttext": "Log out message. Parameters:\n* $1 - (Unused) an URL to [[Special:Userlogin]] containing <code>returnto</code> and <code>returntoquery</code> parameters",
+       "cannotlogoutnow-title": "Error page title shown when logging out is not possible.",
+       "cannotlogoutnow-text": "Error page text shown when logging out is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log out, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
        "welcomeuser": "Text for a welcome heading that users see after registering a user account.\n\nParameters:\n* $1 - the username of the new user. See [[phab:T44215]]",
        "welcomecreation-msg": "A welcome message users see after registering a user account, following a welcomeuser heading.\n\nParameters:\n* $1 - (Unused) the username of the new user.\n\nReplaces [[MediaWiki:welcomecreation|welcomecreation]] in 1.21wmf5, see [[phab:T44215]]",
        "yourname": "Since 1.22 no longer used in core, but used by some extensions.\n{{Identical|Username}}",
        "remembermypassword": "Used as checkbox label on [[Special:ChangePassword]]. Parameters:\n* $1 - number of days\n{{Identical|Remember my login on this computer}}",
        "userlogin-remembermypassword": "The text for a check box in [[Special:UserLogin]].",
        "userlogin-signwithsecure": "Text of link to HTTPS login form.\n\nSee example: [[Special:UserLogin]]",
+       "cannotloginnow-title": "Error page title shown when logging in is not possible.",
+       "cannotloginnow-text": "Error page text shown when logging in is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log in, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
        "yourdomainname": "Used as label for listbox.",
        "password-change-forbidden": "Error message shown when an external authentication source does not allow the password to be changed.",
        "externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
        "resetpass_submit": "Submit button on [[Special:ChangePassword]]",
        "changepassword-success": "Used in [[Special:ChangePassword]].",
        "changepassword-throttled": "Error message shown at [[Special:ChangePassword]] after the user has tried to login with incorrect password too many times.\n\nThe user has to wait a certain time before trying to log in again.\n\nParameters:\n* $1 - the time to wait before the next login attempt. Automatically formatted using the following duration messages:\n** {{msg-mw|Duration-millennia}}\n** {{msg-mw|Duration-centuries}}\n** {{msg-mw|Duration-decades}}\n** {{msg-mw|Duration-years}}\n** {{msg-mw|Duration-weeks}}\n** {{msg-mw|Duration-days}}\n** {{msg-mw|Duration-hours}}\n** {{msg-mw|Duration-minutes}}\n** {{msg-mw|Duration-seconds}}\n\nThis is a protection against robots trying to find the password by trying lots of them.\nThe number of attempts and waiting time are configured via [[mw:Manual:$wgPasswordAttemptThrottle|$wgPasswordAttemptThrottle]].\nThis message is used in html.\n\nSee also:\n* {{msg-mw|Changeemail-throttled}}",
+       "botpasswords": "The name of the special page [[Special:BotPasswords]].",
+       "botpasswords-summary": "Explanatory text shown at the top of [[Special:BotPasswords]].",
+       "botpasswords-disabled": "Error message displayed when bot passwords are not enabled (<code>$wgEnableBotPasswords = false</code>).",
+       "botpasswords-no-central-id": "Error message displayed when the current user does not have a central ID (e.g. they're not logged in or not attached in something like CentralAuth).",
+       "botpasswords-existing": "Form section label for the part of the form listing the user's existing bot passwords.",
+       "botpasswords-createnew": "Form section label for the part of the form related to creating a new bot password.",
+       "botpasswords-editexisting": "Form section label for the part of the form related to editing an existing bot password.",
+       "botpasswords-label-appid": "Form field label for the \"bot name\", internally known as the \"application ID\".",
+       "botpasswords-label-create": "Button label for the button to create a new bot password.\n{{Identical|Create}}",
+       "botpasswords-label-update": "Button label for the button to save changes to a bot password.",
+       "botpasswords-label-cancel": "Button label for a button to cancel the creation or edit of a bot password.\n{{Identical|Cancel}}",
+       "botpasswords-label-delete": "Button label for the button to delete a bot password.\n{{Identical|Delete}}",
+       "botpasswords-label-resetpassword": "Label for the checkbox to reset the actual password for the current bot password.",
+       "botpasswords-label-grants": "Label for the checkmatrix for selecting grants allowed when the bot password is used.",
+       "botpasswords-help-grants": "Help text for the grant selection checkmatrix.",
+       "botpasswords-label-restrictions": "Label for the textarea field in which JSON defining access restrictions (e.g. which IP address ranges are allowed) is entered.",
+       "botpasswords-label-grants-column": "Label for the checkbox column on the checkmatrix for selecting grants allowed when the bot password is used.",
+       "botpasswords-bad-appid": "Used as an error message when an invalid \"bot name\" is supplied on [[Special:BotPasswords]]. Parameters:\n* $1 - The rejected bot name.",
+       "botpasswords-insert-failed": "Error message when saving a new bot password failed. It's likely that the failure was because the user resubmitted the form after a previous successful save. Parameters:\n* $1 - Bot name",
+       "botpasswords-update-failed": "Error message when saving changes to an existing bot password failed. It's likely that the failure was because the user deleted the bot password in another browser window. Parameters:\n* $1 - Bot name",
+       "botpasswords-created-title": "Title of the success page when a new bot password is created.",
+       "botpasswords-created-body": "Success message when a new bot password is created. Parameters:\n* $1 - Bot name",
+       "botpasswords-updated-title": "Title of the success page when a bot password is updated.",
+       "botpasswords-updated-body": "Success message when a bot password is updated. Parameters:\n* $1 - Bot name",
+       "botpasswords-deleted-title": "Title of the success page when a bot password is deleted.",
+       "botpasswords-deleted-body": "Success message when a bot password is deleted. Parameters:\n* $1 - Bot name",
+       "botpasswords-newpassword": "Success message to display the new password when a bot password is created or updated. Parameters:\n* $1 - User name to be used for login.\n* $2 - Password to be used for login.",
+       "botpasswords-no-provider": "Error message when login is attempted but the BotPasswordsSessionProvider is not included in <code>$wgSessionProviders</code>.",
+       "botpasswords-restriction-failed": "Error message when login is rejected because the configured restrictions were not satisfied.",
+       "botpasswords-invalid-name": "Error message when a username lacking the separator character is passed to BotPassword. Parameters:\n* $1 - The separator character.",
+       "botpasswords-not-exist": "Error message when a username exists but does not a bot password for the given \"bot name\". Parameters:\n* $1 - username\n* $2 - bot name",
        "resetpass_forbidden": "Used as error message in changing password. Maybe the external auth plugin won't allow local password changes.",
        "resetpass-no-info": "Error message for [[Special:ChangePassword]].\n\nParameters:\n* $1 (unused) - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description",
        "resetpass-submit-loggedin": "Button on [[Special:ResetPass]] to submit new password.\n\n{{Identical|Change password}}",
        "right-createpage": "{{doc-right|createpage}}\nBasic right to create pages. The right to edit discussion/talk pages is {{msg-mw|right-createtalk}}.",
        "right-createtalk": "{{doc-right|createtalk}}\nBasic right to create discussion/talk pages. The right to edit other pages is {{msg-mw|right-createpage}}.",
        "right-createaccount": "{{doc-right|createaccount}}\nThe right to [[Special:CreateAccount|create a user account]].",
+       "right-autocreateaccount": "{{doc-right|autocreateaccount}}\nThe right to automatically create an account from an external source (e.g. CentralAuth).",
        "right-minoredit": "{{doc-right|minoredit}}\nThe right to use the \"This is a minor edit\" checkbox. See {{msg-mw|minoredit}} for the message used for that checkbox.",
        "right-move": "{{doc-right|move}}\nThe right to move any page that is not protected from moving.\n{{Identical|Move page}}",
        "right-move-subpages": "{{doc-right|move-subpages}}",
        "right-managechangetags": "{{doc-right|managechangetags}}",
        "right-applychangetags": "{{doc-right|applychangetags}}",
        "right-changetags": "{{doc-right|changetags}}",
+       "grant-generic": "Used if the grant name is not defined. Parameters:\n* $1 - grant name\n\nDefined grants (grant name refers: blockusers, createeditmovepage, ...):\n* {{msg-mw|grant-checkuser}}\n* {{msg-mw|grant-blockusers}}\n* {{msg-mw|grant-createaccount}}\n* {{msg-mw|grant-createeditmovepage}}\n* {{msg-mw|grant-delete}}\n* {{msg-mw|grant-editinterface}}\n* {{msg-mw|grant-editmycssjs}}\n* {{msg-mw|grant-editmyoptions}}\n* {{msg-mw|grant-editmywatchlist}}\n* {{msg-mw|grant-editpage}}\n* {{msg-mw|grant-editprotected}}\n* {{msg-mw|grant-highvolume}}\n* {{msg-mw|grant-oversight}}\n* {{msg-mw|grant-patrol}}\n* {{msg-mw|grant-protect}}\n* {{msg-mw|grant-rollback}}\n* {{msg-mw|grant-sendemail}}\n* {{msg-mw|grant-uploadeditmovefile}}\n* {{msg-mw|grant-uploadfile}}\n* {{msg-mw|grant-basic}}\n* {{msg-mw|grant-viewdeleted}}\n* {{msg-mw|grant-viewmywatchlist}}",
+       "grant-group-page-interaction": "{{Related|grant-group}}",
+       "grant-group-file-interaction": "{{Related|grant-group}}",
+       "grant-group-watchlist-interaction": "{{Related|grant-group}}",
+       "grant-group-email": "{{Related|grant-group}}\n{{Identical|E-mail}}",
+       "grant-group-high-volume": "{{Related|grant-group}}",
+       "grant-group-customization": "{{Related|grant-group}}",
+       "grant-group-administration": "{{Related|grant-group}}",
+       "grant-group-other": "{{Related|grant-group}}",
+       "grant-blockusers": "Name for grant \"blockusers\".\n{{Related|grant}}",
+       "grant-createaccount": "Name for grant \"createaccount\".\n{{Related|grant}}",
+       "grant-createeditmovepage": "Name for grant \"createeditmovepage\".\n{{Related|grant}}",
+       "grant-delete": "Name for grant \"delete\".\n{{Related|grant}}",
+       "grant-editinterface": "Name for grant \"editinterface\".\n\n\"JS\" stands for \"JavaScript\".\n{{Related|grant}}",
+       "grant-editmycssjs": "Name for grant \"editmycssjs\".\n\n\"JS\" stands for \"JavaScript\".\n{{Related|grant}}",
+       "grant-editmyoptions": "Name for grant \"editmyoptions\".\n{{Related|grant}}",
+       "grant-editmywatchlist": "Name for grant \"editmywatchlist\".\n{{Related|grant}}\n{{Identical|Edit your watchlist}}",
+       "grant-editpage": "Name for grant \"editpage\".\n{{Related|grant}}",
+       "grant-editprotected": "Name for grant \"editprotected\".\n{{Related|grant}}",
+       "grant-highvolume": "Name for grant \"highvolume\".\n{{Related|grant}}",
+       "grant-oversight": "Name for grant \"oversight\".\n{{Related|grant}}",
+       "grant-patrol": "Name for grant \"patrol\".\n{{Related|grant}}",
+       "grant-protect": "Name for grant \"protect\".\n{{Related|grant}}",
+       "grant-rollback": "Name for grant \"rollback\".\n{{Related|grant}}",
+       "grant-sendemail": "Name for grant \"sendemail\".\n{{Related|grant}}",
+       "grant-uploadeditmovefile": "Name for grant \"uploadeditmovefile\".\n{{Related|grant}}",
+       "grant-uploadfile": "Name for grant \"uploadfile\".\n{{Related|grant}}\n{{Identical|Upload new file}}",
+       "grant-basic": "Name for grant \"basic\".\n{{Related|grant}}",
+       "grant-viewdeleted": "Name for grant \"viewdeleted\".\n{{Related|grant}}",
+       "grant-viewmywatchlist": "Name for grant \"viewmywatchlist\".\n{{Related|grant}}\n{{Identical|View your watchlist}}",
        "newuserlogpage": "{{doc-logpage}}\n\nPart of the \"Newuserlog\" extension. It is both the title of [[Special:Log/newusers]] and the link you can see in [[Special:RecentChanges]].",
        "newuserlogpagetext": "Part of the \"Newuserlog\" extension. It is the description you can see on [[Special:Log/newusers]].",
        "rightslog": "{{doc-logpage}}\n\nIn [[Special:Log]]",
        "action-createpage": "{{Doc-action|createpage}}\n{{Identical|Create page}}",
        "action-createtalk": "{{Doc-action|createtalk}}",
        "action-createaccount": "{{Doc-action|createaccount}}",
+       "action-autocreateaccount": "{{Doc-action|autocreateaccount}}",
        "action-history": "{{Doc-action|history}}",
        "action-minoredit": "{{Doc-action|minoredit}}",
        "action-move": "{{Doc-action|move}}",
        "listgrouprights-namespaceprotection-header": "Shown on [[Special:ListGroupRights]] as the header for the namespace restrictions table.",
        "listgrouprights-namespaceprotection-namespace": "Shown on [[Special:ListGroupRights]] as the 'namespace' column header for the namespace restrictions table.\n{{Identical|Namespace}}",
        "listgrouprights-namespaceprotection-restrictedto": "Shown on [[Special:ListGroupRights]] as the \"right(s) allowing user to edit\" column header for the namespace restrictions table.",
+       "listgrants": "The name of the special page [[Special:ListGrants]].",
+       "listgrants-summary": "Explanatory text shown at the top of the grant/rights mapping table.\n\nRefers to {{msg-mw|Listgrouprights-helppage}}.",
+       "listgrants-grant": "Used as table header for the grant/rights mapping table.\n{{Identical|Grant}}",
+       "listgrants-rights": "Used as table header for the grant/rights mapping table.\n{{Identical|Right}}",
        "trackingcategories": "[[Special:TrackingCategories]] page implementing list of Tracking categories [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]].\n{{Identical|Tracking category}}",
        "trackingcategories-summary": "Description for [[Special:TrackingCategories]] page [[mw:Help:Tracking categories|tracking category]]",
        "trackingcategories-msg": "Header for the message column of the table on [[Special:TrackingCategories]]. This column lists the mediawiki message that controls the tracking category in question.\n{{Identical|Tracking category}}",
        "block-log-flags-hiddenname": "Used as a block log flag in [[Special:Log/block]] and in [[Special:Block]].\n\n{{Related|Block-log-flags}}",
        "range_block_disabled": "Used as error message in [[Special:Block]].\n\nSee also:\n* {{msg-mw|Range block disabled}}\n* {{msg-mw|Ip range invalid}}\n* {{msg-mw|Ip range toolarge}}",
        "ipb_expiry_invalid": "Used as error message in [[Special:Block]].",
+       "ipb_expiry_old": "Used as error message in [[Special:Block]], if the expiry time is in the past.\n{{Identical|protect_expiry_old}}",
        "ipb_expiry_temp": "Warning message displayed on [[Special:BlockIP]] if the option \"hide username\" is selected but the expiry time is not infinite.",
        "ipb_hide_invalid": "Used as error message in [[Special:Block]].\n* $1 - Number of edits (Value of [[mw:Manual:$wgHideUserContribLimit]])",
        "ipb_already_blocked": "{{Identical|$1 is already blocked}}",
        "mw-widgets-dateinput-placeholder-month": "Placeholder displayed in a date input field when it's empty, representing a date format with 4 digits for year and 2 digits for month, separated with hyphens (without a day). This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.",
        "mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
        "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
-       "api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}."
+       "api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}.",
+       "sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
+       "sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.",
+       "sessionprovider-nocookies": "Used to inform the user that sessions may be missing due to lack of cookies."
 }
index 7528cf0..f047b5f 100644 (file)
        "resetpass_submit": "Установить пароль и представиться",
        "changepassword-success": "Ваш пароль был успешно изменён!",
        "changepassword-throttled": "Вы сделали слишком много попыток представиться системе.\nПожалуйста, подождите $1 перед тем, как попробовать снова.",
+       "botpasswords-label-create": "Создать",
+       "botpasswords-label-update": "Обновить",
+       "botpasswords-label-cancel": "Отмена",
+       "botpasswords-label-delete": "Удалить",
+       "botpasswords-label-resetpassword": "Сбросить пароль",
        "resetpass_forbidden": "Пароль не может быть изменён",
        "resetpass-no-info": "Чтобы обращаться непосредственно к этой странице, вам следует представиться системе.",
        "resetpass-submit-loggedin": "Изменить пароль",
        "right-managechangetags": "создание и удаление [[Special:Tags|меток]] из базы данных",
        "right-applychangetags": "применение [[Special:Tags|меток]] вместе со своими правками",
        "right-changetags": "добавление и удаление произвольных [[Special:Tags|меток]] на отдельных правках и записях в журнале",
+       "grant-generic": "Набор прав «$1»",
+       "grant-group-page-interaction": "Взаимодействие со страницами",
+       "grant-group-file-interaction": "Взаимодействие с медиафайлами",
+       "grant-group-watchlist-interaction": "Взаимодействие с вашим списком наблюдения",
+       "grant-group-email": "Отправка писем",
+       "grant-group-high-volume": "Выполнение действий с высокой интенсивностью",
+       "grant-group-customization": "Настройки и предпочтения",
+       "grant-group-administration": "Выполнение административных действий",
+       "grant-group-other": "Разная активность",
+       "grant-blockusers": "Блокировка и разблокировка учётных записей",
+       "grant-createaccount": "Создание учётных записей",
+       "grant-createeditmovepage": "Создание, редактирование и переименование страниц",
+       "grant-delete": "Удаление страниц, правок и записей журнала",
+       "grant-editinterface": "Правка пространства имён MediaWiki и пользовательских CSS/JavaScript",
+       "grant-editmycssjs": "Редактирование ваших пользовательских CSS/JavaScript",
+       "grant-editmyoptions": "Редактирование ваших пользовательских настроек",
+       "grant-editmywatchlist": "Редактирование вашего списка наблюдения",
+       "grant-editpage": "Редактирование существующих страниц",
+       "grant-editprotected": "Редактирование защищённых страниц",
+       "grant-highvolume": "Редактирование с высокой интенсивностью",
+       "grant-oversight": "Сокрытие правок участников и версий страниц",
+       "grant-patrol": "Патрулирование изменений страниц",
+       "grant-protect": "Защита страниц и снятие защиты",
+       "grant-rollback": "Откат изменений страниц",
+       "grant-sendemail": "Отправка электронной почты другим участникам",
+       "grant-uploadeditmovefile": "Загрузка, замена и переименовывание файлов",
+       "grant-uploadfile": "Загрузка новых файлов",
+       "grant-viewdeleted": "Просмотр удалённых файлов и страниц",
+       "grant-viewmywatchlist": "Просмотр вашего списка наблюдения",
        "newuserlogpage": "Журнал регистрации участников",
        "newuserlogpagetext": "Список недавно зарегистрировавшихся участников",
        "rightslog": "Журнал прав участника",
        "listgrouprights-namespaceprotection-header": "Ограничения пространства имён",
        "listgrouprights-namespaceprotection-namespace": "Пространство имён",
        "listgrouprights-namespaceprotection-restrictedto": "Права, позволяющие участнику редактировать",
+       "listgrants": "Разрешения",
+       "listgrants-summary": "Ниже приведён список разрешений с указанием на то, к каким связанным пользовательским правам они дают доступ. Участники могут разрешить приложениям использовать свою учётную запись, но с ограниченными правами на основе разрешений, которые участник предоставляет приложению. Однако, приложение, действующее  от имени участника, на самом деле не сможет воспользоваться правами, отсутствующими у учётной записи.\nОб отдельных правах можно получить [[{{MediaWiki:Listgrouprights-helppage}}|дополнительную  информацию]].",
+       "listgrants-grant": "Разрешение",
+       "listgrants-rights": "Права",
        "trackingcategories": "Отслеживающие категории",
        "trackingcategories-summary": "На этой странице перечислены отслеживающие категории, которые автоматически заполняются программным обеспечением MediaWiki. Их можно переименовать, изменив соответствующие системные сообщения в пространстве имён {{ns:8}}.",
        "trackingcategories-msg": "Отслеживающая категория",
        "javascripttest-pagetext-frameworks": "Пожалуйста, выберите одну из следующих сред тестирования: $1",
        "javascripttest-pagetext-skins": "Выберите тему оформления для запуска тестов:",
        "javascripttest-qunit-intro": "См. [$1 документацию по тестированию] на mediawiki.org.",
-       "tooltip-pt-userpage": "Ваша страница участника",
+       "tooltip-pt-userpage": "{{GENDER:|Ваша}} страница участника",
        "tooltip-pt-anonuserpage": "Страница участника для моего IP",
-       "tooltip-pt-mytalk": "Ваша страница обсуждения",
+       "tooltip-pt-mytalk": "{{GENDER:|Ваша}} страница обсуждения",
        "tooltip-pt-anontalk": "Страница обсуждений для моего IP",
-       "tooltip-pt-preferences": "Ваши настройки",
+       "tooltip-pt-preferences": "{{GENDER:|Ваши}} настройки",
        "tooltip-pt-watchlist": "Список страниц, изменения в которых вы отслеживаете",
-       "tooltip-pt-mycontris": "Список ваших правок",
+       "tooltip-pt-mycontris": "Список {{GENDER:|ваших}} правок",
        "tooltip-pt-anoncontribs": "Список правок, сделанных с этого IP-адреса",
        "tooltip-pt-login": "Здесь можно зарегистрироваться в системе, но это необязательно.",
        "tooltip-pt-logout": "Завершить сеанс работы",
        "tooltip-t-recentchangeslinked": "Последние изменения в страницах, на которые ссылается эта страница",
        "tooltip-feed-rss": "Трансляция в RSS для этой страницы",
        "tooltip-feed-atom": "Трансляция в Atom для этой страницы",
-       "tooltip-t-contributions": "Список страниц, которые изменял этот участник",
-       "tooltip-t-emailuser": "Отправить письмо этому участнику",
+       "tooltip-t-contributions": "Список страниц, которые {{GENDER:$1|изменял этот участник|изменяла эта участница}}",
+       "tooltip-t-emailuser": "Отправить письмо {{GENDER:$1|этому участнику|этой участнице}}",
        "tooltip-t-info": "Подробнее об этой странице",
        "tooltip-t-upload": "Загрузить файлы",
        "tooltip-t-specialpages": "Список служебных страниц",
index 0101f66..5b8a524 100644 (file)
@@ -22,7 +22,8 @@
                        "రాకేశ్వర",
                        "아라",
                        "Macofe",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "రహ్మానుద్దీన్"
                ]
        },
        "tog-underline": "परिसन्धेः अधो रेखाङ्कनम्:",
        "morenotlisted": "एषाऽऽवलिः अपूर्णा अस्ति ।",
        "mypage": "पृष्ठम्",
        "mytalk": "सम्भाषणम्",
-       "anontalk": "à¤\85नà¥\8dतरà¥\8dà¤\9cालसà¤\82वितà¥\8dसमà¥\8dभाषणमà¥\8d",
+       "anontalk": "सम्भाषणम्",
        "navigation": "सञ्चरणम्",
        "and": "&#32;तथा च",
        "qbfind": "अन्विष्यताम्",
        "right-managechangetags": "दत्तांशात् [[Special:Tags|चिह्नानि]] निर्मीयन्ताम्, अपाक्रियन्तां च",
        "right-applychangetags": "[[Special:Tags|चिह्नानि]] एकस्य परिवर्तनेन सह प्रयुञ्जताम् ।",
        "right-changetags": "स्वतन्त्रसंस्करणे, प्रवेशावल्यां च [[Special:Tags|चिह्नानि]] ऐच्छितरीत्या स्थापयतु, निष्कासयतु च",
+       "grant-group-email": "वि-पत्रं प्रेषयतु",
+       "grant-uploadfile": "नवीनसञ्चिकाः आरोप्यन्ताम्",
        "newuserlogpage": "प्रयोक्तृ-सृजन-सूचिका",
        "newuserlogpagetext": "अयं योजकनिर्माणास्य प्रवेशः ।",
        "rightslog": "प्रयोक्तृ-अधिकार-सूचिका",
        "listgrouprights-namespaceprotection-header": "नामाकाशप्रतिबन्धाः",
        "listgrouprights-namespaceprotection-namespace": "नामाकाशः",
        "listgrouprights-namespaceprotection-restrictedto": "सम्पादयितु योजकाय अधिकारदानम्",
+       "listgrants-rights": "अधिकाराः",
        "trackingcategories": "वर्गाणाम् अनुसरणम्",
        "trackingcategories-summary": "एतस्मिन् पृष्ठे आनुपदिकवर्गाणां (tracking) सूची विद्यते । ते आनुपदिकवर्गाः MediaWiki software-संस्थया स्वरचिताः सन्ति । तेषां नामानि परिवर्तयितुं शक्नुमः । नामपरिवर्तयितुं {{ns:8}} इत्यत्र नामाकाशे सम्बन्धिते सन्देशप्रक्रियायां परिवर्तनं करिणीयं भवति ।",
        "trackingcategories-msg": "वर्गाणाम् अनुसरणम्",
        "contributions": "{{GENDER:$1|प्रयोक्तॄणां}} योगदानानि",
        "contributions-title": "$1 कृते सदस्यस्य योगदानानि",
        "mycontris": "योगदानानि",
+       "anoncontribs": "अंशदाता",
        "contribsub2": "($2) कृते {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "\"$1\" इत्यषा सदस्यलेखा पञ्जीकृतं नास्ति ।",
        "nocontribs": "एतादृशयोग्यताभिः समं परिवर्तनानि न दृष्टानि ।",
index 08b592e..9e76258 100644 (file)
        "right-managechangetags": "Ustvarjanje in brisanje [[Special:Tags|oznak]] iz zbirke podatkov",
        "right-applychangetags": "Uveljavitev [[Special:Tags|oznak]] skupaj s spremembami",
        "right-changetags": "Dodajanje in odstranjevanje poljubnih [[Special:Tags|oznak]] na posameznih redakcijah in dnevniških vnosih",
+       "grant-group-page-interaction": "Interakcija s stranmi",
+       "grant-group-file-interaction": "Interakcija z mediji",
+       "grant-group-watchlist-interaction": "Interakcija z vašim spiskom nadzorov",
+       "grant-group-email": "Pošljite e-pošto",
+       "grant-group-high-volume": "Izvedi visokoštevilsko dejanje",
+       "grant-group-customization": "Prilagoditve in nastavitve",
+       "grant-group-administration": "Izvajanje administrativnih dejanj",
+       "grant-group-other": "Druga dejavnost",
+       "grant-createaccount": "Ustvarite račune",
+       "grant-patrol": "Nadzor sprememb strani",
+       "grant-rollback": "Razveljavitev sprememb strani",
+       "grant-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
        "newuserlogpage": "Dnevnik registracij uporabnikov",
        "newuserlogpagetext": "Prikazan je dnevnik nedavnih registracij novih uporabnikov.",
        "rightslog": "Dnevnik uporabniških pravic",
        "javascripttest-pagetext-frameworks": "Prosimo, izberite enega od naslednjih ogrodjev za preizkušanje: $1",
        "javascripttest-pagetext-skins": "Izberite kožo, v kateri želite pognati preizkuse:",
        "javascripttest-qunit-intro": "Oglejte si [$1 dokumentacijo o preizkušanju] na mediawiki.org.",
-       "tooltip-pt-userpage": "Vaša uporabniška stran",
+       "tooltip-pt-userpage": "{{GENDER:|Vaša}} uporabniška stran",
        "tooltip-pt-anonuserpage": "Uporabniška stran IP-naslova, ki ga uporabljate",
-       "tooltip-pt-mytalk": "Vaša pogovorna stran",
+       "tooltip-pt-mytalk": "{{GENDER:|Vaša}} pogovorna stran",
        "tooltip-pt-anontalk": "Pogovor o urejanjih s tega IP-naslova",
-       "tooltip-pt-preferences": "Vaše nastavitve",
+       "tooltip-pt-preferences": "{{GENDER:|Vaše}} nastavitve",
        "tooltip-pt-watchlist": "Seznam strani, katerih spremembe spremljate",
-       "tooltip-pt-mycontris": "Seznam vaših prispevkov",
+       "tooltip-pt-mycontris": "Seznam {{GENDER:|vaših}} prispevkov",
        "tooltip-pt-anoncontribs": "Seznam urejanj s tega IP-naslova",
        "tooltip-pt-login": "Prijava ni obvezna, vendar je zaželena",
        "tooltip-pt-logout": "Odjavite se",
        "tooltip-t-recentchangeslinked": "Zadnje spremembe na s trenutno povezanih straneh",
        "tooltip-feed-rss": "RSS-vir strani",
        "tooltip-feed-atom": "Atom-vir strani",
-       "tooltip-t-contributions": "Preglejte seznam uporabnikovih prispevkov",
-       "tooltip-t-emailuser": "Pošljite uporabniku e-pismo",
+       "tooltip-t-contributions": "Preglejte seznam {{GENDER:$1|uporabnikovih|uporabničinih}} prispevkov",
+       "tooltip-t-emailuser": "Pošljite {{GENDER:$1|uporabniku|uporabnici}} e-pošto",
        "tooltip-t-info": "Več informacij o strani",
        "tooltip-t-upload": "Naložite slike ali predstavnostne datoteke",
        "tooltip-t-specialpages": "Preglejte seznam vseh posebnih strani",
index 17c0220..7fafaed 100644 (file)
        "right-viewmyprivateinfo": "Eegista macluumaadka gaarka ah (sida ciwaanka e-mail-ka, iyo magaca saxda ah)",
        "right-editmyprivateinfo": "Bedel macluumaadka gaarka ah (sida. ciwaanka e-mail-ka , iyo magaca saxda ah)",
        "right-editmyoptions": "Bedelka dooqyada",
+       "grant-createeditmovepage": "Sameeyn, bedel, iyo wareejin bog",
        "newuserlogpage": "Gudagalaha Isticmaale sameeyay",
        "action-read": "akhri boggaan",
        "action-edit": "wax ka bedel boggaan",
        "imagelisttext": "Hoos waxaa yaala liiska '''$1''' {{PLURAL:$1|file|faylalka}} oo u kala soocan $2.",
        "ilsubmit": "Raadi",
        "bydate": "hab taariikheed",
+       "ago": "$1 kahor",
        "metadata-help": "Faylkaan wuxuu leeyahay  faah faahin dheeraad ah,waxaa laga yaabaa in lagu  isticmaalay digital kaamiro ama skanner oo lagu sameeyo sawir ama lagu digitilays gareeyay.\nHadii faylka wax laga badalay sida oo markiisa hore ahaa, waxaa laga yaabaa in ee  faah faahinta faylkaan eesan dhameestirneen.",
        "metadata-fields": "Sawirka qeybihiisa metadata oo ku tixan fariintaan waxaa lagu dari doonaa bogga sawirka meesha laga arko markii miiska metadata la qariyo.Kuwa kale waxaa loo qarinaa sida default.\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",
        "exif-iimcategory": "Qeybta",
index 86fb96b..bac2633 100644 (file)
@@ -65,6 +65,7 @@
        "tog-watchlisthidebots": "Сакриј измене ботова са списка надгледања",
        "tog-watchlisthideminor": "Сакриј мање измене са списка надгледања",
        "tog-watchlisthideliu": "Сакриј измене пријављених корисника са списка надгледања",
+       "tog-watchlistreloadautomatically": "Аутоматски освежи списак надгледања кад год се филтер измени (потребна JavaScript-а)",
        "tog-watchlisthideanons": "Сакриј измене анонимних корисника са списка надгледања",
        "tog-watchlisthidepatrolled": "Сакриј патролиране измене са списка надгледања",
        "tog-watchlisthidecategorization": "Сакриј категоризацију страница",
        "resetpass_submit": "Постави лозинку и пријави ме",
        "changepassword-success": "Ваша лозинка је успешно промењена!",
        "changepassword-throttled": "Превише пута сте покушали да се пријавите.\nМолимо вас да сачекате $1 пре него што покушате поново.",
+       "botpasswords-label-cancel": "Откажи",
+       "botpasswords-label-delete": "Обриши",
        "resetpass_forbidden": "Лозинка не може бити промењена",
        "resetpass-no-info": "Морате бити пријављени да бисте приступили овој страници.",
        "resetpass-submit-loggedin": "Промени лозинку",
        "right-managechangetags": "прављење и/или брисање [[Special:Tags|ознака]] из базе података",
        "right-applychangetags": "примењивање [[Special:Tags|ознака]] на нечије измене",
        "right-changetags": "додавање и уклањање разних [[Special:Tags|ознака]] на појединачним изменама и уносима у дневницима",
+       "grant-group-page-interaction": "Уређивање страница",
+       "grant-group-file-interaction": "Уређивање датотека",
+       "grant-group-watchlist-interaction": "Уређивање вашег списка надгледања",
+       "grant-group-email": "Пошаљи имејл",
+       "grant-createaccount": "Отварање налога",
+       "grant-createeditmovepage": "Прављење, уређивање и премештање страница",
+       "grant-delete": "Брисање страница, измена и уноса у дневницима",
+       "grant-editinterface": "Уређивање Медијавики именског простора и корисничких CSS/JavaScript страница",
+       "grant-editmywatchlist": "Уређивање вашег списка надгледања",
+       "grant-editpage": "Уређивање постојећих страница",
+       "grant-editprotected": "Уређивање заштићених страница",
+       "grant-uploadeditmovefile": "Отпремање, замена и премештање датотека",
+       "grant-uploadfile": "Слање нових датотека",
        "newuserlogpage": "Дневник нових корисника",
        "newuserlogpagetext": "Ово је дневник нових корисника.",
        "rightslog": "Дневник корисничких права",
        "activeusers-hidebots": "Сакриј ботове",
        "activeusers-hidesysops": "Сакриј администраторе",
        "activeusers-noresult": "Корисник није пронађен.",
+       "activeusers-submit": "Прикажи активне кориснике",
        "listgrouprights": "Права корисничких група",
        "listgrouprights-summary": "Следи списак корисничких група на овом викију, заједно с правима приступа.\nПогледајте [[{{MediaWiki:Listgrouprights-helppage}}|више детаља]] о појединачним правима.",
        "listgrouprights-key": "Легенда:\n* <span class=\"listgrouprights-granted\">Додељено право</span>\n* <span class=\"listgrouprights-revoked\">Укинуто право</span>",
        "listgrouprights-namespaceprotection-header": "Ограничења именских простора",
        "listgrouprights-namespaceprotection-namespace": "Именски простор",
        "listgrouprights-namespaceprotection-restrictedto": "Права потребна за уређивање",
+       "listgrants-rights": "Права",
        "trackingcategories": "Медијавики категорије",
        "trackingcategories-summary": "Ова посебна страница је списак категорија које су део Медијавикија, оне се аутоматски ажурирају и њихови називи се могу мењањати уређивањем системских порука у именском простору {{ns:8}}.",
        "trackingcategories-name": "Име поруке",
index a877573..3484028 100644 (file)
        "right-sendemail": "slanje e-poruka drugim korisnicima",
        "right-passwordreset": "pregledanje poruka za obnavljanje lozinke",
        "right-managechangetags": "pravljenje i/ili brisanje [[Special:Tags|oznaka]] iz baze podataka",
+       "grant-group-page-interaction": "Uređivanje stranica",
+       "grant-group-file-interaction": "Uređivanje datoteka",
+       "grant-group-watchlist-interaction": "Uređivanje vašeg spiska nadgledanja",
+       "grant-group-email": "Slanje e-poruka",
+       "grant-createaccount": "Otvaranje naloga",
+       "grant-createeditmovepage": "Pravljenje, uređivanje i premeštanje stranica",
+       "grant-delete": "Brisanje stranica, izmena i unosa u dnevnicima",
+       "grant-editinterface": "Uređivanje Medijaviki imenskog prostora i korisničkih CSS/JavaScript stranica",
+       "grant-editpage": "Uređivanje postojećih stranica",
+       "grant-uploadeditmovefile": "Otpremanje, zamena i premeštanje datoteka",
+       "grant-uploadfile": "Slanje novih datoteka",
        "newuserlogpage": "Dnevnik novih korisnika",
        "newuserlogpagetext": "Ovo je dnevnik novih korisnika.",
        "rightslog": "Dnevnik korisničkih prava",
        "listgrouprights-namespaceprotection-header": "Ograničenja imenskih prostora",
        "listgrouprights-namespaceprotection-namespace": "Imenski prostor",
        "listgrouprights-namespaceprotection-restrictedto": "Prava potrebna za uređivanje",
+       "listgrants-rights": "Prava",
        "trackingcategories": "Medijaviki kategorije",
        "trackingcategories-name": "Ime poruke",
        "trackingcategories-desc": "Koje stranice se nalaze u kategoriji",
index a66f943..cc31b85 100644 (file)
        "right-managechangetags": "Skapa och radera [[Special:Tags|taggar]] från databasen",
        "right-applychangetags": "Tillämpa [[Special:Tags|taggar]] tillsammans med ens ändringar",
        "right-changetags": "Lägg till och ta bort godtyckliga [[Special:Tags|märken]] på individuella sidversioner och loggposter.",
+       "grant-generic": "Rättighetsgrupp \"$1\"",
+       "grant-group-page-interaction": "Interagera med sidor",
+       "grant-group-file-interaction": "Interagera med media",
+       "grant-group-watchlist-interaction": "Interagera med din bevakningslista",
+       "grant-group-email": "Skicka e-post",
+       "grant-group-high-volume": "Utför hög volymaktivitet",
+       "grant-group-customization": "Anpassning och inställningar",
+       "grant-group-administration": "Utför administrativa åtgärder",
+       "grant-group-other": "Diverseaktivitet",
+       "grant-blockusers": "Blockera och avblockera användare",
+       "grant-createaccount": "Skapa konton",
+       "grant-createeditmovepage": "Skapa, redigera och flytta sidor",
+       "grant-delete": "Ta bort sidor, revideringar och loggposter",
+       "grant-editinterface": "Redigera MediaWiki-namnrymden och CSS/JS för användaren",
+       "grant-editmycssjs": "Redigera din CSS/JavaScript för användare",
+       "grant-editmyoptions": "Redigera dina användarinställningar",
+       "grant-editmywatchlist": "Redigera din bevakningslista",
+       "grant-editpage": "Redigera befintliga sidor",
+       "grant-editprotected": "Redigera skyddade sidor",
+       "grant-highvolume": "Högvolymsredigering",
+       "grant-oversight": "Dölj användare och undertryck revideringar",
+       "grant-patrol": "Patrullera ändringar på sidor",
+       "grant-protect": "Skydda och ta bort skydd på sidor",
+       "grant-rollback": "Rulla tillbaka ändringar på sidor",
+       "grant-sendemail": "Skicka e-post till andra användare",
+       "grant-uploadeditmovefile": "Ladda upp, byt och flytta filer",
+       "grant-uploadfile": "Ladda upp nya filer",
+       "grant-viewdeleted": "Visa raderade filer och sidor",
+       "grant-viewmywatchlist": "Visa din bevakningslista",
        "newuserlogpage": "Logg över nya användare",
        "newuserlogpagetext": "Detta är en logg över nya användarkonton.",
        "rightslog": "Användarrättighetslogg",
        "listgrouprights-namespaceprotection-header": "Namnrymdsbegränsningar",
        "listgrouprights-namespaceprotection-namespace": "Namnrymd",
        "listgrouprights-namespaceprotection-restrictedto": "Rättighet(er) som låter användare redigera",
+       "listgrants-summary": "Följande är en lista över OAuth-behörigheter, med deras associerade tillgång till användarrättigheter. Användare kan tillåta applikationer att använda deras konto, men med begränsad åtkomst baserat på de behörigheter användaren gav applikationen. En applikation som agerar på uppdrag av en användare kan i praktiken inte använda rättigheter som den användaren saknar.\nDet kan finnas [[{{MediaWiki:Listgrouprights-helppage}}|ytterligare information]] om individuella rättigheter.",
+       "listgrants-grant": "Behörighet",
+       "listgrants-rights": "Rättigheter",
        "trackingcategories": "Spårningskategorier",
        "trackingcategories-summary": "Denna sida listar spårningskategorier som automatiskt befolkas av MediaWiki-mjukvaran. Deras namn kan ändras genom att ändra det relevanta systemmeddelandena i {{ns:8}}-namnrymden.",
        "trackingcategories-msg": "Spårningskategori",
index 01bd4f7..2886af5 100644 (file)
        "resetpass_submit": "Şifreyi ayarlayın ve oturum açın",
        "changepassword-success": "Parolanız başarıyla değiştirildi!",
        "changepassword-throttled": "Çok fazla yeni oturum açma girişiminde bulundunuz.\nLütfen tekrar denemeden önce $1 bekleyin.",
+       "botpasswords": "Bot şifreleri",
+       "botpasswords-label-create": "Oluştur",
+       "botpasswords-label-update": "Güncelle",
        "resetpass_forbidden": "Parolalar değiştirilememektedir",
        "resetpass-no-info": "Bu sayfaya doğrudan erişmek için oturum açmanız gereklidir.",
        "resetpass-submit-loggedin": "Parolayı değiştir",
        "linkstoimage-redirect": "$1 (dosya yönlendirme) $2",
        "duplicatesoffile": "Şu {{PLURAL:$1|dosya|$1 dosya}}, bu dosyanın kopyası ([[Special:FileDuplicateSearch/$2|daha fazla ayrıntı]]):",
        "sharedupload": "Bu dosya $1 projesinden olup, diğer projelerde kullanılıyor olabilir.",
-       "sharedupload-desc-there": "Bu dosya $1 deposundan ve diğer projeler tarafından kullanılıyor olabilir. Daha fazla bilgi için lütfen [$2 dosya açıklama sayfasına] bakın.",
-       "sharedupload-desc-here": "Bu dosya $1 deposundan ve diğer projeler tarafından kullanılıyor olabilir.\nAşağıda [$2 dosya açıklama sayfasındaki] açıklama gösteriliyor.",
+       "sharedupload-desc-there": "Bu dosya $1 deposunda bulunmaktadır ve diğer projeler tarafından kullanılıyor olabilir. Daha fazla bilgi için lütfen [$2 dosya açıklama sayfasına] bakın.",
+       "sharedupload-desc-here": "Bu dosya $1 deposunda bulunmaktadır ve diğer projeler tarafından kullanılıyor olabilir.\nAşağıda [$2 dosya açıklama sayfasındaki] açıklama gösteriliyor.",
        "sharedupload-desc-edit": "Bu dosya $1 projesinden olup, diğer projelerde kullanılıyor olabilir.\nDosyanın açıklama sayfasında değişiklik yapmak için ilgili sayfaya [$2 buradan] gidebilirsiniz.",
        "sharedupload-desc-create": "Bu dosya $1 projesinden olup, diğer projelerde kullanılıyor olabilir.\nDosyanın açıklama sayfasında değişiklik yapmak için ilgili sayfaya [$2 buradan] gidebilirsiniz.",
        "filepage-nofile": "Bu isimde bir dosya yok.",
        "uploadnewversion-linktext": "Dosyanın yenisini yükleyin",
        "shared-repo-from": "$1'dan",
        "shared-repo": "ortak bir havuz",
-       "shared-repo-name-wikimediacommons": "Wikimedia Commons'ta",
+       "shared-repo-name-wikimediacommons": "Wikimedia Commons",
        "upload-disallowed-here": "Bu dosyanın üzerine yazamazsınız.",
        "filerevert": "$1 dosyasını eski haline döndür",
        "filerevert-legend": "Dosyayı eski haline döndür",
index b8fb4d5..51edb27 100644 (file)
        "rcshowhidemine-show": "Күрсәтү",
        "rcshowhidemine-hide": "Яшер",
        "rcshowhidecategorization-show": "Күрсәт",
-       "rclinks": "СоңгÑ\8b $2 ÐºÓ©Ð½ Ñ\8dÑ\87ендÓ\99 Ñ\81оңгÑ\8b $1 үзгәртүне күрсәт<br />$3",
+       "rclinks": "СоңгÑ\8b $2 ÐºÓ©Ð½ Ñ\8dÑ\87ендÓ\99 Ñ\8fÑ\81алган $1 үзгәртүне күрсәт<br />$3",
        "diff": "аерма",
        "hist": "тарих",
        "hide": "Яшер",
        "confirm_purge_button": "OK",
        "confirm-purge-top": "Бу битнең кэшы чистартылсынмы?",
        "confirm-purge-bottom": "Кэшны чистартудан соң аның соңгы юрамасы күрсәтеләчәк.",
+       "pipe-separator": "&#32;|&#32;",
        "imgmultipageprev": "← алдагы бит",
        "imgmultipagenext": "алдагы бит →",
        "imgmultigo": "Күчү!",
index 942e307..6387e0d 100644 (file)
@@ -63,7 +63,8 @@
                        "Капитан Джон Шепард",
                        "Translatemyname",
                        "Dars",
-                       "Mix Gerder"
+                       "Mix Gerder",
+                       "E.belykh"
                ]
        },
        "tog-underline": "Підкреслювання посилань:",
        "right-managechangetags": "створення та вилучення [[Special:Tags|міток]] з бази даних",
        "right-applychangetags": "додавання [[Special:Tags|міток]] разом зі змінами",
        "right-changetags": "додавання або вилучення будь-яких [[Special:Tags|міток]] для певних версій сторінок або записів журналів",
+       "grant-generic": "Пучок прав \"$1\"",
+       "grant-group-page-interaction": "Взаємодіяти з сторінками",
+       "grant-group-file-interaction": "Взаємодіяти з медіа",
+       "grant-group-watchlist-interaction": "Взаємодіяти з вашим списком спостереження",
+       "grant-group-email": "Надіслати листа",
+       "grant-group-high-volume": "Виконати великий обсяг діяльності",
+       "grant-group-customization": "Налаштування і переваги",
+       "grant-group-administration": "Виконати адміністративні дії",
+       "grant-group-other": "Різна діяльність",
+       "grant-blockusers": "Блокування і розблокування користувачів",
+       "grant-createaccount": "Створити облікові записи",
+       "grant-createeditmovepage": "Створювати, редагувати та перейменовувати сторінки",
+       "grant-delete": "Видалення сторінок, версій і записів журналу",
+       "grant-editinterface": "Редагування простору назв MediaWiki та CSS/JavaScript користувача",
+       "grant-editmycssjs": "Редагування Вашого користувацького CSS/JavaScript",
+       "grant-editmyoptions": "Редагування Ваших налаштувань користувача",
+       "grant-editmywatchlist": "Редагувати список спостереження",
+       "grant-editpage": "Редагувати наявні сторінки",
+       "grant-editprotected": "Редагувати захищені сторінки",
+       "grant-highvolume": "Редагування великих обсягів",
+       "grant-oversight": "Приховати користувачів і подавити версії",
+       "grant-patrol": "Патрулювати зміни на сторінках",
+       "grant-protect": "Захистити і зняти захист сторінок",
+       "grant-rollback": "Відкат змін на сторінках",
+       "grant-sendemail": "Відправляти пошту іншим користувачам",
+       "grant-uploadeditmovefile": "Завантажити, замінити та перемістити файли",
+       "grant-uploadfile": "Завантажити нові файли",
+       "grant-viewdeleted": "Перегляд видалених файлів і сторінок",
+       "grant-viewmywatchlist": "Перегляд списку спостереження",
        "newuserlogpage": "Журнал нових користувачів",
        "newuserlogpagetext": "Список нещодавно зареєстрованих користувачів.",
        "rightslog": "Журнал прав користувача",
        "listgrouprights-namespaceprotection-header": "Обмеження простору назв",
        "listgrouprights-namespaceprotection-namespace": "Простір назв",
        "listgrouprights-namespaceprotection-restrictedto": "Права, що дозволяють користувачу редагувати",
+       "listgrants-summary": "Нижче наведено список OAuth грантів, з їхнім пов'язаним доступом до прав користувача. Користувачі можуть дозволити програмам використовувати свій обліковий запис, але з обмеженими правами на основі грантів користувача наданих до заяви. Програма діє від імені користувача, однак насправді не може використовувати права, які користувач не має.\nТам може бути [[{{MediaWiki:Listgrouprights-helppage}}|додаткова інформація]] про індивідуальні права.",
+       "listgrants-grant": "Грант",
+       "listgrants-rights": "Права",
        "trackingcategories": "Відстежувані категорії",
        "trackingcategories-summary": "На цій сторінці перераховані відстежують категорії, які заповнюються автоматично програмним забезпеченням MediaWiki. Їх можна перейменувати, змінивши відповідні системні повідомлення в просторі назв {{ns:8}}.",
        "trackingcategories-msg": "Відстежувана категорія",
        "hebrew-calendar-m12-gen": "Елула",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|обговорення]])",
        "timezone-utc": "UTC",
-       "timezone-local": "Місцевий",
+       "timezone-local": "Місцеві",
        "duplicate-defaultsort": "Увага. Ключ сортування «$2» перекриває попередній ключ сортування «$1».",
        "duplicate-displaytitle": "<strong>Увага:</strong> Відображений заголовок \"$2\" заміщує раніше відображений заголовок \"$1\".",
        "invalid-indicator-name": "<strong>Помилка:</strong> Сторінка індикатора стану <code>name</code> атрибута не може бути пуста.",
index e4d9e92..17cfff1 100644 (file)
        "recentchanges-legend": "اِختیاراتِ حالیہ تبدیلیاں",
        "recentchanges-summary": "اس صفحے پر ویکی میں ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔",
        "recentchanges-feed-description": "اس خورد میں ویکی پر ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔",
-       "recentchanges-label-newpage": "اِس ترمیم نے نیا صفحہ تخلیق کردیا",
+       "recentchanges-label-newpage": "یہ ترمیم ایک نئے صفحے کی تخلیق ہے",
        "recentchanges-label-minor": "یہ ایک معمولی ترمیم ہے",
-       "recentchanges-label-bot": "یہ ایک روبالہ سے سرانجام شدہ ترمیم ہے",
+       "recentchanges-label-bot": "اس ترمیم کو ایک روبہ نے انجام دیا ہے",
        "recentchanges-label-unpatrolled": "اس ترمیم کی اب تک مراجعت نہیں کی گئی",
        "recentchanges-label-plusminus": "صفحہ کا حجم تبدیل شدہ بلحاظ بائٹ مقدار",
        "recentchanges-legend-heading": "'''اختیارات'''",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (نیز [[Special:NewPages|جدید صفحات کی فہرست]]) ملاحظہ فرمائیں",
        "recentchanges-submit": "دکھائیں",
        "rcnotefrom": "ذیل میں <strong>$3, $4</strong> سے کی گئی {{PLURAL:$5|تبدیلی|تبدیلیاں}} <strong>$1</strong> تک دکھائی جا رہی ہیں۔",
        "rclistfrom": "$3 $2 سےنئی تبدیلیاں دکھانا شروع کریں",
        "monthsall": "تمام",
        "deletedwhileediting": "انتباہ: آپ کے ترمیم شروع کرنے کے بعد یہ صفحہ حذف کیا جا چکا ہے!",
        "confirm_purge_button": "جی!",
+       "semicolon-separator": "؛&#32;",
        "imgmultipageprev": "← پچھلا",
        "imgmultipagenext": "اگلا →",
        "imgmultigo": "جائیں!",
index c28646c..64eb5e7 100644 (file)
        "right-managechangetags": "Tạo và xóa [[Special:Tags|thẻ]] từ cơ sở dữ liệu",
        "right-applychangetags": "Áp dụng [[Special:Tags|thẻ]], cùng với những thay đổi của một người",
        "right-changetags": "Thêm và loại bỏ tùy ý các [[Special:Tags|thẻ]] vào các phiên bản riêng và các mục nhật trình",
+       "grant-generic": "Gói quyền “$1”",
+       "grant-group-page-interaction": "Tương tác với trang",
+       "grant-group-file-interaction": "Tương tác với tập tin",
+       "grant-group-watchlist-interaction": "Tương tác với danh sách theo dõi của bạn",
+       "grant-group-email": "Gửi thư điện tử",
+       "grant-group-high-volume": "Hoạt động với tần số cao",
+       "grant-group-customization": "Tùy biến và tùy chọn",
+       "grant-group-administration": "Thực hiện các hành động bảo quản",
+       "grant-group-other": "Hoạt động khác",
+       "grant-blockusers": "Cấm và bỏ cấm người dùng",
+       "grant-createaccount": "Mở tài khoản",
+       "grant-createeditmovepage": "Tạo, sửa đổi, và di chuyển trang",
+       "grant-delete": "Xóa trang, phiên bản, và mục nhật trình",
+       "grant-editinterface": "Sửa không gian tên MediaWiki và CSS/JS cá nhân",
+       "grant-editmycssjs": "Sửa đổi CSS/JavaScript cá nhân của bạn",
+       "grant-editmyoptions": "Sửa đổi tùy chọn cá nhân của bạn",
+       "grant-editmywatchlist": "Sửa danh sách theo dõi của bạn",
+       "grant-editpage": "Sửa đổi các trang đã tồn tại",
+       "grant-editprotected": "Sửa đội các trang bị khóa",
+       "grant-highvolume": "Sửa đổi tốc độ cao",
+       "grant-oversight": "Ẩn người dùng và phiên bản",
+       "grant-patrol": "Tuần tra các thay đổi trang",
+       "grant-protect": "Khóa và mở khóa các trang",
+       "grant-rollback": "Lùi một loạt thay đổi vào một trang",
+       "grant-sendemail": "Gửi thư điện tử cho người dùng khác",
+       "grant-uploadeditmovefile": "Tải lên, thay thế, và di chuyển tập tin",
+       "grant-uploadfile": "Tải lên tập tin mới",
+       "grant-viewdeleted": "Xem các trang và tập tin đã xóa",
+       "grant-viewmywatchlist": "Xem danh sách theo dõi của bạn",
        "newuserlogpage": "Nhật trình mở tài khoản",
        "newuserlogpagetext": "Đây là danh sách những tài khoản thành viên mở lên gần đây.",
        "rightslog": "Nhật trình cấp quyền thành viên",
        "listgrouprights-namespaceprotection-header": "Hạn chế không gian tên",
        "listgrouprights-namespaceprotection-namespace": "Không gian tên",
        "listgrouprights-namespaceprotection-restrictedto": "Quyền cho phép người dùng sửa đổi",
+       "listgrants-rights": "Cấp phép",
        "trackingcategories": "Thể loại phần mềm",
        "trackingcategories-summary": "Đây là danh sách các thể loại được phần mềm MediaWiki tự động xếp trang vào. Các tên thể loại được định rõ trong các thông điệp thuộc không gian tên {{ns:8}}.",
        "trackingcategories-msg": "Thể loại phần mềm",
index 653f7d2..3b8bbf3 100644 (file)
        "remembermypassword": "געדענקען מיין אַריינלאָגירן אין דעם דאָזיקן בראַוזער (ביז $1 {{PLURAL:$1|טאָג|טעג}})",
        "userlogin-remembermypassword": "לאז מיך בלײַבן ארײַנלאגירט",
        "userlogin-signwithsecure": "ניצן זיכערן סארווער",
+       "cannotloginnow-title": "קען נישט אריינלאגירן אצינד",
        "yourdomainname": "אײַער געביט:",
        "password-change-forbidden": "איר קען נישט ענדערן פאסווערטער אויף דער וויקי.",
        "externaldberror": "עס איז אדער פארגעקומען אן אויטענטיקאציע דאטנבאזע פֿעלער אדער איר זענט נישט ערמעגליכט צו דערהיינטיגן אייער דרויסנדיגע קאנטע.",
        "resetpass_submit": "שטעלן פאסווארט און אריינלאגירן",
        "changepassword-success": "אייער פאַסווארט איז געטוישט געווארן מיט דערפֿאלג!",
        "changepassword-throttled": "איר האט געפרוווט צופֿיל מאל אריינלאגירן.\nזייט אזוי גוט און וואַרט $1 איידער איר פרוווט נאכאמאל.",
+       "botpasswords-label-create": "שאַפֿן",
+       "botpasswords-label-update": "דערהײַנטיקן",
+       "botpasswords-label-cancel": "אַנולירן",
+       "botpasswords-label-resetpassword": "ווידערשטעלן פאַסווארט",
        "resetpass_forbidden": "פאסווערטער קענען נישט ווערן געטוישט",
        "resetpass-no-info": "איר דארפֿט זיין אריינלאגירט צוצוקומען גלייך צו דעם דאזיגן בלאט.",
        "resetpass-submit-loggedin": "טוישן פאסווארט",
        "passwordreset-emailtext-ip": "עמעצער (מסתמא איר, פון IP אדרעס $1) האט געבעטן צוריקצושטעלן אייער פאסווארט פאר {{SITENAME}} ($4). די פאלגנדע באניצער {{PLURAL:$3|קאנטע איז|קאנטעס זענען}}\nפארבונדן מיט דעם ע־פאסט אדרעס:\n\n$2\n\n{{PLURAL:$3|דאס פראוויזארישע פאסווארט|די פראוויזארישע פאסווערטער}} וועלן אויסגיין נאך {{PLURAL:$5|איין טאג|$5 טעג}}.\nאיר זאלט אריינלאגירן און קלויבן א נייע פאסווארט אצינד. טאמער א צווייטער האט געשיקט די בקשה,\nאדער ווען איר געדענקט יא אייער פריעריקע פאסווארט, און וויל עס נישט ענדערן,\n קענט איר איגנארירן דעם אנזאג און ניצן ווייטער דאס אלטע פאסווארט.",
        "passwordreset-emailtext-user": "באניצער $1 אויף  {{SITENAME}} האט געבעטן צוריקצושטעלן אייער פאסווארט פאר {{SITENAME}} ($4).\nדי פאלגנדע באניצער {{PLURAL:$3|קאנטע איז|קאנטעס זענען}} פארבונדן מיט דעם ע־פאסט אדרעס:\n\n$2\n\n{{PLURAL:$3|דאס פראוויזארישע פאסווארט|די פראוויזארישע פאסווערטער}} וועלן אויסגיין נאך {{PLURAL:$5|איין טאג|$5 טעג}}.\nאיר זאלט אריינלאגירן און קלויבן א נייע פאסווארט אצינד. טאמער א צווייטער האט געשיקט די בקשה,\nאדער ווען איר געדענקט יא אייער פריעריקע פאסווארט, און וויל עס נישט ענדערן,\n קענט איר איגנארירן דעם אנזאג און ניצן ווייטער דאס אלטע פאסווארט.",
        "passwordreset-emailelement": "באַניצער נאָמען: \n$1\n\nפראוויזארישער פּאַראָל: \n$2",
-       "passwordreset-emailsentemail": "×\98×\90×\9eער ×\90×\99×\96 ×\93×\90ס ×\90×\9f ×\90×\99×\99× ×\92עשר×\99×\91ענער ×¢Ö¾×¤×\90ס×\98 ×\90×\93רעס ×¤×\90ר אייער קאנטע, וועט מען שיקן א פאסווארט צוריקשטעלן ע-פּאָסט.",
+       "passwordreset-emailsentemail": "×\98×\90×\9eער ×\90×\99×\96 ×\93ער ×¢Ö¾×¤×\90ס×\98 ×\90×\93רעס ×¤×\90רקנ×\99פ×\98 ×\9e×\99×\98 אייער קאנטע, וועט מען שיקן א פאסווארט צוריקשטעלן ע-פּאָסט.",
        "passwordreset-emailsent-capture": "מען האט געשיקט א פאסווארט צוריקשטעלן בליצבריוו, וואס ווערט געוויזן אונטן.",
        "passwordreset-emailerror-capture": "מען האט געשאפן א פאסווארט צוריקשטעלן בליצבריוו, וואס ווערט געוויזן אונטן, אבער שיקן צום {{GENDER:$2|באניצער}}איז דורכגעפאלן: $1",
        "changeemail": "ענדערן אדער אראפנעמען ע-פּאָסט אַדרעס",
        "javascripttest-pagetext-frameworks": "ביטע קלויבט איינעם פון די פאלגנדע טעסטן־גערעם: $1",
        "javascripttest-pagetext-skins": "קלויבט א באניצער־אייבערפלאך מיט וואס דורכצופירן די בדיקות:",
        "javascripttest-qunit-intro": "זעט [$1 דאקומענטאציע פאר טעסטן] בײַ mediawiki.org.",
-       "tooltip-pt-userpage": "אייער באניצער בלאט",
+       "tooltip-pt-userpage": "אייער {{GENDER:|באניצער|באניצערין}} בלאט",
        "tooltip-pt-anonuserpage": "באַניצער בלאַט פון דעם IP אַדרעס",
-       "tooltip-pt-mytalk": "אייער שמועס בלאט",
+       "tooltip-pt-mytalk": "{{GENDER:|אייער}} שמועס בלאט",
        "tooltip-pt-anontalk": "שמועס איבער באטייליגען פון די איי.פי.",
-       "tooltip-pt-preferences": "אייערע פרעפערענצן",
+       "tooltip-pt-preferences": "{{GENDER:|אייערע}} פרעפערענצן",
        "tooltip-pt-watchlist": "ליסטע פון בלעטער וואס איר טוט אויפפאסן נאך ענדערונגן",
-       "tooltip-pt-mycontris": "ליסטע פון אייערע ביישטייערונגען",
+       "tooltip-pt-mycontris": "ליסטע פון {{GENDER:|אייערע}} ביישטייערונגען",
        "tooltip-pt-login": "עס איז רעקאָמענדירט זיך אײַנשרײַבן; ס'איז אבער נישט קיין פֿליכט",
        "tooltip-pt-logout": "ארויסלאגירן",
        "tooltip-pt-createaccount": "איר ווערט דערמוטיגט צו שאפן א קאנטע און אריינלאגירן; ס'איז אביר נישט אבליגאטאריש",
        "tooltip-t-recentchangeslinked": "אלע ענדערונגען פון בלעטער וואס זענען אהער פארבינדען",
        "tooltip-feed-rss": "דערהײַנטיגט אויטאמאטיש פון אר.עס.עס. RSS",
        "tooltip-feed-atom": "לייג צו אן אטאמאטישער אפדעיט דורך אטאם Atom",
-       "tooltip-t-contributions": "אלע בײַשטײַערונגען פון דעם באניצער",
-       "tooltip-t-emailuser": "שיקן א בליצבריוו צו דעם בַאניצער",
+       "tooltip-t-contributions": "א ליסטע פון בײַשטײַערונגען פון {{GENDER:$1|דעם באניצער|דער באניצערין}}",
+       "tooltip-t-emailuser": "שיקן א בליצבריוו צו {{GENDER:$1|דעם באניצער|דער באניצערין}}",
        "tooltip-t-info": "נאָך אינפאָרמאַציע וועגן דעם בלאַט",
        "tooltip-t-upload": "ארויפלאדן טעקעס",
        "tooltip-t-specialpages": "אלע ספעציעלע בלעטער",
        "revdelete-uname-unhid": "באַניצער נאָמען ארויסגעגעבן",
        "revdelete-restricted": "צוגעלייגט באגרעניצונגען פאר סיסאפן",
        "revdelete-unrestricted": "אוועקגענומען באגרעניצונגען פאר סיסאפן",
+       "logentry-suppress-block": "$1 {{GENDER:$2|האט בלאקירט}} {{GENDER:$4|$3}} מיט אן אויסלאז צייט פון $5 $6",
        "logentry-move-move": "$1 {{GENDER:$2|האט באוועגט}} בלאט $3 צו $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|האט באוועגט}} בלאט $3 צו $4 אן לאזן א ווייטערפירונג",
        "logentry-move-move_redir": "$1 {{GENDER:$2|האט באוועגט}} $3 צו $4 אריבער ווייטערפירונג",
index 14b08ca..514030e 100644 (file)
        "right-managechangetags": "从数据库创建和删除[[Special:Tags|标签]]",
        "right-applychangetags": "连同某人的更改一起应用[[Special:Tags|标签]]",
        "right-changetags": "在个别修订和日志记录中添加和移除任意[[Special:Tags|标签]]",
+       "grant-generic": "\" $1 \"权利束",
+       "grant-group-page-interaction": "与页面交互",
+       "grant-group-file-interaction": "与媒体交互",
+       "grant-group-watchlist-interaction": "与您的监视列表交互",
+       "grant-group-email": "发送电邮",
+       "grant-group-high-volume": "执行大量活动",
+       "grant-group-customization": "自定义与设置",
+       "grant-group-administration": "执行行政操作",
+       "grant-group-other": "杂项活动",
+       "grant-blockusers": "封禁与解封用户",
+       "grant-createaccount": "注册账户",
+       "grant-createeditmovepage": "创建、编辑和移动页面",
+       "grant-delete": "删除页面、修改和日志记录",
+       "grant-editinterface": "编辑MediaWiki名字空间和用户CSS/JavaScript",
+       "grant-editmycssjs": "编辑您的用户CSS/JavaScript",
+       "grant-editmyoptions": "编辑您的用户首选项",
+       "grant-editmywatchlist": "编辑您的监视列表",
+       "grant-editpage": "编辑存在的页面",
+       "grant-editprotected": "编辑受保护页面",
+       "grant-highvolume": "大容量编辑",
+       "grant-oversight": "隐藏用户和阻止修订",
+       "grant-patrol": "巡查对页面的更改",
+       "grant-protect": "保护页面和取消页面保护",
+       "grant-rollback": "回退更改到页",
+       "grant-sendemail": "给其他用户发送电子邮件",
+       "grant-uploadeditmovefile": "上传,替换和移动文件",
+       "grant-uploadfile": "上传新文件",
+       "grant-viewdeleted": "查看已删除文件和页面",
+       "grant-viewmywatchlist": "查看您的监视列表",
        "newuserlogpage": "用户创建日志",
        "newuserlogpagetext": "这是用户创建的日志。",
        "rightslog": "用户权限日志",
        "listgrouprights-namespaceprotection-header": "名字空间限制",
        "listgrouprights-namespaceprotection-namespace": "名字空间",
        "listgrouprights-namespaceprotection-restrictedto": "允许用户编辑的权限",
+       "listgrants-summary": "以下是OAuth功能的列表,并与用户权限的访问权相关。用户可以授予应用程序访问它们的账户,但限定于用户授予应用程序的功能权限。代表一位用户的应用程序不能使用用户没有的权限。\n这里可能有关于个别权限的[[{{MediaWiki:Listgrouprights-helppage}}|额外信息]]。",
+       "listgrants-rights": "权限",
        "trackingcategories": "追踪分类",
        "trackingcategories-summary": "本页面列举由MediaWiki软件自动添加的追踪分类。它们的名字可通过修改{{ns:8}}名字空间对应的系统信息而变更。",
        "trackingcategories-msg": "追踪分类",
index 5230c4a..6681df4 100644 (file)
        "october-date": "十月 $1 日",
        "november-date": "十一月 $1 日",
        "december-date": "十二月 $1 日",
+       "period-am": "AM",
+       "period-pm": "PM",
        "pagecategories": "{{PLURAL:$1|分類|$1 個分類}}",
        "category_header": "分類 \"$1\" 中的頁面",
        "subcategories": "子分類",
        "virus-scanfailed": "掃瞄失敗 (代碼 $1)",
        "virus-unknownscanner": "不明的防毒程式:",
        "logouttext": "<strong>您現在已登出。</strong>\n\n請注意,某些頁面會以登入的狀態持續顯示,直到您清除瀏覽器快取為止。",
+       "cannotlogoutnow-title": "現在無法登出",
+       "cannotlogoutnow-text": "使用 $1 時無法登出。",
        "welcomeuser": "歡迎光臨,$1!",
        "welcomecreation-msg": "您的帳號已建立。\n可至 [[Special:Preferences|偏好設定]] 更新您在 {{SITENAME}} 的個人化設定。",
        "yourname": "使用者名稱:",
        "remembermypassword": "在瀏覽器上記住我的登入資訊 (上限 $1 {{PLURAL:$1|天}})",
        "userlogin-remembermypassword": "記住我的登入狀態",
        "userlogin-signwithsecure": "使用安全連線",
+       "cannotloginnow-title": "現在無法登入",
+       "cannotloginnow-text": "使用 $1 時無法登入。",
        "yourdomainname": "您的網域:",
        "password-change-forbidden": "您不可變更此 Wiki 上的密碼。",
        "externaldberror": "這可能是由於資料庫驗證錯誤,或是不允許您更新外部帳號。",
        "resetpass_submit": "設定密碼並登入",
        "changepassword-success": "您的密碼已變更成功!",
        "changepassword-throttled": "您最近嘗試了太多次登入。\n請等待 $1 後再試。",
+       "botpasswords": "機器人密碼",
+       "botpasswords-summary": "<em>機器人密碼</em> 可在不需帳號的主要登入密碼情況下,允許透過 API 存取使用者帳號。 可限制使用機器人密碼登入的使用者權限。\n\n若在尚無法了解為何要設定機器人密碼之前不應使用此功能。 且不該有任何人會向您索取機器人密碼。",
+       "botpasswords-disabled": "機器人密碼已停用。",
+       "botpasswords-no-central-id": "要使用機器人密碼,您必須登入帳號集中管理系統。",
+       "botpasswords-existing": "已存在機器人密碼",
+       "botpasswords-createnew": "建立新機器人密碼",
+       "botpasswords-editexisting": "編輯已存在的機器人密碼",
+       "botpasswords-label-appid": "機器人名稱:",
+       "botpasswords-label-create": "建立",
+       "botpasswords-label-update": "更新",
+       "botpasswords-label-cancel": "取消",
+       "botpasswords-label-delete": "刪除",
+       "botpasswords-label-resetpassword": "重設密碼",
+       "botpasswords-label-grants": "適用的權限:",
+       "botpasswords-label-restrictions": "使用限制:",
+       "botpasswords-label-grants-column": "已授權",
+       "botpasswords-bad-appid": "機器人名稱 \"$1\" 無效。",
+       "botpasswords-insert-failed": "新增機器人名稱 \"$1\" 失敗,是否已新增過?",
+       "botpasswords-update-failed": "更新機器人名稱 \"$1\" 失敗,是否已刪除過?",
+       "botpasswords-created-title": "已建立機器人密碼",
+       "botpasswords-created-body": "機器人密碼 \"$1\" 已建立成功。",
+       "botpasswords-updated-title": "已更新機器人密碼",
+       "botpasswords-updated-body": "機器人密碼 \"$1\" 已修改成功。",
+       "botpasswords-deleted-title": "已刪除機器人密碼",
+       "botpasswords-deleted-body": "機器人密碼 \"$1\" 已刪除。",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider 無法使用。",
+       "botpasswords-restriction-failed": "機器人密碼限制已拒絕此次登入。",
+       "botpasswords-invalid-name": "指定的使用者名稱未包含機器人密碼分隔字元 (\"$1\")。",
+       "botpasswords-not-exist": "使用者 \"$1\" 並沒有名稱為 \"$2\" 的機器人密碼。",
        "resetpass_forbidden": "無法變更密碼",
        "resetpass-no-info": "您必須直接登入存取這個頁面。",
        "resetpass-submit-loggedin": "變更密碼",
        "passwordreset-emailtext-user": "使用者 $1 要求重設在 {{SITENAME}} ($4) 的密碼,下列是與此電子郵件位址有關的使用者{{PLURAL:$3|帳號}}:\n\n$2\n\n{{PLURAL:$3|這個臨時密碼|這些臨時密碼}}將會在{{PLURAL:$5|一天|$5 天}}內到期,\n您應立即登入並更改新的密碼。如果不是您要求重設密碼,或您已想起密碼,並不準備修改,\n您可以忽略此訊息並且繼續使用您原本的密碼。",
        "passwordreset-emailelement": "使用者名稱:\n$1\n\n臨時密碼:\n$2",
        "passwordreset-emailsentemail": "若此確實為您帳號所登記的電子郵件地址,將會寄出重設密碼的信件給您。",
+       "passwordreset-emailsentusername": "若此確實為您使用者名稱所登記的電子郵件地址,將會寄出重設密碼的信件給您。",
        "passwordreset-emailsent-capture": "已寄出重設密碼的電子郵件,並於下方顯示。",
        "passwordreset-emailerror-capture": "下列為重設密碼的電子郵件內容,傳送給{{GENDER:$2|使用者}}失敗:$1",
        "changeemail": "變更或移除電子郵件地址",
        "copyrightwarning2": "請注意,所有於 {{SITENAME}} 所做的貢獻可能會被其他貢獻者編輯,修改或刪除。\n若您不希望您的著作被任意修改與散佈,請勿在此發表文章。<br />\n您同時向我們保証在此的著作內容是您自行撰寫,或是取自不受版權保護的公開領域或自由資源 (詳情請見 $1)。\n<strong>請勿在未經授權的情況下發表文章!</strong>",
        "editpage-cannot-use-custom-model": "此頁面的內容模型不能被修改。",
        "longpageerror": "<strong>錯誤:您所送出的文字內容共有 {{PLURAL:$1|1 KB|$1 KB}},已超出系統上限 {{PLURAL:$2|1 KB|$2 KB}}。</strong>\n\n無法儲存。",
-       "readonlywarning": "<strong>警告:資料庫已被鎖定以進行維護,因此無法儲存您目前所做的編輯動作。</strong>\n您可先複製您的文字並貼上到文字檔案中儲存,稍後再儲存您編輯。\n\n鎖定資料庫的管理員有以下說明:$1",
+       "readonlywarning": "<strong>è­¦å\91\8aï¼\9aè³\87æ\96\99庫已被é\8e\96å®\9a以é\80²è¡\8c維護ï¼\8cå\9b æ­¤ç\84¡æ³\95å\84²å­\98æ\82¨ç\9b®å\89\8dæ\89\80å\81\9aç\9a\84編輯å\8b\95ä½\9cã\80\82</strong>\næ\82¨å\8f¯å\85\88è¤\87製æ\82¨ç\9a\84æ\96\87å­\97並貼ä¸\8aå\88°æ\96\87å­\97æª\94æ¡\88中å\84²å­\98ï¼\8cç¨\8då¾\8cå\86\8då\84²å­\98æ\82¨ç·¨è¼¯ã\80\82\n\né\8e\96å®\9aè³\87æ\96\99庫ç\9a\84系統管ç\90\86å\93¡æ\9c\89以ä¸\8b說æ\98\8eï¼\9a$1",
        "protectedpagewarning": "<strong>警告:本頁已經被保護,只有擁有管理員權限的使用者才可編輯。</strong>\n以下提供最近的日誌以便參考:",
        "semiprotectedpagewarning": "<strong>注意:</strong>本頁已經被保護,只有已註冊的使用者才可編輯。\n以下提供最近的日誌以便參考:",
        "cascadeprotectedwarning": "<strong>警告:</strong>本頁已經被保護,只有擁有管理員權限的使用者才可編輯,此頁面被下列{{PLURAL:$1|頁面|頁面}}引用因此連鎖保護:",
        "permissionserrors": "權限錯誤",
        "permissionserrorstext": "由於下列{{PLURAL:$1|原因}},您沒有權限進行目前的動作:",
        "permissionserrorstext-withaction": "由於下列{{PLURAL:$1|原因}},您沒有權限進行 $2 的動作:",
-       "contentmodelediterror": "æ\82¨ç\84¡æ³\95編輯此修è¨\82ï¼\8cå\9b æ­¤ä¿®è¨\82使ç\94¨ç\9a\84å\85§å®¹æ¨¡å\9e\8bç\82º <code>$1</code> è\80\8cç\9b®å\89\8d使ç\94¨ç\9a\84é \81é\9d¢å\85§å®¹æ¨¡å\9e\8bç\82º <code>$2</code>。",
+       "contentmodelediterror": "æ\82¨ç\84¡æ³\95編輯此修è¨\82ï¼\8cå\9b æ­¤ä¿®è¨\82使ç\94¨ç\9a\84å\85§å®¹æ¨¡å\9e\8bç\82º <code>$1</code> è\88\87ç\9b®å\89\8d使ç\94¨ç\9a\84é \81é\9d¢å\85§å®¹æ¨¡å\9e\8b <code>$2</code> ä¸\8då\90\8c。",
        "recreate-moveddeleted-warn": "<strong>警告:您正重新建立先前已刪除的頁面。</strong>\n\n您應考慮是否繼續編輯此頁。\n在此提供刪除與移動日誌方便作為參考:",
        "moveddeleted-notice": "此頁面已刪除。\n下方提供此頁面的刪除和移動日誌以便參考。",
        "moveddeleted-notice-recent": "抱歉,此頁面最近被刪除 (24 小時內)。\n以下提供此頁面的刪除與移動日誌做為參考。",
        "right-createpage": "建立頁面 (不含討論頁面)",
        "right-createtalk": "建立討論頁面",
        "right-createaccount": "建立新的使用者帳號",
+       "right-autocreateaccount": "使用外部使用者帳號自動登入",
        "right-minoredit": "標示編輯為小修訂",
        "right-move": "移動頁面",
        "right-move-subpages": "移動頁面與其子頁面",
        "right-managechangetags": "建立並自資料庫移除[[Special:Tags|標籤]]",
        "right-applychangetags": "連同某個人的變更一起套用[[Special:Tags|標籤]]",
        "right-changetags": "加入與移除任何於各別修訂與日誌項目的[[Special:Tags|標籤]]",
+       "grant-generic": "\"$1\" 權限組合",
+       "grant-group-page-interaction": "與頁面互動",
+       "grant-group-file-interaction": "與媒體互動",
+       "grant-group-watchlist-interaction": "與您的監視清單互動",
+       "grant-group-email": "傳送電子郵件",
+       "grant-group-high-volume": "執行大量活動",
+       "grant-group-customization": "自訂與偏好設定",
+       "grant-group-administration": "執行管理操作",
+       "grant-group-other": "其他活動",
+       "grant-blockusers": "封鎖與解除封鎖使用者",
+       "grant-createaccount": "建立帳號",
+       "grant-createeditmovepage": "建立、編輯與移動頁面",
+       "grant-delete": "刪除頁面、修訂與日誌記錄",
+       "grant-editinterface": "編輯 MediaWiki 命名空間與使用者 CSS/JavaScript",
+       "grant-editmycssjs": "編輯您的使用者 CSS/JavaScript",
+       "grant-editmyoptions": "編輯您的使用者偏好設定",
+       "grant-editmywatchlist": "編輯您的監視清單",
+       "grant-editpage": "編輯現有的頁面",
+       "grant-editprotected": "編輯受保護的頁面",
+       "grant-highvolume": "大量編輯",
+       "grant-oversight": "隱藏使用者和禁止顯示修訂",
+       "grant-patrol": "巡邏頁面的變更",
+       "grant-protect": "保護與取消保護頁面",
+       "grant-rollback": "還原頁面的變更",
+       "grant-sendemail": "傳送電子郵件聯絡其他使用者",
+       "grant-uploadeditmovefile": "上傳、取代與移動檔案",
+       "grant-uploadfile": "上傳新檔案",
+       "grant-basic": "基本權限",
+       "grant-viewdeleted": "檢視已刪除的檔案及頁面",
+       "grant-viewmywatchlist": "檢視您的監視清單",
        "newuserlogpage": "建立使用者日誌",
        "newuserlogpagetext": "此為建立使用者的日誌。",
        "rightslog": "使用者權限日誌",
        "action-createpage": "建立頁面",
        "action-createtalk": "建立討論頁面",
        "action-createaccount": "建立此使用者帳號",
+       "action-autocreateaccount": "自動建立此外部使用者帳號",
        "action-history": "檢視此頁面歷史",
        "action-minoredit": "標示此編輯為小修訂",
        "action-move": "移動此頁面",
        "foreign-structured-upload-form-label-own-work-message-shared": "我保証我擁有此檔案的版權,並且不反悔同意使用 [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] 授權條款發佈此檔案到維基媒體共享資源,並且我同意 [https://wikimediafoundation.org/wiki/Terms_of_Use 使用條款]。",
        "foreign-structured-upload-form-label-not-own-work-message-shared": "若您並未擁有此檔案的版權,或者您希望使用其他的授權條款發佈此檔案,請考慮使用[https://commons.wikimedia.org/wiki/Special:UploadWizard 通用上傳精靈]。",
        "foreign-structured-upload-form-label-not-own-work-local-shared": "若該站的授權政策允許上傳此檔案,您可能會希望直接嘗試使用 [[Special:Upload|{{SITENAME}} 的上傳頁面]]。",
+       "foreign-structured-upload-form-2-label-intro": "感謝您貢獻影像給 {{SITENAME}},您應確認您的影像符合以下條件下繼續:",
+       "foreign-structured-upload-form-2-label-ownwork": "影像必須完全由<strong>您所創作</strong>,並非取自網際網路",
+       "foreign-structured-upload-form-2-label-noderiv": "影像並未包含<strong>他人的成果</strong>,或是來自他人的靈感",
+       "foreign-structured-upload-form-2-label-useful": "影像應具有<strong>教育意義</strong>並對教學他人有幫助",
+       "foreign-structured-upload-form-2-label-ccbysa": "影像必須採用 [https://creativecommons.org/licenses/by-sa/4.0/ 創用CC 姓名標示─相同方式分享 4.0] 授權條款,<strong>可以永久發布r</strong>於網際網路上",
+       "foreign-structured-upload-form-3-label-question-website": "您是否自其他網站下載此影像,或取自影像搜尋的結果?",
        "foreign-structured-upload-form-3-label-yes": "是",
        "foreign-structured-upload-form-3-label-no": "否",
        "backend-fail-stream": "無法傳輸檔案 \"$1\"。",
        "listgrouprights-namespaceprotection-header": "命名空間限制",
        "listgrouprights-namespaceprotection-namespace": "命名空間",
        "listgrouprights-namespaceprotection-restrictedto": "允許使用者編輯的權限",
+       "listgrants": "授權清單",
+       "listgrants-summary": "以下為與使用者權限相關連的授權清單。使用者可以授權應用程式使用他們自己的帳號,但限於使用者授予應用程式的權限。同時,應用程式亦不能使用使用者本身沒有的權限。\n您可能可以在[[{{MediaWiki:Listgrouprights-helppage}}|這裡]]取得個別權限的額外資料。",
+       "listgrants-grant": "授權",
+       "listgrants-rights": "權限",
        "trackingcategories": "追蹤分類",
        "trackingcategories-summary": "此頁面列出由 MediaWiki 軟體自動產生用來追蹤頁面的分類,這些分類的名稱可由命名空間 {{ns:8}} 中的相關系統訊息中修改。",
        "trackingcategories-msg": "追蹤分類",
        "unblock": "解除封鎖使用者",
        "blockip": "封鎖{{GENDER:$1|使用者}}",
        "blockip-legend": "封鎖使用者",
-       "blockiptext": "填寫以下表單可封鎖特定 IP 位址或使用者名稱的存取權限。\n這個動作應用來避免破壞行為,可根據 [[{{MediaWiki:Policy-url}}|管理政策]]。\n請在下方填寫一個具體的原因 (例如:引述一段破壞頁面的事實)。",
+       "blockiptext": "填寫以下表單可封鎖特定 IP 位址或使用者名稱的存取權限。\n這個動作應用來避免破壞行為,可根據 [[{{MediaWiki:Policy-url}}|管理政策]]。\n請在下方填寫一個具體的原因 (例如:引述一段破壞頁面的事實)。\n您可以使用 [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR] 語法格式封鎖 IP 範圍,最大允許的範圍 IPv4 為 /$1、IPv6 為 /$2。",
        "ipaddressorusername": "IP 位址或使用者名稱:",
        "ipbexpiry": "期限:",
        "ipbreason": "原因:",
        "export-download": "儲存為檔案",
        "export-templates": "包含模板",
        "export-pagelinks": "包含連結的頁面深度:",
+       "export-manual": "手動加入頁面:",
        "allmessages": "系統訊息",
        "allmessagesname": "名稱",
        "allmessagesdefault": "預設的訊息文字",
        "javascripttest-pagetext-frameworks": "請選擇下列一種測試 Framework:$1",
        "javascripttest-pagetext-skins": "選擇執行測試的外觀:",
        "javascripttest-qunit-intro": "請參考 mediawiki.org 的 [$1 測試說明文件]。",
-       "tooltip-pt-userpage": "您的使用者頁面",
+       "tooltip-pt-userpage": "{{GENDER:|您的使用者}}頁面",
        "tooltip-pt-anonuserpage": "您正在作為以下身分編輯此 IP 位址的使用者頁面",
-       "tooltip-pt-mytalk": "您的對話頁面",
+       "tooltip-pt-mytalk": "{{GENDER:|您的}}對話頁面",
        "tooltip-pt-anontalk": "有關來自此 IP 位址編輯的討論",
-       "tooltip-pt-preferences": "您的偏好設定",
+       "tooltip-pt-preferences": "{{GENDER:|您的}}偏好設定",
        "tooltip-pt-watchlist": "您正在監視變更的頁面清單",
-       "tooltip-pt-mycontris": "您的貢獻清單",
+       "tooltip-pt-mycontris": "{{GENDER:|您的}}貢獻清單",
        "tooltip-pt-anoncontribs": "由此 IP 位址編輯的清單",
        "tooltip-pt-login": "建議您先登入,但並非必要。",
        "tooltip-pt-logout": "登出",
        "tooltip-t-recentchangeslinked": "此頁面連結到其他頁面的最近更改",
        "tooltip-feed-rss": "此頁面的 RSS 摘要",
        "tooltip-feed-atom": "此頁面的 Atom 摘要",
-       "tooltip-t-contributions": "此使用者的貢獻清單",
-       "tooltip-t-emailuser": "傳送電子郵件聯絡這位使用者",
+       "tooltip-t-contributions": "{{GENDER:$1|此使用者}}的貢獻清單",
+       "tooltip-t-emailuser": "傳送電子郵件聯絡{{GENDER:$1|這位使用者}}",
        "tooltip-t-info": "更多關於此頁面的資訊",
        "tooltip-t-upload": "上傳檔案",
        "tooltip-t-specialpages": "全部特殊頁面的清單",
        "pageinfo-category-files": "檔案數量",
        "markaspatrolleddiff": "標記為已巡查",
        "markaspatrolledtext": "標記此頁面為已巡查",
+       "markaspatrolledtext-file": "標記此檔案版本為己巡查",
        "markedaspatrolled": "己標記為已巡查",
        "markedaspatrolledtext": "已標記選擇的修訂 [[:$1]] 為已巡查。",
        "rcpatroldisabled": "最近更改巡查已停用",
        "newimages-legend": "搜尋",
        "newimages-label": "檔案名稱 (或部份檔名):",
        "newimages-showbots": "顯示由機器人上傳的檔案",
+       "newimages-hidepatrolled": "隱藏己巡查上傳",
        "noimages": "無任何圖片。",
        "ilsubmit": "搜尋",
        "bydate": "依日期",
        "watchlisttools-edit": "檢視並編輯監視清單",
        "watchlisttools-raw": "編輯原始監視清單",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|對話]])",
+       "timezone-local": "當地",
        "duplicate-defaultsort": "<strong>警告:</strong>預設的排序鍵 \"$2\" 會覆蓋先前預設的排序鍵 \"$1\"。",
        "duplicate-displaytitle": "<strong>警告:</strong> 顯示標題 \"$2\" 覆蓋之前的顯示標題 \"$1\"。",
        "invalid-indicator-name": "<strong>錯誤:</strong>頁面狀態指示的 <code>name</code> 屬性不能為空。",
        "mediastatistics": "媒體統計資訊",
        "mediastatistics-summary": "已上傳檔案類型的統計資訊,此報表僅統計檔案的最新版本,不包含舊的或已刪除的版本。",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 位元組|$1 位元組}} ($2; $3%)",
-       "mediastatistics-bytespertype": "此章節總檔案大小:$1 位元組。",
-       "mediastatistics-allbytes": "所有檔案的總檔案大小:$1 位元組。",
+       "mediastatistics-bytespertype": "此章節總檔案大小:$1 位元組 ($2; $3%)。",
+       "mediastatistics-allbytes": "所有檔案的總檔案大小:$1 位元組 ($2)。",
        "mediastatistics-table-mimetype": "MIME 類型",
        "mediastatistics-table-extensions": "可用的副檔名",
        "mediastatistics-table-count": "檔案數量",
        "mw-widgets-dateinput-no-date": "未選擇日期",
        "mw-widgets-titleinput-description-new-page": "頁面不存在",
        "mw-widgets-titleinput-description-redirect": "重新導向至 $1",
-       "api-error-blacklisted": "請選擇另一個更具描述性的標題。"
+       "api-error-blacklisted": "請選擇另一個更具描述性的標題。",
+       "sessionprovider-generic": "$1 連線階段",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "以 cookie 為基礎的連線階段",
+       "sessionprovider-nocookies": "Cookie 功能可能已被關閉,請確認您改開啟 Cookie 功能並重新啟動。"
 }
index ec20601..165fef1 100644 (file)
@@ -395,6 +395,7 @@ $specialPageAliases = array(
        'Blankpage'                 => array( 'BlankPage' ),
        'Block'                     => array( 'Block', 'BlockIP', 'BlockUser' ),
        'Booksources'               => array( 'BookSources' ),
+       'BotPasswords'              => array( 'BotPasswords' ),
        'BrokenRedirects'           => array( 'BrokenRedirects' ),
        'Categories'                => array( 'Categories' ),
        'ChangeContentModel'        => array( 'ChangeContentModel' ),
@@ -425,6 +426,7 @@ $specialPageAliases = array(
        'Listbots'                  => array( 'ListBots' ),
        'Listfiles'                 => array( 'ListFiles', 'FileList', 'ImageList' ),
        'Listgrouprights'           => array( 'ListGroupRights', 'UserGroupRights' ),
+       'Listgrants'                => array( 'ListGrants' ),
        'Listredirects'             => array( 'ListRedirects' ),
        'ListDuplicatedFiles'       => array( 'ListDuplicatedFiles', 'ListFileDuplicates' ),
        'Listusers'                 => array( 'ListUsers', 'UserList' ),
diff --git a/maintenance/archives/patch-bot_passwords.sql b/maintenance/archives/patch-bot_passwords.sql
new file mode 100644 (file)
index 0000000..bd60ff7
--- /dev/null
@@ -0,0 +1,25 @@
+--
+-- This table contains a user's bot passwords: passwords that allow access to
+-- the account via the API with limited rights.
+--
+CREATE TABLE /*_*/bot_passwords (
+  -- Foreign key to user.user_id
+  bp_user int NOT NULL,
+
+  -- Application identifier
+  bp_app_id varbinary(32) NOT NULL,
+
+  -- Password hashes, like user.user_password
+  bp_password tinyblob NOT NULL,
+
+  -- Like user.user_token
+  bp_token binary(32) NOT NULL default '',
+
+  -- JSON blob for MWRestrictions
+  bp_restrictions blob NOT NULL,
+
+  -- Grants allowed to the account when authenticated with this bot-password
+  bp_grants blob NOT NULL,
+
+  PRIMARY KEY ( bp_user, bp_app_id )
+) /*$wgDBTableOptions*/;
index 86de029..05f091f 100644 (file)
@@ -40,6 +40,7 @@ U+05347升|U+05347升|U+06607昇|U+0965E陞|
 U+0535C卜|U+0535C卜|U+08514蔔|
 U+05360占|U+05360占|U+04F54佔|
 U+05364卤|U+09E75鹵|U+06EF7滷|
+U+05367卧|U+081E5臥|
 U+05377卷|U+05377卷|U+06372捲|
 U+05382厂|U+05EE0廠|U+05382厂|
 U+05386历|U+06B77歷|U+066C6曆|U+053A4厤|
index 7f94ede..944e20e 100644 (file)
 解析度      分辨率
 伺服器      服务器
 區域網      局域网
+區域網路   局域网络
 巨集 宏
 掃瞄器      扫描仪
 寬頻 宽带
 柯德莉·夏萍      奥黛丽·赫本
 華勒沙      瓦文萨
 華里沙      瓦文萨
+賓拉登      本拉登
+賓·拉登    本·拉登
 歐巴馬      奥巴马
 北韓 北朝鲜
 寮人民民主共和國       老挝人民民主共和国
@@ -2592,7 +2595,6 @@ A型肝炎        甲型肝炎
 大英國協   英联邦
 共和联邦   英联邦
 阿布達比   阿布扎比
-蓋曼群島   开曼群岛
 柴契爾      撒切尔
 戴卓爾      撒切尔
 凱薩琳      凯瑟琳
@@ -2609,9 +2611,9 @@ A型肝炎        甲型肝炎
 數位電視   数字电视
 數碼電視   数字电视
 數位技術   数字技术
-數碼技術   数字技术
 數位訊號   数字信号
 數碼訊號   数字信号
+數位化      数字化
 行動網路   移动网络
 流動網絡   移动网络
 咪高峰      麦克风
@@ -2639,7 +2641,7 @@ A型肝炎        甲型肝炎
 西元前      公元前
 翁山蘇姬   昂山素季
 昂山素姬   昂山素季
-西æ´\8bæ£\8b      å\9b¯际象棋
+西æ´\8bæ£\8b      å\9b½际象棋
 私隱 隐私
 格林美獎   格莱美奖
 葛萊美獎   格莱美奖
index acbce96..457457a 100644 (file)
 体里 體裏
 柜里 櫃裏
 电影里      電影裏
+广播里      廣播裏
+电视里      電視裏
+窝里斗      窩裏鬥
 苑裡 苑裡
 霄裡 霄裡
 岸裡 岸裡
 罗纳德·里根      朗奴·列根
 达芬奇      達文西
 达·芬奇    達·文西
+克卜勒      開普勒
 谢丽·布莱尔      彭雪玲
 葉爾欽      葉利欽
 菲利普親王        菲臘親王
 肖邦 蕭邦
 恺撒 凱撒
 肯尼迪      甘迺迪
+賓拉登      本拉登
+賓·拉登    本·拉登
 歐巴馬      奧巴馬
 戈登·布朗 白高敦
 狂牛症      瘋牛症
@@ -2956,7 +2962,6 @@ IP地址  IP位址
 大英國協   英聯邦
 共和联邦   英聯邦
 阿布達比   阿布扎比
-蓋曼群島   開曼群島
 宇航员      太空人
 薛丁格      薛定諤
 凯瑟琳      嘉芙蓮
@@ -2976,6 +2981,8 @@ IP地址  IP位址
 數位技術   數碼技術
 数字信号   數碼訊號
 數碼訊號   數碼訊號
+数字化      數碼化
+數位化      數碼化
 行動網路   流動網絡
 移动网络   流動網絡
 麥克風      咪高峰
index 57f9ab5..ddd6f9d 100644 (file)
@@ -4,7 +4,6 @@
 钩    鉤
 账    帳
 枱    檯
-卧    臥
 睾    睪
 酰    醯
 钫    鍅
 分辨率      解析度
 服务器      伺服器
 局域网      區域網
+局域网络   區域網路
 宽带 寬頻
 数据库      資料庫
 打印机      印表機
 穿梭機      太空梭
 因特网      網際網路
 互聯網      網際網路
+互聯網絡   網際網路
 机器人      機器人
 機械人      機器人
 移动电话   行動電話
 克林頓      柯林頓
 侯赛因      海珊
 侯賽因      海珊
+本拉登      賓拉登
+本·拉登    賓·拉登
 梵高 梵谷
 狄安娜      黛安娜
 戴安娜      黛安娜
 尼克松      尼克森
 威廉姆斯   威廉士
 多普勒      都卜勒
+开普勒      克卜勒
+開普勒      克卜勒
 叶利钦      葉爾欽
 卡斯特罗   卡斯楚
 包豪斯      包浩斯
@@ -663,7 +668,6 @@ IP地址    IP位址
 形而上学   形上學
 当且仅当   若且唯若
 圆珠笔      原子筆
-国际象棋   西洋棋
 可卡因      古柯鹼
 公共交通   公共運輸
 吉尼斯世界纪录  金氏世界紀錄
@@ -718,8 +722,6 @@ IP地址    IP位址
 英聯邦      大英國協
 共和联邦   大英國協
 阿布扎比   阿布達比
-开曼群岛   蓋曼群島
-開曼群島   蓋曼群島
 凯瑟琳      凱薩琳
 嘉芙蓮      凱薩琳
 门德尔松   孟德爾頌
@@ -740,6 +742,8 @@ IP地址    IP位址
 數碼技術   數位技術
 数字信号   數位訊號
 數碼訊號   數位訊號
+数字化      數位化
+數碼化      數位化
 移动网络   行動網路
 流動網絡   行動網路
 网络游戏   網路遊戲
@@ -758,7 +762,7 @@ IP地址    IP位址
 連結他      連結他
 昂山素季   翁山蘇姬
 昂山素姬   翁山蘇姬
\9b¯际象棋   西洋棋
\9b½际象棋   西洋棋
 國際象棋   西洋棋
 私隱 隱私
 硅藻 硅藻
@@ -772,3 +776,4 @@ IP地址    IP位址
 行人路權   行人路權
 行人路权   行人路權
 塑料袋      塑膠袋
+桃金娘      桃金孃
index b30fdd6..2bc8945 100644 (file)
@@ -47,6 +47,7 @@
 黃鈺筑      黃鈺筑
 答复 答覆
 反复 反覆
+反反复复   反反覆覆
 候复 候覆
 待复 待覆
 批复 批覆
 于克-蘭多縣       于克-蘭多縣
 于斯納爾斯貝里  于斯納爾斯貝里
 夏于喬      夏于喬
+于逸堯      于逸堯
 涂澤民      涂澤民
 涂長望      涂長望
 涂敏恆      涂敏恆
index 8e1e90d..309bd5d 100644 (file)
 皺彆
 一彆頭
 并州
+,並力
+,并力攻
+,并力討
 併兼
 併骨
 併網
 艸木丰丰
 張三丰
 復始
+往復式
 複分析
 複輔音
 複元音
 麵點、
 、麵點
 麵製品
+乾脆麵
 冷面相
 糞穢衊面
 僕僕
 瀋丹線
 瀋丹鐵路
 瀋丹客運
+瀋丹高
 瀋大線
 瀋大鐵路
 瀋大高速
 萬紮
 誌異
 筑前
+修築前
+建築前
 筑後
+修築後
+建築後
 筑紫
 筑波
 筑州
 顯示表示
 電錶
 水錶
+水表示
 咪錶
 射鵰
 神鵰
 傲遊 # 浏览器名
 網遊
 桌遊
+手遊
 遊輪
 遊牧
 遊蕩
 鬥垮
 鬥敗
 鬥戰
+窩裡鬥
 石樑
 木樑
 藏歷史
 讚唄
 點讚
 點個讚
+讚一個
 超讚
 飛紮
 紮裹
 醜語
 母醜
 一齣子
-齣兒
 丰標
 丰姿
 丰韻
 上天里
 天里昂
 人生天里
\99½子里
\99¾子里
 朴子里
 翁子里
 田子里
 櫃裡
 片裡
 電影裡
+廣播裡
+電視裡
 裏白 #植物常用名
 烏蘇里 #分詞用
 首發
 王后
 王侯后
 母后
+字母後
+聲母後
 武后
 歌后
 影后
 鮮于
 朝鮮於
 于寶軒
+于承惠
 于震
 於震前
 於震後
 電子製表
 製毒
 製販
+譯製
 遏制 #以下分詞用
 管制
 抑制
diff --git a/maintenance/postgres/archives/patch-bot_passwords.sql b/maintenance/postgres/archives/patch-bot_passwords.sql
new file mode 100644 (file)
index 0000000..8e8a794
--- /dev/null
@@ -0,0 +1,9 @@
+CREATE TABLE bot_passwords (
+  bp_user INTEGER NOT NULL,
+  bp_app_id TEXT NOT NULL,
+  bp_password TEXT NOT NULL,
+  bp_token TEXT NOT NULL,
+  bp_restrictions TEXT NOT NULL,
+  bp_grants TEXT NOT NULL,
+  PRIMARY KEY ( bp_user, bp_app_id )
+);
index ad7bd9d..c9f049b 100644 (file)
@@ -74,6 +74,15 @@ CREATE TABLE user_newtalk (
 CREATE INDEX user_newtalk_id_idx ON user_newtalk (user_id);
 CREATE INDEX user_newtalk_ip_idx ON user_newtalk (user_ip);
 
+CREATE TABLE bot_passwords (
+  bp_user INTEGER NOT NULL,
+  bp_app_id TEXT NOT NULL,
+  bp_password TEXT NOT NULL,
+  bp_token TEXT NOT NULL,
+  bp_restrictions TEXT NOT NULL,
+  bp_grants TEXT NOT NULL,
+  PRIMARY KEY ( bp_user, bp_app_id )
+);
 
 CREATE SEQUENCE page_page_id_seq;
 CREATE TABLE page (
index 756f6c0..743b9be 100644 (file)
@@ -220,6 +220,32 @@ CREATE TABLE /*_*/user_properties (
 CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property);
 CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property);
 
+--
+-- This table contains a user's bot passwords: passwords that allow access to
+-- the account via the API with limited rights.
+--
+CREATE TABLE /*_*/bot_passwords (
+  -- User ID obtained from CentralIdLookup.
+  bp_user int NOT NULL,
+
+  -- Application identifier
+  bp_app_id varbinary(32) NOT NULL,
+
+  -- Password hashes, like user.user_password
+  bp_password tinyblob NOT NULL,
+
+  -- Like user.user_token
+  bp_token binary(32) NOT NULL default '',
+
+  -- JSON blob for MWRestrictions
+  bp_restrictions blob NOT NULL,
+
+  -- Grants allowed to the account when authenticated with this bot-password
+  bp_grants blob NOT NULL,
+
+  PRIMARY KEY ( bp_user, bp_app_id )
+) /*$wgDBTableOptions*/;
+
 --
 -- Core of the wiki: each page has an entry here which identifies
 -- it by title and contains some essential metadata.
index 3548239..a4b86ad 100644 (file)
@@ -21,6 +21,8 @@
        "ooui-dialog-process-dismiss": "Прапусьціць",
        "ooui-dialog-process-retry": "Паспрабаваць зноў",
        "ooui-dialog-process-continue": "Працягваць",
+       "ooui-selectfile-button-select": "Абраць файл",
        "ooui-selectfile-not-supported": "Выбар файлу не падтрымліваецца",
-       "ooui-selectfile-placeholder": "Ніводзін файл не абраны"
+       "ooui-selectfile-placeholder": "Ніводзін файл не абраны",
+       "ooui-selectfile-dragdrop-placeholder": "Перацягніце файл сюды"
 }
index f1a1a47..76e2614 100644 (file)
@@ -23,6 +23,7 @@
        "ooui-dialog-process-dismiss": "დამალვა",
        "ooui-dialog-process-retry": "კიდევ სცადეთ",
        "ooui-dialog-process-continue": "გაგრძელება",
+       "ooui-selectfile-button-select": "აირჩიეთ ფაილი",
        "ooui-selectfile-not-supported": "ფაილის არჩევა არ არის მხარდაჭერილი",
        "ooui-selectfile-placeholder": "ფაილი არ არის არჩეული"
 }
index 1d7317b..779ba7b 100644 (file)
@@ -15,5 +15,9 @@
        "ooui-dialog-process-error": "Бірдеңеден қате кетті",
        "ooui-dialog-process-dismiss": "Тоқтату",
        "ooui-dialog-process-retry": "Қайта байқап көріңіз",
-       "ooui-dialog-process-continue": "Жалғастыру"
+       "ooui-dialog-process-continue": "Жалғастыру",
+       "ooui-selectfile-button-select": "Файлды таңдау",
+       "ooui-selectfile-not-supported": "Файл таңдауды қолдамайды",
+       "ooui-selectfile-placeholder": "Файл таңдалмады",
+       "ooui-selectfile-dragdrop-placeholder": "Файлды мында жылжыту"
 }
index ec17c9b..ab6db14 100644 (file)
@@ -1,7 +1,8 @@
 {
        "@metadata": {
                "authors": [
-                       "Hosseinblue"
+                       "Hosseinblue",
+                       "Arash71"
                ]
        },
        "ooui-outline-control-move-down": "جاوواز کردن ئإ هووار",
@@ -11,7 +12,7 @@
        "ooui-toolgroup-expand": "ویشتر/فرۀتر",
        "ooui-toolgroup-collapse": "کۀمتر",
        "ooui-dialog-message-accept": "خوو/ باشد",
-       "ooui-dialog-message-reject": "ئآهووسانن-لغو",
+       "ooui-dialog-message-reject": "ئآهووسانن/لغو",
        "ooui-dialog-process-error": "مشکلی هۀس",
        "ooui-dialog-process-dismiss": "رد کردن",
        "ooui-dialog-process-retry": "دووآرۀ تلاش کۀ",
diff --git a/resources/lib/oojs-ui/i18n/vep.json b/resources/lib/oojs-ui/i18n/vep.json
new file mode 100644 (file)
index 0000000..b6ad092
--- /dev/null
@@ -0,0 +1,8 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Sebranik"
+               ]
+       },
+       "ooui-toolgroup-expand": "Enamba"
+}
diff --git a/resources/lib/oojs-ui/i18n/war.json b/resources/lib/oojs-ui/i18n/war.json
new file mode 100644 (file)
index 0000000..b0ea30c
--- /dev/null
@@ -0,0 +1,19 @@
+{
+       "@metadata": {
+               "authors": [
+                       "JinJian"
+               ]
+       },
+       "ooui-outline-control-move-down": "Ibalhin paubos",
+       "ooui-outline-control-move-up": "Ibalhin paigbaw",
+       "ooui-outline-control-remove": "Tanggala",
+       "ooui-toolbar-more": "Damo pa",
+       "ooui-toolgroup-expand": "Damo pa",
+       "ooui-toolgroup-collapse": "Guruguti",
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Igpabaliwaray",
+       "ooui-dialog-process-error": "Mayda sayop nga nahitabo",
+       "ooui-dialog-process-retry": "Utroha",
+       "ooui-dialog-process-continue": "Padayon",
+       "ooui-selectfile-button-select": "Pagpili hin file"
+}
index 5e1caa8..ef5a0da 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.14.1
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-12-08T21:43:53Z
+ * Date: 2016-01-12T23:06:40Z
  */
 @-webkit-keyframes oo-ui-progressBarWidget-slide {
        from {
           -moz-transition: border-color 100ms ease;
                transition: border-color 100ms ease;
        background: #eeeeee;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#dddddd');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#fff', endColorstr='#ddd');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #ffffff), color-stop(100%, #dddddd));
        background-image: -webkit-linear-gradient(top, #ffffff 0%, #dddddd 100%);
        background-image:    -moz-linear-gradient(top, #ffffff 0%, #dddddd 100%);
        color: black;
        border-color: #c9c9c9;
        background: #eeeeee;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#dddddd', endColorstr='#ffffff');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ddd', endColorstr='#fff');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #dddddd), color-stop(100%, #ffffff));
        background-image: -webkit-linear-gradient(top, #dddddd 0%, #ffffff 100%);
        background-image:    -moz-linear-gradient(top, #dddddd 0%, #ffffff 100%);
        border-color: rgba(0, 0, 0, 0.2);
        box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
        background: #f8fbfd;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f1f7fb', endColorstr='#ffffff');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#F1F7FB', endColorstr='#fff');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #f1f7fb), color-stop(100%, #ffffff));
        background-image: -webkit-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
        background-image:    -moz-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
        border-bottom-right-radius: 0;
        box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
        background: #f8fbfd;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f1f7fb', endColorstr='#ffffff');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#F1F7FB', endColorstr='#fff');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #f1f7fb), color-stop(100%, #ffffff));
        background-image: -webkit-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
        background-image:    -moz-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
        border-color: rgba(0, 0, 0, 0.1);
        box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
        background: #f8fbfd;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f1f7fb', endColorstr='#ffffff');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#F1F7FB', endColorstr='#fff');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #f1f7fb), color-stop(100%, #ffffff));
        background-image: -webkit-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
        background-image:    -moz-linear-gradient(top, #f1f7fb 0%, #ffffff 100%);
 .oo-ui-toolbar-bar {
        border-bottom: 1px solid #cccccc;
        background: #f8fbfd;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#f1f7fb');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#fff', endColorstr='#F1F7FB');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #ffffff), color-stop(100%, #f1f7fb));
        background-image: -webkit-linear-gradient(top, #ffffff 0%, #f1f7fb 100%);
        background-image:    -moz-linear-gradient(top, #ffffff 0%, #f1f7fb 100%);
        border: 1px solid #cccccc;
        margin-right: 0.5em;
        background: #eeeeee;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#dddddd', endColorstr='#ffffff');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ddd', endColorstr='#fff');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #dddddd), color-stop(100%, #ffffff));
        background-image: -webkit-linear-gradient(top, #dddddd 0%, #ffffff 100%);
        background-image:    -moz-linear-gradient(top, #dddddd 0%, #ffffff 100%);
           -moz-transition: left 250ms ease, margin-left 250ms ease;
                transition: left 250ms ease, margin-left 250ms ease;
        background: #eeeeee;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#dddddd');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#fff', endColorstr='#ddd');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #ffffff), color-stop(100%, #dddddd));
        background-image: -webkit-linear-gradient(top, #ffffff 0%, #dddddd 100%);
        background-image:    -moz-linear-gradient(top, #ffffff 0%, #dddddd 100%);
        height: 1.7em;
        line-height: 1.7em;
        background: #eeeeee;
-       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#dddddd');
+       filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#fff', endColorstr='#ddd');
        background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #ffffff), color-stop(100%, #dddddd));
        background-image: -webkit-linear-gradient(top, #ffffff 0%, #dddddd 100%);
        background-image:    -moz-linear-gradient(top, #ffffff 0%, #dddddd 100%);
index 7db559c..c287b0d 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.14.1
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-12-08T21:43:47Z
+ * Date: 2016-01-12T23:06:31Z
  */
 /**
  * @class
index a3ebbae..f97dcc9 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.14.1
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-12-08T21:43:53Z
+ * Date: 2016-01-12T23:06:40Z
  */
 @-webkit-keyframes oo-ui-progressBarWidget-slide {
        from {
                margin-left: 100%;
        }
 }
-@-ms-keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
 @-o-keyframes oo-ui-progressBarWidget-slide {
        from {
                margin-left: -40%;
        left: 0.2em;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
-       color: #ffffff;
        background: #dddddd;
+       color: #ffffff;
        border: 1px solid #dddddd;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
        margin-bottom: 1.25em;
 }
 .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
-       padding: 0.25em;
-       padding-left: 1em;
+       padding: 0.25em 0.25em 0.25em 1em;
 }
 .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
        padding-top: 0.25em;
 }
 .oo-ui-fieldLayout-messages {
        list-style: none none;
-       margin: 0;
+       margin: 0.25em 0 0 0.25em;
        padding: 0;
-       margin-top: 0.25em;
-       margin-left: 0.25em;
 }
 .oo-ui-fieldLayout-messages > li {
        margin: 0;
        position: relative;
        margin: 0;
        padding: 0;
-       border: none;
+       border: 0;
 }
 .oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
        display: block;
 }
 .oo-ui-panelLayout-framed {
        border: 1px solid #aaaaaa;
-       border-radius: 0.2em;
+       border-radius: 2px;
        box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
 }
 .oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
        color: #555555;
 }
 .oo-ui-toolbar-bar .oo-ui-toolbar-bar {
-       border: none;
+       border: 0;
        background: none;
        box-shadow: none;
 }
        display: block;
        cursor: pointer;
        padding: 0.25em 0.5em;
-       border: none;
+       border: 0;
 }
 .oo-ui-optionWidget.oo-ui-widget-disabled {
        cursor: default;
        background-color: transparent;
 }
 .oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
-       padding: 0.25em;
-       padding-left: 1em;
+       padding: 0.25em 0.25em 0.25em 1em;
 }
 .oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
        margin-right: 0;
        margin-left: 0;
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on {
-       background: #347bff;
+       background-color: #347bff;
        border-color: #347bff;
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
-       background: #ffffff;
+       background-color: #ffffff;
        box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover {
        border-color: #2962cc;
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover.oo-ui-toggleWidget-on {
-       background: #2962cc;
+       background-color: #2962cc;
        border-color: #2962cc;
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus {
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active .oo-ui-toggleSwitchWidget-grip,
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active:hover .oo-ui-toggleSwitchWidget-grip {
-       background: #ffffff;
+       background-color: #ffffff;
        box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
        max-width: 50em;
        background-color: #ffffff;
        border: 1px solid #cccccc;
-       border-radius: 0.1em;
+       border-radius: 2px;
        overflow: hidden;
 }
 .oo-ui-progressBarWidget-bar {
 .oo-ui-popupWidget-popup {
        background-color: #ffffff;
        border: 1px solid #aaaaaa;
-       border-radius: 0.2em;
+       border-radius: 2px;
        box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
 }
 .oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
           -moz-box-sizing: border-box;
                box-sizing: border-box;
        border: 1px solid #cccccc;
-       border-radius: 0.1em;
+       border-radius: 2px;
        padding-left: 1em;
        vertical-align: middle;
 }
        font-family: inherit;
        background-color: #ffffff;
        color: black;
-       border: solid 1px #cccccc;
+       border: 1px solid #cccccc;
        box-shadow: inset 0 0 0 0 #347bff;
-       border-radius: 0.1em;
+       border-radius: 2px;
        -webkit-transition: box-shadow 100ms ease;
           -moz-transition: box-shadow 100ms ease;
                transition: box-shadow 100ms ease;
 }
 .oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input,
 .oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea {
-       border-color: red;
-       box-shadow: inset 0 0 0 0 red;
+       border-color: #ff0000;
+       box-shadow: inset 0 0 0 0 #ff0000;
 }
 .oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input:focus,
 .oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea:focus {
-       border-color: red;
-       box-shadow: inset 0 0 0 0.1em red;
+       border-color: #ff0000;
+       box-shadow: inset 0 0 0 0.1em #ff0000;
 }
 .oo-ui-textInputWidget.oo-ui-widget-disabled input,
 .oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
        background-color: #ffffff;
        margin-top: -1px;
        border: 1px solid #aaaaaa;
-       border-radius: 0 0 0.2em 0.2em;
+       border-radius: 0 0 2px 2px;
        box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
 }
 .oo-ui-menuSelectWidget input {
        height: 2.275em;
        line-height: 1.275;
        border: 1px solid #cccccc;
-       border-radius: 0.1em;
+       border-radius: 2px;
 }
 .oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
        right: 0;
        height: 2.4em;
        background-color: #ffffff;
        border: 1px solid #cccccc;
-       border-radius: 0.1em;
+       border-radius: 2px;
 }
 .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
        right: 0;
        margin-right: 0.5em;
        padding: 0.15em 0.25em;
        border: 1px solid #cccccc;
-       border-radius: 0.1em;
+       border-radius: 2px;
        -webkit-box-sizing: border-box;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
        background-repeat: no-repeat;
 }
 .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
-       border: none;
+       border: 0;
        line-height: 1.675em;
-       margin: 0;
-       margin-left: 0.2em;
+       margin: 0 0 0 0.2em;
        padding: 0;
        font-size: inherit;
        font-family: inherit;
        background-color: #eeeeee;
        border: 1px solid #cccccc;
        color: #555555;
-       border-radius: 0.1em;
+       border-radius: 2px;
 }
 .oo-ui-capsuleItemWidget > .oo-ui-iconElement-icon {
        cursor: pointer;
        padding: 1em;
        border: 1px solid #ff9e9e;
        background-color: #fff7f7;
-       border-radius: 0.25em;
+       border-radius: 2px;
 }
 .oo-ui-windowManager-modal > .oo-ui-dialog {
        position: fixed;
 }
 .oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
        border: 1px solid #aaaaaa;
-       border-radius: 0.2em;
+       border-radius: 2px;
        box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
 }
index 677ab5b..210fec9 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.14.1
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-12-08T21:43:47Z
+ * Date: 2016-01-12T23:06:31Z
  */
 /**
  * @class
index c77bfd7..f280853 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.14.1
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-12-08T21:43:47Z
+ * Date: 2016-01-12T23:06:31Z
  */
 ( function ( OO ) {
 
@@ -44,6 +44,17 @@ OO.ui.Keys = {
        SPACE: 32
 };
 
+/**
+ * Constants for MouseEvent.which
+ *
+ * @property {Object}
+ */
+OO.ui.MouseButtons = {
+       LEFT: 1,
+       MIDDLE: 2,
+       RIGHT: 3
+};
+
 /**
  * @property {Number}
  */
@@ -248,35 +259,27 @@ OO.ui.debounce = function ( func, wait, immediate ) {
 };
 
 /**
- * Proxy for `node.addEventListener( eventName, handler, true )`, if the browser supports it.
- * Otherwise falls back to non-capturing event listeners.
+ * Proxy for `node.addEventListener( eventName, handler, true )`.
  *
  * @param {HTMLElement} node
  * @param {string} eventName
  * @param {Function} handler
+ * @deprecated
  */
 OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
-       if ( node.addEventListener ) {
-               node.addEventListener( eventName, handler, true );
-       } else {
-               node.attachEvent( 'on' + eventName, handler );
-       }
+       node.addEventListener( eventName, handler, true );
 };
 
 /**
- * Proxy for `node.removeEventListener( eventName, handler, true )`, if the browser supports it.
- * Otherwise falls back to non-capturing event listeners.
+ * Proxy for `node.removeEventListener( eventName, handler, true )`.
  *
  * @param {HTMLElement} node
  * @param {string} eventName
  * @param {Function} handler
+ * @deprecated
  */
 OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
-       if ( node.addEventListener ) {
-               node.removeEventListener( eventName, handler, true );
-       } else {
-               node.detachEvent( 'on' + eventName, handler );
-       }
+       node.removeEventListener( eventName, handler, true );
 };
 
 /**
@@ -1487,9 +1490,7 @@ OO.ui.Element.static.getDocument = function ( obj ) {
  */
 OO.ui.Element.static.getWindow = function ( obj ) {
        var doc = this.getDocument( obj );
-       // Support: IE 8
-       // Standard Document.defaultView is IE9+
-       return doc.parentWindow || doc.defaultView;
+       return doc.defaultView;
 };
 
 /**
@@ -1604,14 +1605,8 @@ OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
  */
 OO.ui.Element.static.getBorders = function ( el ) {
        var doc = el.ownerDocument,
-               // Support: IE 8
-               // Standard Document.defaultView is IE9+
-               win = doc.parentWindow || doc.defaultView,
-               style = win && win.getComputedStyle ?
-                       win.getComputedStyle( el, null ) :
-                       // Support: IE 8
-                       // Standard getComputedStyle() is IE9+
-                       el.currentStyle,
+               win = doc.defaultView,
+               style = win.getComputedStyle( el, null ),
                $el = $( el ),
                top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
                left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
@@ -1636,9 +1631,7 @@ OO.ui.Element.static.getBorders = function ( el ) {
 OO.ui.Element.static.getDimensions = function ( el ) {
        var $el, $win,
                doc = el.ownerDocument || el.document,
-               // Support: IE 8
-               // Standard Document.defaultView is IE9+
-               win = doc.parentWindow || doc.defaultView;
+               win = doc.defaultView;
 
        if ( win === el || el === doc.documentElement ) {
                $win = $( win );
@@ -2924,7 +2917,7 @@ OO.ui.Dialog.static.name = '';
  *
  * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
  * that will produce a Label node or string. The title can also be specified with data passed to the
- * constructor (see #getSetupProcess). In this case, the static value will be overriden.
+ * constructor (see #getSetupProcess). In this case, the static value will be overridden.
  *
  * @abstract
  * @static
@@ -2937,7 +2930,7 @@ OO.ui.Dialog.static.title = '';
  * An array of configured {@link OO.ui.ActionWidget action widgets}.
  *
  * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
- * value will be overriden.
+ * value will be overridden.
  *
  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
  *
@@ -2967,7 +2960,7 @@ OO.ui.Dialog.static.escapable = true;
  */
 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
-               this.close();
+               this.executeAction( '' );
                e.preventDefault();
                e.stopPropagation();
        }
@@ -4740,13 +4733,13 @@ OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
                return;
        }
        this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
        // reliably remove the pressed class
-       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
+       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
        // Prevent change of focus unless specifically configured otherwise
        if ( this.constructor.static.cancelButtonMouseDownEvents ) {
                return false;
@@ -4760,12 +4753,12 @@ OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
  * @param {jQuery.Event} e Mouse up event
  */
 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
                return;
        }
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for mouseup, since we only needed this once
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
 };
 
 /**
@@ -4776,7 +4769,7 @@ OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
  * @fires click
  */
 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                if ( this.emit( 'click' ) ) {
                        return false;
                }
@@ -4796,7 +4789,7 @@ OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
        this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the keyup handler no matter where the key is when the button is let go, so we can
        // reliably remove the pressed class
-       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
+       this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
 };
 
 /**
@@ -4811,7 +4804,7 @@ OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
        }
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for keyup, since we only needed this once
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
+       this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
 };
 
 /**
@@ -7491,7 +7484,7 @@ OO.ui.Tool.static.autoAddToGroup = true;
 /**
  * Check if this tool is compatible with given data.
  *
- * This is a stub that can be overriden to provide support for filtering tools based on an
+ * This is a stub that can be overridden to provide support for filtering tools based on an
  * arbitrary piece of information  (e.g., where the cursor is in a document). The implementation
  * must also call this method so that the compatibility check can be performed.
  *
@@ -8310,13 +8303,13 @@ OO.ui.ToolGroup.prototype.updateDisabled = function () {
 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.pressed = this.getTargetTool( e );
                if ( this.pressed ) {
                        this.pressed.setActive( true );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+                       this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
                }
                return false;
        }
@@ -8329,8 +8322,8 @@ OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
  * @param {Event} e Mouse up or key up event
  */
 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+       this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
        // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
        // released, but since `this.pressed` will no longer be true, the second call will be ignored.
        this.onMouseKeyUp( e );
@@ -8347,7 +8340,7 @@ OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
 
        if (
                !this.isDisabled() && this.pressed && this.pressed === tool &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.pressed.onSelect();
                this.pressed = null;
@@ -11815,7 +11808,7 @@ OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
        // Only close toolgroup when a tool was actually selected
        if (
                !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.setActive( false );
        }
@@ -11831,7 +11824,7 @@ OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                return false;
        }
@@ -11846,7 +11839,7 @@ OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.setActive( !this.active );
                return false;
@@ -11865,8 +11858,8 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
        if ( this.active !== value ) {
                this.active = value;
                if ( value ) {
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+                       this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
 
                        this.$clippable.css( 'left', '' );
                        // Try anchoring the popup to the left first
@@ -11894,8 +11887,8 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
                                } );
                        }
                } else {
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
+                       this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+                       this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
                        this.$element.removeClass(
                                'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
                        );
@@ -12092,7 +12085,7 @@ OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
        // Do not close the popup when the user wants to show more/fewer tools
        if (
                $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
                // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
@@ -13863,7 +13856,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
-       if ( e.which === 1 ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                this.focus();
                return false;
        } else {
@@ -14286,7 +14279,7 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.menu.toggle();
        }
        return false;
@@ -16051,7 +16044,7 @@ OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
  * @fires icon
  */
 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
-       if ( e.which === 1 ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                this.$input[ 0 ].focus();
                return false;
        }
@@ -16065,7 +16058,7 @@ OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
  * @fires indicator
  */
 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
-       if ( e.which === 1 ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                if ( this.type === 'search' ) {
                        // Clear the text field
                        this.setValue( '' );
@@ -16349,7 +16342,7 @@ OO.ui.TextInputWidget.prototype.isAutosizing = function () {
  * @chainable
  */
 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
-       var textRange, isBackwards, start, end,
+       var isBackwards, start, end,
                input = this.$input[ 0 ];
 
        to = to || from;
@@ -16360,16 +16353,7 @@ OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
 
        this.focus();
 
-       if ( input.setSelectionRange ) {
-               input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
-       } else if ( input.createTextRange ) {
-               // IE 8 and below
-               textRange = input.createTextRange();
-               textRange.collapse( true );
-               textRange.moveStart( 'character', start );
-               textRange.moveEnd( 'character', end - start );
-               textRange.select();
-       }
+       input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
        return this;
 };
 
@@ -16836,7 +16820,7 @@ OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.menu.toggle();
                this.$input[ 0 ].focus();
        }
@@ -17591,7 +17575,7 @@ OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
  *
  * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
  *
- * @param {boolean} movable Item is removable
+ * @param {boolean} removable Item is removable
  * @chainable
  */
 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
@@ -17815,7 +17799,7 @@ OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
  */
 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
        // Capture clicks outside popup
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
+       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
@@ -17835,7 +17819,7 @@ OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
  * @private
  */
 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
+       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
@@ -17861,7 +17845,7 @@ OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
  * @private
  */
 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
+       this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
 /**
@@ -17870,7 +17854,7 @@ OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
  * @private
  */
 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
+       this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
 /**
@@ -18449,22 +18433,14 @@ OO.ui.SelectWidget.static.passAllFilter = function () {
 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
        var item;
 
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.togglePressed( true );
                item = this.getTargetItem( e );
                if ( item && item.isSelectable() ) {
                        this.pressItem( item );
                        this.selecting = item;
-                       OO.ui.addCaptureEventListener(
-                               this.getElementDocument(),
-                               'mouseup',
-                               this.onMouseUpHandler
-                       );
-                       OO.ui.addCaptureEventListener(
-                               this.getElementDocument(),
-                               'mousemove',
-                               this.onMouseMoveHandler
-                       );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+                       this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
                }
        }
        return false;
@@ -18486,16 +18462,14 @@ OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
                        this.selecting = item;
                }
        }
-       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
                this.pressItem( null );
                this.chooseItem( this.selecting );
                this.selecting = null;
        }
 
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup',
-               this.onMouseUpHandler );
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousemove',
-               this.onMouseMoveHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+       this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
 
        return false;
 };
@@ -18615,7 +18589,7 @@ OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
  * @protected
  */
 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
+       this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
 };
 
 /**
@@ -18624,7 +18598,7 @@ OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
  * @protected
  */
 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
+       this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
 };
 
 /**
@@ -18733,7 +18707,7 @@ OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
  * @protected
  */
 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
+       this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
 };
 
 /**
@@ -18745,7 +18719,7 @@ OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
  * @protected
  */
 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
+       this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
        this.clearKeyPressBuffer();
 };
 
@@ -19551,12 +19525,12 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
                        // Auto-hide
                        if ( this.autoHide ) {
-                               OO.ui.addCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
+                               this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
                        }
                } else {
                        this.unbindKeyDownListener();
                        this.unbindKeyPressListener();
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
+                       this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
                        this.toggleClipping( false );
                }
        }
@@ -20145,7 +20119,7 @@ OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.setValue( !this.value );
        }
        return false;
diff --git a/resources/lib/oojs-ui/themes/apex/icons-content.json b/resources/lib/oojs-ui/themes/apex/icons-content.json
new file mode 100644 (file)
index 0000000..0886fa6
--- /dev/null
@@ -0,0 +1,10 @@
+{
+       "prefix": "oo-ui-icon",
+       "intro": "@import '../../../../src/styles/common';",
+       "images": {
+               "articleRedirect": { "file": {
+                       "ltr": "images/icons/articleRedirect-ltr.svg",
+                       "rtl": "images/icons/articleRedirect-rtl.svg"
+               } }
+       }
+}
index a6abce5..ab04d36 100644 (file)
                        "ltr": "images/icons/find-ltr.svg",
                        "rtl": "images/icons/find-rtl.svg"
                } },
-               "insert": { "file": "images/icons/add.svg" },
+               "language": { "file": {
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
+               } },
                "layout": { "file": {
                        "ltr": "images/icons/layout-ltr.svg",
                        "rtl": "images/icons/layout-rtl.svg"
                        "ltr": "images/icons/newline-ltr.svg",
                        "rtl": "images/icons/newline-rtl.svg"
                } },
-               "redirect": { "file": {
-                       "ltr": "images/icons/redirect-ltr.svg",
-                       "rtl": "images/icons/redirect-rtl.svg"
-               } },
                "noWikiText": { "file": {
                        "ltr": "images/icons/noWikiText-ltr.svg",
                        "rtl": "images/icons/noWikiText-rtl.svg"
@@ -47,8 +46,8 @@
                        "rtl": "images/icons/quotesAdd-rtl.svg"
                } },
                "redirect": { "file": {
-                       "ltr": "images/icons/redirect-ltr.svg",
-                       "rtl": "images/icons/redirect-rtl.svg"
+                       "ltr": "images/icons/articleRedirect-ltr.svg",
+                       "rtl": "images/icons/articleRedirect-rtl.svg"
                } },
                "searchCaseSensitive": { "file": "images/icons/case-sensitive.svg" },
                "searchRegularExpression": { "file": "images/icons/regular-expression.svg" },
@@ -71,8 +70,8 @@
                        "rtl": "images/icons/templateAdd-rtl.svg"
                } },
                "translation": { "file": {
-                       "ltr": "images/icons/translation-ltr.svg",
-                       "rtl": "images/icons/translation-rtl.svg"
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
                } },
                "wikiText": { "file": "images/icons/wikiText.svg" }
        }
index 4fb736c..decae86 100644 (file)
                                "en": "images/icons/underline-u.svg"
                        }
                } },
-               "textLanguage": { "file": "images/icons/language.svg" },
+               "textLanguage": { "file": {
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
+               } },
                "textDirLTR": { "file": "images/icons/text-dir-lefttoright.svg" },
                "textDirRTL": { "file": "images/icons/text-dir-righttoleft.svg" },
                "textStyle": { "file": "images/icons/text-style.svg" }
index 091d5d7..b5fbbed 100644 (file)
@@ -29,7 +29,6 @@
                        "rtl": "images/icons/move-rtl.svg"
                } },
                "notice": { "file": "images/icons/notice.svg" },
-               "picture": { "file": "images/icons/image.svg" },
                "previous": { "file": {
                        "ltr": "images/icons/move-rtl.svg",
                        "rtl": "images/icons/move-ltr.svg"
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.png
new file mode 100644 (file)
index 0000000..8b0920f
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-ltr.svg
new file mode 100644 (file)
index 0000000..028c64c
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="article-redirect">
+        <path id="arrow" d="M18.1 14.2L23 18l-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
+        <path id="page" d="M5 3v13c0 1.7 1.3 3 3 3h3.375c-.157-.205-.3-.43-.438-.656-.42-.688-.77-1.483-.843-2.344H7v-1h3.125l.125-1H7v-1h3.375l.03-.188.283.188H16v1h-3.906l.22.156c.523.375 1.065.64 1.592.844H16v.406c.208-.013.418-.07.625-.094.068-1.294.125-3.874.125-3.874l1.25.968V3H5zm2 2h4v1H7V5zm5 0h4v5h-4V5zM7 7h4v1H7V7zm0 2h4v1H7V9zm0 2h9v1H7v-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.png
new file mode 100644 (file)
index 0000000..709673f
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/articleRedirect-rtl.svg
new file mode 100644 (file)
index 0000000..6a9c683
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="article-redirect">
+        <path id="arrow" d="M5.9 14.2L1 18l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3"/>
+        <path id="page" d="M19 3v13c0 1.7-1.3 3-3 3h-3.375c.157-.205.3-.43.438-.656.42-.688.77-1.483.843-2.344H17v-1h-3.125l-.125-1H17v-1h-3.375l-.03-.188-.283.188H8v1h3.906l-.22.156c-.523.375-1.065.64-1.592.844H8v.406c-.208-.013-.418-.07-.625-.094-.068-1.294-.125-3.874-.125-3.874L6 12.405V3zm-2 2h-4v1h4zm-5 0H8v5h4zm5 2h-4v1h4zm0 2h-4v1h4zm0 2H8v1h9z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.png
new file mode 100644 (file)
index 0000000..ef61b8b
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/language-ltr.svg
new file mode 100644 (file)
index 0000000..4bf074d
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="translation">
+        <path id="english" d="M14.34 9l-3.53 10h2.064l.72-2.406h3.624l.72 2.406H20L16.465 9h-2.12zm1.065 1.53L16.75 15h-2.69z"/>
+        <path id="chinese" d="M8.97 4.22c-.43.29-.88.616-1.25.874l.186.312c.14.194.275.393.407.594H4.47v1.47h1.593c.43 1.41 1.11 2.624 2.03 3.624-1.008.664-2.192 1.248-3.624 1.75L4 13c.317.487.714.976 1.03 1.375l.25-.094c1.593-.59 2.91-1.266 4.032-2.06.818.628 1.71 1.158 2.657 1.592l.56-1.624c-.725-.334-1.36-.692-1.905-1.063.284-.28.59-.634.906-1.156.46-.717.777-1.572 1-2.5h1.658V6h-4.063c-.283-.552-.596-1.083-.97-1.53l-.186-.25zM7.72 7.47h3.186c-.32 1.075-.83 1.937-1.53 2.624-.713-.705-1.26-1.568-1.657-2.625zm6.31 5.31l-.467 1.658c.292-.514.577-1.075.812-1.532l-.344-.125z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.png
new file mode 100644 (file)
index 0000000..8cd9282
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/language-rtl.svg
new file mode 100644 (file)
index 0000000..9b1ac39
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="translation">
+        <path id="english" d="M7.53 9L4 19h2.063l.72-2.406h3.624l.72 2.406h2.062L9.653 9h-2.12zm1.064 1.53L9.938 15H7.25z"/>
+        <path id="chinese" d="M14.594 4.22c-.43.29-.88.616-1.25.874l.187.312c.14.194.276.393.408.594h-3.844v1.47h1.594c.43 1.41 1.11 2.624 2.03 3.624-.662.437-1.413.82-2.25 1.187l.563 1.564c1.11-.48 2.056-1.022 2.908-1.625 1.187.91 2.514 1.63 3.968 2.124l.282.094c.292-.514.577-1.075.812-1.532l-.375-.125c-1.38-.49-2.49-1.052-3.375-1.655.284-.28.59-.634.906-1.156.46-.717.776-1.572 1-2.5h1.657V6H15.75c-.283-.552-.596-1.083-.97-1.53l-.186-.25zm-1.25 3.25h3.187c-.318 1.075-.828 1.937-1.53 2.624-.712-.705-1.26-1.568-1.656-2.625zM9.97 12.874L9.624 13c.196.3.406.594.625.875l-.28-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language.png b/resources/lib/oojs-ui/themes/apex/images/icons/language.png
deleted file mode 100644 (file)
index b4f0875..0000000
Binary files a/resources/lib/oojs-ui/themes/apex/images/icons/language.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/language.svg b/resources/lib/oojs-ui/themes/apex/images/icons/language.svg
deleted file mode 100644 (file)
index 956aba1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="language">
-        <path id="japanese" d="M17.533 9.81l.27-.59 1.042.407-.18.363c.66.27 1.1.468 1.312.59.33.21.618.513.86.904.21.393.316.846.316 1.358 0 .786-.302 1.48-.905 2.083-.604.634-1.66 1.057-3.17 1.268-.12-.36-.257-.68-.407-.95.97-.15 1.65-.333 2.04-.545.455-.21.786-.48 1-.813.21-.303.313-.663.313-1.087 0-.482-.135-.905-.406-1.27-.33-.33-.8-.588-1.402-.77-.332.635-.648 1.118-.95 1.45-.242.332-.694.906-1.358 1.72.09.394.18.71.272.952l-1.043.362-.09-.498c-.424.36-.802.617-1.134.77-.36.15-.664.226-.905.226-.303 0-.574-.136-.814-.407-.243-.3-.362-.68-.362-1.132 0-.6.137-1.143.408-1.63.24-.45.603-.89 1.086-1.31.273-.24.726-.53 1.36-.86 0-.27.03-.8.09-1.584-.514.03-.92.045-1.222.045-.393 0-.71-.015-.95-.045l-.047-1.04c.726.09 1.495.134 2.31.134 0-.15.076-.74.228-1.767l1.177.184c-.15.542-.256 1.04-.316 1.493.24-.03.542-.077.905-.138.36-.06.573-.09.634-.09s.647-.15 1.765-.453l.045 1.04c-.966.242-2.144.44-3.53.59-.063.662-.093 1.085-.093 1.265.664-.15 1.285-.225 1.858-.225zm-2.672 3.893c-.06-.48-.132-1.252-.223-2.31-.573.424-1.04.86-1.403 1.313-.302.423-.45.875-.45 1.358 0 .24.043.438.135.588.09.092.194.137.315.137.364 0 .908-.365 1.63-1.09zm.775-2.763c0 .483.03 1.088.09 1.81.604-.904 1.057-1.598 1.36-2.08-.575.06-1.06.15-1.45.27z"/>
-        <path id="english" d="M9.497 15.98h1.85L8.265 7.033h-1.85l-3.08 8.95h1.85L5.74 14h3.21l.547 1.98zm-3.49-3.376L7.34 8.822l1.343 3.782H6.008z"/>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.png
deleted file mode 100644 (file)
index 18ceb35..0000000
Binary files a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/redirect-ltr.svg
deleted file mode 100644 (file)
index be25d43..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="create_redirect">
-        <g>
-            <path d="M17.7 2.4c-.3-.3-.7-.4-1.2-.4H4.4v16.2c0 .5.1.8.4 1.1s.7.7 1.2.7h10.2c-.6-.2-1.2-.5-1.9-1-.4-.3-.8-.6-1.2-1l-.5-.6H6.4V16h5.4s-.4-1.5-.4-2h-5v-1h9v1c.4.1 1.1.1 1.5.1.4 0 .7 0 1.1-.1V3.5c.1-.5-.1-.9-.3-1.1zM12.5 4h3v4.5h-3V4zM6.4 4h4v1.6h-4V4zm0 3h4v1.5h-4V7zm0 3h9v1.5h-9V10zm12.7 3.1l4.9 3.8-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.png
deleted file mode 100644 (file)
index dc9b0e6..0000000
Binary files a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/redirect-rtl.svg
deleted file mode 100644 (file)
index a41d178..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="create_redirect">
-        <g id="g3264">
-            <path d="M6.3 2.4c.3-.3.7-.4 1.2-.4h12.1v16.2c0 .5-.1.8-.4 1.1-.3.3-.7.7-1.2.7H7.8c.6-.2 1.2-.5 1.9-1 .4-.3.8-.6 1.2-1l.5-.6h6.2V16h-5.4s.4-1.5.4-2h5v-1h-9v1c-.4.1-1.1.1-1.5.1-.4 0-.7 0-1.1-.1V3.5c-.1-.5.1-.9.3-1.1zM11.5 4h-3v4.5h3V4zm6.1 0h-4v1.6h4V4zm0 3h-4v1.5h4V7zm0 3h-9v1.5h9V10z" id="path3266"/>
-            <path d="M4.9 13.1L0 16.9l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3" id="path3268"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.png
deleted file mode 100644 (file)
index 1025461..0000000
Binary files a/resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/translation-ltr.svg
deleted file mode 100644 (file)
index 8954a21..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M11.1 13.1C9.3 11 8.4 8.8 8.1 8h4.7l.7-2H8V3H6v3H1v2h5c-.2.9-1.3 4.8-5.1 7.6l1.2 1.6c2.7-2 4.3-4.5 5.1-6.4.7 1.3 1.7 3 3.2 4.5l.7-2.2zm1.4 6.9l1.3-4h5.3l1.3 4h2.2L18 6h-3l-4.7 14h2.2zm4-12l2 6h-4l2-6z"/>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.png
deleted file mode 100644 (file)
index 38066d6..0000000
Binary files a/resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/translation-rtl.svg
deleted file mode 100644 (file)
index 44ba971..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M12.4 13.1c1.8-2.1 2.7-4.3 3-5.1h-4.7L10 6h5.5V3h2v3h5v2h-5c.2.9 1.3 4.8 5.1 7.6l-1.2 1.6c-2.7-2-4.3-4.5-5.1-6.4-.7 1.3-1.7 3-3.2 4.5l-.7-2.2zM11 20l-1.3-4H4.4l-1.3 4H.9L5.5 6h3l4.7 14H11zM7 8l-2 6h4L7 8z" id="path704"/>
-</svg>
index 18c8dd5..73af2f1 100644 (file)
                        "ltr": "images/icons/articleSearch-ltr.svg",
                        "rtl": "images/icons/articleSearch-rtl.svg"
                } },
+               "articleRedirect": { "file": {
+                       "ltr": "images/icons/articleRedirect-ltr.svg",
+                       "rtl": "images/icons/articleRedirect-rtl.svg"
+               } },
                "book": { "file": {
                        "ltr": "images/icons/book-ltr.svg",
                        "rtl": "images/icons/book-rtl.svg"
index d981728..27e3b0e 100644 (file)
                        "ltr": "images/icons/find-ltr.svg",
                        "rtl": "images/icons/find-rtl.svg"
                } },
-               "insert": { "file": "images/icons/add.svg" },
+               "language": { "file": {
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
+               } },
                "layout": { "file": {
                        "ltr": "images/icons/layout-ltr.svg",
                        "rtl": "images/icons/layout-rtl.svg"
                        "ltr": "images/icons/newline-ltr.svg",
                        "rtl": "images/icons/newline-rtl.svg"
                } },
-               "redirect": { "file": {
-                       "ltr": "images/icons/redirect-ltr.svg",
-                       "rtl": "images/icons/redirect-rtl.svg"
-               } },
                "noWikiText": { "file": {
                        "ltr": "images/icons/noWikiText-ltr.svg",
                        "rtl": "images/icons/noWikiText-rtl.svg"
@@ -53,8 +52,8 @@
                        "rtl": "images/icons/quotesAdd-rtl.svg"
                } },
                "redirect": { "file": {
-                       "ltr": "images/icons/redirect-ltr.svg",
-                       "rtl": "images/icons/redirect-rtl.svg"
+                       "ltr": "images/icons/articleRedirect-ltr.svg",
+                       "rtl": "images/icons/articleRedirect-rtl.svg"
                } },
                "searchCaseSensitive": { "file": "images/icons/case-sensitive.svg" },
                "searchRegularExpression": { "file": "images/icons/regular-expression.svg" },
@@ -77,8 +76,8 @@
                        "rtl": "images/icons/templateAdd-rtl.svg"
                } },
                "translation": { "file": {
-                       "ltr": "images/icons/translation-ltr.svg",
-                       "rtl": "images/icons/translation-rtl.svg"
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
                } },
                "wikiText": { "file": "images/icons/wikiText.svg" }
        }
index 48af33a..e070154 100644 (file)
                                "en": "images/icons/underline-u.svg"
                        }
                } },
-               "textLanguage": { "file": "images/icons/language.svg" },
+               "textLanguage": { "file": {
+                       "ltr": "images/icons/language-ltr.svg",
+                       "rtl": "images/icons/language-rtl.svg"
+               } },
                "textDirLTR": { "file": "images/icons/text-dir-lefttoright.svg" },
                "textDirRTL": { "file": "images/icons/text-dir-righttoleft.svg" },
                "textStyle": { "file": "images/icons/text-style.svg" }
index fc058cd..0c3b4eb 100644 (file)
                        "rtl": "images/icons/move-rtl.svg"
                } },
                "notice": { "file": "images/icons/notice.svg" },
-               "picture": { "file": {
-                       "ltr": "images/icons/image-rtl.svg",
-                       "rtl": "images/icons/image-ltr.svg"
-               } },
                "previous": { "file": {
                        "ltr": "images/icons/move-rtl.svg",
                        "rtl": "images/icons/move-ltr.svg"
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.png
new file mode 100644 (file)
index 0000000..9261197
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr-invert.svg
new file mode 100644 (file)
index 0000000..5317700
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
+    <g id="article-redirect">
+        <path id="arrow" d="M18.1 14.2L23 18l-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
+        <path id="page" d="M5 3v13c0 1.7 1.3 3 3 3h3.375c-.157-.205-.3-.43-.438-.656-.42-.688-.77-1.483-.843-2.344H7v-1h3.125l.125-1H7v-1h3.375l.03-.188.283.188H16v1h-3.906l.22.156c.523.375 1.065.64 1.592.844H16v.406c.208-.013.418-.07.625-.094.068-1.294.125-3.874.125-3.874l1.25.968V3H5zm2 2h4v1H7V5zm5 0h4v5h-4V5zM7 7h4v1H7V7zm0 2h4v1H7V9zm0 2h9v1H7v-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.png
new file mode 100644 (file)
index 0000000..8b0920f
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-ltr.svg
new file mode 100644 (file)
index 0000000..028c64c
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="article-redirect">
+        <path id="arrow" d="M18.1 14.2L23 18l-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
+        <path id="page" d="M5 3v13c0 1.7 1.3 3 3 3h3.375c-.157-.205-.3-.43-.438-.656-.42-.688-.77-1.483-.843-2.344H7v-1h3.125l.125-1H7v-1h3.375l.03-.188.283.188H16v1h-3.906l.22.156c.523.375 1.065.64 1.592.844H16v.406c.208-.013.418-.07.625-.094.068-1.294.125-3.874.125-3.874l1.25.968V3H5zm2 2h4v1H7V5zm5 0h4v5h-4V5zM7 7h4v1H7V7zm0 2h4v1H7V9zm0 2h9v1H7v-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.png
new file mode 100644 (file)
index 0000000..8dd4d77
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl-invert.svg
new file mode 100644 (file)
index 0000000..a0f43ab
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
+    <g id="article-redirect">
+        <path id="arrow" d="M5.9 14.2L1 18l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3"/>
+        <path id="page" d="M19 3v13c0 1.7-1.3 3-3 3h-3.375c.157-.205.3-.43.438-.656.42-.688.77-1.483.843-2.344H17v-1h-3.125l-.125-1H17v-1h-3.375l-.03-.188-.283.188H8v1h3.906l-.22.156c-.523.375-1.065.64-1.592.844H8v.406c-.208-.013-.418-.07-.625-.094-.068-1.294-.125-3.874-.125-3.874L6 12.405V3zm-2 2h-4v1h4zm-5 0H8v5h4zm5 2h-4v1h4zm0 2h-4v1h4zm0 2H8v1h9z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.png
new file mode 100644 (file)
index 0000000..709673f
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/articleRedirect-rtl.svg
new file mode 100644 (file)
index 0000000..6a9c683
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="article-redirect">
+        <path id="arrow" d="M5.9 14.2L1 18l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3"/>
+        <path id="page" d="M19 3v13c0 1.7-1.3 3-3 3h-3.375c.157-.205.3-.43.438-.656.42-.688.77-1.483.843-2.344H17v-1h-3.125l-.125-1H17v-1h-3.375l-.03-.188-.283.188H8v1h3.906l-.22.156c-.523.375-1.065.64-1.592.844H8v.406c-.208-.013-.418-.07-.625-.094-.068-1.294-.125-3.874-.125-3.874L6 12.405V3zm-2 2h-4v1h4zm-5 0H8v5h4zm5 2h-4v1h4zm0 2h-4v1h4zm0 2H8v1h9z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.png
deleted file mode 100644 (file)
index ad816df..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-invert.svg
deleted file mode 100644 (file)
index abc618e..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
-    <g id="language">
-        <path id="japanese" d="M17.533 9.81l.27-.59 1.042.407-.18.363c.66.27 1.1.468 1.312.59.33.21.618.513.86.904.21.393.316.846.316 1.358 0 .786-.302 1.48-.905 2.083-.604.634-1.66 1.057-3.17 1.268-.12-.36-.257-.68-.407-.95.97-.15 1.65-.333 2.04-.545.455-.21.786-.48 1-.813.21-.303.313-.663.313-1.087 0-.482-.135-.905-.406-1.27-.33-.33-.8-.588-1.402-.77-.332.635-.648 1.118-.95 1.45-.242.332-.694.906-1.358 1.72.09.394.18.71.272.952l-1.043.362-.09-.498c-.424.36-.802.617-1.134.77-.36.15-.664.226-.905.226-.303 0-.574-.136-.814-.407-.243-.3-.362-.68-.362-1.132 0-.6.137-1.143.408-1.63.24-.45.603-.89 1.086-1.31.273-.24.726-.53 1.36-.86 0-.27.03-.8.09-1.584-.514.03-.92.045-1.222.045-.393 0-.71-.015-.95-.045l-.047-1.04c.726.09 1.495.134 2.31.134 0-.15.076-.74.228-1.767l1.177.184c-.15.542-.256 1.04-.316 1.493.24-.03.542-.077.905-.138.36-.06.573-.09.634-.09s.647-.15 1.765-.453l.045 1.04c-.966.242-2.144.44-3.53.59-.063.662-.093 1.085-.093 1.265.664-.15 1.285-.225 1.858-.225zm-2.672 3.893c-.06-.48-.132-1.252-.223-2.31-.573.424-1.04.86-1.403 1.313-.302.423-.45.875-.45 1.358 0 .24.043.438.135.588.09.092.194.137.315.137.364 0 .908-.365 1.63-1.09zm.775-2.763c0 .483.03 1.088.09 1.81.604-.904 1.057-1.598 1.36-2.08-.575.06-1.06.15-1.45.27z"/>
-        <path id="english" d="M9.497 15.98h1.85L8.265 7.033h-1.85l-3.08 8.95h1.85L5.74 14h3.21l.547 1.98zm-3.49-3.376L7.34 8.822l1.343 3.782H6.008z"/>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.png
new file mode 100644 (file)
index 0000000..36aaf52
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr-invert.svg
new file mode 100644 (file)
index 0000000..c67db52
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
+    <g id="translation">
+        <path id="english" d="M14.34 9l-3.53 10h2.064l.72-2.406h3.624l.72 2.406H20L16.465 9h-2.12zm1.065 1.53L16.75 15h-2.69z"/>
+        <path id="chinese" d="M8.97 4.22c-.43.29-.88.616-1.25.874l.186.312c.14.194.275.393.407.594H4.47v1.47h1.593c.43 1.41 1.11 2.624 2.03 3.624-1.008.664-2.192 1.248-3.624 1.75L4 13c.317.487.714.976 1.03 1.375l.25-.094c1.593-.59 2.91-1.266 4.032-2.06.818.628 1.71 1.158 2.657 1.592l.56-1.624c-.725-.334-1.36-.692-1.905-1.063.284-.28.59-.634.906-1.156.46-.717.777-1.572 1-2.5h1.658V6h-4.063c-.283-.552-.596-1.083-.97-1.53l-.186-.25zM7.72 7.47h3.186c-.32 1.075-.83 1.937-1.53 2.624-.713-.705-1.26-1.568-1.657-2.625zm6.31 5.31l-.467 1.658c.292-.514.577-1.075.812-1.532l-.344-.125z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.png
new file mode 100644 (file)
index 0000000..ef61b8b
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-ltr.svg
new file mode 100644 (file)
index 0000000..4bf074d
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="translation">
+        <path id="english" d="M14.34 9l-3.53 10h2.064l.72-2.406h3.624l.72 2.406H20L16.465 9h-2.12zm1.065 1.53L16.75 15h-2.69z"/>
+        <path id="chinese" d="M8.97 4.22c-.43.29-.88.616-1.25.874l.186.312c.14.194.275.393.407.594H4.47v1.47h1.593c.43 1.41 1.11 2.624 2.03 3.624-1.008.664-2.192 1.248-3.624 1.75L4 13c.317.487.714.976 1.03 1.375l.25-.094c1.593-.59 2.91-1.266 4.032-2.06.818.628 1.71 1.158 2.657 1.592l.56-1.624c-.725-.334-1.36-.692-1.905-1.063.284-.28.59-.634.906-1.156.46-.717.777-1.572 1-2.5h1.658V6h-4.063c-.283-.552-.596-1.083-.97-1.53l-.186-.25zM7.72 7.47h3.186c-.32 1.075-.83 1.937-1.53 2.624-.713-.705-1.26-1.568-1.657-2.625zm6.31 5.31l-.467 1.658c.292-.514.577-1.075.812-1.532l-.344-.125z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.png
new file mode 100644 (file)
index 0000000..aad12ac
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl-invert.svg
new file mode 100644 (file)
index 0000000..204f565
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
+    <g id="translation">
+        <path id="english" d="M7.53 9L4 19h2.063l.72-2.406h3.624l.72 2.406h2.062L9.653 9h-2.12zm1.064 1.53L9.938 15H7.25z"/>
+        <path id="chinese" d="M14.594 4.22c-.43.29-.88.616-1.25.874l.187.312c.14.194.276.393.408.594h-3.844v1.47h1.594c.43 1.41 1.11 2.624 2.03 3.624-.662.437-1.413.82-2.25 1.187l.563 1.564c1.11-.48 2.056-1.022 2.908-1.625 1.187.91 2.514 1.63 3.968 2.124l.282.094c.292-.514.577-1.075.812-1.532l-.375-.125c-1.38-.49-2.49-1.052-3.375-1.655.284-.28.59-.634.906-1.156.46-.717.776-1.572 1-2.5h1.657V6H15.75c-.283-.552-.596-1.083-.97-1.53l-.186-.25zm-1.25 3.25h3.187c-.318 1.075-.828 1.937-1.53 2.624-.712-.705-1.26-1.568-1.656-2.625zM9.97 12.874L9.624 13c.196.3.406.594.625.875l-.28-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.png
new file mode 100644 (file)
index 0000000..8cd9282
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language-rtl.svg
new file mode 100644 (file)
index 0000000..9b1ac39
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <g id="translation">
+        <path id="english" d="M7.53 9L4 19h2.063l.72-2.406h3.624l.72 2.406h2.062L9.653 9h-2.12zm1.064 1.53L9.938 15H7.25z"/>
+        <path id="chinese" d="M14.594 4.22c-.43.29-.88.616-1.25.874l.187.312c.14.194.276.393.408.594h-3.844v1.47h1.594c.43 1.41 1.11 2.624 2.03 3.624-.662.437-1.413.82-2.25 1.187l.563 1.564c1.11-.48 2.056-1.022 2.908-1.625 1.187.91 2.514 1.63 3.968 2.124l.282.094c.292-.514.577-1.075.812-1.532l-.375-.125c-1.38-.49-2.49-1.052-3.375-1.655.284-.28.59-.634.906-1.156.46-.717.776-1.572 1-2.5h1.657V6H15.75c-.283-.552-.596-1.083-.97-1.53l-.186-.25zm-1.25 3.25h3.187c-.318 1.075-.828 1.937-1.53 2.624-.712-.705-1.26-1.568-1.656-2.625zM9.97 12.874L9.624 13c.196.3.406.594.625.875l-.28-1z"/>
+    </g>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language.png
deleted file mode 100644 (file)
index b4f0875..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/language.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/language.svg
deleted file mode 100644 (file)
index 956aba1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="language">
-        <path id="japanese" d="M17.533 9.81l.27-.59 1.042.407-.18.363c.66.27 1.1.468 1.312.59.33.21.618.513.86.904.21.393.316.846.316 1.358 0 .786-.302 1.48-.905 2.083-.604.634-1.66 1.057-3.17 1.268-.12-.36-.257-.68-.407-.95.97-.15 1.65-.333 2.04-.545.455-.21.786-.48 1-.813.21-.303.313-.663.313-1.087 0-.482-.135-.905-.406-1.27-.33-.33-.8-.588-1.402-.77-.332.635-.648 1.118-.95 1.45-.242.332-.694.906-1.358 1.72.09.394.18.71.272.952l-1.043.362-.09-.498c-.424.36-.802.617-1.134.77-.36.15-.664.226-.905.226-.303 0-.574-.136-.814-.407-.243-.3-.362-.68-.362-1.132 0-.6.137-1.143.408-1.63.24-.45.603-.89 1.086-1.31.273-.24.726-.53 1.36-.86 0-.27.03-.8.09-1.584-.514.03-.92.045-1.222.045-.393 0-.71-.015-.95-.045l-.047-1.04c.726.09 1.495.134 2.31.134 0-.15.076-.74.228-1.767l1.177.184c-.15.542-.256 1.04-.316 1.493.24-.03.542-.077.905-.138.36-.06.573-.09.634-.09s.647-.15 1.765-.453l.045 1.04c-.966.242-2.144.44-3.53.59-.063.662-.093 1.085-.093 1.265.664-.15 1.285-.225 1.858-.225zm-2.672 3.893c-.06-.48-.132-1.252-.223-2.31-.573.424-1.04.86-1.403 1.313-.302.423-.45.875-.45 1.358 0 .24.043.438.135.588.09.092.194.137.315.137.364 0 .908-.365 1.63-1.09zm.775-2.763c0 .483.03 1.088.09 1.81.604-.904 1.057-1.598 1.36-2.08-.575.06-1.06.15-1.45.27z"/>
-        <path id="english" d="M9.497 15.98h1.85L8.265 7.033h-1.85l-3.08 8.95h1.85L5.74 14h3.21l.547 1.98zm-3.49-3.376L7.34 8.822l1.343 3.782H6.008z"/>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.png
deleted file mode 100644 (file)
index 066e17f..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr-invert.svg
deleted file mode 100644 (file)
index 0a4e04e..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
-    <g id="create_redirect">
-        <g>
-            <path d="M17.7 2.4c-.3-.3-.7-.4-1.2-.4H4.4v16.2c0 .5.1.8.4 1.1s.7.7 1.2.7h10.2c-.6-.2-1.2-.5-1.9-1-.4-.3-.8-.6-1.2-1l-.5-.6H6.4V16h5.4s-.4-1.5-.4-2h-5v-1h9v1c.4.1 1.1.1 1.5.1.4 0 .7 0 1.1-.1V3.5c.1-.5-.1-.9-.3-1.1zM12.5 4h3v4.5h-3V4zM6.4 4h4v1.6h-4V4zm0 3h4v1.5h-4V7zm0 3h9v1.5h-9V10zm12.7 3.1l4.9 3.8-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.png
deleted file mode 100644 (file)
index 18ceb35..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-ltr.svg
deleted file mode 100644 (file)
index be25d43..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="create_redirect">
-        <g>
-            <path d="M17.7 2.4c-.3-.3-.7-.4-1.2-.4H4.4v16.2c0 .5.1.8.4 1.1s.7.7 1.2.7h10.2c-.6-.2-1.2-.5-1.9-1-.4-.3-.8-.6-1.2-1l-.5-.6H6.4V16h5.4s-.4-1.5-.4-2h-5v-1h9v1c.4.1 1.1.1 1.5.1.4 0 .7 0 1.1-.1V3.5c.1-.5-.1-.9-.3-1.1zM12.5 4h3v4.5h-3V4zM6.4 4h4v1.6h-4V4zm0 3h4v1.5h-4V7zm0 3h9v1.5h-9V10zm12.7 3.1l4.9 3.8-4.9 4.8v-2.2c-1.7 0-2.9-.2-4.3-1.2-1.2-.8-2.5-2.6-2.3-4.1 1.4 1 2.9 1.5 4.4 1.5.7 0 1.4-.1 2.1-.3l.1-2.3"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.png
deleted file mode 100644 (file)
index cdcd158..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl-invert.svg
deleted file mode 100644 (file)
index 431c5b8..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
-    <g id="create_redirect">
-        <g id="g3264">
-            <path d="M6.3 2.4c.3-.3.7-.4 1.2-.4h12.1v16.2c0 .5-.1.8-.4 1.1-.3.3-.7.7-1.2.7H7.8c.6-.2 1.2-.5 1.9-1 .4-.3.8-.6 1.2-1l.5-.6h6.2V16h-5.4s.4-1.5.4-2h5v-1h-9v1c-.4.1-1.1.1-1.5.1-.4 0-.7 0-1.1-.1V3.5c-.1-.5.1-.9.3-1.1zM11.5 4h-3v4.5h3V4zm6.1 0h-4v1.6h4V4zm0 3h-4v1.5h4V7zm0 3h-9v1.5h9V10z" id="path3266"/>
-            <path d="M4.9 13.1L0 16.9l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3" id="path3268"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.png
deleted file mode 100644 (file)
index dc9b0e6..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/redirect-rtl.svg
deleted file mode 100644 (file)
index a41d178..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="create_redirect">
-        <g id="g3264">
-            <path d="M6.3 2.4c.3-.3.7-.4 1.2-.4h12.1v16.2c0 .5-.1.8-.4 1.1-.3.3-.7.7-1.2.7H7.8c.6-.2 1.2-.5 1.9-1 .4-.3.8-.6 1.2-1l.5-.6h6.2V16h-5.4s.4-1.5.4-2h5v-1h-9v1c-.4.1-1.1.1-1.5.1-.4 0-.7 0-1.1-.1V3.5c-.1-.5.1-.9.3-1.1zM11.5 4h-3v4.5h3V4zm6.1 0h-4v1.6h4V4zm0 3h-4v1.5h4V7zm0 3h-9v1.5h9V10z" id="path3266"/>
-            <path d="M4.9 13.1L0 16.9l4.9 4.8v-2.2c1.7 0 2.9-.2 4.3-1.2 1.2-.8 2.5-2.6 2.3-4.1-1.4 1-2.9 1.5-4.4 1.5-.7 0-1.4-.1-2.1-.3l-.1-2.3" id="path3268"/>
-        </g>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.png
deleted file mode 100644 (file)
index fde9f52..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr-invert.svg
deleted file mode 100644 (file)
index 30915b7..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
-    <path d="M11.1 13.1C9.3 11 8.4 8.8 8.1 8h4.7l.7-2H8V3H6v3H1v2h5c-.2.9-1.3 4.8-5.1 7.6l1.2 1.6c2.7-2 4.3-4.5 5.1-6.4.7 1.3 1.7 3 3.2 4.5l.7-2.2zm1.4 6.9l1.3-4h5.3l1.3 4h2.2L18 6h-3l-4.7 14h2.2zm4-12l2 6h-4l2-6z"/>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.png
deleted file mode 100644 (file)
index 1025461..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-ltr.svg
deleted file mode 100644 (file)
index 8954a21..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M11.1 13.1C9.3 11 8.4 8.8 8.1 8h4.7l.7-2H8V3H6v3H1v2h5c-.2.9-1.3 4.8-5.1 7.6l1.2 1.6c2.7-2 4.3-4.5 5.1-6.4.7 1.3 1.7 3 3.2 4.5l.7-2.2zm1.4 6.9l1.3-4h5.3l1.3 4h2.2L18 6h-3l-4.7 14h2.2zm4-12l2 6h-4l2-6z"/>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.png
deleted file mode 100644 (file)
index 09ab631..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl-invert.svg
deleted file mode 100644 (file)
index de634a8..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #FFFFFF }</style>
-    <path d="M12.4 13.1c1.8-2.1 2.7-4.3 3-5.1h-4.7L10 6h5.5V3h2v3h5v2h-5c.2.9 1.3 4.8 5.1 7.6l-1.2 1.6c-2.7-2-4.3-4.5-5.1-6.4-.7 1.3-1.7 3-3.2 4.5l-.7-2.2zM11 20l-1.3-4H4.4l-1.3 4H.9L5.5 6h3l4.7 14H11zM7 8l-2 6h4L7 8z" id="path704"/>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.png
deleted file mode 100644 (file)
index 38066d6..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/translation-rtl.svg
deleted file mode 100644 (file)
index 44ba971..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M12.4 13.1c1.8-2.1 2.7-4.3 3-5.1h-4.7L10 6h5.5V3h2v3h5v2h-5c.2.9 1.3 4.8 5.1 7.6l-1.2 1.6c-2.7-2-4.3-4.5-5.1-6.4-.7 1.3-1.7 3-3.2 4.5l-.7-2.2zM11 20l-1.3-4H4.4l-1.3 4H.9L5.5 6h3l4.7 14H11zM7 8l-2 6h4L7 8z" id="path704"/>
-</svg>
index 1c48515..f4ddfb2 100644 (file)
@@ -49,6 +49,7 @@ $wgAutoloadClasses += array(
 
        # tests/phpunit/includes
        'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php",
+       'TestLogger' => "$testDir/phpunit/includes/TestLogger.php",
 
        # tests/phpunit/includes/api
        'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php",
@@ -94,6 +95,10 @@ $wgAutoloadClasses += array(
        'ResourceLoaderImageModuleTestable' =>
                "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php",
 
+       # tests/phpunit/includes/session
+       'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php",
+       'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php",
+
        # tests/phpunit/includes/specials
        'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
 
@@ -118,6 +123,9 @@ $wgAutoloadClasses += array(
        'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
        'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
        'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php",
+       'MediaWiki\\Session\\DummySessionBackend'
+               => "$testDir/phpunit/mocks/session/DummySessionBackend.php",
+       'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
 
        # tests/parser
        'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php",
index fc2f743..861e3bd 100644 (file)
@@ -221,6 +221,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        }
 
        protected function tearDown() {
+               global $wgRequest;
+
                $status = ob_get_status();
                if ( isset( $status['name'] ) &&
                        $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
@@ -252,6 +254,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $this->mwGlobals = array();
                RequestContext::resetMain();
                MediaHandler::resetCache();
+               if ( session_id() !== '' ) {
+                       session_write_close();
+                       session_id( '' );
+               }
+               $wgRequest = new FauxRequest();
+               MediaWiki\Session\SessionManager::resetCache();
 
                $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
 
@@ -509,6 +517,13 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                                false,
                                $user
                        );
+
+                       // doEditContent() probably started the session via
+                       // User::loadFromSession(). Close it now.
+                       if ( session_id() !== '' ) {
+                               session_write_close();
+                               session_id( '' );
+                       }
                }
        }
 
diff --git a/tests/phpunit/includes/PagePropsTest.php b/tests/phpunit/includes/PagePropsTest.php
new file mode 100644 (file)
index 0000000..9a1f597
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * @group Database
+ *     ^--- tell jenkins this test needs the database
+ *
+ * @group medium
+ *     ^--- tell phpunit that these test cases may take longer than 2 seconds.
+ */
+class TestPageProps extends MediaWikiLangTestCase {
+
+       /**
+        * @var Title $title1
+        */
+       private $title1;
+
+       /**
+        * @var Title $title2
+        */
+       private $title2;
+
+       /**
+        * @var array $the_properties
+        */
+       private $the_properties;
+
+       protected function setUp() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               parent::setUp();
+
+               $wgExtraNamespaces[12312] = 'Dummy';
+               $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[12312] = 'DUMMY';
+               $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting';
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
+               if ( !$this->the_properties ) {
+                       $this->the_properties = array(
+                               "property1" => "value1",
+                               "property2" => "value2",
+                               "property3" => "value3",
+                               "property4" => "value4"
+                       );
+               }
+
+               if ( !$this->title1 ) {
+                       $page = $this->createPage(
+                               'PagePropsTest_page_1',
+                               "just a dummy page",
+                               CONTENT_MODEL_WIKITEXT
+                       );
+                       $this->title1 = $page->getTitle();
+                       $page1ID = $this->title1->getArticleID();
+                       $this->setProperties( $page1ID, $this->the_properties );
+               }
+
+               if ( !$this->title2 ) {
+                       $page = $this->createPage(
+                               'PagePropsTest_page_2',
+                               "just a dummy page",
+                               CONTENT_MODEL_WIKITEXT
+                       );
+                       $this->title2 = $page->getTitle();
+                       $page2ID = $this->title2->getArticleID();
+                       $this->setProperties( $page2ID, $this->the_properties );
+               }
+       }
+
+       protected function tearDown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               parent::tearDown();
+
+               unset( $wgExtraNamespaces[12312] );
+               unset( $wgExtraNamespaces[12313] );
+
+               unset( $wgNamespaceContentModels[12312] );
+               unset( $wgContentHandlers['DUMMY'] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
+       /**
+        * Test getting a single property from a single page. The property was
+        * set in setUp().
+        */
+       public function testGetSingleProperty() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $result = $pageProps->getProperty( $this->title1, "property1" );
+               $this->assertArrayHasKey( $page1ID, $result, "Found property" );
+               $this->assertEquals( $result[$page1ID], "value1", "Get property" );
+       }
+
+       /**
+        * Test getting a single property from multiple pages. The property was
+        * set in setUp().
+        */
+       public function testGetSinglePropertyMultiplePages() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $page2ID = $this->title2->getArticleID();
+               $titles = array(
+                       $this->title1,
+                       $this->title2
+               );
+               $result = $pageProps->getProperty( $titles, "property1" );
+               $this->assertArrayHasKey( $page1ID, $result, "Found page 1 property" );
+               $this->assertArrayHasKey( $page2ID, $result, "Found page 2 property" );
+               $this->assertEquals( $result[$page1ID], "value1", "Get property page 1" );
+               $this->assertEquals( $result[$page2ID], "value1", "Get property page 2" );
+       }
+
+       /**
+        * Test getting all properties from a single page. The properties were
+        * set in setUp(). The properties retrieved from the page may include
+        * additional properties not set in the test case that are added by
+        * other extensions. Therefore, rather than checking to see if the
+        * properties that were set in the test case exactly match the
+        * retrieved properties, we need to check to see if they are a
+        * subset of the retrieved properties. Since this version of PHPUnit
+        * does not yet include assertArraySubset(), we needed to code the
+        * equivalent functionality.
+        */
+       public function testGetAllProperties() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $result = $pageProps->getProperties( $this->title1 );
+               $this->assertArrayHasKey( $page1ID, $result, "Found properties" );
+               $properties = $result[$page1ID];
+               $patched = array_replace_recursive( $properties, $this->the_properties );
+               $this->assertEquals( $patched, $properties, "Get all properties" );
+       }
+
+       /**
+        * Test getting all properties from multiple pages. The properties were
+        * set in setUp(). See getAllProperties() above for more information.
+        */
+       public function testGetAllPropertiesMultiplePages() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $page2ID = $this->title2->getArticleID();
+               $titles = array(
+                       $this->title1,
+                       $this->title2
+               );
+               $result = $pageProps->getProperties( $titles );
+               $this->assertArrayHasKey( $page1ID, $result, "Found page 1 properties" );
+               $this->assertArrayHasKey( $page2ID, $result, "Found page 2 properties" );
+               $properties1 = $result[$page1ID];
+               $patched = array_replace_recursive( $properties1, $this->the_properties );
+               $this->assertEquals( $patched, $properties1, "Get all properties page 1" );
+               $properties2 = $result[$page2ID];
+               $patched = array_replace_recursive( $properties2, $this->the_properties );
+               $this->assertEquals( $patched, $properties2, "Get all properties page 2" );
+       }
+
+       /**
+        * Test caching when retrieving single properties by getting a property,
+        * saving a new value for the property, then getting the property
+        * again. The cached value for the property rather than the new value
+        * of the property should be returned.
+        */
+       public function testSingleCache() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $value1 = $pageProps->getProperty( $this->title1, "property1" );
+               $this->setProperty( $page1ID, "property1", "another value" );
+               $value2 = $pageProps->getProperty( $this->title1, "property1" );
+               $this->assertEquals( $value1, $value2, "Single cache" );
+       }
+
+       /**
+        * Test caching when retrieving all properties by getting all
+        * properties, saving a new value for a property, then getting all
+        * properties again. The cached value for the properties rather than the
+        * new value of the properties should be returned.
+        */
+       public function testMultiCache() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $properties1 = $pageProps->getProperties( $this->title1 );
+               $this->setProperty( $page1ID, "property1", "another value" );
+               $properties2 = $pageProps->getProperties( $this->title1 );
+               $this->assertEquals( $properties1, $properties2, "Multi Cache" );
+       }
+
+       /**
+        * Test that getting all properties clears the single properties
+        * that have been cached by getting a property, saving a new value for
+        * the property, getting all properties (which clears the cached single
+        * properties), then getting the property again. The new value for the
+        * property rather than the cached value of the property should be
+        * returned.
+        */
+       public function testClearCache() {
+               $pageProps = PageProps::getInstance();
+               $page1ID = $this->title1->getArticleID();
+               $pageProps->getProperty( $this->title1, "property1" );
+               $new_value = "another value";
+               $this->setProperty( $page1ID, "property1", $new_value );
+               $pageProps->getProperties( $this->title1 );
+               $result = $pageProps->getProperty( $this->title1, "property1" );
+               $this->assertArrayHasKey( $page1ID, $result, "Found property" );
+               $this->assertEquals( $result[$page1ID], "another value", "Clear cache" );
+       }
+
+       protected function createPage( $page, $text, $model = null ) {
+               if ( is_string( $page ) ) {
+                       if ( !preg_match( '/:/', $page ) &&
+                               ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
+                       ) {
+                               $ns = $this->getDefaultWikitextNS();
+                               $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page;
+                       }
+
+                       $page = Title::newFromText( $page );
+               }
+
+               if ( $page instanceof Title ) {
+                       $page = new WikiPage( $page );
+               }
+
+               if ( $page->exists() ) {
+                       $page->doDeleteArticle( "done" );
+               }
+
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing", EDIT_NEW );
+
+               return $page;
+       }
+
+       protected function setProperties( $pageID, $properties ) {
+
+               $rows = array();
+
+               foreach ( $properties as $propertyName => $propertyValue ) {
+
+                       $row = array(
+                               'pp_page' => $pageID,
+                               'pp_propname' => $propertyName,
+                               'pp_value' => $propertyValue
+                       );
+
+                       $rows[] = $row;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->replace(
+                       'page_props',
+                       array(
+                               array(
+                                       'pp_page',
+                                       'pp_propname'
+                               )
+                       ),
+                       $rows,
+                       __METHOD__
+               );
+       }
+
+       protected function setProperty( $pageID, $propertyName, $propertyValue ) {
+
+               $properties = array();
+               $properties[$propertyName] = $propertyValue;
+
+               $this->setProperties( $pageID, $properties );
+
+       }
+}
diff --git a/tests/phpunit/includes/TestLogger.php b/tests/phpunit/includes/TestLogger.php
new file mode 100644 (file)
index 0000000..7099c3a
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Testing logger
+ *
+ * Copyright (C) 2015 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Brad Jorsch <bjorsch@wikimedia.org>
+ */
+
+use Psr\Log\LogLevel;
+
+/**
+ * A logger that may be configured to either buffer logs or to print them to
+ * the output where PHPUnit will complain about them.
+ *
+ * @since 1.27
+ */
+class TestLogger extends \Psr\Log\AbstractLogger {
+       private $collect = false;
+       private $buffer = array();
+       private $filter = null;
+
+       /**
+        * @param bool $collect Whether to collect logs
+        * @param callable $filter Filter logs before collecting/printing. Signature is
+        *  string|null function ( string $message, string $level );
+        */
+       public function __construct( $collect = false, $filter = null ) {
+               $this->collect = $collect;
+               $this->filter = $filter;
+       }
+
+       /**
+        * Set the "collect" flag
+        * @param bool $collect
+        */
+       public function setCollect( $collect ) {
+               $this->collect = $collect;
+       }
+
+       /**
+        * Return the collected logs
+        * @return array Array of array( string $level, string $message )
+        */
+       public function getBuffer() {
+               return $this->buffer;
+       }
+
+       /**
+        * Clear the collected log buffer
+        */
+       public function clearBuffer() {
+               $this->buffer = array();
+       }
+
+       public function log( $level, $message, array $context = array() ) {
+               $message = trim( $message );
+
+               if ( $this->filter ) {
+                       $message = call_user_func( $this->filter, $message, $level );
+                       if ( $message === null ) {
+                               return;
+                       }
+               }
+
+               if ( $this->collect ) {
+                       $this->buffer[] = array( $level, $message );
+               } else {
+                       switch ( $level ) {
+                               case LogLevel::DEBUG:
+                               case LogLevel::INFO:
+                               case LogLevel::NOTICE:
+                                       trigger_error( "LOG[$level]: $message", E_USER_NOTICE );
+                                       break;
+
+                               case LogLevel::WARNING:
+                                       trigger_error( "LOG[$level]: $message", E_USER_WARNING );
+                                       break;
+
+                               case LogLevel::ERROR:
+                               case LogLevel::CRITICAL:
+                               case LogLevel::ALERT:
+                               case LogLevel::EMERGENCY:
+                                       trigger_error( "LOG[$level]: $message", E_USER_ERROR );
+                                       break;
+                       }
+               }
+       }
+}
index 7dfd14f..4085925 100644 (file)
@@ -13,9 +13,13 @@ class ApiLoginTest extends ApiTestCase {
         * Test result of attempted login with an empty username
         */
        public function testApiLoginNoName() {
+               $session = array(
+                       'wsLoginToken' => 'foobar'
+               );
                $data = $this->doApiRequest( array( 'action' => 'login',
                        'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
-               ) );
+                       'lgtoken' => 'foobar',
+               ), $session );
                $this->assertEquals( 'NoName', $data[0]['login']['result'] );
        }
 
@@ -179,4 +183,94 @@ class ApiLoginTest extends ApiTestCase {
                $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
        }
 
+       public function testBotPassword() {
+               global $wgServer, $wgSessionProviders;
+
+               if ( !isset( $wgServer ) ) {
+                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+               }
+
+               $this->setMwGlobals( array(
+                       'wgSessionProviders' => array_merge( $wgSessionProviders, array(
+                               array(
+                                       'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider',
+                                       'args' => array( array( 'priority' => 40 ) ),
+                               )
+                       ) ),
+                       'wgEnableBotPasswords' => true,
+                       'wgBotPasswordsDatabase' => false,
+                       'wgCentralIdLookupProvider' => 'local',
+                       'wgGrantPermissions' => array(
+                               'test' => array( 'read' => true ),
+                       ),
+               ) );
+
+               // Make sure our session provider is present
+               $manager = TestingAccessWrapper::newFromObject( MediaWiki\Session\SessionManager::singleton() );
+               if ( !isset( $manager->sessionProviders['MediaWiki\\Session\\BotPasswordSessionProvider'] ) ) {
+                       $tmp = $manager->sessionProviders;
+                       $manager->sessionProviders = null;
+                       $manager->sessionProviders = $tmp + $manager->getProviders();
+               }
+               $this->assertNotNull(
+                       MediaWiki\Session\SessionManager::singleton()->getProvider(
+                               'MediaWiki\\Session\\BotPasswordSessionProvider'
+                       ),
+                       'sanity check'
+               );
+
+               $user = self::$users['sysop'];
+               $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
+               $this->assertNotEquals( 0, $centralId, 'sanity check' );
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
+               $passwordFactory->setDefaultType( 'A' );
+               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->insert(
+                       'bot_passwords',
+                       array(
+                               'bp_user' => $centralId,
+                               'bp_app_id' => 'foo',
+                               'bp_password' => $pwhash->toString(),
+                               'bp_token' => '',
+                               'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
+                               'bp_grants' => '["test"]',
+                       ),
+                       __METHOD__
+               );
+
+               $lgName = $user->username . BotPassword::getSeparator() . 'foo';
+
+               $ret = $this->doApiRequest( array(
+                       'action' => 'login',
+                       'lgname' => $lgName,
+                       'lgpassword' => 'foobaz',
+               ) );
+
+               $result = $ret[0];
+               $this->assertNotInternalType( 'bool', $result );
+               $this->assertNotInternalType( 'null', $result['login'] );
+
+               $a = $result['login']['result'];
+               $this->assertEquals( 'NeedToken', $a );
+               $token = $result['login']['token'];
+
+               $ret = $this->doApiRequest( array(
+                       'action' => 'login',
+                       'lgtoken' => $token,
+                       'lgname' => $lgName,
+                       'lgpassword' => 'foobaz',
+               ), $ret[2] );
+
+               $result = $ret[0];
+               $this->assertNotInternalType( 'bool', $result );
+               $a = $result['login']['result'];
+
+               $this->assertEquals( 'Success', $a );
+       }
+
 }
index aef4815..f02f7df 100644 (file)
@@ -97,7 +97,8 @@ class ApiMainTest extends ApiTestCase {
                $request->setHeaders( $headers );
                $request->response()->statusHeader( 200 ); // Why doesn't it default?
 
-               $api = new ApiMain( $request );
+               $context = $this->apiContext->newTestContext( $request, null );
+               $api = new ApiMain( $context );
                $priv = TestingAccessWrapper::newFromObject( $api );
                $priv->mInternalMode = false;
 
index 01113a6..25ffcb7 100644 (file)
@@ -47,11 +47,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
 
        protected function tearDown() {
                // Avoid leaking session over tests
-               if ( session_id() != '' ) {
-                       global $wgUser;
-                       $wgUser->logout();
-                       session_destroy();
-               }
+               MediaWiki\Session\SessionManager::getGlobalSession()->clear();
 
                parent::tearDown();
        }
index 87f794c..b6ae641 100644 (file)
@@ -15,8 +15,6 @@ abstract class ApiTestCaseUpload extends ApiTestCase {
                        'wgEnableAPI' => true,
                ) );
 
-               wfSetupSession();
-
                $this->clearFakeUploads();
        }
 
index a9e5be2..25969e6 100644 (file)
@@ -37,6 +37,14 @@ class RequestContextTest extends MediaWikiTestCase {
         * @covers RequestContext::importScopedSession
         */
        public function testImportScopedSession() {
+               // Make sure session handling is started
+               if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
+                       MediaWiki\Session\PHPSessionHandler::install(
+                               MediaWiki\Session\SessionManager::singleton()
+                       );
+               }
+               $oldSessionId = session_id();
+
                $context = RequestContext::getMain();
 
                $oInfo = $context->exportSession();
@@ -76,7 +84,16 @@ class RequestContextTest extends MediaWikiTestCase {
                        $context->getRequest()->getAllHeaders(),
                        "Correct context headers."
                );
-               $this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
+               $this->assertEquals(
+                       $sinfo['sessionId'],
+                       MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
+                       "Correct context session ID."
+               );
+               if ( \MediaWiki\Session\PhpSessionHandler::isEnabled() ) {
+                       $this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
+               } else {
+                       $this->assertEquals( $oldSessionId, session_id(), "Unchanged PHP session ID." );
+               }
                $this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." );
                $this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." );
                $this->assertEquals(
diff --git a/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php b/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
new file mode 100644 (file)
index 0000000..a9f7e0e
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @covers JobQueueMemory
+ *
+ * @group JobQueue
+ *
+ * @licence GNU GPL v2+
+ * @author Thiemo Mättig
+ */
+class JobQueueMemoryTest extends PHPUnit_Framework_TestCase {
+
+       public function testGetAllQueuedJobs() {
+               $instance = JobQueueMemoryDouble::newInstance( array(
+                       'wiki' => null,
+                       'type' => null,
+               ) );
+               $actual = $instance->getAllQueuedJobs();
+               $this->assertEquals( new ArrayIterator(), $actual );
+       }
+
+}
+
+class JobQueueMemoryDouble extends JobQueueMemory {
+
+       public static function newInstance( array $params ) {
+               return new self( $params );
+       }
+
+}
index 002e86f..7c0dd2e 100644 (file)
@@ -370,24 +370,6 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $this->assertEquals( "some text", $text );
        }
 
-       /**
-        * @covers WikiPage::getRawText
-        */
-       public function testGetRawText() {
-               $this->hideDeprecated( "WikiPage::getRawText" );
-
-               $page = $this->newPage( "WikiPageTest_testGetRawText" );
-
-               $text = $page->getRawText();
-               $this->assertFalse( $text );
-
-               # -----------------
-               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
-
-               $text = $page->getRawText();
-               $this->assertEquals( "some text", $text );
-       }
-
        /**
         * @covers WikiPage::getContentModel
         */
diff --git a/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php b/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php
new file mode 100644 (file)
index 0000000..52872a4
--- /dev/null
@@ -0,0 +1,288 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\BotPasswordSessionProvider
+ */
+class BotPasswordSessionProviderTest extends MediaWikiTestCase {
+
+       private $config;
+
+       private function getProvider( $name = null, $prefix = null ) {
+               global $wgSessionProviders;
+
+               $params = array(
+                       'priority' => 40,
+                       'sessionCookieName' => $name,
+                       'sessionCookieOptions' => array(),
+               );
+               if ( $prefix !== null ) {
+                       $params['sessionCookieOptions']['prefix'] = $prefix;
+               }
+
+               if ( !$this->config ) {
+                       $this->config = new \HashConfig( array(
+                               'CookiePrefix' => 'wgCookiePrefix',
+                               'EnableBotPasswords' => true,
+                               'BotPasswordsDatabase' => false,
+                               'SessionProviders' => $wgSessionProviders + array(
+                                       'MediaWiki\\Session\\BotPasswordSessionProvider' => array(
+                                               'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider',
+                                               'args' => array( $params ),
+                                       )
+                               ),
+                       ) );
+               }
+               $manager = new SessionManager( array(
+                       'config' => new \MultiConfig( array( $this->config, \RequestContext::getMain()->getConfig() ) ),
+                       'logger' => new \Psr\Log\NullLogger,
+                       'store' => new TestBagOStuff,
+               ) );
+
+               return $manager->getProvider( 'MediaWiki\\Session\\BotPasswordSessionProvider' );
+       }
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgEnableBotPasswords' => true,
+                       'wgBotPasswordsDatabase' => false,
+                       'wgCentralIdLookupProvider' => 'local',
+                       'wgGrantPermissions' => array(
+                               'test' => array( 'read' => true ),
+                       ),
+               ) );
+       }
+
+       public function addDBData() {
+               $passwordFactory = new \PasswordFactory();
+               $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
+               $passwordFactory->setDefaultType( 'A' );
+               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+
+               $userId = \CentralIdLookup::factory( 'local' )->centralIdFromName( 'UTSysop' );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete(
+                       'bot_passwords',
+                       array( 'bp_user' => $userId, 'bp_app_id' => 'BotPasswordSessionProvider' ),
+                       __METHOD__
+               );
+               $dbw->insert(
+                       'bot_passwords',
+                       array(
+                               'bp_user' => $userId,
+                               'bp_app_id' => 'BotPasswordSessionProvider',
+                               'bp_password' => $pwhash->toString(),
+                               'bp_token' => 'token!',
+                               'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
+                               'bp_grants' => '["test"]',
+                       ),
+                       __METHOD__
+               );
+       }
+
+       public function testConstructor() {
+               try {
+                       $provider = new BotPasswordSessionProvider();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: priority must be specified',
+                               $ex->getMessage()
+                       );
+               }
+
+               try {
+                       $provider = new BotPasswordSessionProvider( array(
+                               'priority' => SessionInfo::MIN_PRIORITY - 1
+                       ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: Invalid priority',
+                               $ex->getMessage()
+                       );
+               }
+
+               try {
+                       $provider = new BotPasswordSessionProvider( array(
+                               'priority' => SessionInfo::MAX_PRIORITY + 1
+                       ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: Invalid priority',
+                               $ex->getMessage()
+                       );
+               }
+
+               $provider = new BotPasswordSessionProvider( array(
+                       'priority' => 40
+               ) );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertSame( 40, $priv->priority );
+               $this->assertSame( '_BPsession', $priv->sessionCookieName );
+               $this->assertSame( array(), $priv->sessionCookieOptions );
+
+               $provider = new BotPasswordSessionProvider( array(
+                       'priority' => 40,
+                       'sessionCookieName' => null,
+               ) );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertSame( '_BPsession', $priv->sessionCookieName );
+
+               $provider = new BotPasswordSessionProvider( array(
+                       'priority' => 40,
+                       'sessionCookieName' => 'Foo',
+                       'sessionCookieOptions' => array( 'Bar' ),
+               ) );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertSame( 'Foo', $priv->sessionCookieName );
+               $this->assertSame( array( 'Bar' ), $priv->sessionCookieOptions );
+       }
+
+       public function testBasics() {
+               $provider = $this->getProvider();
+
+               $this->assertTrue( $provider->persistsSessionID() );
+               $this->assertFalse( $provider->canChangeUser() );
+
+               $this->assertNull( $provider->newSessionInfo() );
+               $this->assertNull( $provider->newSessionInfo( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ) );
+       }
+
+       public function testProvideSessionInfo() {
+               $provider = $this->getProvider();
+               $request = new \FauxRequest;
+               $request->setCookie( '_BPsession', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'wgCookiePrefix' );
+
+               if ( !defined( 'MW_API' ) ) {
+                       $this->assertNull( $provider->provideSessionInfo( $request ) );
+                       define( 'MW_API', 1 );
+               }
+
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\SessionInfo', $info );
+               $this->assertSame( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $info->getId() );
+
+               $this->config->set( 'EnableBotPasswords', false );
+               $this->assertNull( $provider->provideSessionInfo( $request ) );
+               $this->config->set( 'EnableBotPasswords', true );
+
+               $this->assertNull( $provider->provideSessionInfo( new \FauxRequest ) );
+       }
+
+       public function testNewSessionInfoForRequest() {
+               $provider = $this->getProvider();
+               $user = \User::newFromName( 'UTSysop' );
+               $request = $this->getMock( 'FauxRequest', array( 'getIP' ) );
+               $request->expects( $this->any() )->method( 'getIP' )
+                       ->will( $this->returnValue( '127.0.0.1' ) );
+               $bp = \BotPassword::newFromUser( $user, 'BotPasswordSessionProvider' );
+
+               $session = $provider->newSessionForRequest( $user, $bp, $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+
+               $this->assertEquals( $session->getId(), $request->getSession()->getId() );
+               $this->assertEquals( $user->getName(), $session->getUser()->getName() );
+
+               $this->assertEquals( array(
+                       'centralId' => $bp->getUserCentralId(),
+                       'appId' => $bp->getAppId(),
+                       'token' => $bp->getToken(),
+                       'rights' => array( 'read' ),
+               ), $session->getProviderMetadata() );
+
+               $this->assertEquals( array( 'read' ), $session->getAllowedUserRights() );
+       }
+
+       public function testCheckSessionInfo() {
+               $logger = new \TestLogger( true, function ( $m ) {
+                       return preg_replace(
+                               '/^Session \[\d+\][a-zA-Z0-9_\\\\]+<(?:null|anon|[+-]:\d+:\w+)>\w+: /', 'Session X: ', $m
+                       );
+               } );
+               $provider = $this->getProvider();
+               $provider->setLogger( $logger );
+
+               $user = \User::newFromName( 'UTSysop' );
+               $request = $this->getMock( 'FauxRequest', array( 'getIP' ) );
+               $request->expects( $this->any() )->method( 'getIP' )
+                       ->will( $this->returnValue( '127.0.0.1' ) );
+               $bp = \BotPassword::newFromUser( $user, 'BotPasswordSessionProvider' );
+
+               $data = array(
+                       'provider' => $provider,
+                       'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+                       'userInfo' => UserInfo::newFromUser( $user, true ),
+                       'persisted' => false,
+                       'metadata' => array(
+                               'centralId' => $bp->getUserCentralId(),
+                               'appId' => $bp->getAppId(),
+                               'token' => $bp->getToken(),
+                       ),
+               );
+               $dataMD = $data['metadata'];
+
+               foreach ( array_keys( $data['metadata'] ) as $key ) {
+                       $data['metadata'] = $dataMD;
+                       unset( $data['metadata'][$key] );
+                       $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data );
+                       $metadata = $info->getProviderMetadata();
+
+                       $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
+                       $this->assertSame( array(
+                               array( LogLevel::INFO, "Session X: Missing metadata: $key" )
+                       ), $logger->getBuffer() );
+                       $logger->clearBuffer();
+               }
+
+               $data['metadata'] = $dataMD;
+               $data['metadata']['appId'] = 'Foobar';
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data );
+               $metadata = $info->getProviderMetadata();
+               $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, "Session X: No BotPassword for {$bp->getUserCentralId()} Foobar" ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $data['metadata'] = $dataMD;
+               $data['metadata']['token'] = 'Foobar';
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data );
+               $metadata = $info->getProviderMetadata();
+               $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'Session X: BotPassword token check failed' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $request2 = $this->getMock( 'FauxRequest', array( 'getIP' ) );
+               $request2->expects( $this->any() )->method( 'getIP' )
+                       ->will( $this->returnValue( '10.0.0.1' ) );
+               $data['metadata'] = $dataMD;
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data );
+               $metadata = $info->getProviderMetadata();
+               $this->assertFalse( $provider->refreshSessionInfo( $info, $request2, $metadata ) );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'Session X: Restrictions check failed' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data );
+               $metadata = $info->getProviderMetadata();
+               $this->assertTrue( $provider->refreshSessionInfo( $info, $request, $metadata ) );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $this->assertEquals( $dataMD + array( 'rights' => array( 'read' ) ), $metadata );
+       }
+}
diff --git a/tests/phpunit/includes/session/CookieSessionProviderTest.php b/tests/phpunit/includes/session/CookieSessionProviderTest.php
new file mode 100644 (file)
index 0000000..a73bf7c
--- /dev/null
@@ -0,0 +1,726 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\CookieSessionProvider
+ */
+class CookieSessionProviderTest extends MediaWikiTestCase {
+
+       private function getConfig() {
+               global $wgCookieExpiration;
+               return new \HashConfig( array(
+                       'CookiePrefix' => 'CookiePrefix',
+                       'CookiePath' => 'CookiePath',
+                       'CookieDomain' => 'CookieDomain',
+                       'CookieSecure' => true,
+                       'CookieHttpOnly' => true,
+                       'SessionName' => false,
+                       'ExtendedLoginCookies' => array( 'UserID', 'Token' ),
+                       'ExtendedLoginCookieExpiration' => $wgCookieExpiration * 2,
+               ) );
+       }
+
+       public function testConstructor() {
+               try {
+                       new CookieSessionProvider();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\CookieSessionProvider::__construct: priority must be specified',
+                               $ex->getMessage()
+                       );
+               }
+
+               try {
+                       new CookieSessionProvider( array( 'priority' => 'foo' ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       new CookieSessionProvider( array( 'priority' => SessionInfo::MIN_PRIORITY - 1 ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       new CookieSessionProvider( array( 'priority' => SessionInfo::MAX_PRIORITY + 1 ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
+                               $ex->getMessage()
+                       );
+               }
+
+               try {
+                       new CookieSessionProvider( array( 'priority' => 1, 'cookieOptions' => null ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\CookieSessionProvider::__construct: cookieOptions must be an array',
+                               $ex->getMessage()
+                       );
+               }
+
+               $config = $this->getConfig();
+               $p = \TestingAccessWrapper::newFromObject(
+                       new CookieSessionProvider( array( 'priority' => 1 ) )
+               );
+               $p->setLogger( new \TestLogger() );
+               $p->setConfig( $config );
+               $this->assertEquals( 1, $p->priority );
+               $this->assertEquals( array(
+                       'callUserSetCookiesHook' => false,
+                       'sessionName' => 'CookiePrefix_session',
+               ), $p->params );
+               $this->assertEquals( array(
+                       'prefix' => 'CookiePrefix',
+                       'path' => 'CookiePath',
+                       'domain' => 'CookieDomain',
+                       'secure' => true,
+                       'httpOnly' => true,
+               ), $p->cookieOptions );
+
+               $config->set( 'SessionName', 'SessionName' );
+               $p = \TestingAccessWrapper::newFromObject(
+                       new CookieSessionProvider( array( 'priority' => 3 ) )
+               );
+               $p->setLogger( new \TestLogger() );
+               $p->setConfig( $config );
+               $this->assertEquals( 3, $p->priority );
+               $this->assertEquals( array(
+                       'callUserSetCookiesHook' => false,
+                       'sessionName' => 'SessionName',
+               ), $p->params );
+               $this->assertEquals( array(
+                       'prefix' => 'CookiePrefix',
+                       'path' => 'CookiePath',
+                       'domain' => 'CookieDomain',
+                       'secure' => true,
+                       'httpOnly' => true,
+               ), $p->cookieOptions );
+
+               $p = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
+                       'priority' => 10,
+                       'callUserSetCookiesHook' => true,
+                       'cookieOptions' => array(
+                               'prefix' => 'XPrefix',
+                               'path' => 'XPath',
+                               'domain' => 'XDomain',
+                               'secure' => 'XSecure',
+                               'httpOnly' => 'XHttpOnly',
+                       ),
+                       'sessionName' => 'XSession',
+               ) ) );
+               $p->setLogger( new \TestLogger() );
+               $p->setConfig( $config );
+               $this->assertEquals( 10, $p->priority );
+               $this->assertEquals( array(
+                       'callUserSetCookiesHook' => true,
+                       'sessionName' => 'XSession',
+               ), $p->params );
+               $this->assertEquals( array(
+                       'prefix' => 'XPrefix',
+                       'path' => 'XPath',
+                       'domain' => 'XDomain',
+                       'secure' => 'XSecure',
+                       'httpOnly' => 'XHttpOnly',
+               ), $p->cookieOptions );
+       }
+
+       public function testBasics() {
+               $provider = new CookieSessionProvider( array( 'priority' => 10 ) );
+
+               $this->assertTrue( $provider->persistsSessionID() );
+               $this->assertTrue( $provider->canChangeUser() );
+
+               $msg = $provider->whyNoSession();
+               $this->assertInstanceOf( 'Message', $msg );
+               $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
+       }
+
+       public function testProvideSessionInfo() {
+               $params = array(
+                       'priority' => 20,
+                       'sessionName' => 'session',
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               );
+               $provider = new CookieSessionProvider( $params );
+               $provider->setLogger( new \TestLogger() );
+               $provider->setConfig( $this->getConfig() );
+               $provider->setManager( new SessionManager() );
+
+               $user = User::newFromName( 'UTSysop' );
+               $id = $user->getId();
+               $name = $user->getName();
+               $token = $user->getToken( true );
+
+               $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+               // No data
+               $request = new \FauxRequest();
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNull( $info );
+
+               // Session key only
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertSame( $sessionId, $info->getId() );
+               $this->assertNull( $info->getUserInfo() );
+               $this->assertFalse( $info->forceHTTPS() );
+
+               // User, no session key
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'xUserID' => $id,
+                       'xToken' => $token,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertNotSame( $sessionId, $info->getId() );
+               $this->assertNotNull( $info->getUserInfo() );
+               $this->assertSame( $id, $info->getUserInfo()->getId() );
+               $this->assertSame( $name, $info->getUserInfo()->getName() );
+               $this->assertFalse( $info->forceHTTPS() );
+
+               // User and session key
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+                       'xToken' => $token,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertSame( $sessionId, $info->getId() );
+               $this->assertNotNull( $info->getUserInfo() );
+               $this->assertSame( $id, $info->getUserInfo()->getId() );
+               $this->assertSame( $name, $info->getUserInfo()->getName() );
+               $this->assertFalse( $info->forceHTTPS() );
+
+               // User with bad token
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+                       'xToken' => 'BADTOKEN',
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNull( $info );
+
+               // User id with no token
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertSame( $sessionId, $info->getId() );
+               $this->assertNotNull( $info->getUserInfo() );
+               $this->assertFalse( $info->getUserInfo()->isVerified() );
+               $this->assertSame( $id, $info->getUserInfo()->getId() );
+               $this->assertSame( $name, $info->getUserInfo()->getName() );
+               $this->assertFalse( $info->forceHTTPS() );
+
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'xUserID' => $id,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNull( $info );
+
+               // User and session key, with forceHTTPS flag
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+                       'xToken' => $token,
+                       'forceHTTPS' => true,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertSame( $sessionId, $info->getId() );
+               $this->assertNotNull( $info->getUserInfo() );
+               $this->assertSame( $id, $info->getUserInfo()->getId() );
+               $this->assertSame( $name, $info->getUserInfo()->getName() );
+               $this->assertTrue( $info->forceHTTPS() );
+
+               // Invalid user id
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => '-1',
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNull( $info );
+
+               // User id with matching name
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+                       'xUserName' => $name,
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNotNull( $info );
+               $this->assertSame( $params['priority'], $info->getPriority() );
+               $this->assertSame( $sessionId, $info->getId() );
+               $this->assertNotNull( $info->getUserInfo() );
+               $this->assertFalse( $info->getUserInfo()->isVerified() );
+               $this->assertSame( $id, $info->getUserInfo()->getId() );
+               $this->assertSame( $name, $info->getUserInfo()->getName() );
+               $this->assertFalse( $info->forceHTTPS() );
+
+               // User id with wrong name
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'session' => $sessionId,
+                       'xUserID' => $id,
+                       'xUserName' => 'Wrong',
+               ), '' );
+               $info = $provider->provideSessionInfo( $request );
+               $this->assertNull( $info );
+       }
+
+       public function testGetVaryCookies() {
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'cookieOptions' => array( 'prefix' => 'MyCookiePrefix' ),
+               ) );
+               $this->assertArrayEquals( array(
+                       'MyCookiePrefixToken',
+                       'MyCookiePrefixLoggedOut',
+                       'MySessionName',
+                       'forceHTTPS',
+               ), $provider->getVaryCookies() );
+       }
+
+       public function testSuggestLoginUsername() {
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) );
+
+               $request = new \FauxRequest();
+               $this->assertEquals( null, $provider->suggestLoginUsername( $request ) );
+
+               $request->setCookies( array(
+                       'xUserName' => 'Example',
+               ), '' );
+               $this->assertEquals( 'Example', $provider->suggestLoginUsername( $request ) );
+       }
+
+       public function testPersistSession() {
+               $this->setMwGlobals( array( 'wgCookieExpiration' => 100 ) );
+
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'callUserSetCookiesHook' => false,
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) );
+               $config = $this->getConfig();
+               $provider->setLogger( new \TestLogger() );
+               $provider->setConfig( $config );
+               $provider->setManager( SessionManager::singleton() );
+
+               $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $store = new \HashBagOStuff();
+               $user = User::newFromName( 'UTSysop' );
+               $anon = new User;
+
+               $backend = new SessionBackend(
+                       new SessionId( $sessionId ),
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                               'provider' => $provider,
+                               'id' => $sessionId,
+                               'persisted' => true,
+                               'idIsSafe' => true,
+                       ) ),
+                       $store,
+                       new \Psr\Log\NullLogger(),
+                       10
+               );
+               \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
+
+               $mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
+               $mock->expects( $this->never() )->method( 'onUserSetCookies' );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
+
+               // Anonymous user
+               $backend->setUser( $anon );
+               $backend->setRememberUser( true );
+               $backend->setForceHTTPS( false );
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( array(), $backend->getData() );
+
+               // Logged-in user, no remember
+               $backend->setUser( $user );
+               $backend->setRememberUser( false );
+               $backend->setForceHTTPS( false );
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( array(), $backend->getData() );
+
+               // Logged-in user, remember
+               $backend->setUser( $user );
+               $backend->setRememberUser( true );
+               $backend->setForceHTTPS( true );
+               $request = new \FauxRequest();
+               $time = time();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( array(), $backend->getData() );
+       }
+
+       /**
+        * @dataProvider provideCookieData
+        * @param bool $secure
+        * @param bool $remember
+        */
+       public function testCookieData( $secure, $remember ) {
+               $this->setMwGlobals( array(
+                       'wgCookieExpiration' => 100,
+                       'wgSecureLogin' => false,
+               ) );
+
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'callUserSetCookiesHook' => false,
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) );
+               $config = $this->getConfig();
+               $config->set( 'CookieSecure', false );
+               $provider->setLogger( new \TestLogger() );
+               $provider->setConfig( $config );
+               $provider->setManager( SessionManager::singleton() );
+
+               $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $user = User::newFromName( 'UTSysop' );
+               $this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
+
+               $backend = new SessionBackend(
+                       new SessionId( $sessionId ),
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                               'provider' => $provider,
+                               'id' => $sessionId,
+                               'persisted' => true,
+                               'idIsSafe' => true,
+                       ) ),
+                       new \EmptyBagOStuff(),
+                       new \Psr\Log\NullLogger(),
+                       10
+               );
+               \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
+               $backend->setUser( $user );
+               $backend->setRememberUser( $remember );
+               $backend->setForceHTTPS( $secure );
+               $request = new \FauxRequest();
+               $time = time();
+               $provider->persistSession( $backend, $request );
+
+               $defaults = array(
+                       'expire' => (int)100,
+                       'path' => $config->get( 'CookiePath' ),
+                       'domain' => $config->get( 'CookieDomain' ),
+                       'secure' => $secure,
+                       'httpOnly' => $config->get( 'CookieHttpOnly' ),
+                       'raw' => false,
+               );
+               $extendedExpiry = $config->get( 'ExtendedLoginCookieExpiration' );
+               $extendedExpiry = (int)( $extendedExpiry === null ? 0 : $extendedExpiry );
+               $this->assertEquals( array( 'UserID', 'Token' ), $config->get( 'ExtendedLoginCookies' ),
+                       'sanity check' );
+               $expect = array(
+                       'MySessionName' => array(
+                               'value' => (string)$sessionId,
+                               'expire' => 0,
+                       ) + $defaults,
+                       'xUserID' => array(
+                               'value' => (string)$user->getId(),
+                               'expire' => $extendedExpiry,
+                       ) + $defaults,
+                       'xUserName' => array(
+                               'value' => $user->getName(),
+                       ) + $defaults,
+                       'xToken' => array(
+                               'value' => $remember ? $user->getToken() : '',
+                               'expire' => $remember ? $extendedExpiry : -31536000,
+                       ) + $defaults,
+                       'forceHTTPS' => !$secure ? null : array(
+                               'value' => 'true',
+                               'secure' => false,
+                               'expire' => $remember ? $defaults['expire'] : null,
+                       ) + $defaults,
+               );
+               foreach ( $expect as $key => $value ) {
+                       $actual = $request->response()->getCookieData( $key );
+                       if ( $actual && $actual['expire'] > 0 ) {
+                               // Round expiry so we don't randomly fail if the seconds ticked during the test.
+                               $actual['expire'] = round( $actual['expire'] - $time, -2 );
+                       }
+                       $this->assertEquals( $value, $actual, "Cookie $key" );
+               }
+       }
+
+       public static function provideCookieData() {
+               return array(
+                       array( false, false ),
+                       array( false, true ),
+                       array( true, false ),
+                       array( true, true ),
+               );
+       }
+
+       protected function getSentRequest() {
+               $sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
+               $sentResponse->expects( $this->any() )->method( 'headersSent' )
+                       ->will( $this->returnValue( true ) );
+               $sentResponse->expects( $this->never() )->method( 'setCookie' );
+               $sentResponse->expects( $this->never() )->method( 'header' );
+
+               $sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
+               $sentRequest->expects( $this->any() )->method( 'response' )
+                       ->will( $this->returnValue( $sentResponse ) );
+               return $sentRequest;
+       }
+
+       public function testPersistSessionWithHook() {
+               $that = $this;
+
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'callUserSetCookiesHook' => true,
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( $this->getConfig() );
+               $provider->setManager( SessionManager::singleton() );
+
+               $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $store = new \HashBagOStuff();
+               $user = User::newFromName( 'UTSysop' );
+               $anon = new User;
+
+               $backend = new SessionBackend(
+                       new SessionId( $sessionId ),
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                               'provider' => $provider,
+                               'id' => $sessionId,
+                               'persisted' => true,
+                               'idIsSafe' => true,
+                       ) ),
+                       $store,
+                       new \Psr\Log\NullLogger(),
+                       10
+               );
+               \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
+
+               // Anonymous user
+               $mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
+               $mock->expects( $this->never() )->method( 'onUserSetCookies' );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
+               $backend->setUser( $anon );
+               $backend->setRememberUser( true );
+               $backend->setForceHTTPS( false );
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( array(), $backend->getData() );
+
+               $provider->persistSession( $backend, $this->getSentRequest() );
+
+               // Logged-in user, no remember
+               $mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
+               $mock->expects( $this->once() )->method( 'onUserSetCookies' )
+                       ->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
+                               $that->assertSame( $user, $u );
+                               $that->assertEquals( array(
+                                       'wsUserID' => $user->getId(),
+                                       'wsUserName' => $user->getName(),
+                                       'wsToken' => $user->getToken(),
+                               ), $sessionData );
+                               $that->assertEquals( array(
+                                       'UserID' => $user->getId(),
+                                       'UserName' => $user->getName(),
+                                       'Token' => false,
+                               ), $cookies );
+
+                               $sessionData['foo'] = 'foo!';
+                               $cookies['bar'] = 'bar!';
+                               return true;
+                       } ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
+               $backend->setUser( $user );
+               $backend->setRememberUser( false );
+               $backend->setForceHTTPS( false );
+               $backend->setLoggedOutTimestamp( $loggedOut = time() );
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( 'bar!', $request->response()->getCookie( 'xbar' ) );
+               $this->assertSame( (string)$loggedOut, $request->response()->getCookie( 'xLoggedOut' ) );
+               $this->assertEquals( array(
+                       'wsUserID' => $user->getId(),
+                       'wsUserName' => $user->getName(),
+                       'wsToken' => $user->getToken(),
+                       'foo' => 'foo!',
+               ), $backend->getData() );
+
+               $provider->persistSession( $backend, $this->getSentRequest() );
+
+               // Logged-in user, remember
+               $mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
+               $mock->expects( $this->once() )->method( 'onUserSetCookies' )
+                       ->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
+                               $that->assertSame( $user, $u );
+                               $that->assertEquals( array(
+                                       'wsUserID' => $user->getId(),
+                                       'wsUserName' => $user->getName(),
+                                       'wsToken' => $user->getToken(),
+                               ), $sessionData );
+                               $that->assertEquals( array(
+                                       'UserID' => $user->getId(),
+                                       'UserName' => $user->getName(),
+                                       'Token' => $user->getToken(),
+                               ), $cookies );
+
+                               $sessionData['foo'] = 'foo 2!';
+                               $cookies['bar'] = 'bar 2!';
+                               return true;
+                       } ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
+               $backend->setUser( $user );
+               $backend->setRememberUser( true );
+               $backend->setForceHTTPS( true );
+               $backend->setLoggedOutTimestamp( 0 );
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
+               $this->assertSame( 'bar 2!', $request->response()->getCookie( 'xbar' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
+               $this->assertEquals( array(
+                       'wsUserID' => $user->getId(),
+                       'wsUserName' => $user->getName(),
+                       'wsToken' => $user->getToken(),
+                       'foo' => 'foo 2!',
+               ), $backend->getData() );
+
+               $provider->persistSession( $backend, $this->getSentRequest() );
+       }
+
+       public function testUnpersistSession() {
+               $provider = new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( $this->getConfig() );
+               $provider->setManager( SessionManager::singleton() );
+
+               $request = new \FauxRequest();
+               $provider->unpersistSession( $request );
+               $this->assertSame( '', $request->response()->getCookie( 'MySessionName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
+               $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
+               $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
+
+               $provider->unpersistSession( $this->getSentRequest() );
+       }
+
+       public function testSetLoggedOutCookie() {
+               $provider = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
+                       'priority' => 1,
+                       'sessionName' => 'MySessionName',
+                       'cookieOptions' => array( 'prefix' => 'x' ),
+               ) ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( $this->getConfig() );
+               $provider->setManager( SessionManager::singleton() );
+
+               $t1 = time();
+               $t2 = time() - 86400 * 2;
+
+               // Set it
+               $request = new \FauxRequest();
+               $provider->setLoggedOutCookie( $t1, $request );
+               $this->assertSame( (string)$t1, $request->response()->getCookie( 'xLoggedOut' ) );
+
+               // Too old
+               $request = new \FauxRequest();
+               $provider->setLoggedOutCookie( $t2, $request );
+               $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
+
+               // Don't reset if it's already set
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       'xLoggedOut' => $t1,
+               ), '' );
+               $provider->setLoggedOutCookie( $t1, $request );
+               $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
+       }
+
+       /**
+        * To be mocked for hooks, since PHPUnit can't otherwise mock methods that
+        * take references.
+        */
+       public function onUserSetCookies( $user, &$sessionData, &$cookies ) {
+       }
+
+}
diff --git a/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php b/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php
new file mode 100644 (file)
index 0000000..e06dfd5
--- /dev/null
@@ -0,0 +1,301 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\ImmutableSessionProviderWithCookie
+ */
+class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
+
+       private function getProvider( $name, $prefix = null ) {
+               $config = new \HashConfig();
+               $config->set( 'CookiePrefix', 'wgCookiePrefix' );
+
+               $params = array(
+                       'sessionCookieName' => $name,
+                       'sessionCookieOptions' => array(),
+               );
+               if ( $prefix !== null ) {
+                       $params['sessionCookieOptions']['prefix'] = $prefix;
+               }
+
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
+                       ->setConstructorArgs( array( $params ) )
+                       ->getMockForAbstractClass();
+               $provider->setLogger( new \TestLogger() );
+               $provider->setConfig( $config );
+               $provider->setManager( new SessionManager() );
+
+               return $provider;
+       }
+
+       public function testConstructor() {
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
+                       ->getMockForAbstractClass();
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertNull( $priv->sessionCookieName );
+               $this->assertSame( array(), $priv->sessionCookieOptions );
+
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
+                       ->setConstructorArgs( array( array(
+                               'sessionCookieName' => 'Foo',
+                               'sessionCookieOptions' => array( 'Bar' ),
+                       ) ) )
+                       ->getMockForAbstractClass();
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertSame( 'Foo', $priv->sessionCookieName );
+               $this->assertSame( array( 'Bar' ), $priv->sessionCookieOptions );
+
+               try {
+                       $provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
+                               ->setConstructorArgs( array( array(
+                                       'sessionCookieName' => false,
+                               ) ) )
+                               ->getMockForAbstractClass();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'sessionCookieName must be a string',
+                               $ex->getMessage()
+                       );
+               }
+
+               try {
+                       $provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
+                               ->setConstructorArgs( array( array(
+                                       'sessionCookieOptions' => 'x',
+                               ) ) )
+                               ->getMockForAbstractClass();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'sessionCookieOptions must be an array',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testBasics() {
+               $provider = $this->getProvider( null );
+               $this->assertFalse( $provider->persistsSessionID() );
+               $this->assertFalse( $provider->canChangeUser() );
+
+               $provider = $this->getProvider( 'Foo' );
+               $this->assertTrue( $provider->persistsSessionID() );
+               $this->assertFalse( $provider->canChangeUser() );
+
+               $msg = $provider->whyNoSession();
+               $this->assertInstanceOf( 'Message', $msg );
+               $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
+       }
+
+       public function testGetVaryCookies() {
+               $provider = $this->getProvider( null );
+               $this->assertSame( array(), $provider->getVaryCookies() );
+
+               $provider = $this->getProvider( 'Foo' );
+               $this->assertSame( array( 'wgCookiePrefixFoo' ), $provider->getVaryCookies() );
+
+               $provider = $this->getProvider( 'Foo', 'Bar' );
+               $this->assertSame( array( 'BarFoo' ), $provider->getVaryCookies() );
+
+               $provider = $this->getProvider( 'Foo', '' );
+               $this->assertSame( array( 'Foo' ), $provider->getVaryCookies() );
+       }
+
+       public function testGetSessionIdFromCookie() {
+               $this->setMwGlobals( 'wgCookiePrefix', 'wgCookiePrefix' );
+               $request = new \FauxRequest();
+               $request->setCookies( array(
+                       '' => 'empty---------------------------',
+                       'Foo' => 'foo-----------------------------',
+                       'wgCookiePrefixFoo' => 'wgfoo---------------------------',
+                       'BarFoo' => 'foobar--------------------------',
+                       'bad' => 'bad',
+               ), '' );
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( null ) );
+               try {
+                       $provider->getSessionIdFromCookie( $request );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\ImmutableSessionProviderWithCookie::getSessionIdFromCookie ' .
+                                       'may not be called when $this->sessionCookieName === null',
+                               $ex->getMessage()
+                       );
+               }
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo' ) );
+               $this->assertSame(
+                       'wgfoo---------------------------',
+                       $provider->getSessionIdFromCookie( $request )
+               );
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', 'Bar' ) );
+               $this->assertSame(
+                       'foobar--------------------------',
+                       $provider->getSessionIdFromCookie( $request )
+               );
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', '' ) );
+               $this->assertSame(
+                       'foo-----------------------------',
+                       $provider->getSessionIdFromCookie( $request )
+               );
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'bad', '' ) );
+               $this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
+
+               $provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'none', '' ) );
+               $this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
+       }
+
+       protected function getSentRequest() {
+               $sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
+               $sentResponse->expects( $this->any() )->method( 'headersSent' )
+                       ->will( $this->returnValue( true ) );
+               $sentResponse->expects( $this->never() )->method( 'setCookie' );
+               $sentResponse->expects( $this->never() )->method( 'header' );
+
+               $sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
+               $sentRequest->expects( $this->any() )->method( 'response' )
+                       ->will( $this->returnValue( $sentResponse ) );
+               return $sentRequest;
+       }
+
+       /**
+        * @dataProvider providePersistSession
+        * @param bool $secure
+        * @param bool $remember
+        */
+       public function testPersistSession( $secure, $remember ) {
+               $this->setMwGlobals( array(
+                       'wgCookieExpiration' => 100,
+                       'wgSecureLogin' => false,
+               ) );
+
+               $provider = $this->getProvider( 'session' );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $priv->sessionCookieOptions = array(
+                       'prefix' => 'x',
+                       'path' => 'CookiePath',
+                       'domain' => 'CookieDomain',
+                       'secure' => false,
+                       'httpOnly' => true,
+               );
+
+               $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $user = User::newFromName( 'UTSysop' );
+               $this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
+
+               $backend = new SessionBackend(
+                       new SessionId( $sessionId ),
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                               'provider' => $provider,
+                               'id' => $sessionId,
+                               'persisted' => true,
+                               'userInfo' => UserInfo::newFromUser( $user, true ),
+                               'idIsSafe' => true,
+                       ) ),
+                       new \EmptyBagOStuff(),
+                       new \Psr\Log\NullLogger(),
+                       10
+               );
+               \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
+               $backend->setRememberUser( $remember );
+               $backend->setForceHTTPS( $secure );
+
+               // No cookie
+               $priv->sessionCookieName = null;
+               $request = new \FauxRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( array(), $request->response()->getCookies() );
+
+               // Cookie
+               $priv->sessionCookieName = 'session';
+               $request = new \FauxRequest();
+               $time = time();
+               $provider->persistSession( $backend, $request );
+
+               $cookie = $request->response()->getCookieData( 'xsession' );
+               $this->assertInternalType( 'array', $cookie );
+               if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
+                       // Round expiry so we don't randomly fail if the seconds ticked during the test.
+                       $cookie['expire'] = round( $cookie['expire'] - $time, -2 );
+               }
+               $this->assertEquals( array(
+                       'value' => $sessionId,
+                       'expire' => null,
+                       'path' => 'CookiePath',
+                       'domain' => 'CookieDomain',
+                       'secure' => $secure,
+                       'httpOnly' => true,
+                       'raw' => false,
+               ), $cookie );
+
+               $cookie = $request->response()->getCookieData( 'forceHTTPS' );
+               if ( $secure ) {
+                       $this->assertInternalType( 'array', $cookie );
+                       if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
+                               // Round expiry so we don't randomly fail if the seconds ticked during the test.
+                               $cookie['expire'] = round( $cookie['expire'] - $time, -2 );
+                       }
+                       $this->assertEquals( array(
+                               'value' => 'true',
+                               'expire' => $remember ? 100 : null,
+                               'path' => 'CookiePath',
+                               'domain' => 'CookieDomain',
+                               'secure' => false,
+                               'httpOnly' => true,
+                               'raw' => false,
+                       ), $cookie );
+               } else {
+                       $this->assertNull( $cookie );
+               }
+
+               // Headers sent
+               $request = $this->getSentRequest();
+               $provider->persistSession( $backend, $request );
+               $this->assertSame( array(), $request->response()->getCookies() );
+       }
+
+       public static function providePersistSession() {
+               return array(
+                       array( false, false ),
+                       array( false, true ),
+                       array( true, false ),
+                       array( true, true ),
+               );
+       }
+
+       public function testUnpersistSession() {
+               $provider = $this->getProvider( 'session', '' );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+
+               // No cookie
+               $priv->sessionCookieName = null;
+               $request = new \FauxRequest();
+               $provider->unpersistSession( $request );
+               $this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
+
+               // Cookie
+               $priv->sessionCookieName = 'session';
+               $request = new \FauxRequest();
+               $provider->unpersistSession( $request );
+               $this->assertSame( '', $request->response()->getCookie( 'session', '' ) );
+
+               // Headers sent
+               $request = $this->getSentRequest();
+               $provider->unpersistSession( $request );
+               $this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/PHPSessionHandlerTest.php b/tests/phpunit/includes/session/PHPSessionHandlerTest.php
new file mode 100644 (file)
index 0000000..c18b821
--- /dev/null
@@ -0,0 +1,353 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiTestCase;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\PHPSessionHandler
+ */
+class PHPSessionHandlerTest extends MediaWikiTestCase {
+
+       private function getResetter( &$rProp = null ) {
+               $reset = array();
+
+               // Ignore "headers already sent" warnings during this test
+               set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
+                       if ( preg_match( '/headers already sent/', $errstr ) ) {
+                               return true;
+                       }
+                       return false;
+               } );
+               $reset[] = new \ScopedCallback( 'restore_error_handler' );
+
+               $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+               $rProp->setAccessible( true );
+               if ( $rProp->getValue() ) {
+                       $old = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+                       $oldManager = $old->manager;
+                       $oldStore = $old->store;
+                       $oldLogger = $old->logger;
+                       $reset[] = new \ScopedCallback(
+                               array( 'MediaWiki\\Session\\PHPSessionHandler', 'install' ),
+                               array( $oldManager, $oldStore, $oldLogger )
+                       );
+               }
+
+               return $reset;
+       }
+
+       public function testEnableFlags() {
+               $handler = \TestingAccessWrapper::newFromObject(
+                       $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
+                               ->setMethods( null )
+                               ->disableOriginalConstructor()
+                               ->getMock()
+               );
+
+               $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+               $rProp->setAccessible( true );
+               $reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $rProp->getValue() ) );
+               $rProp->setValue( $handler );
+
+               $handler->setEnableFlags( 'enable' );
+               $this->assertTrue( $handler->enable );
+               $this->assertFalse( $handler->warn );
+               $this->assertTrue( PHPSessionHandler::isEnabled() );
+
+               $handler->setEnableFlags( 'warn' );
+               $this->assertTrue( $handler->enable );
+               $this->assertTrue( $handler->warn );
+               $this->assertTrue( PHPSessionHandler::isEnabled() );
+
+               $handler->setEnableFlags( 'disable' );
+               $this->assertFalse( $handler->enable );
+               $this->assertFalse( PHPSessionHandler::isEnabled() );
+
+               $rProp->setValue( null );
+               $this->assertFalse( PHPSessionHandler::isEnabled() );
+       }
+
+       public function testInstall() {
+               $reset = $this->getResetter( $rProp );
+               $rProp->setValue( null );
+
+               session_write_close();
+               ini_set( 'session.use_cookies', 1 );
+               ini_set( 'session.use_trans_sid', 1 );
+
+               $store = new \HashBagOStuff();
+               $logger = new \TestLogger();
+               $manager = new SessionManager( array(
+                       'store' => $store,
+                       'logger' => $logger,
+               ) );
+
+               $this->assertFalse( PHPSessionHandler::isInstalled() );
+               PHPSessionHandler::install( $manager );
+               $this->assertTrue( PHPSessionHandler::isInstalled() );
+
+               $this->assertFalse( wfIniGetBool( 'session.use_cookies' ) );
+               $this->assertFalse( wfIniGetBool( 'session.use_trans_sid' ) );
+
+               $this->assertNotNull( $rProp->getValue() );
+               $priv = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+               $this->assertSame( $manager, $priv->manager );
+               $this->assertSame( $store, $priv->store );
+               $this->assertSame( $logger, $priv->logger );
+       }
+
+       /**
+        * @dataProvider provideHandlers
+        * @param string $handler php serialize_handler to use
+        */
+       public function testSessionHandling( $handler ) {
+               $this->hideDeprecated( '$_SESSION' );
+               $reset[] = $this->getResetter( $rProp );
+
+               $this->setMwGlobals( array(
+                       'wgSessionProviders' => array( array( 'class' => 'DummySessionProvider' ) ),
+                       'wgObjectCacheSessionExpiry' => 2,
+               ) );
+
+               $store = new \HashBagOStuff();
+               $logger = new \TestLogger( true, function ( $m ) {
+                       return preg_match( '/^SessionBackend a{32} /', $m ) ? null : $m;
+               } );
+               $manager = new SessionManager( array(
+                       'store' => $store,
+                       'logger' => $logger,
+               ) );
+               PHPSessionHandler::install( $manager );
+               $wrap = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+               $reset[] = new \ScopedCallback(
+                       array( $wrap, 'setEnableFlags' ),
+                       array( $wrap->enable ? $wrap->warn ? 'warn' : 'enable' : 'disable' )
+               );
+               $wrap->setEnableFlags( 'warn' );
+
+               \MediaWiki\suppressWarnings();
+               ini_set( 'session.serialize_handler', $handler );
+               \MediaWiki\restoreWarnings();
+               if ( ini_get( 'session.serialize_handler' ) !== $handler ) {
+                       $this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" );
+               }
+
+               // Session IDs for testing
+               $sessionA = str_repeat( 'a', 32 );
+               $sessionB = str_repeat( 'b', 32 );
+               $sessionC = str_repeat( 'c', 32 );
+
+               // Set up garbage data in the session
+               $_SESSION['AuthenticationSessionTest'] = 'bogus';
+
+               session_id( $sessionA );
+               session_start();
+               $this->assertSame( array(), $_SESSION );
+               $this->assertSame( $sessionA, session_id() );
+
+               // Set some data in the session so we can see if it works.
+               $rand = mt_rand();
+               $_SESSION['AuthenticationSessionTest'] = $rand;
+               $expect = array( 'AuthenticationSessionTest' => $rand );
+               session_write_close();
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Something wrote to $_SESSION!' ),
+               ), $logger->getBuffer() );
+
+               // Screw up $_SESSION so we can tell the difference between "this
+               // worked" and "this did nothing"
+               $_SESSION['AuthenticationSessionTest'] = 'bogus';
+
+               // Re-open the session and see that data was actually reloaded
+               session_start();
+               $this->assertSame( $expect, $_SESSION );
+
+               // Make sure session_reset() works too.
+               if ( function_exists( 'session_reset' ) ) {
+                       $_SESSION['AuthenticationSessionTest'] = 'bogus';
+                       session_reset();
+                       $this->assertSame( $expect, $_SESSION );
+               }
+
+               // Test expiry
+               session_write_close();
+               ini_set( 'session.gc_divisor', 1 );
+               ini_set( 'session.gc_probability', 1 );
+               sleep( 3 );
+               session_start();
+               $this->assertSame( array(), $_SESSION );
+
+               // Re-fill the session, then test that session_destroy() works.
+               $_SESSION['AuthenticationSessionTest'] = $rand;
+               session_write_close();
+               session_start();
+               $this->assertSame( $expect, $_SESSION );
+               session_destroy();
+               session_id( $sessionA );
+               session_start();
+               $this->assertSame( array(), $_SESSION );
+               session_write_close();
+
+               // Test that our session handler won't clone someone else's session
+               session_id( $sessionB );
+               session_start();
+               $this->assertSame( $sessionB, session_id() );
+               $_SESSION['id'] = 'B';
+               session_write_close();
+
+               session_id( $sessionC );
+               session_start();
+               $this->assertSame( array(), $_SESSION );
+               $_SESSION['id'] = 'C';
+               session_write_close();
+
+               session_id( $sessionB );
+               session_start();
+               $this->assertSame( array( 'id' => 'B' ), $_SESSION );
+               session_write_close();
+
+               session_id( $sessionC );
+               session_start();
+               $this->assertSame( array( 'id' => 'C' ), $_SESSION );
+               session_destroy();
+
+               session_id( $sessionB );
+               session_start();
+               $this->assertSame( array( 'id' => 'B' ), $_SESSION );
+
+               // Test merging between Session and $_SESSION
+               session_write_close();
+
+               $session = $manager->getEmptySession();
+               $session->set( 'Unchanged', 'setup' );
+               $session->set( 'Changed in $_SESSION', 'setup' );
+               $session->set( 'Changed in Session', 'setup' );
+               $session->set( 'Changed in both', 'setup' );
+               $session->set( 'Deleted in Session', 'setup' );
+               $session->set( 'Deleted in $_SESSION', 'setup' );
+               $session->set( 'Deleted in both', 'setup' );
+               $session->set( 'Deleted in Session, changed in $_SESSION', 'setup' );
+               $session->set( 'Deleted in $_SESSION, changed in Session', 'setup' );
+               $session->persist();
+               $session->save();
+
+               session_id( $session->getId() );
+               session_start();
+               $session->set( 'Added in Session', 'Session' );
+               $session->set( 'Added in both', 'Session' );
+               $session->set( 'Changed in Session', 'Session' );
+               $session->set( 'Changed in both', 'Session' );
+               $session->set( 'Deleted in $_SESSION, changed in Session', 'Session' );
+               $session->remove( 'Deleted in Session' );
+               $session->remove( 'Deleted in both' );
+               $session->remove( 'Deleted in Session, changed in $_SESSION' );
+               $session->save();
+               $_SESSION['Added in $_SESSION'] = '$_SESSION';
+               $_SESSION['Added in both'] = '$_SESSION';
+               $_SESSION['Changed in $_SESSION'] = '$_SESSION';
+               $_SESSION['Changed in both'] = '$_SESSION';
+               $_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION';
+               unset( $_SESSION['Deleted in $_SESSION'] );
+               unset( $_SESSION['Deleted in both'] );
+               unset( $_SESSION['Deleted in $_SESSION, changed in Session'] );
+               session_write_close();
+
+               $this->assertEquals( array(
+                       'Added in Session' => 'Session',
+                       'Added in $_SESSION' => '$_SESSION',
+                       'Added in both' => 'Session',
+                       'Unchanged' => 'setup',
+                       'Changed in Session' => 'Session',
+                       'Changed in $_SESSION' => '$_SESSION',
+                       'Changed in both' => 'Session',
+                       'Deleted in Session, changed in $_SESSION' => '$_SESSION',
+                       'Deleted in $_SESSION, changed in Session' => 'Session',
+               ), iterator_to_array( $session ) );
+
+               $session->clear();
+               $session->set( 42, 'forty-two' );
+               $session->set( 'forty-two', 42 );
+               $session->set( 'wrong', 43 );
+               $session->persist();
+               $session->save();
+
+               session_start();
+               $this->assertArrayHasKey( 'forty-two', $_SESSION );
+               $this->assertSame( 42, $_SESSION['forty-two'] );
+               $this->assertArrayHasKey( 'wrong', $_SESSION );
+               unset( $_SESSION['wrong'] );
+               session_write_close();
+
+               $this->assertEquals( array(
+                       42 => 'forty-two',
+                       'forty-two' => 42,
+               ), iterator_to_array( $session ) );
+       }
+
+       public static function provideHandlers() {
+               return array(
+                       array( 'php' ),
+                       array( 'php_binary' ),
+                       array( 'php_serialize' ),
+               );
+       }
+
+       /**
+        * @dataProvider provideDisabled
+        * @expectedException BadMethodCallException
+        * @expectedExceptionMessage Attempt to use PHP session management
+        */
+       public function testDisabled( $method, $args ) {
+               $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+               $rProp->setAccessible( true );
+               $handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
+                       ->setMethods( null )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               \TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
+               $oldValue = $rProp->getValue();
+               $rProp->setValue( $handler );
+               $reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $oldValue ) );
+
+               call_user_func_array( array( $handler, $method ), $args );
+       }
+
+       public static function provideDisabled() {
+               return array(
+                       array( 'open', array( '', '' ) ),
+                       array( 'read', array( '' ) ),
+                       array( 'write', array( '', '' ) ),
+                       array( 'destroy', array( '' ) ),
+               );
+       }
+
+       /**
+        * @dataProvider provideWrongInstance
+        * @expectedException UnexpectedValueException
+        * @expectedExceptionMessageRegExp /: Wrong instance called!$/
+        */
+       public function testWrongInstance( $method, $args ) {
+               $handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
+                       ->setMethods( null )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               \TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
+
+               call_user_func_array( array( $handler, $method ), $args );
+       }
+
+       public static function provideWrongInstance() {
+               return array(
+                       array( 'open', array( '', '' ) ),
+                       array( 'close', array() ),
+                       array( 'read', array( '' ) ),
+                       array( 'write', array( '', '' ) ),
+                       array( 'destroy', array( '' ) ),
+                       array( 'gc', array( 0 ) ),
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/SessionBackendTest.php b/tests/phpunit/includes/session/SessionBackendTest.php
new file mode 100644 (file)
index 0000000..d06706b
--- /dev/null
@@ -0,0 +1,757 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionBackend
+ */
+class SessionBackendTest extends MediaWikiTestCase {
+       const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+       protected $manager;
+       protected $config;
+       protected $provider;
+       protected $store;
+
+       protected $onSessionMetadataCalled = false;
+
+       /**
+        * Returns a non-persistent backend that thinks it has at least one session active
+        * @param User|null $user
+        */
+       protected function getBackend( User $user = null ) {
+               if ( !$this->config ) {
+                       $this->config = new \HashConfig();
+                       $this->manager = null;
+               }
+               if ( !$this->store ) {
+                       $this->store = new TestBagOStuff();
+                       $this->manager = null;
+               }
+
+               $logger = new \Psr\Log\NullLogger();
+               if ( !$this->manager ) {
+                       $this->manager = new SessionManager( array(
+                               'store' => $this->store,
+                               'logger' => $logger,
+                               'config' => $this->config,
+                       ) );
+               }
+
+               if ( !$this->provider ) {
+                       $this->provider = new \DummySessionProvider();
+               }
+               $this->provider->setLogger( $logger );
+               $this->provider->setConfig( $this->config );
+               $this->provider->setManager( $this->manager );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this->provider,
+                       'id' => self::SESSIONID,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( $info->getId() );
+
+               $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
+               $priv = \TestingAccessWrapper::newFromObject( $backend );
+               $priv->persist = false;
+               $priv->requests = array( 100 => new \FauxRequest() );
+               $priv->usePhpSessionHandling = false;
+
+               $manager = \TestingAccessWrapper::newFromObject( $this->manager );
+               $manager->allSessionBackends = array( $backend->getId() => $backend );
+               $manager->allSessionIds = array( $backend->getId() => $id );
+               $manager->sessionProviders = array( (string)$this->provider => $this->provider );
+
+               return $backend;
+       }
+
+       public function testConstructor() {
+               // Set variables
+               $this->getBackend();
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this->provider,
+                       'id' => self::SESSIONID,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( $info->getId() );
+               $logger = new \Psr\Log\NullLogger();
+               try {
+                       new SessionBackend( $id, $info, $this->store, $logger, 10 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               "Refusing to create session for unverified user {$info->getUserInfo()}",
+                               $ex->getMessage()
+                       );
+               }
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => self::SESSIONID,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( $info->getId() );
+               try {
+                       new SessionBackend( $id, $info, $this->store, $logger, 10 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
+               }
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this->provider,
+                       'id' => self::SESSIONID,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( '!' . $info->getId() );
+               try {
+                       new SessionBackend( $id, $info, $this->store, $logger, 10 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'SessionId and SessionInfo don\'t match',
+                               $ex->getMessage()
+                       );
+               }
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this->provider,
+                       'id' => self::SESSIONID,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( $info->getId() );
+               $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
+               $this->assertSame( self::SESSIONID, $backend->getId() );
+               $this->assertSame( $id, $backend->getSessionId() );
+               $this->assertSame( $this->provider, $backend->getProvider() );
+               $this->assertInstanceOf( 'User', $backend->getUser() );
+               $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
+               $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
+               $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
+               $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
+
+               $expire = time() + 100;
+               $this->store->setSessionMeta( self::SESSIONID, array( 'expires' => $expire ), 2 );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this->provider,
+                       'id' => self::SESSIONID,
+                       'persisted' => true,
+                       'forceHTTPS' => true,
+                       'metadata' => array( 'foo' ),
+                       'idIsSafe' => true,
+               ) );
+               $id = new SessionId( $info->getId() );
+               $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
+               $this->assertSame( self::SESSIONID, $backend->getId() );
+               $this->assertSame( $id, $backend->getSessionId() );
+               $this->assertSame( $this->provider, $backend->getProvider() );
+               $this->assertInstanceOf( 'User', $backend->getUser() );
+               $this->assertTrue( $backend->getUser()->isAnon() );
+               $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
+               $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
+               $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
+               $this->assertSame( $expire, \TestingAccessWrapper::newFromObject( $backend )->expires );
+               $this->assertSame( array( 'foo' ), $backend->getProviderMetadata() );
+       }
+
+       public function testSessionStuff() {
+               $backend = $this->getBackend();
+               $priv = \TestingAccessWrapper::newFromObject( $backend );
+               $priv->requests = array(); // Remove dummy session
+
+               $manager = \TestingAccessWrapper::newFromObject( $this->manager );
+
+               $request1 = new \FauxRequest();
+               $session1 = $backend->getSession( $request1 );
+               $request2 = new \FauxRequest();
+               $session2 = $backend->getSession( $request2 );
+
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session1 );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session2 );
+               $this->assertSame( 2, count( $priv->requests ) );
+
+               $index = \TestingAccessWrapper::newFromObject( $session1 )->index;
+
+               $this->assertSame( $request1, $backend->getRequest( $index ) );
+               $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
+               $request1->setCookie( 'UserName', 'Example' );
+               $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
+
+               $session1 = null;
+               $this->assertSame( 1, count( $priv->requests ) );
+               $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
+               $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
+               try {
+                       $backend->getRequest( $index );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session index', $ex->getMessage() );
+               }
+               try {
+                       $backend->suggestLoginUsername( $index );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session index', $ex->getMessage() );
+               }
+
+               $session2 = null;
+               $this->assertSame( 0, count( $priv->requests ) );
+               $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
+               $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
+       }
+
+       public function testResetId() {
+               $id = session_id();
+
+               $builder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'persistsSessionId', 'sessionIdWasReset' ) );
+
+               $this->provider = $builder->getMock();
+               $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( false ) );
+               $this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
+               $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
+               $manager = \TestingAccessWrapper::newFromObject( $this->manager );
+               $sessionId = $backend->getSessionId();
+               $backend->resetId();
+               $this->assertSame( self::SESSIONID, $backend->getId() );
+               $this->assertSame( $backend->getId(), $sessionId->getId() );
+               $this->assertSame( $id, session_id() );
+               $this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
+
+               $this->provider = $builder->getMock();
+               $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $backend = $this->getBackend();
+               $this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
+                       ->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
+               $manager = \TestingAccessWrapper::newFromObject( $this->manager );
+               $sessionId = $backend->getSessionId();
+               $backend->resetId();
+               $this->assertNotEquals( self::SESSIONID, $backend->getId() );
+               $this->assertSame( $backend->getId(), $sessionId->getId() );
+               $this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) );
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
+               $this->assertSame( $id, session_id() );
+               $this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
+               $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
+               $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
+       }
+
+       public function testPersist() {
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->once() )->method( 'persistSession' );
+               $backend = $this->getBackend();
+               $this->assertFalse( $backend->isPersistent(), 'sanity check' );
+               $backend->save(); // This one shouldn't call $provider->persistSession()
+
+               $backend->persist();
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+
+               $this->provider = null;
+               $backend = $this->getBackend();
+               $wrap = \TestingAccessWrapper::newFromObject( $backend );
+               $wrap->persist = true;
+               $wrap->expires = 0;
+               $backend->persist();
+               $this->assertNotEquals( 0, $wrap->expires );
+       }
+
+       public function testRememberUser() {
+               $backend = $this->getBackend();
+
+               $remembered = $backend->shouldRememberUser();
+               $backend->setRememberUser( !$remembered );
+               $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
+               $backend->setRememberUser( $remembered );
+               $this->assertEquals( $remembered, $backend->shouldRememberUser() );
+       }
+
+       public function testForceHTTPS() {
+               $backend = $this->getBackend();
+
+               $force = $backend->shouldForceHTTPS();
+               $backend->setForceHTTPS( !$force );
+               $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
+               $backend->setForceHTTPS( $force );
+               $this->assertEquals( $force, $backend->shouldForceHTTPS() );
+       }
+
+       public function testLoggedOutTimestamp() {
+               $backend = $this->getBackend();
+
+               $backend->setLoggedOutTimestamp( 42 );
+               $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
+               $backend->setLoggedOutTimestamp( '123' );
+               $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
+       }
+
+       public function testSetUser() {
+               $user = User::newFromName( 'UTSysop' );
+
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'canChangeUser' ) );
+               $this->provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( false ) );
+               $backend = $this->getBackend();
+               $this->assertFalse( $backend->canSetUser() );
+               try {
+                       $backend->setUser( $user );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'Cannot set user on this session; check $session->canSetUser() first',
+                               $ex->getMessage()
+                       );
+               }
+               $this->assertNotSame( $user, $backend->getUser() );
+
+               $this->provider = null;
+               $backend = $this->getBackend();
+               $this->assertTrue( $backend->canSetUser() );
+               $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
+               $backend->setUser( $user );
+               $this->assertSame( $user, $backend->getUser() );
+       }
+
+       public function testDirty() {
+               $backend = $this->getBackend();
+               $priv = \TestingAccessWrapper::newFromObject( $backend );
+               $priv->dataDirty = false;
+               $backend->dirty();
+               $this->assertTrue( $priv->dataDirty );
+       }
+
+       public function testGetData() {
+               $backend = $this->getBackend();
+               $data = $backend->getData();
+               $this->assertSame( array(), $data );
+               $this->assertTrue( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
+               $data['???'] = '!!!';
+               $this->assertSame( array( '???' => '!!!' ), $data );
+
+               $testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend();
+               $this->assertSame( $testData, $backend->getData() );
+               $this->assertFalse( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
+       }
+
+       public function testAddData() {
+               $backend = $this->getBackend();
+               $priv = \TestingAccessWrapper::newFromObject( $backend );
+
+               $priv->data = array( 'foo' => 1 );
+               $priv->dataDirty = false;
+               $backend->addData( array( 'foo' => 1 ) );
+               $this->assertSame( array( 'foo' => 1 ), $priv->data );
+               $this->assertFalse( $priv->dataDirty );
+
+               $priv->data = array( 'foo' => 1 );
+               $priv->dataDirty = false;
+               $backend->addData( array( 'foo' => '1' ) );
+               $this->assertSame( array( 'foo' => '1' ), $priv->data );
+               $this->assertTrue( $priv->dataDirty );
+
+               $priv->data = array( 'foo' => 1 );
+               $priv->dataDirty = false;
+               $backend->addData( array( 'bar' => 2 ) );
+               $this->assertSame( array( 'foo' => 1, 'bar' => 2 ), $priv->data );
+               $this->assertTrue( $priv->dataDirty );
+       }
+
+       public function testDelaySave() {
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $backend = $this->getBackend();
+               $priv = \TestingAccessWrapper::newFromObject( $backend );
+               $priv->persist = true;
+
+               // Saves happen normally when no delay is in effect
+               $this->onSessionMetadataCalled = false;
+               $priv->metaDirty = true;
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
+
+               $this->onSessionMetadataCalled = false;
+               $priv->metaDirty = true;
+               $priv->autosave();
+               $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
+
+               $delay = $backend->delaySave();
+
+               // Autosave doesn't happen when no delay is in effect
+               $this->onSessionMetadataCalled = false;
+               $priv->metaDirty = true;
+               $priv->autosave();
+               $this->assertFalse( $this->onSessionMetadataCalled );
+
+               // Save still does happen when no delay is in effect
+               $priv->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+
+               // Save happens when delay is consumed
+               $this->onSessionMetadataCalled = false;
+               $priv->metaDirty = true;
+               \ScopedCallback::consume( $delay );
+               $this->assertTrue( $this->onSessionMetadataCalled );
+
+               // Test multiple delays
+               $delay1 = $backend->delaySave();
+               $delay2 = $backend->delaySave();
+               $delay3 = $backend->delaySave();
+               $this->onSessionMetadataCalled = false;
+               $priv->metaDirty = true;
+               $priv->autosave();
+               $this->assertFalse( $this->onSessionMetadataCalled );
+               \ScopedCallback::consume( $delay3 );
+               $this->assertFalse( $this->onSessionMetadataCalled );
+               \ScopedCallback::consume( $delay1 );
+               $this->assertFalse( $this->onSessionMetadataCalled );
+               \ScopedCallback::consume( $delay2 );
+               $this->assertTrue( $this->onSessionMetadataCalled );
+       }
+
+       public function testSave() {
+               $user = User::newFromName( 'UTSysop' );
+               $this->store = new TestBagOStuff();
+               $testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
+
+               $neverHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
+               $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
+
+               $neverProvider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $neverProvider->expects( $this->never() )->method( 'persistSession' );
+
+               // Not persistent or dirty
+               $this->provider = $neverProvider;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               $this->assertFalse( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
+               $backend->save();
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
+
+               // Not persistent, but dirty
+               $this->provider = $neverProvider;
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               $this->assertFalse( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+
+               // Persistent, not dirty
+               $this->provider = $neverProvider;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
+               $backend->save();
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
+
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
+               $backend->save();
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
+
+               // Persistent and dirty
+               $this->provider = $neverProvider;
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+
+               // Not marked dirty, but dirty data
+               $this->provider = $neverProvider;
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
+               \TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
+               $backend->save();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+
+               // Bad hook
+               $this->provider = null;
+               $mockHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
+               $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
+                       ->will( $this->returnCallback(
+                               function ( SessionBackend $backend, array &$metadata, array $requests ) {
+                                       $metadata['userId']++;
+                               }
+                       ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $mockHook ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $backend->dirty();
+               try {
+                       $backend->save();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'SessionMetadata hook changed metadata key "userId"',
+                               $ex->getMessage()
+                       );
+               }
+
+               // SessionManager::preventSessionsForUser
+               \TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = array(
+                       $user->getName() => true,
+               );
+               $this->provider = $neverProvider;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               \TestingAccessWrapper::newFromObject( $backend )->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
+               \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
+               $backend->save();
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
+       }
+
+       public function testRenew() {
+               $user = User::newFromName( 'UTSysop' );
+               $this->store = new TestBagOStuff();
+               $testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
+
+               // Not persistent
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->never() )->method( 'persistSession' );
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               $wrap = \TestingAccessWrapper::newFromObject( $backend );
+               $this->assertFalse( $backend->isPersistent(), 'sanity check' );
+               $wrap->metaDirty = false;
+               $wrap->dataDirty = false;
+               $wrap->forcePersist = false;
+               $wrap->expires = 0;
+               $backend->renew();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+               $this->assertNotEquals( 0, $wrap->expires );
+
+               // Persistent
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               $wrap = \TestingAccessWrapper::newFromObject( $backend );
+               $wrap->persist = true;
+               $this->assertTrue( $backend->isPersistent(), 'sanity check' );
+               $wrap->metaDirty = false;
+               $wrap->dataDirty = false;
+               $wrap->forcePersist = false;
+               $wrap->expires = 0;
+               $backend->renew();
+               $this->assertTrue( $this->onSessionMetadataCalled );
+               $blob = $this->store->getSession( self::SESSIONID );
+               $this->assertInternalType( 'array', $blob );
+               $this->assertArrayHasKey( 'metadata', $blob );
+               $metadata = $blob['metadata'];
+               $this->assertInternalType( 'array', $metadata );
+               $this->assertArrayHasKey( '???', $metadata );
+               $this->assertSame( '!!!', $metadata['???'] );
+               $this->assertNotEquals( 0, $wrap->expires );
+
+               // Not persistent, not expiring
+               $this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
+               $this->provider->expects( $this->never() )->method( 'persistSession' );
+               $this->onSessionMetadataCalled = false;
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
+               $this->store->setSessionData( self::SESSIONID, $testData );
+               $backend = $this->getBackend( $user );
+               $this->store->deleteSession( self::SESSIONID );
+               $wrap = \TestingAccessWrapper::newFromObject( $backend );
+               $this->assertFalse( $backend->isPersistent(), 'sanity check' );
+               $wrap->metaDirty = false;
+               $wrap->dataDirty = false;
+               $wrap->forcePersist = false;
+               $expires = time() + $wrap->lifetime + 100;
+               $wrap->expires = $expires;
+               $backend->renew();
+               $this->assertFalse( $this->onSessionMetadataCalled );
+               $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
+               $this->assertEquals( $expires, $wrap->expires );
+       }
+
+       public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
+               $this->onSessionMetadataCalled = true;
+               $metadata['???'] = '!!!';
+       }
+
+       public function testResetIdOfGlobalSession() {
+               if ( !PHPSessionHandler::isInstalled() ) {
+                       PHPSessionHandler::install( SessionManager::singleton() );
+               }
+               if ( !PHPSessionHandler::isEnabled() ) {
+                       $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+                       $rProp->setAccessible( true );
+                       $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+                       $resetHandler = new \ScopedCallback( function () use ( $handler ) {
+                               session_write_close();
+                               $handler->enable = false;
+                       } );
+                       $handler->enable = true;
+               }
+
+               $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
+               \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
+
+               TestUtils::setSessionManagerSingleton( $this->manager );
+
+               $manager = \TestingAccessWrapper::newFromObject( $this->manager );
+               $request = \RequestContext::getMain()->getRequest();
+               $manager->globalSession = $backend->getSession( $request );
+               $manager->globalSessionRequest = $request;
+
+               session_id( self::SESSIONID );
+               \MediaWiki\quietCall( 'session_start' );
+               $backend->resetId();
+               $this->assertNotEquals( self::SESSIONID, $backend->getId() );
+               $this->assertSame( $backend->getId(), session_id() );
+               session_write_close();
+
+               session_id( '' );
+               $this->assertNotSame( $backend->getId(), session_id(), 'sanity check' );
+               $backend->persist();
+               $this->assertSame( $backend->getId(), session_id() );
+               session_write_close();
+       }
+
+       public function testGetAllowedUserRights() {
+               $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'getAllowedUserRights' ) )
+                       ->getMock();
+               $this->provider->expects( $this->any() )->method( 'getAllowedUserRights' )
+                       ->will( $this->returnValue( array( 'foo', 'bar' ) ) );
+
+               $backend = $this->getBackend();
+               $this->assertSame( array( 'foo', 'bar' ), $backend->getAllowedUserRights() );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/SessionIdTest.php b/tests/phpunit/includes/session/SessionIdTest.php
new file mode 100644 (file)
index 0000000..2b06d97
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\SessionId
+ */
+class SessionIdTest extends MediaWikiTestCase {
+
+       public function testEverything() {
+               $id = new SessionId( 'foo' );
+               $this->assertSame( 'foo', $id->getId() );
+               $this->assertSame( 'foo', (string)$id );
+               $id->setId( 'bar' );
+               $this->assertSame( 'bar', $id->getId() );
+               $this->assertSame( 'bar', (string)$id );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/SessionInfoTest.php b/tests/phpunit/includes/session/SessionInfoTest.php
new file mode 100644 (file)
index 0000000..b411f3c
--- /dev/null
@@ -0,0 +1,328 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiTestCase;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionInfo
+ */
+class SessionInfoTest extends MediaWikiTestCase {
+
+       public function testBasics() {
+               $anonInfo = UserInfo::newAnonymous();
+               $userInfo = UserInfo::newFromName( 'UTSysop', true );
+               $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY - 1, array() );
+                       $this->fail( 'Expected exception not thrown', 'priority < min' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MAX_PRIORITY + 1, array() );
+                       $this->fail( 'Expected exception not thrown', 'priority > max' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => 'ABC?' ) );
+                       $this->fail( 'Expected exception not thrown', 'bad session ID' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'userInfo' => new \stdClass ) );
+                       $this->fail( 'Expected exception not thrown', 'bad userInfo' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array() );
+                       $this->fail( 'Expected exception not thrown', 'no provider, no id' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(),
+                               'no provider, no id' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'copyFrom' => new \stdClass ) );
+                       $this->fail( 'Expected exception not thrown', 'bad copyFrom' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid copyFrom', $ex->getMessage(),
+                               'bad copyFrom' );
+               }
+
+               $manager = new SessionManager();
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
+                       ->getMockForAbstractClass();
+               $provider->setManager( $manager );
+               $provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock' ) );
+
+               $provider2 = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
+                       ->getMockForAbstractClass();
+               $provider2->setManager( $manager );
+               $provider2->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider2->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock2' ) );
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                               'provider' => $provider,
+                               'userInfo' => $anonInfo,
+                               'metadata' => 'foo',
+                       ) );
+                       $this->fail( 'Expected exception not thrown', 'bad metadata' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' );
+               }
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'userInfo' => $anonInfo
+               ) );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $anonInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'userInfo' => $unverifiedUserInfo,
+                       'metadata' => array( 'Foo' ),
+               ) );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array( 'Foo' ), $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $id = $manager->generateSessionId();
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $anonInfo
+               ) );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $anonInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $userInfo,
+                       'metadata' => array( 'Foo' ),
+               ) );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $userInfo,
+               ) );
+               $this->assertFalse( $info->wasRemembered(), 'no provider' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+               ) );
+               $this->assertFalse( $info->wasRemembered(), 'no user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $anonInfo,
+               ) );
+               $this->assertFalse( $info->wasRemembered(), 'anonymous user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $unverifiedUserInfo,
+               ) );
+               $this->assertFalse( $info->wasRemembered(), 'unverified user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => false,
+                       'userInfo' => $userInfo,
+               ) );
+               $this->assertFalse( $info->wasRemembered(), 'specific override' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
+                       'id' => $id,
+                       'idIsSafe' => true,
+               ) );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertTrue( $info->isIdSafe() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+                       'forceHTTPS' => 1,
+               ) );
+               $this->assertTrue( $info->forceHTTPS() );
+
+               $fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id . 'A',
+                       'provider' => $provider,
+                       'userInfo' => $userInfo,
+                       'idIsSafe' => true,
+                       'persisted' => true,
+                       'remembered' => true,
+                       'forceHTTPS' => true,
+                       'metadata' => array( 'foo!' ),
+               ) );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
+                       'copyFrom' => $fromInfo,
+               ) );
+               $this->assertSame( $id . 'A', $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertTrue( $info->forceHTTPS() );
+               $this->assertSame( array( 'foo!' ), $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
+                       'id' => $id . 'X',
+                       'provider' => $provider2,
+                       'userInfo' => $unverifiedUserInfo,
+                       'idIsSafe' => false,
+                       'persisted' => false,
+                       'remembered' => false,
+                       'forceHTTPS' => false,
+                       'metadata' => null,
+                       'copyFrom' => $fromInfo,
+               ) );
+               $this->assertSame( $id . 'X', $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
+               $this->assertSame( $provider2, $info->getProvider() );
+               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+               ) );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]null<null>$id",
+                       (string)$info,
+                       'toString'
+               );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id",
+                       (string)$info,
+                       'toString'
+               );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $unverifiedUserInfo
+               ) );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id",
+                       (string)$info,
+                       'toString'
+               );
+       }
+
+       public function testCompare() {
+               $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array( 'id' => $id ) );
+               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array( 'id' => $id ) );
+
+               $this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' );
+               $this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' );
+               $this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' );
+       }
+}
diff --git a/tests/phpunit/includes/session/SessionManagerTest.php b/tests/phpunit/includes/session/SessionManagerTest.php
new file mode 100644 (file)
index 0000000..dc217cd
--- /dev/null
@@ -0,0 +1,1683 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionManager
+ */
+class SessionManagerTest extends MediaWikiTestCase {
+
+       protected $config, $logger, $store;
+
+       protected function getManager() {
+               \ObjectCache::$instances['testSessionStore'] = new TestBagOStuff();
+               $this->config = new \HashConfig( array(
+                       'LanguageCode' => 'en',
+                       'SessionCacheType' => 'testSessionStore',
+                       'ObjectCacheSessionExpiry' => 100,
+                       'SessionProviders' => array(
+                               array( 'class' => 'DummySessionProvider' ),
+                       )
+               ) );
+               $this->logger = new \TestLogger( false, function ( $m ) {
+                       return substr( $m, 0, 15 ) === 'SessionBackend ' ? null : $m;
+               } );
+               $this->store = new TestBagOStuff();
+
+               return new SessionManager( array(
+                       'config' => $this->config,
+                       'logger' => $this->logger,
+                       'store' => $this->store,
+               ) );
+       }
+
+       protected function objectCacheDef( $object ) {
+               return array( 'factory' => function () use ( $object ) {
+                       return $object;
+               } );
+       }
+
+       public function testSingleton() {
+               $reset = TestUtils::setSessionManagerSingleton( null );
+
+               $singleton = SessionManager::singleton();
+               $this->assertInstanceOf( 'MediaWiki\\Session\\SessionManager', $singleton );
+               $this->assertSame( $singleton, SessionManager::singleton() );
+       }
+
+       public function testGetGlobalSession() {
+               $context = \RequestContext::getMain();
+
+               if ( !PHPSessionHandler::isInstalled() ) {
+                       PHPSessionHandler::install( SessionManager::singleton() );
+               }
+               $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+               $rProp->setAccessible( true );
+               $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+               $oldEnable = $handler->enable;
+               $reset[] = new \ScopedCallback( function () use ( $handler, $oldEnable ) {
+                       if ( $handler->enable ) {
+                               session_write_close();
+                       }
+                       $handler->enable = $oldEnable;
+               } );
+               $reset[] = TestUtils::setSessionManagerSingleton( $this->getManager() );
+
+               $handler->enable = true;
+               $request = new \FauxRequest();
+               $context->setRequest( $request );
+               $id = $request->getSession()->getId();
+
+               session_id( '' );
+               $session = SessionManager::getGlobalSession();
+               $this->assertSame( $id, $session->getId() );
+
+               session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
+               $session = SessionManager::getGlobalSession();
+               $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() );
+               $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() );
+
+               session_write_close();
+               $handler->enable = false;
+               $request = new \FauxRequest();
+               $context->setRequest( $request );
+               $id = $request->getSession()->getId();
+
+               session_id( '' );
+               $session = SessionManager::getGlobalSession();
+               $this->assertSame( $id, $session->getId() );
+
+               session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
+               $session = SessionManager::getGlobalSession();
+               $this->assertSame( $id, $session->getId() );
+               $this->assertSame( $id, $request->getSession()->getId() );
+       }
+
+       public function testConstructor() {
+               $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+               $this->assertSame( $this->config, $manager->config );
+               $this->assertSame( $this->logger, $manager->logger );
+               $this->assertSame( $this->store, $manager->store );
+
+               $manager = \TestingAccessWrapper::newFromObject( new SessionManager() );
+               $this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config );
+
+               $manager = \TestingAccessWrapper::newFromObject( new SessionManager( array(
+                       'config' => $this->config,
+               ) ) );
+               $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store );
+
+               foreach ( array(
+                       'config' => '$options[\'config\'] must be an instance of Config',
+                       'logger' => '$options[\'logger\'] must be an instance of LoggerInterface',
+                       'store' => '$options[\'store\'] must be an instance of BagOStuff',
+               ) as $key => $error ) {
+                       try {
+                               new SessionManager( array( $key => new \stdClass ) );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \InvalidArgumentException $ex ) {
+                               $this->assertSame( $error, $ex->getMessage() );
+                       }
+               }
+       }
+
+       public function testGetSessionForRequest() {
+               $manager = $this->getManager();
+               $request = new \FauxRequest();
+
+               $id1 = '';
+               $id2 = '';
+               $idEmpty = 'empty-session-------------------';
+
+               $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods(
+                               array( 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe' )
+                       );
+
+               $provider1 = $providerBuilder->getMock();
+               $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
+                       ->with( $this->identicalTo( $request ) )
+                       ->will( $this->returnCallback( function ( $request ) {
+                               return $request->info1;
+                       } ) );
+               $provider1->expects( $this->any() )->method( 'newSessionInfo' )
+                       ->will( $this->returnCallback( function () use ( $idEmpty, $provider1 ) {
+                               return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                                       'provider' => $provider1,
+                                       'id' => $idEmpty,
+                                       'persisted' => true,
+                                       'idIsSafe' => true,
+                               ) );
+                       } ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Provider1' ) );
+               $provider1->expects( $this->any() )->method( 'describe' )
+                       ->will( $this->returnValue( '#1 sessions' ) );
+
+               $provider2 = $providerBuilder->getMock();
+               $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
+                       ->with( $this->identicalTo( $request ) )
+                       ->will( $this->returnCallback( function ( $request ) {
+                               return $request->info2;
+                       } ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Provider2' ) );
+               $provider2->expects( $this->any() )->method( 'describe' )
+                       ->will( $this->returnValue( '#2 sessions' ) );
+
+               $this->config->set( 'SessionProviders', array(
+                       $this->objectCacheDef( $provider1 ),
+                       $this->objectCacheDef( $provider2 ),
+               ) );
+
+               // No provider returns info
+               $request->info1 = null;
+               $request->info2 = null;
+               $session = $manager->getSessionForRequest( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $idEmpty, $session->getId() );
+               $this->assertNull( $manager->getPersistedSessionId( $request ) );
+
+               // Both providers return info, picks best one
+               $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+                       'provider' => $provider1,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+                       'provider' => $provider2,
+                       'id' => ( $id2 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $session = $manager->getSessionForRequest( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id2, $session->getId() );
+               $this->assertSame( $id2, $manager->getPersistedSessionId( $request ) );
+
+               $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+                       'provider' => $provider1,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+                       'provider' => $provider2,
+                       'id' => ( $id2 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $session = $manager->getSessionForRequest( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id1, $session->getId() );
+               $this->assertSame( $id1, $manager->getPersistedSessionId( $request ) );
+
+               // Tied priorities
+               $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newAnonymous(),
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => ( $id2 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newAnonymous(),
+                       'idIsSafe' => true,
+               ) );
+               try {
+                       $manager->getSessionForRequest( $request );
+                       $this->fail( 'Expcected exception not thrown' );
+               } catch ( \OverFlowException $ex ) {
+                       $this->assertStringStartsWith(
+                               'Multiple sessions for this request tied for top priority: ',
+                               $ex->getMessage()
+                       );
+                       $this->assertCount( 2, $ex->sessionInfos );
+                       $this->assertContains( $request->info1, $ex->sessionInfos );
+                       $this->assertContains( $request->info2, $ex->sessionInfos );
+               }
+               try {
+                       $manager->getPersistedSessionId( $request );
+                       $this->fail( 'Expcected exception not thrown' );
+               } catch ( \OverFlowException $ex ) {
+                       $this->assertStringStartsWith(
+                               'Multiple sessions for this request tied for top priority: ',
+                               $ex->getMessage()
+                       );
+                       $this->assertCount( 2, $ex->sessionInfos );
+                       $this->assertContains( $request->info1, $ex->sessionInfos );
+                       $this->assertContains( $request->info2, $ex->sessionInfos );
+               }
+
+               // Bad provider
+               $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = null;
+               try {
+                       $manager->getSessionForRequest( $request );
+                       $this->fail( 'Expcected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Provider1 returned session info for a different provider: ' . $request->info1,
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       $manager->getPersistedSessionId( $request );
+                       $this->fail( 'Expcected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Provider1 returned session info for a different provider: ' . $request->info1,
+                               $ex->getMessage()
+                       );
+               }
+
+               // Unusable session info
+               $this->logger->setCollect( true );
+               $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => ( $id2 = $manager->generateSessionId() ),
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $session = $manager->getSessionForRequest( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id2, $session->getId() );
+               $this->assertSame( $id2, $manager->getPersistedSessionId( $request ) );
+               $this->logger->setCollect( false );
+
+               // Unpersisted session ID
+               $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => ( $id1 = $manager->generateSessionId() ),
+                       'persisted' => false,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+                       'idIsSafe' => true,
+               ) );
+               $request->info2 = null;
+               $session = $manager->getSessionForRequest( $request );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id1, $session->getId() );
+               $session->persist();
+               $this->assertTrue( $session->isPersistent(), 'sanity check' );
+               $this->assertNull( $manager->getPersistedSessionId( $request ) );
+       }
+
+       public function testGetSessionById() {
+               $manager = $this->getManager();
+
+               try {
+                       $manager->getSessionById( 'bad' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session ID', $ex->getMessage() );
+               }
+
+               // Unknown session ID
+               $id = $manager->generateSessionId();
+               $session = $manager->getSessionById( $id );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id, $session->getId() );
+
+               $id = $manager->generateSessionId();
+               $this->assertNull( $manager->getSessionById( $id, true ) );
+
+               // Known but unloadable session ID
+               $this->logger->setCollect( true );
+               $id = $manager->generateSessionId();
+               $this->store->setRawSession( $id, array( 'metadata' => array(
+                       'provider' => 'DummySessionProvider',
+                       'userId' => 0,
+                       'userName' => null,
+                       'userToken' => null,
+               ) ) );
+
+               try {
+                       $manager->getSessionById( $id );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Can neither load the session nor create an empty session',
+                               $ex->getMessage()
+                       );
+               }
+
+               $this->assertNull( $manager->getSessionById( $id, true ) );
+               $this->logger->setCollect( false );
+
+               // Known session ID
+               $this->store->setSession( $id, array() );
+               $session = $manager->getSessionById( $id );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $id, $session->getId() );
+       }
+
+       public function testGetEmptySession() {
+               $manager = $this->getManager();
+               $pmanager = \TestingAccessWrapper::newFromObject( $manager );
+               $request = new \FauxRequest();
+
+               $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'provideSessionInfo', 'newSessionInfo', '__toString' ) );
+
+               $expectId = null;
+               $info1 = null;
+               $info2 = null;
+
+               $provider1 = $providerBuilder->getMock();
+               $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
+                       ->will( $this->returnValue( null ) );
+               $provider1->expects( $this->any() )->method( 'newSessionInfo' )
+                       ->with( $this->callback( function ( $id ) use ( &$expectId ) {
+                               return $id === $expectId;
+                       } ) )
+                       ->will( $this->returnCallback( function () use ( &$info1 ) {
+                               return $info1;
+                       } ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider1' ) );
+
+               $provider2 = $providerBuilder->getMock();
+               $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
+                       ->will( $this->returnValue( null ) );
+               $provider2->expects( $this->any() )->method( 'newSessionInfo' )
+                       ->with( $this->callback( function ( $id ) use ( &$expectId ) {
+                               return $id === $expectId;
+                       } ) )
+                       ->will( $this->returnCallback( function () use ( &$info2 ) {
+                               return $info2;
+                       } ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider2' ) );
+
+               $this->config->set( 'SessionProviders', array(
+                       $this->objectCacheDef( $provider1 ),
+                       $this->objectCacheDef( $provider2 ),
+               ) );
+
+               // No info
+               $expectId = null;
+               $info1 = null;
+               $info2 = null;
+               try {
+                       $manager->getEmptySession();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'No provider could provide an empty session!',
+                               $ex->getMessage()
+                       );
+               }
+
+               // Info
+               $expectId = null;
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => 'empty---------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = null;
+               $session = $manager->getEmptySession();
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( 'empty---------------------------', $session->getId() );
+
+               // Info, explicitly
+               $expectId = 'expected------------------------';
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => $expectId,
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = null;
+               $session = $pmanager->getEmptySessionInternal( null, $expectId );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( $expectId, $session->getId() );
+
+               // Wrong ID
+               $expectId = 'expected-----------------------2';
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => "un$expectId",
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = null;
+               try {
+                       $pmanager->getEmptySessionInternal( null, $expectId );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'MockProvider1 returned empty session info with a wrong id: ' .
+                                       "un$expectId != $expectId",
+                               $ex->getMessage()
+                       );
+               }
+
+               // Unsafe ID
+               $expectId = 'expected-----------------------2';
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => $expectId,
+                       'persisted' => true,
+               ) );
+               $info2 = null;
+               try {
+                       $pmanager->getEmptySessionInternal( null, $expectId );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'MockProvider1 returned empty session info with id flagged unsafe',
+                               $ex->getMessage()
+                       );
+               }
+
+               // Wrong provider
+               $expectId = null;
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => 'empty---------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = null;
+               try {
+                       $manager->getEmptySession();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'MockProvider1 returned an empty session info for a different provider: ' . $info1,
+                               $ex->getMessage()
+                       );
+               }
+
+               // Highest priority wins
+               $expectId = null;
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+                       'provider' => $provider1,
+                       'id' => 'empty1--------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => 'empty2--------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $session = $manager->getEmptySession();
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( 'empty1--------------------------', $session->getId() );
+
+               $expectId = null;
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+                       'provider' => $provider1,
+                       'id' => 'empty1--------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+                       'provider' => $provider2,
+                       'id' => 'empty2--------------------------',
+                       'persisted' => true,
+                       'idIsSafe' => true,
+               ) );
+               $session = $manager->getEmptySession();
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertSame( 'empty2--------------------------', $session->getId() );
+
+               // Tied priorities throw an exception
+               $expectId = null;
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider1,
+                       'id' => 'empty1--------------------------',
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newAnonymous(),
+                       'idIsSafe' => true,
+               ) );
+               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => 'empty2--------------------------',
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newAnonymous(),
+                       'idIsSafe' => true,
+               ) );
+               try {
+                       $manager->getEmptySession();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertStringStartsWith(
+                               'Multiple empty sessions tied for top priority: ',
+                               $ex->getMessage()
+                       );
+               }
+
+               // Bad id
+               try {
+                       $pmanager->getEmptySessionInternal( null, 'bad' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session ID', $ex->getMessage() );
+               }
+
+               // Session already exists
+               $expectId = 'expected-----------------------3';
+               $this->store->setSessionMeta( $expectId, array(
+                       'provider' => 'MockProvider2',
+                       'userId' => 0,
+                       'userName' => null,
+                       'userToken' => null,
+               ) );
+               try {
+                       $pmanager->getEmptySessionInternal( null, $expectId );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Session ID already exists', $ex->getMessage() );
+               }
+       }
+
+       public function testGetVaryHeaders() {
+               $manager = $this->getManager();
+
+               $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'getVaryHeaders', '__toString' ) );
+
+               $provider1 = $providerBuilder->getMock();
+               $provider1->expects( $this->once() )->method( 'getVaryHeaders' )
+                       ->will( $this->returnValue( array(
+                               'Foo' => null,
+                               'Bar' => array( 'X', 'Bar1' ),
+                               'Quux' => null,
+                       ) ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider1' ) );
+
+               $provider2 = $providerBuilder->getMock();
+               $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
+                       ->will( $this->returnValue( array(
+                               'Baz' => null,
+                               'Bar' => array( 'X', 'Bar2' ),
+                               'Quux' => array( 'Quux' ),
+                       ) ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider2' ) );
+
+               $this->config->set( 'SessionProviders', array(
+                       $this->objectCacheDef( $provider1 ),
+                       $this->objectCacheDef( $provider2 ),
+               ) );
+
+               $expect = array(
+                       'Foo' => array(),
+                       'Bar' => array( 'X', 'Bar1', 3 => 'Bar2' ),
+                       'Quux' => array( 'Quux' ),
+                       'Baz' => array(),
+                       'Quux' => array( 'Quux' ),
+               );
+
+               $this->assertEquals( $expect, $manager->getVaryHeaders() );
+
+               // Again, to ensure it's cached
+               $this->assertEquals( $expect, $manager->getVaryHeaders() );
+       }
+
+       public function testGetVaryCookies() {
+               $manager = $this->getManager();
+
+               $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'getVaryCookies', '__toString' ) );
+
+               $provider1 = $providerBuilder->getMock();
+               $provider1->expects( $this->once() )->method( 'getVaryCookies' )
+                       ->will( $this->returnValue( array( 'Foo', 'Bar' ) ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider1' ) );
+
+               $provider2 = $providerBuilder->getMock();
+               $provider2->expects( $this->once() )->method( 'getVaryCookies' )
+                       ->will( $this->returnValue( array( 'Foo', 'Baz' ) ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider2' ) );
+
+               $this->config->set( 'SessionProviders', array(
+                       $this->objectCacheDef( $provider1 ),
+                       $this->objectCacheDef( $provider2 ),
+               ) );
+
+               $expect = array( 'Foo', 'Bar', 'Baz' );
+
+               $this->assertEquals( $expect, $manager->getVaryCookies() );
+
+               // Again, to ensure it's cached
+               $this->assertEquals( $expect, $manager->getVaryCookies() );
+       }
+
+       public function testGetProviders() {
+               $realManager = $this->getManager();
+               $manager = \TestingAccessWrapper::newFromObject( $realManager );
+
+               $this->config->set( 'SessionProviders', array(
+                       array( 'class' => 'DummySessionProvider' ),
+               ) );
+               $providers = $manager->getProviders();
+               $this->assertArrayHasKey( 'DummySessionProvider', $providers );
+               $provider = \TestingAccessWrapper::newFromObject( $providers['DummySessionProvider'] );
+               $this->assertSame( $manager->logger, $provider->logger );
+               $this->assertSame( $manager->config, $provider->config );
+               $this->assertSame( $realManager, $provider->getManager() );
+
+               $this->config->set( 'SessionProviders', array(
+                       array( 'class' => 'DummySessionProvider' ),
+                       array( 'class' => 'DummySessionProvider' ),
+               ) );
+               $manager->sessionProviders = null;
+               try {
+                       $manager->getProviders();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Duplicate provider name "DummySessionProvider"',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testShutdown() {
+               $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+               $manager->setLogger( new \Psr\Log\NullLogger() );
+
+               $mock = $this->getMock( 'stdClass', array( 'save' ) );
+               $mock->expects( $this->once() )->method( 'save' );
+
+               $manager->allSessionBackends = array( $mock );
+               $manager->shutdown();
+       }
+
+       public function testGetSessionFromInfo() {
+               $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+               $request = new \FauxRequest();
+
+               $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $manager->getProvider( 'DummySessionProvider' ),
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+                       'idIsSafe' => true,
+               ) );
+               \TestingAccessWrapper::newFromObject( $info )->idIsSafe = true;
+               $session1 = \TestingAccessWrapper::newFromObject(
+                       $manager->getSessionFromInfo( $info, $request )
+               );
+               $session2 = \TestingAccessWrapper::newFromObject(
+                       $manager->getSessionFromInfo( $info, $request )
+               );
+
+               $this->assertSame( $session1->backend, $session2->backend );
+               $this->assertNotEquals( $session1->index, $session2->index );
+               $this->assertSame( $session1->getSessionId(), $session2->getSessionId() );
+               $this->assertSame( $id, $session1->getId() );
+
+               \TestingAccessWrapper::newFromObject( $info )->idIsSafe = false;
+               $session3 = $manager->getSessionFromInfo( $info, $request );
+               $this->assertNotSame( $id, $session3->getId() );
+       }
+
+       public function testBackendRegistration() {
+               $manager = $this->getManager();
+
+               $session = $manager->getSessionForRequest( new \FauxRequest );
+               $backend = \TestingAccessWrapper::newFromObject( $session )->backend;
+               $sessionId = $session->getSessionId();
+               $id = (string)$sessionId;
+
+               $this->assertSame( $sessionId, $manager->getSessionById( $id )->getSessionId() );
+
+               $manager->changeBackendId( $backend );
+               $this->assertSame( $sessionId, $session->getSessionId() );
+               $this->assertNotEquals( $id, (string)$sessionId );
+               $id = (string)$sessionId;
+
+               $this->assertSame( $sessionId, $manager->getSessionById( $id )->getSessionId() );
+
+               // Destruction of the session here causes the backend to be deregistered
+               $session = null;
+
+               try {
+                       $manager->changeBackendId( $backend );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Backend was not registered with this SessionManager', $ex->getMessage()
+                       );
+               }
+
+               try {
+                       $manager->deregisterSessionBackend( $backend );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Backend was not registered with this SessionManager', $ex->getMessage()
+                       );
+               }
+
+               $session = $manager->getSessionById( $id );
+               $this->assertSame( $sessionId, $session->getSessionId() );
+       }
+
+       public function testGenerateSessionId() {
+               $manager = $this->getManager();
+
+               $id = $manager->generateSessionId();
+               $this->assertTrue( SessionManager::validateSessionId( $id ), "Generated ID: $id" );
+       }
+
+       public function testAutoCreateUser() {
+               global $wgGroupPermissions;
+
+               $that = $this;
+
+               \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+               $this->setMwGlobals( array( 'wgMainCacheType' => __METHOD__ ) );
+
+               $this->stashMwGlobals( array( 'wgGroupPermissions' ) );
+               $wgGroupPermissions['*']['createaccount'] = true;
+               $wgGroupPermissions['*']['autocreateaccount'] = false;
+
+               // Replace the global singleton with one configured for testing
+               $manager = $this->getManager();
+               $reset = TestUtils::setSessionManagerSingleton( $manager );
+
+               $logger = new \TestLogger( true, function ( $m ) {
+                       if ( substr( $m, 0, 15 ) === 'SessionBackend ' ) {
+                               // Don't care.
+                               return null;
+                       }
+                       $m = str_replace( 'MediaWiki\Session\SessionManager::autoCreateUser: ', '', $m );
+                       $m = preg_replace( '/ - from: .*$/', ' - from: XXX', $m );
+                       return $m;
+               } );
+               $manager->setLogger( $logger );
+
+               $session = SessionManager::getGlobalSession();
+
+               // Can't create an already-existing user
+               $user = User::newFromName( 'UTSysop' );
+               $id = $user->getId();
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( $id, $user->getId() );
+               $this->assertSame( 'UTSysop', $user->getName() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Sanity check that creation works at all
+               $user = User::newFromName( 'UTSessionAutoCreate1' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $this->assertTrue( $manager->autoCreateUser( $user ) );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSessionAutoCreate1', $user->getName() );
+               $this->assertEquals(
+                       $user->getId(), User::idFromName( 'UTSessionAutoCreate1', User::READ_LATEST )
+               );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate1) - from: XXX' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Check lack of permissions
+               $wgGroupPermissions['*']['createaccount'] = false;
+               $wgGroupPermissions['*']['autocreateaccount'] = false;
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Check other permission
+               $wgGroupPermissions['*']['createaccount'] = false;
+               $wgGroupPermissions['*']['autocreateaccount'] = true;
+               $user = User::newFromName( 'UTSessionAutoCreate2' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $this->assertTrue( $manager->autoCreateUser( $user ) );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSessionAutoCreate2', $user->getName() );
+               $this->assertEquals(
+                       $user->getId(), User::idFromName( 'UTSessionAutoCreate2', User::READ_LATEST )
+               );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate2) - from: XXX' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test account-creation block
+               $anon = new User;
+               $block = new \Block( array(
+                       'address' => $anon->getName(),
+                       'user' => $id,
+                       'reason' => __METHOD__,
+                       'expiry' => time() + 100500,
+                       'createAccount' => true,
+               ) );
+               $block->insert();
+               $this->assertInstanceOf( 'Block', $anon->isBlockedFromCreateAccount(), 'sanity check' );
+               $reset2 = new \ScopedCallback( array( $block, 'delete' ) );
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               \ScopedCallback::consume( $reset2 );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Sanity check that creation still works
+               $user = User::newFromName( 'UTSessionAutoCreate3' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $this->assertTrue( $manager->autoCreateUser( $user ) );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSessionAutoCreate3', $user->getName() );
+               $this->assertEquals(
+                       $user->getId(), User::idFromName( 'UTSessionAutoCreate3', User::READ_LATEST )
+               );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate3) - from: XXX' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test prevention by AuthPlugin
+               global $wgAuth;
+               $oldWgAuth = $wgAuth;
+               $mockWgAuth = $this->getMock( 'AuthPlugin', array( 'autoCreate' ) );
+               $mockWgAuth->expects( $this->once() )->method( 'autoCreate' )
+                       ->will( $this->returnValue( false ) );
+               $this->setMwGlobals( array(
+                       'wgAuth' => $mockWgAuth,
+               ) );
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $this->setMwGlobals( array(
+                       'wgAuth' => $oldWgAuth,
+               ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'denied by AuthPlugin' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test prevention by wfReadOnly()
+               $this->setMwGlobals( array(
+                       'wgReadOnly' => 'Because',
+               ) );
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $this->setMwGlobals( array(
+                       'wgReadOnly' => false,
+               ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'denied by wfReadOnly()' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test prevention by a previous session
+               $session->set( 'MWSession::AutoCreateBlacklist', 'test' );
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'blacklisted in session (test)' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test uncreatable name
+               $user = User::newFromName( 'UTDoesNotExist@' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist@', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'Invalid username, blacklisting' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test AbortAutoAccount hook
+               $mock = $this->getMock( __CLASS__, array( 'onAbortAutoAccount' ) );
+               $mock->expects( $this->once() )->method( 'onAbortAutoAccount' )
+                       ->will( $this->returnCallback( function ( User $user, &$msg ) {
+                               $msg = 'No way!';
+                               return false;
+                       } ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array( $mock ) ) );
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array() ) );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'denied by hook: No way!' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test AbortAutoAccount hook screwing up the name
+               $mock = $this->getMock( 'stdClass', array( 'onAbortAutoAccount' ) );
+               $mock->expects( $this->once() )->method( 'onAbortAutoAccount' )
+                       ->will( $this->returnCallback( function ( User $user ) {
+                               $user->setName( 'UTDoesNotExistEither' );
+                       } ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array( $mock ) ) );
+               try {
+                       $user = User::newFromName( 'UTDoesNotExist' );
+                       $manager->autoCreateUser( $user );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'AbortAutoAccount hook tried to change the user name',
+                               $ex->getMessage()
+                       );
+               }
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertNotSame( 'UTDoesNotExistEither', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExistEither', User::READ_LATEST ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array() ) );
+               $session->clear();
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test for "exception backoff"
+               $user = User::newFromName( 'UTDoesNotExist' );
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $user->getName() ) );
+               $cache->set( $backoffKey, 1, 60 * 10 );
+               $this->assertFalse( $manager->autoCreateUser( $user ) );
+               $this->assertSame( 0, $user->getId() );
+               $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+               $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+               $cache->delete( $backoffKey );
+               $session->clear();
+               $this->assertSame( array(
+                       array( LogLevel::DEBUG, 'denied by prior creation attempt failures' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Sanity check that creation still works, and test completion hook
+               $cb = $this->callback( function ( User $user ) use ( $that ) {
+                       $that->assertNotEquals( 0, $user->getId() );
+                       $that->assertSame( 'UTSessionAutoCreate4', $user->getName() );
+                       $that->assertEquals(
+                               $user->getId(), User::idFromName( 'UTSessionAutoCreate4', User::READ_LATEST )
+                       );
+                       return true;
+               } );
+               $mock = $this->getMock( 'stdClass',
+                       array( 'onAuthPluginAutoCreate', 'onLocalUserCreated' ) );
+               $mock->expects( $this->once() )->method( 'onAuthPluginAutoCreate' )
+                       ->with( $cb );
+               $mock->expects( $this->once() )->method( 'onLocalUserCreated' )
+                       ->with( $cb, $this->identicalTo( true ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+                       'AuthPluginAutoCreate' => array( $mock ),
+                       'LocalUserCreated' => array( $mock ),
+               ) );
+               $user = User::newFromName( 'UTSessionAutoCreate4' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $this->assertTrue( $manager->autoCreateUser( $user ) );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSessionAutoCreate4', $user->getName() );
+               $this->assertEquals(
+                       $user->getId(),
+                       User::idFromName( 'UTSessionAutoCreate4', User::READ_LATEST )
+               );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+                       'AuthPluginAutoCreate' => array(),
+                       'LocalUserCreated' => array(),
+               ) );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate4) - from: XXX' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+       }
+
+       public function onAbortAutoAccount( User $user, &$msg ) {
+       }
+
+       public function testPreventSessionsForUser() {
+               $manager = $this->getManager();
+
+               $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( array( 'preventSessionsForUser', '__toString' ) );
+
+               $provider1 = $providerBuilder->getMock();
+               $provider1->expects( $this->once() )->method( 'preventSessionsForUser' )
+                       ->with( $this->equalTo( 'UTSysop' ) );
+               $provider1->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockProvider1' ) );
+
+               $this->config->set( 'SessionProviders', array(
+                       $this->objectCacheDef( $provider1 ),
+               ) );
+
+               $user = User::newFromName( 'UTSysop' );
+               $token = $user->getToken( true );
+
+               $this->assertFalse( $manager->isUserSessionPrevented( 'UTSysop' ) );
+               $manager->preventSessionsForUser( 'UTSysop' );
+               $this->assertNotEquals( $token, User::newFromName( 'UTSysop' )->getToken() );
+               $this->assertTrue( $manager->isUserSessionPrevented( 'UTSysop' ) );
+       }
+
+       public function testLoadSessionInfoFromStore() {
+               $manager = $this->getManager();
+               $logger = new \TestLogger( true, function ( $m ) {
+                       return preg_replace(
+                               '/^Session \[\d+\]\w+<(?:null|anon|[+-]:\d+:\w+)>\w+: /', 'Session X: ', $m
+                       );
+               } );
+               $manager->setLogger( $logger );
+               $request = new \FauxRequest();
+
+               // TestingAccessWrapper can't handle methods with reference arguments, sigh.
+               $rClass = new \ReflectionClass( $manager );
+               $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
+               $rMethod->setAccessible( true );
+               $loadSessionInfoFromStore = function ( &$info ) use ( $rMethod, $manager, $request ) {
+                       return $rMethod->invokeArgs( $manager, array( &$info, $request ) );
+               };
+
+               $userInfo = UserInfo::newFromName( 'UTSysop', true );
+               $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
+
+               $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $metadata = array(
+                       'userId' => $userInfo->getId(),
+                       'userName' => $userInfo->getName(),
+                       'userToken' => $userInfo->getToken( true ),
+                       'provider' => 'Mock',
+               );
+
+               $builder = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( '__toString', 'mergeMetadata', 'refreshSessionInfo' ) );
+
+               $provider = $builder->getMockForAbstractClass();
+               $provider->setManager( $manager );
+               $provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( 'refreshSessionInfo' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock' ) );
+               $provider->expects( $this->any() )->method( 'mergeMetadata' )
+                       ->will( $this->returnCallback( function ( $a, $b ) {
+                               if ( $b === array( 'Throw' ) ) {
+                                       throw new \UnexpectedValueException( 'no merge!' );
+                               }
+                               return array( 'Merged' );
+                       } ) );
+
+               $provider2 = $builder->getMockForAbstractClass();
+               $provider2->setManager( $manager );
+               $provider2->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( false ) );
+               $provider2->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( false ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock2' ) );
+               $provider2->expects( $this->any() )->method( 'refreshSessionInfo' )
+                       ->will( $this->returnCallback( function ( $info, $request, &$metadata ) {
+                               $metadata['changed'] = true;
+                               return true;
+                       } ) );
+
+               $provider3 = $builder->getMockForAbstractClass();
+               $provider3->setManager( $manager );
+               $provider3->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider3->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider3->expects( $this->once() )->method( 'refreshSessionInfo' )
+                       ->will( $this->returnValue( false ) );
+               $provider3->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock3' ) );
+
+               \TestingAccessWrapper::newFromObject( $manager )->sessionProviders = array(
+                       (string)$provider => $provider,
+                       (string)$provider2 => $provider2,
+                       (string)$provider3 => $provider3,
+               );
+
+               // No metadata, basic usage
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertTrue( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Unverified user, no metadata
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $unverifiedUserInfo
+               ) );
+               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Unverified user provided and no metadata to auth it' )
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // No metadata, missing data
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Null provider and no metadata' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertInstanceOf( 'MediaWiki\\Session\\UserInfo', $info->getUserInfo() );
+               $this->assertTrue( $info->getUserInfo()->isVerified() );
+               $this->assertTrue( $info->getUserInfo()->isAnon() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::INFO, 'Session X: No user provided and provider cannot set user' )
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Incomplete/bad metadata
+               $this->store->setRawSession( $id, true );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Bad data' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $this->store->setRawSession( $id, array( 'data' => array() ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $this->store->deleteSession( $id );
+               $this->store->setRawSession( $id, array( 'metadata' => $metadata ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $this->store->setRawSession( $id, array( 'metadata' => $metadata, 'data' => true ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $this->store->setRawSession( $id, array( 'metadata' => true, 'data' => array() ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               foreach ( $metadata as $key => $dummy ) {
+                       $tmp = $metadata;
+                       unset( $tmp[$key] );
+                       $this->store->setRawSession( $id, array( 'metadata' => $tmp, 'data' => array() ) );
+                       $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+                       $this->assertSame( array(
+                               array( LogLevel::WARNING, 'Session X: Bad metadata' ),
+                       ), $logger->getBuffer() );
+                       $logger->clearBuffer();
+               }
+
+               // Basic usage with metadata
+               $this->store->setRawSession( $id, array( 'metadata' => $metadata, 'data' => array() ) );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Mismatched provider
+               $this->store->setSessionMeta( $id, array( 'provider' => 'Bad' ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Wrong provider, Bad !== Mock' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Unknown provider
+               $this->store->setSessionMeta( $id, array( 'provider' => 'Bad' ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Unknown provider, Bad' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Fill in provider
+               $this->store->setSessionMeta( $id, $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Bad user metadata
+               $this->store->setSessionMeta( $id, array( 'userId' => -1, 'userToken' => null ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::ERROR, 'Session X: Invalid ID' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => 0, 'userName' => '<X>', 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::ERROR, 'Session X: Invalid user name' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Mismatched user by ID
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => $userInfo->getId() + 1, 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: User ID mismatch, 2 !== 1' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Mismatched user by name
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => 0, 'userName' => 'X', 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: User name mismatch, X !== UTSysop' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // ID matches, name doesn't
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array(
+                               LogLevel::WARNING, 'Session X: User ID matched but name didn\'t (rename?), X !== UTSysop'
+                       ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Mismatched anon user
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => 0, 'userName' => null, 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array(
+                               LogLevel::WARNING, 'Session X: Metadata has an anonymous user, but a non-anon user was provided'
+                       ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Lookup user by ID
+               $this->store->setSessionMeta( $id, array( 'userToken' => null ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Lookup user by name
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => 0, 'userName' => 'UTSysop', 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Lookup anonymous user
+               $this->store->setSessionMeta(
+                       $id, array( 'userId' => 0, 'userName' => null, 'userToken' => null ) + $metadata
+               );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->getUserInfo()->isAnon() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Unverified user with metadata
+               $this->store->setSessionMeta( $id, $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $unverifiedUserInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->getUserInfo()->isVerified() );
+               $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
+               $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Unverified user with metadata
+               $this->store->setSessionMeta( $id, $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $unverifiedUserInfo
+               ) );
+               $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->getUserInfo()->isVerified() );
+               $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
+               $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Wrong token
+               $this->store->setSessionMeta( $id, array( 'userToken' => 'Bad' ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: User token mismatch' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Provider metadata
+               $this->store->setSessionMeta( $id, array( 'provider' => 'Mock2' ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider2,
+                       'id' => $id,
+                       'userInfo' => $userInfo,
+                       'metadata' => array( 'Info' ),
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array( 'Info', 'changed' => true ), $info->getProviderMetadata() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $this->store->setSessionMeta( $id, array( 'providerMetadata' => array( 'Saved' ) ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo,
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array( 'Saved' ), $info->getProviderMetadata() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo,
+                       'metadata' => array( 'Info' ),
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array( 'Merged' ), $info->getProviderMetadata() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo,
+                       'metadata' => array( 'Throw' ),
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Metadata merge failed: no merge!' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Remember from session
+               $this->store->setSessionMeta( $id, $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $this->store->setSessionMeta( $id, array( 'remember' => true ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $this->store->setSessionMeta( $id, array( 'remember' => false ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // forceHTTPS from session
+               $this->store->setSessionMeta( $id, $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $this->store->setSessionMeta( $id, array( 'forceHTTPS' => true ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               $this->store->setSessionMeta( $id, array( 'forceHTTPS' => false ) + $metadata );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo,
+                       'forceHTTPS' => true
+               ) );
+               $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Provider refreshSessionInfo() returning false
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider3,
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertSame( array(), $logger->getBuffer() );
+
+               // Hook
+               $that = $this;
+               $called = false;
+               $data = array( 'foo' => 1 );
+               $this->store->setSession( $id, array( 'metadata' => $metadata, 'data' => $data ) );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ) );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+                       'SessionCheckInfo' => array( function ( &$reason, $i, $r, $m, $d ) use (
+                               $that, $info, $metadata, $data, $request, &$called
+                       ) {
+                               $that->assertSame( $info->getId(), $i->getId() );
+                               $that->assertSame( $info->getProvider(), $i->getProvider() );
+                               $that->assertSame( $info->getUserInfo(), $i->getUserInfo() );
+                               $that->assertSame( $request, $r );
+                               $that->assertEquals( $metadata, $m );
+                               $that->assertEquals( $data, $d );
+                               $called = true;
+                               return false;
+                       } )
+               ) );
+               $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+               $this->assertTrue( $called );
+               $this->assertSame( array(
+                       array( LogLevel::WARNING, 'Session X: Hook aborted' ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
+       }
+
+}
diff --git a/tests/phpunit/includes/session/SessionProviderTest.php b/tests/phpunit/includes/session/SessionProviderTest.php
new file mode 100644 (file)
index 0000000..d7aebcd
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionProvider
+ */
+class SessionProviderTest extends MediaWikiTestCase {
+
+       public function testBasics() {
+               $manager = new SessionManager();
+               $logger = new \TestLogger();
+               $config = new \HashConfig();
+
+               $provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider' );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $provider->setConfig( $config );
+               $this->assertSame( $config, $priv->config );
+               $provider->setLogger( $logger );
+               $this->assertSame( $logger, $priv->logger );
+               $provider->setManager( $manager );
+               $this->assertSame( $manager, $priv->manager );
+               $this->assertSame( $manager, $provider->getManager() );
+
+               $this->assertSame( array(), $provider->getVaryHeaders() );
+               $this->assertSame( array(), $provider->getVaryCookies() );
+               $this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) );
+
+               $this->assertSame( get_class( $provider ), (string)$provider );
+
+               $this->assertNull( $provider->whyNoSession() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+                       'provider' => $provider,
+               ) );
+               $metadata = array( 'foo' );
+               $this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) );
+               $this->assertSame( array( 'foo' ), $metadata );
+       }
+
+       /**
+        * @dataProvider provideNewSessionInfo
+        * @param bool $persistId Return value for ->persistsSessionId()
+        * @param bool $persistUser Return value for ->persistsSessionUser()
+        * @param bool $ok Whether a SessionInfo is provided
+        */
+       public function testNewSessionInfo( $persistId, $persistUser, $ok ) {
+               $manager = new SessionManager();
+
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( $persistId ) );
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( $persistUser ) );
+               $provider->setManager( $manager );
+
+               if ( $ok ) {
+                       $info = $provider->newSessionInfo();
+                       $this->assertNotNull( $info );
+                       $this->assertFalse( $info->wasPersisted() );
+                       $this->assertTrue( $info->isIdSafe() );
+
+                       $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+                       $info = $provider->newSessionInfo( $id );
+                       $this->assertNotNull( $info );
+                       $this->assertSame( $id, $info->getId() );
+                       $this->assertFalse( $info->wasPersisted() );
+                       $this->assertTrue( $info->isIdSafe() );
+               } else {
+                       $this->assertNull( $provider->newSessionInfo() );
+               }
+       }
+
+       public function testMergeMetadata() {
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->getMockForAbstractClass();
+
+               try {
+                       $provider->mergeMetadata(
+                               array( 'foo' => 1, 'baz' => 3 ),
+                               array( 'bar' => 2, 'baz' => '3' )
+                       );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'Key "baz" changed', $ex->getMessage() );
+               }
+
+               $res = $provider->mergeMetadata(
+                       array( 'foo' => 1, 'baz' => 3 ),
+                       array( 'bar' => 2, 'baz' => 3 )
+               );
+               $this->assertSame( array( 'bar' => 2, 'baz' => 3 ), $res );
+       }
+
+       public static function provideNewSessionInfo() {
+               return array(
+                       array( false, false, false ),
+                       array( true, false, false ),
+                       array( false, true, false ),
+                       array( true, true, true ),
+               );
+       }
+
+       public function testImmutableSessions() {
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider->preventSessionsForUser( 'Foo' );
+
+               $provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+                       ->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( false ) );
+               try {
+                       $provider->preventSessionsForUser( 'Foo' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+       }
+
+       public function testHashToSessionId() {
+               $config = new \HashConfig( array(
+                       'SecretKey' => 'Shhh!',
+               ) );
+
+               $provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
+                       array(), 'MockSessionProvider' );
+               $provider->setConfig( $config );
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) );
+               $this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9',
+                       $priv->hashToSessionId( 'foobar', 'secret' ) );
+
+               try {
+                       $priv->hashToSessionId( array() );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$data must be a string, array was passed',
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       $priv->hashToSessionId( '', false );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$key must be a string or null, boolean was passed',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testDescribe() {
+               $provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
+                       array(), 'MockSessionProvider' );
+
+               $this->assertSame(
+                       'MockSessionProvider sessions',
+                       $provider->describe( \Language::factory( 'en' ) )
+               );
+       }
+
+       public function testGetAllowedUserRights() {
+               $provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider' );
+               $backend = TestUtils::getDummySessionBackend();
+
+               try {
+                       $provider->getAllowedUserRights( $backend );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Backend\'s provider isn\'t $this',
+                               $ex->getMessage()
+                       );
+               }
+
+               \TestingAccessWrapper::newFromObject( $backend )->provider = $provider;
+               $this->assertNull( $provider->getAllowedUserRights( $backend ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/SessionTest.php b/tests/phpunit/includes/session/SessionTest.php
new file mode 100644 (file)
index 0000000..efc92f7
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Session
+ */
+class SessionTest extends MediaWikiTestCase {
+
+       public function testConstructor() {
+               $backend = TestUtils::getDummySessionBackend();
+               \TestingAccessWrapper::newFromObject( $backend )->requests = array( -1 => 'dummy' );
+               \TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
+
+               $session = new Session( $backend, 42 );
+               $priv = \TestingAccessWrapper::newFromObject( $session );
+               $this->assertSame( $backend, $priv->backend );
+               $this->assertSame( 42, $priv->index );
+
+               $request = new \FauxRequest();
+               $priv2 = \TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
+               $this->assertSame( $backend, $priv2->backend );
+               $this->assertNotSame( $priv->index, $priv2->index );
+               $this->assertSame( $request, $priv2->getRequest() );
+       }
+
+       /**
+        * @dataProvider provideMethods
+        * @param string $m Method to test
+        * @param array $args Arguments to pass to the method
+        * @param bool $index Whether the backend method gets passed the index
+        * @param bool $ret Whether the method returns a value
+        */
+       public function testMethods( $m, $args, $index, $ret ) {
+               $mock = $this->getMock( 'MediaWiki\\Session\\DummySessionBackend',
+                       array( $m, 'deregisterSession' ) );
+               $mock->expects( $this->once() )->method( 'deregisterSession' )
+                       ->with( $this->identicalTo( 42 ) );
+
+               $tmp = $mock->expects( $this->once() )->method( $m );
+               $expectArgs = array();
+               if ( $index ) {
+                       $expectArgs[] = $this->identicalTo( 42 );
+               }
+               foreach ( $args as $arg ) {
+                       $expectArgs[] = $this->identicalTo( $arg );
+               }
+               $tmp = call_user_func_array( array( $tmp, 'with' ), $expectArgs );
+
+               $retval = new \stdClass;
+               $tmp->will( $this->returnValue( $retval ) );
+
+               $session = TestUtils::getDummySession( $mock, 42 );
+
+               if ( $ret ) {
+                       $this->assertSame( $retval, call_user_func_array( array( $session, $m ), $args ) );
+               } else {
+                       $this->assertNull( call_user_func_array( array( $session, $m ), $args ) );
+               }
+
+               // Trigger Session destructor
+               $session = null;
+       }
+
+       public static function provideMethods() {
+               return array(
+                       array( 'getId', array(), false, true ),
+                       array( 'getSessionId', array(), false, true ),
+                       array( 'resetId', array(), false, true ),
+                       array( 'getProvider', array(), false, true ),
+                       array( 'isPersistent', array(), false, true ),
+                       array( 'persist', array(), false, false ),
+                       array( 'shouldRememberUser', array(), false, true ),
+                       array( 'setRememberUser', array( true ), false, false ),
+                       array( 'getRequest', array(), true, true ),
+                       array( 'getUser', array(), false, true ),
+                       array( 'getAllowedUserRights', array(), false, true ),
+                       array( 'canSetUser', array(), false, true ),
+                       array( 'setUser', array( new \stdClass ), false, false ),
+                       array( 'suggestLoginUsername', array(), true, true ),
+                       array( 'shouldForceHTTPS', array(), false, true ),
+                       array( 'setForceHTTPS', array( true ), false, false ),
+                       array( 'getLoggedOutTimestamp', array(), false, true ),
+                       array( 'setLoggedOutTimestamp', array( 123 ), false, false ),
+                       array( 'getProviderMetadata', array(), false, true ),
+                       array( 'save', array(), false, false ),
+                       array( 'delaySave', array(), false, true ),
+                       array( 'renew', array(), false, false ),
+               );
+       }
+
+       public function testDataAccess() {
+               $session = TestUtils::getDummySession();
+               $backend = \TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session->get( 'foo' ) );
+               $this->assertEquals( 'zero', $session->get( 0 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( null, $session->get( 'null' ) );
+               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->set( 'foo', 55 );
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertTrue( $session->exists( 'foo' ) );
+               $this->assertTrue( $session->exists( 1 ) );
+               $this->assertFalse( $session->exists( 'null' ) );
+               $this->assertFalse( $session->exists( 100 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->remove( 'foo' );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               $session->remove( 1 );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->remove( 101 );
+               $this->assertFalse( $backend->dirty );
+
+               $backend->data = array( 'a', 'b', '?' => 'c' );
+               $this->assertSame( 3, $session->count() );
+               $this->assertSame( 3, count( $session ) );
+               $this->assertFalse( $backend->dirty );
+
+               $data = array();
+               foreach ( $session as $key => $value ) {
+                       $data[$key] = $value;
+               }
+               $this->assertEquals( $backend->data, $data );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testClear() {
+               $session = TestUtils::getDummySession();
+               $priv = \TestingAccessWrapper::newFromObject( $session );
+
+               $backend = $this->getMock(
+                       'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
+               );
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( true ) );
+               $backend->expects( $this->once() )->method( 'setUser' )
+                       ->with( $this->callback( function ( $user ) {
+                               return $user instanceof User && $user->isAnon();
+                       } ) );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertSame( array(), $backend->data );
+               $this->assertTrue( $backend->dirty );
+
+               $backend = $this->getMock(
+                       'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
+               );
+               $backend->data = array();
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( true ) );
+               $backend->expects( $this->once() )->method( 'setUser' )
+                       ->with( $this->callback( function ( $user ) {
+                               return $user instanceof User && $user->isAnon();
+                       } ) );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertFalse( $backend->dirty );
+
+               $backend = $this->getMock(
+                       'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
+               );
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( false ) );
+               $backend->expects( $this->never() )->method( 'setUser' );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertSame( array(), $backend->data );
+               $this->assertTrue( $backend->dirty );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/TestBagOStuff.php b/tests/phpunit/includes/session/TestBagOStuff.php
new file mode 100644 (file)
index 0000000..e674e7b
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * BagOStuff with utility functions for MediaWiki\\Session\\* testing
+ */
+class TestBagOStuff extends \HashBagOStuff {
+
+       /**
+        * @param string $id Session ID
+        * @param array $data Session data
+        * @param int $expiry Expiry
+        * @param User $user User for metadata
+        */
+       public function setSessionData( $id, array $data, $expiry = 0, User $user = null ) {
+               $this->setSession( $id, array( 'data' => $data ), $expiry, $user );
+       }
+
+       /**
+        * @param string $id Session ID
+        * @param array $metadata Session metadata
+        * @param int $expiry Expiry
+        */
+       public function setSessionMeta( $id, array $metadata, $expiry = 0 ) {
+               $this->setSession( $id, array( 'metadata' => $metadata ), $expiry );
+       }
+
+       /**
+        * @param string $id Session ID
+        * @param array $blob Session metadata and data
+        * @param int $expiry Expiry
+        * @param User $user User for metadata
+        */
+       public function setSession( $id, array $blob, $expiry = 0, User $user = null ) {
+               $blob += array(
+                       'data' => array(),
+                       'metadata' => array(),
+               );
+               $blob['metadata'] += array(
+                       'userId' => $user ? $user->getId() : 0,
+                       'userName' => $user ? $user->getName() : null,
+                       'userToken' => $user ? $user->getToken( true ) : null,
+                       'provider' => 'DummySessionProvider',
+               );
+
+               $this->setRawSession( $id, $blob, $expiry, $user );
+       }
+
+       /**
+        * @param string $id Session ID
+        * @param array|mixed $blob Session metadata and data
+        * @param int $expiry Expiry
+        */
+       public function setRawSession( $id, $blob, $expiry = 0 ) {
+               if ( $expiry <= 0 ) {
+                       $expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' );
+               }
+
+               $this->set( wfMemcKey( 'MWSession', $id ), $blob, $expiry );
+       }
+
+       /**
+        * @param string $id Session ID
+        * @return mixed
+        */
+       public function getSession( $id ) {
+               return $this->get( wfMemcKey( 'MWSession', $id ) );
+       }
+
+       /**
+        * @param string $id Session ID
+        */
+       public function deleteSession( $id ) {
+               $this->delete( wfMemcKey( 'MWSession', $id ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/session/TestUtils.php b/tests/phpunit/includes/session/TestUtils.php
new file mode 100644 (file)
index 0000000..1619983
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * Utility functions for Session unit tests
+ */
+class TestUtils {
+
+       /**
+        * Override the singleton for unit testing
+        * @param SessionManager|null $manager
+        * @return \\ScopedCallback|null
+        */
+       public static function setSessionManagerSingleton( SessionManager $manager = null ) {
+               session_write_close();
+
+               $rInstance = new \ReflectionProperty(
+                       'MediaWiki\\Session\\SessionManager', 'instance'
+               );
+               $rInstance->setAccessible( true );
+               $rGlobalSession = new \ReflectionProperty(
+                       'MediaWiki\\Session\\SessionManager', 'globalSession'
+               );
+               $rGlobalSession->setAccessible( true );
+               $rGlobalSessionRequest = new \ReflectionProperty(
+                       'MediaWiki\\Session\\SessionManager', 'globalSessionRequest'
+               );
+               $rGlobalSessionRequest->setAccessible( true );
+
+               $oldInstance = $rInstance->getValue();
+
+               $reset = array(
+                       array( $rInstance, $oldInstance ),
+                       array( $rGlobalSession, $rGlobalSession->getValue() ),
+                       array( $rGlobalSessionRequest, $rGlobalSessionRequest->getValue() ),
+               );
+
+               $rInstance->setValue( $manager );
+               $rGlobalSession->setValue( null );
+               $rGlobalSessionRequest->setValue( null );
+               if ( $manager && PHPSessionHandler::isInstalled() ) {
+                       PHPSessionHandler::install( $manager );
+               }
+
+               return new \ScopedCallback( function () use ( &$reset, $oldInstance ) {
+                       foreach ( $reset as &$arr ) {
+                               $arr[0]->setValue( $arr[1] );
+                       }
+                       if ( $oldInstance && PHPSessionHandler::isInstalled() ) {
+                               PHPSessionHandler::install( $oldInstance );
+                       }
+               } );
+       }
+
+       /**
+        * If you need a SessionBackend for testing but don't want to create a real
+        * one, use this.
+        * @return SessionBackend Unconfigured! Use reflection to set any private
+        *  fields necessary.
+        */
+       public static function getDummySessionBackend() {
+               $rc = new \ReflectionClass( 'MediaWiki\\Session\\SessionBackend' );
+               if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
+                       \PHPUnit_Framework_Assert::markTestSkipped(
+                               'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
+                       );
+               }
+
+               return $rc->newInstanceWithoutConstructor();
+       }
+
+       /**
+        * If you need a Session for testing but don't want to create a backend to
+        * construct one, use this.
+        * @param object $backend Object to serve as the SessionBackend
+        * @param int $index Index
+        * @return Session
+        */
+       public static function getDummySession( $backend = null, $index = -1 ) {
+               $rc = new \ReflectionClass( 'MediaWiki\\Session\\Session' );
+               if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
+                       \PHPUnit_Framework_Assert::markTestSkipped(
+                               'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
+                       );
+               }
+
+               if ( $backend === null ) {
+                       $backend = new DummySessionBackend;
+               }
+
+               $session = $rc->newInstanceWithoutConstructor();
+               $priv = \TestingAccessWrapper::newFromObject( $session );
+               $priv->backend = $backend;
+               $priv->index = $index;
+               return $session;
+       }
+
+}
diff --git a/tests/phpunit/includes/session/UserInfoTest.php b/tests/phpunit/includes/session/UserInfoTest.php
new file mode 100644 (file)
index 0000000..121bb72
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\UserInfo
+ */
+class UserInfoTest extends MediaWikiTestCase {
+
+       public function testNewAnonymous() {
+               $userinfo = UserInfo::newAnonymous();
+
+               $this->assertTrue( $userinfo->isAnon() );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( 0, $userinfo->getId() );
+               $this->assertSame( null, $userinfo->getName() );
+               $this->assertSame( null, $userinfo->getToken() );
+               $this->assertNotNull( $userinfo->getUser() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+               $this->assertSame( '<anon>', (string)$userinfo );
+       }
+
+       public function testNewFromId() {
+               $id = wfGetDB( DB_MASTER )->selectField( 'user', 'MAX(user_id)' ) + 1;
+               try {
+                       UserInfo::newFromId( $id );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid ID', $ex->getMessage() );
+               }
+
+               $user = User::newFromName( 'UTSysop' );
+               $userinfo = UserInfo::newFromId( $user->getId() );
+               $this->assertFalse( $userinfo->isAnon() );
+               $this->assertFalse( $userinfo->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo->getId() );
+               $this->assertSame( $user->getName(), $userinfo->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo->getUser() );
+               $userinfo2 = $userinfo->verified();
+               $this->assertNotSame( $userinfo2, $userinfo );
+               $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
+
+               $this->assertFalse( $userinfo2->isAnon() );
+               $this->assertTrue( $userinfo2->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo2->getId() );
+               $this->assertSame( $user->getName(), $userinfo2->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo2->getUser() );
+               $this->assertSame( $userinfo2, $userinfo2->verified() );
+               $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
+
+               $userinfo = UserInfo::newFromId( $user->getId(), true );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+       }
+
+       public function testNewFromName() {
+               try {
+                       UserInfo::newFromName( '<bad name>' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid user name', $ex->getMessage() );
+               }
+
+               // User name that exists
+               $user = User::newFromName( 'UTSysop' );
+               $userinfo = UserInfo::newFromName( $user->getName() );
+               $this->assertFalse( $userinfo->isAnon() );
+               $this->assertFalse( $userinfo->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo->getId() );
+               $this->assertSame( $user->getName(), $userinfo->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo->getUser() );
+               $userinfo2 = $userinfo->verified();
+               $this->assertNotSame( $userinfo2, $userinfo );
+               $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
+
+               $this->assertFalse( $userinfo2->isAnon() );
+               $this->assertTrue( $userinfo2->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo2->getId() );
+               $this->assertSame( $user->getName(), $userinfo2->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo2->getUser() );
+               $this->assertSame( $userinfo2, $userinfo2->verified() );
+               $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
+
+               $userinfo = UserInfo::newFromName( $user->getName(), true );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+
+               // User name that does not exist should still be non-anon
+               $user = User::newFromName( 'DoesNotExist' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $userinfo = UserInfo::newFromName( $user->getName() );
+               $this->assertFalse( $userinfo->isAnon() );
+               $this->assertFalse( $userinfo->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo->getId() );
+               $this->assertSame( $user->getName(), $userinfo->getName() );
+               $this->assertSame( null, $userinfo->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo->getUser() );
+               $userinfo2 = $userinfo->verified();
+               $this->assertNotSame( $userinfo2, $userinfo );
+               $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
+
+               $this->assertFalse( $userinfo2->isAnon() );
+               $this->assertTrue( $userinfo2->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo2->getId() );
+               $this->assertSame( $user->getName(), $userinfo2->getName() );
+               $this->assertSame( null, $userinfo2->getToken() );
+               $this->assertInstanceOf( 'User', $userinfo2->getUser() );
+               $this->assertSame( $userinfo2, $userinfo2->verified() );
+               $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
+
+               $userinfo = UserInfo::newFromName( $user->getName(), true );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+       }
+
+       public function testNewFromUser() {
+               // User that exists
+               $user = User::newFromName( 'UTSysop' );
+               $userinfo = UserInfo::newFromUser( $user );
+               $this->assertFalse( $userinfo->isAnon() );
+               $this->assertFalse( $userinfo->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo->getId() );
+               $this->assertSame( $user->getName(), $userinfo->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo->getToken() );
+               $this->assertSame( $user, $userinfo->getUser() );
+               $userinfo2 = $userinfo->verified();
+               $this->assertNotSame( $userinfo2, $userinfo );
+               $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
+
+               $this->assertFalse( $userinfo2->isAnon() );
+               $this->assertTrue( $userinfo2->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo2->getId() );
+               $this->assertSame( $user->getName(), $userinfo2->getName() );
+               $this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
+               $this->assertSame( $user, $userinfo2->getUser() );
+               $this->assertSame( $userinfo2, $userinfo2->verified() );
+               $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
+
+               $userinfo = UserInfo::newFromUser( $user, true );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+
+               // User name that does not exist should still be non-anon
+               $user = User::newFromName( 'DoesNotExist' );
+               $this->assertSame( 0, $user->getId(), 'sanity check' );
+               $userinfo = UserInfo::newFromUser( $user );
+               $this->assertFalse( $userinfo->isAnon() );
+               $this->assertFalse( $userinfo->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo->getId() );
+               $this->assertSame( $user->getName(), $userinfo->getName() );
+               $this->assertSame( null, $userinfo->getToken() );
+               $this->assertSame( $user, $userinfo->getUser() );
+               $userinfo2 = $userinfo->verified();
+               $this->assertNotSame( $userinfo2, $userinfo );
+               $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
+
+               $this->assertFalse( $userinfo2->isAnon() );
+               $this->assertTrue( $userinfo2->isVerified() );
+               $this->assertSame( $user->getId(), $userinfo2->getId() );
+               $this->assertSame( $user->getName(), $userinfo2->getName() );
+               $this->assertSame( null, $userinfo2->getToken() );
+               $this->assertSame( $user, $userinfo2->getUser() );
+               $this->assertSame( $userinfo2, $userinfo2->verified() );
+               $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
+
+               $userinfo = UserInfo::newFromUser( $user, true );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( $userinfo, $userinfo->verified() );
+
+               // Anonymous user gives anon
+               $userinfo = UserInfo::newFromUser( new User, false );
+               $this->assertTrue( $userinfo->isVerified() );
+               $this->assertSame( 0, $userinfo->getId() );
+               $this->assertSame( null, $userinfo->getName() );
+       }
+
+}
index b749662..428fd27 100644 (file)
@@ -16,7 +16,6 @@ class UploadFromUrlTest extends ApiTestCase {
                        'wgAllowCopyUploads' => true,
                        'wgAllowAsyncCopyUploads' => true,
                ) );
-               wfSetupSession();
 
                if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
                        $this->deleteFile( 'UploadFromUrlTest.png' );
@@ -26,15 +25,12 @@ class UploadFromUrlTest extends ApiTestCase {
        protected function doApiRequest( array $params, array $unused = null,
                $appendModule = false, User $user = null
        ) {
-               $sessionId = session_id();
-               session_write_close();
+               global $wgRequest;
 
-               $req = new FauxRequest( $params, true, $_SESSION );
+               $req = new FauxRequest( $params, true, $wgRequest->getSession() );
                $module = new ApiMain( $req, true );
                $module->execute();
 
-               wfSetupSession( $sessionId );
-
                return array(
                        $module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ),
                        $req
diff --git a/tests/phpunit/includes/user/BotPasswordTest.php b/tests/phpunit/includes/user/BotPasswordTest.php
new file mode 100644 (file)
index 0000000..c118803
--- /dev/null
@@ -0,0 +1,379 @@
+<?php
+
+use MediaWiki\Session\SessionManager;
+
+/**
+ * @covers BotPassword
+ * @group Database
+ */
+class BotPasswordTest extends MediaWikiTestCase {
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgEnableBotPasswords' => true,
+                       'wgBotPasswordsDatabase' => false,
+                       'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
+                       'wgGrantPermissions' => array(
+                               'test' => array( 'read' => true ),
+                       ),
+                       'wgUserrightsInterwikiDelimiter' => '@',
+               ) );
+
+               $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock1->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( true ) );
+               $mock1->expects( $this->any() )->method( 'lookupUserNames' )
+                       ->will( $this->returnValue( array( 'UTSysop' => 42, 'UTDummy' => 43, 'UTInvalid' => 0 ) ) );
+               $mock1->expects( $this->never() )->method( 'lookupCentralIds' );
+
+               $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock2->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( false ) );
+               $mock2->expects( $this->any() )->method( 'lookupUserNames' )
+                       ->will( $this->returnArgument( 0 ) );
+               $mock2->expects( $this->never() )->method( 'lookupCentralIds' );
+
+               $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', array(
+                       'BotPasswordTest OkMock' => array( 'factory' => function () use ( $mock1 ) {
+                               return $mock1;
+                       } ),
+                       'BotPasswordTest FailMock' => array( 'factory' => function () use ( $mock2 ) {
+                               return $mock2;
+                       } ),
+               ) );
+
+               CentralIdLookup::resetCache();
+       }
+
+       public function addDBData() {
+               $passwordFactory = new \PasswordFactory();
+               $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
+               $passwordFactory->setDefaultType( 'A' );
+               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete(
+                       'bot_passwords',
+                       array( 'bp_user' => array( 42, 43 ), 'bp_app_id' => 'BotPassword' ),
+                       __METHOD__
+               );
+               $dbw->insert(
+                       'bot_passwords',
+                       array(
+                               array(
+                                       'bp_user' => 42,
+                                       'bp_app_id' => 'BotPassword',
+                                       'bp_password' => $pwhash->toString(),
+                                       'bp_token' => 'token!',
+                                       'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
+                                       'bp_grants' => '["test"]',
+                               ),
+                               array(
+                                       'bp_user' => 43,
+                                       'bp_app_id' => 'BotPassword',
+                                       'bp_password' => $pwhash->toString(),
+                                       'bp_token' => 'token!',
+                                       'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
+                                       'bp_grants' => '["test"]',
+                               ),
+                       ),
+                       __METHOD__
+               );
+       }
+
+       public function testBasics() {
+               $user = User::newFromName( 'UTSysop' );
+               $bp = BotPassword::newFromUser( $user, 'BotPassword' );
+               $this->assertInstanceOf( 'BotPassword', $bp );
+               $this->assertTrue( $bp->isSaved() );
+               $this->assertSame( 42, $bp->getUserCentralId() );
+               $this->assertSame( 'BotPassword', $bp->getAppId() );
+               $this->assertSame( 'token!', trim( $bp->getToken(), " \0" ) );
+               $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
+               $this->assertSame( array( 'test' ), $bp->getGrants() );
+
+               $this->assertNull( BotPassword::newFromUser( $user, 'DoesNotExist' ) );
+
+               $this->setMwGlobals( array(
+                       'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock'
+               ) );
+               $this->assertNull( BotPassword::newFromUser( $user, 'BotPassword' ) );
+
+               $this->assertSame( '@', BotPassword::getSeparator() );
+               $this->setMwGlobals( array(
+                       'wgUserrightsInterwikiDelimiter' => '#',
+               ) );
+               $this->assertSame( '#', BotPassword::getSeparator() );
+       }
+
+       public function testUnsaved() {
+               $user = User::newFromName( 'UTSysop' );
+               $bp = BotPassword::newUnsaved( array(
+                       'user' => $user,
+                       'appId' => 'DoesNotExist'
+               ) );
+               $this->assertInstanceOf( 'BotPassword', $bp );
+               $this->assertFalse( $bp->isSaved() );
+               $this->assertSame( 42, $bp->getUserCentralId() );
+               $this->assertSame( 'DoesNotExist', $bp->getAppId() );
+               $this->assertEquals( MWRestrictions::newDefault(), $bp->getRestrictions() );
+               $this->assertSame( array(), $bp->getGrants() );
+
+               $bp = BotPassword::newUnsaved( array(
+                       'username' => 'UTDummy',
+                       'appId' => 'DoesNotExist2',
+                       'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
+                       'grants' => array( 'test' ),
+               ) );
+               $this->assertInstanceOf( 'BotPassword', $bp );
+               $this->assertFalse( $bp->isSaved() );
+               $this->assertSame( 43, $bp->getUserCentralId() );
+               $this->assertSame( 'DoesNotExist2', $bp->getAppId() );
+               $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
+               $this->assertSame( array( 'test' ), $bp->getGrants() );
+
+               $user = User::newFromName( 'UTSysop' );
+               $bp = BotPassword::newUnsaved( array(
+                       'centralId' => 45,
+                       'appId' => 'DoesNotExist'
+               ) );
+               $this->assertInstanceOf( 'BotPassword', $bp );
+               $this->assertFalse( $bp->isSaved() );
+               $this->assertSame( 45, $bp->getUserCentralId() );
+               $this->assertSame( 'DoesNotExist', $bp->getAppId() );
+
+               $user = User::newFromName( 'UTSysop' );
+               $bp = BotPassword::newUnsaved( array(
+                       'user' => $user,
+                       'appId' => 'BotPassword'
+               ) );
+               $this->assertInstanceOf( 'BotPassword', $bp );
+               $this->assertFalse( $bp->isSaved() );
+
+               $this->assertNull( BotPassword::newUnsaved( array(
+                       'user' => $user,
+                       'appId' => '',
+               ) ) );
+               $this->assertNull( BotPassword::newUnsaved( array(
+                       'user' => $user,
+                       'appId' => str_repeat( 'X', BotPassword::APPID_MAXLENGTH + 1 ),
+               ) ) );
+               $this->assertNull( BotPassword::newUnsaved( array(
+                       'user' => 'UTSysop',
+                       'appId' => 'Ok',
+               ) ) );
+               $this->assertNull( BotPassword::newUnsaved( array(
+                       'username' => 'UTInvalid',
+                       'appId' => 'Ok',
+               ) ) );
+               $this->assertNull( BotPassword::newUnsaved( array(
+                       'appId' => 'Ok',
+               ) ) );
+       }
+
+       public function testGetPassword() {
+               $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
+
+               $password = $bp->getPassword();
+               $this->assertInstanceOf( 'Password', $password );
+               $this->assertTrue( $password->equals( 'foobaz' ) );
+
+               $bp->centralId = 44;
+               $password = $bp->getPassword();
+               $this->assertInstanceOf( 'InvalidPassword', $password );
+
+               $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'bot_passwords',
+                       array( 'bp_password' => 'garbage' ),
+                       array( 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ),
+                       __METHOD__
+               );
+               $password = $bp->getPassword();
+               $this->assertInstanceOf( 'InvalidPassword', $password );
+       }
+
+       public function testInvalidateAllPasswordsForUser() {
+               $bp1 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
+               $bp2 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
+
+               $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' );
+               $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' );
+               BotPassword::invalidateAllPasswordsForUser( 'UTSysop' );
+               $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() );
+               $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() );
+
+               $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
+               $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() );
+       }
+
+       public function testRemoveAllPasswordsForUser() {
+               $this->assertNotNull( BotPassword::newFromCentralId( 42, 'BotPassword' ), 'sanity check' );
+               $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ), 'sanity check' );
+
+               BotPassword::removeAllPasswordsForUser( 'UTSysop' );
+
+               $this->assertNull( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
+               $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
+       }
+
+       public function testLogin() {
+               // Test failure when bot passwords aren't enabled
+               $this->setMwGlobals( 'wgEnableBotPasswords', false );
+               $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', new FauxRequest );
+               $this->assertEquals( Status::newFatal( 'botpasswords-disabled' ), $status );
+               $this->setMwGlobals( 'wgEnableBotPasswords', true );
+
+               // Test failure when BotPasswordSessionProvider isn't configured
+               $manager = new SessionManager( array(
+                       'logger' => new Psr\Log\NullLogger,
+                       'store' => new EmptyBagOStuff,
+               ) );
+               $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
+               $this->assertNull(
+                       $manager->getProvider( 'MediaWiki\\Session\\BotPasswordSessionProvider' ),
+                       'sanity check'
+               );
+               $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', new FauxRequest );
+               $this->assertEquals( Status::newFatal( 'botpasswords-no-provider' ), $status );
+               ScopedCallback::consume( $reset );
+
+               // Now configure BotPasswordSessionProvider for further tests...
+               $mainConfig = RequestContext::getMain()->getConfig();
+               $config = new HashConfig( array(
+                       'SessionProviders' => $mainConfig->get( 'SessionProviders' ) + array(
+                               'MediaWiki\\Session\\BotPasswordSessionProvider' => array(
+                                       'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider',
+                                       'args' => array( array( 'priority' => 40 ) ),
+                               )
+                       ),
+               ) );
+               $manager = new SessionManager( array(
+                       'config' => new MultiConfig( array( $config, RequestContext::getMain()->getConfig() ) ),
+                       'logger' => new Psr\Log\NullLogger,
+                       'store' => new EmptyBagOStuff,
+               ) );
+               $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
+
+               // No "@"-thing in the username
+               $status = BotPassword::login( 'UTSysop', 'foobaz', new FauxRequest );
+               $this->assertEquals( Status::newFatal( 'botpasswords-invalid-name', '@' ), $status );
+
+               // No base user
+               $status = BotPassword::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest );
+               $this->assertEquals( Status::newFatal( 'nosuchuser', 'UTDummy' ), $status );
+
+               // No bot password
+               $status = BotPassword::login( 'UTSysop@DoesNotExist', 'foobaz', new FauxRequest );
+               $this->assertEquals(
+                       Status::newFatal( 'botpasswords-not-exist', 'UTSysop', 'DoesNotExist' ),
+                       $status
+               );
+
+               // Failed restriction
+               $request = $this->getMock( 'FauxRequest', array( 'getIP' ) );
+               $request->expects( $this->any() )->method( 'getIP' )
+                       ->will( $this->returnValue( '10.0.0.1' ) );
+               $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', $request );
+               $this->assertEquals( Status::newFatal( 'botpasswords-restriction-failed' ), $status );
+
+               // Wrong password
+               $status = BotPassword::login( 'UTSysop@BotPassword', 'UTSysopPassword', new FauxRequest );
+               $this->assertEquals( Status::newFatal( 'wrongpassword' ), $status );
+
+               // Success!
+               $request = new FauxRequest;
+               $this->assertNotInstanceOf(
+                       'MediaWiki\\Session\\BotPasswordSessionProvider',
+                       $request->getSession()->getProvider(),
+                       'sanity check'
+               );
+               $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', $request );
+               $this->assertInstanceOf( 'Status', $status );
+               $this->assertTrue( $status->isGood() );
+               $session = $status->getValue();
+               $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+               $this->assertInstanceOf(
+                       'MediaWiki\\Session\\BotPasswordSessionProvider', $session->getProvider()
+               );
+               $this->assertSame( $session->getId(), $request->getSession()->getId() );
+
+               ScopedCallback::consume( $reset );
+       }
+
+       /**
+        * @dataProvider provideSave
+        * @param string|null $password
+        */
+       public function testSave( $password ) {
+               $passwordFactory = new \PasswordFactory();
+               $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
+               $passwordFactory->setDefaultType( 'A' );
+
+               $bp = BotPassword::newUnsaved( array(
+                       'centralId' => 42,
+                       'appId' => 'TestSave',
+                       'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
+                       'grants' => array( 'test' ),
+               ) );
+               $this->assertFalse( $bp->isSaved(), 'sanity check' );
+               $this->assertNull(
+                       BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check'
+               );
+
+               $pwhash = $password ? $passwordFactory->newFromPlaintext( $password ) : null;
+               $this->assertFalse( $bp->save( 'update', $pwhash ) );
+               $this->assertTrue( $bp->save( 'insert', $pwhash ) );
+               $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
+               $this->assertInstanceOf( 'BotPassword', $bp2 );
+               $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
+               $this->assertEquals( $bp->getAppId(), $bp2->getAppId() );
+               $this->assertEquals( $bp->getToken(), $bp2->getToken() );
+               $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() );
+               $this->assertEquals( $bp->getGrants(), $bp2->getGrants() );
+               $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
+               if ( $password === null ) {
+                       $this->assertInstanceOf( 'InvalidPassword', $pw );
+               } else {
+                       $this->assertTrue( $pw->equals( $password ) );
+               }
+
+               $token = $bp->getToken();
+               $this->assertFalse( $bp->save( 'insert' ) );
+               $this->assertTrue( $bp->save( 'update' ) );
+               $this->assertNotEquals( $token, $bp->getToken() );
+               $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
+               $this->assertInstanceOf( 'BotPassword', $bp2 );
+               $this->assertEquals( $bp->getToken(), $bp2->getToken() );
+               $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
+               if ( $password === null ) {
+                       $this->assertInstanceOf( 'InvalidPassword', $pw );
+               } else {
+                       $this->assertTrue( $pw->equals( $password ) );
+               }
+
+               $pwhash = $passwordFactory->newFromPlaintext( 'XXX' );
+               $token = $bp->getToken();
+               $this->assertTrue( $bp->save( 'update', $pwhash ) );
+               $this->assertNotEquals( $token, $bp->getToken() );
+               $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
+               $this->assertTrue( $pw->equals( 'XXX' ) );
+
+               $this->assertTrue( $bp->delete() );
+               $this->assertFalse( $bp->isSaved() );
+               $this->assertNull( BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ) );
+
+               $this->assertFalse( $bp->save( 'foobar' ) );
+       }
+
+       public static function provideSave() {
+               return array(
+                       array( null ),
+                       array( 'foobar' ),
+               );
+       }
+}
index 45c4b8c..aadc5c9 100644 (file)
@@ -446,89 +446,4 @@ class UserTest extends MediaWikiTestCase {
                $this->assertGreaterThan(
                        $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
        }
-
-       public static function setExtendedLoginCookieDataProvider() {
-               $data = array();
-               $now = time();
-
-               $secondsInDay = 86400;
-
-               // Arbitrary durations, in units of days, to ensure it chooses the
-               // right one.  There is a 5-minute grace period (see testSetExtendedLoginCookie)
-               // to work around slow tests, since we're not currently mocking time() for PHP.
-
-               $durationOne = $secondsInDay * 5;
-               $durationTwo = $secondsInDay * 29;
-               $durationThree = $secondsInDay * 17;
-
-               // If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
-               // set cookie is time() + $wgCookieExpiration
-               $data[] = array(
-                       null,
-                       $durationOne,
-                       $now + $durationOne,
-               );
-
-               // If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
-               // set cookie is $now + $wgExtendedLoginCookieExpiration
-               $data[] = array(
-                       $durationTwo,
-                       $durationThree,
-                       $now + $durationTwo,
-               );
-
-               return $data;
-       }
-
-       /**
-        * @dataProvider setExtendedLoginCookieDataProvider
-        * @covers User::getRequest
-        * @covers User::setCookie
-        * @backupGlobals enabled
-        */
-       public function testSetExtendedLoginCookie(
-               $extendedLoginCookieExpiration,
-               $cookieExpiration,
-               $expectedExpiry
-       ) {
-               $this->setMwGlobals( array(
-                       'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
-                       'wgCookieExpiration' => $cookieExpiration,
-               ) );
-
-               $response = $this->getMock( 'WebResponse' );
-               $setcookieSpy = $this->any();
-               $response->expects( $setcookieSpy )
-                       ->method( 'setcookie' );
-
-               $request = new MockWebRequest( $response );
-               $user = new UserProxy( User::newFromSession( $request ) );
-               $user->setExtendedLoginCookie( 'name', 'value', true );
-
-               $setcookieInvocations = $setcookieSpy->getInvocations();
-               $setcookieInvocation = end( $setcookieInvocations );
-               $actualExpiry = $setcookieInvocation->parameters[2];
-
-               // TODO: ± 600 seconds compensates for
-               // slow-running tests. However, the dependency on the time
-               // function should be removed.  This requires some way
-               // to mock/isolate User->setExtendedLoginCookie's call to time()
-               $this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 );
-       }
-}
-
-class UserProxy extends User {
-
-       /**
-        * @var User
-        */
-       protected $user;
-
-       public function __construct( User $user ) {
-               $this->user = $user;
-       }
-
-       public function setExtendedLoginCookie( $name, $value, $secure ) {
-               $this->user->setExtendedLoginCookie( $name, $value, $secure );
-       }
 }
diff --git a/tests/phpunit/includes/utils/MWGrantsTest.php b/tests/phpunit/includes/utils/MWGrantsTest.php
new file mode 100644 (file)
index 0000000..9d0d962
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+class MWGrantsTest extends MediaWikiTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgGrantPermissions' => array(
+                               'hidden1' => array( 'read' => true, 'autoconfirmed' => false ),
+                               'hidden2' => array( 'autoconfirmed' => true ),
+                               'normal' => array( 'edit' => true ),
+                               'normal2' => array( 'edit' => true, 'create' => true ),
+                               'admin' => array( 'protect' => true, 'delete' => true ),
+                       ),
+                       'wgGrantPermissionGroups' => array(
+                               'hidden1' => 'hidden',
+                               'hidden2' => 'hidden',
+                               'normal' => 'normal-group',
+                               'admin' => 'admin',
+                       ),
+               ) );
+       }
+
+       /**
+        * @covers MWGrants::getValidGrants
+        */
+       public function testGetValidGrants() {
+               $this->assertSame(
+                       array( 'hidden1', 'hidden2', 'normal', 'normal2', 'admin' ),
+                       MWGrants::getValidGrants()
+               );
+       }
+
+       /**
+        * @covers MWGrants::getRightsByGrant
+        */
+       public function testGetRightsByGrant() {
+               $this->assertSame(
+                       array(
+                               'hidden1' => array( 'read' ),
+                               'hidden2' => array( 'autoconfirmed' ),
+                               'normal' => array( 'edit' ),
+                               'normal2' => array( 'edit', 'create' ),
+                               'admin' => array( 'protect', 'delete' ),
+                       ),
+                       MWGrants::getRightsByGrant()
+               );
+       }
+
+       /**
+        * @dataProvider provideGetGrantRights
+        * @covers MWGrants::getGrantRights
+        * @param array|string $grants
+        * @param array $rights
+        */
+       public function testGetGrantRights( $grants, $rights ) {
+               $this->assertSame( $rights, MWGrants::getGrantRights( $grants ) );
+       }
+
+       public static function provideGetGrantRights() {
+               return array(
+                       array( 'hidden1', array( 'read' ) ),
+                       array( array( 'hidden1', 'hidden2', 'hidden3' ), array( 'read', 'autoconfirmed' ) ),
+                       array( array( 'normal1', 'normal2' ), array( 'edit', 'create' ) ),
+               );
+       }
+
+       /**
+        * @dataProvider provideGrantsAreValid
+        * @covers MWGrants::grantsAreValid
+        * @param array $grants
+        * @param bool $valid
+        */
+       public function testGrantsAreValid( $grants, $valid ) {
+               $this->assertSame( $valid, MWGrants::grantsAreValid( $grants ) );
+       }
+
+       public static function provideGrantsAreValid() {
+               return array(
+                       array( array( 'hidden1', 'hidden2' ), true ),
+                       array( array( 'hidden1', 'hidden3' ), false ),
+               );
+       }
+
+       /**
+        * @dataProvider provideGetGrantGroups
+        * @covers MWGrants::getGrantGroups
+        * @param array|null $grants
+        * @param array $expect
+        */
+       public function testGetGrantGroups( $grants, $expect ) {
+               $this->assertSame( $expect, MWGrants::getGrantGroups( $grants ) );
+       }
+
+       public static function provideGetGrantGroups() {
+               return array(
+                       array( null, array(
+                               'hidden' => array( 'hidden1', 'hidden2' ),
+                               'normal-group' => array( 'normal' ),
+                               'other' => array( 'normal2' ),
+                               'admin' => array( 'admin' ),
+                       ) ),
+                       array( array( 'hidden1', 'normal' ), array(
+                               'hidden' => array( 'hidden1' ),
+                               'normal-group' => array( 'normal' ),
+                       ) ),
+               );
+       }
+
+       /**
+        * @covers MWGrants::getHiddenGrants
+        */
+       public function testGetHiddenGrants() {
+               $this->assertSame( array( 'hidden1', 'hidden2' ), MWGrants::getHiddenGrants() );
+       }
+
+}
diff --git a/tests/phpunit/includes/utils/MWRestrictionsTest.php b/tests/phpunit/includes/utils/MWRestrictionsTest.php
new file mode 100644 (file)
index 0000000..66a1130
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+class MWRestrictionsTest extends PHPUnit_Framework_TestCase {
+
+       protected static $restrictionsForChecks;
+
+       public static function setUpBeforeClass() {
+               self::$restrictionsForChecks = MWRestrictions::newFromArray( array(
+                       'IPAddresses' => array(
+                               '10.0.0.0/8',
+                               '172.16.0.0/12',
+                               '2001:db8::/33',
+                       )
+               ) );
+       }
+
+       /**
+        * @covers MWRestrictions::newDefault
+        * @covers MWRestrictions::__construct
+        */
+       public function testNewDefault() {
+               $ret = MWRestrictions::newDefault();
+               $this->assertInstanceOf( 'MWRestrictions', $ret );
+               $this->assertSame(
+                       '{"IPAddresses":["0.0.0.0/0","::/0"]}',
+                       $ret->toJson()
+               );
+       }
+
+       /**
+        * @covers MWRestrictions::newFromArray
+        * @covers MWRestrictions::__construct
+        * @covers MWRestrictions::loadFromArray
+        * @covers MWRestrictions::toArray
+        * @dataProvider provideArray
+        * @param array $data
+        * @param bool|InvalidArgumentException $expect True if the call succeeds,
+        *  otherwise the exception that should be thrown.
+        */
+       public function testArray( $data, $expect ) {
+               if ( $expect === true ) {
+                       $ret = MWRestrictions::newFromArray( $data );
+                       $this->assertInstanceOf( 'MWRestrictions', $ret );
+                       $this->assertSame( $data, $ret->toArray() );
+               } else {
+                       try {
+                               MWRestrictions::newFromArray( $data );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $this->assertEquals( $expect, $ex );
+                       }
+               }
+       }
+
+       public static function provideArray() {
+               return array(
+                       array( array( 'IPAddresses' => array() ), true ),
+                       array( array( 'IPAddresses' => array( '127.0.0.1/32' ) ), true ),
+                       array(
+                               array( 'IPAddresses' => array( '256.0.0.1/32' ) ),
+                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
+                       ),
+                       array(
+                               array( 'IPAddresses' => '127.0.0.1/32' ),
+                               new InvalidArgumentException( 'IPAddresses is not an array' )
+                       ),
+                       array(
+                               array(),
+                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
+                       ),
+                       array(
+                               array( 'foo' => 'bar', 'bar' => 42 ),
+                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
+                       ),
+               );
+       }
+
+       /**
+        * @covers MWRestrictions::newFromJson
+        * @covers MWRestrictions::__construct
+        * @covers MWRestrictions::loadFromArray
+        * @covers MWRestrictions::toJson
+        * @covers MWRestrictions::__toString
+        * @dataProvider provideJson
+        * @param string $json
+        * @param array|InvalidArgumentException $expect
+        */
+       public function testJson( $json, $expect ) {
+               if ( is_array( $expect ) ) {
+                       $ret = MWRestrictions::newFromJson( $json );
+                       $this->assertInstanceOf( 'MWRestrictions', $ret );
+                       $this->assertSame( $expect, $ret->toArray() );
+
+                       $this->assertSame( $json, $ret->toJson( false ) );
+                       $this->assertSame( $json, (string)$ret );
+
+                       $this->assertSame(
+                               FormatJson::encode( $expect, true, FormatJson::ALL_OK ),
+                               $ret->toJson( true )
+                       );
+               } else {
+                       try {
+                               MWRestrictions::newFromJson( $json );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $this->assertTrue( true );
+                       }
+               }
+       }
+
+       public static function provideJson() {
+               return array(
+                       array(
+                               '{"IPAddresses":[]}',
+                               array( 'IPAddresses' => array() )
+                       ),
+                       array(
+                               '{"IPAddresses":["127.0.0.1/32"]}',
+                               array( 'IPAddresses' => array( '127.0.0.1/32' ) )
+                       ),
+                       array(
+                               '{"IPAddresses":["256.0.0.1/32"]}',
+                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
+                       ),
+                       array(
+                               '{"IPAddresses":"127.0.0.1/32"}',
+                               new InvalidArgumentException( 'IPAddresses is not an array' )
+                       ),
+                       array(
+                               '{}',
+                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
+                       ),
+                       array(
+                               '{"foo":"bar","bar":42}',
+                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
+                       ),
+                       array(
+                               '{"IPAddresses":[]',
+                               new InvalidArgumentException( 'Invalid restrictions JSON' )
+                       ),
+                       array(
+                               '"IPAddresses"',
+                               new InvalidArgumentException( 'Invalid restrictions JSON' )
+                       ),
+               );
+       }
+
+       /**
+        * @covers MWRestrictions::checkIP
+        * @dataProvider provideCheckIP
+        * @param string $ip
+        * @param bool $pass
+        */
+       public function testCheckIP( $ip, $pass ) {
+               $this->assertSame( $pass, self::$restrictionsForChecks->checkIP( $ip ) );
+       }
+
+       public static function provideCheckIP() {
+               return array(
+                       array( '10.0.0.1', true ),
+                       array( '172.16.0.0', true ),
+                       array( '192.0.2.1', false ),
+                       array( '2001:db8:1::', true ),
+                       array( '2001:0db8:0000:0000:0000:0000:0000:0000', true ),
+                       array( '2001:0DB8:8000::', false ),
+               );
+       }
+
+       /**
+        * @covers MWRestrictions::check
+        * @dataProvider provideCheck
+        * @param WebRequest $request
+        * @param Status $expect
+        */
+       public function testCheck( $request, $expect ) {
+               $this->assertEquals( $expect, self::$restrictionsForChecks->check( $request ) );
+       }
+
+       public function provideCheck() {
+               $ret = array();
+
+               $mockBuilder = $this->getMockBuilder( 'FauxRequest' )
+                       ->setMethods( array( 'getIP' ) );
+
+               foreach ( self::provideCheckIP() as $checkIP ) {
+                       $ok = array();
+                       $request = $mockBuilder->getMock();
+
+                       $request->expects( $this->any() )->method( 'getIP' )
+                               ->will( $this->returnValue( $checkIP[0] ) );
+                       $ok['ip'] = $checkIP[1];
+
+                       /* If we ever add more restrictions, add nested for loops here:
+                        *  foreach ( self::provideCheckFoo() as $checkFoo ) {
+                        *      $request->expects( $this->any() )->method( 'getFoo' )
+                        *          ->will( $this->returnValue( $checkFoo[0] );
+                        *      $ok['foo'] = $checkFoo[1];
+                        *
+                        *      foreach ( self::provideCheckBar() as $checkBar ) {
+                        *          $request->expects( $this->any() )->method( 'getBar' )
+                        *              ->will( $this->returnValue( $checkBar[0] );
+                        *          $ok['bar'] = $checkBar[1];
+                        *
+                        *          // etc.
+                        *      }
+                        *  }
+                        */
+
+                       $status = Status::newGood();
+                       $status->setResult( $ok === array_filter( $ok ), $ok );
+                       $ret[] = array( $request, $status );
+               }
+
+               return $ret;
+       }
+}
diff --git a/tests/phpunit/mocks/session/DummySessionBackend.php b/tests/phpunit/mocks/session/DummySessionBackend.php
new file mode 100644 (file)
index 0000000..f96e61c
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * Dummy session backend
+ *
+ * This isn't a real backend, but implements some methods that SessionBackend
+ * does so tests can run.
+ */
+class DummySessionBackend {
+       public $data = array(
+               'foo' => 1,
+               'bar' => 2,
+               0 => 'zero',
+       );
+       public $dirty = false;
+
+       public function &getData() {
+               return $this->data;
+       }
+
+       public function dirty() {
+               $this->dirty = true;
+       }
+
+       public function deregisterSession( $index ) {
+       }
+}
diff --git a/tests/phpunit/mocks/session/DummySessionProvider.php b/tests/phpunit/mocks/session/DummySessionProvider.php
new file mode 100644 (file)
index 0000000..4468191
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+use MediaWiki\Session\SessionProvider;
+use MediaWiki\Session\SessionInfo;
+use MediaWiki\Session\SessionBackend;
+use MediaWiki\Session\UserInfo;
+
+/**
+ * Dummy session provider
+ *
+ * An implementation of a session provider that doesn't actually do anything.
+ */
+class DummySessionProvider extends SessionProvider {
+
+       const ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+       public function provideSessionInfo( WebRequest $request ) {
+               return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'provider' => $this,
+                       'id' => self::ID,
+                       'persisted' => true,
+                       'userInfo' => UserInfo::newAnonymous(),
+               ) );
+       }
+
+       public function newSessionInfo( $id = null ) {
+               return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+                       'id' => $id,
+                       'idIsSafe' => true,
+                       'provider' => $this,
+                       'persisted' => false,
+                       'userInfo' => UserInfo::newAnonymous(),
+               ) );
+       }
+
+       public function persistsSessionId() {
+               return true;
+       }
+
+       public function canChangeUser() {
+               return $this->persistsSessionId();
+       }
+
+       public function persistSession( SessionBackend $session, WebRequest $request ) {
+       }
+
+       public function unpersistSession( WebRequest $request ) {
+       }
+
+       public function immutableSessionCouldExistForUser( $user ) {
+               return false;
+       }
+
+       public function preventImmutableSessionsForUser( $user ) {
+       }
+
+       public function suggestLoginUsername( WebRequest $request ) {
+               return $request->getCookie( 'UserName' );
+       }
+
+}
index 09dc931..0ae0b21 100755 (executable)
@@ -70,9 +70,12 @@ class PHPUnitMaintClass extends Maintenance {
                parent::finalSetup();
 
                global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgMainWANCache;
+               global $wgMainStash;
                global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
                global $wgLocaltimezone, $wgLocalisationCacheConf;
                global $wgDevelopmentWarnings;
+               global $wgSessionProviders;
+               global $wgJobTypeConf;
 
                // Inject test autoloader
                require_once __DIR__ . '/../TestsAutoLoader.php';
@@ -95,6 +98,10 @@ class PHPUnitMaintClass extends Maintenance {
                $wgLanguageConverterCacheType = 'hash';
                // Uses db-replicated in DefaultSettings
                $wgMainStash = 'hash';
+               // Use memory job queue
+               $wgJobTypeConf = array(
+                       'default' => array( 'class' => 'JobQueueMemory', 'order' => 'fifo' ),
+               );
 
                $wgUseDatabaseMessages = false; # Set for future resets
 
@@ -103,6 +110,19 @@ class PHPUnitMaintClass extends Maintenance {
 
                $wgLocalisationCacheConf['storeClass'] = 'LCStoreNull';
 
+               // Generic MediaWiki\Session\SessionManager configuration for tests
+               // We use CookieSessionProvider because things might be expecting
+               // cookies to show up in a FauxRequest somewhere.
+               $wgSessionProviders = array(
+                       array(
+                               'class' => 'MediaWiki\\Session\\CookieSessionProvider',
+                               'args' => array( array(
+                                       'priority' => 30,
+                                       'callUserSetCookiesHook' => true,
+                               ) ),
+                       ),
+               );
+
                // Bug 44192 Do not attempt to send a real e-mail
                Hooks::clear( 'AlternateUserMailer' );
                Hooks::register(