Merge "Let BagOStuff::merge() callbacks override the TTL"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 17 May 2016 09:33:16 +0000 (09:33 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 17 May 2016 09:33:16 +0000 (09:33 +0000)
433 files changed:
RELEASE-NOTES-1.27
autoload.php
docs/extension.schema.json
docs/hooks.txt
includes/AuthPlugin.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/Html.php
includes/MediaWikiServices.php
includes/Preferences.php
includes/ServiceWiring.php
includes/Setup.php
includes/Status.php
includes/Title.php
includes/WatchedItem.php
includes/api/ApiAMCreateAccount.php [new file with mode: 0644]
includes/api/ApiAuthManagerHelper.php [new file with mode: 0644]
includes/api/ApiBase.php
includes/api/ApiChangeAuthenticationData.php [new file with mode: 0644]
includes/api/ApiClientLogin.php [new file with mode: 0644]
includes/api/ApiCreateAccount.php
includes/api/ApiFormatJson.php
includes/api/ApiLinkAccount.php [new file with mode: 0644]
includes/api/ApiLogin.php
includes/api/ApiMain.php
includes/api/ApiManageTags.php
includes/api/ApiPageSet.php
includes/api/ApiQuery.php
includes/api/ApiQueryAuthManagerInfo.php [new file with mode: 0644]
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryUsers.php
includes/api/ApiRemoveAuthenticationData.php [new file with mode: 0644]
includes/api/ApiResetPassword.php [new file with mode: 0644]
includes/api/ApiStashEdit.php
includes/api/i18n/ba.json
includes/api/i18n/en.json
includes/api/i18n/id.json
includes/api/i18n/jv.json
includes/api/i18n/ko.json
includes/api/i18n/qqq.json
includes/api/i18n/sv.json
includes/auth/AbstractAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AbstractPreAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AbstractPrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AbstractSecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AuthManager.php [new file with mode: 0644]
includes/auth/AuthManagerAuthPlugin.php [new file with mode: 0644]
includes/auth/AuthPluginPrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/AuthenticationProvider.php [new file with mode: 0644]
includes/auth/AuthenticationRequest.php [new file with mode: 0644]
includes/auth/AuthenticationResponse.php [new file with mode: 0644]
includes/auth/ButtonAuthenticationRequest.php [new file with mode: 0644]
includes/auth/CheckBlocksSecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/ConfirmLinkAuthenticationRequest.php [new file with mode: 0644]
includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/CreateFromLoginAuthenticationRequest.php [new file with mode: 0644]
includes/auth/CreatedAccountAuthenticationRequest.php [new file with mode: 0644]
includes/auth/CreationReasonAuthenticationRequest.php [new file with mode: 0644]
includes/auth/EmailNotificationSecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/LegacyHookPreAuthenticationProvider.php [new file with mode: 0644]
includes/auth/LocalPasswordPrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/PasswordAuthenticationRequest.php [new file with mode: 0644]
includes/auth/PasswordDomainAuthenticationRequest.php [new file with mode: 0644]
includes/auth/PreAuthenticationProvider.php [new file with mode: 0644]
includes/auth/PrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/RememberMeAuthenticationRequest.php [new file with mode: 0644]
includes/auth/ResetPasswordSecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/SecondaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/TemporaryPasswordAuthenticationRequest.php [new file with mode: 0644]
includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php [new file with mode: 0644]
includes/auth/ThrottlePreAuthenticationProvider.php [new file with mode: 0644]
includes/auth/Throttler.php [new file with mode: 0644]
includes/auth/UserDataAuthenticationRequest.php [new file with mode: 0644]
includes/auth/UsernameAuthenticationRequest.php [new file with mode: 0644]
includes/cache/LinkBatch.php
includes/cache/LinkCache.php
includes/changetags/ChangeTags.php
includes/collation/IcuCollation.php
includes/deferred/CdnCacheUpdate.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/exception/UserNotLoggedIn.php
includes/filerepo/file/LocalFile.php
includes/htmlform/OOUIHTMLForm.php
includes/installer/i18n/jv.json
includes/installer/i18n/ko.json
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/AssembleUploadChunksJob.php
includes/jobqueue/jobs/PublishStashedFileJob.php
includes/libs/Xhprof.php
includes/libs/XhprofData.php [new file with mode: 0644]
includes/libs/objectcache/WANObjectCache.php
includes/linker/LinkTarget.php
includes/objectcache/RedisBagOStuff.php
includes/parser/CoreParserFunctions.php
includes/parser/LinkHolderArray.php
includes/parser/StripState.php
includes/password/PasswordPolicyChecks.php
includes/profiler/ProfilerXhprof.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderUploadDialogModule.php [new file with mode: 0644]
includes/resourceloader/ResourceLoaderUserGroupsModule.php
includes/resourceloader/ResourceLoaderUserModule.php
includes/session/CookieSessionProvider.php
includes/session/ImmutableSessionProviderWithCookie.php
includes/session/SessionManager.php
includes/session/SessionProvider.php
includes/skins/SkinTemplate.php
includes/specialpage/AuthManagerSpecialPage.php [new file with mode: 0644]
includes/specialpage/LoginSignupSpecialPage.php [new file with mode: 0644]
includes/specialpage/SpecialPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialChangeCredentials.php [new file with mode: 0644]
includes/specials/SpecialChangeEmail.php
includes/specials/SpecialChangePassword.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialLinkAccounts.php [new file with mode: 0644]
includes/specials/SpecialPasswordReset.php
includes/specials/SpecialRemoveCredentials.php [new file with mode: 0644]
includes/specials/SpecialTags.php
includes/specials/SpecialUnlinkAccounts.php [new file with mode: 0644]
includes/specials/SpecialUserLogin.php [new file with mode: 0644]
includes/specials/SpecialUserLogout.php [new file with mode: 0644]
includes/specials/SpecialUserlogin.php [deleted file]
includes/specials/SpecialUserlogout.php [deleted file]
includes/specials/SpecialUserrights.php
includes/specials/helpers/LoginHelper.php [new file with mode: 0644]
includes/specials/pre-authmanager/README [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialChangeEmail.php [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialChangePassword.php [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialCreateAccount.php [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialPasswordReset.php [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialUserlogin.php [new file with mode: 0644]
includes/specials/pre-authmanager/SpecialUserlogout.php [new file with mode: 0644]
includes/templates/Usercreate.php
includes/templates/Userlogin.php
includes/title/MediaWikiTitleCodec.php
includes/title/TitleFormatter.php
includes/title/TitleValue.php
includes/user/PasswordReset.php [new file with mode: 0644]
includes/user/User.php
languages/i18n/ace.json
languages/i18n/af.json
languages/i18n/aln.json
languages/i18n/am.json
languages/i18n/an.json
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/arq.json
languages/i18n/ary.json
languages/i18n/arz.json
languages/i18n/as.json
languages/i18n/ast.json
languages/i18n/avk.json
languages/i18n/awa.json
languages/i18n/az.json
languages/i18n/azb.json
languages/i18n/ba.json
languages/i18n/bcc.json
languages/i18n/bcl.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bgn.json
languages/i18n/bho.json
languages/i18n/bjn.json
languages/i18n/bn.json
languages/i18n/bpy.json
languages/i18n/br.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/cdo.json
languages/i18n/ce.json
languages/i18n/ceb.json
languages/i18n/ckb.json
languages/i18n/crh-cyrl.json
languages/i18n/crh-latn.json
languages/i18n/cs.json
languages/i18n/csb.json
languages/i18n/cy.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/dsb.json
languages/i18n/dtp.json
languages/i18n/dty.json
languages/i18n/egl.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/ext.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fo.json
languages/i18n/fr.json
languages/i18n/frp.json
languages/i18n/frr.json
languages/i18n/fur.json
languages/i18n/fy.json
languages/i18n/ga.json
languages/i18n/gd.json
languages/i18n/gl.json
languages/i18n/gom-deva.json
languages/i18n/grc.json
languages/i18n/gsw.json
languages/i18n/gu.json
languages/i18n/gv.json
languages/i18n/hak.json
languages/i18n/haw.json
languages/i18n/he.json
languages/i18n/hi.json
languages/i18n/hif-latn.json
languages/i18n/hil.json
languages/i18n/hr.json
languages/i18n/hrx.json
languages/i18n/hsb.json
languages/i18n/ht.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/ilo.json
languages/i18n/inh.json
languages/i18n/io.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jam.json
languages/i18n/jv.json
languages/i18n/ka.json
languages/i18n/kaa.json
languages/i18n/kab.json
languages/i18n/kbd-cyrl.json
languages/i18n/khw.json
languages/i18n/kiu.json
languages/i18n/kk-cyrl.json
languages/i18n/km.json
languages/i18n/kn.json
languages/i18n/ko.json
languages/i18n/krc.json
languages/i18n/ksh.json
languages/i18n/ku-latn.json
languages/i18n/ky.json
languages/i18n/la.json
languages/i18n/lb.json
languages/i18n/lg.json
languages/i18n/li.json
languages/i18n/lij.json
languages/i18n/lki.json
languages/i18n/lmo.json
languages/i18n/lt.json
languages/i18n/lus.json
languages/i18n/lv.json
languages/i18n/lzh.json
languages/i18n/mai.json
languages/i18n/map-bms.json
languages/i18n/mdf.json
languages/i18n/mg.json
languages/i18n/mhr.json
languages/i18n/min.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mn.json
languages/i18n/mr.json
languages/i18n/ms.json
languages/i18n/mt.json
languages/i18n/mwl.json
languages/i18n/myv.json
languages/i18n/nah.json
languages/i18n/nan.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nds.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/nso.json
languages/i18n/oc.json
languages/i18n/or.json
languages/i18n/os.json
languages/i18n/pa.json
languages/i18n/pl.json
languages/i18n/pms.json
languages/i18n/pnb.json
languages/i18n/pnt.json
languages/i18n/prg.json
languages/i18n/ps.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/qu.json
languages/i18n/rm.json
languages/i18n/ro.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/rue.json
languages/i18n/sa.json
languages/i18n/sah.json
languages/i18n/sc.json
languages/i18n/scn.json
languages/i18n/sco.json
languages/i18n/sd.json
languages/i18n/ses.json
languages/i18n/sgs.json
languages/i18n/sh.json
languages/i18n/shi.json
languages/i18n/si.json
languages/i18n/sk.json
languages/i18n/sl.json
languages/i18n/sli.json
languages/i18n/so.json
languages/i18n/sq.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/stq.json
languages/i18n/su.json
languages/i18n/sv.json
languages/i18n/sw.json
languages/i18n/szl.json
languages/i18n/ta.json
languages/i18n/tcy.json
languages/i18n/te.json
languages/i18n/tg-cyrl.json
languages/i18n/tg-latn.json
languages/i18n/th.json
languages/i18n/tk.json
languages/i18n/tl.json
languages/i18n/to.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/tt-latn.json
languages/i18n/ug-arab.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/uz.json
languages/i18n/vec.json
languages/i18n/vep.json
languages/i18n/vi.json
languages/i18n/vo.json
languages/i18n/vro.json
languages/i18n/wa.json
languages/i18n/war.json
languages/i18n/wuu.json
languages/i18n/xal.json
languages/i18n/xmf.json
languages/i18n/yi.json
languages/i18n/yo.json
languages/i18n/yue.json
languages/i18n/zea.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesEn.php
maintenance/updateCollation.php
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.view.metadata.js
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js
resources/src/mediawiki/mediawiki.ForeignUpload.js
resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
resources/src/mediawiki/mediawiki.htmlform.ooui.css
resources/src/mediawiki/mediawiki.jqueryMsg.js
resources/src/mediawiki/mediawiki.util.js
resources/src/moment-dmy.js [new file with mode: 0644]
resources/src/moment-locale-overrides.js
tests/TestsAutoLoader.php
tests/parser/parserTest.inc
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/LinkerTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WatchedItemIntegrationTest.php
tests/phpunit/includes/XmlSelectTest.php
tests/phpunit/includes/XmlTest.php
tests/phpunit/includes/api/ApiCreateAccountTest.php [deleted file]
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthManagerTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthenticationRequestTestCase.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthenticationResponseTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/ThrottlerTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php [new file with mode: 0644]
tests/phpunit/includes/content/ContentHandlerTest.php
tests/phpunit/includes/content/JsonContentTest.php
tests/phpunit/includes/libs/XhprofDataTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/XhprofTest.php
tests/phpunit/includes/objectcache/RedisBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/NewParserTest.php
tests/phpunit/includes/session/CookieSessionProviderTest.php
tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php
tests/phpunit/includes/session/SessionManagerTest.php
tests/phpunit/includes/session/SessionProviderTest.php
tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
tests/phpunit/includes/title/TitleValueTest.php
tests/phpunit/includes/user/PasswordResetTest.php [new file with mode: 0644]
tests/phpunit/phpunit.php
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js

index 3a0326e..7c50e4f 100644 (file)
@@ -117,8 +117,30 @@ The following PHP extensions are strongly recommended:
 * Removed configuration option $wgCopyrightIcon (deprecated since 1.18). Use
   $wgFooterIcons['copyright']['copyright'] instead.
 * If the openssl and mcrypt PHP extensions are both unavailable, secure
-  session storage (soon to be used for login) will raise an exception. This
-  exception may be bypassed by setting $wgSessionInsecureSecrets = true.
+  session storage (used for login) will raise an exception. This exception may
+  be bypassed by setting $wgSessionInsecureSecrets = true.
+* Massive overhaul to authentication:
+** AuthPlugin and AuthPluginUser are deprecated.
+** LoginForm and associated templates are deprecated. Extensions which called
+   static LoginForm methods should be converted into authentication providers.
+** The following hooks are deprecated:
+*** AbortAutoAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AbortLogin (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AbortNewAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AddNewAccount (use LocalUserCreated instead)
+*** AuthPluginSetup (create a MediaWiki\Auth\PrimaryAuthenticationProvider instead)
+*** ChangePasswordForm (use AuthChangeFormFields instead, or security levels)
+*** LoginUserMigrated (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** UserCreateForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
+*** UserLoginForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
+** The following hooks are removed:
+*** AbortChangePassword
+*** LoginPasswordResetMessage
+*** PrefsPasswordAudit
+** The UserLoginComplete hook will no longer be called for all logins, only for
+   those via the web UI. Use UserLoggedIn if you need to do something on all
+   logins.
+** $wgRequirePasswordforEmailChange is removed.
 
 === New features in 1.27 ===
 * $wgDataCenterUpdateStickTTL was also added. This decides how long a user
@@ -198,6 +220,27 @@ The following PHP extensions are strongly recommended:
 * $wgJpegPixelFormat was added to override chroma subsampling for JPEG image
   thumbnails created via ImageMagick. Defaults to 'yuv420', providing bandwidth
   savings versus the previous behavior on many files.
+* MediaWiki\Auth infrastructure (called "AuthManager") allows for more flexible
+  configuration of multiple authentication pieces that was possible with
+  AuthPlugin. For example, it's now easy to plug in second-factor
+  authentication, or add additional checks to the login process, or to support
+  multiple login methods at once, or to support non-password-based login methods.
+** Providers are configured via the global setting $wgAuthManagerConfig.
+** A global, $wgDisableAuthManager, is temporarily available to disable
+   AuthManager until extensions are ready to support it.
+** New hook, AuthChangeFormFields, to adjust the form fields on
+   AuthManager-related special pages.
+** New hook, AuthManagerLoginAuthenticateAudit, for additional logging of
+   AuthManager-related authentication requests.
+** New hook, ChangeAuthenticationDataAudit, for additional logging of
+   AuthManager-related authentication data changes.
+** New hook, SecuritySensitiveOperationStatus, to work with the new mechanism
+   for requiring a recent login before taking security-sensitive operations
+   like changing a password.
+** Two new globals, $wgChangeCredentialsBlacklist and $wgRemoveCredentialsBlacklist
+   can be used to prevent the web UI and the API changing certain authentication data.
+* The file upload dialog (available if you install WikiEditor or VisualEditor)
+  can now be configured using $wgUploadDialog.
 
 === External library changes in 1.27 ===
 
@@ -239,6 +282,18 @@ The following PHP extensions are strongly recommended:
   merely need to change the username and password used after setting up a bot
   password.
 * action=upload no longer understands statuskey, asyncdownload or leavemessage.
+* Several changes when $wgDisableAuthManager is false:
+** action=login is deprecated for uses other than bot passwords.
+** list=users can now indicate if a missing username is creatable.
+** action=createaccount is changed in a non-backwards-compatible manner.
+** Added action=query&meta=authmanagerinfo.
+** Added action=clientlogin to be used to log into the main account instead of
+   action=login.
+** Added action=linkaccount.
+** Added action=unlinkaccount.
+** Added action=changeauthenticationdata.
+** Added action=removeauthenticationdata.
+** Added action=resetpassword.
 
 === Action API internal changes in 1.27 ===
 * ApiQueryORM removed.
@@ -271,6 +326,7 @@ The following PHP extensions are strongly recommended:
 * ApiMain::addFormat() was removed (deprecated in 1.21).
 * ApiMain::getFormats() was removed (deprecated in 1.21).
 * ApiPageSet::finishPageSetGeneration() was removed (deprecated in 1.21).
+* ApiCreateAccount is deprecated, and will be removed soon.
 
 === Languages updated in 1.27 ===
 
@@ -475,6 +531,11 @@ changes to languages because of Phabricator reports.
   performance on complex changes. $wgExternalDiffEngine = 'wikidiff3' therefore
   makes no difference now. Users are still recommended to use wikidiff2 if possible,
   though.
+* User::addNewUserLogEntry() was deprecated.
+* User::addNewUserLogEntryAutoCreate() was deprecated.
+* User::isPasswordReminderThrottled() was deprecated.
+* Bot-oriented parameters to Special:UserLogin (wpCookieCheck, wpSkipCookieCheck)
+  were removed.
 
 == Compatibility ==
 
index 1e656e4..f79bace 100644 (file)
@@ -17,10 +17,14 @@ $wgAutoloadLocalClasses = [
        'AlterSharedConstraints' => __DIR__ . '/maintenance/oracle/alterSharedConstraints.php',
        'AncientPagesPage' => __DIR__ . '/includes/specials/SpecialAncientpages.php',
        'AnsiTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php',
+       'ApiAMCreateAccount' => __DIR__ . '/includes/api/ApiAMCreateAccount.php',
+       'ApiAuthManagerHelper' => __DIR__ . '/includes/api/ApiAuthManagerHelper.php',
        'ApiBase' => __DIR__ . '/includes/api/ApiBase.php',
        'ApiBlock' => __DIR__ . '/includes/api/ApiBlock.php',
+       'ApiChangeAuthenticationData' => __DIR__ . '/includes/api/ApiChangeAuthenticationData.php',
        'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php',
        'ApiClearHasMsg' => __DIR__ . '/includes/api/ApiClearHasMsg.php',
+       'ApiClientLogin' => __DIR__ . '/includes/api/ApiClientLogin.php',
        'ApiComparePages' => __DIR__ . '/includes/api/ApiComparePages.php',
        'ApiContinuationManager' => __DIR__ . '/includes/api/ApiContinuationManager.php',
        'ApiCreateAccount' => __DIR__ . '/includes/api/ApiCreateAccount.php',
@@ -48,6 +52,7 @@ $wgAutoloadLocalClasses = [
        'ApiImageRotate' => __DIR__ . '/includes/api/ApiImageRotate.php',
        'ApiImport' => __DIR__ . '/includes/api/ApiImport.php',
        'ApiImportReporter' => __DIR__ . '/includes/api/ApiImport.php',
+       'ApiLinkAccount' => __DIR__ . '/includes/api/ApiLinkAccount.php',
        'ApiLogin' => __DIR__ . '/includes/api/ApiLogin.php',
        'ApiLogout' => __DIR__ . '/includes/api/ApiLogout.php',
        'ApiMain' => __DIR__ . '/includes/api/ApiMain.php',
@@ -75,6 +80,7 @@ $wgAutoloadLocalClasses = [
        'ApiQueryAllPages' => __DIR__ . '/includes/api/ApiQueryAllPages.php',
        'ApiQueryAllRevisions' => __DIR__ . '/includes/api/ApiQueryAllRevisions.php',
        'ApiQueryAllUsers' => __DIR__ . '/includes/api/ApiQueryAllUsers.php',
+       'ApiQueryAuthManagerInfo' => __DIR__ . '/includes/api/ApiQueryAuthManagerInfo.php',
        'ApiQueryBacklinks' => __DIR__ . '/includes/api/ApiQueryBacklinks.php',
        'ApiQueryBacklinksprop' => __DIR__ . '/includes/api/ApiQueryBacklinksprop.php',
        'ApiQueryBase' => __DIR__ . '/includes/api/ApiQueryBase.php',
@@ -123,6 +129,8 @@ $wgAutoloadLocalClasses = [
        'ApiQueryWatchlist' => __DIR__ . '/includes/api/ApiQueryWatchlist.php',
        'ApiQueryWatchlistRaw' => __DIR__ . '/includes/api/ApiQueryWatchlistRaw.php',
        'ApiRawMessage' => __DIR__ . '/includes/api/ApiMessage.php',
+       'ApiRemoveAuthenticationData' => __DIR__ . '/includes/api/ApiRemoveAuthenticationData.php',
+       'ApiResetPassword' => __DIR__ . '/includes/api/ApiResetPassword.php',
        'ApiResult' => __DIR__ . '/includes/api/ApiResult.php',
        'ApiRevisionDelete' => __DIR__ . '/includes/api/ApiRevisionDelete.php',
        'ApiRollback' => __DIR__ . '/includes/api/ApiRollback.php',
@@ -145,6 +153,7 @@ $wgAutoloadLocalClasses = [
        'AtomFeed' => __DIR__ . '/includes/Feed.php',
        'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
        'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
+       'AuthManagerSpecialPage' => __DIR__ . '/includes/specialpage/AuthManagerSpecialPage.php',
        'AuthPlugin' => __DIR__ . '/includes/AuthPlugin.php',
        'AuthPluginUser' => __DIR__ . '/includes/AuthPlugin.php',
        'AutoLoader' => __DIR__ . '/includes/AutoLoader.php',
@@ -421,6 +430,7 @@ $wgAutoloadLocalClasses = [
        'FSFileOpHandle' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
        'FSLockManager' => __DIR__ . '/includes/filebackend/lockmanager/FSLockManager.php',
        'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php',
+       'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
        'FakeConverter' => __DIR__ . '/languages/FakeConverter.php',
        'FakeMaintenance' => __DIR__ . '/maintenance/Maintenance.php',
        'FakeResultWrapper' => __DIR__ . '/includes/db/DatabaseUtility.php',
@@ -730,7 +740,11 @@ $wgAutoloadLocalClasses = [
        'LogPager' => __DIR__ . '/includes/logging/LogPager.php',
        'LoggedOutEditToken' => __DIR__ . '/includes/user/LoggedOutEditToken.php',
        'LoggedUpdateMaintenance' => __DIR__ . '/maintenance/Maintenance.php',
-       'LoginForm' => __DIR__ . '/includes/specials/SpecialUserlogin.php',
+       'LoginForm' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
+       'LoginFormAuthManager' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
+       'LoginFormPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialUserlogin.php',
+       'LoginHelper' => __DIR__ . '/includes/specials/helpers/LoginHelper.php',
+       'LoginSignupSpecialPage' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
        'LonelyPagesPage' => __DIR__ . '/includes/specials/SpecialLonelypages.php',
        'LongPagesPage' => __DIR__ . '/includes/specials/SpecialLongpages.php',
        'MIMEsearchPage' => __DIR__ . '/includes/specials/SpecialMIMEsearch.php',
@@ -779,6 +793,41 @@ $wgAutoloadLocalClasses = [
        'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
        'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
        'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
+       'MediaWiki\\Auth\\AbstractAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AbstractPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AbstractPreAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPreAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AbstractPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AbstractSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractSecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AuthManager' => __DIR__ . '/includes/auth/AuthManager.php',
+       'MediaWiki\\Auth\\AuthManagerAuthPlugin' => __DIR__ . '/includes/auth/AuthManagerAuthPlugin.php',
+       'MediaWiki\\Auth\\AuthManagerAuthPluginUser' => __DIR__ . '/includes/auth/AuthManagerAuthPlugin.php',
+       'MediaWiki\\Auth\\AuthPluginPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AuthPluginPrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\AuthenticationProvider' => __DIR__ . '/includes/auth/AuthenticationProvider.php',
+       'MediaWiki\\Auth\\AuthenticationRequest' => __DIR__ . '/includes/auth/AuthenticationRequest.php',
+       'MediaWiki\\Auth\\AuthenticationResponse' => __DIR__ . '/includes/auth/AuthenticationResponse.php',
+       'MediaWiki\\Auth\\ButtonAuthenticationRequest' => __DIR__ . '/includes/auth/ButtonAuthenticationRequest.php',
+       'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\ConfirmLinkAuthenticationRequest' => __DIR__ . '/includes/auth/ConfirmLinkAuthenticationRequest.php',
+       'MediaWiki\\Auth\\ConfirmLinkSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\CreateFromLoginAuthenticationRequest' => __DIR__ . '/includes/auth/CreateFromLoginAuthenticationRequest.php',
+       'MediaWiki\\Auth\\CreatedAccountAuthenticationRequest' => __DIR__ . '/includes/auth/CreatedAccountAuthenticationRequest.php',
+       'MediaWiki\\Auth\\CreationReasonAuthenticationRequest' => __DIR__ . '/includes/auth/CreationReasonAuthenticationRequest.php',
+       'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\LegacyHookPreAuthenticationProvider' => __DIR__ . '/includes/auth/LegacyHookPreAuthenticationProvider.php',
+       'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\PasswordAuthenticationRequest' => __DIR__ . '/includes/auth/PasswordAuthenticationRequest.php',
+       'MediaWiki\\Auth\\PasswordDomainAuthenticationRequest' => __DIR__ . '/includes/auth/PasswordDomainAuthenticationRequest.php',
+       'MediaWiki\\Auth\\PreAuthenticationProvider' => __DIR__ . '/includes/auth/PreAuthenticationProvider.php',
+       'MediaWiki\\Auth\\PrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/PrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\RememberMeAuthenticationRequest' => __DIR__ . '/includes/auth/RememberMeAuthenticationRequest.php',
+       'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\SecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/SecondaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest' => __DIR__ . '/includes/auth/TemporaryPasswordAuthenticationRequest.php',
+       'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php',
+       'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => __DIR__ . '/includes/auth/ThrottlePreAuthenticationProvider.php',
+       'MediaWiki\\Auth\\Throttler' => __DIR__ . '/includes/auth/Throttler.php',
+       'MediaWiki\\Auth\\UserDataAuthenticationRequest' => __DIR__ . '/includes/auth/UserDataAuthenticationRequest.php',
+       'MediaWiki\\Auth\\UsernameAuthenticationRequest' => __DIR__ . '/includes/auth/UsernameAuthenticationRequest.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
        'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
@@ -962,6 +1011,7 @@ $wgAutoloadLocalClasses = [
        'PasswordError' => __DIR__ . '/includes/password/PasswordError.php',
        'PasswordFactory' => __DIR__ . '/includes/password/PasswordFactory.php',
        'PasswordPolicyChecks' => __DIR__ . '/includes/password/PasswordPolicyChecks.php',
+       'PasswordReset' => __DIR__ . '/includes/user/PasswordReset.php',
        'PatchSql' => __DIR__ . '/maintenance/patchSql.php',
        'PathRouter' => __DIR__ . '/includes/PathRouter.php',
        'PathRouterPatternReplacer' => __DIR__ . '/includes/PathRouter.php',
@@ -1093,6 +1143,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderSkinModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSkinModule.php',
        'ResourceLoaderSpecialCharacterDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php',
        'ResourceLoaderStartUpModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderStartUpModule.php',
+       'ResourceLoaderUploadDialogModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUploadDialogModule.php',
        'ResourceLoaderUserCSSPrefsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php',
        'ResourceLoaderUserDefaultsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserDefaultsModule.php',
        'ResourceLoaderUserGroupsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserGroupsModule.php',
@@ -1201,11 +1252,15 @@ $wgAutoloadLocalClasses = [
        'SpecialCachedPage' => __DIR__ . '/includes/specials/SpecialCachedPage.php',
        'SpecialCategories' => __DIR__ . '/includes/specials/SpecialCategories.php',
        'SpecialChangeContentModel' => __DIR__ . '/includes/specials/SpecialChangeContentModel.php',
+       'SpecialChangeCredentials' => __DIR__ . '/includes/specials/SpecialChangeCredentials.php',
        'SpecialChangeEmail' => __DIR__ . '/includes/specials/SpecialChangeEmail.php',
+       'SpecialChangeEmailPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialChangeEmail.php',
        'SpecialChangePassword' => __DIR__ . '/includes/specials/SpecialChangePassword.php',
+       'SpecialChangePasswordPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialChangePassword.php',
        'SpecialComparePages' => __DIR__ . '/includes/specials/SpecialComparePages.php',
        'SpecialContributions' => __DIR__ . '/includes/specials/SpecialContributions.php',
        'SpecialCreateAccount' => __DIR__ . '/includes/specials/SpecialCreateAccount.php',
+       'SpecialCreateAccountPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialCreateAccount.php',
        'SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php',
        'SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php',
        'SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php',
@@ -1215,6 +1270,7 @@ $wgAutoloadLocalClasses = [
        'SpecialFilepath' => __DIR__ . '/includes/specials/SpecialFilepath.php',
        'SpecialImport' => __DIR__ . '/includes/specials/SpecialImport.php',
        'SpecialJavaScriptTest' => __DIR__ . '/includes/specials/SpecialJavaScriptTest.php',
+       'SpecialLinkAccounts' => __DIR__ . '/includes/specials/SpecialLinkAccounts.php',
        'SpecialListAdmins' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'SpecialListBots' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'SpecialListFiles' => __DIR__ . '/includes/specials/SpecialListfiles.php',
@@ -1237,6 +1293,7 @@ $wgAutoloadLocalClasses = [
        'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php',
        'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php',
        'SpecialPasswordReset' => __DIR__ . '/includes/specials/SpecialPasswordReset.php',
+       'SpecialPasswordResetPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialPasswordReset.php',
        'SpecialPermanentLink' => __DIR__ . '/includes/specials/SpecialPermanentLink.php',
        'SpecialPreferences' => __DIR__ . '/includes/specials/SpecialPreferences.php',
        'SpecialPrefixindex' => __DIR__ . '/includes/specials/SpecialPrefixindex.php',
@@ -1249,6 +1306,7 @@ $wgAutoloadLocalClasses = [
        'SpecialRecentChangesLinked' => __DIR__ . '/includes/specials/SpecialRecentchangeslinked.php',
        'SpecialRedirect' => __DIR__ . '/includes/specials/SpecialRedirect.php',
        'SpecialRedirectToSpecial' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php',
+       'SpecialRemoveCredentials' => __DIR__ . '/includes/specials/SpecialRemoveCredentials.php',
        'SpecialResetTokens' => __DIR__ . '/includes/specials/SpecialResetTokens.php',
        'SpecialRevisionDelete' => __DIR__ . '/includes/specials/SpecialRevisiondelete.php',
        'SpecialRunJobs' => __DIR__ . '/includes/specials/SpecialRunJobs.php',
@@ -1259,11 +1317,14 @@ $wgAutoloadLocalClasses = [
        'SpecialTrackingCategories' => __DIR__ . '/includes/specials/SpecialTrackingCategories.php',
        'SpecialUnblock' => __DIR__ . '/includes/specials/SpecialUnblock.php',
        'SpecialUndelete' => __DIR__ . '/includes/specials/SpecialUndelete.php',
+       'SpecialUnlinkAccounts' => __DIR__ . '/includes/specials/SpecialUnlinkAccounts.php',
        'SpecialUnlockdb' => __DIR__ . '/includes/specials/SpecialUnlockdb.php',
        'SpecialUpload' => __DIR__ . '/includes/specials/SpecialUpload.php',
        'SpecialUploadStash' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
        'SpecialUploadStashTooLargeException' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
-       'SpecialUserlogout' => __DIR__ . '/includes/specials/SpecialUserlogout.php',
+       'SpecialUserLogin' => __DIR__ . '/includes/specials/SpecialUserLogin.php',
+       'SpecialUserLogout' => __DIR__ . '/includes/specials/SpecialUserLogout.php',
+       'SpecialUserlogoutPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialUserlogout.php',
        'SpecialVersion' => __DIR__ . '/includes/specials/SpecialVersion.php',
        'SpecialWatchlist' => __DIR__ . '/includes/specials/SpecialWatchlist.php',
        'SpecialWhatLinksHere' => __DIR__ . '/includes/specials/SpecialWhatlinkshere.php',
@@ -1465,6 +1526,7 @@ $wgAutoloadLocalClasses = [
        'XMPReader' => __DIR__ . '/includes/media/XMP.php',
        'XMPValidate' => __DIR__ . '/includes/media/XMPValidate.php',
        'Xhprof' => __DIR__ . '/includes/libs/Xhprof.php',
+       'XhprofData' => __DIR__ . '/includes/libs/XhprofData.php',
        'Xml' => __DIR__ . '/includes/Xml.php',
        'XmlDumpWriter' => __DIR__ . '/includes/export/XmlDumpWriter.php',
        'XmlJsCode' => __DIR__ . '/includes/Xml.php',
index ed3eaa9..1d2b2f0 100644 (file)
                        "type": "object",
                        "description": "Registry of factory functions to create Config objects"
                },
+               "SessionProviders": {
+                       "type": "object",
+                       "description": "Session providers"
+               },
+               "AuthManagerAutoConfig": {
+                       "type": "object",
+                       "description": "AuthManager auto-configuration",
+                       "additionalProperties": false,
+                       "properties": {
+                               "preauth": {
+                                       "type": "object",
+                                       "description": "Pre-authentication providers"
+                               },
+                               "primaryauth": {
+                                       "type": "object",
+                                       "description": "Primary authentication providers"
+                               },
+                               "secondaryauth": {
+                                       "type": "object",
+                                       "description": "Secondary authentication providers"
+                               }
+                       }
+               },
                "CentralIdLookupProviders": {
                        "type": "object",
                        "description": "Central ID lookup providers"
index 2d5f6bc..f652786 100644 (file)
@@ -238,9 +238,10 @@ MediaWiki 1.4rc1.
 This is a list of known events and parameters; please add to it if you're going
 to add events to the MediaWiki code.
 
-'AbortAutoAccount': Return false to cancel automated local account creation,
-where normally authentication against an external auth plugin would be creating
-a local account.
+'AbortAutoAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
+Return false to cancel automated local account creation, where normally
+authentication against an external auth plugin would be creating a local
+account.
 $user: the User object about to be created (read-only, incomplete)
 &$abortMsg: out parameter: name of error message to be displayed to user
 
@@ -248,12 +249,6 @@ $user: the User object about to be created (read-only, incomplete)
 $autoblockip: The IP going to be autoblocked.
 &$block: The block from which the autoblock is coming.
 
-'AbortChangePassword': Return false to cancel password change.
-$user: the User object to which the password change is occuring
-$mOldpass: the old password provided by the user
-$newpass: the new password provided by the user
-&$abortMsg: the message identifier for abort reason
-
 'AbortDiffCache': Can be used to cancel the caching of a diff.
 &$diffEngine: DifferenceEngine object
 
@@ -262,7 +257,8 @@ $editor: The User who made the change.
 $title: The Title of the page that was edited.
 $rc: The current RecentChange object.
 
-'AbortLogin': Return false to cancel account login.
+'AbortLogin': DEPRECATED! Create a PreAuthenticationProvider instead.
+Return false to cancel account login.
 $user: the User object being authenticated against
 $password: the password being submitted, not yet checked for validity
 &$retval: a LoginForm class constant to return from authenticateUserData();
@@ -271,7 +267,8 @@ $password: the password being submitted, not yet checked for validity
 &$msg: the message identifier for abort reason (new in 1.18, not available
   before 1.18)
 
-'AbortNewAccount': Return false to cancel explicit account creation.
+'AbortNewAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
+Return false to cancel explicit account creation.
 $user: the User object about to be created (read-only, incomplete)
 &$msg: out parameter: HTML to display on abort
 &$status: out parameter: Status object to return, replaces the older $msg param
@@ -295,7 +292,8 @@ $name: name of the action
 &$fields: HTMLForm descriptor array
 $article: Article object
 
-'AddNewAccount': After a user account is created.
+'AddNewAccount': DEPRECATED! Use LocalUserCreated.
+After a user account is created.
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
 
@@ -744,13 +742,32 @@ viewing.
 redirect was followed.
 &$article: target article (object)
 
+'AuthChangeFormFields': After converting a field information array obtained
+from a set of AuthenticationRequest classes into a form descriptor; hooks
+can tweak the array to change how login etc. forms should look.
+$requests: array of AuthenticationRequests the fields are created from
+$fieldInfo: field information array (union of all AuthenticationRequest::getFieldInfo() responses).
+&$formDescriptor: HTMLForm descriptor. The special key 'weight' can be set
+  to change the order of the fields.
+$action: one of the AuthManager::ACTION_* constants.
+
+'AuthManagerLoginAuthenticateAudit': A login attempt either succeeded or failed
+for a reason other than misconfiguration or session loss. No return data is
+accepted; this hook is for auditing only.
+$response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL state.
+$user: The User object being authenticated against, or null if authentication
+  failed before getting that far.
+$username: A guess at the user name being authenticated, or null if we can't
+  even determine that.
+
 '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).
-Gives a chance for an extension to set it programmatically to a variable class.
+'AuthPluginSetup': DEPRECATED! Extensions should be updated to use AuthManager.
+Update or replace authentication plugin object ($wgAuth). Gives a chance for an
+extension to set it programmatically to a variable class.
 &$auth: the $wgAuth object, probably a stub
 
 'AutopromoteCondition': Check autopromote condition for user.
@@ -916,8 +933,14 @@ $html: Requested html content of anchor
 &$link: Returned value. When set to a non-null value by a hook subscriber
   this value will be used as the anchor instead of Linker::link
 
-'ChangePasswordForm': For extensions that need to add a field to the
-ChangePassword form via the Preferences form.
+'ChangeAuthenticationDataAudit': Called when user changes his password.
+No return data is accepted; this hook is for auditing only.
+$req: AuthenticationRequest object describing the change (and target user)
+$status: StatusValue with the result of the action
+
+'ChangePasswordForm': DEPRECATED! Use AuthChangeFormFields or security levels.
+For extensions that need to add a field to the ChangePassword form via the
+Preferences form.
 &$extraFields: An array of arrays that hold fields like would be passed to the
   pretty function.
 
@@ -1924,16 +1947,11 @@ in LoginForm::$validErrorMessages).
 &$messages: Already added messages (inclusive messages from
   LoginForm::$validErrorMessages)
 
-'LoginPasswordResetMessage': User is being requested to reset their password on
-login. Use this hook to change the Message that will be output on
-Special:ChangePassword.
-&$msg: Message object that will be shown to the user
-$username: Username of the user who's password was expired.
-
-'LoginUserMigrated': Called during login to allow extensions the opportunity to
-inform a user that their username doesn't exist for a specific reason, instead
-of letting the login form give the generic error message that the account does
-not exist. For example, when the account has been renamed or deleted.
+'LoginUserMigrated': DEPRECATED! Create a PreAuthenticationProvider instead.
+Called during login to allow extensions the opportunity to inform a user that
+their username doesn't exist for a specific reason, instead of letting the
+login form give the generic error message that the account does not exist. For
+example, when the account has been renamed or deleted.
 $user: the User object being authenticated against.
 &$msg: the message identifier for abort reason, or an array to pass a message
   key and parameters.
@@ -2427,11 +2445,6 @@ $user: User (object) changing his email address
 $oldaddr: old email address (string)
 $newaddr: new email address (string)
 
-'PrefsPasswordAudit': Called when user changes his password.
-$user: User (object) changing his password
-$newPass: new password
-$error: error (string) 'badretype', 'wrongpassword', 'error' or 'success'
-
 'ProtectionForm::buildForm': Called after all protection type fieldsets are made
 in the form.
 $article: the title being (un)protected
@@ -2571,6 +2584,17 @@ $parserOutput: ParserOutput representing the rendered version of the page
 &$updates: a list of DataUpdate objects, to be modified or replaced by
   the hook handler.
 
+'SecuritySensitiveOperationStatus': Affect the return value from
+MediaWiki\Auth\AuthManager::securitySensitiveOperationStatus().
+&$status: (string) The status to be returned. One of the AuthManager::SEC_*
+  constants. SEC_REAUTH will be automatically changed to SEC_FAIL if
+  authentication isn't possible for the current session type.
+$operation: (string) The operation being checked.
+$session: (MediaWiki\Session\Session) The current session. The
+  currently-authenticated user may be retrieved as $session->getUser().
+$timeSinceAuth: (int) The time since last authentication. PHP_INT_MAX if
+  the time of last auth is unknown, or -1 if authentication is not possible.
+
 'SelfLinkBegin': Called before a link to the current article is displayed to
 allow the display of the link to be customized.
 $nt: the Title object
@@ -3254,7 +3278,8 @@ messages!" message, return false to not delete it.
 &$user: User (object) that will clear the message
 $oldid: ID of the talk page revision being viewed (0 means the most recent one)
 
-'UserCreateForm': change to manipulate the login form
+'UserCreateForm': DEPRECATED! Create an AuthenticationProvider instead.
+Manipulate the login form.
 &$template: SimpleTemplate instance for the form
 
 'UserEffectiveGroups': Called in User::getEffectiveGroups().
@@ -3357,12 +3382,14 @@ $user: User object
 'UserLoggedIn': Called after a user is logged in
 $user: User object for the logged-in user
 
-'UserLoginComplete': After a user has logged in.
+'UserLoginComplete': Show custom content after a user has logged in via the web interface.
+For functionality that needs to run after any login (API or web) use UserLoggedIn.
 &$user: the user object that was created on login
 &$inject_html: Any HTML to inject after the "logged in" message.
 
-'UserLoginForm': change to manipulate the login form
-&$template: SimpleTemplate instance for the form
+'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
+Manipulate the login form.
+&$template: QuickTemplate instance for the form
 
 'UserLogout': Before a user logs out.
 &$user: the user object that is about to be logged out
index add5876..0b65593 100644 (file)
@@ -32,6 +32,8 @@
  * accounts authenticate externally, or use it only as a fallback; also
  * you can transparently create internal wiki accounts the first time
  * someone logs in who can be authenticated externally.
+ *
+ * @deprecated since 1.27
  */
 class AuthPlugin {
        /**
@@ -322,6 +324,9 @@ class AuthPlugin {
        }
 }
 
+/**
+ * @deprecated since 1.27
+ */
 class AuthPluginUser {
        function __construct( $user ) {
                # Override this!
index 383f0ad..0b70d16 100644 (file)
@@ -535,6 +535,64 @@ $wgUseInstantCommons = false;
  */
 $wgForeignUploadTargets = [];
 
+/**
+ * Configuration for file uploads using the embeddable upload dialog
+ * (https://www.mediawiki.org/wiki/Upload_dialog).
+ *
+ * This applies also to foreign uploads to this wiki (the configuration is loaded by remote wikis
+ * using the action=query&meta=siteinfo API).
+ *
+ * See below for documentation of each property. None of the properties may be omitted.
+ */
+$wgUploadDialog = [
+       // Fields to make available in the dialog. `true` means that this field is visible, `false` means
+       // that it is hidden. The "Name" field can't be hidden. Note that you also have to add the
+       // matching replacement to the 'filepage' format key below to make use of these.
+       'fields' => [
+               'description' => true,
+               'date' => false,
+               'categories' => false,
+       ],
+       // Suffix of localisation messages used to describe the license under which the uploaded file will
+       // be released. The same value may be set for both 'local' and 'foreign' uploads.
+       'licensemessages' => [
+               // The 'local' messages are used for local uploads on this wiki:
+               // * upload-form-label-own-work-message-generic-local
+               // * upload-form-label-not-own-work-message-generic-local
+               // * upload-form-label-not-own-work-local-generic-local
+               'local' => 'generic-local',
+               // The 'foreign' messages are used for cross-wiki uploads from other wikis to this wiki:
+               // * upload-form-label-own-work-message-generic-foreign
+               // * upload-form-label-not-own-work-message-generic-foreign
+               // * upload-form-label-not-own-work-local-generic-foreign
+               'foreign' => 'generic-foreign',
+       ],
+       // Upload comment to use. Available replacements:
+       // * $HOST - domain name from which a cross-wiki upload originates
+       // * $PAGENAME - wiki page name from which an upload originates
+       'comment' => '',
+       // Format of the file page wikitext to be generated from the fields input by the user.
+       'format' => [
+               // Wrapper for the whole page. Available replacements:
+               // * $DESCRIPTION - file description, as input by the user (only if the 'description' field is
+               //   enabled), wrapped as defined below in the 'description' key
+               // * $DATE - file creation date, as input by the user (only if the 'date' field is enabled)
+               // * $SOURCE - as defined below in the 'ownwork' key, may be extended in the future
+               // * $AUTHOR - linked user name, may be extended in the future
+               // * $LICENSE - as defined below in the 'license' key, may be extended in the future
+               // * $CATEGORIES - file categories wikitext, as input by the user (only if the 'categories'
+               //   field is enabled), or if no input, as defined below in the 'uncategorized' key
+               'filepage' => '$DESCRIPTION',
+               // Wrapped for file description. Available replacements:
+               // * $LANGUAGE - source wiki's content language
+               // * $TEXT - input by the user
+               'description' => '$TEXT',
+               'ownwork' => '',
+               'license' => '',
+               'uncategorized' => '',
+       ],
+];
+
 /**
  * File backend structure configuration.
  *
@@ -3130,24 +3188,6 @@ $wgHTMLFormAllowTableFormat = true;
  */
 $wgUseMediaWikiUIEverywhere = false;
 
-/**
- * Should we try to make our HTML output well-formed XML?  If set to false,
- * output will be a few bytes shorter, and the HTML will arguably be more
- * readable.  If set to true, life will be much easier for the authors of
- * screen-scraping bots, and the HTML will arguably be more readable.
- *
- * Setting this to false may omit quotation marks on some attributes, omit
- * slashes from some self-closing tags, omit some ending tags, etc., where
- * permitted by HTML5.  Setting it to true will not guarantee that all pages
- * will be well-formed, although non-well-formed pages should be rare and it's
- * a bug if you find one.  Conversely, setting it to false doesn't mean that
- * all XML-y constructs will be omitted, just that they might be.
- *
- * Because of compatibility with screen-scraping bots, and because it's
- * controversial, this is currently left to true by default.
- */
-$wgWellFormedXml = true;
-
 /**
  * Permit other namespaces in addition to the w3.org default.
  *
@@ -4406,6 +4446,144 @@ $wgPasswordPolicy = [
        ],
 ];
 
+/**
+ * Disable AuthManager
+ * @since 1.27
+ * @deprecated since 1.27, for use during development only
+ */
+$wgDisableAuthManager = true;
+
+/**
+ * Configure AuthManager
+ *
+ * All providers are constructed using ObjectFactory, see that for the general
+ * structure. The array may also contain a key "sort" used to order providers:
+ * providers are stably sorted by this value, which should be an integer
+ * (default is 0).
+ *
+ * Elements are:
+ * - preauth: Array (keys ignored) of specifications for PreAuthenticationProviders
+ * - primaryauth: Array (keys ignored) of specifications for PrimaryAuthenticationProviders
+ * - secondaryauth: Array (keys ignored) of specifications for SecondaryAuthenticationProviders
+ *
+ * @since 1.27
+ * @note If this is null or empty, the value from $wgAuthManagerAutoConfig is
+ *  used instead. Local customization should generally set this variable from
+ *  scratch to the desired configuration. Extensions that want to
+ *  auto-configure themselves should use $wgAuthManagerAutoConfig instead.
+ */
+$wgAuthManagerConfig = null;
+
+/**
+ * @see $wgAuthManagerConfig
+ * @since 1.27
+ */
+$wgAuthManagerAutoConfig = [
+       'preauth' => [
+               MediaWiki\Auth\LegacyHookPreAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\LegacyHookPreAuthenticationProvider::class,
+                       'sort' => 0,
+               ],
+               MediaWiki\Auth\ThrottlePreAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\ThrottlePreAuthenticationProvider::class,
+                       'sort' => 0,
+               ],
+       ],
+       'primaryauth' => [
+               // TemporaryPasswordPrimaryAuthenticationProvider should come before
+               // any other PasswordAuthenticationRequest-based
+               // PrimaryAuthenticationProvider (or at least any that might return
+               // FAIL rather than ABSTAIN for a wrong password), or password reset
+               // won't work right. Do not remove this (or change the key) or
+               // auto-configuration of other such providers in extensions will
+               // probably auto-insert themselves in the wrong place.
+               MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class,
+                       'args' => [ [
+                               // Fall through to LocalPasswordPrimaryAuthenticationProvider
+                               'authoritative' => false,
+                       ] ],
+                       'sort' => 0,
+               ],
+               MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class,
+                       'args' => [ [
+                               // Last one should be authoritative, or else the user will get
+                               // a less-than-helpful error message (something like "supplied
+                               // authentication info not supported" rather than "wrong
+                               // password") if it too fails.
+                               'authoritative' => true,
+                       ] ],
+                       'sort' => 100,
+               ],
+       ],
+       'secondaryauth' => [
+               MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider::class,
+                       'sort' => 0,
+               ],
+               MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider::class,
+                       'sort' => 100,
+               ],
+               // Linking during login is experimental, enable at your own risk - T134952
+               // MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class => [
+               //      'class' => MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class,
+               //      'sort' => 100,
+               // ],
+               MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class => [
+                       'class' => MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class,
+                       'sort' => 200,
+               ],
+       ],
+];
+
+/**
+ * If it has been this long since the last authentication, recommend
+ * re-authentication before security-sensitive operations (e.g. password or
+ * email changes). Set negative to disable.
+ * @since 1.27
+ * @var int[] operation => time in seconds. A 'default' key must always be provided.
+ */
+$wgReauthenticateTime = [
+       'default' => 300,
+];
+
+/**
+ * Whether to allow security-sensitive operations when authentication is not possible.
+ * @since 1.27
+ * @var bool[] operation => boolean. A 'default' key must always be provided.
+ */
+$wgAllowSecuritySensitiveOperationIfCannotReauthenticate = [
+       'default' => true,
+];
+
+/**
+ * List of AuthenticationRequest class names which are not changeable through
+ * Special:ChangeCredentials and the changeauthenticationdata API.
+ * This is only enforced on the client level; AuthManager itself (e.g.
+ * AuthManager::allowsAuthenticationDataChange calls) is not affected.
+ * Class names are checked for exact match (not for subclasses).
+ * @since 1.27
+ * @var string[]
+ */
+$wgChangeCredentialsBlacklist = [
+       \MediaWiki\Auth\TemporaryPasswordAuthenticationRequest::class
+];
+
+/**
+ * List of AuthenticationRequest class names which are not removable through
+ * Special:RemoveCredentials and the removeauthenticationdata API.
+ * This is only enforced on the client level; AuthManager itself (e.g.
+ * AuthManager::allowsAuthenticationDataChange calls) is not affected.
+ * Class names are checked for exact match (not for subclasses).
+ * @since 1.27
+ * @var string[]
+ */
+$wgRemoveCredentialsBlacklist = [
+       \MediaWiki\Auth\PasswordAuthenticationRequest::class,
+];
+
 /**
  * For compatibility with old installations set to false
  * @deprecated since 1.24 will be removed in future
@@ -4903,6 +5081,7 @@ $wgGroupPermissions['sysop']['suppressredirect'] = true;
 # $wgGroupPermissions['sysop']['upload_by_url'] = true;
 $wgGroupPermissions['sysop']['mergehistory'] = true;
 $wgGroupPermissions['sysop']['managechangetags'] = true;
+$wgGroupPermissions['sysop']['deletechangetags'] = true;
 
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
index 02093ff..0f52983 100644 (file)
@@ -2835,7 +2835,7 @@ class EditPage {
                                                // Log-in link
                                                '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
                                                // Sign-up link
-                                               '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' ]
+                                               '{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ]
                                );
                        } else {
                                $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
@@ -3860,7 +3860,7 @@ HTML
                        ],
                        $showSignature ? [
                                'id'     => 'mw-editbutton-signature',
-                               'open'   => '--~~~~',
+                               'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
                                'close'  => '',
                                'sample' => '',
                                'tip'    => wfMessage( 'sig_tip' )->text(),
index 537bdef..618fa4c 100644 (file)
@@ -2134,6 +2134,24 @@ function wfTempDir() {
                        return $tmp;
                }
        }
+
+       /**
+        * PHP on Windows will detect C:\Windows\Temp as not writable even though PHP can write to it
+        * so create a directory within that called 'mwtmp' with a suffix of the user running the
+        * current process.
+        * The user is included as if various scripts are run by different users they will likely
+        * not be able to access each others temporary files.
+        */
+       if ( wfIsWindows() ) {
+               $tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mwtmp' . '-' . get_current_user();
+               if ( !file_exists( $tmp ) ) {
+                       mkdir( $tmp );
+               }
+               if ( file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
+                       return $tmp;
+               }
+       }
+
        throw new MWException( 'No writable temporary directory could be found. ' .
                'Please set $wgTmpDirectory to a writable directory.' );
 }
index 890beb0..e5128d1 100644 (file)
@@ -38,8 +38,6 @@
  *
  * $wgMimeType: If this is set to an xml MIME type then output should be
  *     valid XHTML5.
- * $wgWellFormedXml: If this is set to true, then all output should be
- *     well-formed XML (quotes on attributes, self-closing tags, etc.).
  *
  * This class is meant to be confined to utility functions that are called from
  * trusted code paths.  It does not do enforcement of policy like not allowing
@@ -199,8 +197,7 @@ class Html {
         * This is quite similar to Xml::tags(), but it implements some useful
         * HTML-specific logic.  For instance, there is no $allowShortTag
         * parameter: the closing tag is magically omitted if $element has an empty
-        * content model.  If $wgWellFormedXml is false, then a few bytes will be
-        * shaved off the HTML output as well.
+        * content model.
         *
         * @param string $element The element's name, e.g., 'a'
         * @param array $attribs Associative array of attributes, e.g., array(
@@ -211,14 +208,10 @@ class Html {
         * @return string Raw HTML
         */
        public static function rawElement( $element, $attribs = [], $contents = '' ) {
-               global $wgWellFormedXml;
                $start = self::openElement( $element, $attribs );
                if ( in_array( $element, self::$voidElements ) ) {
-                       if ( $wgWellFormedXml ) {
-                               // Silly XML.
-                               return substr( $start, 0, -1 ) . '/>';
-                       }
-                       return $start;
+                       // Silly XML.
+                       return substr( $start, 0, -1 ) . '/>';
                } else {
                        return "$start$contents" . self::closeElement( $element );
                }
@@ -443,8 +436,6 @@ class Html {
         * 'http://www.mediawiki.org/' ) becomes something like
         * ' href="http://www.mediawiki.org"'.  Again, this is like
         * Xml::expandAttributes(), but it implements some HTML-specific logic.
-        * For instance, it will omit quotation marks if $wgWellFormedXml is false,
-        * and will treat boolean attributes specially.
         *
         * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
         * values are allowed as well, which will automagically be normalized
@@ -479,8 +470,6 @@ class Html {
         *   (starting with a space if at least one attribute is output)
         */
        public static function expandAttributes( array $attribs ) {
-               global $wgWellFormedXml;
-
                $ret = '';
                foreach ( $attribs as $key => $value ) {
                        // Support intuitive array( 'checked' => true/false ) form
@@ -564,31 +553,10 @@ class Html {
                                throw new MWException( "HTML attribute $key can not contain a list of values" );
                        }
 
-                       // See the "Attributes" section in the HTML syntax part of HTML5,
-                       // 9.1.2.3 as of 2009-08-10.  Most attributes can have quotation
-                       // marks omitted, but not all.  (Although a literal " is not
-                       // permitted, we don't check for that, since it will be escaped
-                       // anyway.)
-
-                       // See also research done on further characters that need to be
-                       // escaped: http://code.google.com/p/html5lib/issues/detail?id=93
-                       $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
-                               . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
-                               . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
-                       if ( $wgWellFormedXml || $value === '' || preg_match( "![$badChars]!u", $value ) ) {
-                               $quote = '"';
-                       } else {
-                               $quote = '';
-                       }
+                       $quote = '"';
 
                        if ( in_array( $key, self::$boolAttribs ) ) {
-                               // In HTML5, we can leave the value empty. If we don't need
-                               // well-formed XML, we can omit the = entirely.
-                               if ( !$wgWellFormedXml ) {
-                                       $ret .= " $key";
-                               } else {
-                                       $ret .= " $key=\"\"";
-                               }
+                               $ret .= " $key=\"\"";
                        } else {
                                // Apparently we need to entity-encode \n, \r, \t, although the
                                // spec doesn't mention that.  Since we're doing strtr() anyway,
@@ -599,22 +567,18 @@ class Html {
                                // don't because we're stubborn and like our marginal savings on
                                // byte size from not having to encode unnecessary quotes.
                                // The only difference between this transform and the one by
-                               // Sanitizer::encodeAttribute() is '<' is only encoded here if
-                               // $wgWellFormedXml is set, and ' is not encoded.
+                               // Sanitizer::encodeAttribute() is ' is not encoded.
                                $map = [
                                        '&' => '&amp;',
                                        '"' => '&quot;',
                                        '>' => '&gt;',
+                                       // '<' allegedly allowed per spec
+                                       // but breaks some tools if not escaped.
+                                       "<" => '&lt;',
                                        "\n" => '&#10;',
                                        "\r" => '&#13;',
                                        "\t" => '&#9;'
                                ];
-                               if ( $wgWellFormedXml ) {
-                                       // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
-                                       // But reportedly it breaks some XML tools?
-                                       // @todo FIXME: Is this really true?
-                                       $map['<'] = '&lt;';
-                               }
                                $ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
                        }
                }
@@ -631,11 +595,9 @@ class Html {
         * @return string Raw HTML
         */
        public static function inlineScript( $contents ) {
-               global $wgWellFormedXml;
-
                $attrs = [];
 
-               if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+               if ( preg_match( '/[<&]/', $contents ) ) {
                        $contents = "/*<![CDATA[*/$contents/*]]>*/";
                }
 
@@ -665,9 +627,7 @@ class Html {
         * @return string Raw HTML
         */
        public static function inlineStyle( $contents, $media = 'all' ) {
-               global $wgWellFormedXml;
-
-               if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+               if ( preg_match( '/[<&]/', $contents ) ) {
                        $contents = "/*<![CDATA[*/$contents/*]]>*/";
                }
 
index 9e18fd1..891f426 100644 (file)
@@ -8,6 +8,7 @@ use GenderCache;
 use GlobalVarConfig;
 use Hooks;
 use LBFactory;
+use LinkCache;
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
 use LoadBalancer;
 use MediaWiki\Services\ServiceContainer;
@@ -20,6 +21,8 @@ use SiteLookup;
 use SiteStore;
 use WatchedItemStore;
 use SkinFactory;
+use TitleFormatter;
+use TitleParser;
 
 /**
  * Service locator for MediaWiki core services.
@@ -459,6 +462,30 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'GenderCache' );
        }
 
+       /**
+        * @since 1.28
+        * @return LinkCache
+        */
+       public function getLinkCache() {
+               return $this->getService( 'LinkCache' );
+       }
+
+       /**
+        * @since 1.28
+        * @return TitleFormatter
+        */
+       public function getTitleFormatter() {
+               return $this->getService( 'TitleFormatter' );
+       }
+
+       /**
+        * @since 1.28
+        * @return TitleParser
+        */
+       public function getTitleParser() {
+               return $this->getService( 'TitleParser' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index 3f56240..9a55ae3 100644 (file)
@@ -19,6 +19,8 @@
  *
  * @file
  */
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\PasswordAuthenticationRequest;
 
 /**
  * We're now using the HTMLForm object with some customisation to generate the
@@ -205,7 +207,7 @@ class Preferences {
         * @return void
         */
        static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
-               global $wgAuth, $wgContLang, $wgParser;
+               global $wgAuth, $wgContLang, $wgParser, $wgDisableAuthManager;
 
                $config = $context->getConfig();
                // retrieving user name for GENDER and misc.
@@ -281,16 +283,21 @@ class Preferences {
                $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
 
                // Actually changeable stuff
+               $realnameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'realname' )
+                       : AuthManager::singleton()->allowsPropertyChange( 'realname' );
                $defaultPreferences['realname'] = [
                        // (not really "private", but still shouldn't be edited without permission)
-                       'type' => $canEditPrivateInfo && $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
+                       'type' => $canEditPrivateInfo && $realnameChangeAllowed ? 'text' : 'info',
                        'default' => $user->getRealName(),
                        'section' => 'personal/info',
                        'label-message' => 'yourrealname',
                        'help-message' => 'prefs-help-realname',
                ];
 
-               if ( $canEditPrivateInfo && $wgAuth->allowPasswordChange() ) {
+               $allowPasswordChange = $wgDisableAuthManager ? $wgAuth->allowPasswordChange()
+                       : AuthManager::singleton()->allowsAuthenticationDataChange(
+                               new PasswordAuthenticationRequest(), false );
+               if ( $canEditPrivateInfo && $allowPasswordChange ) {
                        $link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
                                $context->msg( 'prefs-resetpass' )->escaped(), [],
                                [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
@@ -411,8 +418,10 @@ class Preferences {
                        'default' => $oldsigHTML,
                        'section' => 'personal/signature',
                ];
+               $nicknameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'nickname' )
+                       : AuthManager::singleton()->allowsPropertyChange( 'nickname' );
                $defaultPreferences['nickname'] = [
-                       'type' => $wgAuth->allowPropChange( 'nickname' ) ? 'text' : 'info',
+                       'type' => $nicknameChangeAllowed ? 'text' : 'info',
                        'maxlength' => $config->get( 'MaxSigChars' ),
                        'label-message' => 'yournick',
                        'validation-callback' => [ 'Preferences', 'validateSignature' ],
@@ -441,7 +450,9 @@ class Preferences {
                                }
 
                                $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
-                               if ( $canEditPrivateInfo && $wgAuth->allowPropChange( 'emailaddress' ) ) {
+                               $emailChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'emailaddress' )
+                                       : AuthManager::singleton()->allowsPropertyChange( 'emailaddress' );
+                               if ( $canEditPrivateInfo && $emailChangeAllowed ) {
                                        $link = Linker::link(
                                                SpecialPage::getTitleFor( 'ChangeEmail' ),
                                                $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
@@ -1410,8 +1421,6 @@ class Preferences {
         * @return bool|Status|string
         */
        static function tryFormSubmit( $formData, $form ) {
-               global $wgAuth;
-
                $user = $form->getModifiedUser();
                $hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
                $result = true;
@@ -1430,6 +1439,7 @@ class Preferences {
 
                // Fortunately, the realname field is MUCH simpler
                // (not really "private", but still shouldn't be edited without permission)
+
                if ( !in_array( 'realname', $hiddenPrefs )
                        && $user->isAllowed( 'editmyprivateinfo' )
                        && array_key_exists( 'realname', $formData )
@@ -1462,7 +1472,7 @@ class Preferences {
                        Hooks::run( 'PreferencesFormPreSave', [ $formData, $form, $user, &$result ] );
                }
 
-               $wgAuth->updateExternalDB( $user );
+               MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
                $user->saveSettings();
 
                return $result;
index e282bda..293e6eb 100644 (file)
@@ -139,10 +139,34 @@ return [
                return $store;
        },
 
+       'LinkCache' => function( MediaWikiServices $services ) {
+               return new LinkCache(
+                       $services->getTitleFormatter()
+               );
+       },
+
        'GenderCache' => function( MediaWikiServices $services ) {
                return new GenderCache();
        },
 
+       '_MediaWikiTitleCodec' => function( MediaWikiServices $services ) {
+               global $wgContLang;
+
+               return new MediaWikiTitleCodec(
+                       $wgContLang,
+                       $services->getGenderCache(),
+                       $services->getMainConfig()->get( 'LocalInterwikis' )
+               );
+       },
+
+       'TitleFormatter' => function( MediaWikiServices $services ) {
+               return $services->getService( '_MediaWikiTitleCodec' );
+       },
+
+       'TitleParser' => function( MediaWikiServices $services ) {
+               return $services->getService( '_MediaWikiTitleCodec' );
+       },
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service here, don't forget to add a getter function
        // in the MediaWikiServices class. The convenience getter should just call
index 9db997a..e57b96a 100644 (file)
@@ -450,6 +450,22 @@ if ( $wgProfileOnly ) {
        $wgDebugLogFile = '';
 }
 
+// Disable AuthManager API modules if $wgDisableAuthManager
+if ( $wgDisableAuthManager ) {
+       $wgAPIModules += [
+               'clientlogin' => 'ApiDisabled',
+               'createaccount' => 'ApiCreateAccount', // Use the non-AuthManager version
+               'linkaccount' => 'ApiDisabled',
+               'unlinkaccount' => 'ApiDisabled',
+               'changeauthenticationdata' => 'ApiDisabled',
+               'removeauthenticationdata' => 'ApiDisabled',
+               'resetpassword' => 'ApiDisabled',
+       ];
+       $wgAPIMetaModules += [
+               'authmanagerinfo' => 'ApiQueryDisabled',
+       ];
+}
+
 // Backwards compatibility with old password limits
 if ( $wgMinimalPasswordLength !== false ) {
        $wgPasswordPolicy['policies']['default']['MinimalPasswordLength'] = $wgMinimalPasswordLength;
@@ -692,9 +708,22 @@ $wgContLang->initContLang();
 $wgRequest->interpolateTitle();
 
 if ( !is_object( $wgAuth ) ) {
-       $wgAuth = new AuthPlugin;
+       $wgAuth = $wgDisableAuthManager ? new AuthPlugin : new MediaWiki\Auth\AuthManagerAuthPlugin;
        Hooks::run( 'AuthPluginSetup', [ &$wgAuth ] );
 }
+if ( !$wgDisableAuthManager &&
+       $wgAuth && !$wgAuth instanceof MediaWiki\Auth\AuthManagerAuthPlugin
+) {
+       MediaWiki\Auth\AuthManager::singleton()->forcePrimaryAuthenticationProviders( [
+               new MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider( [
+                       'authoritative' => false,
+               ] ),
+               new MediaWiki\Auth\AuthPluginPrimaryAuthenticationProvider( $wgAuth ),
+               new MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider( [
+                       'authoritative' => true,
+               ] ),
+       ], '$wgAuth is ' . get_class( $wgAuth ) );
+}
 
 // Set up the session
 $ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
@@ -820,7 +849,15 @@ if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
        $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 );
+               if ( $wgDisableAuthManager ) {
+                       MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser );
+               } else {
+                       MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
+                               $sessionUser,
+                               MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSSION,
+                               true
+                       );
+               }
                Profiler::instance()->scopedProfileOut( $ps_autocreate );
        }
        unset( $sessionUser );
index 3d2c887..f8370e4 100644 (file)
@@ -115,6 +115,14 @@ class Status {
                $this->sv->setResult( $ok, $value );
        }
 
+       /**
+        * Returns the wrapped StatusValue object
+        * @return StatusValue
+        */
+       public function getStatusValue() {
+               return $this->sv;
+       }
+
        /**
         * Returns whether the operation completed and didn't have any error or
         * warnings
index 65b2d3a..25fbce3 100644 (file)
@@ -22,7 +22,6 @@
  * @file
  */
 use MediaWiki\Linker\LinkTarget;
-
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -159,42 +158,6 @@ class Title implements LinkTarget {
        private $mIsBigDeletion = null;
        // @}
 
-       /**
-        * B/C kludge: provide a TitleParser for use by Title.
-        * Ideally, Title would have no methods that need this.
-        * Avoid usage of this singleton by using TitleValue
-        * and the associated services when possible.
-        *
-        * @return MediaWikiTitleCodec
-        */
-       private static function getMediaWikiTitleCodec() {
-               global $wgContLang, $wgLocalInterwikis;
-
-               static $titleCodec = null;
-               static $titleCodecFingerprint = null;
-
-               // $wgContLang and $wgLocalInterwikis may change (especially while testing),
-               // make sure we are using the right one. To detect changes over the course
-               // of a request, we remember a fingerprint of the config used to create the
-               // codec singleton, and re-create it if the fingerprint doesn't match.
-               $fingerprint = spl_object_hash( $wgContLang ) . '|' . implode( '+', $wgLocalInterwikis );
-
-               if ( $fingerprint !== $titleCodecFingerprint ) {
-                       $titleCodec = null;
-               }
-
-               if ( !$titleCodec ) {
-                       $titleCodec = new MediaWikiTitleCodec(
-                               $wgContLang,
-                               GenderCache::singleton(),
-                               $wgLocalInterwikis
-                       );
-                       $titleCodecFingerprint = $fingerprint;
-               }
-
-               return $titleCodec;
-       }
-
        /**
         * B/C kludge: provide a TitleParser for use by Title.
         * Ideally, Title would have no methods that need this.
@@ -204,11 +167,12 @@ class Title implements LinkTarget {
         * @return TitleFormatter
         */
        private static function getTitleFormatter() {
-               // NOTE: we know that getMediaWikiTitleCodec() returns a MediaWikiTitleCodec,
-               //      which implements TitleFormatter.
-               return self::getMediaWikiTitleCodec();
+               return MediaWikiServices::getInstance()->getTitleFormatter();
        }
 
+       /**
+        * @access protected
+        */
        function __construct() {
        }
 
@@ -1749,9 +1713,9 @@ class Title implements LinkTarget {
 
                                if ( $url === false
                                        && $wgVariantArticlePath
+                                       && preg_match( '/^variant=([^&]*)$/', $query, $matches )
                                        && $wgContLang->getCode() === $this->getPageLanguage()->getCode()
                                        && $this->getPageLanguage()->hasVariants()
-                                       && preg_match( '/^variant=([^&]*)$/', $query, $matches )
                                ) {
                                        $variant = urldecode( $matches[1] );
                                        if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
@@ -3334,9 +3298,11 @@ class Title implements LinkTarget {
                // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
                //        the parsing code with Title, while avoiding massive refactoring.
                // @todo: get rid of secureAndSplit, refactor parsing code.
-               $titleParser = self::getMediaWikiTitleCodec();
+               // @note: getTitleParser() returns a TitleParser implementation which does not have a
+               //        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
+               $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
                // MalformedTitleException can be thrown here
-               $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
+               $parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
 
                # Fill fields
                $this->setFragment( '#' . $parts['fragment'] );
index 50c79dc..b070e1e 100644 (file)
@@ -152,7 +152,7 @@ class WatchedItem {
         *             or WatchedItemStore::loadWatchedItem()
         */
        public static function fromUserTitle( $user, $title, $checkRights = User::CHECK_USER_RIGHTS ) {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                return new self( $user, $title, self::DEPRECATED_USAGE_TIMESTAMP, (bool)$checkRights );
        }
 
@@ -160,7 +160,7 @@ class WatchedItem {
         * @deprecated since 1.27 Use WatchedItemStore::resetNotificationTimestamp()
         */
        public function resetNotificationTimestamp( $force = '', $oldid = 0 ) {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
                        return;
                }
@@ -176,7 +176,7 @@ class WatchedItem {
         * @deprecated since 1.27 Use WatchedItemStore::addWatchBatch()
         */
        public static function batchAddWatch( array $items ) {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                if ( !$items ) {
                        return false;
                }
@@ -209,7 +209,7 @@ class WatchedItem {
         * @return bool
         */
        public function addWatch() {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                $this->user->addWatch( $this->getTitle(), $this->checkRights );
                return true;
        }
@@ -219,7 +219,7 @@ class WatchedItem {
         * @return bool
         */
        public function removeWatch() {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
                        return false;
                }
@@ -232,7 +232,7 @@ class WatchedItem {
         * @return bool
         */
        public function isWatched() {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                return $this->user->isWatched( $this->getTitle(), $this->checkRights );
        }
 
@@ -240,7 +240,7 @@ class WatchedItem {
         * @deprecated since 1.27 Use WatchedItemStore::duplicateAllAssociatedEntries()
         */
        public static function duplicateEntries( Title $oldTitle, Title $newTitle ) {
-               // wfDeprecated( __METHOD__, '1.27' );
+               wfDeprecated( __METHOD__, '1.27' );
                $store = MediaWikiServices::getInstance()->getWatchedItemStore();
                $store->duplicateAllAssociatedEntries( $oldTitle, $newTitle );
        }
diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php
new file mode 100644 (file)
index 0000000..806b8d2
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Create an account with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiAMCreateAccount extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'create' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_CREATE,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Make sure it's possible to log in
+               if ( !$manager->canCreateAccounts() ) {
+                       $this->getResult()->addValue( null, 'createaccount', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail(
+                                       $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_CREATE )
+                               )
+                       ) );
+                       return;
+               }
+
+               // Perform the create step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE );
+                       $res = $manager->continueAccountCreation( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_CREATE );
+                       if ( $params['preservestate'] ) {
+                               $req = $helper->getPreservedRequest();
+                               if ( $req ) {
+                                       $reqs[] = $req;
+                               }
+                       }
+                       $res = $manager->beginAccountCreation( $this->getUser(), $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'createaccount',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'createaccount';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE,
+                       'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_CREATE ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=createaccount&username=Example&password=ExamplePassword&retype=ExamplePassword'
+                               . '&createreturnurl=http://example.org/&createtoken=123ABC'
+                               => 'apihelp-createaccount-example-create',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Account_creation';
+       }
+}
diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php
new file mode 100644 (file)
index 0000000..2997405
--- /dev/null
@@ -0,0 +1,365 @@
+<?php
+/**
+ * Copyright © 2016 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
+ * @since 1.27
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
+
+/**
+ * Helper class for AuthManager-using API modules. Intended for use via
+ * composition.
+ *
+ * @ingroup API
+ */
+class ApiAuthManagerHelper {
+
+       /** @var ApiBase API module, for context and parameters */
+       private $module;
+
+       /** @var string Message output format */
+       private $messageFormat;
+
+       /**
+        * @param ApiBase $module API module, for context and parameters
+        */
+       public function __construct( ApiBase $module ) {
+               $this->module = $module;
+
+               $params = $module->extractRequestParams();
+               $this->messageFormat = isset( $params['messageformat'] ) ? $params['messageformat'] : 'wikitext';
+       }
+
+       /**
+        * Static version of the constructor, for chaining
+        * @param ApiBase $module API module, for context and parameters
+        * @return ApiAuthManagerHelper
+        */
+       public static function newForModule( ApiBase $module ) {
+               return new self( $module );
+       }
+
+       /**
+        * Format a message for output
+        * @param array &$res Result array
+        * @param string $key Result key
+        * @param Message $message
+        */
+       private function formatMessage( array &$res, $key, Message $message ) {
+               switch ( $this->messageFormat ) {
+                       case 'none':
+                               break;
+
+                       case 'wikitext':
+                               $res[$key] = $message->setContext( $this->module )->text();
+                               break;
+
+                       case 'html':
+                               $res[$key] = $message->setContext( $this->module )->parseAsBlock();
+                               $res[$key] = Parser::stripOuterParagraph( $res[$key] );
+                               break;
+
+                       case 'raw':
+                               $res[$key] = [
+                                       'key' => $message->getKey(),
+                                       'params' => $message->getParams(),
+                               ];
+                               break;
+               }
+       }
+
+       /**
+        * Call $manager->securitySensitiveOperationStatus()
+        * @param string $operation Operation being checked.
+        * @throws UsageException
+        */
+       public function securitySensitiveOperation( $operation ) {
+               $status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
+               switch ( $status ) {
+                       case AuthManager::SEC_OK:
+                               return;
+
+                       case AuthManager::SEC_REAUTH:
+                               $this->module->dieUsage(
+                                       'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate'
+                               );
+
+                       case AuthManager::SEC_FAIL:
+                               $this->module->dieUsage(
+                                       'This action is not available as your identify cannot be verified.', 'cannotreauthenticate'
+                               );
+
+                       default:
+                               throw new UnexpectedValueException( "Unknown status \"$status\"" );
+               }
+       }
+
+       /**
+        * Filter out authentication requests by class name
+        * @param AuthenticationRequest[] $reqs Requests to filter
+        * @param string[] $blacklist Class names to remove
+        * @return AuthenticationRequest[]
+        */
+       public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
+               if ( $blacklist ) {
+                       $blacklist = array_flip( $blacklist );
+                       $reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
+                               return !isset( $blacklist[get_class( $req )] );
+                       } );
+               }
+               return $reqs;
+       }
+
+       /**
+        * Fetch and load the AuthenticationRequests for an action
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @return AuthenticationRequest[]
+        */
+       public function loadAuthenticationRequests( $action ) {
+               $params = $this->module->extractRequestParams();
+
+               $manager = AuthManager::singleton();
+               $reqs = $manager->getAuthenticationRequests( $action, $this->module->getUser() );
+
+               // Filter requests, if requested to do so
+               $wantedRequests = null;
+               if ( isset( $params['requests'] ) ) {
+                       $wantedRequests = array_flip( $params['requests'] );
+               } elseif ( isset( $params['request'] ) ) {
+                       $wantedRequests = [ $params['request'] => true ];
+               }
+               if ( $wantedRequests !== null ) {
+                       $reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
+                               return isset( $wantedRequests[$req->getUniqueId()] );
+                       } );
+               }
+
+               // Collect the fields for all the requests
+               $fields = [];
+               foreach ( $reqs as $req ) {
+                       $fields += (array)$req->getFieldInfo();
+               }
+
+               // Extract the request data for the fields and mark those request
+               // parameters as used
+               $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
+               $this->module->getMain()->markParamsUsed( array_keys( $data ) );
+
+               return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+       }
+
+       /**
+        * Format an AuthenticationResponse for return
+        * @param AuthenticationResponse $res
+        * @return array
+        */
+       public function formatAuthenticationResponse( AuthenticationResponse $res ) {
+               $params = $this->module->extractRequestParams();
+
+               $ret = [
+                       'status' => $res->status,
+               ];
+
+               if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
+                       $ret['username'] = $res->username;
+               }
+
+               if ( $res->status === AuthenticationResponse::REDIRECT ) {
+                       $ret['redirecttarget'] = $res->redirectTarget;
+                       if ( $res->redirectApiData !== null ) {
+                               $ret['redirectdata'] = $res->redirectApiData;
+                       }
+               }
+
+               if ( $res->status === AuthenticationResponse::REDIRECT ||
+                       $res->status === AuthenticationResponse::UI ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $ret += $this->formatRequests( $res->neededRequests );
+               }
+
+               if ( $res->status === AuthenticationResponse::FAIL ||
+                       $res->status === AuthenticationResponse::UI ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $this->formatMessage( $ret, 'message', $res->message );
+               }
+
+               if ( $res->status === AuthenticationResponse::FAIL ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $this->module->getRequest()->getSession()->set(
+                               'ApiAuthManagerHelper::createRequest',
+                               $res->createRequest
+                       );
+                       $ret['canpreservestate'] = $res->createRequest !== null;
+               } else {
+                       $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
+        * @return CreateFromLoginAuthenticationRequest|null
+        */
+       public function getPreservedRequest() {
+               $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
+               return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
+       }
+
+       /**
+        * Format an array of AuthenticationRequests for return
+        * @param AuthenticationRequest[] $reqs
+        * @return array Will have a 'requests' key, and also 'fields' if $module's
+        *  params include 'mergerequestfields'.
+        */
+       public function formatRequests( array $reqs ) {
+               $params = $this->module->extractRequestParams();
+               $mergeFields = !empty( $params['mergerequestfields'] );
+
+               $ret = [ 'requests' => [] ];
+               foreach ( $reqs as $req ) {
+                       $describe = $req->describeCredentials();
+                       $reqInfo = [
+                               'id' => $req->getUniqueId(),
+                               'metadata' => $req->getMetadata(),
+                       ];
+                       switch ( $req->required ) {
+                               case AuthenticationRequest::OPTIONAL:
+                                       $reqInfo['required'] = 'optional';
+                                       break;
+                               case AuthenticationRequest::REQUIRED:
+                                       $reqInfo['required'] = 'required';
+                                       break;
+                               case AuthenticationRequest::PRIMARY_REQUIRED:
+                                       $reqInfo['required'] = 'primary-required';
+                                       break;
+                       }
+                       $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
+                       $this->formatMessage( $reqInfo, 'account', $describe['account'] );
+                       if ( !$mergeFields ) {
+                               $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
+                       }
+                       $ret['requests'][] = $reqInfo;
+               }
+
+               if ( $mergeFields ) {
+                       $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
+                       $ret['fields'] = $this->formatFields( $fields );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Clean up a field array for output
+        * @param ApiBase $module For context and parameters 'mergerequestfields'
+        *  and 'messageformat'
+        * @param array $fields
+        * @return array
+        */
+       private function formatFields( array $fields ) {
+               static $copy = [
+                       'type' => true,
+                       'image' => true,
+                       'value' => true,
+               ];
+
+               $module = $this->module;
+               $retFields = [];
+
+               foreach ( $fields as $name => $field ) {
+                       $ret = array_intersect_key( $field, $copy );
+
+                       if ( isset( $field['options'] ) ) {
+                               $ret['options'] = array_map( function ( $msg ) use ( $module ) {
+                                       return $msg->setContext( $module )->plain();
+                               }, $field['options'] );
+                               ApiResult::setArrayType( $ret['options'], 'assoc' );
+                       }
+                       $this->formatMessage( $ret, 'label', $field['label'] );
+                       $this->formatMessage( $ret, 'help', $field['help'] );
+                       $ret['optional'] = !empty( $field['optional'] );
+
+                       $retFields[$name] = $ret;
+               }
+
+               ApiResult::setArrayType( $retFields, 'assoc' );
+
+               return $retFields;
+       }
+
+       /**
+        * Fetch the standard parameters this helper recognizes
+        * @param string $action AuthManager action
+        * @param string $param... Parameters to use
+        * @return array
+        */
+       public static function getStandardParams( $action, $param /* ... */ ) {
+               $params = [
+                       'requests' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
+                       ],
+                       'request' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true,
+                               ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
+                       ],
+                       'messageformat' => [
+                               ApiBase::PARAM_DFLT => 'wikitext',
+                               ApiBase::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
+                       ],
+                       'mergerequestfields' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
+                       ],
+                       'preservestate' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
+                       ],
+                       'returnurl' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
+                       ],
+                       'continue' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
+                       ],
+               ];
+
+               $ret = [];
+               $wantedParams = func_get_args();
+               array_shift( $wantedParams );
+               foreach ( $wantedParams as $name ) {
+                       if ( isset( $params[$name] ) ) {
+                               $ret[$name] = $params[$name];
+                       }
+               }
+               return $ret;
+       }
+}
index da64c03..7d0ae32 100644 (file)
@@ -1522,20 +1522,20 @@ abstract class ApiBase extends ContextSource {
                        throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
                }
 
-               $errors = $status->getErrorsArray();
+               $errors = $status->getErrorsByType( 'error' );
                if ( !$errors ) {
                        // No errors? Assume the warnings should be treated as errors
-                       $errors = $status->getWarningsArray();
+                       $errors = $status->getErrorsByType( 'warning' );
                }
                if ( !$errors ) {
                        // Still no errors? Punt
-                       $errors = [ [ 'unknownerror-nocode' ] ];
+                       $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
                }
 
                // Cannot use dieUsageMsg() because extensions might return custom
                // error messages.
-               if ( $errors[0] instanceof Message ) {
-                       $msg = $errors[0];
+               if ( $errors[0]['message'] instanceof Message ) {
+                       $msg = $errors[0]['message'];
                        if ( $msg instanceof IApiMessage ) {
                                $extraData = $msg->getApiData();
                                $code = $msg->getApiCode();
@@ -1543,8 +1543,8 @@ abstract class ApiBase extends ContextSource {
                                $code = $msg->getKey();
                        }
                } else {
-                       $code = array_shift( $errors[0] );
-                       $msg = wfMessage( $code, $errors[0] );
+                       $code = $errors[0]['message'];
+                       $msg = wfMessage( $code, $errors[0]['params'] );
                }
                if ( isset( ApiBase::$messageMap[$code] ) ) {
                        // Translate message to code, for backwards compatibility
diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php
new file mode 100644 (file)
index 0000000..54547ef
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Change authentication data with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiChangeAuthenticationData extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'changeauth' );
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to change authentication data', 'notloggedin' );
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               $helper->securitySensitiveOperation( 'ChangeCredentials' );
+
+               // Fetch the request
+               $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                       $helper->loadAuthenticationRequests( AuthManager::ACTION_CHANGE ),
+                       $this->getConfig()->get( 'ChangeCredentialsBlacklist' )
+               );
+               if ( count( $reqs ) !== 1 ) {
+                       $this->dieUsage( 'Failed to create change request', 'badrequest' );
+               }
+               $req = reset( $reqs );
+
+               // Make the change
+               $status = $manager->allowsAuthenticationDataChange( $req, true );
+               if ( !$status->isGood() ) {
+                       $this->dieStatus( $status );
+               }
+               $manager->changeAuthenticationData( $req );
+
+               $this->getResult()->addValue( null, 'changeauthenticationdata', [ 'status' => 'success' ] );
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CHANGE,
+                       'request'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_CHANGE ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=changeauthenticationdata' .
+                               '&changeauthrequest=MediaWiki%5CAuth%5CPasswordAuthenticationRequest' .
+                               '&password=ExamplePassword&retype=ExamplePassword&changeauthtoken=123ABC'
+                               => 'apihelp-changeauthenticationdata-example-password',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php
new file mode 100644 (file)
index 0000000..711234a
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Log in to the wiki with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiClientLogin extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'login' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_LOGIN,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Make sure it's possible to log in
+               if ( !$manager->canAuthenticateNow() ) {
+                       $this->getResult()->addValue( null, 'clientlogin', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail( $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_LOGIN ) )
+                       ) );
+                       return;
+               }
+
+               // Perform the login step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE );
+                       $res = $manager->continueAuthentication( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LOGIN );
+                       if ( $params['preservestate'] ) {
+                               $req = $helper->getPreservedRequest();
+                               if ( $req ) {
+                                       $reqs[] = $req;
+                               }
+                       }
+                       $res = $manager->beginAuthentication( $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'clientlogin',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function needsToken() {
+               return 'login';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_LOGIN,
+                       'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_LOGIN ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=clientlogin&username=Example&password=ExamplePassword&'
+                               . 'loginreturnurl=http://example.org/&logintoken=123ABC'
+                               => 'apihelp-clientlogin-example-login',
+                       'action=clientlogin&logincontinue=1&OATHToken=987654&logintoken=123ABC'
+                               => 'apihelp-clientlogin-example-login2',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Login';
+       }
+}
index 5552a85..6a48610 100644 (file)
@@ -27,6 +27,7 @@ use MediaWiki\Logger\LoggerFactory;
  * Unit to authenticate account registration attempts to the current wiki.
  *
  * @ingroup API
+ * @deprecated since 1.27, only used when $wgDisableAuthManager is true
  */
 class ApiCreateAccount extends ApiBase {
        public function execute() {
index 36cbbd9..41de925 100644 (file)
@@ -42,7 +42,7 @@ class ApiFormatJson extends ApiFormatBase {
                        # outside the control of the end user.
                        # (and do it here because ApiMain::reportUnusedParams() gets called
                        # before our ::execute())
-                       $this->getMain()->getCheck( '_' );
+                       $this->getMain()->markParamsUsed( '_' );
                }
        }
 
diff --git a/includes/api/ApiLinkAccount.php b/includes/api/ApiLinkAccount.php
new file mode 100644 (file)
index 0000000..14347d8
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Link an account with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiLinkAccount extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'link' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_LINK,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to link accounts', 'notloggedin' );
+               }
+
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               $helper->securitySensitiveOperation( 'LinkAccounts' );
+
+               // Make sure it's possible to link accounts
+               if ( !$manager->canLinkAccounts() ) {
+                       $this->getResult()->addValue( null, 'linkaccount', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail( $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_LINK ) )
+                       ) );
+                       return;
+               }
+
+               // Perform the link step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE );
+                       $res = $manager->continueAccountLink( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LINK );
+                       $res = $manager->beginAccountLink( $this->getUser(), $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'linkaccount',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_LINK,
+                       'requests', 'messageformat', 'mergerequestfields', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_LINK ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=linkaccount&provider=Example&linkreturnurl=http://example.org/&linktoken=123ABC'
+                               => 'apihelp-linkaccount-example-link',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Linkaccount';
+       }
+}
index 3891415..3572229 100644 (file)
@@ -25,6 +25,9 @@
  * @file
  */
 
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
 use MediaWiki\Logger\LoggerFactory;
 
 /**
@@ -38,6 +41,16 @@ class ApiLogin extends ApiBase {
                parent::__construct( $main, $action, 'lg' );
        }
 
+       protected function getDescriptionMessage() {
+               if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                       return 'apihelp-login-description-nonauthmanager';
+               } elseif ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                       return 'apihelp-login-description';
+               } else {
+                       return 'apihelp-login-description-nobotpasswords';
+               }
+       }
+
        /**
         * Executes the log-in attempt using the parameters passed. If
         * the log-in succeeds, it attaches a cookie to the session
@@ -83,11 +96,11 @@ class ApiLogin extends ApiBase {
                $loginType = 'N/A';
 
                // Check login token
-               $token = LoginForm::getLoginToken();
+               $token = $session->getToken( '', 'login' );
                if ( $token->wasNew() || !$params['token'] ) {
-                       $authRes = LoginForm::NEED_TOKEN;
+                       $authRes = 'NeedToken';
                } elseif ( !$token->match( $params['token'] ) ) {
-                       $authRes = LoginForm::WRONG_TOKEN;
+                       $authRes = 'WrongToken';
                }
 
                // Try bot passwords
@@ -99,48 +112,104 @@ class ApiLogin extends ApiBase {
                        );
                        if ( $status->isOK() ) {
                                $session = $status->getValue();
-                               $authRes = LoginForm::SUCCESS;
+                               $authRes = 'Success';
                                $loginType = 'BotPassword';
                        } else {
+                               $authRes = 'Failed';
+                               $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authmanager' )->info(
                                        'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' )
                                );
                        }
                }
 
-               // Normal login
                if ( $authRes === false ) {
-                       $context->setRequest( new DerivativeRequest(
-                               $this->getContext()->getRequest(),
-                               [
-                                       'wpName' => $params['name'],
-                                       'wpPassword' => $params['password'],
-                                       'wpDomain' => $params['domain'],
-                                       'wpLoginToken' => $params['token'],
-                                       'wpRemember' => ''
-                               ]
-                       ) );
-                       $loginForm = new LoginForm();
-                       $loginForm->setContext( $context );
-                       $authRes = $loginForm->authenticateUserData();
-                       $loginType = 'LoginForm';
+                       if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                               // Non-AuthManager login
+                               $context->setRequest( new DerivativeRequest(
+                                       $this->getContext()->getRequest(),
+                                       [
+                                               'wpName' => $params['name'],
+                                               'wpPassword' => $params['password'],
+                                               'wpDomain' => $params['domain'],
+                                               'wpLoginToken' => $params['token'],
+                                               'wpRemember' => ''
+                                       ]
+                               ) );
+                               $loginForm = new LoginForm();
+                               $loginForm->setContext( $context );
+                               $authRes = $loginForm->authenticateUserData();
+                               $loginType = 'LoginForm';
+
+                               switch ( $authRes ) {
+                                       case LoginForm::SUCCESS:
+                                               $authRes = 'Success';
+                                               break;
+                                       case LoginForm::NEED_TOKEN:
+                                               $authRes = 'NeedToken';
+                                               break;
+                               }
+                       } else {
+                               // Simplified AuthManager login, for backwards compatibility
+                               $manager = AuthManager::singleton();
+                               $reqs = AuthenticationRequest::loadRequestsFromSubmission(
+                                       $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN, $this->getUser() ),
+                                       [
+                                               'username' => $params['name'],
+                                               'password' => $params['password'],
+                                               'domain' => $params['domain'],
+                                               'rememberMe' => true,
+                                       ]
+                               );
+                               $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS:
+                                               if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                                                       $warn = 'Main-account login via action=login is deprecated and may stop working ' .
+                                                               'without warning.';
+                                                       $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].';
+                                                       $warn .= ' To safely continue using main-account login, see action=clientlogin.';
+                                               } else {
+                                                       $warn = 'Login via action=login is deprecated and may stop working without warning.';
+                                                       $warn .= ' To safely log in, see action=clientlogin.';
+                                               }
+                                               $this->setWarning( $warn );
+                                               $authRes = 'Success';
+                                               $loginType = 'AuthManager';
+                                               break;
+
+                                       case AuthenticationResponse::FAIL:
+                                               // Hope it's not a PreAuthenticationProvider that failed...
+                                               $authRes = 'Failed';
+                                               $message = $res->message;
+                                               \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                                       ->info( __METHOD__ . ': Authentication failed: ' . $message->plain() );
+                                               break;
+
+                                       default:
+                                               $authRes = 'Aborted';
+                                               break;
+                               }
+                       }
                }
 
+               $result['result'] = $authRes;
                switch ( $authRes ) {
-                       case LoginForm::SUCCESS:
-                               $user = $context->getUser();
-                               $this->getContext()->setUser( $user );
-                               $user->setCookies( $this->getRequest(), null, true );
+                       case 'Success':
+                               if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                                       $user = $context->getUser();
+                                       $this->getContext()->setUser( $user );
+                                       $user->setCookies( $this->getRequest(), null, true );
+                               } else {
+                                       $user = $session->getUser();
+                               }
 
                                ApiQueryInfo::resetTokenCache();
 
-                               // Run hooks.
-                               // @todo FIXME: Split back and frontend from this hook.
-                               // @todo FIXME: This hook should be placed in the backend
+                               // Deprecated hook
                                $injected_html = '';
                                Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
 
-                               $result['result'] = 'Success';
                                $result['lguserid'] = intval( $user->getId() );
                                $result['lgusername'] = $user->getName();
 
@@ -148,15 +217,14 @@ class ApiLogin extends ApiBase {
                                // point (1.28 at the earliest, and see T121527). They were ok
                                // when the core cookie-based login was the only thing, but
                                // CentralAuth broke that a while back and
-                               // SessionManager/AuthManager are *really* going to break it.
+                               // SessionManager/AuthManager *really* break it.
                                $result['lgtoken'] = $user->getToken();
                                $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
                                $result['sessionid'] = $session->getId();
                                break;
 
-                       case LoginForm::NEED_TOKEN:
-                               $result['result'] = 'NeedToken';
-                               $result['token'] = LoginForm::getLoginToken()->toString();
+                       case 'NeedToken':
+                               $result['token'] = $token->toString();
                                $this->setWarning( 'Fetching a token via action=login is deprecated. ' .
                                   'Use action=query&meta=tokens&type=login instead.' );
                                $this->logFeatureUsage( 'action=login&!lgtoken' );
@@ -166,6 +234,25 @@ class ApiLogin extends ApiBase {
                                $result['sessionid'] = $session->getId();
                                break;
 
+                       case 'WrongToken':
+                               break;
+
+                       case 'Failed':
+                               $result['reason'] = $message->useDatabase( 'false' )->inLanguage( 'en' )->text();
+                               break;
+
+                       case 'Aborted':
+                               $result['reason'] = 'Authentication requires user interaction, ' .
+                                  'which is not supported by action=login.';
+                               if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                                       $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].';
+                                       $result['reason'] .= ' To continue using main-account login, see action=clientlogin.';
+                               } else {
+                                       $result['reason'] .= ' To log in, see action=clientlogin.';
+                               }
+                               break;
+
+                       // Results from LoginForm for when $wgDisableAuthManager is true
                        case LoginForm::WRONG_TOKEN:
                                $result['result'] = 'WrongToken';
                                break;
@@ -230,14 +317,22 @@ class ApiLogin extends ApiBase {
 
                $this->getResult()->addValue( null, 'login', $result );
 
+               if ( $loginType === 'LoginForm' && isset( LoginForm::$statusCodes[$authRes] ) ) {
+                       $authRes = LoginForm::$statusCodes[$authRes];
+               }
                LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', [
                        'event' => 'login',
-                       'successful' => $authRes === LoginForm::SUCCESS,
+                       'successful' => $authRes === 'Success',
                        'loginType' => $loginType,
-                       'status' => LoginForm::$statusCodes[$authRes],
+                       'status' => $authRes,
                ] );
        }
 
+       public function isDeprecated() {
+               return !$this->getConfig()->get( 'DisableAuthManager' ) &&
+                       !$this->getConfig()->get( 'EnableBotPasswords' );
+       }
+
        public function mustBePosted() {
                return true;
        }
index 685a9ef..b944385 100644 (file)
@@ -49,8 +49,14 @@ class ApiMain extends ApiBase {
         */
        private static $Modules = [
                'login' => 'ApiLogin',
+               'clientlogin' => 'ApiClientLogin',
                'logout' => 'ApiLogout',
-               'createaccount' => 'ApiCreateAccount',
+               'createaccount' => 'ApiAMCreateAccount',
+               'linkaccount' => 'ApiLinkAccount',
+               'unlinkaccount' => 'ApiRemoveAuthenticationData',
+               'changeauthenticationdata' => 'ApiChangeAuthenticationData',
+               'removeauthenticationdata' => 'ApiRemoveAuthenticationData',
+               'resetpassword' => 'ApiResetPassword',
                'query' => 'ApiQuery',
                'expandtemplates' => 'ApiExpandTemplates',
                'parse' => 'ApiParse',
@@ -1135,7 +1141,7 @@ class ApiMain extends ApiBase {
                                                                        TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
                                                                );
                                                        }
-                                                       Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes ] );
+                                                       Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
                                                        $lastMod = max( $modifiedTimes );
                                                        $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
                                                }
@@ -1438,6 +1444,14 @@ class ApiMain extends ApiBase {
                return array_keys( $this->mParamsUsed );
        }
 
+       /**
+        * Mark parameters as used
+        * @param string|string[] $params
+        */
+       public function markParamsUsed( $params ) {
+               $this->mParamsUsed += array_fill_keys( (array)$params, true );
+       }
+
        /**
         * Get a request value, and register the fact that it was used, for logging.
         * @param string $name
index 60fb4dc..617db22 100644 (file)
@@ -29,8 +29,14 @@ class ApiManageTags extends ApiBase {
                $params = $this->extractRequestParams();
 
                // make sure the user is allowed
-               if ( !$this->getUser()->isAllowed( 'managechangetags' ) ) {
-                       $this->dieUsage( "You don't have permission to manage change tags", 'permissiondenied' );
+               if ( $params['operation'] !== 'delete'
+                       && !$this->getUser()->isAllowed( 'managechangetags' )
+               ) {
+                       $this->dieUsage( "You don't have permission to manage change tags",
+                               'permissiondenied' );
+               } elseif ( !$this->getUser()->isAllowed( 'deletechangetags' ) ) {
+                       $this->dieUsage( "You don't have permission to delete change tags",
+                               'permissiondenied' );
                }
 
                $result = $this->getResult();
index f278989..af4e536 100644 (file)
@@ -178,7 +178,7 @@ class ApiPageSet extends ApiBase {
                                // Prevent warnings from being reported on these parameters
                                $main = $this->getMain();
                                foreach ( $generator->extractRequestParams() as $paramName => $param ) {
-                                       $main->getVal( $generator->encodeParamName( $paramName ) );
+                                       $main->markParamsUsed( $generator->encodeParamName( $paramName ) );
                                }
                        }
 
index 733ea2c..3ca4c08 100644 (file)
@@ -112,6 +112,7 @@ class ApiQuery extends ApiBase {
         */
        private static $QueryMetaModules = [
                'allmessages' => 'ApiQueryAllMessages',
+               'authmanagerinfo' => 'ApiQueryAuthManagerInfo',
                'siteinfo' => 'ApiQuerySiteinfo',
                'userinfo' => 'ApiQueryUserInfo',
                'filerepoinfo' => 'ApiQueryFileRepoInfo',
diff --git a/includes/api/ApiQueryAuthManagerInfo.php b/includes/api/ApiQueryAuthManagerInfo.php
new file mode 100644 (file)
index 0000000..b591f9c
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Copyright © 2016 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
+ * @since 1.27
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * A query action to return meta information about AuthManager state.
+ *
+ * @ingroup API
+ */
+class ApiQueryAuthManagerInfo extends ApiQueryBase {
+
+       public function __construct( ApiQuery $query, $moduleName ) {
+               parent::__construct( $query, $moduleName, 'ami' );
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+               $helper = new ApiAuthManagerHelper( $this );
+
+               $manager = AuthManager::singleton();
+               $ret = [
+                       'canauthenticatenow' => $manager->canAuthenticateNow(),
+                       'cancreateaccounts' => $manager->canCreateAccounts(),
+                       'canlinkaccounts' => $manager->canLinkAccounts(),
+                       'haspreservedstate' => $helper->getPreservedRequest() !== null,
+               ];
+
+               if ( $params['securitysensitiveoperation'] !== null ) {
+                       $ret['securitysensitiveoperationstatus'] = $manager->securitySensitiveOperationStatus(
+                               $params['securitysensitiveoperation']
+                       );
+               }
+
+               if ( $params['requestsfor'] ) {
+                       $reqs = $manager->getAuthenticationRequests( $params['requestsfor'], $this->getUser() );
+
+                       // Filter out blacklisted requests, depending on the action
+                       switch ( $params['requestsfor'] ) {
+                               case AuthManager::ACTION_CHANGE:
+                                       $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                                               $reqs, $this->getConfig()->get( 'ChangeCredentialsBlacklist' )
+                                       );
+                                       break;
+                               case AuthManager::ACTION_REMOVE:
+                                       $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                                               $reqs, $this->getConfig()->get( 'RemoveCredentialsBlacklist' )
+                                       );
+                                       break;
+                       }
+
+                       $ret += $helper->formatRequests( $reqs );
+               }
+
+               $this->getResult()->addValue( [ 'query' ], $this->getModuleName(), $ret );
+       }
+
+       public function getCacheMode( $params ) {
+               return 'public';
+       }
+
+       public function getAllowedParams() {
+               return [
+                       'securitysensitiveoperation' => null,
+                       'requestsfor' => [
+                               ApiBase::PARAM_TYPE => [
+                                       AuthManager::ACTION_LOGIN,
+                                       AuthManager::ACTION_LOGIN_CONTINUE,
+                                       AuthManager::ACTION_CREATE,
+                                       AuthManager::ACTION_CREATE_CONTINUE,
+                                       AuthManager::ACTION_LINK,
+                                       AuthManager::ACTION_LINK_CONTINUE,
+                                       AuthManager::ACTION_CHANGE,
+                                       AuthManager::ACTION_REMOVE,
+                                       AuthManager::ACTION_UNLINK,
+                               ],
+                       ],
+               ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields' );
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=query&meta=authmanagerinfo&amirequestsfor=' . urlencode( AuthManager::ACTION_LOGIN )
+                               => 'apihelp-query+filerepoinfo-example-login',
+                       'action=query&meta=authmanagerinfo&amirequestsfor=' . urlencode( AuthManager::ACTION_LOGIN ) .
+                               '&amimergerequestfields=1'
+                               => 'apihelp-query+filerepoinfo-example-login-merged',
+                       'action=query&meta=authmanagerinfo&amisecuritysensitiveoperation=foo'
+                               => 'apihelp-query+filerepoinfo-example-securitysensitiveoperation',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Authmanagerinfo';
+       }
+}
index f05556e..a08740a 100644 (file)
@@ -108,6 +108,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                case 'defaultoptions':
                                        $fit = $this->appendDefaultOptions( $p );
                                        break;
+                               case 'uploaddialog':
+                                       $fit = $this->appendUploadDialog( $p );
+                                       break;
                                default:
                                        ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
                        }
@@ -771,6 +774,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                return $this->getResult()->addValue( 'query', $property, $options );
        }
 
+       public function appendUploadDialog( $property ) {
+               $config = $this->getConfig()->get( 'UploadDialog' );
+               return $this->getResult()->addValue( 'query', $property, $config );
+       }
+
        private function formatParserTags( $item ) {
                return "<{$item}>";
        }
@@ -838,6 +846,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                        'variables',
                                        'protocols',
                                        'defaultoptions',
+                                       'uploaddialog',
                                ],
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
                        ],
index ee6ec76..68ec38d 100644 (file)
@@ -49,6 +49,7 @@ class ApiQueryUsers extends ApiQueryBase {
                'emailable',
                'gender',
                'centralids',
+               'cancreate',
        ];
 
        public function __construct( ApiQuery $query, $moduleName ) {
@@ -260,6 +261,10 @@ class ApiQueryUsers extends ApiQueryBase {
                                        }
                                } else {
                                        $data[$u]['missing'] = true;
+                                       if ( isset( $this->prop['cancreate'] ) && !$this->getConfig()->get( 'DisableAuthManager' ) ) {
+                                               $data[$u]['cancreate'] = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u )
+                                                       ->isGood();
+                                       }
                                }
                        } else {
                                if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) ) {
@@ -299,7 +304,7 @@ class ApiQueryUsers extends ApiQueryBase {
        }
 
        public function getAllowedParams() {
-               return [
+               $ret = [
                        'prop' => [
                                ApiBase::PARAM_ISMULTI => true,
                                ApiBase::PARAM_TYPE => [
@@ -312,6 +317,8 @@ class ApiQueryUsers extends ApiQueryBase {
                                        'emailable',
                                        'gender',
                                        'centralids',
+                                       // When adding a prop, consider whether it should be added
+                                       // to self::$publicProps
                                ],
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
                        ],
@@ -326,6 +333,10 @@ class ApiQueryUsers extends ApiQueryBase {
                                ApiBase::PARAM_ISMULTI => true
                        ],
                ];
+               if ( !$this->getConfig()->get( 'DisableAuthManager' ) ) {
+                       $ret['prop'][ApiBase::PARAM_TYPE][] = 'cancreate';
+               }
+               return $ret;
        }
 
        protected function getExamplesMessages() {
diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php
new file mode 100644 (file)
index 0000000..30e40fb
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Remove authentication data from AuthManager
+ *
+ * @ingroup API
+ */
+class ApiRemoveAuthenticationData extends ApiBase {
+
+       private $authAction;
+       private $operation;
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action );
+
+               $this->authAction = $action === 'unlinkaccount'
+                       ? AuthManager::ACTION_UNLINK
+                       : AuthManager::ACTION_REMOVE;
+               $this->operation = $action === 'unlinkaccount'
+                       ? 'UnlinkAccount'
+                       : 'RemoveCredentials';
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to remove authentication data', 'notloggedin' );
+               }
+
+               $params = $this->extractRequestParams();
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               ApiAuthManagerHelper::newForModule( $this )->securitySensitiveOperation( $this->operation );
+
+               // Fetch the request. No need to load from the request, so don't use
+               // ApiAuthManagerHelper's method.
+               $blacklist = $this->authAction === AuthManager::ACTION_REMOVE
+                       ? array_flip( $this->getConfig()->get( 'RemoveCredentialsBlacklist' ) )
+                       : [];
+               $reqs = array_filter(
+                       $manager->getAuthenticationRequests( $this->authAction, $this->getUser() ),
+                       function ( $req ) use ( $params, $blacklist ) {
+                               return $req->getUniqueId() === $params['request'] &&
+                                       !isset( $blacklist[get_class( $req )] );
+                       }
+               );
+               if ( count( $reqs ) !== 1 ) {
+                       $this->dieUsage( 'Failed to create change request', 'badrequest' );
+               }
+               $req = reset( $reqs );
+
+               // Perform the removal
+               $status = $manager->allowsAuthenticationDataChange( $req, true );
+               if ( !$status->isGood() ) {
+                       $this->dieStatus( $status );
+               }
+               $manager->changeAuthenticationData( $req );
+
+               $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => 'success' ] );
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( $this->authAction,
+                       'request'
+               );
+       }
+
+       protected function getExamplesMessages() {
+               $path = $this->getModulePath();
+               $action = $this->getModuleName();
+               return [
+                       "action={$action}&request=FooAuthenticationRequest&token=123ABC"
+                               => "apihelp-{$path}-example-simple",
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php
new file mode 100644 (file)
index 0000000..042ad69
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Copyright © 2016 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Reset password, with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiResetPassword extends ApiBase {
+
+       private $hasAnyRoutes = null;
+
+       /**
+        * Determine whether any reset routes are available.
+        * @return bool
+        */
+       private function hasAnyRoutes() {
+               if ( $this->hasAnyRoutes === null ) {
+                       $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+                       $this->hasAnyRoutes = !empty( $resetRoutes['username'] ) || !empty( $resetRoutes['email'] );
+               }
+               return $this->hasAnyRoutes;
+       }
+
+       protected function getDescriptionMessage() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return 'apihelp-resetpassword-description-noroutes';
+               }
+               return parent::getDescriptionMessage();
+       }
+
+       public function execute() {
+               if ( !$this->hasAnyRoutes() ) {
+                       $this->dieUsage( 'No password reset routes are available.', 'moduledisabled' );
+               }
+
+               $params = $this->extractRequestParams() + [
+                       // Make sure the keys exist even if getAllowedParams didn't define them
+                       'user' => null,
+                       'email' => null,
+               ];
+
+               $this->requireOnlyOneParameter( $params, 'user', 'email' );
+
+               $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+
+               $status = $passwordReset->isAllowed( $this->getUser(), $params['capture'] );
+               if ( !$status->isOK() ) {
+                       $this->dieStatus( Status::wrap( $status ) );
+               }
+
+               $status = $passwordReset->execute(
+                       $this->getUser(), $params['user'], $params['email'], $params['capture']
+               );
+               if ( !$status->isOK() ) {
+                       $status->value = null;
+                       $this->dieStatus( Status::wrap( $status ) );
+               }
+
+               $result = $this->getResult();
+               $result->addValue( [ 'resetpassword' ], 'status', 'success' );
+               if ( $params['capture'] ) {
+                       $passwords = $status->getValue() ?: [];
+                       ApiResult::setArrayType( $passwords, 'kvp', 'user' );
+                       ApiResult::setIndexedTagName( $passwords, 'p' );
+                       $result->addValue( [ 'resetpassword' ], 'passwords', $passwords );
+               }
+       }
+
+       public function isWriteMode() {
+               return $this->hasAnyRoutes();
+       }
+
+       public function needsToken() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return false;
+               }
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return [];
+               }
+
+               $ret = [
+                       'user' => [
+                               ApiBase::PARAM_TYPE => 'user',
+                       ],
+                       'email' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                       ],
+                       'capture' => false,
+               ];
+
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+               if ( empty( $resetRoutes['username'] ) ) {
+                       unset( $ret['user'] );
+               }
+               if ( empty( $resetRoutes['email'] ) ) {
+                       unset( $ret['email'] );
+               }
+
+               return $ret;
+       }
+
+       protected function getExamplesMessages() {
+               $ret = [];
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+
+               if ( !empty( $resetRoutes['username'] ) ) {
+                       $ret['action=resetpassword&user=Example&token=123ABC'] = 'apihelp-resetpassword-example-user';
+               }
+               if ( !empty( $resetRoutes['email'] ) ) {
+                       $ret['action=resetpassword&user=user@example.com&token=123ABC'] =
+                               'apihelp-resetpassword-example-email';
+               }
+
+               return $ret;
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
index 5efefbd..3539eed 100644 (file)
@@ -147,13 +147,15 @@ class ApiStashEdit extends ApiBase {
                        Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] );
 
                        list( $stashInfo, $ttl ) = self::buildStashValue(
-                               $editInfo->pstContent, $editInfo->output, $editInfo->timestamp
+                               $editInfo->pstContent,
+                               $editInfo->output,
+                               $editInfo->timestamp,
+                               $user
                        );
 
                        if ( $stashInfo ) {
                                $ok = $cache->set( $key, $stashInfo, $ttl );
                                if ( $ok ) {
-
                                        $logger->debug( "Cached parser output for key '$key'." );
                                        return self::ERROR_NONE;
                                } else {
@@ -223,7 +225,7 @@ class ApiStashEdit extends ApiBase {
                $pOut->setCacheTime( wfTimestampNow() );
 
                // Build a value to cache with a proper TTL
-               list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp );
+               list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp, $user );
                if ( !$stashInfo ) {
                        $logger->info( "Uncacheable parser output for key '$key' (rev/TTL)." );
                        return false;
@@ -291,6 +293,10 @@ class ApiStashEdit extends ApiBase {
                        $stats->increment( 'editstash.cache_hits.presumed_fresh' );
                        $logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." );
                        return $editInfo; // assume nothing changed
+               } elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) {
+                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                       $logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." );
+                       return $editInfo; // use made no local upload/template edits in the meantime
                }
 
                $dbr = wfGetDB( DB_SLAVE );
@@ -385,12 +391,14 @@ class ApiStashEdit extends ApiBase {
         * @param Content $pstContent
         * @param ParserOutput $parserOutput
         * @param string $timestamp TS_MW
+        * @param User $user
         * @return array (stash info array, TTL in seconds) or (null, 0)
         */
        protected static function buildStashValue(
-               Content $pstContent, ParserOutput $parserOutput, $timestamp
+               Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user
        ) {
-               // If an item is renewed, mind the cache TTL determined by config and parser functions
+               // If an item is renewed, mind the cache TTL determined by config and parser functions.
+               // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
                $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() );
                $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 );
 
@@ -399,7 +407,8 @@ class ApiStashEdit extends ApiBase {
                        $stashInfo = (object)[
                                'pstContent' => $pstContent,
                                'output'     => $parserOutput,
-                               'timestamp'  => $timestamp
+                               'timestamp'  => $timestamp,
+                               'edits'      => $user->getEditCount()
                        ];
                        return [ $stashInfo, $ttl ];
                }
index 0e8b437..512ab83 100644 (file)
@@ -12,7 +12,8 @@
                        "Гульчатай",
                        "Ilmira",
                        "Гизатуллина",
-                       "Танзиля Кутлугильдина"
+                       "Танзиля Кутлугильдина",
+                       "Ләйсән"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Документация]]\n* [[mw:API:FAQ|ЧаВО]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Почта таратыу]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce  API яңылыҡтары]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R  Хаталар һәм яуаптар]\n</div>\n<strong>Статус:</strong> Был биттә  күрһәтелгән бар функциялар ҙа эшләргә тейеш,  шулай ҙа  API әүҙем эшкәртеү хәлендә тора һәм теләгән бер ваҡытта үҙгәрергә мөмкин. Яңыртылыуҙарҙы һәр саҡ белеп торор өсөн [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почта таратыу mediawiki-api-announce], ошоға яҙыл.\n\n<strong>Хаталы һоратыуҙар:</strong> Әгәр API хаталы һоратыу алһа,     HTTP баш һүҙе   «MediaWiki-API-Error» асҡысы менән кире ҡайтарыла,  бынан һуң баш һүҙҙең мәғәнәһе һәм хата  коды кире ебәреләсәк һәм кире шул уҡ мәғәнәлә кире ҡуйыласаҡ. Киңерәк мәғлүмәтте ошонан ҡара  [[mw:API:Errors_and_warnings|API:Хаталар һәм иҫкәртеүҙәр]].\n\n<strong>Тестлау:</strong>   API-һоратыуҙарҙы тестлау уңайлы булһын өсөн ҡара. [[Special:ApiSandbox]]",
        "apihelp-edit-param-appendtext": "Был тексты биттең аҙағынаса өҫтәгеҙ.$1text алмаштыра.\n$1section -ды файҙаланығыҙ = яңы, яңы бүлек өҫтәү өсөн, ә был параметрға түгел.",
        "apihelp-edit-param-undo": "Был версияны кире алырға. $1text,$1prependtext,$1appendtext алмаштыра.",
        "apihelp-edit-param-undoafter": "$1undo- нан алып барлыҡ үҙгәртеүҙәрҙе кире алырға. Әгәр ул ҡуйылмаған булһа, бер тикшереүҙе кире алыу ҙа етә.",
-       "apihelp-edit-param-redirect": "Автоматик йүнәлтәүҙе рөхсәт итергә.",
+       "apihelp-edit-param-redirect": "Автоматик йүнәлтеүҙе рөхсәт итергә.",
        "apihelp-edit-param-contentformat": "Текстҡа ҡуйыу өсөн йөкмәткенең сериализация форматы.",
        "apihelp-edit-param-contentmodel": "Яңы йөкмәткенең контент моделе.",
        "apihelp-edit-param-token": "Маркер һуңғы параметр сифатында ебәрелергә тейеш, йәки, һәрхәлдә $1text параметрынан һуң.",
index 2b23da0..a802cc7 100644 (file)
@@ -34,6 +34,9 @@
        "apihelp-block-example-ip-simple": "Block IP address <kbd>192.0.2.5</kbd> for three days with reason <kbd>First strike</kbd>.",
        "apihelp-block-example-user-complex": "Block user <kbd>Vandal</kbd> indefinitely with reason <kbd>Vandalism</kbd>, and prevent new account creation and email sending.",
 
+       "apihelp-changeauthenticationdata-description": "Change authentication data for the current user.",
+       "apihelp-changeauthenticationdata-example-password": "Attempt to change the current user's password to <kbd>ExamplePassword</kbd>.",
+
        "apihelp-checktoken-description": "Check the validity of a token from <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
        "apihelp-checktoken-param-type": "Type of token being tested.",
        "apihelp-checktoken-param-token": "Token to test.",
        "apihelp-clearhasmsg-description": "Clears the <code>hasmsg</code> flag for the current user.",
        "apihelp-clearhasmsg-example-1": "Clear the <code>hasmsg</code> flag for the current user.",
 
+       "apihelp-clientlogin-description": "Log in to the wiki using the interactive flow.",
+       "apihelp-clientlogin-example-login": "Start the process of logging in to the wiki as user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
+       "apihelp-clientlogin-example-login2": "Continue logging in after a UI response for two-factor auth, supplying an <var>OATHToken</var> of <kbd>987654</kbd>.",
+
        "apihelp-compare-description": "Get the difference between 2 pages.\n\nA revision number, a page title, or a page ID for both \"from\" and \"to\" must be passed.",
        "apihelp-compare-param-fromtitle": "First title to compare.",
        "apihelp-compare-param-fromid": "First page ID to compare.",
@@ -53,6 +60,7 @@
        "apihelp-compare-example-1": "Create a diff between revision 1 and 2.",
 
        "apihelp-createaccount-description": "Create a new user account.",
+       "apihelp-createaccount-example-create": "Start the process of creating user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
        "apihelp-createaccount-param-name": "Username.",
        "apihelp-createaccount-param-password": "Password (ignored if <var>$1mailpassword</var> is set).",
        "apihelp-createaccount-param-domain": "Domain for external authentication (optional).",
        "apihelp-import-param-rootpage": "Import as subpage of this page. Cannot be used together with <var>$1namespace</var>.",
        "apihelp-import-example-import": "Import [[meta:Help:ParserFunctions]] to namespace 100 with full history.",
 
-       "apihelp-login-description": "Log in and get authentication cookies.\n\nIn the event of a successful log-in, the needed cookies will be included in the HTTP response headers. In the event of a failed log-in, further attempts may be throttled to limit automated password guessing attacks.",
+       "apihelp-linkaccount-description": "Link an account from a third-party provider to the current user.",
+       "apihelp-linkaccount-example-link": "Start the process of linking to an account from <kbd>Example</kbd>.",
+
+       "apihelp-login-description": "Log in and get authentication cookies.\n\nThis action should only be used in combination with [[Special:BotPasswords]]; use for main-account login is deprecated and may fail without warning. To safely log in to the main account, use <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nobotpasswords": "Log in and get authentication cookies.\n\nThis action is deprecated and may fail without warning. To safely log in, use <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nonauthmanager": "Log in and get authentication cookies.\n\nIn the event of a successful log-in, the needed cookies will be included in the HTTP response headers. In the event of a failed log-in, further attempts may be throttled to limit automated password guessing attacks.",
        "apihelp-login-param-name": "User name.",
        "apihelp-login-param-password": "Password.",
        "apihelp-login-param-domain": "Domain (optional).",
        "apihelp-query+allusers-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, also indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+allusers-example-Y": "List users starting at <kbd>Y</kbd>.",
 
+       "apihelp-query+authmanagerinfo-description": "Retrieve information about the current authentication status.",
+       "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Test whether the user's current authentication status is sufficient for the specified security-sensitive operation.",
+       "apihelp-query+authmanagerinfo-param-requestsfor": "Fetch information about the authentication requests needed for the specified authentication action.",
+       "apihelp-query+filerepoinfo-example-login": "Fetch the requests that may be used when beginning a login.",
+       "apihelp-query+filerepoinfo-example-login-merged": "Fetch the requests that may be used when beginning a login, with form fields merged.",
+       "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Test whether authentication is sufficient for action <kbd>foo</kbd>.",
+
        "apihelp-query+backlinks-description": "Find all pages that link to the given page.",
        "apihelp-query+backlinks-param-title": "Title to search. Cannot be used together with <var>$1pageid</var>.",
        "apihelp-query+backlinks-param-pageid": "Page ID to search. Cannot be used together with <var>$1title</var>.",
        "apihelp-query+siteinfo-paramvalue-prop-variables": "Returns a list of variable IDs.",
        "apihelp-query+siteinfo-paramvalue-prop-protocols": "Returns a list of protocols that are allowed in external links.",
        "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Returns the default values for user preferences.",
+       "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Returns the upload dialog configuration.",
        "apihelp-query+siteinfo-param-filteriw": "Return only local or only nonlocal entries of the interwiki map.",
        "apihelp-query+siteinfo-param-showalldb": "List all database servers, not just the one lagging the most.",
        "apihelp-query+siteinfo-param-numberingroup": "Lists the number of users in user groups.",
        "apihelp-query+users-paramvalue-prop-emailable": "Tags if the user can and wants to receive email through [[Special:Emailuser]].",
        "apihelp-query+users-paramvalue-prop-gender": "Tags the gender of the user. Returns \"male\", \"female\", or \"unknown\".",
        "apihelp-query+users-paramvalue-prop-centralids": "Adds the central IDs and attachment status for the user.",
+       "apihelp-query+users-paramvalue-prop-cancreate": "Indicates whether an account for valid but unregistered usernames can be created.",
        "apihelp-query+users-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+users-param-users": "A list of users to obtain information for.",
        "apihelp-query+users-param-token": "Use <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> instead.",
        "apihelp-query+watchlistraw-example-simple": "List pages on the current user's watchlist.",
        "apihelp-query+watchlistraw-example-generator": "Fetch page info for pages on the current user's watchlist.",
 
+       "apihelp-removeauthenticationdata-description": "Remove authentication data for the current user.",
+       "apihelp-removeauthenticationdata-example-simple": "Attempt to remove the current user's data for <kbd>FooAuthenticationRequest</kbd>.",
+
+       "apihelp-resetpassword-description": "Send a password reset email to a user.",
+       "apihelp-resetpassword-description-noroutes": "No password reset routes are available.\n\nEnable routes in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> to use this module.",
+       "apihelp-resetpassword-param-user": "User being reset.",
+       "apihelp-resetpassword-param-email": "Email address of the user being reset.",
+       "apihelp-resetpassword-param-capture": "Return the temporary passwords that were sent. Requires the <code>passwordreset</code> user right.",
+       "apihelp-resetpassword-example-user": "Send a password reset email to user <kbd>Example</kbd>.",
+       "apihelp-resetpassword-example-email": "Send a password reset email for all users with email address <kbd>user@example.com</kbd>.",
+
        "apihelp-revisiondelete-description": "Delete and undelete revisions.",
        "apihelp-revisiondelete-param-type": "Type of revision deletion being performed.",
        "apihelp-revisiondelete-param-target": "Page title for the revision deletion, if required for the type.",
        "apihelp-undelete-example-page": "Undelete page <kbd>Main Page</kbd>.",
        "apihelp-undelete-example-revisions": "Undelete two revisions of page <kbd>Main Page</kbd>.",
 
+       "apihelp-unlinkaccount-description": "Remove a linked third-party account from the current user.",
+       "apihelp-unlinkaccount-example-simple": "Attempt to remove the current user's link for the provider associated with <kbd>FooAuthenticationRequest</kbd>.",
+
        "apihelp-upload-description": "Upload a file, or get the status of pending uploads.\n\nSeveral methods are available:\n* Upload file contents directly, using the <var>$1file</var> parameter.\n* Upload the file in pieces, using the <var>$1filesize</var>, <var>$1chunk</var>, and <var>$1offset</var> parameters.\n* Have the MediaWiki server fetch a file from a URL, using the <var>$1url</var> parameter.\n* Complete an earlier upload that failed due to warnings, using the <var>$1filekey</var> parameter.\nNote that the HTTP POST must be done as a file upload (i.e. using <code>multipart/form-data</code>) when sending the <var>$1file</var>.",
        "apihelp-upload-param-filename": "Target filename.",
        "apihelp-upload-param-comment": "Upload comment. Also used as the initial page text for new files if <var>$1text</var> is not specified.",
        "api-help-right-apihighlimits": "Use higher limits in API queries (slow queries: $1; fast queries: $2). The limits for slow queries also apply to multivalue parameters.",
        "api-help-open-in-apisandbox": "<small>[open in sandbox]</small>",
 
+       "api-help-authmanager-general-usage": "The general procedure to use this module is:\n# Fetch the fields available from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$4</kbd>, and a <kbd>$5</kbd> token from <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying <var>$1returnurl</var> and any relevant fields.\n# Check the <samp>status</samp> in the response.\n#* If you received <samp>PASS</samp> or <samp>FAIL</samp>, you're done. The operation either succeeded or it didn't.\n#* If you received <samp>UI</samp>, present the new fields to the user and obtain their submission. Then post to this module with <var>$1continue</var> and the relevant fields set, and repeat step 4.\n#* If you received <samp>REDIRECT</samp>, direct the user to the <samp>redirecttarget</samp> and wait for the return to <var>$1returnurl</var>. Then post to this module with <var>$1continue</var> and any fields passed to the return URL, and repeat step 4.\n#* If you received <samp>RESTART</samp>, that means the authentication worked but we don't have an linked user account. You might treat this as UI or as FAIL.",
+       "api-help-authmanagerhelper-requests": "Only use these authentication requests, by the <samp>id</samp> returned from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> or from a previous response from this module.",
+       "api-help-authmanagerhelper-request": "Use this authentication request, by the <samp>id</samp> returned from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd>.",
+       "api-help-authmanagerhelper-messageformat": "Format to use for returning messages.",
+       "api-help-authmanagerhelper-mergerequestfields": "Merge field information for all authentication requests into one array.",
+       "api-help-authmanagerhelper-preservestate": "Preserve state from a previous failed login attempt, if possible.",
+       "api-help-authmanagerhelper-returnurl": "Return URL for third-party authentication flows, must be absolute. Either this or <var>$1continue</var> is required.\n\nUpon receiving a <samp>REDIRECT</samp> response, you will typically open a browser or web view to the specified <samp>redirecttarget</samp> URL for a third-party authentication flow. When that completes, the third party will send the browser or web view to this URL. You should extract any query or POST parameters from the URL and pass them as a <var>$1continue</var> request to this API module.",
+       "api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.",
+       "api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.",
+
        "api-credits-header": "Credits",
        "api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/."
 }
index b114289..dae55de 100644 (file)
@@ -3,11 +3,13 @@
                "authors": [
                        "WongKentir",
                        "Beeyan",
-                       "Rachmat.Wahidi"
+                       "Rachmat.Wahidi",
+                       "Kenrick95"
                ]
        },
        "apihelp-block-description": "Blokir pengguna.",
        "apihelp-block-param-user": "Nama pengguna, alamat IP, atau rentang alamat IP untuk diblokir.",
        "apihelp-createaccount-param-name": "Nama pengguna",
+       "apihelp-login-example-login": "Masuk log.",
        "apihelp-revisiondelete-param-ids": "Penanda untuk perubahan yang akan dihapus"
 }
index af1ca9c..a250db0 100644 (file)
@@ -4,7 +4,7 @@
                        "NoiX180"
                ]
        },
-       "apihelp-delete-example-simple": "Busak <kbd>Kaca Pokok</kbd>.",
+       "apihelp-delete-example-simple": "Busak <kbd>Tepas</kbd>.",
        "apihelp-query+backlinks-example-simple": "Tuduhaké pranala menyang <kbd>Kaca utama</kbd>.",
        "apihelp-query+backlinks-example-generator": "Deleng pratélan bab kaca-kaca sing nggayut <kbd>Kaca utama</kbd>.",
        "apihelp-query+contributors-example-simple": "Tuduhaké para nyumbang <kbd>Kaca Utama</kbd>."
index 713dfc5..a33dedf 100644 (file)
@@ -45,6 +45,7 @@
        "apihelp-checktoken-example-simple": "<kbd>csrf</kbd> 토큰의 유효성을 테스트합니다.",
        "apihelp-clearhasmsg-description": "현재 사용자의 <code>hasmsg</code> 플래그를 비웁니다.",
        "apihelp-clearhasmsg-example-1": "현재 계정의 <code>hasmsg</code> 플래그를 삭제합니다.",
+       "apihelp-compare-description": "두 문서 간의 차이를 가져옵니다.\n\n대상이 되는 두 문서의 판 번호나 문서 제목 또는 문서 ID를 지정해야 합니다.",
        "apihelp-compare-param-fromtitle": "비교할 첫 이름.",
        "apihelp-compare-param-fromid": "비교할 첫 문서 ID.",
        "apihelp-compare-param-fromrev": "비교할 첫 판.",
@@ -66,6 +67,7 @@
        "apihelp-delete-description": "문서 삭제",
        "apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.",
        "apihelp-delete-param-reason": "삭제의 이유. 설정하지 않으면 자동 생성되는 이유를 사용합니다.",
+       "apihelp-delete-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.",
        "apihelp-delete-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.",
        "apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.",
        "apihelp-disabled-description": "이 모듈은 해제되었습니다.",
@@ -76,6 +78,8 @@
        "apihelp-edit-param-minor": "사소한 편집.",
        "apihelp-edit-param-notminor": "사소하지 않은 편집.",
        "apihelp-edit-param-bot": "이 편집을 봇으로 표시.",
+       "apihelp-edit-param-nocreate": "페이지가 존재하지 않으면 오류를 출력합니다.",
+       "apihelp-edit-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.",
        "apihelp-edit-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.",
        "apihelp-edit-param-redirect": "자동으로 넘겨주기 처리하기.",
        "apihelp-edit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
index 4ded4aa..6137457 100644 (file)
@@ -41,6 +41,8 @@
        "apihelp-block-param-watchuser": "{{doc-apihelp-param|block|watchuser}}",
        "apihelp-block-example-ip-simple": "{{doc-apihelp-example|block}}",
        "apihelp-block-example-user-complex": "{{doc-apihelp-example|block}}",
+       "apihelp-changeauthenticationdata-description": "{{doc-apihelp-description|changeauthenticationdata}}",
+       "apihelp-changeauthenticationdata-example-password": "{{doc-apihelp-example|changeauthenticationdata}}",
        "apihelp-checktoken-description": "{{doc-apihelp-description|checktoken}}",
        "apihelp-checktoken-param-type": "{{doc-apihelp-param|checktoken|type}}",
        "apihelp-checktoken-param-token": "{{doc-apihelp-param|checktoken|token}}",
@@ -48,6 +50,9 @@
        "apihelp-checktoken-example-simple": "{{doc-apihelp-example|checktoken}}",
        "apihelp-clearhasmsg-description": "{{doc-apihelp-description|clearhasmsg}}",
        "apihelp-clearhasmsg-example-1": "{{doc-apihelp-example|clearhasmsg}}",
+       "apihelp-clientlogin-description": "{{doc-apihelp-description|clientlogin}}",
+       "apihelp-clientlogin-example-login": "{{doc-apihelp-example|clientlogin}}",
+       "apihelp-clientlogin-example-login2": "{{doc-apihelp-example|clientlogin}}",
        "apihelp-compare-description": "{{doc-apihelp-description|compare}}",
        "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
        "apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
@@ -57,6 +62,7 @@
        "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
        "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}",
        "apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}",
+       "apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}",
        "apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}",
        "apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}",
        "apihelp-createaccount-param-domain": "{{doc-apihelp-param|createaccount|domain}}",
        "apihelp-import-param-namespace": "{{doc-apihelp-param|import|namespace}}",
        "apihelp-import-param-rootpage": "{{doc-apihelp-param|import|rootpage}}",
        "apihelp-import-example-import": "{{doc-apihelp-example|import}}",
-       "apihelp-login-description": "{{doc-apihelp-description|login}}",
+       "apihelp-linkaccount-description": "{{doc-apihelp-description|linkaccount}}",
+       "apihelp-linkaccount-example-link": "{{doc-apihelp-example|linkaccount}}",
+       "apihelp-login-description": "{{doc-apihelp-description|login|info=This message is used when <code>$wgEnableBotPasswords</code> is true.|seealso=* {{msg-mw|apihelp-login-description-nobotpasswords}}}}",
+       "apihelp-login-description-nobotpasswords": "{{doc-apihelp-description|login|info=This message is used when <code>$wgEnableBotPasswords</code> is false.|seealso=* {{msg-mw|apihelp-login-description}}}}",
+       "apihelp-login-description-nonauthmanager": "{{doc-apihelp-description|login|info=This message is used when <code>$wgDisableAuthManager</code> is true.|seealso=* {{msg-mw|apihelp-login-description}}}}",
        "apihelp-login-param-name": "{{doc-apihelp-param|login|name}}\n{{Identical|Username}}",
        "apihelp-login-param-password": "{{doc-apihelp-param|login|password}}\n{{Identical|Password}}",
        "apihelp-login-param-domain": "{{doc-apihelp-param|login|domain}}",
        "apihelp-query+allusers-param-activeusers": "{{doc-apihelp-param|query+allusers|activeusers|params=* $1 - Value of [[mw:Manual:$wgActiveUserDays]]|paramstart=2}}",
        "apihelp-query+allusers-param-attachedwiki": "{{doc-apihelp-param|query+allusers|attachedwiki}}",
        "apihelp-query+allusers-example-Y": "{{doc-apihelp-example|query+allusers}}",
+       "apihelp-query+authmanagerinfo-description": "{{doc-apihelp-description|query+authmanagerinfo}}",
+       "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "{{doc-apihelp-param|query+authmanagerinfo|securitysensitiveoperation}}",
+       "apihelp-query+authmanagerinfo-param-requestsfor": "{{doc-apihelp-param|query+authmanagerinfo|requestsfor}}",
+       "apihelp-query+filerepoinfo-example-login": "{{doc-apihelp-example|query+filerepoinfo}}",
+       "apihelp-query+filerepoinfo-example-login-merged": "{{doc-apihelp-example|query+filerepoinfo}}",
+       "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "{{doc-apihelp-example|query+filerepoinfo}}",
        "apihelp-query+backlinks-description": "{{doc-apihelp-description|query+backlinks}}",
        "apihelp-query+backlinks-param-title": "{{doc-apihelp-param|query+backlinks|title}}",
        "apihelp-query+backlinks-param-pageid": "{{doc-apihelp-param|query+backlinks|pageid}}",
        "apihelp-query+siteinfo-paramvalue-prop-variables": "{{doc-apihelp-paramvalue|query+siteinfo|prop|variables}}",
        "apihelp-query+siteinfo-paramvalue-prop-protocols": "{{doc-apihelp-paramvalue|query+siteinfo|prop|protocols}}",
        "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "{{doc-apihelp-paramvalue|query+siteinfo|prop|defaultoptions}}",
+       "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "{{doc-apihelp-paramvalue|query+siteinfo|prop|uploaddialog}}",
        "apihelp-query+siteinfo-param-filteriw": "{{doc-apihelp-param|query+siteinfo|filteriw}}",
        "apihelp-query+siteinfo-param-showalldb": "{{doc-apihelp-param|query+siteinfo|showalldb}}",
        "apihelp-query+siteinfo-param-numberingroup": "{{doc-apihelp-param|query+siteinfo|numberingroup}}",
        "apihelp-query+users-paramvalue-prop-emailable": "{{doc-apihelp-paramvalue|query+users|prop|emailable}}",
        "apihelp-query+users-paramvalue-prop-gender": "{{doc-apihelp-paramvalue|query+users|prop|gender}}",
        "apihelp-query+users-paramvalue-prop-centralids": "{{doc-apihelp-paramvalue|query+users|prop|centralids}}",
+       "apihelp-query+users-paramvalue-prop-cancreate": "{{doc-apihelp-paramvalue|query+users|prop|cancreate}}",
        "apihelp-query+users-param-attachedwiki": "{{doc-apihelp-param|query+users|attachedwiki}}",
        "apihelp-query+users-param-users": "{{doc-apihelp-param|query+users|users}}",
        "apihelp-query+users-param-token": "{{doc-apihelp-param|query+users|token}}",
        "apihelp-query+watchlistraw-param-totitle": "{{doc-apihelp-param|query+watchlistraw|totitle}}",
        "apihelp-query+watchlistraw-example-simple": "{{doc-apihelp-example|query+watchlistraw}}",
        "apihelp-query+watchlistraw-example-generator": "{{doc-apihelp-example|query+watchlistraw}}",
+       "apihelp-removeauthenticationdata-description": "{{doc-apihelp-description|removeauthenticationdata}}",
+       "apihelp-removeauthenticationdata-example-simple": "{{doc-apihelp-example|removeauthenticationdata}}",
+       "apihelp-resetpassword-description": "{{doc-apihelp-description|resetpassword|seealso=* {{msg-mw|apihelp-resetpassword-description-noroutes}}}}",
+       "apihelp-resetpassword-description-noroutes": "{{doc-apihelp-description|resetpassword|info=This message is used when no known routes are enabled in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>.|seealso={{msg-mw|apihelp-resetpassword-description}}}}",
+       "apihelp-resetpassword-param-user": "{{doc-apihelp-param|resetpassword|user}}",
+       "apihelp-resetpassword-param-email": "{{doc-apihelp-param|resetpassword|email}}",
+       "apihelp-resetpassword-param-capture": "{{doc-apihelp-param|resetpassword|capture}}",
+       "apihelp-resetpassword-example-user": "{{doc-apihelp-example|resetpassword}}",
+       "apihelp-resetpassword-example-email": "{{doc-apihelp-example|resetpassword}}",
        "apihelp-revisiondelete-description": "{{doc-apihelp-description|revisiondelete}}",
        "apihelp-revisiondelete-param-type": "{{doc-apihelp-param|revisiondelete|type}}",
        "apihelp-revisiondelete-param-target": "{{doc-apihelp-param|revisiondelete|target}}",
        "apihelp-undelete-param-watchlist": "{{doc-apihelp-param|undelete|watchlist}}",
        "apihelp-undelete-example-page": "{{doc-apihelp-example|undelete}}",
        "apihelp-undelete-example-revisions": "{{doc-apihelp-example|undelete}}",
+       "apihelp-unlinkaccount-description": "{{doc-apihelp-description|unlinkaccount}}",
+       "apihelp-unlinkaccount-example-simple": "{{doc-apihelp-example|unlinkaccount}}",
        "apihelp-upload-description": "{{doc-apihelp-description|upload}}",
        "apihelp-upload-param-filename": "{{doc-apihelp-param|upload|filename}}",
        "apihelp-upload-param-comment": "{{doc-apihelp-param|upload|comment}}",
        "api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated",
        "api-help-right-apihighlimits": "{{technical}}{{doc-right|apihighlimits|prefix=api-help}}\nThis message is used instead of {{msg-mw|right-apihighlimits}} in the API help to display the actual limits.\n\nParameters:\n* $1 - Limit for slow queries\n* $2 - Limit for fast queries",
        "api-help-open-in-apisandbox": "Text for the link to open an API example in [[Special:ApiSandbox]].",
+       "api-help-authmanager-general-usage": "Text giving a brief overview of how to use an AuthManager-using API module. Parameters:\n* $1 - Module parameter prefix, e.g. \"login\"\n* $2 - Module name, e.g. \"clientlogin\"\n* $3 - Module path, e.g. \"clientlogin\"\n* $4 - AuthManager action to use with this module.\n* $5 - Token type needed by the module.",
+       "api-help-authmanagerhelper-requests": "{{doc-apihelp-param|description=the \"requests\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}",
+       "api-help-authmanagerhelper-request": "{{doc-apihelp-param|description=the \"request\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}",
+       "api-help-authmanagerhelper-messageformat": "{{doc-apihelp-param|description=the \"messageformat\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-mergerequestfields": "{{doc-apihelp-param|description=the \"mergerequestfields\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-preservestate": "{{doc-apihelp-param|description=the \"preservestate\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"",
        "api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}",
        "api-credits": "API credits text, displayed in the API help output"
 }
index 246b05c..5de1dee 100644 (file)
        "apihelp-query+allcategories-param-min": "Returnera endast kategorier med minst så här många medlemmar.",
        "apihelp-query+allcategories-param-max": "Returnera endast kategorier med som mest så här många medlemmar.",
        "apihelp-query+allcategories-param-limit": "Hur många kategorier att returnera.",
+       "apihelp-query+allcategories-paramvalue-prop-size": "Lägger till antal sidor i kategorin.",
+       "apihelp-query+allcategories-paramvalue-prop-hidden": "Märker kategorier som är dolda med <code>_&#95;HIDDENCAT_&#95;</code>.",
        "apihelp-query+alldeletedrevisions-description": "Lista alla raderade revisioner av en användare or inom en namnrymd.",
        "apihelp-query+alldeletedrevisions-paraminfo-useronly": "Kan endast användas med <var>$3user</var>.",
        "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "Kan inte användas med <var>$3user</var>.",
diff --git a/includes/auth/AbstractAuthenticationProvider.php b/includes/auth/AbstractAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..9e38ecc
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use Psr\Log\LoggerInterface;
+
+/**
+ * A base class that implements some of the boilerplate for an AuthenticationProvider
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractAuthenticationProvider implements AuthenticationProvider {
+       /** @var LoggerInterface */
+       protected $logger;
+       /** @var AuthManager */
+       protected $manager;
+       /** @var Config */
+       protected $config;
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       public function setManager( AuthManager $manager ) {
+               $this->manager = $manager;
+       }
+
+       public function setConfig( Config $config ) {
+               $this->config = $config;
+       }
+
+       /**
+        * @inheritdoc
+        * @note Override this if it makes sense to support more than one instance
+        */
+       public function getUniqueId() {
+               return static::class;
+       }
+}
diff --git a/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php b/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..900d2e5
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Password;
+use PasswordFactory;
+use Status;
+
+/**
+ * Basic framework for a primary authentication provider that uses passwords
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPasswordPrimaryAuthenticationProvider
+       extends AbstractPrimaryAuthenticationProvider
+{
+       /** @var bool Whether this provider should ABSTAIN (false) or FAIL (true) on password failure */
+       protected $authoritative = true;
+
+       private $passwordFactory = null;
+
+       /**
+        * @param array $params Settings
+        *  - authoritative: Whether this provider should ABSTAIN (false) or FAIL
+        *    (true) on password failure
+        */
+       public function __construct( array $params = [] ) {
+               $this->authoritative = !isset( $params['authoritative'] ) || (bool)$params['authoritative'];
+       }
+
+       /**
+        * Get the PasswordFactory
+        * @return PasswordFactory
+        */
+       protected function getPasswordFactory() {
+               if ( $this->passwordFactory === null ) {
+                       $this->passwordFactory = new PasswordFactory();
+                       $this->passwordFactory->init( $this->config );
+               }
+               return $this->passwordFactory;
+       }
+
+       /**
+        * Get a Password object from the hash
+        * @param string $hash
+        * @return Password
+        */
+       protected function getPassword( $hash ) {
+               $passwordFactory = $this->getPasswordFactory();
+               try {
+                       return $passwordFactory->newFromCiphertext( $hash );
+               } catch ( \PasswordError $e ) {
+                       $class = static::class;
+                       $this->logger->debug( "Invalid password hash in {$class}::getPassword()" );
+                       return $passwordFactory->newFromCiphertext( null );
+               }
+       }
+
+       /**
+        * Return the appropriate response for failure
+        * @param PasswordAuthenticationRequest $req
+        * @return AuthenticationResponse
+        */
+       protected function failResponse( PasswordAuthenticationRequest $req ) {
+               if ( $this->authoritative ) {
+                       return AuthenticationResponse::newFail(
+                               wfMessage( $req->password === '' ? 'wrongpasswordempty' : 'wrongpassword' )
+                       );
+               } else {
+                       return AuthenticationResponse::newAbstain();
+               }
+       }
+
+       /**
+        * Check that the password is valid
+        *
+        * This should be called *before* validating the password. If the result is
+        * not ok, login should fail immediately.
+        *
+        * @param string $username
+        * @param string $password
+        * @return Status
+        */
+       protected function checkPasswordValidity( $username, $password ) {
+               return \User::newFromName( $username )->checkPasswordValidity( $password );
+       }
+
+       /**
+        * Check if the password should be reset
+        *
+        * This should be called after a successful login. It sets 'reset-pass'
+        * authentication data if necessary, see
+        * ResetPassSecondaryAuthenticationProvider.
+        *
+        * @param string $username
+        * @param Status $status From $this->checkPasswordValidity()
+        * @param mixed $data Passed through to $this->getPasswordResetData()
+        */
+       protected function setPasswordResetFlag( $username, Status $status, $data = null ) {
+               $reset = $this->getPasswordResetData( $username, $data );
+
+               if ( !$reset && $this->config->get( 'InvalidPasswordReset' ) && !$status->isGood() ) {
+                       $reset = (object)[
+                               'msg' => $status->getMessage( 'resetpass-validity-soft' ),
+                               'hard' => false,
+                       ];
+               }
+
+               if ( $reset ) {
+                       $this->manager->setAuthenticationSessionData( 'reset-pass', $reset );
+               }
+       }
+
+       /**
+        * Get password reset data, if any
+        *
+        * @param string $username
+        * @param mixed $data
+        * @return object|null { 'hard' => bool, 'msg' => Message }
+        */
+       protected function getPasswordResetData( $username, $data ) {
+               return null;
+       }
+
+       /**
+        * Get expiration date for a new password, if any
+        *
+        * @param string $username
+        * @return string|null
+        */
+       protected function getNewPasswordExpiry( $username ) {
+               $days = $this->config->get( 'PasswordExpirationDays' );
+               $expires = $days ? wfTimestamp( TS_MW, time() + $days * 86400 ) : null;
+
+               // Give extensions a chance to force an expiration
+               \Hooks::run( 'ResetPasswordExpiration', [ \User::newFromName( $username ), &$expires ] );
+
+               return $expires;
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                       case AuthManager::ACTION_REMOVE:
+                       case AuthManager::ACTION_CREATE:
+                       case AuthManager::ACTION_CHANGE:
+                               return [ new PasswordAuthenticationRequest() ];
+                       default:
+                               return [];
+               }
+       }
+}
diff --git a/includes/auth/AbstractPreAuthenticationProvider.php b/includes/auth/AbstractPreAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..48a9c88
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * A base class that implements some of the boilerplate for a PreAuthenticationProvider
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPreAuthenticationProvider extends AbstractAuthenticationProvider
+       implements PreAuthenticationProvider
+{
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function testForAuthentication( array $reqs ) {
+               return \StatusValue::newGood();
+       }
+
+       public function postAuthentication( $user, AuthenticationResponse $response ) {
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               return \StatusValue::newGood();
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               return \StatusValue::newGood();
+       }
+
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+       }
+
+       public function testForAccountLink( $user ) {
+               return \StatusValue::newGood();
+       }
+
+       public function postAccountLink( $user, AuthenticationResponse $response ) {
+       }
+
+}
diff --git a/includes/auth/AbstractPrimaryAuthenticationProvider.php b/includes/auth/AbstractPrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..2e0d669
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A base class that implements some of the boilerplate for a PrimaryAuthenticationProvider
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPrimaryAuthenticationProvider extends AbstractAuthenticationProvider
+       implements PrimaryAuthenticationProvider
+{
+
+       public function continuePrimaryAuthentication( array $reqs ) {
+               throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+       }
+
+       public function postAuthentication( $user, AuthenticationResponse $response ) {
+       }
+
+       public function testUserCanAuthenticate( $username ) {
+               // Assume it can authenticate if it exists
+               return $this->testUserExists( $username );
+       }
+
+       /**
+        * @inheritdoc
+        * @note Reimplement this if you do anything other than
+        *  User::getCanonicalName( $req->username ) to determine the user being
+        *  authenticated.
+        */
+       public function providerNormalizeUsername( $username ) {
+               $name = User::getCanonicalName( $username );
+               return $name === false ? null : $name;
+       }
+
+       /**
+        * @inheritdoc
+        * @note Reimplement this if self::getAuthenticationRequests( AuthManager::ACTION_REMOVE )
+        *  doesn't return requests that will revoke all access for the user.
+        */
+       public function providerRevokeAccessForUser( $username ) {
+               $reqs = $this->getAuthenticationRequests(
+                       AuthManager::ACTION_REMOVE, [ 'username' => $username ]
+               );
+               foreach ( $reqs as $req ) {
+                       $req->username = $username;
+                       $req->action = AuthManager::ACTION_REMOVE;
+                       $this->providerChangeAuthenticationData( $req );
+               }
+       }
+
+       public function providerAllowsPropertyChange( $property ) {
+               return true;
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               return \StatusValue::newGood();
+       }
+
+       public function continuePrimaryAccountCreation( $user, $creator, array $reqs ) {
+               throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+       }
+
+       public function finishAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+               return null;
+       }
+
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               return \StatusValue::newGood();
+       }
+
+       public function autoCreatedAccount( $user, $source ) {
+       }
+
+       public function beginPrimaryAccountLink( $user, array $reqs ) {
+               if ( $this->accountCreationType() === self::TYPE_LINK ) {
+                       throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+               } else {
+                       throw new \BadMethodCallException(
+                               __METHOD__ . ' should not be called on a non-link provider.'
+                       );
+               }
+       }
+
+       public function continuePrimaryAccountLink( $user, array $reqs ) {
+               throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+       }
+
+       public function postAccountLink( $user, AuthenticationResponse $response ) {
+       }
+
+}
diff --git a/includes/auth/AbstractSecondaryAuthenticationProvider.php b/includes/auth/AbstractSecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..89fd6f9
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * A base class that implements some of the boilerplate for a SecondaryAuthenticationProvider
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractSecondaryAuthenticationProvider extends AbstractAuthenticationProvider
+       implements SecondaryAuthenticationProvider
+{
+
+       public function continueSecondaryAuthentication( $user, array $reqs ) {
+               throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+       }
+
+       public function postAuthentication( $user, AuthenticationResponse $response ) {
+       }
+
+       public function providerAllowsPropertyChange( $property ) {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @note Reimplement this if self::getAuthenticationRequests( AuthManager::ACTION_REMOVE )
+        *  doesn't return requests that will revoke all access for the user.
+        */
+       public function providerRevokeAccessForUser( $username ) {
+               $reqs = $this->getAuthenticationRequests(
+                       AuthManager::ACTION_REMOVE, [ 'username' => $username ]
+               );
+               foreach ( $reqs as $req ) {
+                       $req->username = $username;
+                       $this->providerChangeAuthenticationData( $req );
+               }
+       }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               return \StatusValue::newGood( 'ignored' );
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               return \StatusValue::newGood();
+       }
+
+       public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+       }
+
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               return \StatusValue::newGood();
+       }
+
+       public function autoCreatedAccount( $user, $source ) {
+       }
+}
diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php
new file mode 100644 (file)
index 0000000..efee53c
--- /dev/null
@@ -0,0 +1,2386 @@
+<?php
+/**
+ * Authentication (and possibly Authorization in the future) system 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Status;
+use StatusValue;
+use User;
+use WebRequest;
+
+/**
+ * This serves as the entry point to the authentication system.
+ *
+ * In the future, it may also serve as the entry point to the authorization
+ * system.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class AuthManager implements LoggerAwareInterface {
+       /** Log in with an existing (not necessarily local) user */
+       const ACTION_LOGIN = 'login';
+       /** Continue a login process that was interrupted by the need for user input or communication
+        * with an external provider */
+       const ACTION_LOGIN_CONTINUE = 'login-continue';
+       /** Create a new user */
+       const ACTION_CREATE = 'create';
+       /** Continue a user creation process that was interrupted by the need for user input or
+        * communication with an external provider */
+       const ACTION_CREATE_CONTINUE = 'create-continue';
+       /** Link an existing user to a third-party account */
+       const ACTION_LINK = 'link';
+       /** Continue a user linking process that was interrupted by the need for user input or
+        * communication with an external provider */
+       const ACTION_LINK_CONTINUE = 'link-continue';
+       /** Change a user's credentials */
+       const ACTION_CHANGE = 'change';
+       /** Remove a user's credentials */
+       const ACTION_REMOVE = 'remove';
+       /** Like ACTION_REMOVE but for linking providers only */
+       const ACTION_UNLINK = 'unlink';
+
+       /** Security-sensitive operations are ok. */
+       const SEC_OK = 'ok';
+       /** Security-sensitive operations should re-authenticate. */
+       const SEC_REAUTH = 'reauth';
+       /** Security-sensitive should not be performed. */
+       const SEC_FAIL = 'fail';
+
+       /** Auto-creation is due to SessionManager */
+       const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
+
+       /** @var AuthManager|null */
+       private static $instance = null;
+
+       /** @var WebRequest */
+       private $request;
+
+       /** @var Config */
+       private $config;
+
+       /** @var LoggerInterface */
+       private $logger;
+
+       /** @var AuthenticationProvider[] */
+       private $allAuthenticationProviders = [];
+
+       /** @var PreAuthenticationProvider[] */
+       private $preAuthenticationProviders = null;
+
+       /** @var PrimaryAuthenticationProvider[] */
+       private $primaryAuthenticationProviders = null;
+
+       /** @var SecondaryAuthenticationProvider[] */
+       private $secondaryAuthenticationProviders = null;
+
+       /** @var CreatedAccountAuthenticationRequest[] */
+       private $createdAccountAuthenticationRequests = [];
+
+       /**
+        * Get the global AuthManager
+        * @return AuthManager
+        */
+       public static function singleton() {
+               global $wgDisableAuthManager;
+
+               if ( $wgDisableAuthManager ) {
+                       throw new \BadMethodCallException( '$wgDisableAuthManager is set' );
+               }
+
+               if ( self::$instance === null ) {
+                       self::$instance = new self(
+                               \RequestContext::getMain()->getRequest(),
+                               \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+                       );
+               }
+               return self::$instance;
+       }
+
+       /**
+        * @param WebRequest $request
+        * @param Config $config
+        */
+       public function __construct( WebRequest $request, Config $config ) {
+               $this->request = $request;
+               $this->config = $config;
+               $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
+       }
+
+       /**
+        * @param LoggerInterface $logger
+        */
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @return WebRequest
+        */
+       public function getRequest() {
+               return $this->request;
+       }
+
+       /**
+        * Force certain PrimaryAuthenticationProviders
+        * @deprecated For backwards compatibility only
+        * @param PrimaryAuthenticationProvider[] $providers
+        * @param string $why
+        */
+       public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
+               $this->logger->warning( "Overriding AuthManager primary authn because $why" );
+
+               if ( $this->primaryAuthenticationProviders !== null ) {
+                       $this->logger->warning(
+                               'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
+                       );
+
+                       $this->allAuthenticationProviders = array_diff_key(
+                               $this->allAuthenticationProviders,
+                               $this->primaryAuthenticationProviders
+                       );
+                       $session = $this->request->getSession();
+                       $session->remove( 'AuthManager::authnState' );
+                       $session->remove( 'AuthManager::accountCreationState' );
+                       $session->remove( 'AuthManager::accountLinkState' );
+                       $this->createdAccountAuthenticationRequests = [];
+               }
+
+               $this->primaryAuthenticationProviders = [];
+               foreach ( $providers as $provider ) {
+                       if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+                               throw new \RuntimeException(
+                                       'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
+                                               get_class( $provider )
+                               );
+                       }
+                       $provider->setLogger( $this->logger );
+                       $provider->setManager( $this );
+                       $provider->setConfig( $this->config );
+                       $id = $provider->getUniqueId();
+                       if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+                               throw new \RuntimeException(
+                                       "Duplicate specifications for id $id (classes " .
+                                               get_class( $provider ) . ' and ' .
+                                               get_class( $this->allAuthenticationProviders[$id] ) . ')'
+                               );
+                       }
+                       $this->allAuthenticationProviders[$id] = $provider;
+                       $this->primaryAuthenticationProviders[$id] = $provider;
+               }
+       }
+
+       /**
+        * Call a legacy AuthPlugin method, if necessary
+        * @codeCoverageIgnore
+        * @deprecated For backwards compatibility only, should be avoided in new code
+        * @param string $method AuthPlugin method to call
+        * @param array $params Parameters to pass
+        * @param mixed $return Return value if AuthPlugin wasn't called
+        * @return mixed Return value from the AuthPlugin method, or $return
+        */
+       public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
+               global $wgAuth;
+
+               if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
+                       return call_user_func_array( [ $wgAuth, $method ], $params );
+               } else {
+                       return $return;
+               }
+       }
+
+       /**
+        * @name Authentication
+        * @{
+        */
+
+       /**
+        * Indicate whether user authentication is possible
+        *
+        * It may not be if the session is provided by something like OAuth
+        * for which each individual request includes authentication data.
+        *
+        * @return bool
+        */
+       public function canAuthenticateNow() {
+               return $this->request->getSession()->canSetUser();
+       }
+
+       /**
+        * Start an authentication flow
+        * @param AuthenticationRequest[] $reqs
+        * @param string $returnToUrl Url that REDIRECT responses should eventually
+        *  return to.
+        * @return AuthenticationResponse See self::continueAuthentication()
+        */
+       public function beginAuthentication( array $reqs, $returnToUrl ) {
+               $session = $this->request->getSession();
+               if ( !$session->canSetUser() ) {
+                       // Caller should have called canAuthenticateNow()
+                       $session->remove( 'AuthManager::authnState' );
+                       throw new \LogicException( 'Authentication is not possible now' );
+               }
+
+               $guessUserName = null;
+               foreach ( $reqs as $req ) {
+                       $req->returnToUrl = $returnToUrl;
+                       // @codeCoverageIgnoreStart
+                       if ( $req->username !== null && $req->username !== '' ) {
+                               if ( $guessUserName === null ) {
+                                       $guessUserName = $req->username;
+                               } elseif ( $guessUserName !== $req->username ) {
+                                       $guessUserName = null;
+                                       break;
+                               }
+                       }
+                       // @codeCoverageIgnoreEnd
+               }
+
+               // Check for special-case login of a just-created account
+               $req = AuthenticationRequest::getRequestByClass(
+                       $reqs, CreatedAccountAuthenticationRequest::class
+               );
+               if ( $req ) {
+                       if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
+                               throw new \LogicException(
+                                       'CreatedAccountAuthenticationRequests are only valid on ' .
+                                               'the same AuthManager that created the account'
+                               );
+                       }
+
+                       $user = User::newFromName( $req->username );
+                       // @codeCoverageIgnoreStart
+                       if ( !$user ) {
+                               throw new \UnexpectedValueException(
+                                       "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
+                               );
+                       } elseif ( $user->getId() != $req->id ) {
+                               throw new \UnexpectedValueException(
+                                       "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
+                               );
+                       }
+                       // @codeCoverageIgnoreEnd
+
+                       $this->logger->info( 'Logging in {user} after account creation', [
+                               'user' => $user->getName(),
+                       ] );
+                       $ret = AuthenticationResponse::newPass( $user->getName() );
+                       $this->setSessionDataForUser( $user );
+                       $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+                       $session->remove( 'AuthManager::authnState' );
+                       \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+                       return $ret;
+               }
+
+               $this->removeAuthenticationSessionData( null );
+
+               foreach ( $this->getPreAuthenticationProviders() as $provider ) {
+                       $status = $provider->testForAuthentication( $reqs );
+                       if ( !$status->isGood() ) {
+                               $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
+                               $ret = AuthenticationResponse::newFail(
+                                       Status::wrap( $status )->getMessage()
+                               );
+                               $this->callMethodOnProviders( 7, 'postAuthentication',
+                                       [ User::newFromName( $guessUserName ) ?: null, $ret ]
+                               );
+                               \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
+                               return $ret;
+                       }
+               }
+
+               $state = [
+                       'reqs' => $reqs,
+                       'returnToUrl' => $returnToUrl,
+                       'guessUserName' => $guessUserName,
+                       'primary' => null,
+                       'primaryResponse' => null,
+                       'secondary' => [],
+                       'maybeLink' => [],
+                       'continueRequests' => [],
+               ];
+
+               // Preserve state from a previous failed login
+               $req = AuthenticationRequest::getRequestByClass(
+                       $reqs, CreateFromLoginAuthenticationRequest::class
+               );
+               if ( $req ) {
+                       $state['maybeLink'] = $req->maybeLink;
+               }
+
+               $session = $this->request->getSession();
+               $session->setSecret( 'AuthManager::authnState', $state );
+               $session->persist();
+
+               return $this->continueAuthentication( $reqs );
+       }
+
+       /**
+        * Continue an authentication flow
+        *
+        * Return values are interpreted as follows:
+        * - status FAIL: Authentication failed. If $response->createRequest is
+        *   set, that may be passed to self::beginAuthentication() or to
+        *   self::beginAccountCreation() (after adding a username, if necessary)
+        *   to preserve state.
+        * - status REDIRECT: The client should be redirected to the contained URL,
+        *   new AuthenticationRequests should be made (if any), then
+        *   AuthManager::continueAuthentication() should be called.
+        * - status UI: The client should be presented with a user interface for
+        *   the fields in the specified AuthenticationRequests, then new
+        *   AuthenticationRequests should be made, then
+        *   AuthManager::continueAuthentication() should be called.
+        * - status RESTART: The user logged in successfully with a third-party
+        *   service, but the third-party credentials aren't attached to any local
+        *   account. This could be treated as a UI or a FAIL.
+        * - status PASS: Authentication was successful.
+        *
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse
+        */
+       public function continueAuthentication( array $reqs ) {
+               $session = $this->request->getSession();
+               try {
+                       if ( !$session->canSetUser() ) {
+                               // Caller should have called canAuthenticateNow()
+                               // @codeCoverageIgnoreStart
+                               throw new \LogicException( 'Authentication is not possible now' );
+                               // @codeCoverageIgnoreEnd
+                       }
+
+                       $state = $session->getSecret( 'AuthManager::authnState' );
+                       if ( !is_array( $state ) ) {
+                               return AuthenticationResponse::newFail(
+                                       wfMessage( 'authmanager-authn-not-in-progress' )
+                               );
+                       }
+                       $state['continueRequests'] = [];
+
+                       $guessUserName = $state['guessUserName'];
+
+                       foreach ( $reqs as $req ) {
+                               $req->returnToUrl = $state['returnToUrl'];
+                       }
+
+                       // Step 1: Choose an primary authentication provider, and call it until it succeeds.
+
+                       if ( $state['primary'] === null ) {
+                               // We haven't picked a PrimaryAuthenticationProvider yet
+                               // @codeCoverageIgnoreStart
+                               $guessUserName = null;
+                               foreach ( $reqs as $req ) {
+                                       if ( $req->username !== null && $req->username !== '' ) {
+                                               if ( $guessUserName === null ) {
+                                                       $guessUserName = $req->username;
+                                               } elseif ( $guessUserName !== $req->username ) {
+                                                       $guessUserName = null;
+                                                       break;
+                                               }
+                                       }
+                               }
+                               $state['guessUserName'] = $guessUserName;
+                               // @codeCoverageIgnoreEnd
+                               $state['reqs'] = $reqs;
+
+                               foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
+                                       $res = $provider->beginPrimaryAuthentication( $reqs );
+                                       switch ( $res->status ) {
+                                               case AuthenticationResponse::PASS;
+                                                       $state['primary'] = $id;
+                                                       $state['primaryResponse'] = $res;
+                                                       $this->logger->debug( "Primary login with $id succeeded" );
+                                                       break 2;
+                                               case AuthenticationResponse::FAIL;
+                                                       $this->logger->debug( "Login failed in primary authentication by $id" );
+                                                       if ( $res->createRequest || $state['maybeLink'] ) {
+                                                               $res->createRequest = new CreateFromLoginAuthenticationRequest(
+                                                                       $res->createRequest, $state['maybeLink']
+                                                               );
+                                                       }
+                                                       $this->callMethodOnProviders( 7, 'postAuthentication',
+                                                               [ User::newFromName( $guessUserName ) ?: null, $res ]
+                                                       );
+                                                       $session->remove( 'AuthManager::authnState' );
+                                                       \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
+                                                       return $res;
+                                               case AuthenticationResponse::ABSTAIN;
+                                                       // Continue loop
+                                                       break;
+                                               case AuthenticationResponse::REDIRECT;
+                                               case AuthenticationResponse::UI;
+                                                       $this->logger->debug( "Primary login with $id returned $res->status" );
+                                                       $state['primary'] = $id;
+                                                       $state['continueRequests'] = $res->neededRequests;
+                                                       $session->setSecret( 'AuthManager::authnState', $state );
+                                                       return $res;
+
+                                                       // @codeCoverageIgnoreStart
+                                               default:
+                                                       throw new \DomainException(
+                                                               get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
+                                                       );
+                                                       // @codeCoverageIgnoreEnd
+                                       }
+                               }
+                               if ( $state['primary'] === null ) {
+                                       $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
+                                       $ret = AuthenticationResponse::newFail(
+                                               wfMessage( 'authmanager-authn-no-primary' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAuthentication',
+                                               [ User::newFromName( $guessUserName ) ?: null, $ret ]
+                                       );
+                                       $session->remove( 'AuthManager::authnState' );
+                                       return $ret;
+                               }
+                       } elseif ( $state['primaryResponse'] === null ) {
+                               $provider = $this->getAuthenticationProvider( $state['primary'] );
+                               if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+                                       // Configuration changed? Force them to start over.
+                                       // @codeCoverageIgnoreStart
+                                       $ret = AuthenticationResponse::newFail(
+                                               wfMessage( 'authmanager-authn-not-in-progress' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAuthentication',
+                                               [ User::newFromName( $guessUserName ) ?: null, $ret ]
+                                       );
+                                       $session->remove( 'AuthManager::authnState' );
+                                       return $ret;
+                                       // @codeCoverageIgnoreEnd
+                               }
+                               $id = $provider->getUniqueId();
+                               $res = $provider->continuePrimaryAuthentication( $reqs );
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS;
+                                               $state['primaryResponse'] = $res;
+                                               $this->logger->debug( "Primary login with $id succeeded" );
+                                               break;
+                                       case AuthenticationResponse::FAIL;
+                                               $this->logger->debug( "Login failed in primary authentication by $id" );
+                                               if ( $res->createRequest || $state['maybeLink'] ) {
+                                                       $res->createRequest = new CreateFromLoginAuthenticationRequest(
+                                                               $res->createRequest, $state['maybeLink']
+                                                       );
+                                               }
+                                               $this->callMethodOnProviders( 7, 'postAuthentication',
+                                                       [ User::newFromName( $guessUserName ) ?: null, $res ]
+                                               );
+                                               $session->remove( 'AuthManager::authnState' );
+                                               \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
+                                               return $res;
+                                       case AuthenticationResponse::REDIRECT;
+                                       case AuthenticationResponse::UI;
+                                               $this->logger->debug( "Primary login with $id returned $res->status" );
+                                               $state['continueRequests'] = $res->neededRequests;
+                                               $session->setSecret( 'AuthManager::authnState', $state );
+                                               return $res;
+                                       default:
+                                               throw new \DomainException(
+                                                       get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
+                                               );
+                               }
+                       }
+
+                       $res = $state['primaryResponse'];
+                       if ( $res->username === null ) {
+                               $provider = $this->getAuthenticationProvider( $state['primary'] );
+                               if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+                                       // Configuration changed? Force them to start over.
+                                       // @codeCoverageIgnoreStart
+                                       $ret = AuthenticationResponse::newFail(
+                                               wfMessage( 'authmanager-authn-not-in-progress' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAuthentication',
+                                               [ User::newFromName( $guessUserName ) ?: null, $ret ]
+                                       );
+                                       $session->remove( 'AuthManager::authnState' );
+                                       return $ret;
+                                       // @codeCoverageIgnoreEnd
+                               }
+
+                               if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
+                                       $res->linkRequest &&
+                                        // don't confuse the user with an incorrect message if linking is disabled
+                                       $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
+                               ) {
+                                       $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
+                                       $msg = 'authmanager-authn-no-local-user-link';
+                               } else {
+                                       $msg = 'authmanager-authn-no-local-user';
+                               }
+                               $this->logger->debug(
+                                       "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
+                               );
+                               $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
+                               $ret->neededRequests = $this->getAuthenticationRequestsInternal(
+                                       self::ACTION_LOGIN,
+                                       [],
+                                       $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
+                               );
+                               if ( $res->createRequest || $state['maybeLink'] ) {
+                                       $ret->createRequest = new CreateFromLoginAuthenticationRequest(
+                                               $res->createRequest, $state['maybeLink']
+                                       );
+                                       $ret->neededRequests[] = $ret->createRequest;
+                               }
+                               $session->setSecret( 'AuthManager::authnState', [
+                                       'reqs' => [], // Will be filled in later
+                                       'primary' => null,
+                                       'primaryResponse' => null,
+                                       'secondary' => [],
+                                       'continueRequests' => $ret->neededRequests,
+                               ] + $state );
+                               return $ret;
+                       }
+
+                       // Step 2: Primary authentication succeeded, create the User object
+                       // (and add the user locally if necessary)
+
+                       $user = User::newFromName( $res->username, 'usable' );
+                       if ( !$user ) {
+                               throw new \DomainException(
+                                       get_class( $provider ) . " returned an invalid username: {$res->username}"
+                               );
+                       }
+                       if ( $user->getId() === 0 ) {
+                               // User doesn't exist locally. Create it.
+                               $this->logger->info( 'Auto-creating {user} on login', [
+                                       'user' => $user->getName(),
+                               ] );
+                               $status = $this->autoCreateUser( $user, $state['primary'], false );
+                               if ( !$status->isGood() ) {
+                                       $ret = AuthenticationResponse::newFail(
+                                               Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+                                       $session->remove( 'AuthManager::authnState' );
+                                       \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+                                       return $ret;
+                               }
+                       }
+
+                       // Step 3: Iterate over all the secondary authentication providers.
+
+                       $beginReqs = $state['reqs'];
+
+                       foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
+                               if ( !isset( $state['secondary'][$id] ) ) {
+                                       // This provider isn't started yet, so we pass it the set
+                                       // of reqs from beginAuthentication instead of whatever
+                                       // might have been used by a previous provider in line.
+                                       $func = 'beginSecondaryAuthentication';
+                                       $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
+                               } elseif ( !$state['secondary'][$id] ) {
+                                       $func = 'continueSecondaryAuthentication';
+                                       $res = $provider->continueSecondaryAuthentication( $user, $reqs );
+                               } else {
+                                       continue;
+                               }
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS;
+                                               $this->logger->debug( "Secondary login with $id succeeded" );
+                                               // fall through
+                                       case AuthenticationResponse::ABSTAIN;
+                                               $state['secondary'][$id] = true;
+                                               break;
+                                       case AuthenticationResponse::FAIL;
+                                               $this->logger->debug( "Login failed in secondary authentication by $id" );
+                                               $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
+                                               $session->remove( 'AuthManager::authnState' );
+                                               \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
+                                               return $res;
+                                       case AuthenticationResponse::REDIRECT;
+                                       case AuthenticationResponse::UI;
+                                               $this->logger->debug( "Secondary login with $id returned " . $res->status );
+                                               $state['secondary'][$id] = false;
+                                               $state['continueRequests'] = $res->neededRequests;
+                                               $session->setSecret( 'AuthManager::authnState', $state );
+                                               return $res;
+
+                                               // @codeCoverageIgnoreStart
+                                       default:
+                                               throw new \DomainException(
+                                                       get_class( $provider ) . "::{$func}() returned $res->status"
+                                               );
+                                               // @codeCoverageIgnoreEnd
+                               }
+                       }
+
+                       // Step 4: Authentication complete! Set the user in the session and
+                       // clean up.
+
+                       $this->logger->info( 'Login for {user} succeeded', [
+                               'user' => $user->getName(),
+                       ] );
+                       $req = AuthenticationRequest::getRequestByClass(
+                               $beginReqs, RememberMeAuthenticationRequest::class
+                       );
+                       $this->setSessionDataForUser( $user, $req && $req->rememberMe );
+                       $ret = AuthenticationResponse::newPass( $user->getName() );
+                       $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+                       $session->remove( 'AuthManager::authnState' );
+                       $this->removeAuthenticationSessionData( null );
+                       \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+                       return $ret;
+               } catch ( \Exception $ex ) {
+                       $session->remove( 'AuthManager::authnState' );
+                       throw $ex;
+               }
+       }
+
+       /**
+        * Whether security-sensitive operations should proceed.
+        *
+        * A "security-sensitive operation" is something like a password or email
+        * change, that would normally have a "reenter your password to confirm"
+        * box if we only supported password-based authentication.
+        *
+        * @param string $operation Operation being checked. This should be a
+        *  message-key-like string such as 'change-password' or 'change-email'.
+        * @return string One of the SEC_* constants.
+        */
+       public function securitySensitiveOperationStatus( $operation ) {
+               $status = self::SEC_OK;
+
+               $this->logger->debug( __METHOD__ . ": Checking $operation" );
+
+               $session = $this->request->getSession();
+               $aId = $session->getUser()->getId();
+               if ( $aId === 0 ) {
+                       // User isn't authenticated. DWIM?
+                       $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
+                       $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
+                       return $status;
+               }
+
+               if ( $session->canSetUser() ) {
+                       $id = $session->get( 'AuthManager:lastAuthId' );
+                       $last = $session->get( 'AuthManager:lastAuthTimestamp' );
+                       if ( $id !== $aId || $last === null ) {
+                               $timeSinceLogin = PHP_INT_MAX; // Forever ago
+                       } else {
+                               $timeSinceLogin = max( 0, time() - $last );
+                       }
+
+                       $thresholds = $this->config->get( 'ReauthenticateTime' );
+                       if ( isset( $thresholds[$operation] ) ) {
+                               $threshold = $thresholds[$operation];
+                       } elseif ( isset( $thresholds['default'] ) ) {
+                               $threshold = $thresholds['default'];
+                       } else {
+                               throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
+                       }
+
+                       if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
+                               $status = self::SEC_REAUTH;
+                       }
+               } else {
+                       $timeSinceLogin = -1;
+
+                       $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
+                       if ( isset( $pass[$operation] ) ) {
+                               $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
+                       } elseif ( isset( $pass['default'] ) ) {
+                               $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
+                       } else {
+                               throw new \UnexpectedValueException(
+                                       '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
+                               );
+                       }
+               }
+
+               \Hooks::run( 'SecuritySensitiveOperationStatus', [
+                       &$status, $operation, $session, $timeSinceLogin
+               ] );
+
+               // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
+               if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
+                       $status = self::SEC_FAIL;
+               }
+
+               $this->logger->info( __METHOD__ . ": $operation is $status" );
+
+               return $status;
+       }
+
+       /**
+        * Determine whether a username can authenticate
+        *
+        * @param string $username
+        * @return bool
+        */
+       public function userCanAuthenticate( $username ) {
+               foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+                       if ( $provider->testUserCanAuthenticate( $username ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Provide normalized versions of the username for security checks
+        *
+        * Since different providers can normalize the input in different ways,
+        * this returns an array of all the different ways the name might be
+        * normalized for authentication.
+        *
+        * The returned strings should not be revealed to the user, as that might
+        * leak private information (e.g. an email address might be normalized to a
+        * username).
+        *
+        * @param string $username
+        * @return string[]
+        */
+       public function normalizeUsername( $username ) {
+               $ret = [];
+               foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+                       $normalized = $provider->providerNormalizeUsername( $username );
+                       if ( $normalized !== null ) {
+                               $ret[$normalized] = true;
+                       }
+               }
+               return array_keys( $ret );
+       }
+
+       /**@}*/
+
+       /**
+        * @name Authentication data changing
+        * @{
+        */
+
+       /**
+        * Revoke any authentication credentials for a user
+        *
+        * After this, the user should no longer be able to log in.
+        *
+        * @param string $username
+        */
+       public function revokeAccessForUser( $username ) {
+               $this->logger->info( 'Revoking access for {user}', [
+                       'user' => $username,
+               ] );
+               $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
+       }
+
+       /**
+        * Validate a change of authentication data (e.g. passwords)
+        * @param AuthenticationRequest $req
+        * @param bool $checkData If false, $req hasn't been loaded from the
+        *  submission so checks on user-submitted fields should be skipped. $req->username is
+        *  considered user-submitted for this purpose, even if it cannot be changed via
+        *  $req->loadFromSubmission.
+        * @return Status
+        */
+       public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
+               $any = false;
+               $providers = $this->getPrimaryAuthenticationProviders() +
+                       $this->getSecondaryAuthenticationProviders();
+               foreach ( $providers as $provider ) {
+                       $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
+                       if ( !$status->isGood() ) {
+                               return Status::wrap( $status );
+                       }
+                       $any = $any || $status->value !== 'ignored';
+               }
+               if ( !$any ) {
+                       $status = Status::newGood( 'ignored' );
+                       $status->warning( 'authmanager-change-not-supported' );
+                       return $status;
+               }
+               return Status::newGood();
+       }
+
+       /**
+        * Change authentication data (e.g. passwords)
+        *
+        * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
+        * result in a successful login in the future.
+        *
+        * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
+        * no longer result in a successful login.
+        *
+        * @param AuthenticationRequest $req
+        */
+       public function changeAuthenticationData( AuthenticationRequest $req ) {
+               $this->logger->info( 'Changing authentication data for {user} class {what}', [
+                       'user' => is_string( $req->username ) ? $req->username : '<no name>',
+                       'what' => get_class( $req ),
+               ] );
+
+               $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
+
+               // When the main account's authentication data is changed, invalidate
+               // all BotPasswords too.
+               \BotPassword::invalidateAllPasswordsForUser( $req->username );
+       }
+
+       /**@}*/
+
+       /**
+        * @name Account creation
+        * @{
+        */
+
+       /**
+        * Determine whether accounts can be created
+        * @return bool
+        */
+       public function canCreateAccounts() {
+               foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+                       switch ( $provider->accountCreationType() ) {
+                               case PrimaryAuthenticationProvider::TYPE_CREATE:
+                               case PrimaryAuthenticationProvider::TYPE_LINK:
+                                       return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Determine whether a particular account can be created
+        * @param string $username
+        * @param int $flags Bitfield of User:READ_* constants
+        * @return Status
+        */
+       public function canCreateAccount( $username, $flags = User::READ_NORMAL ) {
+               if ( !$this->canCreateAccounts() ) {
+                       return Status::newFatal( 'authmanager-create-disabled' );
+               }
+
+               if ( $this->userExists( $username, $flags ) ) {
+                       return Status::newFatal( 'userexists' );
+               }
+
+               $user = User::newFromName( $username, 'creatable' );
+               if ( !is_object( $user ) ) {
+                       return Status::newFatal( 'noname' );
+               } else {
+                       $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
+                       if ( $user->getId() !== 0 ) {
+                               return Status::newFatal( 'userexists' );
+                       }
+               }
+
+               // Denied by providers?
+               $providers = $this->getPreAuthenticationProviders() +
+                       $this->getPrimaryAuthenticationProviders() +
+                       $this->getSecondaryAuthenticationProviders();
+               foreach ( $providers as $provider ) {
+                       $status = $provider->testUserForCreation( $user, false );
+                       if ( !$status->isGood() ) {
+                               return Status::wrap( $status );
+                       }
+               }
+
+               return Status::newGood();
+       }
+
+       /**
+        * Basic permissions checks on whether a user can create accounts
+        * @param User $creator User doing the account creation
+        * @return Status
+        */
+       public function checkAccountCreatePermissions( User $creator ) {
+               // Wiki is read-only?
+               if ( wfReadOnly() ) {
+                       return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
+               }
+
+               // This is awful, this permission check really shouldn't go through Title.
+               $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
+                       ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
+               if ( $permErrors ) {
+                       $status = Status::newGood();
+                       foreach ( $permErrors as $args ) {
+                               call_user_func_array( [ $status, 'fatal' ], $args );
+                       }
+                       return $status;
+               }
+
+               $block = $creator->isBlockedFromCreateAccount();
+               if ( $block ) {
+                       $errorParams = [
+                               $block->getTarget(),
+                               $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
+                               $block->getByName()
+                       ];
+
+                       if ( $block->getType() === \Block::TYPE_RANGE ) {
+                               $errorMessage = 'cantcreateaccount-range-text';
+                               $errorParams[] = $this->getRequest()->getIP();
+                       } else {
+                               $errorMessage = 'cantcreateaccount-text';
+                       }
+
+                       return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
+               }
+
+               $ip = $this->getRequest()->getIP();
+               if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
+                       return Status::newFatal( 'sorbs_create_account_reason' );
+               }
+
+               return Status::newGood();
+       }
+
+       /**
+        * Start an account creation flow
+        * @param User $creator User doing the account creation
+        * @param AuthenticationRequest[] $reqs
+        * @param string $returnToUrl Url that REDIRECT responses should eventually
+        *  return to.
+        * @return AuthenticationResponse
+        */
+       public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
+               $session = $this->request->getSession();
+               if ( !$this->canCreateAccounts() ) {
+                       // Caller should have called canCreateAccounts()
+                       $session->remove( 'AuthManager::accountCreationState' );
+                       throw new \LogicException( 'Account creation is not possible' );
+               }
+
+               try {
+                       $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $username = null;
+               }
+               if ( $username === null ) {
+                       $this->logger->debug( __METHOD__ . ': No username provided' );
+                       return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+               }
+
+               // Permissions check
+               $status = $this->checkAccountCreatePermissions( $creator );
+               if ( !$status->isGood() ) {
+                       $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
+                               'user' => $username,
+                               'creator' => $creator->getName(),
+                               'reason' => $status->getWikiText( null, null, 'en' )
+                       ] );
+                       return AuthenticationResponse::newFail( $status->getMessage() );
+               }
+
+               $status = $this->canCreateAccount( $username, User::READ_LOCKING );
+               if ( !$status->isGood() ) {
+                       $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
+                               'user' => $username,
+                               'creator' => $creator->getName(),
+                               'reason' => $status->getWikiText( null, null, 'en' )
+                       ] );
+                       return AuthenticationResponse::newFail( $status->getMessage() );
+               }
+
+               $user = User::newFromName( $username, 'creatable' );
+               foreach ( $reqs as $req ) {
+                       $req->username = $username;
+                       $req->returnToUrl = $returnToUrl;
+                       if ( $req instanceof UserDataAuthenticationRequest ) {
+                               $status = $req->populateUser( $user );
+                               if ( !$status->isGood() ) {
+                                       $status = Status::wrap( $status );
+                                       $session->remove( 'AuthManager::accountCreationState' );
+                                       $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
+                                               'user' => $user->getName(),
+                                               'creator' => $creator->getName(),
+                                               'reason' => $status->getWikiText( null, null, 'en' ),
+                                       ] );
+                                       return AuthenticationResponse::newFail( $status->getMessage() );
+                               }
+                       }
+               }
+
+               $this->removeAuthenticationSessionData( null );
+
+               $state = [
+                       'username' => $username,
+                       'userid' => 0,
+                       'creatorid' => $creator->getId(),
+                       'creatorname' => $creator->getName(),
+                       'reqs' => $reqs,
+                       'returnToUrl' => $returnToUrl,
+                       'primary' => null,
+                       'primaryResponse' => null,
+                       'secondary' => [],
+                       'continueRequests' => [],
+                       'maybeLink' => [],
+                       'ranPreTests' => false,
+               ];
+
+               // Special case: converting a login to an account creation
+               $req = AuthenticationRequest::getRequestByClass(
+                       $reqs, CreateFromLoginAuthenticationRequest::class
+               );
+               if ( $req ) {
+                       $state['maybeLink'] = $req->maybeLink;
+
+                       // If we get here, the user didn't submit a form with any of the
+                       // usual AuthenticationRequests that are needed for an account
+                       // creation. So we need to determine if there are any and return a
+                       // UI response if so.
+                       if ( $req->createRequest ) {
+                               // We have a createRequest from a
+                               // PrimaryAuthenticationProvider, so don't ask.
+                               $providers = $this->getPreAuthenticationProviders() +
+                                       $this->getSecondaryAuthenticationProviders();
+                       } else {
+                               // We're only preserving maybeLink, so ask for primary fields
+                               // too.
+                               $providers = $this->getPreAuthenticationProviders() +
+                                       $this->getPrimaryAuthenticationProviders() +
+                                       $this->getSecondaryAuthenticationProviders();
+                       }
+                       $reqs = $this->getAuthenticationRequestsInternal(
+                               self::ACTION_CREATE,
+                               [],
+                               $providers
+                       );
+                       // See if we need any requests to begin
+                       foreach ( (array)$reqs as $r ) {
+                               if ( !$r instanceof UsernameAuthenticationRequest &&
+                                       !$r instanceof UserDataAuthenticationRequest &&
+                                       !$r instanceof CreationReasonAuthenticationRequest
+                               ) {
+                                       // Needs some reqs, so request them
+                                       $reqs[] = new CreateFromLoginAuthenticationRequest( $req->createRequest, [] );
+                                       $state['continueRequests'] = $reqs;
+                                       $session->setSecret( 'AuthManager::accountCreationState', $state );
+                                       $session->persist();
+                                       return AuthenticationResponse::newUI( $reqs, wfMessage( 'authmanager-create-from-login' ) );
+                               }
+                       }
+                       // No reqs needed, so we can just continue.
+                       $req->createRequest->returnToUrl = $returnToUrl;
+                       $reqs = [ $req->createRequest ];
+               }
+
+               $session->setSecret( 'AuthManager::accountCreationState', $state );
+               $session->persist();
+
+               return $this->continueAccountCreation( $reqs );
+       }
+
+       /**
+        * Continue an account creation flow
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse
+        */
+       public function continueAccountCreation( array $reqs ) {
+               $session = $this->request->getSession();
+               try {
+                       if ( !$this->canCreateAccounts() ) {
+                               // Caller should have called canCreateAccounts()
+                               $session->remove( 'AuthManager::accountCreationState' );
+                               throw new \LogicException( 'Account creation is not possible' );
+                       }
+
+                       $state = $session->getSecret( 'AuthManager::accountCreationState' );
+                       if ( !is_array( $state ) ) {
+                               return AuthenticationResponse::newFail(
+                                       wfMessage( 'authmanager-create-not-in-progress' )
+                               );
+                       }
+                       $state['continueRequests'] = [];
+
+                       // Step 0: Prepare and validate the input
+
+                       $user = User::newFromName( $state['username'], 'creatable' );
+                       if ( !is_object( $user ) ) {
+                               $session->remove( 'AuthManager::accountCreationState' );
+                               $this->logger->debug( __METHOD__ . ': Invalid username', [
+                                       'user' => $state['username'],
+                               ] );
+                               return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+                       }
+
+                       if ( $state['creatorid'] ) {
+                               $creator = User::newFromId( $state['creatorid'] );
+                       } else {
+                               $creator = new User;
+                               $creator->setName( $state['creatorname'] );
+                       }
+
+                       // Avoid account creation races on double submissions
+                       $cache = \ObjectCache::getLocalClusterInstance();
+                       $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
+                       if ( !$lock ) {
+                               // Don't clear AuthManager::accountCreationState for this code
+                               // path because the process that won the race owns it.
+                               $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
+                                       'user' => $user->getName(),
+                                       'creator' => $creator->getName(),
+                               ] );
+                               return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
+                       }
+
+                       // Permissions check
+                       $status = $this->checkAccountCreatePermissions( $creator );
+                       if ( !$status->isGood() ) {
+                               $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
+                                       'user' => $user->getName(),
+                                       'creator' => $creator->getName(),
+                                       'reason' => $status->getWikiText( null, null, 'en' )
+                               ] );
+                               $ret = AuthenticationResponse::newFail( $status->getMessage() );
+                               $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                               $session->remove( 'AuthManager::accountCreationState' );
+                               return $ret;
+                       }
+
+                       // Load from master for existence check
+                       $user->load( User::READ_LOCKING );
+
+                       if ( $state['userid'] === 0 ) {
+                               if ( $user->getId() != 0 ) {
+                                       $this->logger->debug( __METHOD__ . ': User exists locally', [
+                                               'user' => $user->getName(),
+                                               'creator' => $creator->getName(),
+                                       ] );
+                                       $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
+                                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                       $session->remove( 'AuthManager::accountCreationState' );
+                                       return $ret;
+                               }
+                       } else {
+                               if ( $user->getId() == 0 ) {
+                                       $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
+                                               'user' => $user->getName(),
+                                               'creator' => $creator->getName(),
+                                               'expected_id' => $state['userid'],
+                                       ] );
+                                       throw new \UnexpectedValueException(
+                                               "User \"{$state['username']}\" should exist now, but doesn't!"
+                                       );
+                               }
+                               if ( $user->getId() != $state['userid'] ) {
+                                       $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
+                                               'user' => $user->getName(),
+                                               'creator' => $creator->getName(),
+                                               'expected_id' => $state['userid'],
+                                               'actual_id' => $user->getId(),
+                                       ] );
+                                       throw new \UnexpectedValueException(
+                                               "User \"{$state['username']}\" exists, but " .
+                                                       "ID {$user->getId()} != {$state['userid']}!"
+                                       );
+                               }
+                       }
+                       foreach ( $state['reqs'] as $req ) {
+                               if ( $req instanceof UserDataAuthenticationRequest ) {
+                                       $status = $req->populateUser( $user );
+                                       if ( !$status->isGood() ) {
+                                               // This should never happen...
+                                               $status = Status::wrap( $status );
+                                               $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                                       'reason' => $status->getWikiText( null, null, 'en' ),
+                                               ] );
+                                               $ret = AuthenticationResponse::newFail( $status->getMessage() );
+                                               $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                               $session->remove( 'AuthManager::accountCreationState' );
+                                               return $ret;
+                                       }
+                               }
+                       }
+
+                       foreach ( $reqs as $req ) {
+                               $req->returnToUrl = $state['returnToUrl'];
+                               $req->username = $state['username'];
+                       }
+
+                       // If we're coming in from a create-from-login UI response, we need
+                       // to extract the createRequest (if any).
+                       $req = AuthenticationRequest::getRequestByClass(
+                               $reqs, CreateFromLoginAuthenticationRequest::class
+                       );
+                       if ( $req && $req->createRequest ) {
+                               $reqs[] = $req->createRequest;
+                       }
+
+                       // Run pre-creation tests, if we haven't already
+                       if ( !$state['ranPreTests'] ) {
+                               $providers = $this->getPreAuthenticationProviders() +
+                                       $this->getPrimaryAuthenticationProviders() +
+                                       $this->getSecondaryAuthenticationProviders();
+                               foreach ( $providers as $id => $provider ) {
+                                       $status = $provider->testForAccountCreation( $user, $creator, $reqs );
+                                       if ( !$status->isGood() ) {
+                                               $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               $ret = AuthenticationResponse::newFail(
+                                                       Status::wrap( $status )->getMessage()
+                                               );
+                                               $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                               $session->remove( 'AuthManager::accountCreationState' );
+                                               return $ret;
+                                       }
+                               }
+
+                               $state['ranPreTests'] = true;
+                       }
+
+                       // Step 1: Choose a primary authentication provider and call it until it succeeds.
+
+                       if ( $state['primary'] === null ) {
+                               // We haven't picked a PrimaryAuthenticationProvider yet
+                               foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
+                                       if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
+                                               continue;
+                                       }
+                                       $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
+                                       switch ( $res->status ) {
+                                               case AuthenticationResponse::PASS;
+                                                       $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
+                                                               'user' => $user->getName(),
+                                                               'creator' => $creator->getName(),
+                                                       ] );
+                                                       $state['primary'] = $id;
+                                                       $state['primaryResponse'] = $res;
+                                                       break 2;
+                                               case AuthenticationResponse::FAIL;
+                                                       $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
+                                                               'user' => $user->getName(),
+                                                               'creator' => $creator->getName(),
+                                                       ] );
+                                                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
+                                                       $session->remove( 'AuthManager::accountCreationState' );
+                                                       return $res;
+                                               case AuthenticationResponse::ABSTAIN;
+                                                       // Continue loop
+                                                       break;
+                                               case AuthenticationResponse::REDIRECT;
+                                               case AuthenticationResponse::UI;
+                                                       $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
+                                                               'user' => $user->getName(),
+                                                               'creator' => $creator->getName(),
+                                                       ] );
+                                                       $state['primary'] = $id;
+                                                       $state['continueRequests'] = $res->neededRequests;
+                                                       $session->setSecret( 'AuthManager::accountCreationState', $state );
+                                                       return $res;
+
+                                                       // @codeCoverageIgnoreStart
+                                               default:
+                                                       throw new \DomainException(
+                                                               get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
+                                                       );
+                                                       // @codeCoverageIgnoreEnd
+                                       }
+                               }
+                               if ( $state['primary'] === null ) {
+                                       $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
+                                               'user' => $user->getName(),
+                                               'creator' => $creator->getName(),
+                                       ] );
+                                       $ret = AuthenticationResponse::newFail(
+                                               wfMessage( 'authmanager-create-no-primary' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                       $session->remove( 'AuthManager::accountCreationState' );
+                                       return $ret;
+                               }
+                       } elseif ( $state['primaryResponse'] === null ) {
+                               $provider = $this->getAuthenticationProvider( $state['primary'] );
+                               if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+                                       // Configuration changed? Force them to start over.
+                                       // @codeCoverageIgnoreStart
+                                       $ret = AuthenticationResponse::newFail(
+                                               wfMessage( 'authmanager-create-not-in-progress' )
+                                       );
+                                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                       $session->remove( 'AuthManager::accountCreationState' );
+                                       return $ret;
+                                       // @codeCoverageIgnoreEnd
+                               }
+                               $id = $provider->getUniqueId();
+                               $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS;
+                                               $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               $state['primaryResponse'] = $res;
+                                               break;
+                                       case AuthenticationResponse::FAIL;
+                                               $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
+                                               $session->remove( 'AuthManager::accountCreationState' );
+                                               return $res;
+                                       case AuthenticationResponse::REDIRECT;
+                                       case AuthenticationResponse::UI;
+                                               $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               $state['continueRequests'] = $res->neededRequests;
+                                               $session->setSecret( 'AuthManager::accountCreationState', $state );
+                                               return $res;
+                                       default:
+                                               throw new \DomainException(
+                                                       get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
+                                               );
+                               }
+                       }
+
+                       // Step 2: Primary authentication succeeded, create the User object
+                       // and add the user locally.
+
+                       if ( $state['userid'] === 0 ) {
+                               $this->logger->info( 'Creating user {user} during account creation', [
+                                       'user' => $user->getName(),
+                                       'creator' => $creator->getName(),
+                               ] );
+                               $status = $user->addToDatabase();
+                               if ( !$status->isOk() ) {
+                                       // @codeCoverageIgnoreStart
+                                       $ret = AuthenticationResponse::newFail( $status->getMessage() );
+                                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                                       $session->remove( 'AuthManager::accountCreationState' );
+                                       return $ret;
+                                       // @codeCoverageIgnoreEnd
+                               }
+                               $this->setDefaultUserOptions( $user, $creator->isAnon() );
+                               \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+                               $user->saveSettings();
+                               $state['userid'] = $user->getId();
+
+                               // Update user count
+                               \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+
+                               // Watch user's userpage and talk page
+                               $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+
+                               // Inform the provider
+                               $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
+
+                               // Log the creation
+                               if ( $this->config->get( 'NewUserLog' ) ) {
+                                       $isAnon = $creator->isAnon();
+                                       $logEntry = new \ManualLogEntry(
+                                               'newusers',
+                                               $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
+                                       );
+                                       $logEntry->setPerformer( $isAnon ? $user : $creator );
+                                       $logEntry->setTarget( $user->getUserPage() );
+                                       $req = AuthenticationRequest::getRequestByClass(
+                                               $state['reqs'], CreationReasonAuthenticationRequest::class
+                                       );
+                                       $logEntry->setComment( $req ? $req->reason : '' );
+                                       $logEntry->setParameters( [
+                                               '4::userid' => $user->getId(),
+                                       ] );
+                                       $logid = $logEntry->insert();
+                                       $logEntry->publish( $logid );
+                               }
+                       }
+
+                       // Step 3: Iterate over all the secondary authentication providers.
+
+                       $beginReqs = $state['reqs'];
+
+                       foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
+                               if ( !isset( $state['secondary'][$id] ) ) {
+                                       // This provider isn't started yet, so we pass it the set
+                                       // of reqs from beginAuthentication instead of whatever
+                                       // might have been used by a previous provider in line.
+                                       $func = 'beginSecondaryAccountCreation';
+                                       $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
+                               } elseif ( !$state['secondary'][$id] ) {
+                                       $func = 'continueSecondaryAccountCreation';
+                                       $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
+                               } else {
+                                       continue;
+                               }
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS;
+                                               $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               // fall through
+                                       case AuthenticationResponse::ABSTAIN;
+                                               $state['secondary'][$id] = true;
+                                               break;
+                                       case AuthenticationResponse::REDIRECT;
+                                       case AuthenticationResponse::UI;
+                                               $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
+                                                       'user' => $user->getName(),
+                                                       'creator' => $creator->getName(),
+                                               ] );
+                                               $state['secondary'][$id] = false;
+                                               $state['continueRequests'] = $res->neededRequests;
+                                               $session->setSecret( 'AuthManager::accountCreationState', $state );
+                                               return $res;
+                                       case AuthenticationResponse::FAIL;
+                                               throw new \DomainException(
+                                                       get_class( $provider ) . "::{$func}() returned $res->status." .
+                                                       ' Secondary providers are not allowed to fail account creation, that' .
+                                                       ' should have been done via testForAccountCreation().'
+                                               );
+                                                       // @codeCoverageIgnoreStart
+                                       default:
+                                               throw new \DomainException(
+                                                       get_class( $provider ) . "::{$func}() returned $res->status"
+                                               );
+                                                       // @codeCoverageIgnoreEnd
+                               }
+                       }
+
+                       $id = $user->getId();
+                       $name = $user->getName();
+                       $req = new CreatedAccountAuthenticationRequest( $id, $name );
+                       $ret = AuthenticationResponse::newPass( $name );
+                       $ret->loginRequest = $req;
+                       $this->createdAccountAuthenticationRequests[] = $req;
+
+                       $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
+                               'user' => $user->getName(),
+                               'creator' => $creator->getName(),
+                       ] );
+
+                       $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+                       $session->remove( 'AuthManager::accountCreationState' );
+                       $this->removeAuthenticationSessionData( null );
+                       return $ret;
+               } catch ( \Exception $ex ) {
+                       $session->remove( 'AuthManager::accountCreationState' );
+                       throw $ex;
+               }
+       }
+
+       /**
+        * Auto-create an account, and log into that account
+        * @param User $user User to auto-create
+        * @param string $source What caused the auto-creation? This must be the ID
+        *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
+        * @param bool $login Whether to also log the user in
+        * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
+        */
+       public function autoCreateUser( User $user, $source, $login = true ) {
+               if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
+                       !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
+               ) {
+                       throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
+               }
+
+               $username = $user->getName();
+
+               // Try the local user from the slave DB
+               $localId = User::idFromName( $username );
+               $flags = User::READ_NORMAL;
+
+               // 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( $username, User::READ_LATEST );
+                       $flags = User::READ_LATEST;
+               }
+               // @codeCoverageIgnoreEnd
+
+               if ( $localId ) {
+                       $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
+                               'username' => $username,
+                       ] );
+                       $user->setId( $localId );
+                       $user->loadFromId( $flags );
+                       if ( $login ) {
+                               $this->setSessionDataForUser( $user );
+                       }
+                       $status = Status::newGood();
+                       $status->warning( 'userexists' );
+                       return $status;
+               }
+
+               // Wiki is read-only?
+               if ( wfReadOnly() ) {
+                       $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
+                               'username' => $username,
+                               'reason' => wfReadOnlyReason(),
+                       ] );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
+               }
+
+               // Check the session, if we tried to create this user already there's
+               // no point in retrying.
+               $session = $this->request->getSession();
+               if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
+                       $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
+                               'username' => $username,
+                               'sessionid' => $session->getId(),
+                       ] );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
+                       if ( $reason instanceof StatusValue ) {
+                               return Status::wrap( $reason );
+                       } else {
+                               return Status::newFatal( $reason );
+                       }
+               }
+
+               // Is the username creatable?
+               if ( !User::isCreatableName( $username ) ) {
+                       $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
+                               'username' => $username,
+                       ] );
+                       $session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return Status::newFatal( 'noname' );
+               }
+
+               // Is the IP user able to create accounts?
+               $anon = new User;
+               if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
+                       $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
+                               'username' => $username,
+                               'ip' => $anon->getName(),
+                       ] );
+                       $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
+                       $session->persist();
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return Status::newFatal( 'authmanager-autocreate-noperm' );
+               }
+
+               // Avoid account creation races on double submissions
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+               if ( !$lock ) {
+                       $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
+                               'user' => $username,
+                       ] );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return Status::newFatal( 'usernameinprogress' );
+               }
+
+               // Denied by providers?
+               $providers = $this->getPreAuthenticationProviders() +
+                       $this->getPrimaryAuthenticationProviders() +
+                       $this->getSecondaryAuthenticationProviders();
+               foreach ( $providers as $provider ) {
+                       $status = $provider->testUserForCreation( $user, $source );
+                       if ( !$status->isGood() ) {
+                               $ret = Status::wrap( $status );
+                               $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
+                                       'username' => $username,
+                                       'reason' => $ret->getWikiText( null, null, 'en' ),
+                               ] );
+                               $session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
+                               $user->setId( 0 );
+                               $user->loadFromId();
+                               return $ret;
+                       }
+               }
+
+               // Ignore warnings about master connections/writes...hard to avoid here
+               \Profiler::instance()->getTransactionProfiler()->resetExpectations();
+
+               $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+               if ( $cache->get( $backoffKey ) ) {
+                       $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
+                               'username' => $username,
+                       ] );
+                       $user->setId( 0 );
+                       $user->loadFromId();
+                       return Status::newFatal( 'authmanager-autocreate-exception' );
+               }
+
+               // Checks passed, create the user...
+               $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
+               $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
+                       'username' => $username,
+                       'from' => $from,
+               ] );
+
+               try {
+                       $status = $user->addToDatabase();
+                       if ( !$status->isOk() ) {
+                               // double-check for a race condition (T70012)
+                               $localId = User::idFromName( $username, User::READ_LATEST );
+                               if ( $localId ) {
+                                       $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
+                                               'username' => $username,
+                                       ] );
+                                       $user->setId( $localId );
+                                       $user->loadFromId( User::READ_LATEST );
+                                       if ( $login ) {
+                                               $this->setSessionDataForUser( $user );
+                                       }
+                                       $status = Status::newGood();
+                                       $status->warning( 'userexists' );
+                               } else {
+                                       $this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
+                                               'username' => $username,
+                                               'message' => $status->getWikiText( null, null, 'en' )
+                                       ] );
+                                       $user->setId( 0 );
+                                       $user->loadFromId();
+                               }
+                               return $status;
+                       }
+               } catch ( \Exception $ex ) {
+                       $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
+                               'username' => $username,
+                               'exception' => $ex,
+                       ] );
+                       // Do not keep throwing errors for a while
+                       $cache->set( $backoffKey, 1, 600 );
+                       // Bubble up error; which should normally trigger DB rollbacks
+                       throw $ex;
+               }
+
+               $this->setDefaultUserOptions( $user, true );
+
+               // Inform the providers
+               $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
+
+               \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
+               \Hooks::run( 'LocalUserCreated', [ $user, true ] );
+               $user->saveSettings();
+
+               // Update user count
+               \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+
+               // Watch user's userpage and talk page
+               $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+
+               // Log the creation
+               if ( $this->config->get( 'NewUserLog' ) ) {
+                       $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
+                       $logEntry->setPerformer( $user );
+                       $logEntry->setTarget( $user->getUserPage() );
+                       $logEntry->setComment( '' );
+                       $logEntry->setParameters( [
+                               '4::userid' => $user->getId(),
+                       ] );
+                       $logid = $logEntry->insert();
+               }
+
+               if ( $login ) {
+                       $this->setSessionDataForUser( $user );
+               }
+
+               return Status::newGood();
+       }
+
+       /**@}*/
+
+       /**
+        * @name Account linking
+        * @{
+        */
+
+       /**
+        * Determine whether accounts can be linked
+        * @return bool
+        */
+       public function canLinkAccounts() {
+               foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+                       if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Start an account linking flow
+        *
+        * @param User $user User being linked
+        * @param AuthenticationRequest[] $reqs
+        * @param string $returnToUrl Url that REDIRECT responses should eventually
+        *  return to.
+        * @return AuthenticationResponse
+        */
+       public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
+               $session = $this->request->getSession();
+               $session->remove( 'AuthManager::accountLinkState' );
+
+               if ( !$this->canLinkAccounts() ) {
+                       // Caller should have called canLinkAccounts()
+                       throw new \LogicException( 'Account linking is not possible' );
+               }
+
+               if ( $user->getId() === 0 ) {
+                       if ( !User::isUsableName( $user->getName() ) ) {
+                               $msg = wfMessage( 'noname' );
+                       } else {
+                               $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
+                       }
+                       return AuthenticationResponse::newFail( $msg );
+               }
+               foreach ( $reqs as $req ) {
+                       $req->username = $user->getName();
+                       $req->returnToUrl = $returnToUrl;
+               }
+
+               $this->removeAuthenticationSessionData( null );
+
+               $providers = $this->getPreAuthenticationProviders();
+               foreach ( $providers as $id => $provider ) {
+                       $status = $provider->testForAccountLink( $user );
+                       if ( !$status->isGood() ) {
+                               $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
+                                       'user' => $user->getName(),
+                               ] );
+                               $ret = AuthenticationResponse::newFail(
+                                       Status::wrap( $status )->getMessage()
+                               );
+                               $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+                               return $ret;
+                       }
+               }
+
+               $state = [
+                       'username' => $user->getName(),
+                       'userid' => $user->getId(),
+                       'returnToUrl' => $returnToUrl,
+                       'primary' => null,
+                       'continueRequests' => [],
+               ];
+
+               $providers = $this->getPrimaryAuthenticationProviders();
+               foreach ( $providers as $id => $provider ) {
+                       if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
+                               continue;
+                       }
+
+                       $res = $provider->beginPrimaryAccountLink( $user, $reqs );
+                       switch ( $res->status ) {
+                               case AuthenticationResponse::PASS;
+                                       $this->logger->info( "Account linked to {user} by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+                                       return $res;
+
+                               case AuthenticationResponse::FAIL;
+                                       $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+                                       return $res;
+
+                               case AuthenticationResponse::ABSTAIN;
+                                       // Continue loop
+                                       break;
+
+                               case AuthenticationResponse::REDIRECT;
+                               case AuthenticationResponse::UI;
+                                       $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $state['primary'] = $id;
+                                       $state['continueRequests'] = $res->neededRequests;
+                                       $session->setSecret( 'AuthManager::accountLinkState', $state );
+                                       $session->persist();
+                                       return $res;
+
+                                       // @codeCoverageIgnoreStart
+                               default:
+                                       throw new \DomainException(
+                                               get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
+                                       );
+                                       // @codeCoverageIgnoreEnd
+                       }
+               }
+
+               $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
+                       'user' => $user->getName(),
+               ] );
+               $ret = AuthenticationResponse::newFail(
+                       wfMessage( 'authmanager-link-no-primary' )
+               );
+               $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+               return $ret;
+       }
+
+       /**
+        * Continue an account linking flow
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse
+        */
+       public function continueAccountLink( array $reqs ) {
+               $session = $this->request->getSession();
+               try {
+                       if ( !$this->canLinkAccounts() ) {
+                               // Caller should have called canLinkAccounts()
+                               $session->remove( 'AuthManager::accountLinkState' );
+                               throw new \LogicException( 'Account linking is not possible' );
+                       }
+
+                       $state = $session->getSecret( 'AuthManager::accountLinkState' );
+                       if ( !is_array( $state ) ) {
+                               return AuthenticationResponse::newFail(
+                                       wfMessage( 'authmanager-link-not-in-progress' )
+                               );
+                       }
+                       $state['continueRequests'] = [];
+
+                       // Step 0: Prepare and validate the input
+
+                       $user = User::newFromName( $state['username'], 'usable' );
+                       if ( !is_object( $user ) ) {
+                               $session->remove( 'AuthManager::accountLinkState' );
+                               return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+                       }
+                       if ( $user->getId() != $state['userid'] ) {
+                               throw new \UnexpectedValueException(
+                                       "User \"{$state['username']}\" is valid, but " .
+                                               "ID {$user->getId()} != {$state['userid']}!"
+                               );
+                       }
+
+                       foreach ( $reqs as $req ) {
+                               $req->username = $state['username'];
+                               $req->returnToUrl = $state['returnToUrl'];
+                       }
+
+                       // Step 1: Call the primary again until it succeeds
+
+                       $provider = $this->getAuthenticationProvider( $state['primary'] );
+                       if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+                               // Configuration changed? Force them to start over.
+                               // @codeCoverageIgnoreStart
+                               $ret = AuthenticationResponse::newFail(
+                                       wfMessage( 'authmanager-link-not-in-progress' )
+                               );
+                               $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+                               $session->remove( 'AuthManager::accountLinkState' );
+                               return $ret;
+                               // @codeCoverageIgnoreEnd
+                       }
+                       $id = $provider->getUniqueId();
+                       $res = $provider->continuePrimaryAccountLink( $user, $reqs );
+                       switch ( $res->status ) {
+                               case AuthenticationResponse::PASS;
+                                       $this->logger->info( "Account linked to {user} by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+                                       $session->remove( 'AuthManager::accountLinkState' );
+                                       return $res;
+                               case AuthenticationResponse::FAIL;
+                                       $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+                                       $session->remove( 'AuthManager::accountLinkState' );
+                                       return $res;
+                               case AuthenticationResponse::REDIRECT;
+                               case AuthenticationResponse::UI;
+                                       $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
+                                               'user' => $user->getName(),
+                                       ] );
+                                       $state['continueRequests'] = $res->neededRequests;
+                                       $session->setSecret( 'AuthManager::accountLinkState', $state );
+                                       return $res;
+                               default:
+                                       throw new \DomainException(
+                                               get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
+                                       );
+                       }
+               } catch ( \Exception $ex ) {
+                       $session->remove( 'AuthManager::accountLinkState' );
+                       throw $ex;
+               }
+       }
+
+       /**@}*/
+
+       /**
+        * @name Information methods
+        * @{
+        */
+
+       /**
+        * Return the applicable list of AuthenticationRequests
+        *
+        * Possible values for $action:
+        *  - ACTION_LOGIN: Valid for passing to beginAuthentication
+        *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
+        *  - ACTION_CREATE: Valid for passing to beginAccountCreation
+        *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
+        *  - ACTION_LINK: Valid for passing to beginAccountLink
+        *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
+        *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
+        *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
+        *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
+        *
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @param User|null $user User being acted on, instead of the current user.
+        * @return AuthenticationRequest[]
+        */
+       public function getAuthenticationRequests( $action, User $user = null ) {
+               $options = [];
+               $providerAction = $action;
+
+               // Figure out which providers to query
+               switch ( $action ) {
+                       case self::ACTION_LOGIN:
+                       case self::ACTION_CREATE:
+                               $providers = $this->getPreAuthenticationProviders() +
+                                       $this->getPrimaryAuthenticationProviders() +
+                                       $this->getSecondaryAuthenticationProviders();
+                               break;
+
+                       case self::ACTION_LOGIN_CONTINUE:
+                               $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
+                               return is_array( $state ) ? $state['continueRequests'] : [];
+
+                       case self::ACTION_CREATE_CONTINUE:
+                               $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
+                               return is_array( $state ) ? $state['continueRequests'] : [];
+
+                       case self::ACTION_LINK:
+                               $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
+                                       return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
+                               } );
+                               break;
+
+                       case self::ACTION_UNLINK:
+                               $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
+                                       return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
+                               } );
+
+                               // To providers, unlink and remove are identical.
+                               $providerAction = self::ACTION_REMOVE;
+                               break;
+
+                       case self::ACTION_LINK_CONTINUE:
+                               $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
+                               return is_array( $state ) ? $state['continueRequests'] : [];
+
+                       case self::ACTION_CHANGE:
+                       case self::ACTION_REMOVE:
+                               $providers = $this->getPrimaryAuthenticationProviders() +
+                                       $this->getSecondaryAuthenticationProviders();
+                               break;
+
+                       // @codeCoverageIgnoreStart
+                       default:
+                               throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
+               }
+               // @codeCoverageIgnoreEnd
+
+               return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
+       }
+
+       /**
+        * Internal request lookup for self::getAuthenticationRequests
+        *
+        * @param string $providerAction Action to pass to providers
+        * @param array $options Options to pass to providers
+        * @param AuthenticationProvider[] $providers
+        * @param User|null $user
+        * @return AuthenticationRequest[]
+        */
+       private function getAuthenticationRequestsInternal(
+               $providerAction, array $options, array $providers, User $user = null
+       ) {
+               $user = $user ?: \RequestContext::getMain()->getUser();
+               $options['username'] = $user->isAnon() ? null : $user->getName();
+
+               // Query them and merge results
+               $reqs = [];
+               $allPrimaryRequired = null;
+               foreach ( $providers as $provider ) {
+                       $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
+                       $thisRequired = [];
+                       foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
+                               $id = $req->getUniqueId();
+
+                               // If it's from a Primary, mark it as "primary-required" but
+                               // track it for later.
+                               if ( $isPrimary ) {
+                                       if ( $req->required ) {
+                                               $thisRequired[$id] = true;
+                                               $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
+                                       }
+                               }
+
+                               if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
+                                       $reqs[$id] = $req;
+                               }
+                       }
+
+                       // Track which requests are required by all primaries
+                       if ( $isPrimary ) {
+                               $allPrimaryRequired = $allPrimaryRequired === null
+                                       ? $thisRequired
+                                       : array_intersect_key( $allPrimaryRequired, $thisRequired );
+                       }
+               }
+               // Any requests that were required by all primaries are required.
+               foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
+                       $reqs[$id]->required = AuthenticationRequest::REQUIRED;
+               }
+
+               // AuthManager has its own req for some actions
+               switch ( $providerAction ) {
+                       case self::ACTION_LOGIN:
+                               $reqs[] = new RememberMeAuthenticationRequest;
+                               break;
+
+                       case self::ACTION_CREATE:
+                               $reqs[] = new UsernameAuthenticationRequest;
+                               $reqs[] = new UserDataAuthenticationRequest;
+                               if ( $options['username'] !== null ) {
+                                       $reqs[] = new CreationReasonAuthenticationRequest;
+                                       $options['username'] = null; // Don't fill in the username below
+                               }
+                               break;
+               }
+
+               // Fill in reqs data
+               foreach ( $reqs as $req ) {
+                       $req->action = $providerAction;
+                       if ( $req->username === null ) {
+                               $req->username = $options['username'];
+                       }
+               }
+
+               // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
+               if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
+                       $reqs = array_filter( $reqs, function ( $req ) {
+                               return $this->allowsAuthenticationDataChange( $req, false )->isGood();
+                       } );
+               }
+
+               return array_values( $reqs );
+       }
+
+       /**
+        * Determine whether a username exists
+        * @param string $username
+        * @param int $flags Bitfield of User:READ_* constants
+        * @return bool
+        */
+       public function userExists( $username, $flags = User::READ_NORMAL ) {
+               foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+                       if ( $provider->testUserExists( $username, $flags ) ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Determine whether a user property should be allowed to be changed.
+        *
+        * Supported properties are:
+        *  - emailaddress
+        *  - realname
+        *  - nickname
+        *
+        * @param string $property
+        * @return bool
+        */
+       public function allowsPropertyChange( $property ) {
+               $providers = $this->getPrimaryAuthenticationProviders() +
+                       $this->getSecondaryAuthenticationProviders();
+               foreach ( $providers as $provider ) {
+                       if ( !$provider->providerAllowsPropertyChange( $property ) ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**@}*/
+
+       /**
+        * @name Internal methods
+        * @{
+        */
+
+       /**
+        * Store authentication in the current session
+        * @protected For use by AuthenticationProviders
+        * @param string $key
+        * @param mixed $data Must be serializable
+        */
+       public function setAuthenticationSessionData( $key, $data ) {
+               $session = $this->request->getSession();
+               $arr = $session->getSecret( 'authData' );
+               if ( !is_array( $arr ) ) {
+                       $arr = [];
+               }
+               $arr[$key] = $data;
+               $session->setSecret( 'authData', $arr );
+       }
+
+       /**
+        * Fetch authentication data from the current session
+        * @protected For use by AuthenticationProviders
+        * @param string $key
+        * @param mixed $default
+        * @return mixed
+        */
+       public function getAuthenticationSessionData( $key, $default = null ) {
+               $arr = $this->request->getSession()->getSecret( 'authData' );
+               if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
+                       return $arr[$key];
+               } else {
+                       return $default;
+               }
+       }
+
+       /**
+        * Remove authentication data
+        * @protected For use by AuthenticationProviders
+        * @param string|null $key If null, all data is removed
+        */
+       public function removeAuthenticationSessionData( $key ) {
+               $session = $this->request->getSession();
+               if ( $key === null ) {
+                       $session->remove( 'authData' );
+               } else {
+                       $arr = $session->getSecret( 'authData' );
+                       if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
+                               unset( $arr[$key] );
+                               $session->setSecret( 'authData', $arr );
+                       }
+               }
+       }
+
+       /**
+        * Create an array of AuthenticationProviders from an array of ObjectFactory specs
+        * @param string $class
+        * @param array[] $specs
+        * @return AuthenticationProvider[]
+        */
+       protected function providerArrayFromSpecs( $class, array $specs ) {
+               $i = 0;
+               foreach ( $specs as &$spec ) {
+                       $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
+               }
+               unset( $spec );
+               usort( $specs, function ( $a, $b ) {
+                       return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
+                               ?: $a['sort2'] - $b['sort2'];
+               } );
+
+               $ret = [];
+               foreach ( $specs as $spec ) {
+                       $provider = \ObjectFactory::getObjectFromSpec( $spec );
+                       if ( !$provider instanceof $class ) {
+                               throw new \RuntimeException(
+                                       "Expected instance of $class, got " . get_class( $provider )
+                               );
+                       }
+                       $provider->setLogger( $this->logger );
+                       $provider->setManager( $this );
+                       $provider->setConfig( $this->config );
+                       $id = $provider->getUniqueId();
+                       if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+                               throw new \RuntimeException(
+                                       "Duplicate specifications for id $id (classes " .
+                                       get_class( $provider ) . ' and ' .
+                                       get_class( $this->allAuthenticationProviders[$id] ) . ')'
+                               );
+                       }
+                       $this->allAuthenticationProviders[$id] = $provider;
+                       $ret[$id] = $provider;
+               }
+               return $ret;
+       }
+
+       /**
+        * Get the configuration
+        * @return array
+        */
+       private function getConfiguration() {
+               return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
+       }
+
+       /**
+        * Get the list of PreAuthenticationProviders
+        * @return PreAuthenticationProvider[]
+        */
+       protected function getPreAuthenticationProviders() {
+               if ( $this->preAuthenticationProviders === null ) {
+                       $conf = $this->getConfiguration();
+                       $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
+                               PreAuthenticationProvider::class, $conf['preauth']
+                       );
+               }
+               return $this->preAuthenticationProviders;
+       }
+
+       /**
+        * Get the list of PrimaryAuthenticationProviders
+        * @return PrimaryAuthenticationProvider[]
+        */
+       protected function getPrimaryAuthenticationProviders() {
+               if ( $this->primaryAuthenticationProviders === null ) {
+                       $conf = $this->getConfiguration();
+                       $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
+                               PrimaryAuthenticationProvider::class, $conf['primaryauth']
+                       );
+               }
+               return $this->primaryAuthenticationProviders;
+       }
+
+       /**
+        * Get the list of SecondaryAuthenticationProviders
+        * @return SecondaryAuthenticationProvider[]
+        */
+       protected function getSecondaryAuthenticationProviders() {
+               if ( $this->secondaryAuthenticationProviders === null ) {
+                       $conf = $this->getConfiguration();
+                       $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
+                               SecondaryAuthenticationProvider::class, $conf['secondaryauth']
+                       );
+               }
+               return $this->secondaryAuthenticationProviders;
+       }
+
+       /**
+        * Get a provider by ID
+        * @param string $id
+        * @return AuthenticationProvider|null
+        */
+       protected function getAuthenticationProvider( $id ) {
+               // Fast version
+               if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+                       return $this->allAuthenticationProviders[$id];
+               }
+
+               // Slow version: instantiate each kind and check
+               $providers = $this->getPrimaryAuthenticationProviders();
+               if ( isset( $providers[$id] ) ) {
+                       return $providers[$id];
+               }
+               $providers = $this->getSecondaryAuthenticationProviders();
+               if ( isset( $providers[$id] ) ) {
+                       return $providers[$id];
+               }
+               $providers = $this->getPreAuthenticationProviders();
+               if ( isset( $providers[$id] ) ) {
+                       return $providers[$id];
+               }
+
+               return null;
+       }
+
+       /**
+        * @param User $user
+        * @param bool|null $remember
+        */
+       private function setSessionDataForUser( $user, $remember = null ) {
+               $session = $this->request->getSession();
+               $delay = $session->delaySave();
+
+               $session->resetId();
+               if ( $session->canSetUser() ) {
+                       $session->setUser( $user );
+               }
+               if ( $remember !== null ) {
+                       $session->setRememberUser( $remember );
+               }
+               $session->set( 'AuthManager:lastAuthId', $user->getId() );
+               $session->set( 'AuthManager:lastAuthTimestamp', time() );
+               $session->persist();
+
+               \ScopedCallback::consume( $delay );
+
+               \Hooks::run( 'UserLoggedIn', [ $user ] );
+       }
+
+       /**
+        * @param User $user
+        * @param bool $useContextLang Use 'uselang' to set the user's language
+        */
+       private function setDefaultUserOptions( User $user, $useContextLang ) {
+               global $wgContLang;
+
+               \MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $user );
+
+               $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
+               $user->setOption( 'language', $lang->getPreferredVariant() );
+
+               if ( $wgContLang->hasVariants() ) {
+                       $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
+               }
+       }
+
+       /**
+        * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
+        * @param string $method
+        * @param array $args
+        */
+       private function callMethodOnProviders( $which, $method, array $args ) {
+               $providers = [];
+               if ( $which & 1 ) {
+                       $providers += $this->getPreAuthenticationProviders();
+               }
+               if ( $which & 2 ) {
+                       $providers += $this->getPrimaryAuthenticationProviders();
+               }
+               if ( $which & 4 ) {
+                       $providers += $this->getSecondaryAuthenticationProviders();
+               }
+               foreach ( $providers as $provider ) {
+                       call_user_func_array( [ $provider, $method ], $args );
+               }
+       }
+
+       /**
+        * 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::$instance = null;
+       }
+
+       /**@}*/
+
+}
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */
diff --git a/includes/auth/AuthManagerAuthPlugin.php b/includes/auth/AuthManagerAuthPlugin.php
new file mode 100644 (file)
index 0000000..bf1e021
--- /dev/null
@@ -0,0 +1,251 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * Backwards-compatibility wrapper for AuthManager via $wgAuth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthManagerAuthPlugin extends \AuthPlugin {
+       /** @var string|null */
+       protected $domain = null;
+
+       /** @var \\Psr\\Log\\LoggerInterface */
+       protected $logger = null;
+
+       public function __construct() {
+               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' );
+       }
+
+       public function userExists( $name ) {
+               return AuthManager::singleton()->userExists( $name );
+       }
+
+       public function authenticate( $username, $password ) {
+               $data = [
+                       'username' => $username,
+                       'password' => $password,
+               ];
+               if ( $this->domain !== null && $this->domain !== '' ) {
+                       $data['domain'] = $this->domain;
+               }
+               $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+               $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+
+               $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+               switch ( $res->status ) {
+                       case AuthenticationResponse::PASS:
+                               return true;
+                       case AuthenticationResponse::FAIL:
+                               // Hope it's not a PreAuthenticationProvider that failed...
+                               $msg = $res->message instanceof \Message ? $res->message : new \Message( $res->message );
+                               $this->logger->info( __METHOD__ . ': Authentication failed: ' . $msg->plain() );
+                               return false;
+                       default:
+                               throw new \BadMethodCallException(
+                                       'AuthManager does not support such simplified authentication'
+                               );
+               }
+       }
+
+       public function modifyUITemplate( &$template, &$type ) {
+               // AuthManager does not support direct UI screwing-around-with
+       }
+
+       public function setDomain( $domain ) {
+               $this->domain = $domain;
+       }
+
+       public function getDomain() {
+               if ( isset( $this->domain ) ) {
+                       return $this->domain;
+               } else {
+                       return 'invaliddomain';
+               }
+       }
+
+       public function validDomain( $domain ) {
+               $domainList = $this->domainList();
+               return $domainList ? in_array( $domain, $domainList, true ) : $domain === '';
+       }
+
+       public function updateUser( &$user ) {
+               \Hooks::run( 'UserLoggedIn', [ $user ] );
+               return true;
+       }
+
+       public function autoCreate() {
+               return true;
+       }
+
+       public function allowPropChange( $prop = '' ) {
+               return AuthManager::singleton()->allowsPropertyChange( $prop );
+       }
+
+       public function allowPasswordChange() {
+               $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_CHANGE );
+               foreach ( $reqs as $req ) {
+                       if ( $req instanceof PasswordAuthenticationRequest ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       public function allowSetLocalPassword() {
+               // There should be a PrimaryAuthenticationProvider that does this, if necessary
+               return false;
+       }
+
+       public function setPassword( $user, $password ) {
+               $data = [
+                       'username' => $user->getName(),
+                       'password' => $password,
+               ];
+               if ( $this->domain !== null && $this->domain !== '' ) {
+                       $data['domain'] = $this->domain;
+               }
+               $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_CHANGE );
+               $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+               foreach ( $reqs as $req ) {
+                       $status = AuthManager::singleton()->allowsAuthenticationDataChange( $req );
+                       if ( !$status->isOk() ) {
+                               $this->logger->info( __METHOD__ . ': Password change rejected: {reason}', [
+                                       'username' => $data['username'],
+                                       'reason' => $status->getWikiText( null, null, 'en' ),
+                               ] );
+                               return false;
+                       }
+               }
+               foreach ( $reqs as $req ) {
+                       AuthManager::singleton()->changeAuthenticationData( $req );
+               }
+               return true;
+       }
+
+       public function updateExternalDB( $user ) {
+               // This fires the necessary hook
+               $user->saveSettings();
+               return true;
+       }
+
+       public function updateExternalDBGroups( $user, $addgroups, $delgroups = [] ) {
+               \Hooks::run( 'UserGroupsChanged', [ $user, $addgroups, $delgroups ] );
+               return true;
+       }
+
+       public function canCreateAccounts() {
+               return AuthManager::singleton()->canCreateAccounts();
+       }
+
+       public function addUser( $user, $password, $email = '', $realname = '' ) {
+               global $wgUser;
+
+               $data = [
+                       'username' => $user->getName(),
+                       'password' => $password,
+                       'retype' => $password,
+                       'email' => $email,
+                       'realname' => $realname,
+               ];
+               if ( $this->domain !== null && $this->domain !== '' ) {
+                       $data['domain'] = $this->domain;
+               }
+               $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_CREATE );
+               $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+
+               $res = AuthManager::singleton()->beginAccountCreation( $wgUser, $reqs, 'null:' );
+               switch ( $res->status ) {
+                       case AuthenticationResponse::PASS:
+                               return true;
+                       case AuthenticationResponse::FAIL:
+                               // Hope it's not a PreAuthenticationProvider that failed...
+                               $msg = $res->message instanceof \Message ? $res->message : new \Message( $res->message );
+                               $this->logger->info( __METHOD__ . ': Authentication failed: ' . $msg->plain() );
+                               return false;
+                       default:
+                               throw new \BadMethodCallException(
+                                       'AuthManager does not support such simplified account creation'
+                               );
+               }
+       }
+
+       public function strict() {
+               // There should be a PrimaryAuthenticationProvider that does this, if necessary
+               return true;
+       }
+
+       public function strictUserAuth( $username ) {
+               // There should be a PrimaryAuthenticationProvider that does this, if necessary
+               return true;
+       }
+
+       public function initUser( &$user, $autocreate = false ) {
+               \Hooks::run( 'LocalUserCreated', [ $user, $autocreate ] );
+       }
+
+       public function getCanonicalName( $username ) {
+               // AuthManager doesn't support restrictions beyond MediaWiki's
+               return $username;
+       }
+
+       public function getUserInstance( User &$user ) {
+               return new AuthManagerAuthPluginUser( $user );
+       }
+
+       public function domainList() {
+               return [];
+       }
+}
+
+/**
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthManagerAuthPluginUser extends \AuthPluginUser {
+       /** @var User */
+       private $user;
+
+       function __construct( $user ) {
+               $this->user = $user;
+       }
+
+       public function getId() {
+               return $this->user->getId();
+       }
+
+       public function isLocked() {
+               return $this->user->isLocked();
+       }
+
+       public function isHidden() {
+               return $this->user->isHidden();
+       }
+
+       public function resetAuthToken() {
+               \MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $this->user );
+               return true;
+       }
+}
diff --git a/includes/auth/AuthPluginPrimaryAuthenticationProvider.php b/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..9746637
--- /dev/null
@@ -0,0 +1,429 @@
+<?php
+/**
+ * Primary authentication provider wrapper for AuthPlugin
+ *
+ * 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use AuthPlugin;
+use User;
+
+/**
+ * Primary authentication provider wrapper for AuthPlugin
+ * @warning If anything depends on the wrapped AuthPlugin being $wgAuth, it won't work with this!
+ * @ingroup Auth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthPluginPrimaryAuthenticationProvider
+       extends AbstractPasswordPrimaryAuthenticationProvider
+{
+       private $auth;
+       private $hasDomain;
+       private $requestType = null;
+
+       /**
+        * @param AuthPlugin $auth AuthPlugin to wrap
+        * @param string|null $requestType Class name of the
+        *  PasswordAuthenticationRequest to use. If $auth->domainList() returns
+        *  more than one domain, this must be a PasswordDomainAuthenticationRequest.
+        */
+       public function __construct( AuthPlugin $auth, $requestType = null ) {
+               parent::__construct();
+
+               if ( $auth instanceof AuthManagerAuthPlugin ) {
+                       throw new \InvalidArgumentException(
+                               'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
+                                       'makes no sense.'
+                       );
+               }
+
+               $need = count( $auth->domainList() ) > 1
+                       ? PasswordDomainAuthenticationRequest::class
+                       : PasswordAuthenticationRequest::class;
+               if ( $requestType === null ) {
+                       $requestType = $need;
+               } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) {
+                       throw new \InvalidArgumentException( "$requestType is not a $need" );
+               }
+
+               $this->auth = $auth;
+               $this->requestType = $requestType;
+               $this->hasDomain = (
+                       $requestType === PasswordDomainAuthenticationRequest::class ||
+                       is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class )
+               );
+               $this->authoritative = $auth->strict();
+
+               // Registering hooks from core is unusual, but is needed here to be
+               // able to call the AuthPlugin methods those hooks replace.
+               \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] );
+               \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] );
+               \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] );
+               \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] );
+       }
+
+       /**
+        * Create an appropriate AuthenticationRequest
+        * @return PasswordAuthenticationRequest
+        */
+       protected function makeAuthReq() {
+               $class = $this->requestType;
+               if ( $this->hasDomain ) {
+                       return new $class( $this->auth->domainList() );
+               } else {
+                       return new $class();
+               }
+       }
+
+       /**
+        * Call $this->auth->setDomain()
+        * @param PasswordAuthenticationRequest $req
+        */
+       protected function setDomain( $req ) {
+               if ( $this->hasDomain ) {
+                       $domain = $req->domain;
+               } else {
+                       // Just grab the first one.
+                       $domainList = $this->auth->domainList();
+                       $domain = reset( $domainList );
+               }
+
+               // Special:UserLogin does this. Strange.
+               if ( !$this->auth->validDomain( $domain ) ) {
+                       $domain = $this->auth->getDomain();
+               }
+               $this->auth->setDomain( $domain );
+       }
+
+       /**
+        * Hook function to call AuthPlugin::updateExternalDB()
+        * @param User $user
+        * @codeCoverageIgnore
+        */
+       public function onUserSaveSettings( $user ) {
+               // No way to know the domain, just hope the provider handles that.
+               $this->auth->updateExternalDB( $user );
+       }
+
+       /**
+        * Hook function to call AuthPlugin::updateExternalDBGroups()
+        * @param User $user
+        * @param array $added
+        * @param array $removed
+        */
+       public function onUserGroupsChanged( $user, $added, $removed ) {
+               // No way to know the domain, just hope the provider handles that.
+               $this->auth->updateExternalDBGroups( $user, $added, $removed );
+       }
+
+       /**
+        * Hook function to call AuthPlugin::updateUser()
+        * @param User $user
+        */
+       public function onUserLoggedIn( $user ) {
+               $hookUser = $user;
+               // No way to know the domain, just hope the provider handles that.
+               $this->auth->updateUser( $hookUser );
+               if ( $hookUser !== $user ) {
+                       throw new \UnexpectedValueException(
+                               get_class( $this->auth ) . '::updateUser() tried to replace $user!'
+                       );
+               }
+       }
+
+       /**
+        * Hook function to call AuthPlugin::initUser()
+        * @param User $user
+        * @param bool $autocreated
+        */
+       public function onLocalUserCreated( $user, $autocreated ) {
+               // For $autocreated, see self::autoCreatedAccount()
+               if ( !$autocreated ) {
+                       $hookUser = $user;
+                       // No way to know the domain, just hope the provider handles that.
+                       $this->auth->initUser( $hookUser, $autocreated );
+                       if ( $hookUser !== $user ) {
+                               throw new \UnexpectedValueException(
+                                       get_class( $this->auth ) . '::initUser() tried to replace $user!'
+                               );
+                       }
+               }
+       }
+
+       public function getUniqueId() {
+               return parent::getUniqueId() . ':' . get_class( $this->auth );
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                       case AuthManager::ACTION_CREATE:
+                               return [ $this->makeAuthReq() ];
+
+                       case AuthManager::ACTION_CHANGE:
+                       case AuthManager::ACTION_REMOVE:
+                               // No way to know the domain, just hope the provider handles that.
+                               return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : [];
+
+                       default:
+                               return [];
+               }
+       }
+
+       public function beginPrimaryAuthentication( array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
+               if ( !$req || $req->username === null || $req->password === null ||
+                       ( $this->hasDomain && $req->domain === null )
+               ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $this->setDomain( $req );
+               if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) &&
+                       $this->auth->authenticate( $username, $req->password )
+               ) {
+                       return AuthenticationResponse::newPass( $username );
+               } else {
+                       $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
+                       return $this->failResponse( $req );
+               }
+       }
+
+       public function testUserCanAuthenticate( $username ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               // We have to check every domain, because at least LdapAuthentication
+               // interprets AuthPlugin::userExists() as applying only to the current
+               // domain.
+               $curDomain = $this->auth->getDomain();
+               $domains = $this->auth->domainList() ?: [ '' ];
+               foreach ( $domains as $domain ) {
+                       $this->auth->setDomain( $domain );
+                       if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) ) {
+                               $this->auth->setDomain( $curDomain );
+                               return true;
+                       }
+               }
+               $this->auth->setDomain( $curDomain );
+               return false;
+       }
+
+       /**
+        * @see self::testUserCanAuthenticate
+        * @note The caller is responsible for calling $this->auth->setDomain()
+        * @param User $user
+        * @return bool
+        */
+       private function testUserCanAuthenticateInternal( $user ) {
+               if ( $this->auth->userExists( $user->getName() ) ) {
+                       return !$this->auth->getUserInstance( $user )->isLocked();
+               } else {
+                       return false;
+               }
+       }
+
+       public function providerRevokeAccessForUser( $username ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return;
+               }
+               $user = User::newFromName( $username );
+               if ( $user ) {
+                       // Reset the password on every domain.
+                       $curDomain = $this->auth->getDomain();
+                       $domains = $this->auth->domainList() ?: [ '' ];
+                       $failed = [];
+                       foreach ( $domains as $domain ) {
+                               $this->auth->setDomain( $domain );
+                               if ( $this->testUserCanAuthenticateInternal( $user ) &&
+                                       !$this->auth->setPassword( $user, null )
+                               ) {
+                                       $failed[] = $domain === '' ? '(default)' : $domain;
+                               }
+                       }
+                       $this->auth->setDomain( $curDomain );
+                       if ( $failed ) {
+                               throw new \UnexpectedValueException(
+                                       "AuthPlugin failed to reset password for $username in the following domains: "
+                                               . join( ' ', $failed )
+                               );
+                       }
+               }
+       }
+
+       public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               // We have to check every domain, because at least LdapAuthentication
+               // interprets AuthPlugin::userExists() as applying only to the current
+               // domain.
+               $curDomain = $this->auth->getDomain();
+               $domains = $this->auth->domainList() ?: [ '' ];
+               foreach ( $domains as $domain ) {
+                       $this->auth->setDomain( $domain );
+                       if ( $this->auth->userExists( $username ) ) {
+                               $this->auth->setDomain( $curDomain );
+                               return true;
+                       }
+               }
+               $this->auth->setDomain( $curDomain );
+               return false;
+       }
+
+       public function providerAllowsPropertyChange( $property ) {
+               // No way to know the domain, just hope the provider handles that.
+               return $this->auth->allowPropChange( $property );
+       }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               if ( get_class( $req ) !== $this->requestType ) {
+                       return \StatusValue::newGood( 'ignored' );
+               }
+
+               // Hope it works, AuthPlugin gives us no way to do this.
+               $curDomain = $this->auth->getDomain();
+               $this->setDomain( $req );
+               try {
+                       // If !$checkData the domain might be wrong. Nothing we can do about that.
+                       if ( !$this->auth->allowPasswordChange() ) {
+                               return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' );
+                       }
+
+                       if ( !$checkData ) {
+                               return \StatusValue::newGood();
+                       }
+
+                       if ( $this->hasDomain ) {
+                               if ( $req->domain === null ) {
+                                       return \StatusValue::newGood( 'ignored' );
+                               }
+                               if ( !$this->auth->validDomain( $domain ) ) {
+                                       return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
+                               }
+                       }
+
+                       $username = User::getCanonicalName( $req->username, 'usable' );
+                       if ( $username !== false ) {
+                               $sv = \StatusValue::newGood();
+                               if ( $req->password !== null ) {
+                                       if ( $req->password !== $req->retype ) {
+                                               $sv->fatal( 'badretype' );
+                                       } else {
+                                               $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+                                       }
+                               }
+                               return $sv;
+                       } else {
+                               return \StatusValue::newGood( 'ignored' );
+                       }
+               } finally {
+                       $this->auth->setDomain( $curDomain );
+               }
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+               if ( get_class( $req ) === $this->requestType ) {
+                       $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+                       if ( $username === false ) {
+                               return;
+                       }
+
+                       if ( $this->hasDomain && $req->domain === null ) {
+                               return;
+                       }
+
+                       $this->setDomain( $req );
+                       $user = User::newFromName( $username );
+                       if ( !$this->auth->setPassword( $user, $req->password ) ) {
+                               // This is totally unfriendly and leaves other
+                               // AuthenticationProviders in an uncertain state, but what else
+                               // can we do?
+                               throw new \ErrorPageError(
+                                       'authmanager-authplugin-setpass-failed-title',
+                                       'authmanager-authplugin-setpass-failed-message'
+                               );
+                       }
+               }
+       }
+
+       public function accountCreationType() {
+               // No way to know the domain, just hope the provider handles that.
+               return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE;
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               return \StatusValue::newGood();
+       }
+
+       public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+               if ( $this->accountCreationType() === self::TYPE_NONE ) {
+                       throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+               }
+
+               $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
+               if ( !$req || $req->username === null || $req->password === null ||
+                       ( $this->hasDomain && $req->domain === null )
+               ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $this->setDomain( $req );
+               if ( $this->auth->addUser(
+                       $user, $req->password, $user->getEmail(), $user->getRealName()
+               ) ) {
+                       return AuthenticationResponse::newPass();
+               } else {
+                       return AuthenticationResponse::newFail(
+                               new \Message( 'authmanager-authplugin-create-fail' )
+                       );
+               }
+       }
+
+       public function autoCreatedAccount( $user, $source ) {
+               $hookUser = $user;
+               // No way to know the domain, just hope the provider handles that.
+               $this->auth->initUser( $hookUser, true );
+               if ( $hookUser !== $user ) {
+                       throw new \UnexpectedValueException(
+                               get_class( $this->auth ) . '::initUser() tried to replace $user!'
+                       );
+               }
+       }
+}
diff --git a/includes/auth/AuthenticationProvider.php b/includes/auth/AuthenticationProvider.php
new file mode 100644 (file)
index 0000000..4db0a84
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Authentication 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * An AuthenticationProvider is used by AuthManager when authenticating users.
+ * @ingroup Auth
+ * @since 1.27
+ */
+interface AuthenticationProvider extends LoggerAwareInterface {
+
+       /**
+        * Set AuthManager
+        * @param AuthManager $manager
+        */
+       public function setManager( AuthManager $manager );
+
+       /**
+        * Set configuration
+        * @param Config $config
+        */
+       public function setConfig( Config $config );
+
+       /**
+        * Return a unique identifier for this instance
+        *
+        * This must be the same across requests. If multiple instances return the
+        * same ID, exceptions will be thrown from AuthManager.
+        *
+        * @return string
+        */
+       public function getUniqueId();
+
+       /**
+        * Return the applicable list of AuthenticationRequests
+        *
+        * Possible values for $action depend on whether the implementing class is
+        * also a PreAuthenticationProvider, PrimaryAuthenticationProvider, or
+        * SecondaryAuthenticationProvider.
+        *  - ACTION_LOGIN: Valid for passing to beginAuthentication. Called on all
+        *    providers.
+        *  - ACTION_CREATE: Valid for passing to beginAccountCreation. Called on
+        *    all providers.
+        *  - ACTION_LINK: Valid for passing to beginAccountLink. Called on linking
+        *    primary providers only.
+        *  - ACTION_CHANGE: Valid for passing to AuthManager::changeAuthenticationData
+        *    to change credentials. Called on primary and secondary providers.
+        *  - ACTION_REMOVE: Valid for passing to AuthManager::changeAuthenticationData
+        *    to remove credentials. Must work without additional user input (i.e.
+        *    without calling loadFromSubmission). Called on primary and secondary
+        *    providers.
+        *
+        * @see AuthManager::getAuthenticationRequests()
+        * @param string $action
+        * @param array $options Options are:
+        *  - username: User name related to the action, or null/unset if anon.
+        *    - ACTION_LOGIN: The currently logged-in user, if any.
+        *    - ACTION_CREATE: The account creator, if non-anonymous.
+        *    - ACTION_LINK: The local user being linked to.
+        *    - ACTION_CHANGE: The user having data changed.
+        *    - ACTION_REMOVE: The user having data removed.
+        *    This does not need to be copied into the returned requests, you only
+        *    need to pay attention to it if the set of requests differs based on
+        *    the user.
+        * @return AuthenticationRequest[]
+        */
+       public function getAuthenticationRequests( $action, array $options );
+
+}
diff --git a/includes/auth/AuthenticationRequest.php b/includes/auth/AuthenticationRequest.php
new file mode 100644 (file)
index 0000000..3c19b87
--- /dev/null
@@ -0,0 +1,338 @@
+<?php
+/**
+ * Authentication request value 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
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is a value object for authentication requests.
+ *
+ * An AuthenticationRequest represents a set of form fields that are needed on
+ * and provided from the login, account creation, or password change forms.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AuthenticationRequest {
+
+       /** Indicates that the request is not required for authentication to proceed. */
+       const OPTIONAL = 0;
+
+       /** Indicates that the request is required for authentication to proceed. */
+       const REQUIRED = 1;
+
+       /** Indicates that the request is required by a primary authentication
+        * provdier, but other primary authentication providers do not require it. */
+       const PRIMARY_REQUIRED = 2;
+
+       /** @var string|null The AuthManager::ACTION_* constant this request was
+        * created to be used for. The *_CONTINUE constants are not used here, the
+        * corresponding "begin" constant is used instead.
+        */
+       public $action = null;
+
+       /** @var int For login, continue, and link actions, one of self::OPTIONAL,
+        * self::REQUIRED, or self::PRIMARY_REQUIRED */
+       public $required = self::REQUIRED;
+
+       /** @var string|null Return-to URL, in case of redirect */
+       public $returnToUrl = null;
+
+       /** @var string|null Username. May not be used by all subclasses. */
+       public $username = null;
+
+       /**
+        * Supply a unique key for deduplication
+        *
+        * When the AuthenticationRequests instances returned by the providers are
+        * merged, the value returned here is used for keeping only one copy of
+        * duplicate requests.
+        *
+        * Subclasses should override this if multiple distinct instances would
+        * make sense, i.e. the request class has internal state of some sort.
+        *
+        * This value might be exposed to the user in web forms so it should not
+        * contain private information.
+        *
+        * @return string
+        */
+       public function getUniqueId() {
+               return get_called_class();
+       }
+
+       /**
+        * Fetch input field info
+        *
+        * The field info is an associative array mapping field names to info
+        * arrays. The info arrays have the following keys:
+        *  - type: (string) Type of input. Types and equivalent HTML widgets are:
+        *     - string: <input type="text">
+        *     - password: <input type="password">
+        *     - select: <select>
+        *     - checkbox: <input type="checkbox">
+        *     - multiselect: More a grid of checkboxes than <select multi>
+        *     - button: <input type="image"> if 'image' is set, otherwise <input type="submit">
+        *       (uses 'label' as button text)
+        *     - hidden: Not visible to the user, but needs to be preserved for the next request
+        *     - null: No widget, just display the 'label' message.
+        *  - options: (array) Maps option values to Messages for the
+        *      'select' and 'multiselect' types.
+        *  - value: (string) Value (for 'null' and 'hidden') or default value (for other types).
+        *  - image: (string) URL of an image to use in connection with the input
+        *  - label: (Message) Text suitable for a label in an HTML form
+        *  - help: (Message) Text suitable as a description of what the field is
+        *  - optional: (bool) If set and truthy, the field may be left empty
+        *
+        * @return array As above
+        */
+       abstract public function getFieldInfo();
+
+       /**
+        * Returns metadata about this request.
+        *
+        * This is mainly for the benefit of API clients which need more detailed render hints
+        * than what's available through getFieldInfo(). Semantics are unspecified and left to the
+        * individual subclasses, but the contents of the array should be primitive types so that they
+        * can be transformed into JSON or similar formats.
+        *
+        * @return array A (possibly nested) array with primitive types
+        */
+       public function getMetadata() {
+               return [];
+       }
+
+       /**
+        * Initialize form submitted form data.
+        *
+        * Should always return false if self::getFieldInfo() returns an empty
+        * array.
+        *
+        * @param array $data Submitted data as an associative array
+        * @return bool Whether the request data was successfully loaded
+        */
+       public function loadFromSubmission( array $data ) {
+               $fields = array_filter( $this->getFieldInfo(), function ( $info ) {
+                       return $info['type'] !== 'null';
+               } );
+               if ( !$fields ) {
+                       return false;
+               }
+
+               foreach ( $fields as $field => $info ) {
+                       // Checkboxes and buttons are special. Depending on the method used
+                       // to populate $data, they might be unset meaning false or they
+                       // might be boolean. Further, image buttons might submit the
+                       // coordinates of the click rather than the expected value.
+                       if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
+                               $this->$field = isset( $data[$field] ) && $data[$field] !== false
+                                       || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
+                               if ( !$this->$field && empty( $info['optional'] ) ) {
+                                       return false;
+                               }
+                               continue;
+                       }
+
+                       // Multiselect are too, slightly
+                       if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
+                               $data[$field] = [];
+                       }
+
+                       if ( !isset( $data[$field] ) ) {
+                               return false;
+                       }
+                       if ( $data[$field] === '' || $data[$field] === [] ) {
+                               if ( empty( $info['optional'] ) ) {
+                                       return false;
+                               }
+                       } else {
+                               switch ( $info['type'] ) {
+                                       case 'select':
+                                               if ( !isset( $info['options'][$data[$field]] ) ) {
+                                                       return false;
+                                               }
+                                               break;
+
+                                       case 'multiselect':
+                                               $data[$field] = (array)$data[$field];
+                                               $allowed = array_keys( $info['options'] );
+                                               if ( array_diff( $data[$field], $allowed ) !== [] ) {
+                                                       return false;
+                                               }
+                                               break;
+                               }
+                       }
+
+                       $this->$field = $data[$field];
+               }
+
+               return true;
+       }
+
+       /**
+        * Describe the credentials represented by this request
+        *
+        * This is used on requests returned by
+        * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK
+        * and ACTION_REMOVE and for requests returned in
+        * AuthenticationResponse::$linkRequest to create useful user interfaces.
+        *
+        * @return Message[] with the following keys:
+        *  - provider: A Message identifying the service that provides
+        *    the credentials, e.g. the name of the third party authentication
+        *    service.
+        *  - account: A Message identifying the credentials themselves,
+        *    e.g. the email address used with the third party authentication
+        *    service.
+        */
+       public function describeCredentials() {
+               return [
+                       'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
+                       'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
+               ];
+       }
+
+       /**
+        * Update a set of requests with form submit data, discarding ones that fail
+        * @param AuthenticationRequest[] $reqs
+        * @param array $data
+        * @return AuthenticationRequest[]
+        */
+       public static function loadRequestsFromSubmission( array $reqs, array $data ) {
+               return array_values( array_filter( $reqs, function ( $req ) use ( $data ) {
+                       return $req->loadFromSubmission( $data );
+               } ) );
+       }
+
+       /**
+        * Select a request by class name.
+        * @param AuthenticationRequest[] $reqs
+        * @param string $class Class name
+        * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given
+        *   class.
+        * @return AuthenticationRequest|null Returns null if there is not exactly
+        *  one matching request.
+        */
+       public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
+               $requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) {
+                       if ( $allowSubclasses ) {
+                               return is_a( $req, $class, false );
+                       } else {
+                               return get_class( $req ) === $class;
+                       }
+               } );
+               return count( $requests ) === 1 ? reset( $requests ) : null;
+       }
+
+       /**
+        * Get the username from the set of requests
+        *
+        * Only considers requests that have a "username" field.
+        *
+        * @param AuthenticationRequest[] $requests
+        * @return string|null
+        * @throws \UnexpectedValueException If multiple different usernames are present.
+        */
+       public static function getUsernameFromRequests( array $reqs ) {
+               $username = null;
+               $otherClass = null;
+               foreach ( $reqs as $req ) {
+                       $info = $req->getFieldInfo();
+                       if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
+                               if ( $username === null ) {
+                                       $username = $req->username;
+                                       $otherClass = get_class( $req );
+                               } elseif ( $username !== $req->username ) {
+                                       $requestClass = get_class( $req );
+                                       throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
+                                               . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
+                               }
+                       }
+               }
+               return $username;
+       }
+
+       /**
+        * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
+        * @param AuthenticationRequest[] $reqs
+        * @return array
+        * @throws \UnexpectedValueException If fields cannot be merged
+        */
+       public static function mergeFieldInfo( array $reqs ) {
+               $merged = [];
+
+               foreach ( $reqs as $req ) {
+                       $info = $req->getFieldInfo();
+                       if ( !$info ) {
+                               continue;
+                       }
+
+                       foreach ( $info as $name => $options ) {
+                               if ( $req->required !== self::REQUIRED ) {
+                                       // If the request isn't required, its fields aren't required either.
+                                       $options['optional'] = true;
+                               } else {
+                                       $options['optional'] = !empty( $options['optional'] );
+                               }
+
+                               if ( !array_key_exists( $name, $merged ) ) {
+                                       $merged[$name] = $options;
+                               } elseif ( $merged[$name]['type'] !== $options['type'] ) {
+                                       throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
+                                               "\"{$merged[$name]['type']}\" vs \"{$options['type']}\""
+                                       );
+                               } else {
+                                       if ( isset( $options['options'] ) ) {
+                                               if ( isset( $merged[$name]['options'] ) ) {
+                                                       $merged[$name]['options'] += $options['options'];
+                                               } else {
+                                                       // @codeCoverageIgnoreStart
+                                                       $merged[$name]['options'] = $options['options'];
+                                                       // @codeCoverageIgnoreEnd
+                                               }
+                                       }
+
+                                       $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
+
+                                       // No way to merge 'value', 'image', 'help', or 'label', so just use
+                                       // the value from the first request.
+                               }
+                       }
+               }
+
+               return $merged;
+       }
+
+       /**
+        * Implementing this mainly for use from the unit tests.
+        * @param array $data
+        * @return AuthenticationRequest
+        */
+       public static function __set_state( $data ) {
+               $ret = new static();
+               foreach ( $data as $k => $v ) {
+                       $ret->$k = $v;
+               }
+               return $ret;
+       }
+}
diff --git a/includes/auth/AuthenticationResponse.php b/includes/auth/AuthenticationResponse.php
new file mode 100644 (file)
index 0000000..db01825
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+/**
+ * Authentication response value 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
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is a value object to hold authentication response data
+ * @ingroup Auth
+ * @since 1.27
+ */
+class AuthenticationResponse {
+       /** Indicates that the authentication succeeded. */
+       const PASS = 'PASS';
+
+       /** Indicates that the authentication failed. */
+       const FAIL = 'FAIL';
+
+       /** Indicates that third-party authentication succeeded but no user exists.
+        * Either treat this like a UI response or pass $this->createRequest to
+        * AuthManager::beginCreateAccount().
+        */
+       const RESTART = 'RESTART';
+
+       /** Indicates that the authentication provider does not handle this request. */
+       const ABSTAIN = 'ABSTAIN';
+
+       /** Indicates that the authentication needs further user input of some sort. */
+       const UI = 'UI';
+
+       /** Indicates that the authentication needs to be redirected to a third party to proceed. */
+       const REDIRECT = 'REDIRECT';
+
+       /** @var string One of the constants above */
+       public $status;
+
+       /** @var string|null URL to redirect to for a REDIRECT response */
+       public $redirectTarget = null;
+
+       /**
+        * @var mixed Data for a REDIRECT response that a client might use to
+        * query the remote site via its API rather than by following $redirectTarget.
+        * Value must be something acceptable to ApiResult::addValue().
+        */
+       public $redirectApiData = null;
+
+       /**
+        * @var AuthenticationRequest[] Needed AuthenticationRequests to continue
+        * after a UI or REDIRECT response
+        */
+       public $neededRequests = [];
+
+       /** @var Message|null I18n message to display in case of UI or FAIL */
+       public $message = null;
+
+       /**
+        * @var string|null Local user name from authentication.
+        * May be null if the authentication passed but no local user is known.
+        */
+       public $username = null;
+
+       /**
+        * @var AuthenticationRequest|null
+        *
+        * Returned with a PrimaryAuthenticationProvider login FAIL, this holds a
+        * request that should result in a PASS when passed to that provider's
+        * PrimaryAuthenticationProvider::beginPrimaryAccountCreation().
+        *
+        * Returned with an AuthManager login FAIL or RESTART, this holds a request
+        * that may be passed to AuthManager::beginCreateAccount() after setting
+        * its ->returnToUrl property. It may also be passed to
+        * AuthManager::beginAuthentication() to preserve state.
+        */
+       public $createRequest = null;
+
+       /**
+        * @var AuthenticationRequest|null Returned with a PrimaryAuthenticationProvider
+        *  login PASS with no username, this holds a request to pass to
+        *  AuthManager::changeAuthenticationData() to link the account once the
+        *  local user has been determined.
+        */
+       public $linkRequest = null;
+
+       /**
+        * @var AuthenticationRequest|null Returned with an AuthManager account
+        *  creation PASS, this holds a request to pass to AuthManager::beginAuthentication()
+        *  to immediately log into the created account.
+        */
+       public $loginRequest = null;
+
+       /**
+        * @param string|null $username Local username
+        * @return AuthenticationResponse
+        */
+       public static function newPass( $username = null ) {
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::PASS;
+               $ret->username = $username;
+               return $ret;
+       }
+
+       /**
+        * @param Message $msg
+        * @return AuthenticationResponse
+        */
+       public static function newFail( Message $msg ) {
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::FAIL;
+               $ret->message = $msg;
+               return $ret;
+       }
+
+       /**
+        * @param Message $msg
+        * @return AuthenticationResponse
+        */
+       public static function newRestart( Message $msg ) {
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::RESTART;
+               $ret->message = $msg;
+               return $ret;
+       }
+
+       /**
+        * @return AuthenticationResponse
+        */
+       public static function newAbstain() {
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::ABSTAIN;
+               return $ret;
+       }
+
+       /**
+        * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
+        * @param Message $msg
+        * @return AuthenticationResponse
+        */
+       public static function newUI( array $reqs, Message $msg ) {
+               if ( !$reqs ) {
+                       throw new \InvalidArgumentException( '$reqs may not be empty' );
+               }
+
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::UI;
+               $ret->neededRequests = $reqs;
+               $ret->message = $msg;
+               return $ret;
+       }
+
+       /**
+        * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
+        * @param string $redirectTarget URL
+        * @param mixed $redirectApiData Data suitable for adding to an ApiResult
+        * @return AuthenticationResponse
+        */
+       public static function newRedirect( array $reqs, $redirectTarget, $redirectApiData = null ) {
+               if ( !$reqs ) {
+                       throw new \InvalidArgumentException( '$reqs may not be empty' );
+               }
+
+               $ret = new AuthenticationResponse;
+               $ret->status = AuthenticationResponse::REDIRECT;
+               $ret->neededRequests = $reqs;
+               $ret->redirectTarget = $redirectTarget;
+               $ret->redirectApiData = $redirectApiData;
+               return $ret;
+       }
+
+}
diff --git a/includes/auth/ButtonAuthenticationRequest.php b/includes/auth/ButtonAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..055d7ea
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is an authentication request that just implements a simple button.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ButtonAuthenticationRequest extends AuthenticationRequest {
+       /** @var string */
+       protected $name;
+
+       /** @var Message */
+       protected $label;
+
+       /** @var Message */
+       protected $help;
+
+       /**
+        * @param string $name Button name
+        * @param Message $label Button label
+        * @param Message $help Button help
+        * @param bool $required The button is required for authentication to proceed.
+        */
+       public function __construct( $name, Message $label, Message $help, $required = false ) {
+               $this->name = $name;
+               $this->label = $label;
+               $this->help = $help;
+               $this->required = $required ? self::REQUIRED : self::OPTIONAL;
+       }
+
+       public function getUniqueId() {
+               return parent::getUniqueId() . ':' . $this->name;
+       }
+
+       public function getFieldInfo() {
+               return [
+                       $this->name => [
+                               'type' => 'button',
+                               'label' => $this->label,
+                               'help' => $this->help,
+                       ]
+               ];
+       }
+
+       /**
+        * Fetch a ButtonAuthenticationRequest or subclass by name
+        * @param AuthenticationRequest[] $reqs Requests to search
+        * @param string $name Name to look for
+        * @return ButtonAuthenticationRequest|null Returns null if there is not
+        *  exactly one matching request.
+        */
+       public static function getRequestByName( array $reqs, $name ) {
+               $requests = array_filter( $reqs, function ( $req ) use ( $name ) {
+                       return $req instanceof ButtonAuthenticationRequest && $req->name === $name;
+               } );
+               return count( $requests ) === 1 ? reset( $requests ) : null;
+       }
+
+       /**
+        * @codeCoverageIgnore
+        */
+       public static function __set_state( $data ) {
+               if ( !isset( $data['label'] ) ) {
+                       $data['label'] = new \RawMessage( '$1', $data['name'] );
+               } elseif ( is_string( $data['label'] ) ) {
+                       $data['label'] = new \Message( $data['label'] );
+               } elseif ( is_array( $data['label'] ) ) {
+                       $data['label'] = call_user_func_array( 'Message::newFromKey', $data['label'] );
+               }
+               if ( !isset( $data['help'] ) ) {
+                       $data['help'] = new \RawMessage( '$1', $data['name'] );
+               } elseif ( is_string( $data['help'] ) ) {
+                       $data['help'] = new \Message( $data['help'] );
+               } elseif ( is_array( $data['help'] ) ) {
+                       $data['help'] = call_user_func_array( 'Message::newFromKey', $data['help'] );
+               }
+               $ret = new static( $data['name'], $data['label'], $data['help'] );
+               foreach ( $data as $k => $v ) {
+                       $ret->$k = $v;
+               }
+               return $ret;
+       }
+}
diff --git a/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php b/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..070da9f
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use StatusValue;
+use User;
+
+/**
+ * Check if the user is blocked, and prevent authentication if so.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CheckBlocksSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+       /** @var bool */
+       protected $blockDisablesLogin = null;
+
+       /**
+        * @param array $params
+        *  - blockDisablesLogin: (bool) Whether blocked accounts can log in,
+        *    defaults to $wgBlockDisablesLogin
+        */
+       public function __construct( $params = [] ) {
+               if ( isset( $params['blockDisablesLogin'] ) ) {
+                       $this->blockDisablesLogin = (bool)$params['blockDisablesLogin'];
+               }
+       }
+
+       public function setConfig( Config $config ) {
+               parent::setConfig( $config );
+
+               if ( $this->blockDisablesLogin === null ) {
+                       $this->blockDisablesLogin = $this->config->get( 'BlockDisablesLogin' );
+               }
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               if ( !$this->blockDisablesLogin ) {
+                       return AuthenticationResponse::newAbstain();
+               } elseif ( $user->isBlocked() ) {
+                       return AuthenticationResponse::newFail(
+                               new \Message( 'login-userblocked', [ $user->getName() ] )
+                       );
+               } else {
+                       return AuthenticationResponse::newPass();
+               }
+       }
+
+       public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               $block = $user->isBlockedFromCreateAccount();
+               if ( $block ) {
+                       $errorParams = [
+                               $block->getTarget(),
+                               $block->mReason ?: \Message::newFromKey( 'blockednoreason' )->text(),
+                               $block->getByName()
+                       ];
+
+                       if ( $block->getType() === \Block::TYPE_RANGE ) {
+                               $errorMessage = 'cantcreateaccount-range-text';
+                               $errorParams[] = $this->manager->getRequest()->getIP();
+                       } else {
+                               $errorMessage = 'cantcreateaccount-text';
+                       }
+
+                       return StatusValue::newFatal(
+                               new \Message( $errorMessage, $errorParams )
+                       );
+               } else {
+                       return StatusValue::newGood();
+               }
+       }
+
+}
diff --git a/includes/auth/ConfirmLinkAuthenticationRequest.php b/includes/auth/ConfirmLinkAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..b82914f
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+class ConfirmLinkAuthenticationRequest extends AuthenticationRequest {
+       /** @var AuthenticationRequest[] */
+       protected $linkRequests;
+
+       /** @var string[] List of unique IDs of the confirmed accounts. */
+       public $confirmedLinkIDs = [];
+
+       /**
+        * @param AuthenticationRequest[] $linkRequests A list of autolink requests
+        *  which need to be confirmed.
+        */
+       public function __construct( array $linkRequests ) {
+               if ( !$linkRequests ) {
+                       throw new \InvalidArgumentException( '$linkRequests must not be empty' );
+               }
+               $this->linkRequests = $linkRequests;
+       }
+
+       public function getFieldInfo() {
+               $options = [];
+               foreach ( $this->linkRequests as $req ) {
+                       $description = $req->describeCredentials();
+                       $options[$req->getUniqueId()] = wfMessage(
+                               'authprovider-confirmlink-option',
+                               $description['provider']->text(), $description['account']->text()
+                       );
+               }
+               return [
+                       'confirmedLinkIDs' => [
+                               'type' => 'multiselect',
+                               'options' => $options,
+                               'label' => wfMessage( 'authprovider-confirmlink-request-label' ),
+                               'help' => wfMessage( 'authprovider-confirmlink-request-help' ),
+                               'optional' => true,
+                       ]
+               ];
+       }
+
+       public function getUniqueId() {
+               return parent::getUniqueId() . ':' . implode( '|', array_map( function ( $req ) {
+                       return $req->getUniqueId();
+               }, $this->linkRequests ) );
+       }
+
+       /**
+        * Implementing this mainly for use from the unit tests.
+        * @param array $data
+        * @return AuthenticationRequest
+        */
+       public static function __set_state( $data ) {
+               $ret = new static( $data['linkRequests'] );
+               foreach ( $data as $k => $v ) {
+                       $ret->$k = $v;
+               }
+               return $ret;
+       }
+}
diff --git a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..180aaae
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * Links third-party authentication to the user's account
+ *
+ * If the user logged into linking provider accounts that aren't linked to a
+ * local user, this provider will prompt the user to link them after a
+ * successful login or account creation.
+ *
+ * To avoid confusing behavior, this provider should be later in the
+ * configuration list than any provider that can abort the authentication
+ * process, so that it is only invoked for successful authentication.
+ */
+class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               return $this->beginLinkAttempt( $user, 'AuthManager::authnState' );
+       }
+
+       public function continueSecondaryAuthentication( $user, array $reqs ) {
+               return $this->continueLinkAttempt( $user, 'AuthManager::authnState', $reqs );
+       }
+
+       public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               return $this->beginLinkAttempt( $user, 'AuthManager::accountCreationState' );
+       }
+
+       public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               return $this->continueLinkAttempt( $user, 'AuthManager::accountCreationState', $reqs );
+       }
+
+       /**
+        * Begin the link attempt
+        * @param User $user
+        * @param string $key Session key to look in
+        * @return AuthenticationResponse
+        */
+       protected function beginLinkAttempt( $user, $key ) {
+               $session = $this->manager->getRequest()->getSession();
+               $state = $session->getSecret( $key );
+               if ( !is_array( $state ) ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+               $maybeLink = $state['maybeLink'];
+               if ( !$maybeLink ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $req = new ConfirmLinkAuthenticationRequest( $maybeLink );
+               return AuthenticationResponse::newUI(
+                       [ $req ],
+                       wfMessage( 'authprovider-confirmlink-message' )
+               );
+       }
+
+       /**
+        * Continue the link attempt
+        * @param User $user
+        * @param string $key Session key to look in
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse
+        */
+       protected function continueLinkAttempt( $user, $key, array $reqs ) {
+               $req = ButtonAuthenticationRequest::getRequestByName( $reqs, 'linkOk' );
+               if ( $req ) {
+                       return AuthenticationResponse::newPass();
+               }
+
+               $req = AuthenticationRequest::getRequestByClass( $reqs, ConfirmLinkAuthenticationRequest::class );
+               if ( !$req ) {
+                       // WTF? Retry.
+                       return $this->beginLinkAttempt( $user, $key );
+               }
+
+               $session = $this->manager->getRequest()->getSession();
+               $state = $session->getSecret( $key );
+               if ( !is_array( $state ) ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $maybeLink = [];
+               foreach ( $state['maybeLink'] as $linkReq ) {
+                       $maybeLink[$linkReq->getUniqueId()] = $linkReq;
+               }
+               if ( !$maybeLink ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $state['maybeLink'] = [];
+               $session->setSecret( $key, $state );
+
+               $statuses = [];
+               $anyFailed = false;
+               foreach ( $req->confirmedLinkIDs as $id ) {
+                       if ( isset( $maybeLink[$id] ) ) {
+                               $req = $maybeLink[$id];
+                               $req->username = $user->getName();
+                               if ( !$req->action ) {
+                                       // Make sure the action is set, but don't override it if
+                                       // the provider filled it in.
+                                       $req->action = AuthManager::ACTION_CHANGE;
+                               }
+                               $status = $this->manager->allowsAuthenticationDataChange( $req );
+                               $statuses[] = [ $req, $status ];
+                               if ( $status->isGood() ) {
+                                       $this->manager->changeAuthenticationData( $req );
+                               } else {
+                                       $anyFailed = true;
+                               }
+                       }
+               }
+               if ( !$anyFailed ) {
+                       return AuthenticationResponse::newPass();
+               }
+
+               $combinedStatus = \Status::newGood();
+               foreach ( $statuses as $data ) {
+                       list( $req, $status ) = $data;
+                       $descriptionInfo = $req->describeCredentials();
+                       $description = wfMessage(
+                               'authprovider-confirmlink-option',
+                               $descriptionInfo['provider']->text(), $descriptionInfo['account']->text()
+                       )->text();
+                       if ( $status->isGood() ) {
+                               $combinedStatus->error( wfMessage( 'authprovider-confirmlink-success-line', $description ) );
+                       } else {
+                               $combinedStatus->error( wfMessage(
+                                       'authprovider-confirmlink-failure-line', $description, $status->getMessage()->text()
+                               ) );
+                       }
+               }
+               return AuthenticationResponse::newUI(
+                       [
+                               new ButtonAuthenticationRequest(
+                                       'linkOk', wfMessage( 'ok' ), wfMessage( 'authprovider-confirmlink-ok-help' )
+                               )
+                       ],
+                       $combinedStatus->getMessage( 'authprovider-confirmlink-failed' )
+               );
+       }
+}
diff --git a/includes/auth/CreateFromLoginAuthenticationRequest.php b/includes/auth/CreateFromLoginAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..949302d
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This transfers state between the login and account creation flows.
+ *
+ * AuthManager::getAuthenticationRequests() won't return this type, but it
+ * may be passed to AuthManager::beginAccountCreation() anyway.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CreateFromLoginAuthenticationRequest extends AuthenticationRequest {
+       public $required = self::OPTIONAL;
+
+       /** @var AuthenticationRequest|null */
+       public $createRequest;
+
+       /** @var AuthenticationRequest[] */
+       public $maybeLink = [];
+
+       /**
+        * @param AuthenticationRequest|null $createRequest A request to use to
+        *  begin creating the account
+        * @param AuthenticationRequest[] $maybeLink Additional accounts to link
+        *  after creation.
+        */
+       public function __construct(
+               AuthenticationRequest $createRequest = null, array $maybeLink = []
+       ) {
+               $this->createRequest = $createRequest;
+               $this->maybeLink = $maybeLink;
+       }
+
+       public function getFieldInfo() {
+               return [];
+       }
+
+       public function loadFromSubmission( array $data ) {
+               return true;
+       }
+}
diff --git a/includes/auth/CreatedAccountAuthenticationRequest.php b/includes/auth/CreatedAccountAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..48a6e1d
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * Returned from account creation to allow for logging into the created account
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CreatedAccountAuthenticationRequest extends AuthenticationRequest {
+
+       public $required = self::OPTIONAL;
+
+       /** @var int User id */
+       public $id;
+
+       public function getFieldInfo() {
+               return [];
+       }
+
+       /**
+        * @param int $id User id
+        * @param string $name Username
+        */
+       public function __construct( $id, $name ) {
+               $this->id = $id;
+               $this->username = $name;
+       }
+}
diff --git a/includes/auth/CreationReasonAuthenticationRequest.php b/includes/auth/CreationReasonAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..1711aec
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * Authentication request for the reason given for account creation.
+ * Used in logs and for notification.
+ */
+class CreationReasonAuthenticationRequest extends AuthenticationRequest {
+       /** @var string Account creation reason (only used when creating for someone else) */
+       public $reason;
+
+       public function getFieldInfo() {
+               return [
+                       'reason' => [
+                               'type' => 'string',
+                               'label' => wfMessage( 'createacct-reason' ),
+                               'help' => wfMessage( 'createacct-reason-help' ),
+                       ],
+               ];
+       }
+}
diff --git a/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php b/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..c632e3c
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Config;
+use StatusValue;
+
+/**
+ * Handles email notification / email address confirmation for account creation.
+ *
+ * Set 'no-email' to true (via AuthManager::setAuthenticationSessionData) to skip this provider.
+ * Primary providers doing so are expected to take care of email address confirmation.
+ */
+class EmailNotificationSecondaryAuthenticationProvider
+       extends AbstractSecondaryAuthenticationProvider
+{
+       /** @var bool */
+       protected $sendConfirmationEmail;
+
+       /**
+        * @param array $params
+        *  - sendConfirmationEmail: (bool) send an email asking the user to confirm their email
+        *    address after a successful registration
+        */
+       public function __construct( $params = [] ) {
+               if ( isset( $params['sendConfirmationEmail'] ) ) {
+                       $this->sendConfirmationEmail = (bool)$params['sendConfirmationEmail'];
+               }
+       }
+
+       public function setConfig( Config $config ) {
+               parent::setConfig( $config );
+
+               if ( $this->sendConfirmationEmail === null ) {
+                       $this->sendConfirmationEmail = $this->config->get( 'EnableEmail' )
+                               && $this->config->get( 'EmailAuthentication' );
+               }
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               if (
+                       $this->sendConfirmationEmail
+                       && $user->getEmail()
+                       && !$this->manager->getAuthenticationSessionData( 'no-email' )
+               ) {
+                       $status = $user->sendConfirmationMail();
+                       $user->saveSettings();
+                       if ( $status->isGood() ) {
+                               // TODO show 'confirmemail_oncreate' success message
+                       } else {
+                               // TODO show 'confirmemail_sendfailed' error message
+                               $this->logger->warning( 'Could not send confirmation email: ' .
+                                       $status->getWikiText( false, false, 'en' ) );
+                       }
+               }
+
+               return AuthenticationResponse::newPass();
+       }
+}
diff --git a/includes/auth/LegacyHookPreAuthenticationProvider.php b/includes/auth/LegacyHookPreAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..1a8a758
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use LoginForm;
+use StatusValue;
+use User;
+
+/**
+ * A pre-authentication provider to call some legacy hooks.
+ * @ingroup Auth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvider {
+
+       public function testForAuthentication( array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+               if ( $req ) {
+                       $user = User::newFromName( $req->username );
+                       $password = $req->password;
+               } else {
+                       $user = null;
+                       foreach ( $reqs as $req ) {
+                               if ( $req->username !== null ) {
+                                       $user = User::newFromName( $req->username );
+                                       break;
+                               }
+                       }
+                       if ( !$user ) {
+                               $this->logger->debug( __METHOD__ . ': No username in $reqs, skipping hooks' );
+                               return StatusValue::newGood();
+                       }
+
+                       // Something random for the 'AbortLogin' hook.
+                       $password = wfRandomString( 32 );
+               }
+
+               $msg = null;
+               if ( !\Hooks::run( 'LoginUserMigrated', [ $user, &$msg ] ) ) {
+                       return $this->makeFailResponse(
+                               $user, null, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'
+                       );
+               }
+
+               $abort = LoginForm::ABORTED;
+               $msg = null;
+               if ( !\Hooks::run( 'AbortLogin', [ $user, $password, &$abort, &$msg ] ) ) {
+                       return $this->makeFailResponse( $user, null, $abort, $msg, 'AbortLogin' );
+               }
+
+               return StatusValue::newGood();
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               $abortError = '';
+               $abortStatus = null;
+               if ( !\Hooks::run( 'AbortNewAccount', [ $user, &$abortError, &$abortStatus ] ) ) {
+                       // Hook point to add extra creation throttles and blocks
+                       $this->logger->debug( __METHOD__ . ': a hook blocked creation' );
+                       if ( $abortStatus === null ) {
+                               // Report back the old string as a raw message status.
+                               // This will report the error back as 'createaccount-hook-aborted'
+                               // with the given string as the message.
+                               // To return a different error code, return a StatusValue object.
+                               $msg = wfMessage( 'createaccount-hook-aborted' )->rawParams( $abortError );
+                               return StatusValue::newFatal( $msg );
+                       } else {
+                               // For MediaWiki 1.23+ and updated hooks, return the Status object
+                               // returned from the hook.
+                               $ret = StatusValue::newGood();
+                               $ret->merge( $abortStatus );
+                               return $ret;
+                       }
+               }
+
+               return StatusValue::newGood();
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               if ( $autocreate !== false ) {
+                       $abortError = '';
+                       if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ] ) ) {
+                               // Hook point to add extra creation throttles and blocks
+                               $this->logger->debug( __METHOD__ . ": a hook blocked auto-creation: $abortError\n" );
+                               return $this->makeFailResponse(
+                                       $user, $user, LoginForm::ABORTED, $abortError, 'AbortAutoAccount'
+                               );
+                       }
+               } else {
+                       $abortError = '';
+                       $abortStatus = null;
+                       if ( !\Hooks::run( 'AbortNewAccount', [ $user, &$abortError, &$abortStatus ] ) ) {
+                               // Hook point to add extra creation throttles and blocks
+                               $this->logger->debug( __METHOD__ . ': a hook blocked creation' );
+                               if ( $abortStatus === null ) {
+                                       // Report back the old string as a raw message status.
+                                       // This will report the error back as 'createaccount-hook-aborted'
+                                       // with the given string as the message.
+                                       // To return a different error code, return a StatusValue object.
+                                       $msg = wfMessage( 'createaccount-hook-aborted' )->rawParams( $abortError );
+                                       return StatusValue::newFatal( $msg );
+                               } else {
+                                       // For MediaWiki 1.23+ and updated hooks, return the Status object
+                                       // returned from the hook.
+                                       $ret = StatusValue::newGood();
+                                       $ret->merge( $abortStatus );
+                                       return $ret;
+                               }
+                       }
+               }
+
+               return StatusValue::newGood();
+       }
+
+       /**
+        * Construct an appropriate failure response
+        * @param User $user
+        * @param User|null $creator
+        * @param int $constant LoginForm constant
+        * @param string|null $msg Message
+        * @param string $hook Hook
+        * @return StatusValue
+        */
+       protected function makeFailResponse( $user, $creator, $constant, $msg, $hook ) {
+               switch ( $constant ) {
+                       case LoginForm::SUCCESS:
+                               // WTF?
+                               $this->logger->debug( "$hook is SUCCESS?!" );
+                               return StatusValue::newGood();
+
+                       case LoginForm::NEED_TOKEN:
+                               return StatusValue::newFatal( $msg ?: 'nocookiesforlogin' );
+
+                       case LoginForm::WRONG_TOKEN:
+                               return StatusValue::newFatal( $msg ?: 'sessionfailure' );
+
+                       case LoginForm::NO_NAME:
+                       case LoginForm::ILLEGAL:
+                               return StatusValue::newFatal( $msg ?: 'noname' );
+
+                       case LoginForm::WRONG_PLUGIN_PASS:
+                       case LoginForm::WRONG_PASS:
+                               return StatusValue::newFatal( $msg ?: 'wrongpassword' );
+
+                       case LoginForm::NOT_EXISTS:
+                               return StatusValue::newFatal( $msg ?: 'nosuchusershort', wfEscapeWikiText( $user->getName() ) );
+
+                       case LoginForm::EMPTY_PASS:
+                               return StatusValue::newFatal( $msg ?: 'wrongpasswordempty' );
+
+                       case LoginForm::RESET_PASS:
+                               return StatusValue::newFatal( $msg ?: 'resetpass_announce' );
+
+                       case LoginForm::THROTTLED:
+                               $throttle = $this->config->get( 'PasswordAttemptThrottle' );
+                               return StatusValue::newFatal(
+                                       $msg ?: 'login-throttled',
+                                       \Message::durationParam( $throttle['seconds'] )
+                               );
+
+                       case LoginForm::USER_BLOCKED:
+                               return StatusValue::newFatal(
+                                       $msg ?: 'login-userblocked', wfEscapeWikiText( $user->getName() )
+                               );
+
+                       case LoginForm::ABORTED:
+                               return StatusValue::newFatal(
+                                       $msg ?: 'login-abort-generic', wfEscapeWikiText( $user->getName() )
+                               );
+
+                       case LoginForm::USER_MIGRATED:
+                               $error = $msg ?: 'login-migrated-generic';
+                               return call_user_func_array( 'StatusValue::newFatal', (array)$error );
+
+                       // @codeCoverageIgnoreStart
+                       case LoginForm::CREATE_BLOCKED: // Can never happen
+                       default:
+                               throw new \DomainException( __METHOD__ . ": Unhandled case value from $hook" );
+               }
+                       // @codeCoverageIgnoreEnd
+       }
+}
diff --git a/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php b/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..5f5ef79
--- /dev/null
@@ -0,0 +1,314 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A primary authentication provider that uses the password field in the 'user' table.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class LocalPasswordPrimaryAuthenticationProvider
+       extends AbstractPasswordPrimaryAuthenticationProvider
+{
+
+       /** @var bool If true, this instance is for legacy logins only. */
+       protected $loginOnly = false;
+
+       /**
+        * @param array $params Settings
+        *  - loginOnly: If true, the local passwords are for legacy logins only:
+        *    the local password will be invalidated when authentication is changed
+        *    and new users will not have a valid local password set.
+        */
+       public function __construct( $params = [] ) {
+               parent::__construct( $params );
+               $this->loginOnly = !empty( $params['loginOnly'] );
+       }
+
+       protected function getPasswordResetData( $username, $row ) {
+               $now = wfTimestamp();
+               $expiration = wfTimestampOrNull( TS_UNIX, $row->user_password_expires );
+               if ( $expiration === null || $expiration >= $now ) {
+                       return null;
+               }
+
+               $grace = $this->config->get( 'PasswordExpireGrace' );
+               if ( $expiration + $grace < $now ) {
+                       $data = [
+                               'hard' => true,
+                               'msg' => \Status::newFatal( 'resetpass-expired' )->getMessage(),
+                       ];
+               } else {
+                       $data = [
+                               'hard' => false,
+                               'msg' => \Status::newFatal( 'resetpass-expired-soft' )->getMessage(),
+                       ];
+               }
+
+               return (object)$data;
+       }
+
+       public function beginPrimaryAuthentication( array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+               if ( !$req ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               if ( $req->username === null || $req->password === null ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $fields = [
+                       'user_id', 'user_password', 'user_password_expires',
+               ];
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       $fields,
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+               if ( !$row ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               // Check for *really* old password hashes that don't even have a type
+               // The old hash format was just an md5 hex hash, with no type information
+               if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
+                       if ( $this->config->get( 'PasswordSalt' ) ) {
+                               $row->user_password = ":A:{$row->user_id}:{$row->user_password}";
+                       } else {
+                               $row->user_password = ":A:{$row->user_password}";
+                       }
+               }
+
+               $status = $this->checkPasswordValidity( $username, $req->password );
+               if ( !$status->isOk() ) {
+                       // Fatal, can't log in
+                       return AuthenticationResponse::newFail( $status->getMessage() );
+               }
+
+               $pwhash = $this->getPassword( $row->user_password );
+               if ( !$pwhash->equals( $req->password ) ) {
+                       if ( $this->config->get( 'LegacyEncoding' ) ) {
+                               // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+                               // Check for this with iconv
+                               $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $req->password );
+                               if ( $cp1252Password === $req->password || !$pwhash->equals( $cp1252Password ) ) {
+                                       return $this->failResponse( $req );
+                               }
+                       } else {
+                               return $this->failResponse( $req );
+                       }
+               }
+
+               // @codeCoverageIgnoreStart
+               if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
+                       $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+                       $dbw->update(
+                               'user',
+                               [ 'user_password' => $pwhash->toString() ],
+                               [ 'user_id' => $row->user_id ],
+                               __METHOD__
+                       );
+               }
+               // @codeCoverageIgnoreEnd
+
+               $this->setPasswordResetFlag( $username, $status, $row );
+
+               return AuthenticationResponse::newPass( $username );
+       }
+
+       public function testUserCanAuthenticate( $username ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       [ 'user_password' ],
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+               if ( !$row ) {
+                       return false;
+               }
+
+               // Check for *really* old password hashes that don't even have a type
+               // The old hash format was just an md5 hex hash, with no type information
+               if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
+                       return true;
+               }
+
+               return !$this->getPassword( $row->user_password ) instanceof \InvalidPassword;
+       }
+
+       public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
+               return (bool)wfGetDB( $db )->selectField(
+                       [ 'user' ],
+                       [ 'user_id' ],
+                       [ 'user_name' => $username ],
+                       __METHOD__,
+                       $options
+               );
+       }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               // We only want to blank the password if something else will accept the
+               // new authentication data, so return 'ignore' here.
+               if ( $this->loginOnly ) {
+                       return \StatusValue::newGood( 'ignored' );
+               }
+
+               if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
+                       if ( !$checkData ) {
+                               return \StatusValue::newGood();
+                       }
+
+                       $username = User::getCanonicalName( $req->username, 'usable' );
+                       if ( $username !== false ) {
+                               $row = wfGetDB( DB_MASTER )->selectRow(
+                                       'user',
+                                       [ 'user_id' ],
+                                       [ 'user_name' => $username ],
+                                       __METHOD__
+                               );
+                               if ( $row ) {
+                                       $sv = \StatusValue::newGood();
+                                       if ( $req->password !== null ) {
+                                               if ( $req->password !== $req->retype ) {
+                                                       $sv->fatal( 'badretype' );
+                                               } else {
+                                                       $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+                                               }
+                                       }
+                                       return $sv;
+                               }
+                       }
+               }
+
+               return \StatusValue::newGood( 'ignored' );
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+               $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+               if ( $username === false ) {
+                       return;
+               }
+
+               $pwhash = null;
+
+               if ( $this->loginOnly ) {
+                       $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
+                       $expiry = null;
+                       // @codeCoverageIgnoreStart
+               } elseif ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
+                       // @codeCoverageIgnoreEnd
+                       $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+                       $expiry = $this->getNewPasswordExpiry( $username );
+               }
+
+               if ( $pwhash ) {
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->update(
+                               'user',
+                               [
+                                       'user_password' => $pwhash->toString(),
+                                       'user_password_expires' => $dbw->timestampOrNull( $expiry ),
+                               ],
+                               [ 'user_name' => $username ],
+                               __METHOD__
+                       );
+               }
+       }
+
+       public function accountCreationType() {
+               return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+
+               $ret = \StatusValue::newGood();
+               if ( !$this->loginOnly && $req && $req->username !== null && $req->password !== null ) {
+                       if ( $req->password !== $req->retype ) {
+                               $ret->fatal( 'badretype' );
+                       } else {
+                               $ret->merge(
+                                       $this->checkPasswordValidity( $user->getName(), $req->password )
+                               );
+                       }
+               }
+               return $ret;
+       }
+
+       public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+               if ( $this->accountCreationType() === self::TYPE_NONE ) {
+                       throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+               }
+
+               $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+               if ( $req ) {
+                       if ( $req->username !== null && $req->password !== null ) {
+                               // Nothing we can do besides claim it, because the user isn't in
+                               // the DB yet
+                               if ( $req->username !== $user->getName() ) {
+                                       $req = clone( $req );
+                                       $req->username = $user->getName();
+                               }
+                               $ret = AuthenticationResponse::newPass( $req->username );
+                               $ret->createRequest = $req;
+                               return $ret;
+                       }
+               }
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
+               if ( $this->accountCreationType() === self::TYPE_NONE ) {
+                       throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+               }
+
+               // Now that the user is in the DB, set the password on it.
+               $this->providerChangeAuthenticationData( $res->createRequest );
+
+               return null;
+       }
+}
diff --git a/includes/auth/PasswordAuthenticationRequest.php b/includes/auth/PasswordAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..187c29a
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This is a value object for authentication requests with a username and password
+ * @ingroup Auth
+ * @since 1.27
+ */
+class PasswordAuthenticationRequest extends AuthenticationRequest {
+       /** @var string Password */
+       public $password = null;
+
+       /** @var string Password, again */
+       public $retype = null;
+
+       public function getFieldInfo() {
+               if ( $this->action === AuthManager::ACTION_REMOVE ) {
+                       return [];
+               }
+
+               // for password change it's nice to make extra clear that we are asking for the new password
+               $forNewPassword = $this->action === AuthManager::ACTION_CHANGE;
+               $passwordLabel = $forNewPassword ? 'newpassword' : 'userlogin-yourpassword';
+               $retypeLabel = $forNewPassword ? 'retypenew' : 'yourpasswordagain';
+
+               $ret = [
+                       'username' => [
+                               'type' => 'string',
+                               'label' => wfMessage( 'userlogin-yourname' ),
+                               'help' => wfMessage( 'authmanager-username-help' ),
+                       ],
+                       'password' => [
+                               'type' => 'password',
+                               'label' => wfMessage( $passwordLabel ),
+                               'help' => wfMessage( 'authmanager-password-help' ),
+                       ],
+               ];
+
+               switch ( $this->action ) {
+                       case AuthManager::ACTION_CHANGE:
+                       case AuthManager::ACTION_REMOVE:
+                               unset( $ret['username'] );
+                               break;
+               }
+
+               if ( $this->action !== AuthManager::ACTION_LOGIN ) {
+                       $ret['retype'] = [
+                               'type' => 'password',
+                               'label' => wfMessage( $retypeLabel ),
+                               'help' => wfMessage( 'authmanager-retype-help' ),
+                       ];
+               }
+
+               return $ret;
+       }
+
+       public function describeCredentials() {
+               return [
+                       'provider' => wfMessage( 'authmanager-provider-password' ),
+                       'account' => new \RawMessage( '$1', [ $this->username ] ),
+               ];
+       }
+}
diff --git a/includes/auth/PasswordDomainAuthenticationRequest.php b/includes/auth/PasswordDomainAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..ddad54b
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This is a value object for authentication requests with a username, password, and domain
+ * @ingroup Auth
+ * @since 1.27
+ */
+class PasswordDomainAuthenticationRequest extends PasswordAuthenticationRequest {
+       /** @var string[] Domains available */
+       private $domainList;
+
+       /** @var string Domain */
+       public $domain = null;
+
+       /**
+        * @param string[] $domainList List of available domains
+        */
+       public function __construct( array $domainList ) {
+               $this->domainList = $domainList;
+       }
+
+       public function getFieldInfo() {
+               $ret = parent::getFieldInfo();
+
+               // Only add a domain field if we have the username field included
+               if ( isset( $ret['username'] ) ) {
+                       $ret['domain'] = [
+                               'type' => 'select',
+                               'options' => [],
+                               'label' => wfMessage( 'yourdomainname' ),
+                               'help' => wfMessage( 'authmanager-domain-help' ),
+                       ];
+                       foreach ( $this->domainList as $domain ) {
+                               $ret['domain']['options'][$domain] = new \RawMessage( '$1', [ $domain ] );
+                       }
+               }
+
+               return $ret;
+       }
+
+       public function describeCredentials() {
+               return [
+                       'provider' => wfMessage( 'authmanager-provider-password-domain' ),
+                       'account' => wfMessage(
+                               'authmanager-account-password-domain', [ $this->username, $this->domain ]
+                       ),
+               ];
+       }
+
+       /**
+        * @codeCoverageIgnore
+        */
+       public static function __set_state( $data ) {
+               $ret = new static( $data['domainList'] );
+               foreach ( $data as $k => $v ) {
+                       if ( $k !== 'domainList' ) {
+                               $ret->$k = $v;
+                       }
+               }
+               return $ret;
+       }
+}
diff --git a/includes/auth/PreAuthenticationProvider.php b/includes/auth/PreAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..846d16e
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Pre-authentication 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A pre-authentication provider is a check that must pass for authentication
+ * to proceed.
+ *
+ * A PreAuthenticationProvider is used to supply arbitrary checks to be
+ * performed before the PrimaryAuthenticationProviders are consulted during the
+ * login process. Possible uses include checking that a per-IP throttle has not
+ * been reached or that a captcha has been solved.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+interface PreAuthenticationProvider extends AuthenticationProvider {
+
+       /**
+        * Determine whether an authentication may begin
+        *
+        * Called from AuthManager::beginAuthentication()
+        *
+        * @param AuthenticationRequest[] $reqs
+        * @return StatusValue
+        */
+       public function testForAuthentication( array $reqs );
+
+       /**
+        * Post-login callback
+        * @param User|null $user User that was attempted to be logged in, if known.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAuthentication( $user, AuthenticationResponse $response );
+
+       /**
+        * Determine whether an account creation may begin
+        *
+        * Called from AuthManager::beginAccountCreation()
+        *
+        * @note No need to test if the account exists, AuthManager checks that
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return StatusValue
+        */
+       public function testForAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Determine whether an account may be created
+        *
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param bool|string $autocreate False if this is not an auto-creation, or
+        *  the source of the auto-creation passed to AuthManager::autoCreateUser().
+        * @return StatusValue
+        */
+       public function testUserForCreation( $user, $autocreate );
+
+       /**
+        * Post-creation callback
+        * @param User $user User that was attempted to be created.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+       /**
+        * Determine whether an account may linked to another authentication method
+        *
+        * @param User $user User being linked.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @return StatusValue
+        */
+       public function testForAccountLink( $user );
+
+       /**
+        * Post-link callback
+        * @param User $user User that was attempted to be linked.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAccountLink( $user, AuthenticationResponse $response );
+
+}
diff --git a/includes/auth/PrimaryAuthenticationProvider.php b/includes/auth/PrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..169e7f1
--- /dev/null
@@ -0,0 +1,334 @@
+<?php
+/**
+ * Primary authentication 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A primary authentication provider determines which user is trying to log in.
+ *
+ * A PrimaryAuthenticationProvider is used as part of presenting a login form
+ * to authenticate a user. In particular, the PrimaryAuthenticationProvider
+ * takes form data and determines the authenticated user (if any) corresponds
+ * to that form data. It might do this on the basis of a username and password
+ * in that data, or by interacting with an external authentication service
+ * (e.g. using OpenID), or by some other mechanism.
+ *
+ * A PrimaryAuthenticationProvider would not be appropriate for something like
+ * HTTP authentication, OAuth, or SSL client certificates where each HTTP
+ * request contains all the information needed to identify the user. In that
+ * case you'll want to be looking at a \\MediaWiki\\Session\\SessionProvider
+ * instead.
+ *
+ * This interface also provides methods for changing authentication data such
+ * as passwords and for creating new users who can later be authenticated with
+ * this provider.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+interface PrimaryAuthenticationProvider extends AuthenticationProvider {
+       /** Provider can create accounts */
+       const TYPE_CREATE = 'create';
+       /** Provider can link to existing accounts elsewhere */
+       const TYPE_LINK = 'link';
+       /** Provider cannot create or link to accounts */
+       const TYPE_NONE = 'none';
+
+       /**
+        * Start an authentication flow
+        *
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Secondary providers will now run.
+        *  - FAIL: The user is not authenticated. Fail the authentication process.
+        *  - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+        *  - UI: The $reqs are accepted, no other primary provider will run.
+        *    Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: The $reqs are accepted, no other primary provider will run.
+        *    Redirection to a third party is needed to complete the process.
+        */
+       public function beginPrimaryAuthentication( array $reqs );
+
+       /**
+        * Continue an authentication flow
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Secondary providers will now run.
+        *  - FAIL: The user is not authenticated. Fail the authentication process.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function continuePrimaryAuthentication( array $reqs );
+
+       /**
+        * Post-login callback
+        * @param User|null $user User that was attempted to be logged in, if known.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAuthentication( $user, AuthenticationResponse $response );
+
+       /**
+        * Test whether the named user exists
+        * @param string $username
+        * @param int $flags Bitfield of User:READ_* constants
+        * @return bool
+        */
+       public function testUserExists( $username, $flags = User::READ_NORMAL );
+
+       /**
+        * Test whether the named user can authenticate with this provider
+        * @param string $username
+        * @return bool
+        */
+       public function testUserCanAuthenticate( $username );
+
+       /**
+        * Normalize the username for authentication
+        *
+        * Any two inputs that would result in the same user being authenticated
+        * should return the same string here, while inputs that would result in
+        * different users should return different strings.
+        *
+        * If possible, the best thing to do here is to return the canonicalized
+        * name of the local user account that would be used. If not, return
+        * something that would be invalid as a local username (e.g. wrap an email
+        * address in "<>", or append "#servicename" to the username passed to a
+        * third-party service).
+        *
+        * If the provider doesn't use a username at all in its
+        * AuthenticationRequests, return null. If the name is syntactically
+        * invalid, it's probably best to return null.
+        *
+        * @param string $username
+        * @return string|null
+        */
+       public function providerNormalizeUsername( $username );
+
+       /**
+        * Revoke the user's credentials
+        *
+        * This may cause the user to no longer exist for the provider, or the user
+        * may continue to exist in a "disabled" state.
+        *
+        * The intention is that the named account will never again be usable for
+        * normal login (i.e. there is no way to undo the revocation of access).
+        *
+        * @param string $username
+        */
+       public function providerRevokeAccessForUser( $username );
+
+       /**
+        * Determine whether a property can change
+        * @see AuthManager::allowsPropertyChange()
+        * @param string $property
+        * @return bool
+        */
+       public function providerAllowsPropertyChange( $property );
+
+       /**
+        * Validate a change of authentication data (e.g. passwords)
+        *
+        * Return StatusValue::newGood( 'ignored' ) if you don't support this
+        * AuthenticationRequest type.
+        *
+        * @param AuthenticationRequest $req
+        * @param bool $checkData If false, $req hasn't been loaded from the
+        *  submission so checks on user-submitted fields should be skipped.
+        *  $req->username is considered user-submitted for this purpose, even
+        *  if it cannot be changed via $req->loadFromSubmission.
+        * @return StatusValue
+        */
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       );
+
+       /**
+        * Change or remove authentication data (e.g. passwords)
+        *
+        * If $req was returned for AuthManager::ACTION_CHANGE, the corresponding
+        * credentials should result in a successful login in the future.
+        *
+        * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
+        * credentials should no longer result in a successful login.
+        *
+        * @param AuthenticationRequest $req
+        */
+       public function providerChangeAuthenticationData( AuthenticationRequest $req );
+
+       /**
+        * Fetch the account-creation type
+        * @return string One of the TYPE_* constants
+        */
+       public function accountCreationType();
+
+       /**
+        * Determine whether an account creation may begin
+        *
+        * Called from AuthManager::beginAccountCreation()
+        *
+        * @note No need to test if the account exists, AuthManager checks that
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return StatusValue
+        */
+       public function testForAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Start an account creation flow
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user may be created. Secondary providers will now run.
+        *  - FAIL: The user may not be created. Fail the creation process.
+        *  - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+        *  - UI: The $reqs are accepted, no other primary provider will run.
+        *    Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: The $reqs are accepted, no other primary provider will run.
+        *    Redirection to a third party is needed to complete the process.
+        */
+       public function beginPrimaryAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Continue an account creation flow
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user may be created. Secondary providers will now run.
+        *  - FAIL: The user may not be created. Fail the creation process.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function continuePrimaryAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Post-creation callback
+        *
+        * Called after the user is added to the database, before secondary
+        * authentication providers are run.
+        *
+        * @param User $user User being created (has been added to the database now).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationResponse $response PASS response returned earlier
+        * @return string|null 'newusers' log subtype to use for logging the
+        *   account creation. If null, either 'create' or 'create2' will be used
+        *   depending on $creator.
+        */
+       public function finishAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+       /**
+        * Post-creation callback
+        *
+        * Called when the account creation process ends.
+        *
+        * @param User $user User that was attempted to be created.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+       /**
+        * Determine whether an account may be created
+        *
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param bool|string $autocreate False if this is not an auto-creation, or
+        *  the source of the auto-creation passed to AuthManager::autoCreateUser().
+        * @return StatusValue
+        */
+       public function testUserForCreation( $user, $autocreate );
+
+       /**
+        * Post-auto-creation callback
+        * @param User $user User being created (has been added to the database now).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param string $source The source of the auto-creation passed to
+        *  AuthManager::autoCreateUser().
+        */
+       public function autoCreatedAccount( $user, $source );
+
+       /**
+        * Start linking an account to an existing user
+        * @param User $user User being linked.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is linked.
+        *  - FAIL: The user is not linked. Fail the linking process.
+        *  - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+        *  - UI: The $reqs are accepted, no other primary provider will run.
+        *    Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: The $reqs are accepted, no other primary provider will run.
+        *    Redirection to a third party is needed to complete the process.
+        */
+       public function beginPrimaryAccountLink( $user, array $reqs );
+
+       /**
+        * Continue linking an account to an existing user
+        * @param User $user User being linked.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is linked.
+        *  - FAIL: The user is not linked. Fail the linking process.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function continuePrimaryAccountLink( $user, array $reqs );
+
+       /**
+        * Post-link callback
+        * @param User $user User that was attempted to be linked.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAccountLink( $user, AuthenticationResponse $response );
+
+}
diff --git a/includes/auth/RememberMeAuthenticationRequest.php b/includes/auth/RememberMeAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..d487e31
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\Session\SessionManager;
+use MediaWiki\Session\SessionProvider;
+
+/**
+ * This is an authentication request added by AuthManager to show a "remember
+ * me" checkbox. When checked, it will take more time for the authenticated session to expire.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class RememberMeAuthenticationRequest extends AuthenticationRequest {
+
+       public $required = self::OPTIONAL;
+
+       /** @var int How long the user will be remembered, in seconds */
+       protected $expiration = null;
+
+       /** @var bool */
+       public $rememberMe = false;
+
+       public function __construct() {
+               /** @var SessionProvider $provider */
+               $provider = SessionManager::getGlobalSession()->getProvider();
+               $this->expiration = $provider->getRememberUserDuration();
+       }
+
+       public function getFieldInfo() {
+               if ( !$this->expiration ) {
+                       return [];
+               }
+
+               $expirationDays = ceil( $this->expiration / ( 3600 * 24 ) );
+               return [
+                       'rememberMe' => [
+                               'type' => 'checkbox',
+                               'label' => wfMessage( 'userlogin-remembermypassword' )->numParams( $expirationDays ),
+                               'help' => wfMessage( 'authmanager-userlogin-remembermypassword-help' ),
+                               'optional' => true,
+                       ]
+               ];
+       }
+}
diff --git a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..2e51cf2
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * Reset the local password, if signalled via $this->manager->setAuthenticationSessionData()
+ *
+ * The authentication data key is 'reset-pass'; the data is an object with the
+ * following properties:
+ * - msg: Message object to display to the user
+ * - hard: Boolean, if true the reset cannot be skipped.
+ * - req: Optional PasswordAuthenticationRequest to use to actually reset the
+ *   password. Won't be displayed to the user.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               return $this->tryReset( $user, $reqs );
+       }
+
+       public function continueSecondaryAuthentication( $user, array $reqs ) {
+               return $this->tryReset( $user, $reqs );
+       }
+
+       public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               return $this->tryReset( $user, $reqs );
+       }
+
+       public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+               return $this->tryReset( $user, $reqs );
+       }
+
+       /**
+        * Try to reset the password
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse
+        */
+       protected function tryReset( \User $user, array $reqs ) {
+               $data = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+               if ( !$data ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               if ( is_array( $data ) ) {
+                       $data = (object)$data;
+               }
+               if ( !is_object( $data ) ) {
+                       throw new \UnexpectedValueException( 'reset-pass is not valid' );
+               }
+
+               if ( !isset( $data->msg ) ) {
+                       throw new \UnexpectedValueException( 'reset-pass msg is missing' );
+               } elseif ( !$data->msg instanceof \Message ) {
+                       throw new \UnexpectedValueException( 'reset-pass msg is not valid' );
+               } elseif ( !isset( $data->hard ) ) {
+                       throw new \UnexpectedValueException( 'reset-pass hard is missing' );
+               } elseif ( isset( $data->req ) && (
+                       !$data->req instanceof PasswordAuthenticationRequest ||
+                       !array_key_exists( 'retype', $data->req->getFieldInfo() )
+               ) ) {
+                       throw new \UnexpectedValueException( 'reset-pass req is not valid' );
+               }
+
+               if ( !$data->hard ) {
+                       $req = ButtonAuthenticationRequest::getRequestByName( $reqs, 'skipReset' );
+                       if ( $req ) {
+                               $this->manager->removeAuthenticationSessionData( 'reset-pass' );
+                               return AuthenticationResponse::newPass();
+                       }
+               }
+
+               if ( isset( $data->req ) ) {
+                       $needReq = $data->req;
+               } else {
+                       $needReq = new PasswordAuthenticationRequest();
+                       $needReq->action = AuthManager::ACTION_CHANGE;
+               }
+               $needReqs = [ $needReq ];
+               if ( !$data->hard ) {
+                       $needReqs[] = new ButtonAuthenticationRequest(
+                               'skipReset',
+                               wfMessage( 'authprovider-resetpass-skip-label' ),
+                               wfMessage( 'authprovider-resetpass-skip-help' )
+                       );
+               }
+
+               $req = AuthenticationRequest::getRequestByClass( $reqs, get_class( $needReq ) );
+               if ( !$req || !array_key_exists( 'retype', $req->getFieldInfo() ) ) {
+                       return AuthenticationResponse::newUI( $needReqs, $data->msg );
+               }
+
+               if ( $req->password !== $req->retype ) {
+                       return AuthenticationResponse::newUI( $needReqs, new \Message( 'badretype' ) );
+               }
+
+               $req->username = $user->getName();
+               $status = $this->manager->allowsAuthenticationDataChange( $req );
+               if ( !$status->isGood() ) {
+                       return AuthenticationResponse::newUI( $needReqs, $status->getMessage() );
+               }
+               $this->manager->changeAuthenticationData( $req );
+
+               $this->manager->removeAuthenticationSessionData( 'reset-pass' );
+               return AuthenticationResponse::newPass();
+       }
+}
diff --git a/includes/auth/SecondaryAuthenticationProvider.php b/includes/auth/SecondaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..0d52d25
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/**
+ * Secondary authentication 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 Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A secondary authentication provider performs additional authentication steps
+ * after a PrimaryAuthenticationProvider has done its thing.
+ *
+ * A SecondaryAuthenticationProvider is used to perform arbitrary checks on an
+ * authentication request after the user itself has been authenticated. For
+ * example, it might implement a password reset, request the second factor for
+ * two-factor auth, or prevent the login if the account is blocked.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+interface SecondaryAuthenticationProvider extends AuthenticationProvider {
+
+       /**
+        * Start an authentication flow
+        *
+        * Note that this may be called for a user even if
+        * beginSecondaryAccountCreation() was never called. The module should take
+        * the opportunity to do any necessary setup in that case.
+        *
+        * @param User $user User being authenticated. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Additional secondary providers may run.
+        *  - FAIL: The user is not authenticated. Fail the authentication process.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function beginSecondaryAuthentication( $user, array $reqs );
+
+       /**
+        * Continue an authentication flow
+        * @param User $user User being authenticated. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Additional secondary providers may run.
+        *  - FAIL: The user is not authenticated. Fail the authentication process.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function continueSecondaryAuthentication( $user, array $reqs );
+
+       /**
+        * Post-login callback
+        * @param User|null $user User that was attempted to be logged in, if known.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAuthentication( $user, AuthenticationResponse $response );
+
+       /**
+        * Revoke the user's credentials
+        *
+        * This may cause the user to no longer exist for the provider, or the user
+        * may continue to exist in a "disabled" state.
+        *
+        * The intention is that the named account will never again be usable for
+        * normal login (i.e. there is no way to undo the revocation of access).
+        *
+        * @param string $username
+        */
+       public function providerRevokeAccessForUser( $username );
+
+       /**
+        * Determine whether a property can change
+        * @see AuthManager::allowsPropertyChange()
+        * @param string $property
+        * @return bool
+        */
+       public function providerAllowsPropertyChange( $property );
+
+       /**
+        * Validate a change of authentication data (e.g. passwords)
+        *
+        * Return StatusValue::newGood( 'ignored' ) if you don't support this
+        * AuthenticationRequest type.
+        *
+        * @param AuthenticationRequest $req
+        * @param bool $checkData If false, $req hasn't been loaded from the
+        *  submission so checks on user-submitted fields should be skipped.
+        *  $req->username is considered user-submitted for this purpose, even
+        *  if it cannot be changed via $req->loadFromSubmission.
+        * @return StatusValue
+        */
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       );
+
+       /**
+        * Change or remove authentication data (e.g. passwords)
+        *
+        * If $req was returned for AuthManager::ACTION_CHANGE, the corresponding
+        * credentials should result in a successful login in the future.
+        *
+        * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
+        * credentials should no longer result in a successful login.
+        *
+        * @param AuthenticationRequest $req
+        */
+       public function providerChangeAuthenticationData( AuthenticationRequest $req );
+
+       /**
+        * Determine whether an account creation may begin
+        *
+        * Called from AuthManager::beginAccountCreation()
+        *
+        * @note No need to test if the account exists, AuthManager checks that
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return StatusValue
+        */
+       public function testForAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Start an account creation flow
+        * @param User $user User being created (has been added to the database).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user creation is ok. Additional secondary providers may run.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function beginSecondaryAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Continue an authentication flow
+        * @param User $user User being created (has been added to the database).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user creation is ok. Additional secondary providers may run.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the process.
+        */
+       public function continueSecondaryAccountCreation( $user, $creator, array $reqs );
+
+       /**
+        * Post-creation callback
+        * @param User $user User that was attempted to be created.
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param User $creator User doing the creation. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationResponse $response Authentication response that will be returned
+        */
+       public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+       /**
+        * Determine whether an account may be created
+        *
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param bool|string $autocreate False if this is not an auto-creation, or
+        *  the source of the auto-creation passed to AuthManager::autoCreateUser().
+        * @return StatusValue
+        */
+       public function testUserForCreation( $user, $autocreate );
+
+       /**
+        * Post-auto-creation callback
+        * @param User $user User being created (has been added to the database now).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @param string $source The source of the auto-creation passed to
+        *  AuthManager::autoCreateUser().
+        */
+       public function autoCreatedAccount( $user, $source );
+
+}
diff --git a/includes/auth/TemporaryPasswordAuthenticationRequest.php b/includes/auth/TemporaryPasswordAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..42f0e70
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This represents the intention to set a temporary password for the user.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class TemporaryPasswordAuthenticationRequest extends AuthenticationRequest {
+       /** @var string|null Temporary password */
+       public $password;
+
+       /** @var bool Email password to the user. */
+       public $mailpassword = false;
+
+       /**
+        * @var bool Do not fail certain operations if the password cannot be mailed, there is a
+        *   backchannel present.
+        */
+       public $hasBackchannel = false;
+
+       /** @var string Username or IP address of the caller */
+       public $caller;
+
+       public function getFieldInfo() {
+               return [
+                       'mailpassword' => [
+                               'type' => 'checkbox',
+                               'label' => wfMessage( 'createaccountmail' ),
+                               'help' => wfMessage( 'createaccountmail-help' ),
+                       ],
+               ];
+       }
+
+       /**
+        * @param string|null $password
+        */
+       public function __construct( $password = null ) {
+               $this->password = $password;
+               if ( $password ) {
+                       $this->mailpassword = true;
+               }
+       }
+
+       /**
+        * Return an instance with a new, random password
+        * @return TemporaryPasswordAuthenticationRequest
+        */
+       public static function newRandom() {
+               $config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+
+               // get the min password length
+               $minLength = $config->get( 'MinimalPasswordLength' );
+               $policy = $config->get( 'PasswordPolicy' );
+               foreach ( $policy['policies'] as $p ) {
+                       if ( isset( $p['MinimalPasswordLength'] ) ) {
+                               $minLength = max( $minLength, $p['MinimalPasswordLength'] );
+                       }
+                       if ( isset( $p['MinimalPasswordLengthToLogin'] ) ) {
+                               $minLength = max( $minLength, $p['MinimalPasswordLengthToLogin'] );
+                       }
+               }
+
+               $password = \PasswordFactory::generateRandomPasswordString( $minLength );
+
+               return new self( $password );
+       }
+
+       /**
+        * Return an instance with an invalid password
+        * @return TemporaryPasswordAuthenticationRequest
+        */
+       public static function newInvalid() {
+               $request = new self( null );
+               return $request;
+       }
+
+       public function describeCredentials() {
+               return [
+                       'provider' => wfMessage( 'authmanager-provider-temporarypassword' ),
+                       'account' => new \RawMessage( '$1', [ $this->username ] ),
+               ] + parent::describeCredentials();
+       }
+
+}
diff --git a/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php b/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..46cbab5
--- /dev/null
@@ -0,0 +1,454 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A primary authentication provider that uses the temporary password field in
+ * the 'user' table.
+ *
+ * A successful login will force a password reset.
+ *
+ * @note For proper operation, this should generally come before any other
+ *  password-based authentication providers.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class TemporaryPasswordPrimaryAuthenticationProvider
+       extends AbstractPasswordPrimaryAuthenticationProvider
+{
+       /** @var bool */
+       protected $emailEnabled = null;
+
+       /** @var int */
+       protected $newPasswordExpiry = null;
+
+       /** @var int */
+       protected $passwordReminderResendTime = null;
+
+       /**
+        * @param array $params
+        *  - emailEnabled: (bool) must be true for the option to email passwords to be present
+        *  - newPasswordExpiry: (int) expiraton time of temporary passwords, in seconds
+        *  - passwordReminderResendTime: (int) cooldown period in hours until a password reminder can
+        *    be sent to the same user again,
+        */
+       public function __construct( $params = [] ) {
+               parent::__construct( $params );
+
+               if ( isset( $params['emailEnabled'] ) ) {
+                       $this->emailEnabled = (bool)$params['emailEnabled'];
+               }
+               if ( isset( $params['newPasswordExpiry'] ) ) {
+                       $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
+               }
+               if ( isset( $params['passwordReminderResendTime'] ) ) {
+                       $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
+               }
+       }
+
+       public function setConfig( \Config $config ) {
+               parent::setConfig( $config );
+
+               if ( $this->emailEnabled === null ) {
+                       $this->emailEnabled = $this->config->get( 'EnableEmail' );
+               }
+               if ( $this->newPasswordExpiry === null ) {
+                       $this->newPasswordExpiry = $this->config->get( 'NewPasswordExpiry' );
+               }
+               if ( $this->passwordReminderResendTime === null ) {
+                       $this->passwordReminderResendTime = $this->config->get( 'PasswordReminderResendTime' );
+               }
+       }
+
+       protected function getPasswordResetData( $username, $data ) {
+               // Always reset
+               return (object)[
+                       'msg' => wfMessage( 'resetpass-temp-emailed' ),
+                       'hard' => true,
+               ];
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                               return [ new PasswordAuthenticationRequest() ];
+
+                       case AuthManager::ACTION_CHANGE:
+                               return [ TemporaryPasswordAuthenticationRequest::newRandom() ];
+
+                       case AuthManager::ACTION_CREATE:
+                               if ( isset( $options['username'] ) && $this->emailEnabled ) {
+                                       // Creating an account for someone else
+                                       return [ TemporaryPasswordAuthenticationRequest::newRandom() ];
+                               } else {
+                                       // It's not terribly likely that an anonymous user will
+                                       // be creating an account for someone else.
+                                       return [];
+                               }
+
+                       case AuthManager::ACTION_REMOVE:
+                               return [ new TemporaryPasswordAuthenticationRequest ];
+
+                       default:
+                               return [];
+               }
+       }
+
+       public function beginPrimaryAuthentication( array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+               if ( !$req || $req->username === null || $req->password === null ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       [
+                               'user_id', 'user_newpassword', 'user_newpass_time',
+                       ],
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+               if ( !$row ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $status = $this->checkPasswordValidity( $username, $req->password );
+               if ( !$status->isOk() ) {
+                       // Fatal, can't log in
+                       return AuthenticationResponse::newFail( $status->getMessage() );
+               }
+
+               $pwhash = $this->getPassword( $row->user_newpassword );
+               if ( !$pwhash->equals( $req->password ) ) {
+                       return $this->failResponse( $req );
+               }
+
+               if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
+                       return $this->failResponse( $req );
+               }
+
+               $this->setPasswordResetFlag( $username, $status );
+
+               return AuthenticationResponse::newPass( $username );
+       }
+
+       public function testUserCanAuthenticate( $username ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       [ 'user_newpassword', 'user_newpass_time' ],
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+               if ( !$row ) {
+                       return false;
+               }
+
+               if ( $this->getPassword( $row->user_newpassword ) instanceof \InvalidPassword ) {
+                       return false;
+               }
+
+               if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
+               return (bool)wfGetDB( $db )->selectField(
+                       [ 'user' ],
+                       [ 'user_id' ],
+                       [ 'user_name' => $username ],
+                       __METHOD__,
+                       $options
+               );
+       }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
+                       // We don't really ignore it, but this is what the caller expects.
+                       return \StatusValue::newGood( 'ignored' );
+               }
+
+               if ( !$checkData ) {
+                       return \StatusValue::newGood();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return \StatusValue::newGood( 'ignored' );
+               }
+
+               $row = wfGetDB( DB_MASTER )->selectRow(
+                       'user',
+                       [ 'user_id', 'user_newpass_time' ],
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+
+               if ( !$row ) {
+                       return \StatusValue::newGood( 'ignored' );
+               }
+
+               $sv = \StatusValue::newGood();
+               if ( $req->password !== null ) {
+                       $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+
+                       if ( $req->mailpassword ) {
+                               if ( !$this->emailEnabled && !$req->hasBackchannel ) {
+                                       return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
+                               }
+
+                               // We don't check whether the user has an email address;
+                               // that information should not be exposed to the caller.
+
+                               // do not allow temporary password creation within
+                               // $wgPasswordReminderResendTime from the last attempt
+                               if (
+                                       $this->passwordReminderResendTime
+                                       && $row->user_newpass_time
+                                       && time() < wfTimestamp( TS_UNIX, $row->user_newpass_time )
+                                               + $this->passwordReminderResendTime * 3600
+                               ) {
+                                       // Round the time in hours to 3 d.p., in case someone is specifying
+                                       // minutes or seconds.
+                                       return \StatusValue::newFatal( 'throttled-mailpassword',
+                                               round( $this->passwordReminderResendTime, 3 ) );
+                               }
+
+                               if ( !$req->caller ) {
+                                       return \StatusValue::newFatal( 'passwordreset-nocaller' );
+                               }
+                               if ( !\IP::isValid( $req->caller ) ) {
+                                       $caller = User::newFromName( $req->caller );
+                                       if ( !$caller ) {
+                                               return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
+                                       }
+                               }
+                       }
+               }
+               return $sv;
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+               $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+               if ( $username === false ) {
+                       return;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               $sendMail = false;
+               if ( $req->action !== AuthManager::ACTION_REMOVE &&
+                       get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
+               ) {
+                       $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+                       $newpassTime = $dbw->timestamp();
+                       $sendMail = $req->mailpassword;
+               } else {
+                       // Invalidate the temporary password when any other auth is reset, or when removing
+                       $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
+                       $newpassTime = null;
+               }
+
+               $dbw->update(
+                       'user',
+                       [
+                               'user_newpassword' => $pwhash->toString(),
+                               'user_newpass_time' => $newpassTime,
+                       ],
+                       [ 'user_name' => $username ],
+                       __METHOD__
+               );
+
+               if ( $sendMail ) {
+                       $this->sendPasswordResetEmail( $req );
+               }
+       }
+
+       public function accountCreationType() {
+               return self::TYPE_CREATE;
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               /** @var TemporaryPasswordAuthenticationRequest $req */
+               $req = AuthenticationRequest::getRequestByClass(
+                       $reqs, TemporaryPasswordAuthenticationRequest::class
+               );
+
+               $ret = \StatusValue::newGood();
+               if ( $req ) {
+                       if ( $req->mailpassword && !$req->hasBackchannel ) {
+                               if ( !$this->emailEnabled ) {
+                                       $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
+                               } elseif ( !$user->getEmail() ) {
+                                       $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
+                               }
+                       }
+
+                       $ret->merge(
+                               $this->checkPasswordValidity( $user->getName(), $req->password )
+                       );
+               }
+               return $ret;
+       }
+
+       public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+               /** @var TemporaryPasswordAuthenticationRequest $req */
+               $req = AuthenticationRequest::getRequestByClass(
+                       $reqs, TemporaryPasswordAuthenticationRequest::class
+               );
+               if ( $req ) {
+                       if ( $req->username !== null && $req->password !== null ) {
+                               // Nothing we can do yet, because the user isn't in the DB yet
+                               if ( $req->username !== $user->getName() ) {
+                                       $req = clone( $req );
+                                       $req->username = $user->getName();
+                               }
+
+                               if ( $req->mailpassword ) {
+                                       // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
+                                       $this->manager->setAuthenticationSessionData( 'no-email', true );
+                               }
+
+                               $ret = AuthenticationResponse::newPass( $req->username );
+                               $ret->createRequest = $req;
+                               return $ret;
+                       }
+               }
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
+               /** @var TemporaryPasswordAuthenticationRequest $req */
+               $req = $res->createRequest;
+               $mailpassword = $req->mailpassword;
+               $req->mailpassword = false; // providerChangeAuthenticationData would send the wrong email
+
+               // Now that the user is in the DB, set the password on it.
+               $this->providerChangeAuthenticationData( $req );
+
+               if ( $mailpassword ) {
+                       $this->sendNewAccountEmail( $user, $creator, $req->password );
+               }
+
+               return $mailpassword ? 'byemail' : null;
+       }
+
+       /**
+        * Check that a temporary password is still valid (hasn't expired).
+        * @param string $timestamp A timestamp in MediaWiki (TS_MW) format
+        * @return bool
+        */
+       protected function isTimestampValid( $timestamp ) {
+               $time = wfTimestampOrNull( TS_MW, $timestamp );
+               if ( $time !== null ) {
+                       $expiry = wfTimestamp( TS_UNIX, $time ) + $this->newPasswordExpiry;
+                       if ( time() >= $expiry ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Send an email about the new account creation and the temporary password.
+        * @param User $user The new user account
+        * @param User $creatingUser The user who created the account (can be anonymous)
+        * @param string $password The temporary password
+        * @return \Status
+        */
+       protected function sendNewAccountEmail( User $user, User $creatingUser, $password ) {
+               $ip = $creatingUser->getRequest()->getIP();
+               // @codeCoverageIgnoreStart
+               if ( !$ip ) {
+                       return \Status::newFatal( 'badipaddress' );
+               }
+               // @codeCoverageIgnoreEnd
+
+               \Hooks::run( 'User::mailPasswordInternal', [ &$creatingUser, &$ip, &$user ] );
+
+               $mainPageUrl = \Title::newMainPage()->getCanonicalURL();
+               $userLanguage = $user->getOption( 'language' );
+               $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
+               $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
+                       '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
+                       ->inLanguage( $userLanguage );
+
+               $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
+
+               // TODO show 'mailerror' message on error, 'accmailtext' success message otherwise?
+               // @codeCoverageIgnoreStart
+               if ( !$status->isGood() ) {
+                       $this->logger->warning( 'Could not send account creation email: ' .
+                               $status->getWikiText( false, false, 'en' ) );
+               }
+               // @codeCoverageIgnoreEnd
+
+               return $status;
+       }
+
+       /**
+        * @param TemporaryPasswordAuthenticationRequest $req
+        * @return \Status
+        */
+       protected function sendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ) {
+                       $user = User::newFromName( $req->username );
+                       if ( !$user ) {
+                               return \Status::newFatal( 'noname' );
+                       }
+                       $userLanguage = $user->getOption( 'language' );
+                       $callerIsAnon = \IP::isValid( $req->caller );
+                       $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
+                       $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
+                               $req->password )->inLanguage( $userLanguage );
+                       $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
+                               : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
+                       $emailMessage->params( $callerName, $passwordMessage->text(), 1,
+                               '<' . \Title::newMainPage()->getCanonicalURL() . '>',
+                               round( $this->newPasswordExpiry / 86400 ) );
+                       $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
+                       return $user->sendMail( $emailTitle->text(), $emailMessage->text() );
+       }
+}
diff --git a/includes/auth/ThrottlePreAuthenticationProvider.php b/includes/auth/ThrottlePreAuthenticationProvider.php
new file mode 100644 (file)
index 0000000..e2123ef
--- /dev/null
@@ -0,0 +1,170 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use Config;
+
+/**
+ * A pre-authentication provider to throttle authentication actions.
+ *
+ * Adding this provider will throttle account creations and primary authentication attempts
+ * (more specifically, any authentication that returns FAIL on failure). Secondary authentication
+ * cannot be easily throttled on a framework level (since it would typically return UI on failure);
+ * secondary providers are expected to do their own throttling.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvider {
+       /** @var array */
+       protected $throttleSettings;
+
+       /** @var Throttler */
+       protected $accountCreationThrottle;
+
+       /** @var Throttler */
+       protected $passwordAttemptThrottle;
+
+       /** @var BagOStuff */
+       protected $cache;
+
+       /**
+        * @param array $params
+        *  - accountCreationThrottle: (array) Condition array for the account creation throttle; an array
+        *    of arrays in a format like $wgPasswordAttemptThrottle, passed to the Throttler constructor.
+        *  - passwordAttemptThrottle: (array) Condition array for the password attempt throttle, in the
+        *    same format as accountCreationThrottle.
+        *  - cache: (BagOStuff) Where to store the throttle, defaults to the local cluster instance.
+        */
+       public function __construct( $params = [] ) {
+               $this->throttleSettings = array_intersect_key( $params,
+                       [ 'accountCreationThrottle' => true, 'passwordAttemptThrottle' => true ] );
+               $this->cache = isset( $params['cache'] ) ? $params['cache'] :
+                       \ObjectCache::getLocalClusterInstance();
+       }
+
+       public function setConfig( Config $config ) {
+               parent::setConfig( $config );
+
+               // @codeCoverageIgnoreStart
+               $this->throttleSettings += [
+               // @codeCoverageIgnoreEnd
+                       'accountCreationThrottle' => [ [
+                               'count' => $this->config->get( 'AccountCreationThrottle' ),
+                               'seconds' => 86400,
+                       ] ],
+                       'passwordAttemptThrottle' => $this->config->get( 'PasswordAttemptThrottle' ),
+               ];
+
+               if ( !empty( $this->throttleSettings['accountCreationThrottle'] ) ) {
+                       $this->accountCreationThrottle = new Throttler(
+                               $this->throttleSettings['accountCreationThrottle'], [
+                                       'type' => 'acctcreate',
+                                       'cache' => $this->cache,
+                               ]
+                       );
+               }
+               if ( !empty( $this->throttleSettings['passwordAttemptThrottle'] ) ) {
+                       $this->passwordAttemptThrottle = new Throttler(
+                               $this->throttleSettings['passwordAttemptThrottle'], [
+                                       'type' => 'password',
+                                       'cache' => $this->cache,
+                               ]
+                       );
+               }
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               if ( !$this->accountCreationThrottle || !$creator->isPingLimitable() ) {
+                       return \StatusValue::newGood();
+               }
+
+               $ip = $this->manager->getRequest()->getIP();
+
+               if ( !\Hooks::run( 'ExemptFromAccountCreationThrottle', [ $ip ] ) ) {
+                       $this->logger->debug( __METHOD__ . ": a hook allowed account creation w/o throttle\n" );
+                       return \StatusValue::newGood();
+               }
+
+               $result = $this->accountCreationThrottle->increase( null, $ip, __METHOD__ );
+               if ( $result ) {
+                       return \StatusValue::newFatal( 'acct_creation_throttle_hit', $result['count'] );
+               }
+
+               return \StatusValue::newGood();
+       }
+
+       public function testForAuthentication( array $reqs ) {
+               if ( !$this->passwordAttemptThrottle ) {
+                       return \StatusValue::newGood();
+               }
+
+               $ip = $this->manager->getRequest()->getIP();
+               try {
+                       $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
+               } catch ( \UnexpectedValueException $e ) {
+                       $username = '';
+               }
+
+               // Get everything this username could normalize to, and throttle each one individually.
+               // If nothing uses usernames, just throttle by IP.
+               $usernames = $this->manager->normalizeUsername( $username );
+               $result = false;
+               foreach ( $usernames as $name ) {
+                       $r = $this->passwordAttemptThrottle->increase( $name, $ip, __METHOD__ );
+                       if ( $r && ( !$result || $result['wait'] < $r['wait'] ) ) {
+                               $result = $r;
+                       }
+               }
+
+               if ( $result ) {
+                       $message = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
+                       return \StatusValue::newFatal( $message );
+               } else {
+                       $this->manager->setAuthenticationSessionData( 'LoginThrottle',
+                               [ 'users' => $usernames, 'ip' => $ip ] );
+                       return \StatusValue::newGood();
+               }
+       }
+
+       /**
+        * @param null|\User $user
+        * @param AuthenticationResponse $response
+        */
+       public function postAuthentication( $user, AuthenticationResponse $response ) {
+               if ( $response->status !== AuthenticationResponse::PASS ) {
+                       return;
+               } elseif ( !$this->passwordAttemptThrottle ) {
+                       return;
+               }
+
+               $data = $this->manager->getAuthenticationSessionData( 'LoginThrottle' );
+               if ( !$data ) {
+                       $this->logger->error( 'throttler data not found for {user}', [ 'user' => $user->getName() ] );
+                       return;
+               }
+
+               foreach ( $data['users'] as $name ) {
+                       $this->passwordAttemptThrottle->clear( $name, $data['ip'] );
+               }
+       }
+}
diff --git a/includes/auth/Throttler.php b/includes/auth/Throttler.php
new file mode 100644 (file)
index 0000000..5b14a3b
--- /dev/null
@@ -0,0 +1,210 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use Config;
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use Psr\Log\NullLogger;
+use User;
+
+/**
+ * A helper class for throttling authentication attempts.
+ * @package MediaWiki\Auth
+ * @ingroup Auth
+ * @since 1.27
+ */
+class Throttler implements LoggerAwareInterface {
+       /** @var string */
+       protected $type;
+       /**
+        * See documentation of $wgPasswordAttemptThrottle for format. Old (pre-1.27) format is not
+        * allowed here.
+        * @var array
+        * @see https://www.mediawiki.org/wiki/Manual:$wgPasswordAttemptThrottle
+        */
+       protected $conditions;
+       /** @var BagOStuff */
+       protected $cache;
+       /** @var LoggerInterface */
+       protected $logger;
+       /** @var int|float */
+       protected $warningLimit;
+
+       /**
+        * @param array $conditions An array of arrays describing throttling conditions.
+        *     Defaults to $wgPasswordAttemptThrottle. See documentation of that variable for format.
+        * @param array $params Parameters (all optional):
+        *   - type: throttle type, used as a namespace for counters,
+        *   - cache: a BagOStuff object where throttle counters are stored.
+        *   - warningLimit: the log level will be raised to warning when rejecting an attempt after
+        *     no less than this many failures.
+        */
+       public function __construct( array $conditions = null, array $params = [] ) {
+               $invalidParams = array_diff_key( $params,
+                       array_fill_keys( [ 'type', 'cache', 'warningLimit' ], true ) );
+               if ( $invalidParams ) {
+                       throw new \InvalidArgumentException( 'unrecognized parameters: '
+                               . implode( ', ', array_keys( $invalidParams ) ) );
+               }
+
+               if ( $conditions === null ) {
+                       $config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+                       $conditions = $config->get( 'PasswordAttemptThrottle' );
+                       $params += [
+                               'type' => 'password',
+                               'cache' => \ObjectCache::getLocalClusterInstance(),
+                               'warningLimit' => 50,
+                       ];
+               } else {
+                       $params += [
+                               'type' => 'custom',
+                               'cache' => \ObjectCache::getLocalClusterInstance(),
+                               'warningLimit' => INF,
+                       ];
+               }
+
+               $this->type = $params['type'];
+               $this->conditions = static::normalizeThrottleConditions( $conditions );
+               $this->cache = $params['cache'];
+               $this->warningLimit = $params['warningLimit'];
+
+               $this->setLogger( LoggerFactory::getInstance( 'throttler' ) );
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Increase the throttle counter and return whether the attempt should be throttled.
+        *
+        * Should be called before an authentication attempt.
+        *
+        * @param string|null $username
+        * @param string|null $ip
+        * @param string|null $caller The authentication method from which we were called.
+        * @return array|false False if the attempt should not be throttled, an associative array
+        *   with three keys otherwise:
+        *   - throttleIndex: which throttle condition was met (a key of the conditions array)
+        *   - count: throttle count (ie. number of failed attempts)
+        *   - wait: time in seconds until authentication can be attempted
+        */
+       public function increase( $username = null, $ip = null, $caller = null ) {
+               if ( $username === null && $ip === null ) {
+                       throw new \InvalidArgumentException( 'Either username or IP must be set for throttling' );
+               }
+
+               $userKey = $username ? md5( $username ) : null;
+               foreach ( $this->conditions as $index => $throttleCondition ) {
+                       $ipKey = isset( $throttleCondition['allIPs'] ) ? null : $ip;
+                       $count = $throttleCondition['count'];
+                       $expiry = $throttleCondition['seconds'];
+
+                       // a limit of 0 is used as a disable flag in some throttling configuration settings
+                       // throttling the whole world is probably a bad idea
+                       if ( !$count || $userKey === null && $ipKey === null ) {
+                               continue;
+                       }
+
+                       $throttleKey = wfGlobalCacheKey( 'throttler', $this->type, $index, $ipKey, $userKey );
+                       $throttleCount = $this->cache->get( $throttleKey );
+
+                       if ( !$throttleCount ) {  // counter not started yet
+                               $this->cache->add( $throttleKey, 1, $expiry );
+                       } elseif ( $throttleCount < $count ) { // throttle limited not yet reached
+                               $this->cache->incr( $throttleKey );
+                       } else { // throttled
+                               $this->logRejection( [
+                                       'type' => $this->type,
+                                       'index' => $index,
+                                       'ip' => $ipKey,
+                                       'username' => $username,
+                                       'count' => $count,
+                                       'expiry' => $expiry,
+                                       // @codeCoverageIgnoreStart
+                                       'method' => $caller ?: __METHOD__,
+                                       // @codeCoverageIgnoreEnd
+                               ] );
+
+                               return [
+                                       'throttleIndex' => $index,
+                                       'count' => $count,
+                                       'wait' => $expiry,
+                               ];
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Clear the throttle counter.
+        *
+        * Should be called after a successful authentication attempt.
+        *
+        * @param string|null $username
+        * @param string|null $ip
+        * @throws \MWException
+        */
+       public function clear( $username = null, $ip = null ) {
+               $userKey = $username ? md5( $username ) : null;
+               foreach ( $this->conditions as $index => $specificThrottle ) {
+                       $ipKey = isset( $specificThrottle['allIPs'] ) ? null : $ip;
+                       $throttleKey = wfGlobalCacheKey( 'throttler', $this->type, $index, $ipKey, $userKey );
+                       $this->cache->delete( $throttleKey );
+               }
+       }
+
+       /**
+        * Handles B/C for $wgPasswordAttemptThrottle.
+        * @param array $throttleConditions
+        * @return array
+        * @see $wgPasswordAttemptThrottle for structure
+        */
+       protected static function normalizeThrottleConditions( $throttleConditions ) {
+               if ( !is_array( $throttleConditions ) ) {
+                       return [];
+               }
+               if ( isset( $throttleConditions['count'] ) ) { // old style
+                       $throttleConditions = [ $throttleConditions ];
+               }
+               return $throttleConditions;
+       }
+
+       protected function logRejection( array $context ) {
+               $logMsg = 'Throttle {type} hit, throttled for {expiry} seconds due to {count} attempts '
+                       . 'from username {username} and IP {ip}';
+
+               // If we are hitting a throttle for >= warningLimit attempts, it is much more likely to be
+               // an attack than someone simply forgetting their password, so log it at a higher level.
+               $level = $context['count'] >= $this->warningLimit ? LogLevel::WARNING : LogLevel::INFO;
+
+               // It should be noted that once the throttle is hit, every attempt to login will
+               // generate the log message until the throttle expires, not just the attempt that
+               // puts the throttle over the top.
+               $this->logger->log( $level, $logMsg, $context );
+       }
+
+}
diff --git a/includes/auth/UserDataAuthenticationRequest.php b/includes/auth/UserDataAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..ee77d7b
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * This represents additional user data requested on the account creation form
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class UserDataAuthenticationRequest extends AuthenticationRequest {
+       /** @var string|null Email address */
+       public $email;
+
+       /** @var string|null Real name */
+       public $realname;
+
+       public function getFieldInfo() {
+               $config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+               $ret = [
+                       'email' => [
+                               'type' => 'string',
+                               'label' => wfMessage( 'authmanager-email-label' ),
+                               'help' => wfMessage( 'authmanager-email-help' ),
+                               'optional' => true,
+                       ],
+                       'realname' => [
+                               'type' => 'string',
+                               'label' => wfMessage( 'authmanager-realname-label' ),
+                               'help' => wfMessage( 'authmanager-realname-help' ),
+                               'optional' => true,
+                       ],
+               ];
+
+               if ( !$config->get( 'EnableEmail' ) ) {
+                       unset( $ret['email'] );
+               }
+
+               if ( in_array( 'realname', $config->get( 'HiddenPrefs' ), true ) ) {
+                       unset( $ret['realname'] );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Add data to the User object
+        * @param User $user User being created (not added to the database yet).
+        *   This may become a "UserValue" in the future, or User may be refactored
+        *   into such.
+        * @return StatusValue
+        */
+       public function populateUser( $user ) {
+               if ( $this->email !== null && $this->email !== '' ) {
+                       if ( !\Sanitizer::validateEmail( $this->email ) ) {
+                               return StatusValue::newFatal( 'invalidemailaddress' );
+                       }
+                       $user->setEmail( $this->email );
+               }
+               if ( $this->realname !== null && $this->realname !== '' ) {
+                       $user->setRealName( $this->realname );
+               }
+               return StatusValue::newGood();
+       }
+
+}
diff --git a/includes/auth/UsernameAuthenticationRequest.php b/includes/auth/UsernameAuthenticationRequest.php
new file mode 100644 (file)
index 0000000..7bf8f13
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * AuthenticationRequest to ensure something with a username is present
+ * @ingroup Auth
+ * @since 1.27
+ */
+class UsernameAuthenticationRequest extends AuthenticationRequest {
+       public function getFieldInfo() {
+               return [
+                       'username' => [
+                               'type' => 'string',
+                               'label' => wfMessage( 'userlogin-yourname' ),
+                               'help' => wfMessage( 'authmanager-username-help' ),
+                       ],
+               ];
+       }
+}
index c5bd290..a7dd570 100644 (file)
@@ -179,8 +179,6 @@ class LinkBatch {
         * @return bool|ResultWrapper
         */
        public function doQuery() {
-               global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
-
                if ( $this->isEmpty() ) {
                        return false;
                }
@@ -188,15 +186,10 @@ class LinkBatch {
                // This is similar to LinkHolderArray::replaceInternal
                $dbr = wfGetDB( DB_SLAVE );
                $table = 'page';
-               $fields = [ 'page_id', 'page_namespace', 'page_title', 'page_len',
-                       'page_is_redirect', 'page_latest' ];
-
-               if ( $wgContentHandlerUseDB ) {
-                       $fields[] = 'page_content_model';
-               }
-               if ( $wgPageLanguageUseDB ) {
-                       $fields[] = 'page_lang';
-               }
+               $fields = array_merge(
+                       LinkCache::getSelectFields(),
+                       [ 'page_namespace', 'page_title' ]
+               );
 
                $conds = $this->constructSet( 'page', $dbr );
 
index b8f2329..de44f9b 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  * @ingroup Cache
  */
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Cache for article titles (prefixed DB keys) and ids linked from one source
@@ -38,56 +40,30 @@ class LinkCache {
        private $mForUpdate = false;
 
        /**
-        * How many Titles to store. There are two caches, so the amount actually
-        * stored in memory can be up to twice this.
+        * @var TitleFormatter
         */
-       const MAX_SIZE = 10000;
+       private $titleFormatter;
 
        /**
-        * @var LinkCache
+        * How many Titles to store. There are two caches, so the amount actually
+        * stored in memory can be up to twice this.
         */
-       protected static $instance;
+       const MAX_SIZE = 10000;
 
-       public function __construct() {
+       public function __construct( TitleFormatter $titleFormatter ) {
                $this->mGoodLinks = new HashBagOStuff( [ 'maxKeys' => self::MAX_SIZE ] );
                $this->mBadLinks = new HashBagOStuff( [ 'maxKeys' => self::MAX_SIZE ] );
+               $this->titleFormatter = $titleFormatter;
        }
 
        /**
         * Get an instance of this class.
         *
         * @return LinkCache
+        * @deprecated since 1.28, use MediaWikiServices instead
         */
        public static function singleton() {
-               if ( !self::$instance ) {
-                       self::$instance = new LinkCache;
-               }
-
-               return self::$instance;
-       }
-
-       /**
-        * Destroy the singleton instance
-        *
-        * A new one will be created next time singleton() is called.
-        *
-        * @since 1.22
-        */
-       public static function destroySingleton() {
-               self::$instance = null;
-       }
-
-       /**
-        * Set the singleton instance to a given object.
-        *
-        * Since we do not have an interface for LinkCache, you have to be sure the
-        * given object implements all the LinkCache public methods.
-        *
-        * @param LinkCache $instance
-        * @since 1.22
-        */
-       public static function setSingleton( LinkCache $instance ) {
-               self::$instance = $instance;
+               return MediaWikiServices::getInstance()->getLinkCache();
        }
 
        /**
@@ -119,12 +95,12 @@ class LinkCache {
        /**
         * Get a field of a title object from cache.
         * If this link is not a cached good title, it will return NULL.
-        * @param Title $title
+        * @param LinkTarget $target
         * @param string $field ('length','redirect','revision','model')
         * @return string|int|null
         */
-       public function getGoodLinkFieldObj( Title $title, $field ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function getGoodLinkFieldObj( LinkTarget $target, $field ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $info = $this->mGoodLinks->get( $dbkey );
                if ( !$info ) {
                        return null;
@@ -145,17 +121,17 @@ class LinkCache {
         * Add a link for the title to the link cache
         *
         * @param int $id Page's ID
-        * @param Title $title
+        * @param LinkTarget $target
         * @param int $len Text's length
         * @param int $redir Whether the page is a redirect
         * @param int $revision Latest revision's ID
         * @param string|null $model Latest revision's content model ID
         * @param string|null $lang Language code of the page, if not the content language
         */
-       public function addGoodLinkObj( $id, Title $title, $len = -1, $redir = null,
+       public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null,
                $revision = 0, $model = null, $lang = null
        ) {
-               $dbkey = $title->getPrefixedDBkey();
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mGoodLinks->set( $dbkey, [
                        'id' => (int)$id,
                        'length' => (int)$len,
@@ -169,12 +145,12 @@ class LinkCache {
        /**
         * Same as above with better interface.
         * @since 1.19
-        * @param Title $title
+        * @param LinkTarget $target
         * @param stdClass $row Object which has the fields page_id, page_is_redirect,
         *  page_latest and page_content_model
         */
-       public function addGoodLinkObjFromRow( Title $title, $row ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function addGoodLinkObjFromRow( LinkTarget $target, $row ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mGoodLinks->set( $dbkey, [
                        'id' => intval( $row->page_id ),
                        'length' => intval( $row->page_len ),
@@ -186,10 +162,10 @@ class LinkCache {
        }
 
        /**
-        * @param Title $title
+        * @param LinkTarget $target
         */
-       public function addBadLinkObj( Title $title ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function addBadLinkObj( LinkTarget $target ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                if ( !$this->isBadLink( $dbkey ) ) {
                        $this->mBadLinks->set( $dbkey, 1 );
                }
@@ -203,10 +179,10 @@ class LinkCache {
        }
 
        /**
-        * @param Title $title
+        * @param LinkTarget $target
         */
-       public function clearLink( Title $title ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function clearLink( LinkTarget $target ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mBadLinks->delete( $dbkey );
                $this->mGoodLinks->delete( $dbkey );
        }
@@ -214,6 +190,7 @@ class LinkCache {
        /**
         * Add a title to the link cache, return the page_id or zero if non-existent
         *
+        * @deprecated since 1.27, unused
         * @param string $title Prefixed DB key
         * @return int Page ID or zero
         */
@@ -226,15 +203,33 @@ class LinkCache {
        }
 
        /**
-        * Add a title to the link cache, return the page_id or zero if non-existent
+        * Fields that LinkCache needs to select
         *
-        * @param Title $nt Title object to add
-        * @return int Page ID or zero
+        * @since 1.28
+        * @return array
         */
-       public function addLinkObj( Title $nt ) {
+       public static function getSelectFields() {
                global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
 
-               $key = $nt->getPrefixedDBkey();
+               $fields = [ 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ];
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+               if ( $wgPageLanguageUseDB ) {
+                       $fields[] = 'page_lang';
+               }
+
+               return $fields;
+       }
+
+       /**
+        * Add a title to the link cache, return the page_id or zero if non-existent
+        *
+        * @param LinkTarget $nt LinkTarget object to add
+        * @return int Page ID or zero
+        */
+       public function addLinkObj( LinkTarget $nt ) {
+               $key = $this->titleFormatter->getPrefixedDBkey( $nt );
                if ( $this->isBadLink( $key ) || $nt->isExternal() ) {
                        return 0;
                }
@@ -250,15 +245,7 @@ class LinkCache {
                // Some fields heavily used for linking...
                $db = $this->mForUpdate ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
 
-               $fields = [ 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ];
-               if ( $wgContentHandlerUseDB ) {
-                       $fields[] = 'page_content_model';
-               }
-               if ( $wgPageLanguageUseDB ) {
-                       $fields[] = 'page_lang';
-               }
-
-               $row = $db->selectRow( 'page', $fields,
+               $row = $db->selectRow( 'page', self::getSelectFields(),
                        [ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ],
                        __METHOD__
                );
index 2d4d20f..a2945af 100644 (file)
@@ -1055,8 +1055,8 @@ class ChangeTags {
                $tagUsage = self::tagUsageStatistics();
 
                if ( !is_null( $user ) ) {
-                       if ( !$user->isAllowed( 'managechangetags' ) ) {
-                               return Status::newFatal( 'tags-manage-no-permission' );
+                       if ( !$user->isAllowed( 'deletechangetags' ) ) {
+                               return Status::newFatal( 'tags-delete-no-permission' );
                        } elseif ( $user->isBlocked() ) {
                                return Status::newFatal( 'tags-manage-blocked' );
                        }
index 942036b..a374b13 100644 (file)
@@ -234,32 +234,33 @@ class IcuCollation extends Collation {
 
        /**
         * @since 1.16.3
+        * @return array
         */
        public function getFirstLetterData() {
-               if ( $this->firstLetterData !== null ) {
-                       return $this->firstLetterData;
-               }
-
-               $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
-               $cacheKey = $cache->makeKey(
-                       'first-letters',
-                       $this->locale,
-                       $this->digitTransformLanguage->getCode(),
-                       self::getICUVersion()
-               );
-               $cacheEntry = $cache->get( $cacheKey );
-
-               if ( $cacheEntry && isset( $cacheEntry['version'] )
-                       && $cacheEntry['version'] == self::FIRST_LETTER_VERSION
-               ) {
-                       $this->firstLetterData = $cacheEntry;
-                       return $this->firstLetterData;
+               if ( $this->firstLetterData === null ) {
+                       $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
+                       $cacheKey = $cache->makeKey(
+                               'first-letters',
+                               $this->locale,
+                               $this->digitTransformLanguage->getCode(),
+                               self::getICUVersion(),
+                               self::FIRST_LETTER_VERSION
+                       );
+                       $this->firstLetterData = $cache->getWithSetCallback( $cacheKey, $cache::TTL_WEEK, function () {
+                               return $this->fetchFirstLetterData();
+                       } );
                }
+               return $this->firstLetterData;
+       }
 
+       /**
+        * @return array
+        * @throws MWException
+        */
+       private function fetchFirstLetterData() {
                // Generate data from serialized data file
-
                if ( isset( self::$tailoringFirstLetters[$this->locale] ) ) {
-                       $letters = wfGetPrecompiledData( "first-letters-root.ser" );
+                       $letters = wfGetPrecompiledData( 'first-letters-root.ser' );
                        // Append additional characters
                        $letters = array_merge( $letters, self::$tailoringFirstLetters[$this->locale] );
                        // Remove unnecessary ones, if any
@@ -374,15 +375,11 @@ class IcuCollation extends Collation {
                $data = [
                        'chars' => array_values( $letterMap ),
                        'keys' => array_keys( $letterMap ),
-                       'version' => self::FIRST_LETTER_VERSION,
                ];
 
                // Reduce memory usage before caching
                unset( $letterMap );
 
-               // Save to cache
-               $this->firstLetterData = $data;
-               $cache->set( $cacheKey, $data, $cache::TTL_WEEK );
                return $data;
        }
 
@@ -390,30 +387,21 @@ class IcuCollation extends Collation {
         * @since 1.16.3
         */
        public function getLetterByIndex( $index ) {
-               if ( $this->firstLetterData === null ) {
-                       $this->getFirstLetterData();
-               }
-               return $this->firstLetterData['chars'][$index];
+               return $this->getFirstLetterData()['chars'][$index];
        }
 
        /**
         * @since 1.16.3
         */
        public function getSortKeyByLetterIndex( $index ) {
-               if ( $this->firstLetterData === null ) {
-                       $this->getFirstLetterData();
-               }
-               return $this->firstLetterData['keys'][$index];
+               return $this->getFirstLetterData()['keys'][$index];
        }
 
        /**
         * @since 1.16.3
         */
        public function getFirstLetterCount() {
-               if ( $this->firstLetterData === null ) {
-                       $this->getFirstLetterData();
-               }
-               return count( $this->firstLetterData['chars'] );
+               return count( $this->getFirstLetterData()['chars'] );
        }
 
        /**
index 65ff9f3..4ce9e62 100644 (file)
@@ -112,12 +112,18 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate {
                // Reliably broadcast the purge to all edge nodes
                $relayer = MediaWikiServices::getInstance()->getEventRelayerGroup()
                                        ->getRelayer( 'cdn-url-purges' );
-               $relayer->notify(
+               $ts = microtime( true );
+               $relayer->notifyMulti(
                        'cdn-url-purges',
-                       [
-                               'urls' => array_values( $urlArr ), // JSON array
-                               'timestamp' => microtime( true )
-                       ]
+                       array_map(
+                               function ( $url ) use ( $ts ) {
+                                       return [
+                                               'url' => $url,
+                                               'timestamp' => $ts,
+                                       ];
+                               },
+                               $urlArr
+                       )
                );
 
                // Send lossy UDP broadcasting if enabled
index 1770639..65a8c0e 100644 (file)
@@ -49,6 +49,9 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
        public function doUpdate() {
                # Page may already be deleted, so don't just getId()
                $id = $this->pageId;
+               // Make sure all links update threads see the changes of each other.
+               // This handles the case when updates have to batched into several COMMITs.
+               $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id );
 
                # Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
@@ -101,6 +104,11 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                                $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ );
                        }
                }
+
+               $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
+                       // Release the lock *after* the final COMMIT for correctness
+                       ScopedCallback::consume( $scopedLock );
+               } );
        }
 
        public function getAsJobSpecification() {
index c0205be..ac08374 100644 (file)
  */
 
 /**
- * See docs/deferred.txt
+ * Class the manages updates of *_link tables as well as similar extension-managed tables
+ *
+ * @note: LinksUpdate is managed by DeferredUpdates::execute(). Do not run this in a transaction.
  *
- * @todo document (e.g. one-sentence top-level class description).
+ * See docs/deferred.txt
  */
 class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
        // @todo make members protected, but make sure extensions don't break
@@ -82,6 +84,8 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         */
        private $user;
 
+       const BATCH_SIZE = 500; // try to keep typical updates in a single transaction
+
        /**
         * Constructor
         *
@@ -91,7 +95,8 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @throws MWException
         */
        function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
-               parent::__construct( false ); // no implicit transaction
+               // Implicit transactions are disabled as they interfere with batching
+               parent::__construct( false );
 
                $this->mTitle = $title;
                $this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
@@ -141,16 +146,46 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
        /**
         * Update link tables with outgoing links from an updated article
+        *
+        * @note: this is managed by DeferredUpdates::execute(). Do not run this in a transaction.
         */
        public function doUpdate() {
+               // Make sure all links update threads see the changes of each other.
+               // This handles the case when updates have to batched into several COMMITs.
+               $scopedLock = self::acquirePageLock( $this->mDb, $this->mId );
+
                Hooks::run( 'LinksUpdate', [ &$this ] );
                $this->doIncrementalUpdate();
 
-               $this->mDb->onTransactionIdle( function() {
+               $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
                        Hooks::run( 'LinksUpdateComplete', [ &$this ] );
+                       // Release the lock *after* the final COMMIT for correctness
+                       ScopedCallback::consume( $scopedLock );
                } );
        }
 
+       /**
+        * Acquire a lock for performing link table updates for a page on a DB
+        *
+        * @param IDatabase $dbw
+        * @param integer $pageId
+        * @return ScopedCallback|null Returns null on failure
+        * @throws RuntimeException
+        * @since 1.27
+        */
+       public static function acquirePageLock( IDatabase $dbw, $pageId ) {
+               $scopedLock = $dbw->getScopedLockAndFlush(
+                       "LinksUpdate:pageid:$pageId",
+                       __METHOD__,
+                       15
+               );
+               if ( !$scopedLock ) {
+                       throw new RuntimeException( "Could not acquire lock on page #$pageId." );
+               }
+
+               return $scopedLock;
+       }
+
        protected function doIncrementalUpdate() {
                # Page links
                $existing = $this->getExistingLinks();
@@ -160,7 +195,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Image links
                $existing = $this->getExistingImages();
-
                $imageDeletes = $this->getImageDeletions( $existing );
                $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes,
                        $this->getImageInsertions( $existing ) );
@@ -191,9 +225,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Category links
                $existing = $this->getExistingCategories();
-
                $categoryDeletes = $this->getCategoryDeletions( $existing );
-
                $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes,
                        $this->getCategoryInsertions( $existing ) );
 
@@ -205,9 +237,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Page properties
                $existing = $this->getExistingProperties();
-
                $propertiesDeletes = $this->getPropertyDeletions( $existing );
-
                $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes,
                        $this->getPropertyInsertions( $existing ) );
 
@@ -307,44 +337,69 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @param array $deletions
         * @param array $insertions Rows to insert
         */
-       function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
-               if ( $table == 'page_props' ) {
+       private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
+               if ( $table === 'page_props' ) {
                        $fromField = 'pp_page';
                } else {
                        $fromField = "{$prefix}_from";
                }
-               $where = [ $fromField => $this->mId ];
-               if ( $table == 'pagelinks' || $table == 'templatelinks' || $table == 'iwlinks' ) {
-                       if ( $table == 'iwlinks' ) {
-                               $baseKey = 'iwl_prefix';
-                       } else {
-                               $baseKey = "{$prefix}_namespace";
+
+               $deleteWheres = []; // list of WHERE clause arrays for each DB delete() call
+               if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) {
+                       $baseKey =  ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace";
+
+                       $curBatchSize = 0;
+                       $curDeletionBatch = [];
+                       $deletionBatches = [];
+                       foreach ( $deletions as $ns => $dbKeys ) {
+                               foreach ( $dbKeys as $dbKey => $unused ) {
+                                       $curDeletionBatch[$ns][$dbKey] = 1;
+                                       if ( ++$curBatchSize >= self::BATCH_SIZE ) {
+                                               $deletionBatches[] = $curDeletionBatch;
+                                               $curDeletionBatch = [];
+                                               $curBatchSize = 0;
+                                       }
+                               }
                        }
-                       $clause = $this->mDb->makeWhereFrom2d( $deletions, $baseKey, "{$prefix}_title" );
-                       if ( $clause ) {
-                               $where[] = $clause;
-                       } else {
-                               $where = false;
+                       if ( $curDeletionBatch ) {
+                               $deletionBatches[] = $curDeletionBatch;
+                       }
+
+                       foreach ( $deletionBatches as $deletionBatch ) {
+                               $deleteWheres[] = [
+                                       $fromField => $this->mId,
+                                       $this->mDb->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
+                               ];
                        }
                } else {
-                       if ( $table == 'langlinks' ) {
+                       if ( $table === 'langlinks' ) {
                                $toField = 'll_lang';
-                       } elseif ( $table == 'page_props' ) {
+                       } elseif ( $table === 'page_props' ) {
                                $toField = 'pp_propname';
                        } else {
                                $toField = $prefix . '_to';
                        }
-                       if ( count( $deletions ) ) {
-                               $where[$toField] = array_keys( $deletions );
-                       } else {
-                               $where = false;
+
+                       $deletionBatches = array_chunk( array_keys( $deletions ), self::BATCH_SIZE );
+                       foreach ( $deletionBatches as $deletionBatch ) {
+                               $deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ];
                        }
                }
-               if ( $where ) {
-                       $this->mDb->delete( $table, $where, __METHOD__ );
+
+               foreach ( $deleteWheres as $deleteWhere ) {
+                       $this->mDb->delete( $table, $deleteWhere, __METHOD__ );
+                       $this->mDb->commit( __METHOD__, 'flush' );
+                       wfGetLBFactory()->waitForReplication();
+               }
+
+               $insertBatches = array_chunk( $insertions, self::BATCH_SIZE );
+               foreach ( $insertBatches as $insertBatch ) {
+                       $this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
+                       $this->mDb->commit( __METHOD__, 'flush' );
+                       wfGetLBFactory()->waitForReplication();
                }
+
                if ( count( $insertions ) ) {
-                       $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
                        Hooks::run( 'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
                }
        }
index 916be2d..b7c3489 100644 (file)
@@ -25,7 +25,7 @@
  * 'exception-nologin' as a title and 'exception-nologin-text' for the message.
  *
  * @note In order for this exception to redirect, the error message passed to the
- * constructor has to be explicitly added to LoginForm::validErrorMessages or with
+ * constructor has to be explicitly added to LoginHelper::validErrorMessages or with
  * the LoginFormValidErrorMessages hook. Otherwise, the user will just be shown the message
  * rather than redirected.
  *
@@ -79,7 +79,7 @@ class UserNotLoggedIn extends ErrorPageError {
        public function report() {
                // If an unsupported message is used, don't try redirecting to Special:Userlogin,
                // since the message may not be compatible.
-               if ( !in_array( $this->msg, LoginForm::getValidErrorMessages() ) ) {
+               if ( !in_array( $this->msg, LoginHelper::getValidErrorMessages() ) ) {
                        parent::report();
                }
 
index f7275fc..2a15fd7 100644 (file)
@@ -1920,12 +1920,15 @@ class LocalFile extends File {
                        // Also, that would cause contention on INSERT of similarly named rows.
                        $backend = $this->getRepo()->getBackend();
                        $lockPaths = [ $this->getPath() ]; // represents all versions of the file
-                       $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
+                       $start = microtime( true );
+                       $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 10 );
+                       $waited = microtime( true ) - $start;
                        if ( !$status->isGood() ) {
                                if ( $this->lockedOwnTrx ) {
                                        $dbw->rollback( __METHOD__ );
                                }
-                               throw new LocalFileLockError( "Could not acquire lock for '{$this->getName()}.'" );
+                               throw new LocalFileLockError(
+                                       "Could not acquire lock for '{$this->getName()}' ($waited sec)." );
                        }
                        // Release the lock *after* commit to avoid row-level contention
                        $this->locked++;
index 7a2ed50..711750b 100644 (file)
@@ -221,22 +221,27 @@ class OOUIHTMLForm extends HTMLForm {
                // FIXME This only works for forms with no subsections
                if ( $fieldset instanceof OOUI\FieldsetLayout ) {
                        $classes = [ 'mw-htmlform-ooui-header' ];
-                       if ( !$this->mHeader ) {
-                               $classes[] = 'mw-htmlform-ooui-header-empty';
-                       }
                        if ( $this->oouiErrors ) {
                                $classes[] = 'mw-htmlform-ooui-header-errors';
                        }
-                       $fieldset->addItems( [
-                               new OOUI\FieldLayout(
-                                       new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] ),
-                                       [
-                                               'align' => 'top',
-                                               'errors' => $this->oouiErrors,
-                                               'classes' => $classes,
-                                       ]
-                               )
-                       ], 0 );
+                       if ( $this->mHeader || $this->oouiErrors ) {
+                               // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
+                               if ( $this->mHeader ) {
+                                       $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
+                               } else {
+                                       $element = new OOUI\Widget( [] );
+                               }
+                               $fieldset->addItems( [
+                                       new OOUI\FieldLayout(
+                                               $element,
+                                               [
+                                                       'align' => 'top',
+                                                       'errors' => $this->oouiErrors,
+                                                       'classes' => $classes,
+                                               ]
+                                       )
+                               ], 0 );
+                       }
                }
                return $fieldset;
        }
index ee8ba4b..45a17c1 100644 (file)
@@ -5,7 +5,7 @@
                        "NoiX180"
                ]
        },
-       "config-install-mainpage-failed": "Ora bisa nglebokaké kaca pokok: $1",
+       "config-install-mainpage-failed": "Ora bisa nglebokaké tepas: $1",
        "mainpagetext": "'''Prangkat empuk wiki wis suksès dipasang.'''",
        "mainpagedocfooter": "Mangga maca [//meta.wikimedia.org/wiki/Help:Contents User's Guide] kanggo katrangan luwih langkung prakara panggunan prangkat empuk wiki\n== Miwiti panggunan  ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Daftar pangaturan préférènsi]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]"
 }
index 14c0820..62afeee 100644 (file)
@@ -72,6 +72,7 @@
        "config-ctype": "<strong>치명</strong>: PHP는 [http://www.php.net/manual/en/ctype.installation.php Ctype 확장 기능]을 지원하도록 하여 컴파일해야 합니다.",
        "config-iconv": "<strong>치명</strong>: PHP는 [http://www.php.net/manual/en/iconv.installation.php iconv 확장 기능]을 지원하도록 하여 컴파일해야 합니다.",
        "config-json": "<strong>치명:</strong> PHP가 JSON 지원이 없이 컴파일되었습니다.\n미디어위키를 설치하기 전에 PHP JSON 확장 기능이나 [http://pecl.php.net/package/jsonc PECL jsonc] 확장 기능 중 하나를 설치해야 합니다.\n* PHP 확장 기능은 Red Hat Enterprise Linux (CentOS) 5와 6에 포함되어 있지만, <code>/etc/php.ini</code>나 <code>/etc/php.d/json.ini</code>에서 활성화해야 합니다.\n* 2013년 5월 이후에 출시된 일부 리눅스 배포판은 PHP 확장 기능이 생략된 대신, <code>php5-json</code>이나 <code>php-pecl-jsonc</code>로 PECL 확장 기능이 포장되어 있습니다.",
+       "config-mbstring-absent": "<strong>치명적 오류:</strong> PHP는 [http://www.php.net/manual/en/mbstring.setup.php mbstring 확장]을 지원하도록 컴파일되어야 합니다.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache]가 설치되었습니다",
        "config-apc": "[http://www.php.net/apc APC]가 설치되었습니다",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache]가 설치되었습니다",
index a2f55b9..1350958 100644 (file)
@@ -268,13 +268,19 @@ class JobRunner implements LoggerAwareInterface {
 
                        DeferredUpdates::doUpdates();
                        $this->commitMasterChanges( $job );
-                       $job->teardown();
                } catch ( Exception $e ) {
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
                        $status = false;
                        $error = get_class( $e ) . ': ' . $e->getMessage();
                        MWExceptionHandler::logException( $e );
                }
+               // Always attempt to call teardown() even if Job throws exception.
+               try {
+                       $job->teardown();
+               } catch ( Exception $e ) {
+                       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.
index 16e35f1..1e804c4 100644 (file)
@@ -33,7 +33,6 @@ class AssembleUploadChunksJob extends Job {
        }
 
        public function run() {
-               /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = RequestContext::importScopedSession( $this->params['session'] );
                $this->addTeardownCallback( function () use ( &$scope ) {
                        ScopedCallback::consume( $scope ); // T126450
index d2825a8..78531dc 100644 (file)
@@ -35,7 +35,6 @@ class PublishStashedFileJob extends Job {
        }
 
        public function run() {
-               /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = RequestContext::importScopedSession( $this->params['session'] );
                $this->addTeardownCallback( function () use ( &$scope ) {
                        ScopedCallback::consume( $scope ); // T126450
index d0f067f..9c1ec8e 100644 (file)
  * @file
  */
 
-use RunningStat\RunningStat;
-
 /**
  * Convenience class for working with XHProf
  * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
  * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
  *
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
+ * @since 1.28
  */
 class Xhprof {
-
-       /**
-        * @var array $config
-        */
-       protected $config;
-
-       /**
-        * Hierarchical profiling data returned by xhprof.
-        * @var array $hieraData
-        */
-       protected $hieraData;
-
        /**
-        * Per-function inclusive data.
-        * @var array $inclusive
+        * @var bool $enabled Whether XHProf is currently running.
         */
-       protected $inclusive;
+       protected static $enabled;
 
        /**
-        * Per-function inclusive and exclusive data.
-        * @var array $complete
+        * Start xhprof profiler
         */
-       protected $complete;
-
-       /**
-        * Configuration data can contain:
-        * - flags:   Optional flags to add additional information to the
-        *            profiling data collected.
-        *            (XHPROF_FLAGS_NO_BUILTINS, XHPROF_FLAGS_CPU,
-        *            XHPROF_FLAGS_MEMORY)
-        * - exclude: Array of function names to exclude from profiling.
-        * - include: Array of function names to include in profiling.
-        * - sort:    Key to sort per-function reports on.
-        *
-        * Note: When running under HHVM, xhprof will always behave as though the
-        * XHPROF_FLAGS_NO_BUILTINS flag has been used unless the
-        * Eval.JitEnableRenameFunction option is enabled for the HHVM process.
-        *
-        * @param array $config
-        */
-       public function __construct( array $config = [] ) {
-               $this->config = array_merge(
-                       [
-                               'flags' => 0,
-                               'exclude' => [],
-                               'include' => null,
-                               'sort' => 'wt',
-                       ],
-                       $config
-               );
-
-               xhprof_enable( $this->config['flags'], [
-                       'ignored_functions' => $this->config['exclude']
-               ] );
+       public static function isEnabled() {
+               return self::$enabled;
        }
 
        /**
-        * Stop collecting profiling data.
-        *
-        * Only the first invocation of this method will effect the internal
-        * object state. Subsequent calls will return the data collected by the
-        * initial call.
-        *
-        * @return array Collected profiling data (possibly cached)
+        * Start xhprof profiler
         */
-       public function stop() {
-               if ( $this->hieraData === null ) {
-                       $this->hieraData = $this->pruneData( xhprof_disable() );
+       public static function enable( $flags = 0, $options = [] ) {
+               if ( self::isEnabled() ) {
+                       throw new Exception( 'Xhprof profiling is already enabled.' );
                }
-               return $this->hieraData;
-       }
-
-       /**
-        * Load raw data from a prior run for analysis.
-        * Stops any existing data collection and clears internal caches.
-        *
-        * Any 'include' filters configured for this Xhprof instance will be
-        * enforced on the data as it is loaded. 'exclude' filters will however
-        * not be enforced as they are an XHProf intrinsic behavior.
-        *
-        * @param array $data
-        * @see getRawData()
-        */
-       public function loadRawData( array $data ) {
-               $this->stop();
-               $this->inclusive = null;
-               $this->complete = null;
-               $this->hieraData = $this->pruneData( $data );
-       }
-
-       /**
-        * Get raw data collected by xhprof.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * Each key in the returned array is an edge label for the call graph in
-        * the form "caller==>callee". There is once special case edge labled
-        * simply "main()" which represents the global scope entry point of the
-        * application.
-        *
-        * XHProf will collect different data depending on the flags that are used:
-        * - ct:    Number of matching events seen.
-        * - wt:    Inclusive elapsed wall time for this event in microseconds.
-        * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
-        *          (XHPROF_FLAGS_CPU)
-        * - mu:    Delta of memory usage from start to end of callee in bytes.
-        *          (XHPROF_FLAGS_MEMORY)
-        * - pmu:   Delta of peak memory usage from start to end of callee in
-        *          bytes. (XHPROF_FLAGS_MEMORY)
-        * - alloc: Delta of amount memory requested from malloc() by the callee,
-        *          in bytes. (XHPROF_FLAGS_MALLOC)
-        * - free:  Delta of amount of memory passed to free() by the callee, in
-        *          bytes. (XHPROF_FLAGS_MALLOC)
-        *
-        * @return array
-        * @see stop()
-        * @see getInclusiveMetrics()
-        * @see getCompleteMetrics()
-        */
-       public function getRawData() {
-               return $this->stop();
+               self::$enabled = true;
+               xhprof_enable( $flags, $options );
        }
 
        /**
-        * Convert an xhprof data key into an array of ['parent', 'child']
-        * function names.
-        *
-        * The resulting array is left padded with nulls, so a key
-        * with no parent (eg 'main()') will return [null, 'function'].
+        * Stop xhprof profiler
         *
-        * @return array
+        * @return array|null xhprof data from the run, or null if xhprof was not running.
         */
-       public static function splitKey( $key ) {
-               return array_pad( explode( '==>', $key, 2 ), -2, null );
-       }
-
-       /**
-        * Remove data for functions that are not included in the 'include'
-        * configuration array.
-        *
-        * @param array $data Raw xhprof data
-        * @return array
-        */
-       protected function pruneData( $data ) {
-               if ( !$this->config['include'] ) {
-                       return $data;
-               }
-
-               $want = array_fill_keys( $this->config['include'], true );
-               $want['main()'] = true;
-
-               $keep = [];
-               foreach ( $data as $key => $stats ) {
-                       list( $parent, $child ) = self::splitKey( $key );
-                       if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
-                               $keep[$key] = $stats;
-                       }
+       public static function disable() {
+               if ( self::isEnabled() ) {
+                       self::$enabled = false;
+                       return xhprof_disable();
                }
-               return $keep;
-       }
-
-       /**
-        * Get the inclusive metrics for each function call. Inclusive metrics
-        * for given function include the metrics for all functions that were
-        * called from that function during the measurement period.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * See getRawData() for a description of the metric that are returned for
-        * each funcition call. The values for the wt, cpu, mu and pmu metrics are
-        * arrays with these values:
-        * - total: Cumulative value
-        * - min: Minimum value
-        * - mean: Mean (average) value
-        * - max: Maximum value
-        * - variance: Variance (spread) of the values
-        *
-        * @return array
-        * @see getRawData()
-        * @see getCompleteMetrics()
-        */
-       public function getInclusiveMetrics() {
-               if ( $this->inclusive === null ) {
-                       // Make sure we have data to work with
-                       $this->stop();
-
-                       $main = $this->hieraData['main()'];
-                       $hasCpu = isset( $main['cpu'] );
-                       $hasMu = isset( $main['mu'] );
-                       $hasAlloc = isset( $main['alloc'] );
-
-                       $this->inclusive = [];
-                       foreach ( $this->hieraData as $key => $stats ) {
-                               list( $parent, $child ) = self::splitKey( $key );
-                               if ( !isset( $this->inclusive[$child] ) ) {
-                                       $this->inclusive[$child] = [
-                                               'ct' => 0,
-                                               'wt' => new RunningStat(),
-                                       ];
-                                       if ( $hasCpu ) {
-                                               $this->inclusive[$child]['cpu'] = new RunningStat();
-                                       }
-                                       if ( $hasMu ) {
-                                               $this->inclusive[$child]['mu'] = new RunningStat();
-                                               $this->inclusive[$child]['pmu'] = new RunningStat();
-                                       }
-                                       if ( $hasAlloc ) {
-                                               $this->inclusive[$child]['alloc'] = new RunningStat();
-                                               $this->inclusive[$child]['free'] = new RunningStat();
-                                       }
-                               }
-
-                               $this->inclusive[$child]['ct'] += $stats['ct'];
-                               foreach ( $stats as $stat => $value ) {
-                                       if ( $stat === 'ct' ) {
-                                               continue;
-                                       }
-
-                                       if ( !isset( $this->inclusive[$child][$stat] ) ) {
-                                               // Ignore unknown stats
-                                               continue;
-                                       }
-
-                                       for ( $i = 0; $i < $stats['ct']; $i++ ) {
-                                               $this->inclusive[$child][$stat]->addObservation(
-                                                       $value / $stats['ct']
-                                               );
-                                       }
-                               }
-                       }
-
-                       // Convert RunningStat instances to static arrays and add
-                       // percentage stats.
-                       foreach ( $this->inclusive as $func => $stats ) {
-                               foreach ( $stats as $name => $value ) {
-                                       if ( $value instanceof RunningStat ) {
-                                               $total = $value->m1 * $value->n;
-                                               $percent = ( isset( $main[$name] ) && $main[$name] )
-                                                       ? 100 * $total / $main[$name]
-                                                       : 0;
-                                               $this->inclusive[$func][$name] = [
-                                                       'total' => $total,
-                                                       'min' => $value->min,
-                                                       'mean' => $value->m1,
-                                                       'max' => $value->max,
-                                                       'variance' => $value->m2,
-                                                       'percent' => $percent,
-                                               ];
-                                       }
-                               }
-                       }
-
-                       uasort( $this->inclusive, self::makeSortFunction(
-                               $this->config['sort'], 'total'
-                       ) );
-               }
-               return $this->inclusive;
-       }
-
-       /**
-        * Get the inclusive and exclusive metrics for each function call.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * In addition to the normal data contained in the inclusive metrics, the
-        * metrics have an additional 'exclusive' measurement which is the total
-        * minus the totals of all child function calls.
-        *
-        * @return array
-        * @see getRawData()
-        * @see getInclusiveMetrics()
-        */
-       public function getCompleteMetrics() {
-               if ( $this->complete === null ) {
-                       // Start with inclusive data
-                       $this->complete = $this->getInclusiveMetrics();
-
-                       foreach ( $this->complete as $func => $stats ) {
-                               foreach ( $stats as $stat => $value ) {
-                                       if ( $stat === 'ct' ) {
-                                               continue;
-                                       }
-                                       // Initialize exclusive data with inclusive totals
-                                       $this->complete[$func][$stat]['exclusive'] = $value['total'];
-                               }
-                               // Add sapce for call tree information to be filled in later
-                               $this->complete[$func]['calls'] = [];
-                               $this->complete[$func]['subcalls'] = [];
-                       }
-
-                       foreach ( $this->hieraData as $key => $stats ) {
-                               list( $parent, $child ) = self::splitKey( $key );
-                               if ( $parent !== null ) {
-                                       // Track call tree information
-                                       $this->complete[$child]['calls'][$parent] = $stats;
-                                       $this->complete[$parent]['subcalls'][$child] = $stats;
-                               }
-
-                               if ( isset( $this->complete[$parent] ) ) {
-                                       // Deduct child inclusive data from exclusive data
-                                       foreach ( $stats as $stat => $value ) {
-                                               if ( $stat === 'ct' ) {
-                                                       continue;
-                                               }
-
-                                               if ( !isset( $this->complete[$parent][$stat] ) ) {
-                                                       // Ignore unknown stats
-                                                       continue;
-                                               }
-
-                                               $this->complete[$parent][$stat]['exclusive'] -= $value;
-                                       }
-                               }
-                       }
-
-                       uasort( $this->complete, self::makeSortFunction(
-                               $this->config['sort'], 'exclusive'
-                       ) );
-               }
-               return $this->complete;
-       }
-
-       /**
-        * Get a list of all callers of a given function.
-        *
-        * @param string $function Function name
-        * @return array
-        * @see getEdges()
-        */
-       public function getCallers( $function ) {
-               $edges = $this->getCompleteMetrics();
-               if ( isset( $edges[$function]['calls'] ) ) {
-                       return array_keys( $edges[$function]['calls'] );
-               } else {
-                       return [];
-               }
-       }
-
-       /**
-        * Get a list of all callees from a given function.
-        *
-        * @param string $function Function name
-        * @return array
-        * @see getEdges()
-        */
-       public function getCallees( $function ) {
-               $edges = $this->getCompleteMetrics();
-               if ( isset( $edges[$function]['subcalls'] ) ) {
-                       return array_keys( $edges[$function]['subcalls'] );
-               } else {
-                       return [];
-               }
-       }
-
-       /**
-        * Find the critical path for the given metric.
-        *
-        * @param string $metric Metric to find critical path for
-        * @return array
-        */
-       public function getCriticalPath( $metric = 'wt' ) {
-               $this->stop();
-               $func = 'main()';
-               $path = [
-                       $func => $this->hieraData[$func],
-               ];
-               while ( $func ) {
-                       $callees = $this->getCallees( $func );
-                       $maxCallee = null;
-                       $maxCall = null;
-                       foreach ( $callees as $callee ) {
-                               $call = "{$func}==>{$callee}";
-                               if ( $maxCall === null ||
-                                       $this->hieraData[$call][$metric] >
-                                               $this->hieraData[$maxCall][$metric]
-                               ) {
-                                       $maxCallee = $callee;
-                                       $maxCall = $call;
-                               }
-                       }
-                       if ( $maxCall !== null ) {
-                               $path[$maxCall] = $this->hieraData[$maxCall];
-                       }
-                       $func = $maxCallee;
-               }
-               return $path;
-       }
-
-       /**
-        * Make a closure to use as a sort function. The resulting function will
-        * sort by descending numeric values (largest value first).
-        *
-        * @param string $key Data key to sort on
-        * @param string $sub Sub key to sort array values on
-        * @return Closure
-        */
-       public static function makeSortFunction( $key, $sub ) {
-               return function ( $a, $b ) use ( $key, $sub ) {
-                       if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
-                               // Descending sort: larger values will be first in result.
-                               // Assumes all values are numeric.
-                               // Values for 'main()' will not have sub keys
-                               $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
-                               $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
-                               return $valB - $valA;
-                       } else {
-                               // Sort datum with the key before those without
-                               return isset( $a[$key] ) ? -1 : 1;
-                       }
-               };
        }
 }
diff --git a/includes/libs/XhprofData.php b/includes/libs/XhprofData.php
new file mode 100644 (file)
index 0000000..c6da432
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use RunningStat\RunningStat;
+
+/**
+ * Convenience class for working with XHProf profiling data
+ * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
+ * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
+ *
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.28
+ */
+class XhprofData {
+
+       /**
+        * @var array $config
+        */
+       protected $config;
+
+       /**
+        * Hierarchical profiling data returned by xhprof.
+        * @var array $hieraData
+        */
+       protected $hieraData;
+
+       /**
+        * Per-function inclusive data.
+        * @var array $inclusive
+        */
+       protected $inclusive;
+
+       /**
+        * Per-function inclusive and exclusive data.
+        * @var array $complete
+        */
+       protected $complete;
+
+       /**
+        * Configuration data can contain:
+        * - include: Array of function names to include in profiling.
+        * - sort:    Key to sort per-function reports on.
+        *
+        * @param array $data Xhprof profiling data, as returned by xhprof_disable()
+        * @param array $config
+        */
+       public function __construct( array $data, array $config = [] ) {
+               $this->config = array_merge( [
+                       'include' => null,
+                       'sort' => 'wt',
+               ], $config );
+
+               $this->hieraData = $this->pruneData( $data );
+       }
+
+       /**
+        * Get raw data collected by xhprof.
+        *
+        * Each key in the returned array is an edge label for the call graph in
+        * the form "caller==>callee". There is once special case edge labled
+        * simply "main()" which represents the global scope entry point of the
+        * application.
+        *
+        * XHProf will collect different data depending on the flags that are used:
+        * - ct:    Number of matching events seen.
+        * - wt:    Inclusive elapsed wall time for this event in microseconds.
+        * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
+        *          (XHPROF_FLAGS_CPU)
+        * - mu:    Delta of memory usage from start to end of callee in bytes.
+        *          (XHPROF_FLAGS_MEMORY)
+        * - pmu:   Delta of peak memory usage from start to end of callee in
+        *          bytes. (XHPROF_FLAGS_MEMORY)
+        * - alloc: Delta of amount memory requested from malloc() by the callee,
+        *          in bytes. (XHPROF_FLAGS_MALLOC)
+        * - free:  Delta of amount of memory passed to free() by the callee, in
+        *          bytes. (XHPROF_FLAGS_MALLOC)
+        *
+        * @return array
+        * @see getInclusiveMetrics()
+        * @see getCompleteMetrics()
+        */
+       public function getRawData() {
+               return $this->hieraData;
+       }
+
+       /**
+        * Convert an xhprof data key into an array of ['parent', 'child']
+        * function names.
+        *
+        * The resulting array is left padded with nulls, so a key
+        * with no parent (eg 'main()') will return [null, 'function'].
+        *
+        * @return array
+        */
+       public static function splitKey( $key ) {
+               return array_pad( explode( '==>', $key, 2 ), -2, null );
+       }
+
+       /**
+        * Remove data for functions that are not included in the 'include'
+        * configuration array.
+        *
+        * @param array $data Raw xhprof data
+        * @return array
+        */
+       protected function pruneData( $data ) {
+               if ( !$this->config['include'] ) {
+                       return $data;
+               }
+
+               $want = array_fill_keys( $this->config['include'], true );
+               $want['main()'] = true;
+
+               $keep = [];
+               foreach ( $data as $key => $stats ) {
+                       list( $parent, $child ) = self::splitKey( $key );
+                       if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
+                               $keep[$key] = $stats;
+                       }
+               }
+               return $keep;
+       }
+
+       /**
+        * Get the inclusive metrics for each function call. Inclusive metrics
+        * for given function include the metrics for all functions that were
+        * called from that function during the measurement period.
+        *
+        * See getRawData() for a description of the metric that are returned for
+        * each funcition call. The values for the wt, cpu, mu and pmu metrics are
+        * arrays with these values:
+        * - total: Cumulative value
+        * - min: Minimum value
+        * - mean: Mean (average) value
+        * - max: Maximum value
+        * - variance: Variance (spread) of the values
+        *
+        * @return array
+        * @see getRawData()
+        * @see getCompleteMetrics()
+        */
+       public function getInclusiveMetrics() {
+               if ( $this->inclusive === null ) {
+                       $main = $this->hieraData['main()'];
+                       $hasCpu = isset( $main['cpu'] );
+                       $hasMu = isset( $main['mu'] );
+                       $hasAlloc = isset( $main['alloc'] );
+
+                       $this->inclusive = [];
+                       foreach ( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( !isset( $this->inclusive[$child] ) ) {
+                                       $this->inclusive[$child] = [
+                                               'ct' => 0,
+                                               'wt' => new RunningStat(),
+                                       ];
+                                       if ( $hasCpu ) {
+                                               $this->inclusive[$child]['cpu'] = new RunningStat();
+                                       }
+                                       if ( $hasMu ) {
+                                               $this->inclusive[$child]['mu'] = new RunningStat();
+                                               $this->inclusive[$child]['pmu'] = new RunningStat();
+                                       }
+                                       if ( $hasAlloc ) {
+                                               $this->inclusive[$child]['alloc'] = new RunningStat();
+                                               $this->inclusive[$child]['free'] = new RunningStat();
+                                       }
+                               }
+
+                               $this->inclusive[$child]['ct'] += $stats['ct'];
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+
+                                       if ( !isset( $this->inclusive[$child][$stat] ) ) {
+                                               // Ignore unknown stats
+                                               continue;
+                                       }
+
+                                       for ( $i = 0; $i < $stats['ct']; $i++ ) {
+                                               $this->inclusive[$child][$stat]->addObservation(
+                                                       $value / $stats['ct']
+                                               );
+                                       }
+                               }
+                       }
+
+                       // Convert RunningStat instances to static arrays and add
+                       // percentage stats.
+                       foreach ( $this->inclusive as $func => $stats ) {
+                               foreach ( $stats as $name => $value ) {
+                                       if ( $value instanceof RunningStat ) {
+                                               $total = $value->m1 * $value->n;
+                                               $percent = ( isset( $main[$name] ) && $main[$name] )
+                                                       ? 100 * $total / $main[$name]
+                                                       : 0;
+                                               $this->inclusive[$func][$name] = [
+                                                       'total' => $total,
+                                                       'min' => $value->min,
+                                                       'mean' => $value->m1,
+                                                       'max' => $value->max,
+                                                       'variance' => $value->m2,
+                                                       'percent' => $percent,
+                                               ];
+                                       }
+                               }
+                       }
+
+                       uasort( $this->inclusive, self::makeSortFunction(
+                               $this->config['sort'], 'total'
+                       ) );
+               }
+               return $this->inclusive;
+       }
+
+       /**
+        * Get the inclusive and exclusive metrics for each function call.
+        *
+        * In addition to the normal data contained in the inclusive metrics, the
+        * metrics have an additional 'exclusive' measurement which is the total
+        * minus the totals of all child function calls.
+        *
+        * @return array
+        * @see getRawData()
+        * @see getInclusiveMetrics()
+        */
+       public function getCompleteMetrics() {
+               if ( $this->complete === null ) {
+                       // Start with inclusive data
+                       $this->complete = $this->getInclusiveMetrics();
+
+                       foreach ( $this->complete as $func => $stats ) {
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+                                       // Initialize exclusive data with inclusive totals
+                                       $this->complete[$func][$stat]['exclusive'] = $value['total'];
+                               }
+                               // Add sapce for call tree information to be filled in later
+                               $this->complete[$func]['calls'] = [];
+                               $this->complete[$func]['subcalls'] = [];
+                       }
+
+                       foreach ( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( $parent !== null ) {
+                                       // Track call tree information
+                                       $this->complete[$child]['calls'][$parent] = $stats;
+                                       $this->complete[$parent]['subcalls'][$child] = $stats;
+                               }
+
+                               if ( isset( $this->complete[$parent] ) ) {
+                                       // Deduct child inclusive data from exclusive data
+                                       foreach ( $stats as $stat => $value ) {
+                                               if ( $stat === 'ct' ) {
+                                                       continue;
+                                               }
+
+                                               if ( !isset( $this->complete[$parent][$stat] ) ) {
+                                                       // Ignore unknown stats
+                                                       continue;
+                                               }
+
+                                               $this->complete[$parent][$stat]['exclusive'] -= $value;
+                                       }
+                               }
+                       }
+
+                       uasort( $this->complete, self::makeSortFunction(
+                               $this->config['sort'], 'exclusive'
+                       ) );
+               }
+               return $this->complete;
+       }
+
+       /**
+        * Get a list of all callers of a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallers( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['calls'] ) ) {
+                       return array_keys( $edges[$function]['calls'] );
+               } else {
+                       return [];
+               }
+       }
+
+       /**
+        * Get a list of all callees from a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallees( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['subcalls'] ) ) {
+                       return array_keys( $edges[$function]['subcalls'] );
+               } else {
+                       return [];
+               }
+       }
+
+       /**
+        * Find the critical path for the given metric.
+        *
+        * @param string $metric Metric to find critical path for
+        * @return array
+        */
+       public function getCriticalPath( $metric = 'wt' ) {
+               $func = 'main()';
+               $path = [
+                       $func => $this->hieraData[$func],
+               ];
+               while ( $func ) {
+                       $callees = $this->getCallees( $func );
+                       $maxCallee = null;
+                       $maxCall = null;
+                       foreach ( $callees as $callee ) {
+                               $call = "{$func}==>{$callee}";
+                               if ( $maxCall === null ||
+                                       $this->hieraData[$call][$metric] >
+                                               $this->hieraData[$maxCall][$metric]
+                               ) {
+                                       $maxCallee = $callee;
+                                       $maxCall = $call;
+                               }
+                       }
+                       if ( $maxCall !== null ) {
+                               $path[$maxCall] = $this->hieraData[$maxCall];
+                       }
+                       $func = $maxCallee;
+               }
+               return $path;
+       }
+
+       /**
+        * Make a closure to use as a sort function. The resulting function will
+        * sort by descending numeric values (largest value first).
+        *
+        * @param string $key Data key to sort on
+        * @param string $sub Sub key to sort array values on
+        * @return Closure
+        */
+       public static function makeSortFunction( $key, $sub ) {
+               return function ( $a, $b ) use ( $key, $sub ) {
+                       if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
+                               // Descending sort: larger values will be first in result.
+                               // Assumes all values are numeric.
+                               // Values for 'main()' will not have sub keys
+                               $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
+                               $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
+                               return $valB - $valA;
+                       } else {
+                               // Sort datum with the key before those without
+                               return isset( $a[$key] ) ? -1 : 1;
+                       }
+               };
+       }
+}
index 18cc10e..470a38c 100644 (file)
@@ -110,12 +110,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** Cache format version number */
        const VERSION = 1;
 
-       const FLD_VERSION = 0;
-       const FLD_VALUE = 1;
-       const FLD_TTL = 2;
-       const FLD_TIME = 3;
-       const FLD_FLAGS = 4;
-       const FLD_HOLDOFF = 5;
+       const FLD_VERSION = 0; // key to cache version number
+       const FLD_VALUE = 1; // key to the cached value
+       const FLD_TTL = 2; // key to the original TTL
+       const FLD_TIME = 3; // key to the cache time
+       const FLD_FLAGS = 4; // key to the flags bitfield
+       const FLD_HOLDOFF = 5; // key to any hold-off TTL
 
        /** @var integer Treat this value as expired-on-arrival */
        const FLG_STALE = 1;
@@ -377,8 +377,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         */
        final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
+               $now = microtime( true );
                $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
-               $age = isset( $opts['since'] ) ? max( 0, microtime( true ) - $opts['since'] ) : 0;
+               $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
                $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
 
                // Do not cache potentially uncommitted data as it might get rolled back
@@ -413,7 +414,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                }
 
                // Wrap that value with time/TTL/version metadata
-               $wrapped = $this->wrap( $value, $ttl ) + $wrapExtra;
+               $wrapped = $this->wrap( $value, $ttl, $now ) + $wrapExtra;
 
                $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
                        return ( is_string( $cWrapped ) )
@@ -1009,14 +1010,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * @param mixed $value
         * @param integer $ttl [0=forever]
+        * @param float $now Unix Current timestamp just before calling set()
         * @return array
         */
-       protected function wrap( $value, $ttl ) {
+       protected function wrap( $value, $ttl, $now ) {
                return [
                        self::FLD_VERSION => self::VERSION,
                        self::FLD_VALUE => $value,
                        self::FLD_TTL => $ttl,
-                       self::FLD_TIME => microtime( true )
+                       self::FLD_TIME => $now
                ];
        }
 
@@ -1024,7 +1026,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * Do not use this method outside WANObjectCache
         *
         * @param array|string|bool $wrapped
-        * @param float $now Unix Current timestamp (preferrable pre-query)
+        * @param float $now Unix Current timestamp (preferrably pre-query)
         * @return array (mixed; false if absent/invalid, current time left)
         */
        protected function unwrap( $wrapped, $now ) {
index 7b59751..da48e00 100644 (file)
@@ -33,6 +33,14 @@ interface LinkTarget {
         */
        public function getNamespace();
 
+       /**
+        * Convenience function to test if it is in the namespace
+        *
+        * @param int $ns
+        * @return bool
+        */
+       public function inNamespace( $ns );
+
        /**
         * Get the link fragment (i.e. the bit after the #) in text form.
         *
index 61e6926..90508da 100644 (file)
@@ -310,7 +310,8 @@ class RedisBagOStuff extends BagOStuff {
         * @return mixed
         */
        protected function unserialize( $data ) {
-               return ctype_digit( $data ) ? intval( $data ) : unserialize( $data );
+               $int = intval( $data );
+               return $data === (string)$int ? $int : unserialize( $data );
        }
 
        /**
index a55ddf3..3b8b513 100644 (file)
@@ -456,10 +456,18 @@ class CoreParserFunctions {
                                                $converter->markNoConversion( wfEscapeWikiText( $text ) )
                                        )->inContentLanguage()->text() .
                                        '</span>';
+                       } else {
+                               return '';
                        }
+               } else {
+                       $converter = $parser->getConverterLanguage()->getConverter();
+                       return '<span class="error">' .
+                               wfMessage( 'restricted-displaytitle',
+                                       // Message should be parsed, but this param should only be escaped.
+                                       $converter->markNoConversion( wfEscapeWikiText( $text ) )
+                               )->inContentLanguage()->text() .
+                               '</span>';
                }
-
-               return '';
        }
 
        /**
index 04b5614..8575e69 100644 (file)
@@ -282,7 +282,7 @@ class LinkHolderArray {
                        return;
                }
 
-               global $wgContLang, $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+               global $wgContLang;
 
                $colours = [];
                $linkCache = LinkCache::singleton();
@@ -297,7 +297,9 @@ class LinkHolderArray {
                $linkcolour_ids = [];
 
                # Generate query
-               $queries = [];
+               $lb = new LinkBatch();
+               $lb->setCaller( __METHOD__ );
+
                foreach ( $this->internals as $ns => $entries ) {
                        foreach ( $entries as $entry ) {
                                /** @var Title $title */
@@ -325,37 +327,21 @@ class LinkHolderArray {
                                                $colours[$pdbk] = 'new';
                                        } else {
                                                # Not in the link cache, add it to the query
-                                               $queries[$ns][] = $title->getDBkey();
+                                               $lb->addObj( $title );
                                        }
                                }
                        }
                }
-               if ( $queries ) {
-                       $where = [];
-                       foreach ( $queries as $ns => $pages ) {
-                               $where[] = $dbr->makeList(
-                                       [
-                                               'page_namespace' => $ns,
-                                               'page_title' => array_unique( $pages ),
-                                       ],
-                                       LIST_AND
-                               );
-                       }
-
-                       $fields = [ 'page_id', 'page_namespace', 'page_title',
-                               'page_is_redirect', 'page_len', 'page_latest' ];
-
-                       if ( $wgContentHandlerUseDB ) {
-                               $fields[] = 'page_content_model';
-                       }
-                       if ( $wgPageLanguageUseDB ) {
-                               $fields[] = 'page_lang';
-                       }
+               if ( !$lb->isEmpty() ) {
+                       $fields = array_merge(
+                               LinkCache::getSelectFields(),
+                               [ 'page_namespace', 'page_title' ]
+                       );
 
                        $res = $dbr->select(
                                'page',
                                $fields,
-                               $dbr->makeList( $where, LIST_OR ),
+                               $lb->constructSet( 'page', $dbr ),
                                __METHOD__
                        );
 
@@ -463,7 +449,7 @@ class LinkHolderArray {
         * @param array $colours
         */
        protected function doVariants( &$colours ) {
-               global $wgContLang, $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+               global $wgContLang;
                $linkBatch = new LinkBatch();
                $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
                $output = $this->parent->getOutput();
@@ -513,9 +499,6 @@ class LinkHolderArray {
                                }
 
                                $variantTitle = Title::makeTitle( $ns, $textVariant );
-                               if ( is_null( $variantTitle ) ) {
-                                       continue;
-                               }
 
                                // Self-link checking for mixed/different variant titles. At this point, we
                                // already know the exact title does not exist, so the link cannot be to a
@@ -552,15 +535,10 @@ class LinkHolderArray {
                if ( !$linkBatch->isEmpty() ) {
                        // construct query
                        $dbr = wfGetDB( DB_SLAVE );
-                       $fields = [ 'page_id', 'page_namespace', 'page_title',
-                               'page_is_redirect', 'page_len', 'page_latest' ];
-
-                       if ( $wgContentHandlerUseDB ) {
-                               $fields[] = 'page_content_model';
-                       }
-                       if ( $wgPageLanguageUseDB ) {
-                               $fields[] = 'page_lang';
-                       }
+                       $fields = array_merge(
+                               LinkCache::getSelectFields(),
+                               [ 'page_namespace', 'page_title' ]
+                       );
 
                        $varRes = $dbr->select( 'page',
                                $fields,
index c168aa6..4ed176c 100644 (file)
@@ -50,7 +50,7 @@ class StripState {
                        'nowiki' => [],
                        'general' => []
                ];
-               $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f]+)" . Parser::MARKER_SUFFIX . '/';
+               $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
                $this->circularRefGuard = [];
        }
 
index d7aee5b..b3776bd 100644 (file)
@@ -105,11 +105,15 @@ class PasswordPolicyChecks {
 
                $status = Status::newGood();
                $username = $user->getName();
-               if ( $policyVal
-                       && isset( $blockedLogins[$username] )
-                       && $password == $blockedLogins[$username]
-               ) {
-                       $status->error( 'password-login-forbidden' );
+               if ( $policyVal ) {
+                       if ( isset( $blockedLogins[$username] ) && $password == $blockedLogins[$username] ) {
+                               $status->error( 'password-login-forbidden' );
+                       }
+
+                       // Example from ApiChangeAuthenticationRequest
+                       if ( $password === 'ExamplePassword' ) {
+                               $status->error( 'password-login-forbidden' );
+                       }
                }
                return $status;
        }
index 7c4fde4..8fc0b77 100644 (file)
@@ -52,9 +52,9 @@
  */
 class ProfilerXhprof extends Profiler {
        /**
-        * @var Xhprof $xhprof
+        * @var XhprofData|null $xhprofData
         */
-       protected $xhprof;
+       protected $xhprofData;
 
        /**
         * Profiler for explicit, arbitrary, frame labels
@@ -68,10 +68,24 @@ class ProfilerXhprof extends Profiler {
         */
        public function __construct( array $params = [] ) {
                parent::__construct( $params );
-               $this->xhprof = new Xhprof( $params );
+
+               $flags = isset( $params['flags'] ) ? $params['flags'] : 0;
+               $options = isset( $params['exclude'] )
+                       ? [ 'ignored_functions' => $params['exclude'] ] : [];
+               Xhprof::enable( $flags, $options );
                $this->sprofiler = new SectionProfiler();
        }
 
+       /**
+        * @return XhprofData
+        */
+       public function getXhprofData() {
+               if ( !$this->xhprofData ) {
+                       $this->xhprofData = new XhprofData( Xhprof::disable(), $this->params );
+               }
+               return $this->xhprofData;
+       }
+
        public function scopedProfileIn( $section ) {
                $key = 'section.' . ltrim( $section, '.' );
                return $this->sprofiler->scopedProfileIn( $key );
@@ -112,7 +126,7 @@ class ProfilerXhprof extends Profiler {
        }
 
        public function getFunctionStats() {
-               $metrics = $this->xhprof->getCompleteMetrics();
+               $metrics = $this->getXhprofData()->getCompleteMetrics();
                $profile = [];
 
                $main = null; // units in ms
@@ -216,6 +230,6 @@ class ProfilerXhprof extends Profiler {
         * @return array
         */
        public function getRawData() {
-               return $this->xhprof->getRawData();
+               return $this->getXhprofData()->getRawData();
        }
 }
index 415e664..78f9370 100644 (file)
@@ -23,6 +23,8 @@ class ExtensionProcessor implements Processor {
                'AvailableRights',
                'ContentHandlers',
                'ConfigRegistry',
+               'SessionProviders',
+               'AuthManagerAutoConfig',
                'CentralIdLookupProviders',
                'RateLimits',
                'RecentChangesFlags',
@@ -67,6 +69,7 @@ class ExtensionProcessor implements Processor {
                'wgNamespaceProtection' => 'array_plus',
                'wgCapitalLinkOverrides' => 'array_plus',
                'wgRateLimits' => 'array_plus_2d',
+               'wgAuthManagerAutoConfig' => 'array_plus_2d',
        ];
 
        /**
index 8e0239a..85fc53d 100644 (file)
@@ -227,15 +227,17 @@ class ResourceLoaderContext {
         * Get the possibly-cached User object for the specified username
         *
         * @since 1.25
-        * @return User|bool false if a valid object cannot be created
+        * @return User
         */
        public function getUserObj() {
                if ( $this->userObj === null ) {
                        $username = $this->getUser();
                        if ( $username ) {
-                               $this->userObj = User::newFromName( $username );
+                               // Use provided username if valid, fallback to anonymous user
+                               $this->userObj = User::newFromName( $username ) ?: new User;
                        } else {
-                               $this->userObj = new User; // Anonymous user
+                               // Anonymous user
+                               $this->userObj = new User;
                        }
                }
 
diff --git a/includes/resourceloader/ResourceLoaderUploadDialogModule.php b/includes/resourceloader/ResourceLoaderUploadDialogModule.php
new file mode 100644 (file)
index 0000000..52e2210
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * 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
+ */
+
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * @since 1.27
+ */
+class ResourceLoaderUploadDialogModule extends ResourceLoaderModule {
+
+       protected $targets = [ 'desktop', 'mobile' ];
+
+       public function getScript( ResourceLoaderContext $context ) {
+               $config = $context->getResourceLoader()->getConfig();
+               return ResourceLoader::makeConfigSetScript( [
+                       'wgUploadDialog' => $config->get( 'UploadDialog' ),
+               ] );
+       }
+
+       public function enableModuleContentVersion() {
+               return true;
+       }
+}
index e2a8e41..b225185 100644 (file)
@@ -40,7 +40,7 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
                }
 
                $user = $context->getUserObj();
-               if ( !$user || $user->isAnon() ) {
+               if ( $user->isAnon() ) {
                        return [];
                }
 
index d584165..c38f8d8 100644 (file)
@@ -43,7 +43,7 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
                }
 
                $user = $context->getUserObj();
-               if ( !$user || $user->isAnon() ) {
+               if ( $user->isAnon() ) {
                        return [];
                }
 
index 8ce3174..3df0dae 100644 (file)
@@ -217,19 +217,13 @@ class CookieSessionProvider extends SessionProvider {
                        [ '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 );
+                               $expirationDuration = $this->getLoginCookieExpiration( $key );
+                               $expiration = $expirationDuration ? $expirationDuration + time() : null;
+                               $response->setCookie( $key, (string)$value, $expiration, $options );
                        }
                }
 
@@ -276,7 +270,13 @@ class CookieSessionProvider extends SessionProvider {
        ) {
                $response = $request->response();
                if ( $set ) {
-                       $response->setCookie( 'forceHTTPS', 'true', $backend->shouldRememberUser() ? 0 : null,
+                       if ( $backend->shouldRememberUser() ) {
+                               $expirationDuration = $this->getLoginCookieExpiration( 'forceHTTPS' );
+                               $expiration = $expirationDuration ? $expirationDuration + time() : null;
+                       } else {
+                               $expiration = null;
+                       }
+                       $response->setCookie( 'forceHTTPS', 'true', $expiration,
                                [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
                } else {
                        $response->clearCookie( 'forceHTTPS',
@@ -396,4 +396,24 @@ class CookieSessionProvider extends SessionProvider {
                return wfMessage( 'sessionprovider-nocookies' );
        }
 
+       public function getRememberUserDuration() {
+               return min( $this->getLoginCookieExpiration( 'UserID' ),
+                       $this->getLoginCookieExpiration( 'Token' ) ) ?: null;
+       }
+
+       /**
+        * Returns the lifespan of the login cookies, in seconds. 0 means until the end of the session.
+        * @param string $cookieName
+        * @return int Cookie expiration time in seconds; 0 for session cookies
+        */
+       protected function getLoginCookieExpiration( $cookieName ) {
+               $normalExpiration = $this->config->get( 'CookieExpiration' );
+               $extendedExpiration = $this->config->get( 'ExtendedLoginCookieExpiration' );
+               $extendedCookies = $this->config->get( 'ExtendedLoginCookies' );
+
+               if ( !in_array( $cookieName, $extendedCookies, true ) ) {
+                       return (int)$normalExpiration;
+               }
+               return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
+       }
 }
index 2f6e133..1cab3d3 100644 (file)
@@ -113,7 +113,7 @@ abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
 
                $options = $this->sessionCookieOptions;
                if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
-                       $response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
+                       $response->setCookie( 'forceHTTPS', 'true', null,
                                [ 'prefix' => '', 'secure' => false ] + $options );
                        $options['secure'] = true;
                }
index 777d3d6..c3481e8 100644 (file)
@@ -302,12 +302,13 @@ final class SessionManager implements SessionManagerInterface {
        }
 
        public function invalidateSessionsForUser( User $user ) {
-               global $wgAuth;
-
                $user->setToken();
                $user->saveSettings();
 
-               $wgAuth->getUserInstance( $user )->resetAuthToken();
+               $authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] );
+               if ( $authUser ) {
+                       $authUser->resetAuthToken();
+               }
 
                foreach ( $this->getProviders() as $provider ) {
                        $provider->invalidateSessionsForUser( $user );
@@ -370,14 +371,23 @@ final class SessionManager implements SessionManagerInterface {
        /**
         * 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.
+        * @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead
         * @param User $user User to auto-create
         * @return bool Success
         */
        public static function autoCreateUser( User $user ) {
-               global $wgAuth;
+               global $wgAuth, $wgDisableAuthManager;
+
+               // @codeCoverageIgnoreStart
+               if ( !$wgDisableAuthManager ) {
+                       wfDeprecated( __METHOD__, '1.27' );
+                       return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
+                               $user,
+                               \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSSION,
+                               false
+                       )->isGood();
+               }
+               // @codeCoverageIgnoreEnd
 
                $logger = self::singleton()->logger;
 
index ed113b7..50794d0 100644 (file)
@@ -275,6 +275,16 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         */
        abstract public function canChangeUser();
 
+       /**
+        * Returns the duration (in seconds) for which users will be remembered when
+        * Session::setRememberUser() is set. Null means setting the remember flag will
+        * have no effect (and endpoints should not offer that option).
+        * @return int|null
+        */
+       public function getRememberUserDuration() {
+               return null;
+       }
+
        /**
         * Notification that the session ID was reset
         *
index e5dc59f..10d9cb9 100644 (file)
@@ -663,19 +663,35 @@ class SkinTemplate extends Skin {
                        $loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink
                                ? 'nav-login-createaccount'
                                : 'pt-login';
-                       $is_signup = $request->getText( 'type' ) == 'signup';
 
-                       $login_url = [
-                               'text' => $this->msg( $loginlink )->text(),
-                               'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
-                               'active' => $title->isSpecial( 'Userlogin' )
-                                       && ( $loginlink == 'nav-login-createaccount' || !$is_signup ),
-                       ];
-                       $createaccount_url = [
-                               'text' => $this->msg( 'pt-createaccount' )->text(),
-                               'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
-                               'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
-                       ];
+                       // TODO remove this after AuthManager is stable
+                       global $wgDisableAuthManager;
+                       if ( $wgDisableAuthManager ) {
+                               $is_signup = $request->getText( 'type' ) == 'signup';
+                               $login_url = [
+                                       'text' => $this->msg( $loginlink )->text(),
+                                       'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
+                                       'active' => $title->isSpecial( 'Userlogin' )
+                                               && ( $loginlink == 'nav-login-createaccount' || !$is_signup ),
+                               ];
+                               $createaccount_url = [
+                                       'text' => $this->msg( 'pt-createaccount' )->text(),
+                                       'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
+                                       'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
+                               ];
+                       } else {
+                               $login_url = [
+                                       'text' => $this->msg( $loginlink )->text(),
+                                       'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
+                                       'active' => $title->isSpecial( 'Userlogin' ) ||
+                                               $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink,
+                               ];
+                               $createaccount_url = [
+                                       'text' => $this->msg( 'pt-createaccount' )->text(),
+                                       'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ),
+                                       'active' => $title->isSpecial( 'CreateAccount' ),
+                               ];
+                       }
 
                        // No need to show Talk and Contributions to anons if they can't contribute!
                        if ( User::groupHasPermission( '*', 'edit' ) ) {
diff --git a/includes/specialpage/AuthManagerSpecialPage.php b/includes/specialpage/AuthManagerSpecialPage.php
new file mode 100644 (file)
index 0000000..7866c12
--- /dev/null
@@ -0,0 +1,744 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
+use MediaWiki\Session\Token;
+
+/**
+ * A special page subclass for authentication-related special pages. It generates a form from
+ * a set of AuthenticationRequest objects, submits the result to AuthManager and
+ * partially handles the response.
+ */
+abstract class AuthManagerSpecialPage extends SpecialPage {
+       /** @var string[] The list of actions this special page deals with. Subclasses should override
+        * this. */
+       protected static $allowedActions = [
+               AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
+               AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
+               AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
+               AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
+       ];
+
+       /** @var array Customized messages */
+       protected static $messages = [];
+
+       /** @var string one of the AuthManager::ACTION_* constants. */
+       protected $authAction;
+
+       /** @var AuthenticationRequest[] */
+       protected $authRequests;
+
+       /** @var string Subpage of the special page. */
+       protected $subPage;
+
+       /** @var bool True if the current request is a result of returning from a redirect flow. */
+       protected $isReturn;
+
+       /** @var WebRequest|null If set, will be used instead of the real request. Used for redirection. */
+       protected $savedRequest;
+
+       /**
+        * Change the form descriptor that determines how a field will look in the authentication form.
+        * Called from fieldInfoToFormDescriptor().
+        * @param AuthenticationRequest[] $requests
+        * @param string $fieldInfo Field information array (union of all
+        *    AuthenticationRequest::getFieldInfo() responses).
+        * @param array $formDescriptor HTMLForm descriptor. The special key 'weight' can be set to
+        *    change the order of the fields.
+        * @param string $action Authentication type (one of the AuthManager::ACTION_* constants)
+        * @return bool
+        */
+       public function onAuthChangeFormFields(
+               array $requests, array $fieldInfo, array &$formDescriptor, $action
+       ) {
+               return true;
+       }
+
+       protected function getLoginSecurityLevel() {
+               return $this->getName();
+       }
+
+       public function getRequest() {
+               return $this->savedRequest ?: $this->getContext()->getRequest();
+       }
+
+       /**
+        * Override the POST data, GET data from the real request is preserved.
+        *
+        * Used to preserve POST data over a HTTP redirect.
+        *
+        * @param array $data
+        * @param bool $wasPosted
+        */
+       protected function setRequest( array $data, $wasPosted = null ) {
+               $request = $this->getContext()->getRequest();
+               if ( $wasPosted === null ) {
+                       $wasPosted = $request->wasPosted();
+               }
+               $this->savedRequest = new DerivativeRequest( $request, $data + $request->getQueryValues(),
+                       $wasPosted );
+       }
+
+       protected function beforeExecute( $subPage ) {
+               $this->getOutput()->disallowUserJs();
+
+               return $this->handleReturnBeforeExecute( $subPage )
+                       && $this->handleReauthBeforeExecute( $subPage );
+       }
+
+       /**
+        * Handle redirection from the /return subpage.
+        *
+        * This is used in the redirect flow where we need
+        * to be able to process data that was sent via a GET request. We set the /return subpage as
+        * the reentry point so we know we need to treat GET as POST, but we don't want to handle all
+        * future GETs as POSTs so we need to normalize the URL. (Also we don't want to show any
+        * received parameters around in the URL; they are ugly and might be sensitive.)
+        *
+        * Thus when on the /return subpage, we stash the request data in the session, redirect, then
+        * use the session to detect that we have been redirected, recover the data and replace the
+        * real WebRequest with a fake one that contains the saved data.
+        *
+        * @param string $subPage
+        * @return bool False if execution should be stopped.
+        */
+       protected function handleReturnBeforeExecute( $subPage ) {
+               $authManager = AuthManager::singleton();
+               $key = 'AuthManagerSpecialPage:return:' . $this->getName();
+
+               if ( $subPage === 'return' ) {
+                       $this->loadAuth( $subPage );
+                       $preservedParams = $this->getPreservedParams( false );
+
+                       // FIXME save POST values only from request
+                       $authData = array_diff_key( $this->getRequest()->getValues(),
+                               $preservedParams, [ 'title' => 1 ] );
+                       $authManager->setAuthenticationSessionData( $key, $authData );
+
+                       $url = $this->getPageTitle()->getFullURL( $preservedParams, false, PROTO_HTTPS );
+                       $this->getOutput()->redirect( $url );
+                       return false;
+               }
+
+               $authData = $authManager->getAuthenticationSessionData( $key );
+               if ( $authData ) {
+                       $authManager->removeAuthenticationSessionData( $key );
+                       $this->isReturn = true;
+                       $this->setRequest( $authData, true );
+               }
+
+               return true;
+       }
+
+       /**
+        * Handle redirection when the user needs to (re)authenticate.
+        *
+        * Send the user to the login form if needed; in case the request was a POST, stash in the
+        * session and simulate it once the user gets back.
+        *
+        * @param string $subPage
+        * @return bool False if execution should be stopped.
+        * @throws ErrorPageError When the user is not allowed to use this page.
+        */
+       protected function handleReauthBeforeExecute( $subPage ) {
+               $authManager = AuthManager::singleton();
+               $request = $this->getRequest();
+               $key = 'AuthManagerSpecialPage:reauth:' . $this->getName();
+
+               $securityLevel = $this->getLoginSecurityLevel();
+               if ( $securityLevel ) {
+                       $securityStatus = AuthManager::singleton()
+                               ->securitySensitiveOperationStatus( $securityLevel );
+                       if ( $securityStatus === AuthManager::SEC_REAUTH ) {
+                               $queryParams = array_diff_key( $request->getQueryValues(), [ 'title' => true ] );
+
+                               if ( $request->wasPosted() ) {
+                                       // unique ID in case the same special page is open in multiple browser tabs
+                                       $uniqueId = MWCryptRand::generateHex( 6 );
+                                       $key = $key . ':' . $uniqueId;
+
+                                       $queryParams = [ 'authUniqueId' => $uniqueId ] + $queryParams;
+                                       $authData = array_diff_key( $request->getValues(),
+                                                       $this->getPreservedParams( false ), [ 'title' => 1 ] );
+                                       $authManager->setAuthenticationSessionData( $key, $authData );
+                               }
+
+                               $title = SpecialPage::getTitleFor( 'Userlogin' );
+                               $url = $title->getFullURL( [
+                                       'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
+                                       'returntoquery' => wfArrayToCgi( $queryParams ),
+                                       'force' => $securityLevel,
+                               ], false, PROTO_HTTPS );
+
+                               $this->getOutput()->redirect( $url );
+                               return false;
+                       } elseif ( $securityStatus !== AuthManager::SEC_OK ) {
+                               throw new ErrorPageError( 'cannotauth-not-allowed-title', 'cannotauth-not-allowed' );
+                       }
+               }
+
+               $uniqueId = $request->getVal( 'authUniqueId' );
+               if ( $uniqueId ) {
+                       $key = $key . ':' . $uniqueId;
+                       $authData = $authManager->getAuthenticationSessionData( $key );
+                       if ( $authData ) {
+                               $authManager->removeAuthenticationSessionData( $key );
+                               $this->setRequest( $authData, true );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Get the default action for this special page, if none is given via URL/POST data.
+        * Subclasses should override this (or override loadAuth() so this is never called).
+        * @param string $subPage Subpage of the special page.
+        * @return string an AuthManager::ACTION_* constant.
+        */
+       protected function getDefaultAction( $subPage ) {
+               throw new BadMethodCallException( 'Subclass did not implement getDefaultAction' );
+       }
+
+       /**
+        * Return custom message key.
+        * Allows subclasses to customize messages.
+        * @return string
+        */
+       protected function messageKey( $defaultKey ) {
+               return array_key_exists( $defaultKey, static::$messages )
+                       ? static::$messages[$defaultKey] : $defaultKey;
+       }
+
+       /**
+        * Allows blacklisting certain request types.
+        * @return array A list of AuthenticationRequest subclass names
+        */
+       protected function getRequestBlacklist() {
+               return [];
+       }
+
+       /**
+        * Load or initialize $authAction, $authRequests and $subPage.
+        * Subclasses should call this from execute() or otherwise ensure the variables are initialized.
+        * @param string $subPage Subpage of the special page.
+        * @param string $authAction Override auth action specified in request (this is useful
+        *    when the form needs to be changed from <action> to <action>_CONTINUE after a successful
+        *    authentication step)
+        * @param bool $reset Regenerate the requests even if a cached version is available
+        */
+       protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
+               // Do not load if already loaded, to cut down on the number of getAuthenticationRequests
+               // calls. This is important for requests which have hidden information so any
+               // getAuthenticationRequests call would mean putting data into some cache.
+               if (
+                       !$reset && $this->subPage === $subPage && $this->authAction
+                       && ( !$authAction || $authAction === $this->authAction )
+               ) {
+                       return;
+               }
+
+               $request = $this->getRequest();
+               $this->subPage = $subPage;
+               $this->authAction = $authAction ?: $request->getText( 'authAction' );
+               if ( !in_array( $this->authAction, static::$allowedActions, true ) ) {
+                       $this->authAction = $this->getDefaultAction( $subPage );
+                       if ( $request->wasPosted() ) {
+                               $continueAction = $this->getContinueAction( $this->authAction );
+                               if ( in_array( $continueAction, static::$allowedActions, true ) ) {
+                                       $this->authAction = $continueAction;
+                               }
+                       }
+               }
+
+               $allReqs = AuthManager::singleton()->getAuthenticationRequests(
+                       $this->authAction, $this->getUser() );
+               $this->authRequests = array_filter( $allReqs, function ( $req ) use ( $subPage ) {
+                       return !in_array( get_class( $req ), $this->getRequestBlacklist(), true );
+               } );
+       }
+
+       /**
+        * Returns true if this is not the first step of the authentication.
+        * @return bool
+        */
+       protected function isContinued() {
+               return in_array( $this->authAction, [
+                       AuthManager::ACTION_LOGIN_CONTINUE,
+                       AuthManager::ACTION_CREATE_CONTINUE,
+                       AuthManager::ACTION_LINK_CONTINUE,
+               ], true );
+       }
+
+       /**
+        * Gets the _CONTINUE version of an action.
+        * @param string $action An AuthManager::ACTION_* constant.
+        * @return string An AuthManager::ACTION_*_CONTINUE constant.
+        */
+       protected function getContinueAction( $action ) {
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                               $action = AuthManager::ACTION_LOGIN_CONTINUE;
+                               break;
+                       case AuthManager::ACTION_CREATE:
+                               $action = AuthManager::ACTION_CREATE_CONTINUE;
+                               break;
+                       case AuthManager::ACTION_LINK:
+                               $action = AuthManager::ACTION_LINK_CONTINUE;
+                               break;
+               }
+               return $action;
+       }
+
+       /**
+        * Checks whether AuthManager is ready to perform the action.
+        * ACTION_CHANGE needs special verification (AuthManager::allowsAuthenticationData*) which is
+        * the caller's responsibility.
+        * @param string $action One of the AuthManager::ACTION_* constants in static::$allowedActions
+        * @return bool
+        * @throws LogicException if $action is invalid
+        */
+       protected function isActionAllowed( $action ) {
+               $authManager = AuthManager::singleton();
+               if ( !in_array( $action, static::$allowedActions, true ) ) {
+                       throw new InvalidArgumentException( 'invalid action: ' . $action );
+               }
+
+               // calling getAuthenticationRequests can be expensive, avoid if possible
+               $requests = ( $action === $this->authAction ) ? $this->authRequests
+                       : $authManager->getAuthenticationRequests( $action );
+               if ( !$requests ) {
+                       // no provider supports this action in the current state
+                       return false;
+               }
+
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                       case AuthManager::ACTION_LOGIN_CONTINUE:
+                               return $authManager->canAuthenticateNow();
+                       case AuthManager::ACTION_CREATE:
+                       case AuthManager::ACTION_CREATE_CONTINUE:
+                               return $authManager->canCreateAccounts();
+                       case AuthManager::ACTION_LINK:
+                       case AuthManager::ACTION_LINK_CONTINUE:
+                               return $authManager->canLinkAccounts();
+                       case AuthManager::ACTION_CHANGE:
+                       case AuthManager::ACTION_REMOVE:
+                       case AuthManager::ACTION_UNLINK:
+                               return true;
+                       default:
+                               // should never reach here but makes static code analyzers happy
+                               throw new InvalidArgumentException( 'invalid action: ' . $action );
+               }
+       }
+
+       /**
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @param AuthenticationRequest[] $requests
+        * @return AuthenticationResponse
+        * @throws LogicException if $action is invalid
+        */
+       protected function performAuthenticationStep( $action, array $requests ) {
+               if ( !in_array( $action, static::$allowedActions, true ) ) {
+                       throw new InvalidArgumentException( 'invalid action: ' . $action );
+               }
+
+               $authManager = AuthManager::singleton();
+               $returnToUrl = $this->getPageTitle( 'return' )
+                       ->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
+
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                               return $authManager->beginAuthentication( $requests, $returnToUrl );
+                       case AuthManager::ACTION_LOGIN_CONTINUE:
+                               return $authManager->continueAuthentication( $requests );
+                       case AuthManager::ACTION_CREATE:
+                               return $authManager->beginAccountCreation( $this->getUser(), $requests,
+                                       $returnToUrl );
+                       case AuthManager::ACTION_CREATE_CONTINUE:
+                               return $authManager->continueAccountCreation( $requests );
+                       case AuthManager::ACTION_LINK:
+                               return $authManager->beginAccountLink( $this->getUser(), $requests, $returnToUrl );
+                       case AuthManager::ACTION_LINK_CONTINUE:
+                               return $authManager->continueAccountLink( $requests );
+                       case AuthManager::ACTION_CHANGE:
+                       case AuthManager::ACTION_REMOVE:
+                       case AuthManager::ACTION_UNLINK:
+                               if ( count( $requests ) > 1 ) {
+                                       throw new InvalidArgumentException( 'only one auth request can be changed at a time' );
+                               } elseif ( !$requests ) {
+                                       throw new InvalidArgumentException( 'no auth request' );
+                               }
+                               $req = reset( $requests );
+                               $status = $authManager->allowsAuthenticationDataChange( $req );
+                               Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] );
+                               if ( !$status->isOK() ) {
+                                       return AuthenticationResponse::newFail( $status->getMessage() );
+                               }
+                               $authManager->changeAuthenticationData( $req );
+                               return AuthenticationResponse::newPass();
+                       default:
+                               // should never reach here but makes static code analyzers happy
+                               throw new InvalidArgumentException( 'invalid action: ' . $action );
+               }
+       }
+
+       /**
+        * Attempts to do an authentication step with the submitted data.
+        * Subclasses should probably call this from execute().
+        * @return false|Status
+        *    - false if there was no submit at all
+        *    - a good Status wrapping an AuthenticationResponse if the form submit was successful.
+        *      This does not necessarily mean that the authentication itself was successful; see the
+        *      response for that.
+        *    - a bad Status for form errors.
+        */
+       protected function trySubmit() {
+               $status = false;
+
+               $form = $this->getAuthForm( $this->authRequests, $this->authAction );
+               $form->setSubmitCallback( [ $this, 'handleFormSubmit' ] );
+
+               if ( $this->getRequest()->wasPosted() ) {
+                       // handle tokens manually; $form->tryAuthorizedSubmit only works for logged-in users
+                       $requestTokenValue = $this->getRequest()->getVal( $this->getTokenName() );
+                       $sessionToken = $this->getToken();
+                       if ( $sessionToken->wasNew() ) {
+                               return Status::newFatal( $this->messageKey( 'authform-newtoken' ) );
+                       } elseif ( !$requestTokenValue ) {
+                               return Status::newFatal( $this->messageKey( 'authform-notoken' ) );
+                       } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
+                               return Status::newFatal( $this->messageKey( 'authform-wrongtoken' ) );
+                       }
+
+                       $form->prepareForm();
+                       $status = $form->trySubmit();
+
+                       // HTMLForm submit return values are a mess; let's ensure it is false or a Status
+                       // FIXME this probably should be in HTMLForm
+                       if ( $status === true ) {
+                               // not supposed to happen since our submit handler should always return a Status
+                               throw new UnexpectedValueException( 'HTMLForm::trySubmit() returned true' );
+                       } elseif ( $status === false ) {
+                               // form was not submitted; nothing to do
+                       } elseif ( $status instanceof Status ) {
+                               // already handled by the form; nothing to do
+                       } elseif ( $status instanceof StatusValue ) {
+                               // in theory not an allowed return type but nothing stops the submit handler from
+                               // accidentally returning it so best check and fix
+                               $status = Status::wrap( $status );
+                       } elseif ( is_string( $status ) ) {
+                               $status = Status::newFatal( new RawMessage( '$1', $status ) );
+                       } elseif ( is_array( $status ) ) {
+                               if ( is_string( reset( $status ) ) ) {
+                                       $status = call_user_func_array( 'Status::newFatal', $status );
+                               } elseif ( is_array( reset( $status ) ) ) {
+                                       $status = Status::newGood();
+                                       foreach ( $status as $message ) {
+                                               call_user_func_array( [ $status, 'fatal' ], $message );
+                                       }
+                               } else {
+                                       throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return value: '
+                                               . 'first element of array is ' . gettype( reset( $status ) ) );
+                               }
+                       } else {
+                               // not supposed to happen but HTMLForm does not actually verify the return type
+                               // from the submit callback; better safe then sorry
+                               throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return type: '
+                                       . gettype( $status ) );
+                       }
+
+                       if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
+                               // This is awkward. There was a form validation error, which means the data was not
+                               // passed to AuthManager. Normally we would display the form with an error message,
+                               // but for the data we received via the redirect flow that would not be helpful at all.
+                               // Let's just submit the data to AuthManager directly instead.
+                               LoggerFactory::getInstance( 'authmanager' )
+                                       ->warning( 'Validation error on return', [ 'data' => $form->mFieldData,
+                                               'status' => $status->getWikiText() ] );
+                               $status = $this->handleFormSubmit( $form->mFieldData );
+                       }
+               }
+
+               $changeActions = [
+                       AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
+               ];
+               if ( in_array( $this->authAction, $changeActions, true ) && $status && !$status->isOK() ) {
+                       Hooks::run( 'ChangeAuthenticationDataAudit', [ reset( $this->authRequests ), $status ] );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Submit handler callback for HTMLForm
+        * @private
+        * @param $data array Submitted data
+        * @return Status
+        */
+       public function handleFormSubmit( $data ) {
+               $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
+               $response = $this->performAuthenticationStep( $this->authAction, $requests );
+
+               // we can't handle FAIL or similar as failure here since it might require changing the form
+               return Status::newGood( $response );
+       }
+
+       /**
+        * Returns URL query parameters which can be used to reload the page (or leave and return) while
+        * preserving all information that is necessary for authentication to continue. These parameters
+        * will be preserved in the action URL of the form and in the return URL for redirect flow.
+        * @param bool $withToken Include CSRF token
+        * @return array
+        */
+       protected function getPreservedParams( $withToken = false ) {
+               $params = [];
+               if ( $this->authAction !== $this->getDefaultAction( $this->subPage ) ) {
+                       $params['authAction'] = $this->getContinueAction( $this->authAction );
+               }
+               if ( $withToken ) {
+                       $params[$this->getTokenName()] = $this->getToken()->toString();
+               }
+               return $params;
+       }
+
+       /**
+        * Generates a HTMLForm descriptor array from a set of authentication requests.
+        * @param AuthenticationRequest[] $requests
+        * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants)
+        * @return array
+        */
+       protected function getAuthFormDescriptor( $requests, $action ) {
+               $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
+               $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $action );
+
+               $this->addTabIndex( $formDescriptor );
+
+               return $formDescriptor;
+       }
+
+       /**
+        * @param AuthenticationRequest[] $requests
+        * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants)
+        * @return HTMLForm
+        */
+       protected function getAuthForm( array $requests, $action ) {
+               $formDescriptor = $this->getAuthFormDescriptor( $requests, $action );
+               $context = $this->getContext();
+               if ( $context->getRequest() !== $this->getRequest() ) {
+                       // We have overridden the request, need to make sure the form uses that too.
+                       $context = new DerivativeContext( $this->getContext() );
+                       $context->setRequest( $this->getRequest() );
+               }
+               $form = HTMLForm::factory( 'ooui', $formDescriptor, $context );
+               $form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) );
+               $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
+               $form->addHiddenField( 'authAction', $this->authAction );
+               $form->suppressDefaultSubmit( !$this->needsSubmitButton( $formDescriptor ) );
+
+               return $form;
+       }
+
+       /**
+        * Display the form.
+        * @param false|Status|StatusValue $status A form submit status, as in HTMLForm::trySubmit()
+        */
+       protected function displayForm( $status ) {
+               if ( $status instanceof StatusValue ) {
+                       $status = Status::wrap( $status );
+               }
+               $form = $this->getAuthForm( $this->authRequests, $this->authAction );
+               $form->prepareForm()->displayForm( $status );
+       }
+
+       /**
+        * Returns true if the form has fields which take values. If all available providers use the
+        * redirect flow, the form might contain nothing but submit buttons, in which case we should
+        * not add an extra submit button which does nothing.
+        *
+        * @param array $formDescriptor A HTMLForm descriptor
+        * @return bool
+        */
+       protected function needsSubmitButton( $formDescriptor ) {
+               return (bool)array_filter( $formDescriptor, function ( $item ) {
+                       $class = false;
+                       if ( array_key_exists( 'class', $item ) ) {
+                               $class = $item['class'];
+                       } elseif ( array_key_exists( 'type', $item ) ) {
+                               $class = HTMLForm::$typeMappings[$item['type']];
+                       }
+                       return !in_array( $class, [ 'HTMLInfoField', 'HTMLSubmitField' ], true );
+               } );
+       }
+
+       /**
+        * Adds a sequential tabindex starting from 1 to all form elements. This way the user can
+        * use the tab key to traverse the form without having to step through all links and such.
+        * @param $formDescriptor
+        */
+       protected function addTabIndex( &$formDescriptor ) {
+               $i = 1;
+               foreach ( $formDescriptor as $field => &$definition ) {
+                       $class = false;
+                       if ( array_key_exists( 'class', $definition ) ) {
+                               $class = $definition['class'];
+                       } elseif ( array_key_exists( 'type', $definition ) ) {
+                               $class = HTMLForm::$typeMappings[$definition['type']];
+                       }
+                       if ( $class !== 'HTMLInfoField' ) {
+                               $definition['tabindex'] = $i;
+                               $i++;
+                       }
+               }
+       }
+
+       /**
+        * Returns the CSRF token.
+        * @return Token
+        */
+       protected function getToken() {
+               return $this->getRequest()->getSession()->getToken( 'AuthManagerSpecialPage:'
+                       . $this->getName() );
+       }
+
+       /**
+        * Returns the name of the CSRF token (under which it should be found in the POST or GET data).
+        * @return string
+        */
+       protected function getTokenName() {
+               return 'wpAuthToken';
+       }
+
+       /**
+        * Turns a field info array into a form descriptor. Behavior can be modified by the
+        * AuthChangeFormFields hook.
+        * @param AuthenticationRequest[] $requests
+        * @param array $fieldInfo Field information, in the format used by
+        *   AuthenticationRequest::getFieldInfo()
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @return array A form descriptor that can be passed to HTMLForm
+        */
+       protected function fieldInfoToFormDescriptor( array $requests, array $fieldInfo, $action ) {
+               $formDescriptor = [];
+               foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
+                       $formDescriptor[$fieldName] = self::mapSingleFieldInfo( $singleFieldInfo, $fieldName );
+               }
+
+               $requestSnapshot = serialize( $requests );
+               $this->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
+               \Hooks::run( 'AuthChangeFormFields', [ $requests, $fieldInfo, &$formDescriptor, $action ] );
+               if ( $requestSnapshot !== serialize( $requests ) ) {
+                       LoggerFactory::getInstance( 'authentication' )->warning(
+                               'AuthChangeFormFields hook changed auth requests' );
+               }
+
+               // Process the special 'weight' property, which is a way for AuthChangeFormFields hook
+               // subscribers (who only see one field at a time) to influence ordering.
+               self::sortFormDescriptorFields( $formDescriptor );
+
+               return $formDescriptor;
+       }
+
+       /**
+        * Maps an authentication field configuration for a single field (as returned by
+        * AuthenticationRequest::getFieldInfo()) to a HTMLForm field descriptor.
+        * @param array $singleFieldInfo
+        * @return array
+        */
+       protected static function mapSingleFieldInfo( $singleFieldInfo, $fieldName ) {
+               $type = self::mapFieldInfoTypeToFormDescriptorType( $singleFieldInfo['type'] );
+               $descriptor = [
+                       'type' => $type,
+                       // Do not prefix input name with 'wp'. This is important for the redirect flow.
+                       'name' => $fieldName,
+               ];
+
+               if ( $type === 'submit' && isset( $singleFieldInfo['label'] ) ) {
+                       $descriptor['default'] = wfMessage( $singleFieldInfo['label'] )->plain();
+               } elseif ( $type !== 'submit' ) {
+                       $descriptor += array_filter( [
+                               // help-message is omitted as it is usually not really useful for a web interface
+                               'label-message' => self::getField( $singleFieldInfo, 'label' ),
+                       ] );
+
+                       if ( isset( $singleFieldInfo['options'] ) ) {
+                               $descriptor['options'] = array_flip( array_map( function ( $message ) {
+                                       /** @var $message Message */
+                                       return $message->parse();
+                               }, $singleFieldInfo['options'] ) );
+                       }
+
+                       if ( isset( $singleFieldInfo['value'] ) ) {
+                               $descriptor['default'] = $singleFieldInfo['value'];
+                       }
+
+                       if ( empty( $singleFieldInfo['optional'] ) ) {
+                               $descriptor['required'] = true;
+                       }
+               }
+
+               return $descriptor;
+       }
+
+       /**
+        * Sort the fields of a form descriptor by their 'weight' property. (Fields with higher weight
+        * are shown closer to the bottom; weight defaults to 0. Negative weight is allowed.)
+        * Keep order if weights are equal.
+        * @param array $formDescriptor
+        * @return array
+        */
+       protected static function sortFormDescriptorFields( array &$formDescriptor ) {
+               $i = 0;
+               foreach ( $formDescriptor as &$field ) {
+                       $field['__index'] = $i++;
+               }
+               uasort( $formDescriptor, function ( $first, $second ) {
+                       return self::getField( $first, 'weight', 0 ) - self::getField( $second, 'weight', 0 )
+                               ?: $first['__index'] - $second['__index'];
+               } );
+               foreach ( $formDescriptor as &$field ) {
+                       unset( $field['__index'] );
+               }
+       }
+
+       /**
+        * Get an array value, or a default if it does not exist.
+        * @param array $array
+        * @param string $fieldName
+        * @param mixed $default
+        * @return mixed
+        */
+       protected static function getField( array $array, $fieldName, $default = null ) {
+               if ( array_key_exists( $fieldName, $array ) ) {
+                       return $array[$fieldName];
+               } else {
+                       return $default;
+               }
+       }
+
+       /**
+        * Maps AuthenticationRequest::getFieldInfo() types to HTMLForm types
+        * @param string $type
+        * @return string
+        * @throws \LogicException
+        */
+       protected static function mapFieldInfoTypeToFormDescriptorType( $type ) {
+               $map = [
+                       'string' => 'text',
+                       'password' => 'password',
+                       'select' => 'select',
+                       'checkbox' => 'check',
+                       'multiselect' => 'multiselect',
+                       'button' => 'submit',
+                       'hidden' => 'hidden',
+                       'null' => 'info',
+               ];
+               if ( !array_key_exists( $type, $map ) ) {
+                       throw new \LogicException( 'invalid field type: ' . $type );
+               }
+               return $map[$type];
+       }
+}
diff --git a/includes/specialpage/LoginSignupSpecialPage.php b/includes/specialpage/LoginSignupSpecialPage.php
new file mode 100644 (file)
index 0000000..0e4252c
--- /dev/null
@@ -0,0 +1,1636 @@
+<?php
+/**
+ * Holds shared logic for login and account creation pages.
+ *
+ * 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
+ */
+
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\Throttler;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
+use Psr\Log\LogLevel;
+
+/**
+ * Holds shared logic for login and account creation pages.
+ *
+ * @ingroup SpecialPage
+ */
+abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
+       protected $mReturnTo;
+       protected $mPosted;
+       protected $mAction;
+       protected $mLanguage;
+       protected $mReturnToQuery;
+       protected $mToken;
+       protected $mStickHTTPS;
+       protected $mFromHTTP;
+       protected $mEntryError = '';
+       protected $mEntryErrorType = 'error';
+
+       protected $mLoaded = false;
+       protected $mSecureLoginUrl;
+
+       /** @var string */
+       protected $securityLevel;
+
+       /** @var bool True if the user if creating an account for someone else. Flag used for internal
+        * communication, only set at the very end. */
+       protected $proxyAccountCreation;
+       /** @var User FIXME another flag for passing data. */
+       protected $targetUser;
+
+       /** @var HTMLForm */
+       protected $authForm;
+
+       /** @var FakeAuthTemplate */
+       protected $fakeTemplate;
+
+       abstract protected function isSignup();
+
+       /**
+        * @param bool $direct True if the action was successful just now; false if that happened
+        *    pre-redirection (so this handler was called already)
+        * @param StatusValue|null $extraMessages
+        * @return void
+        */
+       abstract protected function successfulAction( $direct = false, $extraMessages = null );
+
+       /**
+        * Logs to the authmanager-stats channel.
+        * @param bool $success
+        * @param string|null $status Error message key
+        */
+       abstract protected function logAuthResult( $success, $status = null );
+
+       public function __construct( $name ) {
+               global $wgUseMediaWikiUIEverywhere;
+               parent::__construct( $name );
+
+               // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
+               $wgUseMediaWikiUIEverywhere = true;
+       }
+
+       /**
+        * Load data from request.
+        * @private
+        * @param string $subPage Subpage of Special:Userlogin
+        */
+       protected function load( $subPage ) {
+               global $wgSecureLogin;
+
+               if ( $this->mLoaded ) {
+                       return;
+               }
+               $this->mLoaded = true;
+
+               $request = $this->getRequest();
+
+               $this->mPosted = $request->wasPosted();
+               $this->mIsReturn = $subPage === 'return';
+               $this->mAction = $request->getVal( 'action' );
+               $this->mFromHTTP = $request->getBool( 'fromhttp', false )
+                       || $request->getBool( 'wpFromhttp', false );
+               $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
+                       || $request->getBool( 'wpForceHttps', false );
+               $this->mLanguage = $request->getText( 'uselang' );
+               $this->mReturnTo = $request->getVal( 'returnto', '' );
+               $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
+
+               $securityLevel = $this->getRequest()->getText( 'force' );
+               if (
+                       $securityLevel && AuthManager::singleton()->securitySensitiveOperationStatus(
+                               $securityLevel ) === AuthManager::SEC_REAUTH
+               ) {
+                       $this->securityLevel = $securityLevel;
+               }
+
+               $this->loadAuth( $subPage );
+
+               $this->mToken = $request->getVal( $this->getTokenName() );
+
+               // Show an error or warning passed on from a previous page
+               $entryError = $this->msg( $request->getVal( 'error', '' ) );
+               $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
+               // bc: provide login link as a parameter for messages where the translation
+               // was not updated
+               $loginreqlink = Linker::linkKnown(
+                       $this->getPageTitle(),
+                       $this->msg( 'loginreqlink' )->escaped(),
+                       [],
+                       [
+                               'returnto' => $this->mReturnTo,
+                               'returntoquery' => $this->mReturnToQuery,
+                               'uselang' => $this->mLanguage,
+                               'fromhttp' => $wgSecureLogin && $this->mFromHTTP ? '1' : null,
+                       ]
+               );
+
+               // Only show valid error or warning messages.
+               if ( $entryError->exists()
+                       && in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true )
+               ) {
+                       $this->mEntryErrorType = 'error';
+                       $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
+
+               } elseif ( $entryWarning->exists()
+                       && in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true )
+               ) {
+                       $this->mEntryErrorType = 'warning';
+                       $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
+               }
+
+               # 1. When switching accounts, it sucks to get automatically logged out
+               # 2. Do not return to PasswordReset after a successful password change
+               #    but goto Wiki start page (Main_Page) instead ( bug 33997 )
+               $returnToTitle = Title::newFromText( $this->mReturnTo );
+               if ( is_object( $returnToTitle )
+                       && ( $returnToTitle->isSpecial( 'Userlogout' )
+                               || $returnToTitle->isSpecial( 'PasswordReset' ) )
+               ) {
+                       $this->mReturnTo = '';
+                       $this->mReturnToQuery = '';
+               }
+       }
+
+       protected function getPreservedParams( $withToken = false ) {
+               global $wgSecureLogin;
+
+               $params = parent::getPreservedParams( $withToken );
+               $params += [
+                       'returnto' => $this->mReturnTo ?: null,
+                       'returntoquery' => $this->mReturnToQuery ?: null,
+               ];
+               if ( $wgSecureLogin && !$this->isSignup() ) {
+                       $params['fromhttp'] = $this->mFromHTTP ? '1' : null;
+               }
+               return $params;
+       }
+
+       /**
+        * @param string|null $subPage
+        */
+       public function execute( $subPage ) {
+               $authManager = AuthManager::singleton();
+               $session = SessionManager::getGlobalSession();
+
+               // Session data is used for various things in the authentication process, so we must make
+               // sure a session cookie or some equivalent mechanism is set.
+               $session->persist();
+
+               $this->load( $subPage );
+               $this->setHeaders();
+               $this->checkPermissions();
+
+               // Make sure it's possible to log in
+               if ( !$this->isSignup() && !$session->canSetUser() ) {
+                       throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
+                                       $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
+                * page. The use case scenario for this is when a user opens a large number
+                * of tabs, is redirected to the login page on all of them, and then logs
+                * in on one, expecting all the others to work properly.
+                *
+                * However, do show the form if it was visited intentionally (no 'returnto'
+                * is present). People who often switch between several accounts have grown
+                * accustomed to this behavior.
+                *
+                * Also make an exception when force=<level> is set in the URL, which means the user must
+                * reauthenticate for security reasons.
+                */
+               if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
+                        ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
+                        $this->getUser()->isLoggedIn()
+               ) {
+                       $this->successfulAction();
+               }
+
+               // If logging in and not on HTTPS, either redirect to it or offer a link.
+               global $wgSecureLogin;
+               if ( $this->getRequest()->getProtocol() !== 'https' ) {
+                       $title = $this->getFullTitle();
+                       $query = $this->getPreservedParams( false ) + [
+                                       'title' => null,
+                                       ( $this->mEntryErrorType === 'error' ? 'error'
+                                               : 'warning' ) => $this->mEntryError,
+                               ] + $this->getRequest()->getQueryValues();
+                       $url = $title->getFullURL( $query, false, PROTO_HTTPS );
+                       if ( $wgSecureLogin && !$this->mFromHTTP &&
+                                wfCanIPUseHTTPS( $this->getRequest()->getIP() )
+                       ) {
+                               // Avoid infinite redirect
+                               $url = wfAppendQuery( $url, 'fromhttp=1' );
+                               $this->getOutput()->redirect( $url );
+                               // Since we only do this redir to change proto, always vary
+                               $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
+
+                               return;
+                       } else {
+                               // A wiki without HTTPS login support should set $wgServer to
+                               // http://somehost, in which case the secure URL generated
+                               // above won't actually start with https://
+                               if ( substr( $url, 0, 8 ) === 'https://' ) {
+                                       $this->mSecureLoginUrl = $url;
+                               }
+                       }
+               }
+
+               if ( !$this->isActionAllowed( $this->authAction ) ) {
+                       // FIXME how do we explain this to the user? can we handle session loss better?
+                       // messages used: authpage-cannot-login, authpage-cannot-login-continue,
+                       // authpage-cannot-create, authpage-cannot-create-continue
+                       $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction );
+                       return;
+               }
+
+               $status = $this->trySubmit();
+
+               if ( !$status || !$status->isGood() ) {
+                       $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' );
+                       return;
+               }
+
+               /** @var AuthenticationResponse $response */
+               $response = $status->getValue();
+
+               $returnToUrl = $this->getPageTitle( 'return' )
+                       ->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
+               switch ( $response->status ) {
+                       case AuthenticationResponse::PASS:
+                               $this->logAuthResult( true );
+                               $this->proxyAccountCreation = $this->isSignup() && !$this->getUser()->isAnon();
+                               $this->targetUser = User::newFromName( $response->username );
+
+                               if (
+                                       !$this->proxyAccountCreation
+                                       && $response->loginRequest
+                                       && $authManager->canAuthenticateNow()
+                               ) {
+                                       // successful registration; log the user in instantly
+                                       $response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
+                                               $returnToUrl );
+                                       if ( $response2->status !== AuthenticationResponse::PASS ) {
+                                               LoggerFactory::getInstance( 'login' )
+                                                       ->error( 'Could not log in after account creation' );
+                                               $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
+                                               break;
+                                       }
+                               }
+
+                               if ( !$this->proxyAccountCreation ) {
+                                       // Ensure that the context user is the same as the session user.
+                                       $this->setSessionUserForCurrentRequest();
+                               }
+
+                               $this->successfulAction( true );
+                               break;
+                       case AuthenticationResponse::FAIL:
+                               // fall through
+                       case AuthenticationResponse::RESTART:
+                               unset( $this->authForm );
+                               if ( $response->status === AuthenticationResponse::FAIL ) {
+                                       $action = $this->getDefaultAction( $subPage );
+                                       $messageType = 'error';
+                               } else {
+                                       $action = $this->getContinueAction( $this->authAction );
+                                       $messageType = 'warning';
+                               }
+                               $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' );
+                               $this->loadAuth( $subPage, $action, true );
+                               $this->mainLoginForm( $this->authRequests, $response->message, $messageType );
+                               break;
+                       case AuthenticationResponse::REDIRECT:
+                               unset( $this->authForm );
+                               $this->getOutput()->redirect( $response->redirectTarget );
+                               break;
+                       case AuthenticationResponse::UI:
+                               unset( $this->authForm );
+                               $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
+                                       : AuthManager::ACTION_LOGIN_CONTINUE;
+                               $this->authRequests = $response->neededRequests;
+                               $this->mainLoginForm( $response->neededRequests, $response->message, 'warning' );
+                               break;
+                       default:
+                               throw new LogicException( 'invalid AuthenticationResponse' );
+               }
+       }
+
+       /**
+        * Show the success page.
+        *
+        * @param string $type Condition of return to; see `executeReturnTo`
+        * @param string|Message $title Page's title
+        * @param string $msgname
+        * @param string $injected_html
+        * @param StatusValue|null $extraMessages
+        */
+       protected function showSuccessPage(
+               $type, $title, $msgname, $injected_html, $extraMessages
+       ) {
+               $out = $this->getOutput();
+               $out->setPageTitle( $title );
+               if ( $msgname ) {
+                       $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
+               }
+               if ( $extraMessages ) {
+                       $extraMessages = Status::wrap( $extraMessages );
+                       $out->addWikiText( $extraMessages->getWikiText() );
+               }
+
+               $out->addHTML( $injected_html );
+
+               $helper = new LoginHelper( $this->getContext() );
+               $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
+       }
+
+       /**
+        * Add a "return to" link or redirect to it.
+        * Extensions can use this to reuse the "return to" logic after
+        * inject steps (such as redirection) into the login process.
+        *
+        * @param string $type One of the following:
+        *    - error: display a return to link ignoring $wgRedirectOnLogin
+        *    - signup: display a return to link using $wgRedirectOnLogin if needed
+        *    - success: display a return to link using $wgRedirectOnLogin if needed
+        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
+        * @param string $returnTo
+        * @param array|string $returnToQuery
+        * @param bool $stickHTTPS Keep redirect link on HTTPS
+        * @since 1.22
+        */
+       public function showReturnToPage(
+               $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
+       ) {
+               $helper = new LoginHelper( $this->getContext() );
+               $helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS );
+       }
+
+       /**
+        * Replace some globals to make sure the fact that the user has just been logged in is
+        * reflected in the current request.
+        * @param User $user
+        */
+       protected function setSessionUserForCurrentRequest() {
+               global $wgUser, $wgLang;
+
+               $context = RequestContext::getMain();
+               $localContext = $this->getContext();
+               if ( $context !== $localContext ) {
+                       // remove AuthManagerSpecialPage context hack
+                       $this->setContext( $context );
+               }
+
+               $user = $context->getRequest()->getSession()->getUser();
+
+               $wgUser = $user;
+               $context->setUser( $user );
+
+               $code = $this->getRequest()->getVal( 'uselang', $user->getOption( 'language' ) );
+               $userLang = Language::factory( $code );
+               $wgLang = $userLang;
+               $context->setLanguage( $userLang );
+       }
+
+       /**
+        * @param AuthenticationRequest[] $requests A list of AuthorizationRequest objects,
+        *   used to generate the form fields. An empty array means a fatal error
+        *   (authentication cannot continue).
+        * @param string|Message $msg
+        * @param string $msgtype
+        * @throws ErrorPageError
+        * @throws Exception
+        * @throws FatalError
+        * @throws MWException
+        * @throws PermissionsError
+        * @throws ReadOnlyError
+        * @private
+        */
+       protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
+               $titleObj = $this->getPageTitle();
+               $user = $this->getUser();
+               $out = $this->getOutput();
+
+               // FIXME how to handle empty $requests - restart, or no form, just an error message?
+               // no form would be better for no session type errors, restart is better when can* fails.
+               if ( !$requests ) {
+                       $this->authAction = $this->getDefaultAction( $this->subPage );
+                       $this->authForm = null;
+                       $requests = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $user );
+               }
+
+               // Generic styles and scripts for both login and signup form
+               $out->addModuleStyles( [
+                       'mediawiki.ui',
+                       'mediawiki.ui.button',
+                       'mediawiki.ui.checkbox',
+                       'mediawiki.ui.input',
+                       'mediawiki.special.userlogin.common.styles'
+               ] );
+               if ( $this->isSignup() ) {
+                       // XXX hack pending RL or JS parse() support for complex content messages T27349
+                       $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp',
+                               $this->msg( 'createacct-imgcaptcha-help' )->parse() );
+
+                       // Additional styles and scripts for signup form
+                       $out->addModules( [
+                               'mediawiki.special.userlogin.signup.js'
+                       ] );
+                       $out->addModuleStyles( [
+                               'mediawiki.special.userlogin.signup.styles'
+                       ] );
+               } else {
+                       // Additional styles for login form
+                       $out->addModuleStyles( [
+                               'mediawiki.special.userlogin.login.styles'
+                       ] );
+               }
+               $out->disallowUserJs(); // just in case...
+
+               $form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
+               $form->prepareForm();
+               $formHtml = $form->getHTML( $msg ? Status::newFatal( $msg ) : false );
+
+               $out->addHTML( $this->getPageHtml( $formHtml ) );
+       }
+
+       /**
+        * Add page elements which are outside the form.
+        * FIXME this should probably be a template, but use a sane language (handlebars?)
+        * @param string $formHtml
+        * @return string
+        */
+       protected function getPageHtml( $formHtml ) {
+               global $wgLoginLanguageSelector;
+
+               $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div',
+                       [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
+               $languageLinks = $wgLoginLanguageSelector ? $this->makeLanguageSelector() : '';
+               $signupStartMsg = $this->msg( 'signupstart' );
+               $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() )
+                       ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : '';
+               if ( $languageLinks ) {
+                       $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ],
+                               Html::rawElement( 'p', [], $languageLinks )
+                       );
+               }
+
+               $benefitsContainer = '';
+               if ( $this->isSignup() && $this->showExtraInformation() ) {
+                       // messages used:
+                       // createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1
+                       // createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2
+                       // createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3
+                       $benefitCount = 3;
+                       $benefitList = '';
+                       for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
+                               $headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text();
+                               $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->escaped();
+                               $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ],
+                                       Html::rawElement( 'h3', [],
+                                               $this->msg( "createacct-benefit-head$benefitIdx" )->escaped()
+                                       )
+                                       . Html::rawElement( 'p', [],
+                                               $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
+                                       )
+                               );
+                       }
+                       $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
+                               Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() )
+                               . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ],
+                                       $benefitList
+                               )
+                       );
+               }
+
+               $html = Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
+                       $loginPrompt
+                       . $languageLinks
+                       . $signupStart
+                       . Html::rawElement( 'div', [ 'id' => 'userloginForm' ],
+                               $formHtml
+                       )
+                       . $benefitsContainer
+               );
+
+               return $html;
+       }
+
+       /**
+        * Generates a form from the given request.
+        * @param AuthenticationRequest[] $requests
+        * @param string $action AuthManager action name
+        * @param string|Message $msg
+        * @param string $msgType
+        * @return HTMLForm
+        */
+       protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
+               global $wgSecureLogin, $wgLoginLanguageSelector;
+               // FIXME merge this with parent
+
+               if ( isset( $this->authForm ) ) {
+                       return $this->authForm;
+               }
+
+               $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
+
+               // get basic form description from the auth logic
+               $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
+               $fakeTemplate = $this->getFakeTemplate( $msg, $msgType );
+               $this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook
+               // this will call onAuthChangeFormFields()
+               $formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
+               $this->postProcessFormDescriptor( $formDescriptor );
+
+               $context = $this->getContext();
+               if ( $context->getRequest() !== $this->getRequest() ) {
+                       // We have overridden the request, need to make sure the form uses that too.
+                       $context = new DerivativeContext( $this->getContext() );
+                       $context->setRequest( $this->getRequest() );
+               }
+               $form = HTMLForm::factory( 'vform', $formDescriptor, $context );
+
+               $form->addHiddenField( 'authAction', $this->authAction );
+               if ( $wgLoginLanguageSelector ) {
+                       $form->addHiddenField( 'uselang', $this->mLanguage );
+               }
+               $form->addHiddenField( 'force', $this->securityLevel );
+               $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
+               if ( $wgSecureLogin ) {
+                       // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
+                       if ( !$this->isSignup() ) {
+                               $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
+                               $form->addHiddenField( 'wpFromhttp', $usingHTTPS );
+                       }
+               }
+
+               // set properties of the form itself
+               $form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) );
+               $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
+               if ( $this->isSignup() ) {
+                       $form->setId( 'userlogin2' );
+               }
+
+               // add pre/post text
+               // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
+               // should be above the error message but HTMLForm doesn't support that
+               $form->addHeaderText( $fakeTemplate->html( 'header' ) );
+
+               // FIXME the old form used this for error/warning messages which does not play well with
+               // HTMLForm (maybe it could with a subclass?); for now only display it for signups
+               // (where the JS username validation needs it) and alway empty
+               if ( $this->isSignup() ) {
+                       // used by the mediawiki.special.userlogin.signup.js module
+                       $statusAreaAttribs = [ 'id' => 'mw-createacct-status-area' ];
+                       // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ];
+                       $form->addHeaderText( Html::element( 'div', $statusAreaAttribs ) );
+               }
+
+               // header used by MobileFrontend
+               $form->addHeaderText( $fakeTemplate->html( 'formheader' ) );
+
+               // blank signup footer for site customization
+               if ( $this->isSignup() && $this->showExtraInformation() ) {
+                       // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
+                       $signupendMsg = $this->msg( 'signupend' );
+                       $signupendHttpsMsg = $this->msg( 'signupend-https' );
+                       if ( !$signupendMsg->isDisabled() ) {
+                               $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
+                                       ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
+                               $form->addPostText( Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ) );
+                       }
+               }
+
+               // warning header for non-standard workflows (e.g. security reauthentication)
+               if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
+                       $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
+                       $form->addHeaderText( Html::rawElement( 'div', [ 'class' => 'warningbox' ],
+                               $this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
+               }
+
+               if ( !$this->isSignup() && $this->showExtraInformation() ) {
+                       $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+                       if ( $passwordReset->isAllowed( $this->getUser() ) ) {
+                               $form->addFooterText( Html::rawElement(
+                                       'div',
+                                       [ 'class' => 'mw-ui-vform-field mw-form-related-link-container' ],
+                                       Linker::link(
+                                               SpecialPage::getTitleFor( 'PasswordReset' ),
+                                               $this->msg( 'userlogin-resetpassword-link' )->escaped()
+                                       )
+                               ) );
+                       }
+
+                       // Don't show a "create account" link if the user can't.
+                       if ( $this->showCreateAccountLink() ) {
+                               // link to the other action
+                               $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
+                               $linkq = $this->getReturnToQueryStringFragment();
+                               // Pass any language selection on to the mode switch link
+                               if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                                       $linkq .= '&uselang=' . $this->mLanguage;
+                               }
+                               $createOrLoginHref = $linkTitle->getLocalURL( $linkq );
+
+                               if ( $this->getUser()->isLoggedIn() ) {
+                                       $createOrLoginHtml = Html::rawElement( 'div',
+                                               [ 'class' => 'mw-ui-vform-field' ],
+                                               Html::element( 'a',
+                                                       [
+                                                               'id' => 'mw-createaccount-join',
+                                                               'href' => $createOrLoginHref,
+                                                               // put right after all auth inputs in the tab order
+                                                               'tabindex' => 100,
+                                                       ],
+                                                       $this->msg( 'userlogin-createanother' )->escaped()
+                                               )
+                                       );
+                               } else {
+                                       $createOrLoginHtml = Html::rawElement( 'div',
+                                               [ 'id' => 'mw-createaccount-cta',
+                                                       'class' => 'mw-ui-vform-field' ],
+                                               $this->msg( 'userlogin-noaccount' )->escaped()
+                                               . Html::element( 'a',
+                                                       [
+                                                               'id' => 'mw-createaccount-join',
+                                                               'href' => $createOrLoginHref,
+                                                               'class' => 'mw-ui-button',
+                                                               'tabindex' => 100,
+                                                       ],
+                                                       $this->msg( 'userlogin-joinproject' )->escaped()
+                                               )
+                                       );
+                               }
+                               $form->addFooterText( $createOrLoginHtml );
+                       }
+               }
+
+               $form->suppressDefaultSubmit();
+
+               $this->authForm = $form;
+
+               return $form;
+       }
+
+       /**
+        * Temporary B/C method to handle extensions using the UserLoginForm/UserCreateForm hooks.
+        * @param string|Message $msg
+        * @param string $msgType
+        * @return FakeAuthTemplate
+        */
+       protected function getFakeTemplate( $msg, $msgType ) {
+               global $wgAuth, $wgEnableEmail, $wgHiddenPrefs, $wgEmailConfirmToEdit, $wgEnableUserEmail,
+                          $wgSecureLogin, $wgLoginLanguageSelector, $wgPasswordResetRoutes;
+
+               // make a best effort to get the value of fields which used to be fixed in the old login
+               // template but now might or might not exist depending on what providers are used
+               $request = $this->getRequest();
+               $data = (object) [
+                       'mUsername' => $request->getText( 'wpName' ),
+                       'mPassword' => $request->getText( 'wpPassword' ),
+                       'mRetype' => $request->getText( 'wpRetype' ),
+                       'mEmail' => $request->getText( 'wpEmail' ),
+                       'mRealName' => $request->getText( 'wpRealName' ),
+                       'mDomain' => $request->getText( 'wpDomain' ),
+                       'mReason' => $request->getText( 'wpReason' ),
+                       'mRemember' => $request->getCheck( 'wpRemember' ),
+               ];
+
+               // Preserves a bunch of logic from the old code that was rewritten in getAuthForm().
+               // There is no code reuse to make this easier to remove .
+               // If an extension tries to change any of these values, they are out of luck - we only
+               // actually use the domain/usedomain/domainnames, extraInput and extrafields keys.
+
+               $titleObj = $this->getPageTitle();
+               $user = $this->getUser();
+               $template = new FakeAuthTemplate();
+
+               // Pre-fill username (if not creating an account, bug 44775).
+               if ( $data->mUsername == '' && $this->isSignup() ) {
+                       if ( $user->isLoggedIn() ) {
+                               $data->mUsername = $user->getName();
+                       } else {
+                               $data->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
+                       }
+               }
+
+               if ( $this->isSignup() ) {
+                       // Must match number of benefits defined in messages
+                       $template->set( 'benefitCount', 3 );
+
+                       $q = 'action=submitlogin&type=signup';
+                       $linkq = 'type=login';
+               } else {
+                       $q = 'action=submitlogin&type=login';
+                       $linkq = 'type=signup';
+               }
+
+               if ( $this->mReturnTo !== '' ) {
+                       $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+                       if ( $this->mReturnToQuery !== '' ) {
+                               $returnto .= '&returntoquery=' .
+                                                        wfUrlencode( $this->mReturnToQuery );
+                       }
+                       $q .= $returnto;
+                       $linkq .= $returnto;
+               }
+
+               # Don't show a "create account" link if the user can't.
+               if ( $this->showCreateAccountLink() ) {
+                       # Pass any language selection on to the mode switch link
+                       if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                               $linkq .= '&uselang=' . $this->mLanguage;
+                       }
+                       // Supply URL, login template creates the button.
+                       $template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
+               } else {
+                       $template->set( 'link', '' );
+               }
+
+               $resetLink = $this->isSignup()
+                       ? null
+                       : is_array( $wgPasswordResetRoutes )
+                         && in_array( true, array_values( $wgPasswordResetRoutes ), true );
+
+               $template->set( 'header', '' );
+               $template->set( 'formheader', '' );
+               $template->set( 'skin', $this->getSkin() );
+
+               $template->set( 'name', $data->mUsername );
+               $template->set( 'password', $data->mPassword );
+               $template->set( 'retype', $data->mRetype );
+               $template->set( 'createemailset', false ); // no easy way to get that from AuthManager
+               $template->set( 'email', $data->mEmail );
+               $template->set( 'realname', $data->mRealName );
+               $template->set( 'domain', $data->mDomain );
+               $template->set( 'reason', $data->mReason );
+               $template->set( 'remember', $data->mRemember );
+
+               $template->set( 'action', $titleObj->getLocalURL( $q ) );
+               $template->set( 'message', $msg );
+               $template->set( 'messagetype', $msgType );
+               $template->set( 'createemail', $wgEnableEmail && $user->isLoggedIn() );
+               $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs, true ) );
+               $template->set( 'useemail', $wgEnableEmail );
+               $template->set( 'emailrequired', $wgEmailConfirmToEdit );
+               $template->set( 'emailothers', $wgEnableUserEmail );
+               $template->set( 'canreset', $wgAuth->allowPasswordChange() );
+               $template->set( 'resetlink', $resetLink );
+               $template->set( 'canremember', $request->getSession()->getProvider()
+                       ->getRememberUserDuration() !== null );
+               $template->set( 'usereason', $user->isLoggedIn() );
+               $template->set( 'cansecurelogin', ( $wgSecureLogin ) );
+               $template->set( 'stickhttps', (int)$this->mStickHTTPS );
+               $template->set( 'loggedin', $user->isLoggedIn() );
+               $template->set( 'loggedinuser', $user->getName() );
+               $template->set( 'token', $this->getToken()->toString() );
+
+               $action = $this->isSignup() ? 'signup' : 'login';
+               $wgAuth->modifyUITemplate( $template, $action );
+
+               $oldTemplate = $template;
+               $hookName = $this->isSignup() ? 'UserCreateForm' : 'UserLoginForm';
+               Hooks::run( $hookName, [ &$template ] );
+               if ( $oldTemplate !== $template ) {
+                       wfDeprecated( "reference in $hookName hook", '1.27' );
+               }
+
+               return $template;
+
+       }
+
+       public function onAuthChangeFormFields(
+               array $requests, array $fieldInfo, array &$formDescriptor, $action
+       ) {
+               $coreFieldDescriptors = $this->getFieldDefinitions( $this->fakeTemplate );
+               $specialFields = array_merge( [ 'extraInput', 'linkcontainer' ],
+                       array_keys( $this->fakeTemplate->getExtraInputDefinitions() ) );
+
+               // keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
+               foreach ( $coreFieldDescriptors as $fieldName => $coreField ) {
+                       $requestField = isset( $formDescriptor[$fieldName] ) ?
+                               $formDescriptor[$fieldName] : [];
+
+                       // remove everything that is not in the fieldinfo, is not marked as a supplemental field
+                       // to something in the fieldinfo, and is not a generic or B/C field or a submit button
+                       if (
+                               !isset( $fieldInfo[$fieldName] )
+                               && (
+                                       !isset( $coreField['baseField'] )
+                                       || !isset( $fieldInfo[$coreField['baseField']] )
+                               ) && !in_array( $fieldName, $specialFields, true )
+                               && $coreField['type'] !== 'submit'
+                       ) {
+                               $coreFieldDescriptors[$fieldName] = null;
+                               continue;
+                       }
+
+                       // core message labels should always take priority
+                       if (
+                               isset( $coreField['label'] )
+                               || isset( $coreField['label-message'] )
+                               || isset( $coreField['label-raw'] )
+                       ) {
+                               unset( $requestField['label'], $requestField['label-message'], $coreField['label-raw'] );
+                       }
+
+                       $coreFieldDescriptors[$fieldName] += $requestField;
+               }
+
+               $formDescriptor = array_filter( $coreFieldDescriptors + $formDescriptor );
+               return true;
+       }
+
+       /**
+        * Show extra information such as password recovery information, link from login to signup,
+        * CTA etc? Such information should only be shown on the "landing page", ie. when the user
+        * is at the first step of the authentication process.
+        * @return bool
+        */
+       protected function showExtraInformation() {
+               return $this->authAction !== $this->getContinueAction( $this->authAction )
+                       && !$this->securityLevel;
+       }
+
+       /**
+        * Create a HTMLForm descriptor for the core login fields.
+        * @param FakeAuthTemplate $template B/C data (not used but needed by getBCFieldDefinitions)
+        * @return array
+        */
+       protected function getFieldDefinitions( $template ) {
+               global $wgEmailConfirmToEdit;
+
+               $isLoggedIn = $this->getUser()->isLoggedIn();
+               $continuePart = $this->isContinued() ? 'continue-' : '';
+               $anotherPart = $isLoggedIn ? 'another-' : '';
+               $expiration = $this->getRequest()->getSession()->getProvider()
+                       ->getRememberUserDuration();
+               $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
+               $secureLoginLink = '';
+               if ( $this->mSecureLoginUrl ) {
+                       $secureLoginLink = Html::element( 'a', [
+                               'href' => $this->mSecureLoginUrl,
+                               'class' => 'mw-ui-flush-right mw-secure',
+                       ], $this->msg( 'userlogin-signwithsecure' )->text() );
+               }
+
+               if ( $this->isSignup() ) {
+                       $fieldDefinitions = [
+                               'username' => [
+                                       'label-message' => 'userlogin-yourname',
+                                       // FIXME help-message does not match old formatting
+                                       'help-message' => 'createacct-helpusername',
+                                       'id' => 'wpName2',
+                                       'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
+                                               : 'userlogin-yourname-ph',
+                               ],
+                               'mailpassword' => [
+                                       // create account without providing password, a temporary one will be mailed
+                                       'type' => 'check',
+                                       'label-message' => 'createaccountmail',
+                                       'name' => 'wpCreateaccountMail',
+                                       'id' => 'wpCreateaccountMail',
+                               ],
+                               'password' => [
+                                       'id' => 'wpPassword2',
+                                       'placeholder-message' => 'createacct-yourpassword-ph',
+                                       'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
+                               ],
+                               'domain' => [],
+                               'retype' => [
+                                       'baseField' => 'password',
+                                       'type' => 'password',
+                                       'label-message' => 'createacct-yourpasswordagain',
+                                       'id' => 'wpRetype',
+                                       'cssclass' => 'loginPassword',
+                                       'size' => 20,
+                                       'validation-callback' => function ( $value, $alldata ) {
+                                               if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
+                                                       if ( !$value ) {
+                                                               return $this->msg( 'htmlform-required' );
+                                                       } elseif ( $value !== $alldata['password'] ) {
+                                                               return $this->msg( 'badretype' );
+                                                       }
+                                               }
+                                               return true;
+                                       },
+                                       'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
+                                       'placeholder-message' => 'createacct-yourpasswordagain-ph',
+                               ],
+                               'email' => [
+                                       'type' => 'email',
+                                       'label-message' => $wgEmailConfirmToEdit ? 'createacct-emailrequired'
+                                               : 'createacct-emailoptional',
+                                       'id' => 'wpEmail',
+                                       'cssclass' => 'loginText',
+                                       'size' => '20',
+                                       // FIXME will break non-standard providers
+                                       'required' => $wgEmailConfirmToEdit,
+                                       'validation-callback' => function ( $value, $alldata ) {
+                                               global $wgEmailConfirmToEdit;
+
+                                               // AuthManager will check most of these, but that will make the auth
+                                               // session fail and this won't, so nicer to do it this way
+                                               if ( !$value && $wgEmailConfirmToEdit ) {
+                                                       // no point in allowing registration without email when email is
+                                                       // required to edit
+                                                       return $this->msg( 'noemailtitle' );
+                                               } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
+                                                       // cannot send password via email when there is no email address
+                                                       return $this->msg( 'noemailcreate' );
+                                               } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
+                                                       return $this->msg( 'invalidemailaddress' );
+                                               }
+                                               return true;
+                                       },
+                                       'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
+                               ],
+                               'realname' => [
+                                       'type' => 'text',
+                                       'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
+                                               : 'prefs-help-realname',
+                                       'label-message' => 'createacct-realname',
+                                       'cssclass' => 'loginText',
+                                       'size' => 20,
+                                       'id' => 'wpRealName',
+                               ],
+                               'reason' => [
+                                       // comment for the user creation log
+                                       'type' => 'text',
+                                       'label-message' => 'createacct-reason',
+                                       'cssclass' => 'loginText',
+                                       'id' => 'wpReason',
+                                       'size' => '20',
+                                       'placeholder-message' => 'createacct-reason-ph',
+                               ],
+                               'extrainput' => [], // placeholder for fields coming from the template
+                               'createaccount' => [
+                                       // submit button
+                                       'type' => 'submit',
+                                       'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
+                                               'submit' )->text(),
+                                       'name' => 'wpCreateaccount',
+                                       'id' => 'wpCreateaccount',
+                                       'weight' => 100,
+                               ],
+                       ];
+               } else {
+                       $fieldDefinitions = [
+                               'username' => [
+                                       'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
+                                       'id' => 'wpName1',
+                                       'placeholder-message' => 'userlogin-yourname-ph',
+                               ],
+                               'password' => [
+                                       'id' => 'wpPassword1',
+                                       'placeholder-message' => 'userlogin-yourpassword-ph',
+                               ],
+                               'domain' => [],
+                               'extrainput' => [],
+                               'rememberMe' => [
+                                       // option for saving the user token to a cookie
+                                       'type' => 'check',
+                                       'label-message' => $this->msg( 'userlogin-remembermypassword' )
+                                               ->numParams( $expirationDays ),
+                                       'id' => 'wpRemember',
+                               ],
+                               'loginattempt' => [
+                                       // submit button
+                                       'type' => 'submit',
+                                       'name' => 'wpRemember',
+                                       'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
+                                       'id' => 'wpLoginAttempt',
+                                       'weight' => 100,
+                               ],
+                               'linkcontainer' => [
+                                       // help link
+                                       'type' => 'info',
+                                       'cssclass' => 'mw-form-related-link-container',
+                                       'id' => 'mw-userlogin-help',
+                                       'raw' => true,
+                                       'default' => Html::element( 'a', [
+                                               'href' => Skin::makeInternalOrExternalUrl( wfMessage( 'helplogin-url' )
+                                                       ->inContentLanguage()
+                                                       ->text() ),
+                                       ], $this->msg( 'userlogin-helplink2' )->text() ),
+                                       'weight' => 200,
+                               ],
+                       ];
+               }
+               $fieldDefinitions['username'] += [
+                       'type' => 'text',
+                       'name' => 'wpName',
+                       'cssclass' => 'loginText',
+                       'size' => 20,
+                       // 'required' => true,
+               ];
+               $fieldDefinitions['password'] += [
+                       'type' => 'password',
+                       // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
+                       'name' => 'wpPassword',
+                       'cssclass' => 'loginPassword',
+                       'size' => 20,
+                       // 'required' => true,
+               ];
+
+               if ( !$this->showExtraInformation() ) {
+                       unset( $fieldDefinitions['linkcontainer'] );
+               }
+
+               $fieldDefinitions = $this->getBCFieldDefinitions( $fieldDefinitions, $template );
+               $fieldDefinitions = array_filter( $fieldDefinitions );
+
+               return $fieldDefinitions;
+       }
+
+       /**
+        * Adds fields provided via the deprecated UserLoginForm / UserCreateForm hooks
+        * @param $fieldDefinitions array
+        * @param FakeAuthTemplate $template
+        * @return array
+        */
+       protected function getBCFieldDefinitions( $fieldDefinitions, $template ) {
+               if ( $template->get( 'usedomain', false ) ) {
+                       // TODO probably should be translated to the new domain notation in AuthManager
+                       $fieldDefinitions['domain'] = [
+                               'type' => 'select',
+                               'label-message' => 'yourdomainname',
+                               'options' => array_combine( $template->get( 'domainnames', [] ),
+                                       $template->get( 'domainnames', [] ) ),
+                               'default' => $template->get( 'domain', '' ),
+                               'name' => 'wpDomain',
+                               // FIXME id => 'mw-user-domain-section' on the parent div
+                       ];
+               }
+
+               // poor man's associative array_splice
+               $extraInputPos = array_search( 'extrainput', array_keys( $fieldDefinitions ), true );
+               $fieldDefinitions = array_slice( $fieldDefinitions, 0, $extraInputPos, true )
+                                                       + $template->getExtraInputDefinitions()
+                                                       + array_slice( $fieldDefinitions, $extraInputPos + 1, null, true );
+
+               return $fieldDefinitions;
+       }
+
+       /**
+        * Check if a session cookie is present.
+        *
+        * This will not pick up a cookie set during _this_ request, but is meant
+        * to ensure that the client is returning the cookie which was set on a
+        * previous pass through the system.
+        *
+        * @return bool
+        */
+       protected function hasSessionCookie() {
+               global $wgDisableCookieCheck, $wgInitialSessionId;
+
+               return $wgDisableCookieCheck || (
+                       $wgInitialSessionId &&
+                       $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
+               );
+       }
+
+       /**
+        * Returns a string that can be appended to the URL (without encoding) to preserve the
+        * return target. Does not include leading '?'/'&'.
+        */
+       protected function getReturnToQueryStringFragment() {
+               $returnto = '';
+               if ( $this->mReturnTo !== '' ) {
+                       $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
+                       if ( $this->mReturnToQuery !== '' ) {
+                               $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
+                       }
+               }
+               return $returnto;
+       }
+
+       /**
+        * Whether the login/create account form should display a link to the
+        * other form (in addition to whatever the skin provides).
+        * @return bool
+        */
+       private function showCreateAccountLink() {
+               if ( $this->isSignup() ) {
+                       return true;
+               } elseif ( $this->getUser()->isAllowed( 'createaccount' ) ) {
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       protected function getTokenName() {
+               return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
+       }
+
+       /**
+        * Produce a bar of links which allow the user to select another language
+        * during login/registration but retain "returnto"
+        *
+        * @return string
+        */
+       protected function makeLanguageSelector() {
+               $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
+               if ( $msg->isBlank() ) {
+                       return '';
+               }
+               $langs = explode( "\n", $msg->text() );
+               $links = [];
+               foreach ( $langs as $lang ) {
+                       $lang = trim( $lang, '* ' );
+                       $parts = explode( '|', $lang );
+                       if ( count( $parts ) >= 2 ) {
+                               $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
+                       }
+               }
+
+               return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
+                       $this->getLanguage()->pipeList( $links ) )->escaped() : '';
+       }
+
+       /**
+        * Create a language selector link for a particular language
+        * Links back to this page preserving type and returnto
+        *
+        * @param string $text Link text
+        * @param string $lang Language code
+        * @return string
+        */
+       protected function makeLanguageSelectorLink( $text, $lang ) {
+               if ( $this->getLanguage()->getCode() == $lang ) {
+                       // no link for currently used language
+                       return htmlspecialchars( $text );
+               }
+               $query = [ 'uselang' => $lang ];
+               if ( $this->mReturnTo !== '' ) {
+                       $query['returnto'] = $this->mReturnTo;
+                       $query['returntoquery'] = $this->mReturnToQuery;
+               }
+
+               $attr = [];
+               $targetLanguage = Language::factory( $lang );
+               $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
+
+               return Linker::linkKnown(
+                       $this->getPageTitle(),
+                       htmlspecialchars( $text ),
+                       $attr,
+                       $query
+               );
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+
+       /**
+        * @param array $formDescriptor
+        */
+       protected function postProcessFormDescriptor( &$formDescriptor ) {
+               // Pre-fill username (if not creating an account, T46775).
+               if (
+                       isset( $formDescriptor['username'] ) &&
+                       !isset( $formDescriptor['username']['default'] ) &&
+                       !$this->isSignup()
+               ) {
+                       $user = $this->getUser();
+                       if ( $user->isLoggedIn() ) {
+                               $formDescriptor['username']['default'] = $user->getName();
+                       } else {
+                               $formDescriptor['username']['default'] =
+                                       $this->getRequest()->getSession()->suggestLoginUsername();
+                       }
+               }
+
+               // don't show a submit button if there is nothing to submit (i.e. the only form content
+               // is other submit buttons, for redirect flows)
+               if ( !$this->needsSubmitButton( $formDescriptor ) ) {
+                       unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
+               }
+
+               if ( !$this->isSignup() ) {
+                       // FIXME HACK don't focus on non-empty field
+                       // maybe there should be an autofocus-if similar to hide-if?
+                       if (
+                               isset( $formDescriptor['username'] )
+                               && empty( $formDescriptor['username']['default'] )
+                               && !$this->getRequest()->getCheck( 'wpName' )
+                       ) {
+                               $formDescriptor['username']['autofocus'] = true;
+                       } elseif ( isset( $formDescriptor['password'] ) ) {
+                               $formDescriptor['password']['autofocus'] = true;
+                       }
+               }
+
+               $this->addTabIndex( $formDescriptor );
+       }
+}
+
+/**
+ * B/C class to try handling login/signup template modifications even though login/signup does not
+ * actually happen through a template anymore. Just collects extra field definitions and allows
+ * some other class to do decide what to do with threm..
+ * TODO find the right place for adding extra fields and kill this
+ */
+class FakeAuthTemplate extends BaseTemplate {
+       public function execute() {
+               throw new LogicException( 'not used' );
+       }
+
+       /**
+        * Extensions (AntiSpoof and TitleBlacklist) call this in response to
+        * UserCreateForm hook to add checkboxes to the create account form.
+        */
+       public function addInputItem( $name, $value, $type, $msg, $helptext = false ) {
+               // use the same indexes as UserCreateForm just in case someone adds an item manually
+               $this->data['extrainput'][] = [
+                       'name' => $name,
+                       'value' => $value,
+                       'type' => $type,
+                       'msg' => $msg,
+                       'helptext' => $helptext,
+               ];
+       }
+
+       /**
+        * Turns addInputItem-style field definitions into HTMLForm field definitions.
+        * @return array
+        */
+       public function getExtraInputDefinitions() {
+               $definitions = [];
+
+               foreach ( $this->get( 'extrainput', [] ) as $field ) {
+                       $definition = [
+                               'type' => $field['type'] === 'checkbox' ? 'check' : $field['type'],
+                               'name' => $field['name'],
+                               'value' => $field['value'],
+                               'id' => $field['name'],
+                       ];
+                       if ( $field['msg'] ) {
+                               $definition['label-message'] = $this->getMsg( $field['msg'] );
+                       }
+                       if ( $field['helptext'] ) {
+                               $definition['help'] = $this->msgWiki( $field['helptext'] );
+                       }
+
+                       // the array key doesn't matter much when name is defined explicitly but
+                       // let's try and follow HTMLForm conventions
+                       $name = preg_replace( '/^wp(?=[A-Z])/', '', $field['name'] );
+                       $definitions[$name] = $definition;
+               }
+
+               if ( $this->haveData( 'extrafields' ) ) {
+                       $definitions['extrafields'] = [
+                               'type' => 'info',
+                               'raw' => true,
+                               'default' => $this->get( 'extrafields' ),
+                       ];
+               }
+
+               return $definitions;
+       }
+}
+
+/**
+ * A horrible hack to handle AuthManager's feature flag. For other special pages this is done in
+ * SpecialPageFactory, but LoginForm is used directly by some extensions. Will be killed as soon
+ * as AuthManager is stable.
+ */
+class LoginForm extends SpecialPage {
+       private $realLoginForm;
+
+       public function __construct( $request = null ) {
+               global $wgDisableAuthManager;
+               if ( $wgDisableAuthManager ) {
+                       $this->realLoginForm = new LoginFormPreAuthManager( $request );
+               } else {
+                       $this->realLoginForm = new LoginFormAuthManager( $request );
+               }
+       }
+
+       // proxy everything
+
+       public function __get( $name ) {
+               return $this->realLoginForm->$name;
+       }
+
+       public function __set( $name, $value ) {
+               $this->realLoginForm->$name = $value;
+       }
+
+       public function __call( $name, $args ) {
+               return call_user_func_array( [ $this->realLoginForm, $name ], $args );
+       }
+
+       public static function __callStatic( $name, $args ) {
+               global $wgDisableAuthManager;
+               return call_user_func_array( [ $wgDisableAuthManager ? LoginFormPreAuthManager::class
+                       : LoginFormAuthManager::class, $name ], $args );
+       }
+
+       // all public SpecialPage methods need to be proxied explicitly
+
+       public function getName() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getRestriction() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function isListed() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function setListed( $listed ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function listed( $x = null ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function isIncludable() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function including( $x = null ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getLocalName() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function isExpensive() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function isCached() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function isRestricted() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function userCanExecute( User $user ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function displayRestrictionError() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function checkPermissions() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function checkReadOnly() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function requireLogin(
+               $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
+       ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function prefixSearchSubpages( $search, $limit, $offset ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function execute( $subPage ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getDescription() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       function getTitle( $subpage = false ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       function getPageTitle( $subpage = false ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function setContext( $context ) {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getContext() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getRequest() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getOutput() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getUser() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getSkin() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getLanguage() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getConfig() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getFullTitle() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function getFinalGroupName() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+       public function doesWrites() {
+               return call_user_func_array( [ $this->realLoginForm, __FUNCTION__ ], func_get_args() );
+       }
+
+       // no way to proxy constants and static properties
+
+       const SUCCESS = 0;
+       const NO_NAME = 1;
+       const ILLEGAL = 2;
+       const WRONG_PLUGIN_PASS = 3;
+       const NOT_EXISTS = 4;
+       const WRONG_PASS = 5;
+       const EMPTY_PASS = 6;
+       const RESET_PASS = 7;
+       const ABORTED = 8;
+       const CREATE_BLOCKED = 9;
+       const THROTTLED = 10;
+       const USER_BLOCKED = 11;
+       const NEED_TOKEN = 12;
+       const WRONG_TOKEN = 13;
+       const USER_MIGRATED = 14;
+
+       public static $statusCodes = [
+               self::SUCCESS => 'success',
+               self::NO_NAME => 'no_name',
+               self::ILLEGAL => 'illegal',
+               self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass',
+               self::NOT_EXISTS => 'not_exists',
+               self::WRONG_PASS => 'wrong_pass',
+               self::EMPTY_PASS => 'empty_pass',
+               self::RESET_PASS => 'reset_pass',
+               self::ABORTED => 'aborted',
+               self::CREATE_BLOCKED => 'create_blocked',
+               self::THROTTLED => 'throttled',
+               self::USER_BLOCKED => 'user_blocked',
+               self::NEED_TOKEN => 'need_token',
+               self::WRONG_TOKEN => 'wrong_token',
+               self::USER_MIGRATED => 'user_migrated',
+       ];
+
+       public static $validErrorMessages = [
+               'exception-nologin-text',
+               'watchlistanontext',
+               'changeemail-no-info',
+               'resetpass-no-info',
+               'confirmemail_needlogin',
+               'prefsnologintext2',
+       ];
+}
+
+/**
+ * LoginForm as a special page has been replaced by SpecialUserLogin and SpecialCreateAccount,
+ * but some extensions called its public methods directly, so the class is retained as a
+ * B/C wrapper. Anything that used it before should use AuthManager instead.
+ */
+class LoginFormAuthManager extends SpecialPage {
+       const SUCCESS = 0;
+       const NO_NAME = 1;
+       const ILLEGAL = 2;
+       const WRONG_PLUGIN_PASS = 3;
+       const NOT_EXISTS = 4;
+       const WRONG_PASS = 5;
+       const EMPTY_PASS = 6;
+       const RESET_PASS = 7;
+       const ABORTED = 8;
+       const CREATE_BLOCKED = 9;
+       const THROTTLED = 10;
+       const USER_BLOCKED = 11;
+       const NEED_TOKEN = 12;
+       const WRONG_TOKEN = 13;
+       const USER_MIGRATED = 14;
+
+       public static $statusCodes = [
+               self::SUCCESS => 'success',
+               self::NO_NAME => 'no_name',
+               self::ILLEGAL => 'illegal',
+               self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass',
+               self::NOT_EXISTS => 'not_exists',
+               self::WRONG_PASS => 'wrong_pass',
+               self::EMPTY_PASS => 'empty_pass',
+               self::RESET_PASS => 'reset_pass',
+               self::ABORTED => 'aborted',
+               self::CREATE_BLOCKED => 'create_blocked',
+               self::THROTTLED => 'throttled',
+               self::USER_BLOCKED => 'user_blocked',
+               self::NEED_TOKEN => 'need_token',
+               self::WRONG_TOKEN => 'wrong_token',
+               self::USER_MIGRATED => 'user_migrated',
+       ];
+
+       /**
+        * @param WebRequest $request
+        */
+       public function __construct( $request = null ) {
+               wfDeprecated( 'LoginForm', '1.27' );
+               parent::__construct();
+       }
+
+       /**
+        * @deprecated since 1.27 - call LoginHelper::getValidErrorMessages instead.
+        */
+       public static function getValidErrorMessages() {
+               return LoginHelper::getValidErrorMessages();
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function incrementLoginThrottle( $username ) {
+               wfDeprecated( __METHOD__, "1.27" );
+               global $wgRequest;
+               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
+               $throttler = new Throttler();
+               return $throttler->increase( $username, $wgRequest->getIP(), __METHOD__ );
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function incLoginThrottle( $username ) {
+               wfDeprecated( __METHOD__, "1.27" );
+               $res = self::incrementLoginThrottle( $username );
+               return is_array( $res ) ? true : 0;
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function clearLoginThrottle( $username ) {
+               wfDeprecated( __METHOD__, "1.27" );
+               global $wgRequest;
+               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
+               $throttler = new Throttler();
+               return $throttler->clear( $username, $wgRequest->getIP() );
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function getLoginToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+               global $wgRequest;
+               return $wgRequest->getSession()->getToken( '', 'login' )->toString();
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function setLoginToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function clearLoginToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+               global $wgRequest;
+               $wgRequest->getSession()->resetToken( 'login' );
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function getCreateaccountToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+               global $wgRequest;
+               return $wgRequest->getSession()->getToken( '', 'createaccount' )->toString();
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function setCreateaccountToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+       }
+
+       /**
+        * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
+        */
+       public static function clearCreateaccountToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+               global $wgRequest;
+               $wgRequest->getSession()->resetToken( 'createaccount' );
+       }
+}
index b274017..408c726 100644 (file)
@@ -23,6 +23,8 @@ use MediaWiki\MediaWikiServices;
  * @ingroup SpecialPage
  */
 
+use MediaWiki\Auth\AuthManager;
+
 /**
  * Parent class for all special pages.
  *
@@ -296,6 +298,66 @@ class SpecialPage {
                }
        }
 
+       /**
+        * Tells if the special page does something security-sensitive and needs extra defense against
+        * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the
+        * authentication framework.
+        * @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus().
+        *   Typically a special page needing elevated security would return its name here.
+        */
+       protected function getLoginSecurityLevel() {
+               return false;
+       }
+
+       /**
+        * Verifies that the user meets the security level, possibly reauthenticating them in the process.
+        *
+        * This should be used when the page does something security-sensitive and needs extra defense
+        * against a stolen account (e.g. a reauthentication). The authentication framework will make
+        * an extra effort to make sure the user account is not compromised. What that exactly means
+        * will depend on the system and user settings; e.g. the user might be required to log in again
+        * unless their last login happened recently, or they might be given a second-factor challenge.
+        *
+        * Calling this method will result in one if these actions:
+        * - return true: all good.
+        * - return false and set a redirect: caller should abort; the redirect will take the user
+        *   to the login page for reauthentication, and back.
+        * - throw an exception if there is no way for the user to meet the requirements without using
+        *   a different access method (e.g. this functionality is only available from a specific IP).
+        *
+        * Note that this does not in any way check that the user is authorized to use this special page
+        * (use checkPermissions() for that).
+        *
+        * @param string $level A security level. Can be an arbitrary string, defaults to the page name.
+        * @return bool False means a redirect to the reauthentication page has been set and processing
+        *   of the special page should be aborted.
+        * @throws ErrorPageError If the security level cannot be met, even with reauthentication.
+        */
+       protected function checkLoginSecurityLevel( $level = null ) {
+               $level = $level ?: $this->getName();
+               $securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level );
+               if ( $securityStatus === AuthManager::SEC_OK ) {
+                       return true;
+               } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) {
+                       $request = $this->getRequest();
+                       $title = SpecialPage::getTitleFor( 'Userlogin' );
+                       $query = [
+                               'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
+                               'returntoquery' => wfArrayToCgi( array_diff_key( $request->getQueryValues(),
+                                       [ 'title' => true ] ) ),
+                               'force' => $level,
+                       ];
+                       $url = $title->getFullURL( $query, false, PROTO_HTTPS );
+
+                       $this->getOutput()->redirect( $url );
+                       return false;
+               }
+
+               $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' );
+               $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' );
+               throw new ErrorPageError( $titleMessage, $errorMessage );
+       }
+
        /**
         * Return an array of subpages beginning with $search that this special page will accept.
         *
@@ -463,6 +525,7 @@ class SpecialPage {
        public function execute( $subPage ) {
                $this->setHeaders();
                $this->checkPermissions();
+               $this->checkLoginSecurityLevel( $this->getLoginSecurityLevel() );
                $this->outputHeader();
        }
 
index 4c869f9..73efa4e 100644 (file)
@@ -81,18 +81,23 @@ class SpecialPageFactory {
                'PagesWithProp' => 'SpecialPagesWithProp',
                'TrackingCategories' => 'SpecialTrackingCategories',
 
-               // Login/create account
-               'Userlogin' => 'LoginForm',
-               'CreateAccount' => 'SpecialCreateAccount',
+               // Authentication
+               'Userlogin' => 'SpecialUserLogin',
+               'Userlogout' => 'SpecialUserlogoutPreAuthManager',
+               'CreateAccount' => 'SpecialCreateAccountPreAuthManager',
+               'LinkAccounts' => 'SpecialLinkAccounts',
+               'UnlinkAccounts' => 'SpecialUnlinkAccounts',
+               'ChangeCredentials' => 'SpecialChangeCredentials',
+               'RemoveCredentials' => 'SpecialRemoveCredentials',
 
                // Users and rights
                'Activeusers' => 'SpecialActiveUsers',
                'Block' => 'SpecialBlock',
                'Unblock' => 'SpecialUnblock',
                'BlockList' => 'SpecialBlockList',
-               'ChangePassword' => 'SpecialChangePassword',
+               'ChangePassword' => 'SpecialChangePasswordPreAuthManager',
                'BotPasswords' => 'SpecialBotPasswords',
-               'PasswordReset' => 'SpecialPasswordReset',
+               'PasswordReset' => 'SpecialPasswordResetPreAuthManager',
                'DeletedContributions' => 'DeletedContributionsPage',
                'Preferences' => 'SpecialPreferences',
                'ResetTokens' => 'SpecialResetTokens',
@@ -178,7 +183,6 @@ class SpecialPageFactory {
                'Revisiondelete' => 'SpecialRevisionDelete',
                'RunJobs' => 'SpecialRunJobs',
                'Specialpages' => 'SpecialSpecialpages',
-               'Userlogout' => 'SpecialUserlogout',
        ];
 
        private static $list;
@@ -226,6 +230,7 @@ class SpecialPageFactory {
                global $wgDisableInternalSearch, $wgEmailAuthentication;
                global $wgEnableEmail, $wgEnableJavaScriptTest;
                global $wgPageLanguageUseDB, $wgContentHandlerUseDB;
+               global $wgDisableAuthManager;
 
                if ( !is_array( self::$list ) ) {
 
@@ -241,7 +246,7 @@ class SpecialPageFactory {
                        }
 
                        if ( $wgEnableEmail ) {
-                               self::$list['ChangeEmail'] = 'SpecialChangeEmail';
+                               self::$list['ChangeEmail'] = 'SpecialChangeEmailPreAuthManager';
                        }
 
                        if ( $wgEnableJavaScriptTest ) {
@@ -255,6 +260,20 @@ class SpecialPageFactory {
                                self::$list['ChangeContentModel'] = 'SpecialChangeContentModel';
                        }
 
+                       // horrible hack to allow selection between old and new classes via a feature flag - T110756
+                       // will be removed once AuthManager is stable
+                       if ( !$wgDisableAuthManager ) {
+                               self::$list = array_map( function ( $class ) {
+                                       return preg_replace( '/PreAuthManager$/', '', $class );
+                               }, self::$list );
+                               self::$list['Userlogout'] = 'SpecialUserLogout'; // case matters
+                       } else {
+                               self::$list['Userlogin'] = 'LoginForm';
+                               self::$list = array_diff_key( self::$list, array_fill_keys( [
+                                       'LinkAccounts', 'UnlinkAccounts', 'ChangeCredentials', 'RemoveCredentials',
+                               ], true ) );
+                       }
+
                        // Add extension special pages
                        self::$list = array_merge( self::$list, $wgSpecialPages );
 
diff --git a/includes/specials/SpecialChangeCredentials.php b/includes/specials/SpecialChangeCredentials.php
new file mode 100644 (file)
index 0000000..382dac7
--- /dev/null
@@ -0,0 +1,252 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Session\SessionManager;
+
+/**
+ * Special change to change credentials (such as the password).
+ *
+ * Also does most of the work for SpecialRemoveCredentials.
+ */
+class SpecialChangeCredentials extends AuthManagerSpecialPage {
+       protected static $allowedActions = [ AuthManager::ACTION_CHANGE ];
+
+       protected static $messagePrefix = 'changecredentials';
+
+       /** Change action needs user data; remove action does not */
+       protected static $loadUserData = true;
+
+       public function __construct( $name = 'ChangeCredentials' ) {
+               parent::__construct( $name, 'editmyprivateinfo' );
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       public function isListed() {
+               $this->loadAuth( '' );
+               return (bool)$this->authRequests;
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_CHANGE;
+       }
+
+       protected function getPreservedParams( $withToken = false ) {
+               $request = $this->getRequest();
+               $params = parent::getPreservedParams( $withToken );
+               $params += [
+                       'returnto' => $request->getVal( 'returnto' ),
+                       'returntoquery' => $request->getVal( 'returntoquery' ),
+               ];
+               return $params;
+       }
+
+       public function onAuthChangeFormFields(
+               array $requests, array $fieldInfo, array &$formDescriptor, $action
+       ) {
+               // This method is never called for remove actions.
+
+               $extraFields = [];
+               Hooks::run( 'ChangePasswordForm', [ &$extraFields ], '1.27' );
+               foreach ( $extraFields as $extra ) {
+                       list( $name, $label, $type, $default ) = $extra;
+                       $formDescriptor[$name] = [
+                               'type' => $type,
+                               'name' => $name,
+                               'label-message' => $label,
+                               'default' => $default,
+                       ];
+
+               }
+
+               return parent::onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
+       }
+
+       public function execute( $subPage ) {
+               $this->setHeaders();
+               $this->outputHeader();
+
+               $this->loadAuth( $subPage );
+
+               if ( !$subPage ) {
+                       $this->showSubpageList();
+                       return;
+               }
+
+               if ( $this->getRequest()->getCheck( 'wpCancel' ) ) {
+                       $returnUrl = $this->getReturnUrl() ?: Title::newMainPage()->getFullURL();
+                       $this->getOutput()->redirect( $returnUrl );
+                       return;
+               }
+
+               if ( !$this->authRequests ) {
+                       // messages used: changecredentials-invalidsubpage, removecredentials-invalidsubpage
+                       $this->showSubpageList( $this->msg( static::$messagePrefix . '-invalidsubpage', $subPage ) );
+                       return;
+               }
+
+               $status = $this->trySubmit();
+
+               if ( $status === false || !$status->isOK() ) {
+                       $this->displayForm( $status );
+                       return;
+               }
+
+               $response = $status->getValue();
+
+               switch ( $response->status ) {
+                       case AuthenticationResponse::PASS:
+                               $this->success();
+                               break;
+                       case AuthenticationResponse::FAIL:
+                               $this->displayForm( Status::newFatal( $response->message ) );
+                               break;
+                       default:
+                               throw new LogicException( 'invalid AuthenticationResponse' );
+               }
+       }
+
+       protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
+               parent::loadAuth( $subPage, $authAction );
+               if ( $subPage ) {
+                       $this->authRequests = array_filter( $this->authRequests, function ( $req ) use ( $subPage ) {
+                               return $req->getUniqueId() === $subPage;
+                       } );
+                       if ( count( $this->authRequests ) > 1 ) {
+                               throw new LogicException( 'Multiple AuthenticationRequest objects with same ID!' );
+                       }
+               }
+       }
+
+       protected function getAuthFormDescriptor( $requests, $action ) {
+               if ( !static::$loadUserData ) {
+                       return [];
+               } else {
+                       return parent::getAuthFormDescriptor( $requests, $action );
+               }
+       }
+
+       protected function getAuthForm( array $requests, $action ) {
+               $form = parent::getAuthForm( $requests, $action );
+               $req = reset( $requests );
+               $info = $req->describeCredentials();
+
+               $form->addPreText(
+                       Html::openElement( 'dl' )
+                       . Html::element( 'dt', [], wfMessage( 'credentialsform-provider' ) )
+                       . Html::element( 'dd', [], $info['provider'] )
+                       . Html::element( 'dt', [], wfMessage( 'credentialsform-account' ) )
+                       . Html::element( 'dd', [], $info['account'] )
+                       . Html::closeElement( 'dl' )
+               );
+
+               // messages used: changecredentials-submit removecredentials-submit
+               // changecredentials-submit-cancel removecredentials-submit-cancel
+               $form->setSubmitTextMsg( static::$messagePrefix . '-submit' );
+               $form->addButton( [
+                       'name' => 'wpCancel',
+                       'value' => $this->msg( static::$messagePrefix . '-submit-cancel' )->text()
+               ] );
+
+               return $form;
+       }
+
+       protected function needsSubmitButton( $formDescriptor ) {
+               // Change/remove forms show are built from a single AuthenticationRequest and do not allow
+               // for redirect flow; they always need a submit button.
+               return true;
+       }
+
+       public function handleFormSubmit( $data ) {
+               // remove requests do not accept user input
+               $requests = $this->authRequests;
+               if ( static::$loadUserData ) {
+                       $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
+               }
+
+               $response = $this->performAuthenticationStep( $this->authAction, $requests );
+
+               // we can't handle FAIL or similar as failure here since it might require changing the form
+               return Status::newGood( $response );
+       }
+
+       /**
+        * @param Message|null $error
+        */
+       protected function showSubpageList( $error = null ) {
+               $out = $this->getOutput();
+
+               if ( $error ) {
+                       $out->addHTML( $error->parse() );
+               }
+
+               $groupedRequests = [];
+               foreach ( $this->authRequests as $req ) {
+                       $info = $req->describeCredentials();
+                       $groupedRequests[(string)$info['provider']][] = $req;
+               }
+
+               $out->addHTML( Html::openElement( 'dl' ) );
+               foreach ( $groupedRequests as $group => $members ) {
+                       $out->addHTML( Html::element( 'dt', [], $group ) );
+                       foreach ( $members as $req ) {
+                               /** @var AuthenticationRequest $req */
+                               $info = $req->describeCredentials();
+                               $out->addHTML( Html::rawElement( 'dd', [],
+                                       Linker::link( $this->getPageTitle( $req->getUniqueId() ),
+                                               htmlspecialchars( $info['account'], ENT_QUOTES ) )
+                               ) );
+                       }
+               }
+               $out->addHTML( Html::closeElement( 'dl' ) );
+       }
+
+       protected function success() {
+               $session = $this->getRequest()->getSession();
+               $user = $this->getUser();
+               $out = $this->getOutput();
+               $returnUrl = $this->getReturnUrl();
+
+               // change user token and update the session
+               SessionManager::singleton()->invalidateSessionsForUser( $user );
+               $session->setUser( $user );
+               $session->resetId();
+
+               if ( $returnUrl ) {
+                       $out->redirect( $returnUrl );
+               } else {
+                       // messages used: changecredentials-success removecredentials-success
+                       $out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", static::$messagePrefix
+                               . '-success' );
+                       $out->returnToMain();
+               }
+       }
+
+       /**
+        * @return string|null
+        */
+       protected function getReturnUrl() {
+               $request = $this->getRequest();
+               $returnTo = $request->getText( 'returnto' );
+               $returnToQuery = $request->getText( 'returntoquery', '' );
+
+               if ( !$returnTo ) {
+                       return null;
+               }
+
+               $title = Title::newFromText( $returnTo );
+               return $title->getFullURL( $returnToQuery );
+       }
+
+       protected function getRequestBlacklist() {
+               return $this->getConfig()->get( 'ChangeCredentialsBlacklist' );
+       }
+}
index b35446d..785447f 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\Auth\AuthManager;
+
 /**
  * Let users change their email address.
  *
@@ -44,9 +46,7 @@ class SpecialChangeEmail extends FormSpecialPage {
         * @return bool
         */
        public function isListed() {
-               global $wgAuth;
-
-               return $wgAuth->allowPropChange( 'emailaddress' );
+               return AuthManager::singleton()->allowsPropertyChange( 'emailaddress' );
        }
 
        /**
@@ -54,6 +54,8 @@ class SpecialChangeEmail extends FormSpecialPage {
         * @param string $par
         */
        function execute( $par ) {
+               $this->checkLoginSecurityLevel();
+
                $out = $this->getOutput();
                $out->disallowUserJs();
 
@@ -61,9 +63,8 @@ class SpecialChangeEmail extends FormSpecialPage {
        }
 
        protected function checkExecutePermissions( User $user ) {
-               global $wgAuth;
 
-               if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
+               if ( !AuthManager::singleton()->allowsPropertyChange( 'emailaddress' ) ) {
                        throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
                }
 
@@ -100,13 +101,6 @@ class SpecialChangeEmail extends FormSpecialPage {
                        ],
                ];
 
-               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
-                       $fields['Password'] = [
-                               'type' => 'password',
-                               'label-message' => 'changeemail-password'
-                       ];
-               }
-
                return $fields;
        }
 
@@ -121,14 +115,10 @@ class SpecialChangeEmail extends FormSpecialPage {
                $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
 
                $form->addHeaderText( $this->msg( 'changeemail-header' )->parseAsBlock() );
-               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
-                       $form->addHeaderText( $this->msg( 'changeemail-passwordrequired' )->parseAsBlock() );
-               }
        }
 
        public function onSubmit( array $data ) {
-               $password = isset( $data['Password'] ) ? $data['Password'] : null;
-               $status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] );
+               $status = $this->attemptChange( $this->getUser(), $data['NewEmail'] );
 
                $this->status = $status;
 
@@ -158,12 +148,11 @@ class SpecialChangeEmail extends FormSpecialPage {
 
        /**
         * @param User $user
-        * @param string $pass
         * @param string $newaddr
         * @return Status
         */
-       private function attemptChange( User $user, $pass, $newaddr ) {
-               global $wgAuth;
+       private function attemptChange( User $user, $newaddr ) {
+               $authManager = AuthManager::singleton();
 
                if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) {
                        return Status::newFatal( 'invalidemailaddress' );
@@ -173,24 +162,6 @@ class SpecialChangeEmail extends FormSpecialPage {
                        return Status::newFatal( 'changeemail-nochange' );
                }
 
-               $throttleInfo = LoginForm::incrementLoginThrottle( $user->getName() );
-               if ( $throttleInfo ) {
-                       $lang = $this->getLanguage();
-                       return Status::newFatal(
-                               'changeemail-throttled',
-                               $lang->formatDuration( $throttleInfo['wait'] )
-                       );
-               }
-
-               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' )
-                       && !$user->checkTemporaryPassword( $pass )
-                       && !$user->checkPassword( $pass )
-               ) {
-                       return Status::newFatal( 'wrongpassword' );
-               }
-
-               LoginForm::clearLoginThrottle( $user->getName() );
-
                $oldaddr = $user->getEmail();
                $status = $user->setEmailWithConfirmation( $newaddr );
                if ( !$status->isGood() ) {
@@ -200,8 +171,7 @@ class SpecialChangeEmail extends FormSpecialPage {
                Hooks::run( 'PrefsEmailAudit', [ $user, $oldaddr, $newaddr ] );
 
                $user->saveSettings();
-
-               $wgAuth->updateExternalDB( $user );
+               MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
 
                return $status;
        }
index 5adc315..ce769bf 100644 (file)
  * @ingroup SpecialPage
  */
 
+use MediaWiki\Auth\PasswordAuthenticationRequest;
+
 /**
  * Let users recover their password.
  *
  * @ingroup SpecialPage
  */
-class SpecialChangePassword extends FormSpecialPage {
-       protected $mUserName;
-       protected $mDomain;
-
-       // Optional Wikitext Message to show above the password change form
-       protected $mPreTextMessage = null;
-
-       // label for old password input
-       protected $mOldPassMsg = null;
-
+class SpecialChangePassword extends SpecialRedirectToSpecial {
        public function __construct() {
-               parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
-               $this->listed( false );
-       }
-
-       public function doesWrites() {
-               return true;
-       }
-
-       /**
-        * Main execution point
-        * @param string|null $par
-        */
-       function execute( $par ) {
-               $this->getOutput()->disallowUserJs();
-
-               parent::execute( $par );
-       }
-
-       protected function checkExecutePermissions( User $user ) {
-               parent::checkExecutePermissions( $user );
-
-               if ( !$this->getRequest()->wasPosted() ) {
-                       $this->requireLogin( 'resetpass-no-info' );
-               }
-       }
-
-       /**
-        * Set a message at the top of the Change Password form
-        * @since 1.23
-        * @param Message $msg Message to parse and add to the form header
-        */
-       public function setChangeMessage( Message $msg ) {
-               $this->mPreTextMessage = $msg;
-       }
-
-       /**
-        * Set a message at the top of the Change Password form
-        * @since 1.23
-        * @param string $msg Message label for old/temp password field
-        */
-       public function setOldPasswordMessage( $msg ) {
-               $this->mOldPassMsg = $msg;
-       }
-
-       protected function getFormFields() {
-               $user = $this->getUser();
-               $request = $this->getRequest();
-
-               $oldpassMsg = $this->mOldPassMsg;
-               if ( $oldpassMsg === null ) {
-                       $oldpassMsg = $user->isLoggedIn() ? 'oldpassword' : 'resetpass-temp-password';
-               }
-
-               $fields = [
-                       'Name' => [
-                               'type' => 'info',
-                               'label-message' => 'username',
-                               'default' => $request->getVal( 'wpName', $user->getName() ),
-                       ],
-                       'Password' => [
-                               'type' => 'password',
-                               'label-message' => $oldpassMsg,
-                       ],
-                       'NewPassword' => [
-                               'type' => 'password',
-                               'label-message' => 'newpassword',
-                       ],
-                       'Retype' => [
-                               'type' => 'password',
-                               'label-message' => 'retypenew',
-                       ],
-               ];
-
-               if ( !$this->getUser()->isLoggedIn() ) {
-                       $fields['LoginOnChangeToken'] = [
-                               'type' => 'hidden',
-                               'label' => 'Change Password Token',
-                               'default' => LoginForm::getLoginToken()->toString(),
-                       ];
-               }
-
-               $extraFields = [];
-               Hooks::run( 'ChangePasswordForm', [ &$extraFields ] );
-               foreach ( $extraFields as $extra ) {
-                       list( $name, $label, $type, $default ) = $extra;
-                       $fields[$name] = [
-                               'type' => $type,
-                               'name' => $name,
-                               'label-message' => $label,
-                               'default' => $default,
-                       ];
-               }
-
-               if ( !$user->isLoggedIn() ) {
-                       $fields['Remember'] = [
-                               'type' => 'check',
-                               'label' => $this->msg( 'remembermypassword' )
-                                               ->numParams(
-                                                       ceil( $this->getConfig()->get( 'CookieExpiration' ) / ( 3600 * 24 ) )
-                                               )->text(),
-                               'default' => $request->getVal( 'wpRemember' ),
-                       ];
-               }
-
-               return $fields;
-       }
-
-       protected function alterForm( HTMLForm $form ) {
-               $form->setId( 'mw-resetpass-form' );
-               $form->setTableId( 'mw-resetpass-table' );
-               $form->setWrapperLegendMsg( 'resetpass_header' );
-               $form->setSubmitTextMsg(
-                       $this->getUser()->isLoggedIn()
-                               ? 'resetpass-submit-loggedin'
-                               : 'resetpass_submit'
-               );
-               $form->addButton( [
-                       'name' => 'wpCancel',
-                       'value' => $this->msg( 'resetpass-submit-cancel' )->text()
-               ] );
-               $form->setHeaderText( $this->msg( 'resetpass_text' )->parseAsBlock() );
-               if ( $this->mPreTextMessage instanceof Message ) {
-                       $form->addPreText( $this->mPreTextMessage->parseAsBlock() );
-               }
-               $form->addHiddenFields(
-                       $this->getRequest()->getValues( 'wpName', 'wpDomain', 'returnto', 'returntoquery' ) );
-       }
-
-       public function onSubmit( array $data ) {
-               global $wgAuth;
-
-               $request = $this->getRequest();
-
-               if ( $request->getCheck( 'wpLoginToken' ) ) {
-                       // This comes from Special:Userlogin when logging in with a temporary password
-                       return false;
-               }
-
-               if ( !$this->getUser()->isLoggedIn()
-                       && !LoginForm::getLoginToken()->match( $request->getVal( 'wpLoginOnChangeToken' ) )
-               ) {
-                       // Potential CSRF (bug 62497)
-                       return false;
-               }
-
-               if ( $request->getCheck( 'wpCancel' ) ) {
-                       $returnto = $request->getVal( 'returnto' );
-                       $titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
-                       if ( !$titleObj instanceof Title ) {
-                               $titleObj = Title::newMainPage();
-                       }
-                       $query = $request->getVal( 'returntoquery' );
-                       $this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
-
-                       return true;
-               }
-
-               $this->mUserName = $request->getVal( 'wpName', $this->getUser()->getName() );
-               $this->mDomain = $wgAuth->getDomain();
-
-               if ( !$wgAuth->allowPasswordChange() ) {
-                       throw new ErrorPageError( 'changepassword', 'resetpass_forbidden' );
-               }
-
-               $status = $this->attemptReset( $data['Password'], $data['NewPassword'], $data['Retype'] );
-
-               return $status;
-       }
-
-       public function onSuccess() {
-               if ( $this->getUser()->isLoggedIn() ) {
-                       $this->getOutput()->wrapWikiMsg(
-                               "<div class=\"successbox\">\n$1\n</div>",
-                               'changepassword-success'
-                       );
-                       $this->getOutput()->returnToMain();
-               } else {
-                       $request = $this->getRequest();
-                       LoginForm::clearLoginToken();
-                       $token = LoginForm::getLoginToken()->toString();
-                       $data = [
-                               'action' => 'submitlogin',
-                               'wpName' => $this->mUserName,
-                               'wpDomain' => $this->mDomain,
-                               'wpLoginToken' => $token,
-                               'wpPassword' => $request->getVal( 'wpNewPassword' ),
-                       ] + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' );
-                       $login = new LoginForm( new DerivativeRequest( $request, $data, true ) );
-                       $login->setContext( $this->getContext() );
-                       $login->execute( null );
-               }
-       }
-
-       /**
-        * Checks the new password if it meets the requirements for passwords and set
-        * it as a current password, otherwise set the passed Status object to fatal
-        * and doesn't change anything
-        *
-        * @param string $oldpass The current (temporary) password.
-        * @param string $newpass The password to set.
-        * @param string $retype The string of the retype password field to check with newpass
-        * @return Status
-        */
-       protected function attemptReset( $oldpass, $newpass, $retype ) {
-               $isSelf = ( $this->mUserName === $this->getUser()->getName() );
-               if ( $isSelf ) {
-                       $user = $this->getUser();
-               } else {
-                       $user = User::newFromName( $this->mUserName );
-               }
-
-               if ( !$user || $user->isAnon() ) {
-                       return Status::newFatal( $this->msg( 'nosuchusershort', $this->mUserName ) );
-               }
-
-               if ( $newpass !== $retype ) {
-                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'badretype' ] );
-                       return Status::newFatal( $this->msg( 'badretype' ) );
-               }
-
-               $throttleInfo = LoginForm::incrementLoginThrottle( $this->mUserName );
-               if ( $throttleInfo ) {
-                       return Status::newFatal( $this->msg( 'changepassword-throttled' )
-                               ->durationParams( $throttleInfo['wait'] )
-                       );
-               }
-
-               // @todo Make these separate messages, since the message is written for both cases
-               if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) {
-                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'wrongpassword' ] );
-                       return Status::newFatal( $this->msg( 'resetpass-wrong-oldpass' ) );
-               }
-
-               // User is resetting their password to their old password
-               if ( $oldpass === $newpass ) {
-                       return Status::newFatal( $this->msg( 'resetpass-recycled' ) );
-               }
-
-               // Do AbortChangePassword after checking mOldpass, so we don't leak information
-               // by possibly aborting a new password before verifying the old password.
-               $abortMsg = 'resetpass-abort-generic';
-               if ( !Hooks::run( 'AbortChangePassword', [ $user, $oldpass, $newpass, &$abortMsg ] ) ) {
-                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'abortreset' ] );
-                       return Status::newFatal( $this->msg( $abortMsg ) );
-               }
-
-               // Please reset throttle for successful logins, thanks!
-               LoginForm::clearLoginThrottle( $this->mUserName );
-
-               try {
-                       $user->setPassword( $newpass );
-                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'success' ] );
-               } catch ( PasswordError $e ) {
-                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'error' ] );
-                       return Status::newFatal( new RawMessage( $e->getMessage() ) );
-               }
-
-               if ( $isSelf ) {
-                       // This is needed to keep the user connected since
-                       // changing the password also modifies the user's token.
-                       $remember = $this->getRequest()->getCookie( 'Token' ) !== null;
-                       $user->setCookies( null, null, $remember );
-               }
-               $user->saveSettings();
-               $this->resetPasswordExpiration( $user );
-               return Status::newGood();
-       }
-
-       public function requiresUnblock() {
-               return false;
-       }
-
-       protected function getGroupName() {
-               return 'users';
-       }
-
-       /**
-        * For resetting user password expiration, until AuthManager comes along
-        * @param User $user
-        */
-       private function resetPasswordExpiration( User $user ) {
-               global $wgPasswordExpirationDays;
-               $newExpire = null;
-               if ( $wgPasswordExpirationDays ) {
-                       $newExpire = wfTimestamp(
-                               TS_MW,
-                               time() + ( $wgPasswordExpirationDays * 24 * 3600 )
-                       );
-               }
-               // Give extensions a chance to force an expiration
-               Hooks::run( 'ResetPasswordExpiration', [ $this, &$newExpire ] );
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       [ 'user_password_expires' => $dbw->timestampOrNull( $newExpire ) ],
-                       [ 'user_id' => $user->getId() ],
-                       __METHOD__
-               );
-       }
-
-       protected function getDisplayFormat() {
-               return 'ooui';
+               parent::__construct( 'ChangePassword', 'ChangeCredentials',
+                       PasswordAuthenticationRequest::class, [ 'returnto', 'returntoquery' ] );
        }
 }
index 69ddcf9..b046bf9 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
+ * Implements Special:CreateAccount
  *
  * 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
  * @ingroup SpecialPage
  */
 
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LogLevel;
+
 /**
- * Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
- * @todo FIXME: This (and the rest of the login frontend) needs to die a horrible painful death
+ * Implements Special:CreateAccount
  *
  * @ingroup SpecialPage
  */
-class SpecialCreateAccount extends SpecialRedirectToSpecial {
-       function __construct() {
-               parent::__construct(
-                       'CreateAccount',
-                       'Userlogin',
-                       'signup',
-                       [ 'returnto', 'returntoquery', 'uselang' ]
-               );
-       }
+class SpecialCreateAccount extends LoginSignupSpecialPage {
+       protected static $allowedActions = [
+               AuthManager::ACTION_CREATE,
+               AuthManager::ACTION_CREATE_CONTINUE
+       ];
 
-       public function doesWrites() {
-               return true;
+       protected static $messages = [
+               'authform-newtoken' => 'nocookiesfornew',
+               'authform-notoken' => 'sessionfailure',
+               'authform-wrongtoken' => 'sessionfailure',
+       ];
+
+       public function __construct() {
+               parent::__construct( 'CreateAccount' );
        }
 
-       // No reason to hide this link on Special:Specialpages
-       public function isListed() {
+       public function doesWrites() {
                return true;
        }
 
@@ -54,7 +58,112 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial {
                return $user->isAllowed( 'createaccount' );
        }
 
+       public function checkPermissions() {
+               parent::checkPermissions();
+
+               $user = $this->getUser();
+               $status = AuthManager::singleton()->checkAccountCreatePermissions( $user );
+               if ( !$status->isGood() ) {
+                       throw new ErrorPageError( 'createacct-error', $status->getMessage() );
+               }
+       }
+
+       protected function getLoginSecurityLevel() {
+               return false;
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_CREATE;
+       }
+
+       public function getDescription() {
+               return $this->msg( 'createaccount' )->text();
+       }
+
+       protected function isSignup() {
+               return true;
+       }
+
+       /**
+        * Run any hooks registered for logins, then display a message welcoming
+        * the user.
+        * @param bool $direct True if the action was successful just now; false if that happened
+        *    pre-redirection (so this handler was called already)
+        * @param StatusValue|null $extraMessages
+        */
+       protected function successfulAction( $direct = false, $extraMessages = null ) {
+               $session = $this->getRequest()->getSession();
+               $user = $this->targetUser ?: $this->getUser();
+
+               if ( $direct ) {
+                       # Only save preferences if the user is not creating an account for someone else.
+                       if ( !$this->proxyAccountCreation ) {
+                               Hooks::run( 'AddNewAccount', [ $user, false ] );
+
+                               // If the user does not have a session cookie at this point, they probably need to
+                               // do something to their browser.
+                               if ( !$this->hasSessionCookie() ) {
+                                       $this->mainLoginForm( [ /*?*/ ], $session->getProvider()->whyNoSession() );
+                                       // TODO something more specific? This used to use nocookiesnew
+                                       // FIXME should redirect to login page instead?
+                                       return;
+                               }
+                       } else {
+                               $byEmail = false; // FIXME no way to set this
+
+                               Hooks::run( 'AddNewAccount', [ $user, $byEmail ] );
+
+                               $out = $this->getOutput();
+                               $out->setPageTitle( $this->msg( $byEmail ? 'accmailtitle' : 'accountcreated' ) );
+                               if ( $byEmail ) {
+                                       $out->addWikiMsg( 'accmailtext', $user->getName(), $user->getEmail() );
+                               } else {
+                                       $out->addWikiMsg( 'accountcreatedtext', $user->getName() );
+                               }
+                               $out->addReturnTo( $this->getPageTitle() );
+                               return;
+                       }
+               }
+
+               $this->clearToken();
+
+               # Run any hooks; display injected HTML
+               $injected_html = '';
+               $welcome_creation_msg = 'welcomecreation-msg';
+               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
+
+               /**
+                * Let any extensions change what message is shown.
+                * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeWelcomeCreation
+                * @since 1.18
+                */
+               Hooks::run( 'BeforeWelcomeCreation', [ &$welcome_creation_msg, &$injected_html ] );
+
+               $this->showSuccessPage( 'signup', $this->msg( 'welcomeuser', $this->getUser()->getName() ),
+                       $welcome_creation_msg, $injected_html, $extraMessages );
+       }
+
+       protected function getToken() {
+               return $this->getRequest()->getSession()->getToken( '', 'createaccount' );
+       }
+
+       protected function clearToken() {
+               return $this->getRequest()->getSession()->resetToken( 'createaccount' );
+       }
+
+       protected function getTokenName() {
+               return 'wpCreateaccountToken';
+       }
+
        protected function getGroupName() {
                return 'login';
        }
+
+       protected function logAuthResult( $success, $status = null ) {
+               LoggerFactory::getInstance( 'authmanager-stats' )->info( 'Account creation attempt', [
+                       'event' => 'accountcreation',
+                       'successful' => $success,
+                       'status' => $status,
+               ] );
+       }
 }
diff --git a/includes/specials/SpecialLinkAccounts.php b/includes/specials/SpecialLinkAccounts.php
new file mode 100644 (file)
index 0000000..da10b90
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Links/unlinks external accounts to the current user.
+ *
+ * To interact with this page, account providers need to register themselves with AuthManager.
+ */
+class SpecialLinkAccounts extends AuthManagerSpecialPage {
+       protected static $allowedActions = [
+               AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
+       ];
+
+       public function __construct() {
+               parent::__construct( 'LinkAccounts' );
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       public function isListed() {
+               return AuthManager::singleton()->canLinkAccounts();
+       }
+
+       protected function getRequestBlacklist() {
+               return $this->getConfig()->get( 'ChangeCredentialsBlacklist' );
+       }
+
+       /**
+        * @param null|string $subPage
+        * @throws MWException
+        * @throws PermissionsError
+        */
+       public function execute( $subPage ) {
+               $this->setHeaders();
+               $this->loadAuth( $subPage );
+
+               if ( !$this->isActionAllowed( $this->authAction ) ) {
+                       if ( $this->authAction === AuthManager::ACTION_LINK ) {
+                               // looks like no linking provider is installed or willing to take this user
+                               $titleMessage = wfMessage( 'cannotlink-no-provider-title' );
+                               $errorMessage = wfMessage( 'cannotlink-no-provider' );
+                               throw new ErrorPageError( $titleMessage, $errorMessage );
+                       } else {
+                               // user probably back-button-navigated into an auth session that no longer exists
+                               // FIXME would be nice to show a message
+                               $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( '', false,
+                                       PROTO_HTTPS ) );
+                               return;
+                       }
+               }
+
+               $this->outputHeader();
+
+               $status = $this->trySubmit();
+
+               if ( $status === false || !$status->isOK() ) {
+                       $this->displayForm( $status );
+                       return;
+               }
+
+               $response = $status->getValue();
+
+               switch ( $response->status ) {
+                       case AuthenticationResponse::PASS:
+                               $this->success();
+                               break;
+                       case AuthenticationResponse::FAIL:
+                               $this->loadAuth( '', AuthManager::ACTION_LINK, true );
+                               $this->displayForm( StatusValue::newFatal( $response->message ) );
+                               break;
+                       case AuthenticationResponse::REDIRECT:
+                               $this->getOutput()->redirect( $response->redirectTarget );
+                               break;
+                       case AuthenticationResponse::UI:
+                               $this->authAction = AuthManager::ACTION_LINK_CONTINUE;
+                               $this->authRequests = $response->neededRequests;
+                               $this->displayForm( StatusValue::newFatal( $response->message ) );
+                               break;
+                       default:
+                               throw new LogicException( 'invalid AuthenticationResponse' );
+               }
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_LINK;
+       }
+
+       /**
+        * @param AuthenticationRequest[] $requests
+        * @param string $action AuthManager action name, should be ACTION_LINK or ACTION_LINK_CONTINUE
+        * @return HTMLForm
+        */
+       protected function getAuthForm( array $requests, $action ) {
+               $form = parent::getAuthForm( $requests, $action );
+               $form->setSubmitTextMsg( 'linkaccounts-submit' );
+               return $form;
+       }
+
+       /**
+        * Show a success message.
+        */
+       protected function success() {
+               $this->loadAuth( '', AuthManager::ACTION_LINK, true );
+               $this->displayForm( StatusValue::newFatal( $this->msg( 'linkaccounts-success-text' ) ) );
+       }
+}
index c3ed91f..9746ef6 100644 (file)
  * @ingroup SpecialPage
  */
 
+use MediaWiki\Auth\AuthManager;
+
 /**
- * Special page for requesting a password reset email
+ * Special page for requesting a password reset email.
+ *
+ * Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
+ * EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
+ * functionality) to be enabled.
  *
  * @ingroup SpecialPage
  */
 class SpecialPasswordReset extends FormSpecialPage {
-       /**
-        * @var Message
-        */
-       private $email;
+       /** @var PasswordReset */
+       private $passwordReset;
 
        /**
-        * @var User
+        * @var string[] Temporary storage for the passwords which have been sent out, keyed by username.
         */
-       private $firstUser;
+       private $passwords = [];
 
        /**
         * @var Status
@@ -49,6 +53,7 @@ class SpecialPasswordReset extends FormSpecialPage {
 
        public function __construct() {
                parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
+               $this->passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
        }
 
        public function doesWrites() {
@@ -56,22 +61,19 @@ class SpecialPasswordReset extends FormSpecialPage {
        }
 
        public function userCanExecute( User $user ) {
-               return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
+               return $this->passwordReset->isAllowed( $user )->isGood();
        }
 
        public function checkExecutePermissions( User $user ) {
-               $error = $this->canChangePassword( $user );
-               if ( is_string( $error ) ) {
-                       throw new ErrorPageError( 'internalerror', $error );
-               } elseif ( !$error ) {
-                       throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
+               $status = Status::wrap( $this->passwordReset->isAllowed( $user ) );
+               if ( !$status->isGood() ) {
+                       throw new ErrorPageError( 'internalerror', $status->getMessage() );
                }
 
                parent::checkExecutePermissions( $user );
        }
 
        protected function getFormFields() {
-               global $wgAuth;
                $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
                $a = [];
                if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
@@ -92,15 +94,6 @@ class SpecialPasswordReset extends FormSpecialPage {
                        ];
                }
 
-               if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
-                       $domains = $wgAuth->domainList();
-                       $a['Domain'] = [
-                               'type' => 'select',
-                               'options' => $domains,
-                               'label-message' => 'passwordreset-domain',
-                       ];
-               }
-
                if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
                        $a['Capture'] = [
                                'type' => 'check',
@@ -128,9 +121,6 @@ class SpecialPasswordReset extends FormSpecialPage {
                if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
                        $i++;
                }
-               if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
-                       $i++;
-               }
 
                $message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
 
@@ -145,180 +135,54 @@ class SpecialPasswordReset extends FormSpecialPage {
         * @param array $data
         * @throws MWException
         * @throws ThrottledError|PermissionsError
-        * @return bool|array
+        * @return Status
         */
        public function onSubmit( array $data ) {
-               global $wgAuth, $wgMinimalPasswordLength;
-
-               if ( isset( $data['Domain'] ) ) {
-                       if ( $wgAuth->validDomain( $data['Domain'] ) ) {
-                               $wgAuth->setDomain( $data['Domain'] );
-                       } else {
-                               $wgAuth->setDomain( 'invaliddomain' );
-                       }
-               }
-
                if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) {
                        // The user knows they don't have the passwordreset permission,
                        // but they tried to spoof the form. That's naughty
                        throw new PermissionsError( 'passwordreset' );
                }
 
-               /**
-                * @var $firstUser User
-                * @var $users User[]
-                */
-
-               if ( isset( $data['Username'] ) && $data['Username'] !== '' ) {
-                       $method = 'username';
-                       $users = [ User::newFromName( $data['Username'] ) ];
-               } elseif ( isset( $data['Email'] )
-                       && $data['Email'] !== ''
-                       && Sanitizer::validateEmail( $data['Email'] )
-               ) {
-                       $method = 'email';
-                       $res = wfGetDB( DB_SLAVE )->select(
-                               'user',
-                               User::selectFields(),
-                               [ 'user_email' => $data['Email'] ],
-                               __METHOD__
-                       );
-
-                       if ( $res ) {
-                               $users = [];
-
-                               foreach ( $res as $row ) {
-                                       $users[] = User::newFromRow( $row );
-                               }
-                       } else {
-                               // Some sort of database error, probably unreachable
-                               throw new MWException( 'Unknown database error in ' . __METHOD__ );
-                       }
-               } else {
-                       // The user didn't supply any data
-                       return false;
-               }
-
-               // Check for hooks (captcha etc), and allow them to modify the users list
-               $error = [];
-               if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
-                       return [ $error ];
-               }
-
-               $this->method = $method;
-
-               if ( count( $users ) == 0 ) {
-                       if ( $method == 'email' ) {
-                               // Don't reveal whether or not an email address is in use
-                               return true;
-                       } else {
-                               return [ 'noname' ];
-                       }
-               }
-
-               $firstUser = $users[0];
+               $username = isset( $data['Username'] ) ? $data['Username'] : null;
+               $email = isset( $data['Email'] ) ? $data['Email'] : null;
+               $capture = !empty( $data['Capture'] );
 
-               if ( !$firstUser instanceof User || !$firstUser->getId() ) {
-                       // Don't parse username as wikitext (bug 65501)
-                       return [ [ 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ] ];
+               $this->method = $username ? 'username' : 'email';
+               $this->result = Status::wrap(
+                       $this->passwordReset->execute( $this->getUser(), $username, $email, $capture ) );
+               if ( $capture && $this->result->isOK() ) {
+                       $this->passwords = $this->result->getValue();
                }
 
-               // Check against the rate limiter
-               if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
+               if ( $this->result->hasMessage( 'actionthrottledtext' ) ) {
                        throw new ThrottledError;
                }
 
-               // Check against password throttle
-               foreach ( $users as $user ) {
-                       if ( $user->isPasswordReminderThrottled() ) {
-
-                               # Round the time in hours to 3 d.p., in case someone is specifying
-                               # minutes or seconds.
-                               return [ [
-                                       'throttled-mailpassword',
-                                       round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 )
-                               ] ];
-                       }
-               }
-
-               // All the users will have the same email address
-               if ( $firstUser->getEmail() == '' ) {
-                       // This won't be reachable from the email route, so safe to expose the username
-                       return [ [ 'noemail', wfEscapeWikiText( $firstUser->getName() ) ] ];
-               }
-
-               // We need to have a valid IP address for the hook, but per bug 18347, we should
-               // send the user's name if they're logged in.
-               $ip = $this->getRequest()->getIP();
-               if ( !$ip ) {
-                       return [ 'badipaddress' ];
-               }
-               $caller = $this->getUser();
-               Hooks::run( 'User::mailPasswordInternal', [ &$caller, &$ip, &$firstUser ] );
-               $username = $caller->getName();
-               $msg = IP::isValid( $username )
-                       ? 'passwordreset-emailtext-ip'
-                       : 'passwordreset-emailtext-user';
-
-               // Send in the user's language; which should hopefully be the same
-               $userLanguage = $firstUser->getOption( 'language' );
-
-               $passwords = [];
-               foreach ( $users as $user ) {
-                       $password = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
-                       $user->setNewpassword( $password );
-                       $user->saveSettings();
-                       $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password )
-                               ->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
-               }
-               $passwordBlock = implode( "\n\n", $passwords );
-
-               $this->email = $this->msg( $msg )->inLanguage( $userLanguage );
-               $this->email->params(
-                       $username,
-                       $passwordBlock,
-                       count( $passwords ),
-                       '<' . Title::newMainPage()->getCanonicalURL() . '>',
-                       round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 )
-               );
-
-               $title = $this->msg( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
-
-               $this->result = $firstUser->sendMail( $title->text(), $this->email->text() );
-
-               if ( isset( $data['Capture'] ) && $data['Capture'] ) {
-                       // Save the user, will be used if an error occurs when sending the email
-                       $this->firstUser = $firstUser;
-               } else {
-                       // Blank the email if the user is not supposed to see it
-                       $this->email = null;
-               }
-
-               if ( $this->result->isGood() ) {
-                       return true;
-               } elseif ( isset( $data['Capture'] ) && $data['Capture'] ) {
-                       // The email didn't send, but maybe they knew that and that's why they captured it
-                       return true;
-               } else {
-                       // @todo FIXME: The email wasn't sent, but we have already set
-                       // the password throttle timestamp, so they won't be able to try
-                       // again until it expires...  :(
-                       return [ [ 'mailerror', $this->result->getMessage() ] ];
-               }
+               return $this->result;
        }
 
        public function onSuccess() {
-               if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
+               if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->passwords ) {
                        // @todo Logging
 
                        if ( $this->result->isGood() ) {
-                               $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
+                               $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture2',
+                                       count( $this->passwords ) );
                        } else {
-                               $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
-                                       $this->result->getMessage(), $this->firstUser->getName() );
+                               $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture2',
+                                       $this->result->getMessage(), key( $this->passwords ), count( $this->passwords ) );
                        }
 
-                       $this->getOutput()->addHTML( Html::rawElement( 'pre', [], $this->email->escaped() ) );
+                       $this->getOutput()->addHTML( Html::openElement( 'ul' ) );
+                       foreach ( $this->passwords as $username => $pwd ) {
+                               $this->getOutput()->addHTML( Html::rawElement( 'li', [],
+                                       htmlspecialchars( $username, ENT_QUOTES )
+                                       . $this->msg( 'colon-separator' )->text()
+                                       . htmlspecialchars( $pwd, ENT_QUOTES )
+                               ) );
+                       }
+                       $this->getOutput()->addHTML( Html::closeElement( 'ul' ) );
                }
 
                if ( $this->method === 'email' ) {
@@ -330,42 +194,12 @@ class SpecialPasswordReset extends FormSpecialPage {
                $this->getOutput()->returnToMain();
        }
 
-       protected function canChangePassword( User $user ) {
-               global $wgAuth;
-               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
-
-               // Maybe password resets are disabled, or there are no allowable routes
-               if ( !is_array( $resetRoutes ) ||
-                       !in_array( true, array_values( $resetRoutes ) )
-               ) {
-                       return 'passwordreset-disabled';
-               }
-
-               // Maybe the external auth plugin won't allow local password changes
-               if ( !$wgAuth->allowPasswordChange() ) {
-                       return 'resetpass_forbidden';
-               }
-
-               // Maybe email features have been disabled
-               if ( !$this->getConfig()->get( 'EnableEmail' ) ) {
-                       return 'passwordreset-emaildisabled';
-               }
-
-               // Maybe the user is blocked (check this here rather than relying on the parent
-               // method as we have a more specific error message to use here
-               if ( $user->isBlocked() ) {
-                       return 'blocked-mailpassword';
-               }
-
-               return true;
-       }
-
        /**
         * Hide the password reset page if resets are disabled.
         * @return bool
         */
-       function isListed() {
-               if ( $this->canChangePassword( $this->getUser() ) === true ) {
+       public function isListed() {
+               if ( $this->passwordReset->isAllowed( $this->getUser() )->isGood() ) {
                        return parent::isListed();
                }
 
diff --git a/includes/specials/SpecialRemoveCredentials.php b/includes/specials/SpecialRemoveCredentials.php
new file mode 100644 (file)
index 0000000..4efec03
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Special change to remove credentials (such as a two-factor token).
+ */
+class SpecialRemoveCredentials extends SpecialChangeCredentials {
+       protected static $allowedActions = [ AuthManager::ACTION_REMOVE ];
+
+       protected static $messagePrefix = 'removecredentials';
+
+       protected static $loadUserData = false;
+
+       public function __construct() {
+               parent::__construct( 'RemoveCredentials' );
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_REMOVE;
+       }
+
+       protected function getRequestBlacklist() {
+               return $this->getConfig()->get( 'RemoveCredentialsBlacklist' );
+       }
+}
index e79fd6e..2139949 100644 (file)
@@ -77,6 +77,7 @@ class SpecialTags extends SpecialPage {
 
                $user = $this->getUser();
                $userCanManage = $user->isAllowed( 'managechangetags' );
+               $userCanDelete = $user->isAllowed( 'deletechangetags' );
                $userCanEditInterface = $user->isAllowed( 'editinterface' );
 
                // Show form to create a tag
@@ -154,12 +155,13 @@ class SpecialTags extends SpecialPage {
 
                // Insert tags that have been applied at least once
                foreach ( $tagStats as $tag => $hitcount ) {
-                       $html .= $this->doTagRow( $tag, $hitcount, $userCanManage, $userCanEditInterface );
+                       $html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
+                               $userCanDelete, $userCanEditInterface );
                }
                // Insert tags defined somewhere but never applied
                foreach ( $definedTags as $tag ) {
                        if ( !isset( $tagStats[$tag] ) ) {
-                               $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanEditInterface );
+                               $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
                        }
                }
 
@@ -170,7 +172,7 @@ class SpecialTags extends SpecialPage {
                ) );
        }
 
-       function doTagRow( $tag, $hitcount, $showActions, $showEditLinks ) {
+       function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
                $newRow = '';
                $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
 
@@ -229,16 +231,17 @@ class SpecialTags extends SpecialPage {
                $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
 
                // actions
-               if ( $showActions ) { // we've already checked that the user had the requisite userright
-                       $actionLinks = [];
+               $actionLinks = [];
 
-                       // delete
-                       if ( ChangeTags::canDeleteTag( $tag )->isOK() ) {
-                               $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ),
-                                       $this->msg( 'tags-delete' )->escaped(),
-                                       [],
-                                       [ 'tag' => $tag ] );
-                       }
+               // delete
+               if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
+                       $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ),
+                               $this->msg( 'tags-delete' )->escaped(),
+                               [],
+                               [ 'tag' => $tag ] );
+               }
+
+               if ( $showManageActions ) { // we've already checked that the user had the requisite userright
 
                        // activate
                        if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
@@ -256,6 +259,9 @@ class SpecialTags extends SpecialPage {
                                        [ 'tag' => $tag ] );
                        }
 
+               }
+
+               if ( $actionLinks ) {
                        $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
                }
 
@@ -319,8 +325,8 @@ class SpecialTags extends SpecialPage {
 
        protected function showDeleteTagForm( $tag ) {
                $user = $this->getUser();
-               if ( !$user->isAllowed( 'managechangetags' ) ) {
-                       throw new PermissionsError( 'managechangetags' );
+               if ( !$user->isAllowed( 'deletechangetags' ) ) {
+                       throw new PermissionsError( 'deletechangetags' );
                }
 
                $out = $this->getOutput();
diff --git a/includes/specials/SpecialUnlinkAccounts.php b/includes/specials/SpecialUnlinkAccounts.php
new file mode 100644 (file)
index 0000000..86bc7ed
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Session\SessionManager;
+
+class SpecialUnlinkAccounts extends AuthManagerSpecialPage {
+       protected static $allowedActions = [ AuthManager::ACTION_UNLINK ];
+
+       public function __construct() {
+               parent::__construct( 'UnlinkAccounts' );
+       }
+
+       protected function getLoginSecurityLevel() {
+               return 'UnlinkAccount';
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_UNLINK;
+       }
+
+       /**
+        * Under which header this special page is listed in Special:SpecialPages.
+        */
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       public function isListed() {
+               return AuthManager::singleton()->canLinkAccounts();
+       }
+
+       protected function getRequestBlacklist() {
+               return $this->getConfig()->get( 'RemoveCredentialsBlacklist' );
+       }
+
+       public function execute( $subPage ) {
+               $this->setHeaders();
+               $this->loadAuth( $subPage );
+               $this->outputHeader();
+
+               $status = $this->trySubmit();
+
+               if ( $status === false || !$status->isOK() ) {
+                       $this->displayForm( $status );
+                       return;
+               }
+
+               /** @var AuthenticationResponse $response */
+               $response = $status->getValue();
+
+               if ( $response->status === AuthenticationResponse::FAIL ) {
+                       $this->displayForm( StatusValue::newFatal( $response->message ) );
+                       return;
+               }
+
+               $status = StatusValue::newGood();
+               $status->warning( wfMessage( 'unlinkaccounts-success' ) );
+               $this->loadAuth( $subPage, null, true ); // update requests so the unlinked one doesn't show up
+
+               // Reset sessions - if the user unlinked an account because it was compromised,
+               // log attackers out from sessions obtained via that account.
+               $session = $this->getRequest()->getSession();
+               $user = $this->getUser();
+               SessionManager::singleton()->invalidateSessionsForUser( $user );
+               $session->setUser( $user );
+               $session->resetId();
+
+               $this->displayForm( $status );
+       }
+
+       public function handleFormSubmit( $data ) {
+               // unlink requests do not accept user input so repeat parent code but skip call to
+               // AuthenticationRequest::loadRequestsFromSubmission
+               $response = $this->performAuthenticationStep( $this->authAction, $this->authRequests );
+               return Status::newGood( $response );
+       }
+}
diff --git a/includes/specials/SpecialUserLogin.php b/includes/specials/SpecialUserLogin.php
new file mode 100644 (file)
index 0000000..28c68aa
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Implements Special:UserLogin
+ *
+ * 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LogLevel;
+
+/**
+ * Implements Special:UserLogin
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialUserLogin extends LoginSignupSpecialPage {
+       protected static $allowedActions = [
+               AuthManager::ACTION_LOGIN,
+               AuthManager::ACTION_LOGIN_CONTINUE
+       ];
+
+       protected static $messages = [
+               'authform-newtoken' => 'nocookiesforlogin',
+               'authform-notoken' => 'sessionfailure',
+               'authform-wrongtoken' => 'sessionfailure',
+       ];
+
+       public function __construct() {
+               parent::__construct( 'Userlogin' );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       protected function getLoginSecurityLevel() {
+               return false;
+       }
+
+       protected function getDefaultAction( $subPage ) {
+               return AuthManager::ACTION_LOGIN;
+       }
+
+       public function getDescription() {
+               return $this->msg( 'login' )->text();
+       }
+
+       public function setHeaders() {
+               // override the page title if we are doing a forced reauthentication
+               parent::setHeaders();
+               if ( $this->securityLevel && $this->getUser()->isLoggedIn() ) {
+                       $this->getOutput()->setPageTitle( $this->msg( 'login-security' ) );
+               }
+       }
+
+       protected function isSignup() {
+               return false;
+       }
+
+       protected function beforeExecute( $subPage ) {
+               if ( $subPage === 'signup' || $this->getRequest()->getText( 'type' ) === 'signup' ) {
+                       // B/C for old account creation URLs
+                       $title = SpecialPage::getTitleFor( 'CreateAccount' );
+                       $query = array_diff_key( $this->getRequest()->getValues(),
+                               array_fill_keys( [ 'type', 'title' ], true ) );
+                       $url = $title->getFullURL( $query, false, PROTO_CURRENT );
+                       $this->getOutput()->redirect( $url );
+                       return false;
+               }
+               return parent::beforeExecute( $subPage );
+       }
+
+       /**
+        * Run any hooks registered for logins, then HTTP redirect to
+        * $this->mReturnTo (or Main Page if that's undefined).  Formerly we had a
+        * nice message here, but that's really not as useful as just being sent to
+        * wherever you logged in from.  It should be clear that the action was
+        * successful, given the lack of error messages plus the appearance of your
+        * name in the upper right.
+        * @param bool $direct True if the action was successful just now; false if that happened
+        *    pre-redirection (so this handler was called already)
+        * @param StatusValue|null $extraMessages
+        */
+       protected function successfulAction( $direct = false, $extraMessages = null ) {
+               global $wgSecureLogin;
+
+               $user = $this->targetUser ?: $this->getUser();
+               $session = $this->getRequest()->getSession();
+
+               if ( $direct ) {
+                       $user->touch();
+
+                       $this->clearToken();
+
+                       if ( $user->requiresHTTPS() ) {
+                               $this->mStickHTTPS = true;
+                       }
+                       $session->setForceHTTPS( $wgSecureLogin && $this->mStickHTTPS );
+
+                       // If the user does not have a session cookie at this point, they probably need to
+                       // do something to their browser.
+                       if ( !$this->hasSessionCookie() ) {
+                               $this->mainLoginForm( [ /*?*/ ], $session->getProvider()->whyNoSession() );
+                               // TODO something more specific? This used to use nocookieslogin
+                               return;
+                       }
+               }
+
+               # Run any hooks; display injected HTML if any, else redirect
+               $injected_html = '';
+               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
+
+               if ( $injected_html !== '' || $extraMessages ) {
+                       $this->showSuccessPage( 'success', $this->msg( 'loginsuccesstitle' ),
+                               'loginsuccess', $injected_html, $extraMessages );
+               } else {
+                       $helper = new LoginHelper( $this->getContext() );
+                       $helper->showReturnToPage( 'successredirect', $this->mReturnTo, $this->mReturnToQuery,
+                               $this->mStickHTTPS );
+               }
+       }
+
+       protected function getToken() {
+               return $this->getRequest()->getSession()->getToken( '', 'login' );
+       }
+
+       protected function clearToken() {
+               return $this->getRequest()->getSession()->resetToken( 'login' );
+       }
+
+       protected function getTokenName() {
+               return 'wpLoginToken';
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+
+       protected function logAuthResult( $success, $status = null ) {
+               LoggerFactory::getInstance( 'authmanager-stats' )->info( 'Login attempt', [
+                       'event' => 'login',
+                       'successful' => $success,
+                       'status' => $status,
+               ] );
+       }
+}
diff --git a/includes/specials/SpecialUserLogout.php b/includes/specials/SpecialUserLogout.php
new file mode 100644 (file)
index 0000000..c067f44
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Implements Special:Userlogout
+ *
+ * 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
+ */
+
+/**
+ * Implements Special:Userlogout
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialUserLogout extends UnlistedSpecialPage {
+       function __construct() {
+               parent::__construct( 'Userlogout' );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       function execute( $par ) {
+               /**
+                * Some satellite ISPs use broken precaching schemes that log people out straight after
+                * they're logged in (bug 17790). Luckily, there's a way to detect such requests.
+                */
+               if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
+                       wfDebug( "Special:UserLogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
+                       throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
+               }
+
+               $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',
+                               [
+                                       $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
+                               ]
+                       );
+               }
+
+               $user = $this->getUser();
+               $oldName = $user->getName();
+
+               $user->logout();
+
+               $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
+                       $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
+               $out = $this->getOutput();
+               $out->addWikiMsg( 'logouttext', $loginURL );
+
+               // Hook.
+               $injected_html = '';
+               Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
+               $out->addHTML( $injected_html );
+
+               $out->returnToMain();
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+}
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
deleted file mode 100644 (file)
index 45315a7..0000000
+++ /dev/null
@@ -1,1842 +0,0 @@
-<?php
-/**
- * Implements Special:UserLogin
- *
- * 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
- */
-use MediaWiki\Logger\LoggerFactory;
-use Psr\Log\LogLevel;
-use MediaWiki\Session\SessionManager;
-
-/**
- * Implements Special:UserLogin
- *
- * @ingroup SpecialPage
- */
-class LoginForm extends SpecialPage {
-       const SUCCESS = 0;
-       const NO_NAME = 1;
-       const ILLEGAL = 2;
-       const WRONG_PLUGIN_PASS = 3;
-       const NOT_EXISTS = 4;
-       const WRONG_PASS = 5;
-       const EMPTY_PASS = 6;
-       const RESET_PASS = 7;
-       const ABORTED = 8;
-       const CREATE_BLOCKED = 9;
-       const THROTTLED = 10;
-       const USER_BLOCKED = 11;
-       const NEED_TOKEN = 12;
-       const WRONG_TOKEN = 13;
-       const USER_MIGRATED = 14;
-
-       public static $statusCodes = [
-               self::SUCCESS => 'success',
-               self::NO_NAME => 'no_name',
-               self::ILLEGAL => 'illegal',
-               self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass',
-               self::NOT_EXISTS => 'not_exists',
-               self::WRONG_PASS => 'wrong_pass',
-               self::EMPTY_PASS => 'empty_pass',
-               self::RESET_PASS => 'reset_pass',
-               self::ABORTED => 'aborted',
-               self::CREATE_BLOCKED => 'create_blocked',
-               self::THROTTLED => 'throttled',
-               self::USER_BLOCKED => 'user_blocked',
-               self::NEED_TOKEN => 'need_token',
-               self::WRONG_TOKEN => 'wrong_token',
-               self::USER_MIGRATED => 'user_migrated',
-       ];
-
-       /**
-        * Valid error and warning messages
-        *
-        * Special:Userlogin can show an error or warning message on the form when
-        * coming from another page. This is done via the ?error= or ?warning= GET
-        * parameters.
-        *
-        * This array is the list of valid message keys. All other values will be
-        * ignored.
-        *
-        * @since 1.24
-        * @var string[]
-        */
-       public static $validErrorMessages = [
-               'exception-nologin-text',
-               'watchlistanontext',
-               'changeemail-no-info',
-               'resetpass-no-info',
-               'confirmemail_needlogin',
-               'prefsnologintext2',
-       ];
-
-       public $mAbortLoginErrorMsg = null;
-       /**
-        * @var int How many seconds user is throttled for
-        * @since 1.27
-        */
-       public $mThrottleWait = '?';
-
-       protected $mUsername;
-       protected $mPassword;
-       protected $mRetype;
-       protected $mReturnTo;
-       protected $mCookieCheck;
-       protected $mPosted;
-       protected $mAction;
-       protected $mCreateaccount;
-       protected $mCreateaccountMail;
-       protected $mLoginattempt;
-       protected $mRemember;
-       protected $mEmail;
-       protected $mDomain;
-       protected $mLanguage;
-       protected $mSkipCookieCheck;
-       protected $mReturnToQuery;
-       protected $mToken;
-       protected $mStickHTTPS;
-       protected $mType;
-       protected $mReason;
-       protected $mRealName;
-       protected $mEntryError = '';
-       protected $mEntryErrorType = 'error';
-
-       private $mTempPasswordUsed;
-       private $mLoaded = false;
-       private $mSecureLoginUrl;
-
-       /** @var WebRequest */
-       private $mOverrideRequest = null;
-
-       /** @var WebRequest Effective request; set at the beginning of load */
-       private $mRequest = null;
-
-       /**
-        * @param WebRequest $request
-        */
-       public function __construct( $request = null ) {
-               global $wgUseMediaWikiUIEverywhere;
-               parent::__construct( 'Userlogin' );
-
-               $this->mOverrideRequest = $request;
-               // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
-               $wgUseMediaWikiUIEverywhere = true;
-       }
-
-       public function doesWrites() {
-               return true;
-       }
-
-       /**
-        * Returns an array of all valid error messages.
-        *
-        * @return array
-        */
-       public static function getValidErrorMessages() {
-               static $messages = null;
-               if ( !$messages ) {
-                       $messages = self::$validErrorMessages;
-                       Hooks::run( 'LoginFormValidErrorMessages', [ &$messages ] );
-               }
-
-               return $messages;
-       }
-
-       /**
-        * Loader
-        */
-       function load() {
-               global $wgAuth, $wgHiddenPrefs, $wgEnableEmail;
-
-               if ( $this->mLoaded ) {
-                       return;
-               }
-               $this->mLoaded = true;
-
-               if ( $this->mOverrideRequest === null ) {
-                       $request = $this->getRequest();
-               } else {
-                       $request = $this->mOverrideRequest;
-               }
-               $this->mRequest = $request;
-
-               $this->mType = $request->getText( 'type' );
-               $this->mUsername = $request->getText( 'wpName' );
-               $this->mPassword = $request->getText( 'wpPassword' );
-               $this->mRetype = $request->getText( 'wpRetype' );
-               $this->mDomain = $request->getText( 'wpDomain' );
-               $this->mReason = $request->getText( 'wpReason' );
-               $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
-               $this->mPosted = $request->wasPosted();
-               $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
-                       && $wgEnableEmail;
-               $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ) && !$this->mCreateaccountMail;
-               $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
-               $this->mAction = $request->getVal( 'action' );
-               $this->mRemember = $request->getCheck( 'wpRemember' );
-               $this->mFromHTTP = $request->getBool( 'fromhttp', false )
-                       || $request->getBool( 'wpFromhttp', false );
-               $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
-                       || $request->getBool( 'wpForceHttps', false );
-               $this->mLanguage = $request->getText( 'uselang' );
-               $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
-               $this->mToken = $this->mType == 'signup'
-                       ? $request->getVal( 'wpCreateaccountToken' )
-                       : $request->getVal( 'wpLoginToken' );
-               $this->mReturnTo = $request->getVal( 'returnto', '' );
-               $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
-
-               // Show an error or warning passed on from a previous page
-               $entryError = $this->msg( $request->getVal( 'error', '' ) );
-               $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
-               // bc: provide login link as a parameter for messages where the translation
-               // was not updated
-               $loginreqlink = Linker::linkKnown(
-                       $this->getPageTitle(),
-                       $this->msg( 'loginreqlink' )->escaped(),
-                       [],
-                       [
-                               'returnto' => $this->mReturnTo,
-                               'returntoquery' => $this->mReturnToQuery,
-                               'uselang' => $this->mLanguage,
-                               'fromhttp' => $this->mFromHTTP ? '1' : '0',
-                       ]
-               );
-
-               // Only show valid error or warning messages.
-               if ( $entryError->exists()
-                       && in_array( $entryError->getKey(), self::getValidErrorMessages() )
-               ) {
-                       $this->mEntryErrorType = 'error';
-                       $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
-
-               } elseif ( $entryWarning->exists()
-                       && in_array( $entryWarning->getKey(), self::getValidErrorMessages() )
-               ) {
-                       $this->mEntryErrorType = 'warning';
-                       $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
-               }
-
-               if ( $wgEnableEmail ) {
-                       $this->mEmail = $request->getText( 'wpEmail' );
-               } else {
-                       $this->mEmail = '';
-               }
-               if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
-                       $this->mRealName = $request->getText( 'wpRealName' );
-               } else {
-                       $this->mRealName = '';
-               }
-
-               if ( !$wgAuth->validDomain( $this->mDomain ) ) {
-                       $this->mDomain = $wgAuth->getDomain();
-               }
-               $wgAuth->setDomain( $this->mDomain );
-
-               # 1. When switching accounts, it sucks to get automatically logged out
-               # 2. Do not return to PasswordReset after a successful password change
-               #    but goto Wiki start page (Main_Page) instead ( bug 33997 )
-               $returnToTitle = Title::newFromText( $this->mReturnTo );
-               if ( is_object( $returnToTitle )
-                       && ( $returnToTitle->isSpecial( 'Userlogout' )
-                               || $returnToTitle->isSpecial( 'PasswordReset' ) )
-               ) {
-                       $this->mReturnTo = '';
-                       $this->mReturnToQuery = '';
-               }
-       }
-
-       function getDescription() {
-               if ( $this->mType === 'signup' ) {
-                       return $this->msg( 'createaccount' )->text();
-               } else {
-                       return $this->msg( 'login' )->text();
-               }
-       }
-
-       /**
-        * @param string|null $subPage
-        */
-       public function execute( $subPage ) {
-               // Make sure session is persisted
-               $session = SessionManager::getGlobalSession();
-               $session->persist();
-
-               $this->load();
-
-               // Check for [[Special:Userlogin/signup]]. This affects form display and
-               // page title.
-               if ( $subPage == 'signup' ) {
-                       $this->mType = 'signup';
-               }
-               $this->setHeaders();
-
-               // Make sure it's possible to log in
-               if ( $this->mType !== 'signup' && !$session->canSetUser() ) {
-                       throw new ErrorPageError(
-                               'cannotloginnow-title',
-                               'cannotloginnow-text',
-                               [
-                                       $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
-                * page. The use case scenario for this is when a user opens a large number
-                * of tabs, is redirected to the login page on all of them, and then logs
-                * in on one, expecting all the others to work properly.
-                *
-                * However, do show the form if it was visited intentionally (no 'returnto'
-                * is present). People who often switch between several accounts have grown
-                * accustomed to this behavior.
-                */
-               if (
-                       $this->mType !== 'signup' &&
-                       !$this->mPosted &&
-                       $this->getUser()->isLoggedIn() &&
-                       ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' )
-               ) {
-                       $this->successfulLogin();
-               }
-
-               // If logging in and not on HTTPS, either redirect to it or offer a link.
-               global $wgSecureLogin;
-               if ( $this->mRequest->getProtocol() !== 'https' ) {
-                       $title = $this->getFullTitle();
-                       $query = [
-                               'returnto' => $this->mReturnTo !== '' ? $this->mReturnTo : null,
-                               'returntoquery' => $this->mReturnToQuery !== '' ?
-                                       $this->mReturnToQuery : null,
-                               'title' => null,
-                               ( $this->mEntryErrorType === 'error' ? 'error' : 'warning' ) => $this->mEntryError,
-                       ] + $this->mRequest->getQueryValues();
-                       $url = $title->getFullURL( $query, false, PROTO_HTTPS );
-                       if ( $wgSecureLogin
-                               && wfCanIPUseHTTPS( $this->getRequest()->getIP() )
-                               && !$this->mFromHTTP ) // Avoid infinite redirect
-                       {
-                               $url = wfAppendQuery( $url, 'fromhttp=1' );
-                               $this->getOutput()->redirect( $url );
-                               // Since we only do this redir to change proto, always vary
-                               $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
-
-                               return;
-                       } else {
-                               // A wiki without HTTPS login support should set $wgServer to
-                               // http://somehost, in which case the secure URL generated
-                               // above won't actually start with https://
-                               if ( substr( $url, 0, 8 ) === 'https://' ) {
-                                       $this->mSecureLoginUrl = $url;
-                               }
-                       }
-               }
-
-               if ( !is_null( $this->mCookieCheck ) ) {
-                       $this->onCookieRedirectCheck( $this->mCookieCheck );
-
-                       return;
-               } elseif ( $this->mPosted ) {
-                       if ( $this->mCreateaccount ) {
-                               $this->addNewAccount();
-
-                               return;
-                       } elseif ( $this->mCreateaccountMail ) {
-                               $this->addNewAccountMailPassword();
-
-                               return;
-                       } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
-                               $this->processLogin();
-
-                               return;
-                       }
-               }
-               $this->mainLoginForm( $this->mEntryError, $this->mEntryErrorType );
-       }
-
-       /**
-        * @private
-        */
-       function addNewAccountMailPassword() {
-               if ( $this->mEmail == '' ) {
-                       $this->mainLoginForm( $this->msg( 'noemailcreate' )->escaped() );
-
-                       return;
-               }
-
-               $status = $this->addNewAccountInternal();
-               LoggerFactory::getInstance( 'authmanager' )->info(
-                       'Account creation attempt with mailed password',
-                       [ 'event' => 'accountcreation', 'status' => $status ]
-               );
-               if ( !$status->isGood() ) {
-                       $error = $status->getMessage();
-                       $this->mainLoginForm( $error->toString() );
-
-                       return;
-               }
-
-               /** @var User $u */
-               $u = $status->getValue();
-
-               // Wipe the initial password and mail a temporary one
-               $u->setPassword( null );
-               $u->saveSettings();
-               $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
-
-               Hooks::run( 'AddNewAccount', [ $u, true ] );
-               $u->addNewUserLogEntry( 'byemail', $this->mReason );
-
-               $out = $this->getOutput();
-               $out->setPageTitle( $this->msg( 'accmailtitle' ) );
-
-               if ( !$result->isGood() ) {
-                       $this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() );
-               } else {
-                       $out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
-                       $this->executeReturnTo( 'success' );
-               }
-       }
-
-       /**
-        * @private
-        * @return bool
-        */
-       function addNewAccount() {
-               global $wgContLang, $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector;
-
-               # Create the account and abort if there's a problem doing so
-               $status = $this->addNewAccountInternal();
-               LoggerFactory::getInstance( 'authmanager' )->info( 'Account creation attempt', [
-                       'event' => 'accountcreation',
-                       'status' => $status,
-               ] );
-
-               if ( !$status->isGood() ) {
-                       $error = $status->getMessage();
-                       $this->mainLoginForm( $error->toString() );
-
-                       return false;
-               }
-
-               $u = $status->getValue();
-
-               # Only save preferences if the user is not creating an account for someone else.
-               if ( $this->getUser()->isAnon() ) {
-                       # If we showed up language selection links, and one was in use, be
-                       # smart (and sensible) and save that language as the user's preference
-                       if ( $wgLoginLanguageSelector && $this->mLanguage ) {
-                               $u->setOption( 'language', $this->mLanguage );
-                       } else {
-
-                               # Otherwise the user's language preference defaults to $wgContLang,
-                               # but it may be better to set it to their preferred $wgContLang variant,
-                               # based on browser preferences or URL parameters.
-                               $u->setOption( 'language', $wgContLang->getPreferredVariant() );
-                       }
-                       if ( $wgContLang->hasVariants() ) {
-                               $u->setOption( 'variant', $wgContLang->getPreferredVariant() );
-                       }
-               }
-
-               $out = $this->getOutput();
-
-               # Send out an email authentication message if needed
-               if ( $wgEmailAuthentication && Sanitizer::validateEmail( $u->getEmail() ) ) {
-                       $status = $u->sendConfirmationMail();
-                       if ( $status->isGood() ) {
-                               $out->addWikiMsg( 'confirmemail_oncreate' );
-                       } else {
-                               $out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) );
-                       }
-               }
-
-               # Save settings (including confirmation token)
-               $u->saveSettings();
-
-               # If not logged in, assume the new account as the current one and set
-               # session cookies then show a "welcome" message or a "need cookies"
-               # message as needed
-               if ( $this->getUser()->isAnon() ) {
-                       $u->setCookies();
-                       $wgUser = $u;
-                       // This should set it for OutputPage and the Skin
-                       // which is needed or the personal links will be
-                       // wrong.
-                       $this->getContext()->setUser( $u );
-                       Hooks::run( 'AddNewAccount', [ $u, false ] );
-                       $u->addNewUserLogEntry( 'create' );
-                       if ( $this->hasSessionCookie() ) {
-                               $this->successfulCreation();
-                       } else {
-                               $this->cookieRedirectCheck( 'new' );
-                       }
-               } else {
-                       # Confirm that the account was created
-                       $out->setPageTitle( $this->msg( 'accountcreated' ) );
-                       $out->addWikiMsg( 'accountcreatedtext', $u->getName() );
-                       $out->addReturnTo( $this->getPageTitle() );
-                       Hooks::run( 'AddNewAccount', [ $u, false ] );
-                       $u->addNewUserLogEntry( 'create2', $this->mReason );
-               }
-
-               return true;
-       }
-
-       /**
-        * Make a new user account using the loaded data.
-        * @private
-        * @throws PermissionsError|ReadOnlyError
-        * @return Status
-        */
-       public function addNewAccountInternal() {
-               global $wgAuth, $wgAccountCreationThrottle, $wgEmailConfirmToEdit;
-
-               // If the user passes an invalid domain, something is fishy
-               if ( !$wgAuth->validDomain( $this->mDomain ) ) {
-                       return Status::newFatal( 'wrongpassword' );
-               }
-
-               // If we are not allowing users to login locally, we should be checking
-               // to see if the user is actually able to authenticate to the authenti-
-               // cation server before they create an account (otherwise, they can
-               // create a local account and login as any domain user). We only need
-               // to check this for domains that aren't local.
-               if ( 'local' != $this->mDomain && $this->mDomain != '' ) {
-                       if (
-                               !$wgAuth->canCreateAccounts() &&
-                               (
-                                       !$wgAuth->userExists( $this->mUsername ) ||
-                                       !$wgAuth->authenticate( $this->mUsername, $this->mPassword )
-                               )
-                       ) {
-                               return Status::newFatal( 'wrongpassword' );
-                       }
-               }
-
-               if ( wfReadOnly() ) {
-                       throw new ReadOnlyError;
-               }
-
-               # Request forgery checks.
-               $token = self::getCreateaccountToken();
-               if ( $token->wasNew() ) {
-                       return Status::newFatal( 'nocookiesfornew' );
-               }
-
-               # The user didn't pass a createaccount token
-               if ( !$this->mToken ) {
-                       return Status::newFatal( 'sessionfailure' );
-               }
-
-               # Validate the createaccount token
-               if ( !$token->match( $this->mToken ) ) {
-                       return Status::newFatal( 'sessionfailure' );
-               }
-
-               # Check permissions
-               $currentUser = $this->getUser();
-               $creationBlock = $currentUser->isBlockedFromCreateAccount();
-               if ( !$currentUser->isAllowed( 'createaccount' ) ) {
-                       throw new PermissionsError( 'createaccount' );
-               } elseif ( $creationBlock instanceof Block ) {
-                       // Throws an ErrorPageError.
-                       $this->userBlockedMessage( $creationBlock );
-
-                       // This should never be reached.
-                       return false;
-               }
-
-               # Include checks that will include GlobalBlocking (Bug 38333)
-               $permErrors = $this->getPageTitle()->getUserPermissionsErrors(
-                       'createaccount',
-                       $currentUser,
-                       true
-               );
-
-               if ( count( $permErrors ) ) {
-                       throw new PermissionsError( 'createaccount', $permErrors );
-               }
-
-               $ip = $this->getRequest()->getIP();
-               if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
-                       return Status::newFatal( 'sorbs_create_account_reason' );
-               }
-
-               # Now create a dummy user ($u) and check if it is valid
-               $u = User::newFromName( $this->mUsername, 'creatable' );
-               if ( !$u ) {
-                       return Status::newFatal( 'noname' );
-               }
-
-               $cache = ObjectCache::getLocalClusterInstance();
-               # Make sure the user does not exist already
-               $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $this->mUsername ) ) );
-               if ( !$lock ) {
-                       return Status::newFatal( 'usernameinprogress' );
-               } elseif ( $u->idForName( User::READ_LOCKING ) ) {
-                       return Status::newFatal( 'userexists' );
-               }
-
-               if ( $this->mCreateaccountMail ) {
-                       # do not force a password for account creation by email
-                       # set invalid password, it will be replaced later by a random generated password
-                       $this->mPassword = null;
-               } else {
-                       if ( $this->mPassword !== $this->mRetype ) {
-                               return Status::newFatal( 'badretype' );
-                       }
-
-                       # check for password validity, return a fatal Status if invalid
-                       $validity = $u->checkPasswordValidity( $this->mPassword, 'create' );
-                       if ( !$validity->isGood() ) {
-                               $validity->ok = false; // make sure this Status is fatal
-                               return $validity;
-                       }
-               }
-
-               # if you need a confirmed email address to edit, then obviously you
-               # need an email address.
-               if ( $wgEmailConfirmToEdit && strval( $this->mEmail ) === '' ) {
-                       return Status::newFatal( 'noemailtitle' );
-               }
-
-               if ( strval( $this->mEmail ) !== '' && !Sanitizer::validateEmail( $this->mEmail ) ) {
-                       return Status::newFatal( 'invalidemailaddress' );
-               }
-
-               # Set some additional data so the AbortNewAccount hook can be used for
-               # more than just username validation
-               $u->setEmail( $this->mEmail );
-               $u->setRealName( $this->mRealName );
-
-               $abortError = '';
-               $abortStatus = null;
-               if ( !Hooks::run( 'AbortNewAccount', [ $u, &$abortError, &$abortStatus ] ) ) {
-                       // Hook point to add extra creation throttles and blocks
-                       wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
-                       if ( $abortStatus === null ) {
-                               // Report back the old string as a raw message status.
-                               // This will report the error back as 'createaccount-hook-aborted'
-                               // with the given string as the message.
-                               // To return a different error code, return a Status object.
-                               $abortError = new Message( 'createaccount-hook-aborted', [ $abortError ] );
-                               $abortError->text();
-
-                               return Status::newFatal( $abortError );
-                       } else {
-                               // For MediaWiki 1.23+ and updated hooks, return the Status object
-                               // returned from the hook.
-                               return $abortStatus;
-                       }
-               }
-
-               // Hook point to check for exempt from account creation throttle
-               if ( !Hooks::run( 'ExemptFromAccountCreationThrottle', [ $ip ] ) ) {
-                       wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook " .
-                               "allowed account creation w/o throttle\n" );
-               } else {
-                       if ( ( $wgAccountCreationThrottle && $currentUser->isPingLimitable() ) ) {
-                               $key = wfGlobalCacheKey( 'acctcreate', 'ip', $ip );
-                               $value = $cache->get( $key );
-                               if ( !$value ) {
-                                       $cache->set( $key, 0, $cache::TTL_DAY );
-                               }
-                               if ( $value >= $wgAccountCreationThrottle ) {
-                                       return Status::newFatal( 'acct_creation_throttle_hit', $wgAccountCreationThrottle );
-                               }
-                               $cache->incr( $key );
-                       }
-               }
-
-               if ( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
-                       return Status::newFatal( 'externaldberror' );
-               }
-
-               self::clearCreateaccountToken();
-
-               return $this->initUser( $u, false );
-       }
-
-       /**
-        * Actually add a user to the database.
-        * Give it a User object that has been initialised with a name.
-        *
-        * @param User $u
-        * @param bool $autocreate True if this is an autocreation via auth plugin
-        * @return Status Status object, with the User object in the value member on success
-        * @private
-        */
-       function initUser( $u, $autocreate ) {
-               global $wgAuth;
-
-               $status = $u->addToDatabase();
-               if ( !$status->isOK() ) {
-                       return $status;
-               }
-
-               if ( $wgAuth->allowPasswordChange() ) {
-                       $u->setPassword( $this->mPassword );
-               }
-
-               $u->setEmail( $this->mEmail );
-               $u->setRealName( $this->mRealName );
-               SessionManager::singleton()->invalidateSessionsForUser( $u );
-
-               Hooks::run( 'LocalUserCreated', [ $u, $autocreate ] );
-               $oldUser = $u;
-               $wgAuth->initUser( $u, $autocreate );
-               if ( $oldUser !== $u ) {
-                       wfWarn( get_class( $wgAuth ) . '::initUser() replaced the user object' );
-               }
-
-               $u->saveSettings();
-
-               // Update user count
-               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
-
-               // Watch user's userpage and talk page
-               $u->addWatch( $u->getUserPage(), User::IGNORE_USER_RIGHTS );
-
-               return Status::newGood( $u );
-       }
-
-       /**
-        * Internally authenticate the login request.
-        *
-        * This may create a local account as a side effect if the
-        * authentication plugin allows transparent local account
-        * creation.
-        * @return int
-        */
-       public function authenticateUserData() {
-               global $wgUser, $wgAuth;
-
-               $this->load();
-
-               if ( $this->mUsername == '' ) {
-                       return self::NO_NAME;
-               }
-
-               // We require a login token to prevent login CSRF
-               // Handle part of this before incrementing the throttle so
-               // token-less login attempts don't count towards the throttle
-               // but wrong-token attempts do.
-
-               // If the user doesn't have a login token yet, set one.
-               $token = self::getLoginToken();
-               if ( $token->wasNew() ) {
-                       return self::NEED_TOKEN;
-               }
-               // If the user didn't pass a login token, tell them we need one
-               if ( !$this->mToken ) {
-                       return self::NEED_TOKEN;
-               }
-
-               $throttleCount = self::incrementLoginThrottle( $this->mUsername );
-               if ( $throttleCount ) {
-                       $this->mThrottleWait = $throttleCount['wait'];
-                       return self::THROTTLED;
-               }
-
-               // Validate the login token
-               if ( !$token->match( $this->mToken ) ) {
-                       return self::WRONG_TOKEN;
-               }
-
-               // Load the current user now, and check to see if we're logging in as
-               // the same name. This is necessary because loading the current user
-               // (say by calling getName()) calls the UserLoadFromSession hook, which
-               // potentially creates the user in the database. Until we load $wgUser,
-               // checking for user existence using User::newFromName($name)->getId() below
-               // will effectively be using stale data.
-               if ( $this->getUser()->getName() === $this->mUsername ) {
-                       wfDebug( __METHOD__ . ": already logged in as {$this->mUsername}\n" );
-
-                       return self::SUCCESS;
-               }
-
-               $u = User::newFromName( $this->mUsername );
-               if ( $u === false ) {
-                       return self::ILLEGAL;
-               }
-
-               $msg = null;
-               // Give extensions a way to indicate the username has been updated,
-               // rather than telling the user the account doesn't exist.
-               if ( !Hooks::run( 'LoginUserMigrated', [ $u, &$msg ] ) ) {
-                       $this->mAbortLoginErrorMsg = $msg;
-                       return self::USER_MIGRATED;
-               }
-
-               if ( !User::isUsableName( $u->getName() ) ) {
-                       return self::ILLEGAL;
-               }
-
-               $isAutoCreated = false;
-               if ( $u->getId() == 0 ) {
-                       $status = $this->attemptAutoCreate( $u );
-                       if ( $status !== self::SUCCESS ) {
-                               return $status;
-                       } else {
-                               $isAutoCreated = true;
-                       }
-               } else {
-                       $u->load();
-               }
-
-               // Give general extensions, such as a captcha, a chance to abort logins
-               $abort = self::ABORTED;
-               if ( !Hooks::run( 'AbortLogin', [ $u, $this->mPassword, &$abort, &$msg ] ) ) {
-                       if ( !in_array( $abort, array_keys( self::$statusCodes ), true ) ) {
-                               throw new Exception( 'Invalid status code returned from AbortLogin hook: ' . $abort );
-                       }
-                       $this->mAbortLoginErrorMsg = $msg;
-                       return $abort;
-               }
-
-               global $wgBlockDisablesLogin;
-               if ( !$u->checkPassword( $this->mPassword ) ) {
-                       if ( $u->checkTemporaryPassword( $this->mPassword ) ) {
-                               /**
-                                * The e-mailed temporary password should not be used for actu-
-                                * al logins; that's a very sloppy habit, and insecure if an
-                                * attacker has a few seconds to click "search" on someone's
-                                * open mail reader.
-                                *
-                                * Allow it to be used only to reset the password a single time
-                                * to a new value, which won't be in the user's e-mail ar-
-                                * chives.
-                                *
-                                * For backwards compatibility, we'll still recognize it at the
-                                * login form to minimize surprises for people who have been
-                                * logging in with a temporary password for some time.
-                                *
-                                * As a side-effect, we can authenticate the user's e-mail ad-
-                                * dress if it's not already done, since the temporary password
-                                * was sent via e-mail.
-                                */
-                               if ( !$u->isEmailConfirmed() && !wfReadOnly() ) {
-                                       $u->confirmEmail();
-                                       $u->saveSettings();
-                               }
-
-                               // At this point we just return an appropriate code/ indicating
-                               // that the UI should show a password reset form; bot inter-
-                               // faces etc will probably just fail cleanly here.
-                               $this->mAbortLoginErrorMsg = 'resetpass-temp-emailed';
-                               $this->mTempPasswordUsed = true;
-                               $retval = self::RESET_PASS;
-                       } else {
-                               $retval = ( $this->mPassword == '' ) ? self::EMPTY_PASS : self::WRONG_PASS;
-                       }
-               } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) {
-                       // If we've enabled it, make it so that a blocked user cannot login
-                       $retval = self::USER_BLOCKED;
-               } elseif ( $this->checkUserPasswordExpired( $u ) == 'hard' ) {
-                       // Force reset now, without logging in
-                       $retval = self::RESET_PASS;
-                       $this->mAbortLoginErrorMsg = 'resetpass-expired';
-               } else {
-                       Hooks::run( 'UserLoggedIn', [ $u ] );
-                       $oldUser = $u;
-                       $wgAuth->updateUser( $u );
-                       if ( $oldUser !== $u ) {
-                               wfWarn( get_class( $wgAuth ) . '::updateUser() replaced the user object' );
-                       }
-                       $wgUser = $u;
-                       // This should set it for OutputPage and the Skin
-                       // which is needed or the personal links will be
-                       // wrong.
-                       $this->getContext()->setUser( $u );
-
-                       // Please reset throttle for successful logins, thanks!
-                       self::clearLoginThrottle( $this->mUsername );
-
-                       if ( $isAutoCreated ) {
-                               // Must be run after $wgUser is set, for correct new user log
-                               Hooks::run( 'AuthPluginAutoCreate', [ $u ] );
-                       }
-
-                       $retval = self::SUCCESS;
-               }
-               Hooks::run( 'LoginAuthenticateAudit', [ $u, $this->mPassword, $retval ] );
-
-               return $retval;
-       }
-
-       /**
-        * Increment the login attempt throttle hit count for the (username,current IP)
-        * tuple unless the throttle was already reached.
-        *
-        * @since 1.27 Return value changed.
-        * @param string $username The user name
-        * @return bool|array false if below limit or an array if above limit
-        *   Array contains keys wait, count, and throttleIndex
-        */
-       public static function incrementLoginThrottle( $username ) {
-               global $wgPasswordAttemptThrottle, $wgRequest;
-               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
-
-               $throttleCount = 0;
-               if ( is_array( $wgPasswordAttemptThrottle ) ) {
-                       $throttleConfig = $wgPasswordAttemptThrottle;
-                       if ( isset( $wgPasswordAttemptThrottle['count'] ) ) {
-                               // old style. Convert for backwards compat.
-                               $throttleConfig = [ $wgPasswordAttemptThrottle ];
-                       }
-                       foreach ( $throttleConfig as $index => $specificThrottle ) {
-                               if ( isset( $specificThrottle['allIPs'] ) ) {
-                                       $ip = 'All';
-                               } else {
-                                       $ip = $wgRequest->getIP();
-                               }
-                               $throttleKey = wfGlobalCacheKey( 'password-throttle',
-                                       $index, $ip, md5( $username )
-                               );
-                               $count = $specificThrottle['count'];
-                               $period = $specificThrottle['seconds'];
-
-                               $cache = ObjectCache::getLocalClusterInstance();
-                               $throttleCount = $cache->get( $throttleKey );
-                               if ( !$throttleCount ) {
-                                       $cache->add( $throttleKey, 1, $period ); // start counter
-                               } elseif ( $throttleCount < $count ) {
-                                       $cache->incr( $throttleKey );
-                               } elseif ( $throttleCount >= $count ) {
-                                       $logMsg = 'Login attempt rejected because logins to '
-                                               . '{acct} from IP {ip} have been throttled for '
-                                               . '{period} seconds due to {count} failed attempts';
-                                       // If we are hitting a throttle for >= 50 attempts,
-                                       // it is much more likely to be an attack than someone
-                                       // simply forgetting their password, so log it at a
-                                       // higher level.
-                                       $level = $count >= 50 ? LogLevel::WARNING : LogLevel::INFO;
-                                       // It should be noted that once the throttle is hit,
-                                       // every attempt to login will generate the log message
-                                       // until the throttle expires, not just the attempt that
-                                       // puts the throttle over the top.
-                                       LoggerFactory::getInstance( 'password-throttle' )->log(
-                                               $level,
-                                               $logMsg,
-                                               [
-                                                       'ip' => $ip,
-                                                       'period' => $period,
-                                                       'acct' => $username,
-                                                       'count' => $count,
-                                                       'throttleIdentifier' => $index,
-                                                       'method' => __METHOD__
-                                               ]
-                                       );
-
-                                       return [
-                                               'throttleIndex' => $index,
-                                               'wait' => $period,
-                                               'count' => $count
-                                       ];
-                               }
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Increment the login attempt throttle hit count for the (username,current IP)
-        * tuple unless the throttle was already reached.
-        *
-        * @deprecated Use LoginForm::incrementLoginThrottle instead
-        * @param string $username The user name
-        * @return bool|int true if above throttle, or 0 (prior to 1.27, returned current count)
-        */
-       public static function incLoginThrottle( $username ) {
-               wfDeprecated( __METHOD__, "1.27" );
-               $res = self::incrementLoginThrottle( $username );
-               return is_array( $res ) ? true : 0;
-       }
-
-       /**
-        * Clear the login attempt throttle hit count for the (username,current IP) tuple.
-        * @param string $username The user name
-        * @return void
-        */
-       public static function clearLoginThrottle( $username ) {
-               global $wgRequest, $wgPasswordAttemptThrottle;
-               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
-
-               if ( is_array( $wgPasswordAttemptThrottle ) ) {
-                       $throttleConfig = $wgPasswordAttemptThrottle;
-                       if ( isset( $wgPasswordAttemptThrottle['count'] ) ) {
-                               // old style. Convert for backwards compat.
-                               $throttleConfig = [ $wgPasswordAttemptThrottle ];
-                       }
-                       foreach ( $throttleConfig as $index => $specificThrottle ) {
-                               if ( isset( $specificThrottle['allIPs'] ) ) {
-                                       $ip = 'All';
-                               } else {
-                                       $ip = $wgRequest->getIP();
-                               }
-                               $throttleKey = wfGlobalCacheKey( 'password-throttle', $index,
-                                       $ip, md5( $username )
-                               );
-                               ObjectCache::getLocalClusterInstance()->delete( $throttleKey );
-                       }
-               }
-       }
-
-       /**
-        * Attempt to automatically create a user on login. Only succeeds if there
-        * is an external authentication method which allows it.
-        *
-        * @param User $user
-        *
-        * @return int Status code
-        */
-       function attemptAutoCreate( $user ) {
-               global $wgAuth;
-
-               if ( $this->getUser()->isBlockedFromCreateAccount() ) {
-                       wfDebug( __METHOD__ . ": user is blocked from account creation\n" );
-
-                       return self::CREATE_BLOCKED;
-               }
-
-               if ( !$wgAuth->autoCreate() ) {
-                       return self::NOT_EXISTS;
-               }
-
-               if ( !$wgAuth->userExists( $user->getName() ) ) {
-                       wfDebug( __METHOD__ . ": user does not exist\n" );
-
-                       return self::NOT_EXISTS;
-               }
-
-               if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
-                       wfDebug( __METHOD__ . ": \$wgAuth->authenticate() returned false, aborting\n" );
-
-                       return self::WRONG_PLUGIN_PASS;
-               }
-
-               $abortError = '';
-               if ( !Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ] ) ) {
-                       // Hook point to add extra creation throttles and blocks
-                       wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" );
-                       $this->mAbortLoginErrorMsg = $abortError;
-
-                       return self::ABORTED;
-               }
-
-               wfDebug( __METHOD__ . ": creating account\n" );
-               $status = $this->initUser( $user, true );
-
-               if ( !$status->isOK() ) {
-                       $errors = $status->getErrorsByType( 'error' );
-                       $this->mAbortLoginErrorMsg = $errors[0]['message'];
-
-                       return self::ABORTED;
-               }
-
-               return self::SUCCESS;
-       }
-
-       function processLogin() {
-               global $wgLang, $wgSecureLogin, $wgInvalidPasswordReset;
-
-               $authRes = $this->authenticateUserData();
-               switch ( $authRes ) {
-                       case self::SUCCESS:
-                               # We've verified now, update the real record
-                               $user = $this->getUser();
-                               $user->touch();
-
-                               if ( $user->requiresHTTPS() ) {
-                                       $this->mStickHTTPS = true;
-                               }
-
-                               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
-                                       $user->setCookies( $this->mRequest, false, $this->mRemember );
-                               } else {
-                                       $user->setCookies( $this->mRequest, null, $this->mRemember );
-                               }
-                               self::clearLoginToken();
-
-                               // Reset the throttle
-                               self::clearLoginThrottle( $this->mUsername );
-
-                               $request = $this->getRequest();
-                               if ( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
-                                       /* Replace the language object to provide user interface in
-                                        * correct language immediately on this first page load.
-                                        */
-                                       $code = $request->getVal( 'uselang', $user->getOption( 'language' ) );
-                                       $userLang = Language::factory( $code );
-                                       $wgLang = $userLang;
-                                       RequestContext::getMain()->setLanguage( $userLang );
-                                       $this->getContext()->setLanguage( $userLang );
-                                       // Reset SessionID on Successful login (bug 40995)
-                                       $this->renewSessionId();
-                                       if ( $this->checkUserPasswordExpired( $this->getUser() ) == 'soft' ) {
-                                               $this->resetLoginForm( $this->msg( 'resetpass-expired-soft' ) );
-                                       } elseif ( $wgInvalidPasswordReset
-                                               && !$user->isValidPassword( $this->mPassword )
-                                       ) {
-                                               $status = $user->checkPasswordValidity(
-                                                       $this->mPassword,
-                                                       'login'
-                                               );
-                                               $this->resetLoginForm(
-                                                       $status->getMessage( 'resetpass-validity-soft' )
-                                               );
-                                       } else {
-                                               $this->successfulLogin();
-                                       }
-                               } else {
-                                       $this->cookieRedirectCheck( 'login' );
-                               }
-                               break;
-
-                       case self::NEED_TOKEN:
-                               $error = $this->mAbortLoginErrorMsg ?: 'nocookiesforlogin';
-                               $this->mainLoginForm( $this->msg( $error )->parse() );
-                               break;
-                       case self::WRONG_TOKEN:
-                               $error = $this->mAbortLoginErrorMsg ?: 'sessionfailure';
-                               $this->mainLoginForm( $this->msg( $error )->text() );
-                               break;
-                       case self::NO_NAME:
-                       case self::ILLEGAL:
-                               $error = $this->mAbortLoginErrorMsg ?: 'noname';
-                               $this->mainLoginForm( $this->msg( $error )->text() );
-                               break;
-                       case self::WRONG_PLUGIN_PASS:
-                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
-                               $this->mainLoginForm( $this->msg( $error )->text() );
-                               break;
-                       case self::NOT_EXISTS:
-                               if ( $this->getUser()->isAllowed( 'createaccount' ) ) {
-                                       $error = $this->mAbortLoginErrorMsg ?: 'nosuchuser';
-                                       $this->mainLoginForm( $this->msg( $error,
-                                               wfEscapeWikiText( $this->mUsername ) )->parse() );
-                               } else {
-                                       $error = $this->mAbortLoginErrorMsg ?: 'nosuchusershort';
-                                       $this->mainLoginForm( $this->msg( $error,
-                                               wfEscapeWikiText( $this->mUsername ) )->text() );
-                               }
-                               break;
-                       case self::WRONG_PASS:
-                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
-                               $this->mainLoginForm( $this->msg( $error )->text() );
-                               break;
-                       case self::EMPTY_PASS:
-                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpasswordempty';
-                               $this->mainLoginForm( $this->msg( $error )->text() );
-                               break;
-                       case self::RESET_PASS:
-                               $error = $this->mAbortLoginErrorMsg ?: 'resetpass_announce';
-                               $this->resetLoginForm( $this->msg( $error ) );
-                               break;
-                       case self::CREATE_BLOCKED:
-                               $this->userBlockedMessage( $this->getUser()->isBlockedFromCreateAccount() );
-                               break;
-                       case self::THROTTLED:
-                               $error = $this->mAbortLoginErrorMsg ?: 'login-throttled';
-                               $this->mainLoginForm( $this->msg( $error )
-                                       ->durationParams( $this->mThrottleWait )->text()
-                               );
-                               break;
-                       case self::USER_BLOCKED:
-                               $error = $this->mAbortLoginErrorMsg ?: 'login-userblocked';
-                               $this->mainLoginForm( $this->msg( $error, $this->mUsername )->escaped() );
-                               break;
-                       case self::ABORTED:
-                               $error = $this->mAbortLoginErrorMsg ?: 'login-abort-generic';
-                               $this->mainLoginForm( $this->msg( $error,
-                                               wfEscapeWikiText( $this->mUsername ) )->text() );
-                               break;
-                       case self::USER_MIGRATED:
-                               $error = $this->mAbortLoginErrorMsg ?: 'login-migrated-generic';
-                               $params = [];
-                               if ( is_array( $error ) ) {
-                                       $error = array_shift( $this->mAbortLoginErrorMsg );
-                                       $params = $this->mAbortLoginErrorMsg;
-                               }
-                               $this->mainLoginForm( $this->msg( $error, $params )->text() );
-                               break;
-                       default:
-                               throw new MWException( 'Unhandled case value' );
-               }
-
-               LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', [
-                       'event' => 'login',
-                       'successful' => $authRes === self::SUCCESS,
-                       'status' => LoginForm::$statusCodes[$authRes],
-               ] );
-       }
-
-       /**
-        * Show the Special:ChangePassword form, with custom message
-        * @param Message $msg
-        */
-       protected function resetLoginForm( Message $msg ) {
-               // Allow hooks to explain this password reset in more detail
-               Hooks::run( 'LoginPasswordResetMessage', [ &$msg, $this->mUsername ] );
-               $reset = new SpecialChangePassword();
-               $derivative = new DerivativeContext( $this->getContext() );
-               $derivative->setTitle( $reset->getPageTitle() );
-               $reset->setContext( $derivative );
-               if ( !$this->mTempPasswordUsed ) {
-                       $reset->setOldPasswordMessage( 'oldpassword' );
-               }
-               $reset->setChangeMessage( $msg );
-               $reset->execute( null );
-       }
-
-       /**
-        * @param User $u
-        * @param bool $throttle
-        * @param string $emailTitle Message name of email title
-        * @param string $emailText Message name of email text
-        * @return Status
-        */
-       function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle',
-               $emailText = 'passwordremindertext'
-       ) {
-               global $wgNewPasswordExpiry, $wgMinimalPasswordLength;
-
-               if ( $u->getEmail() == '' ) {
-                       return Status::newFatal( 'noemail', $u->getName() );
-               }
-               $ip = $this->getRequest()->getIP();
-               if ( !$ip ) {
-                       return Status::newFatal( 'badipaddress' );
-               }
-
-               $currentUser = $this->getUser();
-               Hooks::run( 'User::mailPasswordInternal', [ &$currentUser, &$ip, &$u ] );
-
-               $np = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
-               $u->setNewpassword( $np, $throttle );
-               $u->saveSettings();
-               $userLanguage = $u->getOption( 'language' );
-
-               $mainPage = Title::newMainPage();
-               $mainPageUrl = $mainPage->getCanonicalURL();
-
-               $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $mainPageUrl . '>',
-                       round( $wgNewPasswordExpiry / 86400 ) )->inLanguage( $userLanguage )->text();
-               $result = $u->sendMail( $this->msg( $emailTitle )->inLanguage( $userLanguage )->text(), $m );
-
-               return $result;
-       }
-
-       /**
-        * Run any hooks registered for logins, then HTTP redirect to
-        * $this->mReturnTo (or Main Page if that's undefined).  Formerly we had a
-        * nice message here, but that's really not as useful as just being sent to
-        * wherever you logged in from.  It should be clear that the action was
-        * successful, given the lack of error messages plus the appearance of your
-        * name in the upper right.
-        *
-        * @private
-        */
-       function successfulLogin() {
-               # Run any hooks; display injected HTML if any, else redirect
-               $currentUser = $this->getUser();
-               $injected_html = '';
-               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
-
-               if ( $injected_html !== '' ) {
-                       $this->displaySuccessfulAction( 'success', $this->msg( 'loginsuccesstitle' ),
-                               'loginsuccess', $injected_html );
-               } else {
-                       $this->executeReturnTo( 'successredirect' );
-               }
-       }
-
-       /**
-        * Run any hooks registered for logins, then display a message welcoming
-        * the user.
-        *
-        * @private
-        */
-       function successfulCreation() {
-               # Run any hooks; display injected HTML
-               $currentUser = $this->getUser();
-               $injected_html = '';
-               $welcome_creation_msg = 'welcomecreation-msg';
-
-               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
-
-               /**
-                * Let any extensions change what message is shown.
-                * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeWelcomeCreation
-                * @since 1.18
-                */
-               Hooks::run( 'BeforeWelcomeCreation', [ &$welcome_creation_msg, &$injected_html ] );
-
-               $this->displaySuccessfulAction(
-                       'signup',
-                       $this->msg( 'welcomeuser', $this->getUser()->getName() ),
-                       $welcome_creation_msg, $injected_html
-               );
-       }
-
-       /**
-        * Display a "successful action" page.
-        *
-        * @param string $type Condition of return to; see `executeReturnTo`
-        * @param string|Message $title Page's title
-        * @param string $msgname
-        * @param string $injected_html
-        */
-       private function displaySuccessfulAction( $type, $title, $msgname, $injected_html ) {
-               $out = $this->getOutput();
-               $out->setPageTitle( $title );
-               if ( $msgname ) {
-                       $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
-               }
-
-               $out->addHTML( $injected_html );
-
-               $this->executeReturnTo( $type );
-       }
-
-       /**
-        * Output a message that informs the user that they cannot create an account because
-        * there is a block on them or their IP which prevents account creation.  Note that
-        * User::isBlockedFromCreateAccount(), which gets this block, ignores the 'hardblock'
-        * setting on blocks (bug 13611).
-        * @param Block $block The block causing this error
-        * @throws ErrorPageError
-        */
-       function userBlockedMessage( Block $block ) {
-               # Let's be nice about this, it's likely that this feature will be used
-               # for blocking large numbers of innocent people, e.g. range blocks on
-               # schools. Don't blame it on the user. There's a small chance that it
-               # really is the user's fault, i.e. the username is blocked and they
-               # haven't bothered to log out before trying to create an account to
-               # evade it, but we'll leave that to their guilty conscience to figure
-               # out.
-               $errorParams = [
-                       $block->getTarget(),
-                       $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(),
-                       $block->getByName()
-               ];
-
-               if ( $block->getType() === Block::TYPE_RANGE ) {
-                       $errorMessage = 'cantcreateaccount-range-text';
-                       $errorParams[] = $this->getRequest()->getIP();
-               } else {
-                       $errorMessage = 'cantcreateaccount-text';
-               }
-
-               throw new ErrorPageError(
-                       'cantcreateaccounttitle',
-                       $errorMessage,
-                       $errorParams
-               );
-       }
-
-       /**
-        * Add a "return to" link or redirect to it.
-        * Extensions can use this to reuse the "return to" logic after
-        * inject steps (such as redirection) into the login process.
-        *
-        * @param string $type One of the following:
-        *    - error: display a return to link ignoring $wgRedirectOnLogin
-        *    - signup: display a return to link using $wgRedirectOnLogin if needed
-        *    - success: display a return to link using $wgRedirectOnLogin if needed
-        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
-        * @param string $returnTo
-        * @param array|string $returnToQuery
-        * @param bool $stickHTTPs Keep redirect link on HTTPs
-        * @since 1.22
-        */
-       public function showReturnToPage(
-               $type, $returnTo = '', $returnToQuery = '', $stickHTTPs = false
-       ) {
-               $this->mReturnTo = $returnTo;
-               $this->mReturnToQuery = $returnToQuery;
-               $this->mStickHTTPS = $stickHTTPs;
-               $this->executeReturnTo( $type );
-       }
-
-       /**
-        * Add a "return to" link or redirect to it.
-        *
-        * @param string $type One of the following:
-        *    - error: display a return to link ignoring $wgRedirectOnLogin
-        *    - signup: display a return to link using $wgRedirectOnLogin if needed
-        *    - success: display a return to link using $wgRedirectOnLogin if needed
-        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
-        */
-       private function executeReturnTo( $type ) {
-               global $wgRedirectOnLogin, $wgSecureLogin;
-
-               if ( $type != 'error' && $wgRedirectOnLogin !== null ) {
-                       $returnTo = $wgRedirectOnLogin;
-                       $returnToQuery = [];
-               } else {
-                       $returnTo = $this->mReturnTo;
-                       $returnToQuery = wfCgiToArray( $this->mReturnToQuery );
-               }
-
-               // Allow modification of redirect behavior
-               Hooks::run( 'PostLoginRedirect', [ &$returnTo, &$returnToQuery, &$type ] );
-
-               $returnToTitle = Title::newFromText( $returnTo );
-               if ( !$returnToTitle ) {
-                       $returnToTitle = Title::newMainPage();
-               }
-
-               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
-                       $options = [ 'http' ];
-                       $proto = PROTO_HTTP;
-               } elseif ( $wgSecureLogin ) {
-                       $options = [ 'https' ];
-                       $proto = PROTO_HTTPS;
-               } else {
-                       $options = [];
-                       $proto = PROTO_RELATIVE;
-               }
-
-               if ( $type == 'successredirect' ) {
-                       $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
-                       $this->getOutput()->redirect( $redirectUrl );
-               } else {
-                       $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
-               }
-       }
-
-       /**
-        * @param string $msg
-        * @param string $msgtype
-        * @throws ErrorPageError
-        * @throws Exception
-        * @throws FatalError
-        * @throws MWException
-        * @throws PermissionsError
-        * @throws ReadOnlyError
-        * @private
-        */
-       function mainLoginForm( $msg, $msgtype = 'error' ) {
-               global $wgEnableEmail, $wgEnableUserEmail;
-               global $wgHiddenPrefs, $wgLoginLanguageSelector;
-               global $wgAuth, $wgEmailConfirmToEdit;
-               global $wgSecureLogin, $wgPasswordResetRoutes;
-               global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
-
-               $titleObj = $this->getPageTitle();
-               $user = $this->getUser();
-               $out = $this->getOutput();
-
-               if ( $this->mType == 'signup' ) {
-                       // Block signup here if in readonly. Keeps user from
-                       // going through the process (filling out data, etc)
-                       // and being informed later.
-                       $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $user, true );
-                       if ( count( $permErrors ) ) {
-                               throw new PermissionsError( 'createaccount', $permErrors );
-                       } elseif ( $user->isBlockedFromCreateAccount() ) {
-                               $this->userBlockedMessage( $user->isBlockedFromCreateAccount() );
-
-                               return;
-                       } elseif ( wfReadOnly() ) {
-                               throw new ReadOnlyError;
-                       }
-               }
-
-               // Pre-fill username (if not creating an account, bug 44775).
-               if ( $this->mUsername == '' && $this->mType != 'signup' ) {
-                       if ( $user->isLoggedIn() ) {
-                               $this->mUsername = $user->getName();
-                       } else {
-                               $this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
-                       }
-               }
-
-               // Generic styles and scripts for both login and signup form
-               $out->addModuleStyles( [
-                       'mediawiki.ui',
-                       'mediawiki.ui.button',
-                       'mediawiki.ui.checkbox',
-                       'mediawiki.ui.input',
-                       'mediawiki.special.userlogin.common.styles'
-               ] );
-
-               if ( $this->mType == 'signup' ) {
-                       // Additional styles and scripts for signup form
-                       $out->addModules( [
-                               'mediawiki.special.userlogin.signup.js'
-                       ] );
-                       $out->addModuleStyles( [
-                               'mediawiki.special.userlogin.signup.styles'
-                       ] );
-
-                       $template = new UsercreateTemplate( $this->getConfig() );
-
-                       // Must match number of benefits defined in messages
-                       $template->set( 'benefitCount', 3 );
-
-                       $q = 'action=submitlogin&type=signup';
-                       $linkq = 'type=login';
-               } else {
-                       // Additional styles for login form
-                       $out->addModuleStyles( [
-                               'mediawiki.special.userlogin.login.styles'
-                       ] );
-
-                       $template = new UserloginTemplate( $this->getConfig() );
-
-                       $q = 'action=submitlogin&type=login';
-                       $linkq = 'type=signup';
-               }
-
-               if ( $this->mReturnTo !== '' ) {
-                       $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
-                       if ( $this->mReturnToQuery !== '' ) {
-                               $returnto .= '&returntoquery=' .
-                                       wfUrlencode( $this->mReturnToQuery );
-                       }
-                       $q .= $returnto;
-                       $linkq .= $returnto;
-               }
-
-               # Don't show a "create account" link if the user can't.
-               if ( $this->showCreateOrLoginLink( $user ) ) {
-                       # Pass any language selection on to the mode switch link
-                       if ( $wgLoginLanguageSelector && $this->mLanguage ) {
-                               $linkq .= '&uselang=' . $this->mLanguage;
-                       }
-                       // Supply URL, login template creates the button.
-                       $template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
-               } else {
-                       $template->set( 'link', '' );
-               }
-
-               $resetLink = $this->mType == 'signup'
-                       ? null
-                       : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) );
-
-               $template->set( 'header', '' );
-               $template->set( 'formheader', '' );
-               $template->set( 'skin', $this->getSkin() );
-               $template->set( 'name', $this->mUsername );
-               $template->set( 'password', $this->mPassword );
-               $template->set( 'retype', $this->mRetype );
-               $template->set( 'createemailset', $this->mCreateaccountMail );
-               $template->set( 'email', $this->mEmail );
-               $template->set( 'realname', $this->mRealName );
-               $template->set( 'domain', $this->mDomain );
-               $template->set( 'reason', $this->mReason );
-
-               $template->set( 'action', $titleObj->getLocalURL( $q ) );
-               $template->set( 'message', $msg );
-               $template->set( 'messagetype', $msgtype );
-               $template->set( 'createemail', $wgEnableEmail && $user->isLoggedIn() );
-               $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs ) );
-               $template->set( 'useemail', $wgEnableEmail );
-               $template->set( 'emailrequired', $wgEmailConfirmToEdit );
-               $template->set( 'emailothers', $wgEnableUserEmail );
-               $template->set( 'canreset', $wgAuth->allowPasswordChange() );
-               $template->set( 'resetlink', $resetLink );
-               $template->set( 'canremember', $wgExtendedLoginCookieExpiration === null ?
-                       ( $wgCookieExpiration > 0 ) :
-                       ( $wgExtendedLoginCookieExpiration > 0 ) );
-               $template->set( 'usereason', $user->isLoggedIn() );
-               $template->set( 'remember', $this->mRemember );
-               $template->set( 'cansecurelogin', ( $wgSecureLogin === true ) );
-               $template->set( 'stickhttps', (int)$this->mStickHTTPS );
-               $template->set( 'loggedin', $user->isLoggedIn() );
-               $template->set( 'loggedinuser', $user->getName() );
-
-               if ( $this->mType == 'signup' ) {
-                       $template->set( 'token', self::getCreateaccountToken()->toString() );
-               } else {
-                       $template->set( 'token', self::getLoginToken()->toString() );
-               }
-
-               # Prepare language selection links as needed
-               if ( $wgLoginLanguageSelector ) {
-                       $template->set( 'languages', $this->makeLanguageSelector() );
-                       if ( $this->mLanguage ) {
-                               $template->set( 'uselang', $this->mLanguage );
-                       }
-               }
-
-               $template->set( 'secureLoginUrl', $this->mSecureLoginUrl );
-               // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
-               $usingHTTPS = $this->mRequest->getProtocol() == 'https';
-               $signupendHTTPS = $this->msg( 'signupend-https' );
-               if ( $usingHTTPS && !$signupendHTTPS->isBlank() ) {
-                       $template->set( 'signupend', $signupendHTTPS->parse() );
-               } else {
-                       $template->set( 'signupend', $this->msg( 'signupend' )->parse() );
-               }
-
-               // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
-               if ( $usingHTTPS ) {
-                       $template->set( 'fromhttp', $this->mFromHTTP );
-               }
-
-               // Give authentication and captcha plugins a chance to modify the form
-               $wgAuth->modifyUITemplate( $template, $this->mType );
-               if ( $this->mType == 'signup' ) {
-                       Hooks::run( 'UserCreateForm', [ &$template ] );
-               } else {
-                       Hooks::run( 'UserLoginForm', [ &$template ] );
-               }
-
-               $out->disallowUserJs(); // just in case...
-               $out->addTemplate( $template );
-       }
-
-       /**
-        * Whether the login/create account form should display a link to the
-        * other form (in addition to whatever the skin provides).
-        *
-        * @param User $user
-        * @return bool
-        */
-       private function showCreateOrLoginLink( &$user ) {
-               if ( $this->mType == 'signup' ) {
-                       return true;
-               } elseif ( $user->isAllowed( 'createaccount' ) ) {
-                       return true;
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Check if a session cookie is present.
-        *
-        * This will not pick up a cookie set during _this_ request, but is meant
-        * to ensure that the client is returning the cookie which was set on a
-        * previous pass through the system.
-        *
-        * @private
-        * @return bool
-        */
-       function hasSessionCookie() {
-               global $wgDisableCookieCheck, $wgInitialSessionId;
-
-               return $wgDisableCookieCheck || (
-                       $wgInitialSessionId &&
-                       $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
-               );
-       }
-
-       /**
-        * Get the login token from the current session
-        * @since 1.27 returns a MediaWiki\Session\Token instead of a string
-        * @return MediaWiki\Session\Token
-        */
-       public static function getLoginToken() {
-               global $wgRequest;
-               return $wgRequest->getSession()->getToken( '', 'login' );
-       }
-
-       /**
-        * Formerly randomly generated a login token that would be returned by
-        * $this->getLoginToken().
-        *
-        * Since 1.27, this is a no-op. The token is generated as necessary by
-        * $this->getLoginToken().
-        *
-        * @deprecated since 1.27
-        */
-       public static function setLoginToken() {
-               wfDeprecated( __METHOD__, '1.27' );
-       }
-
-       /**
-        * Remove any login token attached to the current session
-        */
-       public static function clearLoginToken() {
-               global $wgRequest;
-               $wgRequest->getSession()->resetToken( 'login' );
-       }
-
-       /**
-        * Get the createaccount token from the current session
-        * @since 1.27 returns a MediaWiki\Session\Token instead of a string
-        * @return MediaWiki\Session\Token
-        */
-       public static function getCreateaccountToken() {
-               global $wgRequest;
-               return $wgRequest->getSession()->getToken( '', 'createaccount' );
-       }
-
-       /**
-        * Formerly randomly generated a createaccount token that would be returned
-        * by $this->getCreateaccountToken().
-        *
-        * Since 1.27, this is a no-op. The token is generated as necessary by
-        * $this->getCreateaccountToken().
-        *
-        * @deprecated since 1.27
-        */
-       public static function setCreateaccountToken() {
-               wfDeprecated( __METHOD__, '1.27' );
-       }
-
-       /**
-        * Remove any createaccount token attached to the current session
-        */
-       public static function clearCreateaccountToken() {
-               global $wgRequest;
-               $wgRequest->getSession()->resetToken( 'createaccount' );
-       }
-
-       /**
-        * Renew the user's session id, using strong entropy
-        */
-       private function renewSessionId() {
-               global $wgSecureLogin, $wgCookieSecure;
-               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
-                       $wgCookieSecure = false;
-               }
-
-               SessionManager::getGlobalSession()->resetId();
-       }
-
-       /**
-        * @param string $type
-        * @private
-        */
-       function cookieRedirectCheck( $type ) {
-               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
-               $query = [ 'wpCookieCheck' => $type ];
-               if ( $this->mReturnTo !== '' ) {
-                       $query['returnto'] = $this->mReturnTo;
-                       $query['returntoquery'] = $this->mReturnToQuery;
-               }
-               $check = $titleObj->getFullURL( $query );
-
-               $this->getOutput()->redirect( $check );
-       }
-
-       /**
-        * @param string $type
-        * @private
-        */
-       function onCookieRedirectCheck( $type ) {
-               if ( !$this->hasSessionCookie() ) {
-                       if ( $type == 'new' ) {
-                               $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() );
-                       } elseif ( $type == 'login' ) {
-                               $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() );
-                       } else {
-                               # shouldn't happen
-                               $this->mainLoginForm( $this->msg( 'error' )->text() );
-                       }
-               } else {
-                       $this->successfulLogin();
-               }
-       }
-
-       /**
-        * Produce a bar of links which allow the user to select another language
-        * during login/registration but retain "returnto"
-        *
-        * @return string
-        */
-       function makeLanguageSelector() {
-               $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
-               if ( $msg->isBlank() ) {
-                       return '';
-               }
-               $langs = explode( "\n", $msg->text() );
-               $links = [];
-               foreach ( $langs as $lang ) {
-                       $lang = trim( $lang, '* ' );
-                       $parts = explode( '|', $lang );
-                       if ( count( $parts ) >= 2 ) {
-                               $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
-                       }
-               }
-
-               return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
-                       $this->getLanguage()->pipeList( $links ) )->escaped() : '';
-       }
-
-       /**
-        * Create a language selector link for a particular language
-        * Links back to this page preserving type and returnto
-        *
-        * @param string $text Link text
-        * @param string $lang Language code
-        * @return string
-        */
-       function makeLanguageSelectorLink( $text, $lang ) {
-               if ( $this->getLanguage()->getCode() == $lang ) {
-                       // no link for currently used language
-                       return htmlspecialchars( $text );
-               }
-               $query = [ 'uselang' => $lang ];
-               if ( $this->mType == 'signup' ) {
-                       $query['type'] = 'signup';
-               }
-               if ( $this->mReturnTo !== '' ) {
-                       $query['returnto'] = $this->mReturnTo;
-                       $query['returntoquery'] = $this->mReturnToQuery;
-               }
-
-               $attr = [];
-               $targetLanguage = Language::factory( $lang );
-               $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
-
-               return Linker::linkKnown(
-                       $this->getPageTitle(),
-                       htmlspecialchars( $text ),
-                       $attr,
-                       $query
-               );
-       }
-
-       protected function getGroupName() {
-               return 'login';
-       }
-
-       /**
-        * Private function to check password expiration, until AuthManager comes
-        * along to handle that.
-        * @param User $user
-        * @return string|bool
-        */
-       private function checkUserPasswordExpired( User $user ) {
-               global $wgPasswordExpireGrace;
-               $dbr = wfGetDB( DB_SLAVE );
-               $ts = $dbr->selectField( 'user', 'user_password_expires', [ 'user_id' => $user->getId() ] );
-
-               $expired = false;
-               $now = wfTimestamp();
-               $expUnix = wfTimestamp( TS_UNIX, $ts );
-               if ( $ts !== null && $expUnix < $now ) {
-                       $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
-               }
-               return $expired;
-       }
-
-       protected function getSubpagesForPrefixSearch() {
-               return [ 'signup' ];
-       }
-}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
deleted file mode 100644 (file)
index 5789e3a..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-/**
- * Implements Special:Userlogout
- *
- * 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
- */
-
-/**
- * Implements Special:Userlogout
- *
- * @ingroup SpecialPage
- */
-class SpecialUserlogout extends UnlistedSpecialPage {
-       function __construct() {
-               parent::__construct( 'Userlogout' );
-       }
-
-       public function doesWrites() {
-               return true;
-       }
-
-       function execute( $par ) {
-               /**
-                * Some satellite ISPs use broken precaching schemes that log people out straight after
-                * they're logged in (bug 17790). Luckily, there's a way to detect such requests.
-                */
-               if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
-                       wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
-                       throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
-               }
-
-               $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',
-                               [
-                                       $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
-                               ]
-                       );
-               }
-
-               $user = $this->getUser();
-               $oldName = $user->getName();
-               $user->logout();
-
-               $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
-                       $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
-
-               $out = $this->getOutput();
-               $out->addWikiMsg( 'logouttext', $loginURL );
-
-               // Hook.
-               $injected_html = '';
-               Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
-               $out->addHTML( $injected_html );
-
-               $out->returnToMain();
-       }
-
-       protected function getGroupName() {
-               return 'login';
-       }
-}
index be110aa..d5affc7 100644 (file)
@@ -250,8 +250,6 @@ class UserrightsPage extends SpecialPage {
         * @return array Tuple of added, then removed groups
         */
        function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
-               global $wgAuth;
-
                // Validate input set...
                $isself = $user->getName() == $this->getUser()->getName();
                $groups = $user->getGroups();
@@ -293,7 +291,9 @@ class UserrightsPage extends SpecialPage {
 
                // update groups in external authentication database
                Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(), $reason ] );
-               $wgAuth->updateExternalDBGroups( $user, $add, $remove );
+               MediaWiki\Auth\AuthManager::callLegacyAuthPlugin(
+                       'updateExternalDBGroups', [ $user, $add, $remove ]
+               );
 
                wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" );
                wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" );
diff --git a/includes/specials/helpers/LoginHelper.php b/includes/specials/helpers/LoginHelper.php
new file mode 100644 (file)
index 0000000..f853f41
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * Helper functions for the login form that need to be shared with other special pages
+ * (such as CentralAuth's SpecialCentralLogin).
+ * @since 1.27
+ */
+class LoginHelper extends ContextSource {
+       /**
+        * Valid error and warning messages
+        *
+        * Special:Userlogin can show an error or warning message on the form when
+        * coming from another page. This is done via the ?error= or ?warning= GET
+        * parameters.
+        *
+        * This array is the list of valid message keys. Further keys can be added by the
+        * LoginFormValidErrorMessages hook. All other values will be ignored.
+        *
+        * @var string[]
+        */
+       public static $validErrorMessages = [
+               'exception-nologin-text',
+               'watchlistanontext',
+               'changeemail-no-info',
+               'resetpass-no-info',
+               'confirmemail_needlogin',
+               'prefsnologintext2',
+       ];
+
+       /**
+        * Returns an array of all valid error messages.
+        *
+        * @return array
+        * @see LoginHelper::$validErrorMessages
+        */
+       public static function getValidErrorMessages() {
+               static $messages = null;
+               if ( !$messages ) {
+                       $messages = self::$validErrorMessages;
+                       Hooks::run( 'LoginFormValidErrorMessages', [ &$messages ] );
+               }
+
+               return $messages;
+       }
+
+       public function __construct( IContextSource $context ) {
+               $this->setContext( $context );
+       }
+
+       /**
+        * Show a return link or redirect to it.
+        * Extensions can change where the link should point or inject content into the page
+        * (which will change it from redirect to link mode).
+        *
+        * @param string $type One of the following:
+        *    - error: display a return to link ignoring $wgRedirectOnLogin
+        *    - success: display a return to link using $wgRedirectOnLogin if needed
+        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
+        * @param string $returnTo
+        * @param array|string $returnToQuery
+        * @param bool $stickHTTPS Keep redirect link on HTTPS
+        */
+       public function showReturnToPage(
+               $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
+       ) {
+               global $wgRedirectOnLogin, $wgSecureLogin;
+
+               if ( $type !== 'error' && $wgRedirectOnLogin !== null ) {
+                       $returnTo = $wgRedirectOnLogin;
+                       $returnToQuery = [];
+               } elseif ( is_string( $returnToQuery ) ) {
+                       $returnToQuery = wfCgiToArray( $returnToQuery );
+               }
+
+               // Allow modification of redirect behavior
+               Hooks::run( 'PostLoginRedirect', [ &$returnTo, &$returnToQuery, &$type ] );
+
+               $returnToTitle = Title::newFromText( $returnTo ) ?:  Title::newMainPage();
+
+               if ( $wgSecureLogin && !$stickHTTPS ) {
+                       $options = [ 'http' ];
+                       $proto = PROTO_HTTP;
+               } elseif ( $wgSecureLogin ) {
+                       $options = [ 'https' ];
+                       $proto = PROTO_HTTPS;
+               } else {
+                       $options = [];
+                       $proto = PROTO_RELATIVE;
+               }
+
+               if ( $type === 'successredirect' ) {
+                       $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
+                       $this->getOutput()->redirect( $redirectUrl );
+               } else {
+                       $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
+               }
+       }
+}
diff --git a/includes/specials/pre-authmanager/README b/includes/specials/pre-authmanager/README
new file mode 100644 (file)
index 0000000..1cfdd5f
--- /dev/null
@@ -0,0 +1,10 @@
+This directory temporarily hosts pre-AuthManager code as a way of feature-flagging.
+Class names are postfixed with 'PreAuthManager' and SpecialPageFactory adds/removes
+that postfix based on the feature flag.
+
+This is a horrible hack that will only be in place for a few weeks, to allow instant
+rollback while AuthManager is tested in WMF production and major problems are ironed
+out. In the past such issues have been handled via deployment branches, but that
+meant blocking the work of all WMF developers from being deployed. This is hoped
+to be a less disruptive method.
+
diff --git a/includes/specials/pre-authmanager/SpecialChangeEmail.php b/includes/specials/pre-authmanager/SpecialChangeEmail.php
new file mode 100644 (file)
index 0000000..7861562
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+/**
+ * Implements Special:ChangeEmail
+ *
+ * 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 change their email address.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialChangeEmailPreAuthManager extends FormSpecialPage {
+       /**
+        * @var Status
+        */
+       private $status;
+
+       public function __construct() {
+               parent::__construct( 'ChangeEmail', 'editmyprivateinfo' );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       /**
+        * @return bool
+        */
+       public function isListed() {
+               global $wgAuth;
+
+               return $wgAuth->allowPropChange( 'emailaddress' );
+       }
+
+       /**
+        * Main execution point
+        * @param string $par
+        */
+       function execute( $par ) {
+               $out = $this->getOutput();
+               $out->disallowUserJs();
+
+               parent::execute( $par );
+       }
+
+       protected function checkExecutePermissions( User $user ) {
+               global $wgAuth;
+
+               if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
+                       throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
+               }
+
+               $this->requireLogin( 'changeemail-no-info' );
+
+               // This could also let someone check the current email address, so
+               // require both permissions.
+               if ( !$this->getUser()->isAllowed( 'viewmyprivateinfo' ) ) {
+                       throw new PermissionsError( 'viewmyprivateinfo' );
+               }
+
+               parent::checkExecutePermissions( $user );
+       }
+
+       protected function getFormFields() {
+               $user = $this->getUser();
+
+               $fields = [
+                       'Name' => [
+                               'type' => 'info',
+                               'label-message' => 'username',
+                               'default' => $user->getName(),
+                       ],
+                       'OldEmail' => [
+                               'type' => 'info',
+                               'label-message' => 'changeemail-oldemail',
+                               'default' => $user->getEmail() ?: $this->msg( 'changeemail-none' )->text(),
+                       ],
+                       'NewEmail' => [
+                               'type' => 'email',
+                               'label-message' => 'changeemail-newemail',
+                               'autofocus' => true,
+                               'help-message' => 'changeemail-newemail-help',
+                       ],
+               ];
+
+               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
+                       $fields['Password'] = [
+                               'type' => 'password',
+                               'label-message' => 'changeemail-password'
+                       ];
+               }
+
+               return $fields;
+       }
+
+       protected function getDisplayFormat() {
+               return 'ooui';
+       }
+
+       protected function alterForm( HTMLForm $form ) {
+               $form->setId( 'mw-changeemail-form' );
+               $form->setTableId( 'mw-changeemail-table' );
+               $form->setSubmitTextMsg( 'changeemail-submit' );
+               $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
+               $form->addHeaderText( $this->msg( 'changeemail-header' )->parseAsBlock() );
+               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
+                       $form->addHeaderText( $this->msg( 'changeemail-passwordrequired' )->parseAsBlock() );
+               }
+       }
+
+       public function onSubmit( array $data ) {
+               $password = isset( $data['Password'] ) ? $data['Password'] : null;
+               $status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] );
+
+               $this->status = $status;
+
+               return $status;
+       }
+
+       public function onSuccess() {
+               $request = $this->getRequest();
+
+               $returnto = $request->getVal( 'returnto' );
+               $titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
+               if ( !$titleObj instanceof Title ) {
+                       $titleObj = Title::newMainPage();
+               }
+               $query = $request->getVal( 'returntoquery' );
+
+               if ( $this->status->value === true ) {
+                       $this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
+               } elseif ( $this->status->value === 'eauth' ) {
+                       # Notify user that a confirmation email has been sent...
+                       $this->getOutput()->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1\n</div>",
+                               'eauthentsent', $this->getUser()->getName() );
+                       // just show the link to go back
+                       $this->getOutput()->addReturnTo( $titleObj, wfCgiToArray( $query ) );
+               }
+       }
+
+       /**
+        * @param User $user
+        * @param string $pass
+        * @param string $newaddr
+        * @return Status
+        */
+       private function attemptChange( User $user, $pass, $newaddr ) {
+               global $wgAuth;
+
+               if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) {
+                       return Status::newFatal( 'invalidemailaddress' );
+               }
+
+               if ( $newaddr === $user->getEmail() ) {
+                       return Status::newFatal( 'changeemail-nochange' );
+               }
+
+               $throttleInfo = LoginForm::incrementLoginThrottle( $user->getName() );
+               if ( $throttleInfo ) {
+                       $lang = $this->getLanguage();
+                       return Status::newFatal(
+                               'changeemail-throttled',
+                               $lang->formatDuration( $throttleInfo['wait'] )
+                       );
+               }
+
+               if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' )
+                       && !$user->checkTemporaryPassword( $pass )
+                       && !$user->checkPassword( $pass )
+               ) {
+                       return Status::newFatal( 'wrongpassword' );
+               }
+
+               LoginForm::clearLoginThrottle( $user->getName() );
+
+               $oldaddr = $user->getEmail();
+               $status = $user->setEmailWithConfirmation( $newaddr );
+               if ( !$status->isGood() ) {
+                       return $status;
+               }
+
+               Hooks::run( 'PrefsEmailAudit', [ $user, $oldaddr, $newaddr ] );
+
+               $user->saveSettings();
+
+               $wgAuth->updateExternalDB( $user );
+
+               return $status;
+       }
+
+       public function requiresUnblock() {
+               return false;
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+}
diff --git a/includes/specials/pre-authmanager/SpecialChangePassword.php b/includes/specials/pre-authmanager/SpecialChangePassword.php
new file mode 100644 (file)
index 0000000..3955fee
--- /dev/null
@@ -0,0 +1,343 @@
+<?php
+/**
+ * Implements Special:ChangePassword
+ *
+ * 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 recover their password.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialChangePasswordPreAuthManager extends FormSpecialPage {
+       protected $mUserName;
+       protected $mDomain;
+
+       // Optional Wikitext Message to show above the password change form
+       protected $mPreTextMessage = null;
+
+       // label for old password input
+       protected $mOldPassMsg = null;
+
+       public function __construct() {
+               parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
+               $this->listed( false );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       /**
+        * Main execution point
+        * @param string|null $par
+        */
+       function execute( $par ) {
+               $this->getOutput()->disallowUserJs();
+
+               parent::execute( $par );
+       }
+
+       protected function checkExecutePermissions( User $user ) {
+               parent::checkExecutePermissions( $user );
+
+               if ( !$this->getRequest()->wasPosted() ) {
+                       $this->requireLogin( 'resetpass-no-info' );
+               }
+       }
+
+       /**
+        * Set a message at the top of the Change Password form
+        * @since 1.23
+        * @param Message $msg Message to parse and add to the form header
+        */
+       public function setChangeMessage( Message $msg ) {
+               $this->mPreTextMessage = $msg;
+       }
+
+       /**
+        * Set a message at the top of the Change Password form
+        * @since 1.23
+        * @param string $msg Message label for old/temp password field
+        */
+       public function setOldPasswordMessage( $msg ) {
+               $this->mOldPassMsg = $msg;
+       }
+
+       protected function getFormFields() {
+               $user = $this->getUser();
+               $request = $this->getRequest();
+
+               $oldpassMsg = $this->mOldPassMsg;
+               if ( $oldpassMsg === null ) {
+                       $oldpassMsg = $user->isLoggedIn() ? 'oldpassword' : 'resetpass-temp-password';
+               }
+
+               $fields = [
+                       'Name' => [
+                               'type' => 'info',
+                               'label-message' => 'username',
+                               'default' => $request->getVal( 'wpName', $user->getName() ),
+                       ],
+                       'Password' => [
+                               'type' => 'password',
+                               'label-message' => $oldpassMsg,
+                       ],
+                       'NewPassword' => [
+                               'type' => 'password',
+                               'label-message' => 'newpassword',
+                       ],
+                       'Retype' => [
+                               'type' => 'password',
+                               'label-message' => 'retypenew',
+                       ],
+               ];
+
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $fields['LoginOnChangeToken'] = [
+                               'type' => 'hidden',
+                               'label' => 'Change Password Token',
+                               'default' => LoginForm::getLoginToken()->toString(),
+                       ];
+               }
+
+               $extraFields = [];
+               Hooks::run( 'ChangePasswordForm', [ &$extraFields ] );
+               foreach ( $extraFields as $extra ) {
+                       list( $name, $label, $type, $default ) = $extra;
+                       $fields[$name] = [
+                               'type' => $type,
+                               'name' => $name,
+                               'label-message' => $label,
+                               'default' => $default,
+                       ];
+               }
+
+               if ( !$user->isLoggedIn() ) {
+                       $fields['Remember'] = [
+                               'type' => 'check',
+                               'label' => $this->msg( 'remembermypassword' )
+                                               ->numParams(
+                                                       ceil( $this->getConfig()->get( 'CookieExpiration' ) / ( 3600 * 24 ) )
+                                               )->text(),
+                               'default' => $request->getVal( 'wpRemember' ),
+                       ];
+               }
+
+               return $fields;
+       }
+
+       protected function alterForm( HTMLForm $form ) {
+               $form->setId( 'mw-resetpass-form' );
+               $form->setTableId( 'mw-resetpass-table' );
+               $form->setWrapperLegendMsg( 'resetpass_header' );
+               $form->setSubmitTextMsg(
+                       $this->getUser()->isLoggedIn()
+                               ? 'resetpass-submit-loggedin'
+                               : 'resetpass_submit'
+               );
+               $form->addButton( [
+                       'name' => 'wpCancel',
+                       'value' => $this->msg( 'resetpass-submit-cancel' )->text()
+               ] );
+               $form->setHeaderText( $this->msg( 'resetpass_text' )->parseAsBlock() );
+               if ( $this->mPreTextMessage instanceof Message ) {
+                       $form->addPreText( $this->mPreTextMessage->parseAsBlock() );
+               }
+               $form->addHiddenFields(
+                       $this->getRequest()->getValues( 'wpName', 'wpDomain', 'returnto', 'returntoquery' ) );
+       }
+
+       public function onSubmit( array $data ) {
+               global $wgAuth;
+
+               $request = $this->getRequest();
+
+               if ( $request->getCheck( 'wpLoginToken' ) ) {
+                       // This comes from Special:Userlogin when logging in with a temporary password
+                       return false;
+               }
+
+               if ( !$this->getUser()->isLoggedIn()
+                       && !LoginForm::getLoginToken()->match( $request->getVal( 'wpLoginOnChangeToken' ) )
+               ) {
+                       // Potential CSRF (bug 62497)
+                       return false;
+               }
+
+               if ( $request->getCheck( 'wpCancel' ) ) {
+                       $returnto = $request->getVal( 'returnto' );
+                       $titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
+                       if ( !$titleObj instanceof Title ) {
+                               $titleObj = Title::newMainPage();
+                       }
+                       $query = $request->getVal( 'returntoquery' );
+                       $this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
+
+                       return true;
+               }
+
+               $this->mUserName = $request->getVal( 'wpName', $this->getUser()->getName() );
+               $this->mDomain = $wgAuth->getDomain();
+
+               if ( !$wgAuth->allowPasswordChange() ) {
+                       throw new ErrorPageError( 'changepassword', 'resetpass_forbidden' );
+               }
+
+               $status = $this->attemptReset( $data['Password'], $data['NewPassword'], $data['Retype'] );
+
+               return $status;
+       }
+
+       public function onSuccess() {
+               if ( $this->getUser()->isLoggedIn() ) {
+                       $this->getOutput()->wrapWikiMsg(
+                               "<div class=\"successbox\">\n$1\n</div>",
+                               'changepassword-success'
+                       );
+                       $this->getOutput()->returnToMain();
+               } else {
+                       $request = $this->getRequest();
+                       LoginForm::clearLoginToken();
+                       $token = LoginForm::getLoginToken()->toString();
+                       $data = [
+                               'action' => 'submitlogin',
+                               'wpName' => $this->mUserName,
+                               'wpDomain' => $this->mDomain,
+                               'wpLoginToken' => $token,
+                               'wpPassword' => $request->getVal( 'wpNewPassword' ),
+                       ] + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' );
+                       $login = new LoginForm( new DerivativeRequest( $request, $data, true ) );
+                       $login->setContext( $this->getContext() );
+                       $login->execute( null );
+               }
+       }
+
+       /**
+        * Checks the new password if it meets the requirements for passwords and set
+        * it as a current password, otherwise set the passed Status object to fatal
+        * and doesn't change anything
+        *
+        * @param string $oldpass The current (temporary) password.
+        * @param string $newpass The password to set.
+        * @param string $retype The string of the retype password field to check with newpass
+        * @return Status
+        */
+       protected function attemptReset( $oldpass, $newpass, $retype ) {
+               $isSelf = ( $this->mUserName === $this->getUser()->getName() );
+               if ( $isSelf ) {
+                       $user = $this->getUser();
+               } else {
+                       $user = User::newFromName( $this->mUserName );
+               }
+
+               if ( !$user || $user->isAnon() ) {
+                       return Status::newFatal( $this->msg( 'nosuchusershort', $this->mUserName ) );
+               }
+
+               if ( $newpass !== $retype ) {
+                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'badretype' ] );
+                       return Status::newFatal( $this->msg( 'badretype' ) );
+               }
+
+               $throttleInfo = LoginForm::incrementLoginThrottle( $this->mUserName );
+               if ( $throttleInfo ) {
+                       return Status::newFatal( $this->msg( 'changepassword-throttled' )
+                               ->durationParams( $throttleInfo['wait'] )
+                       );
+               }
+
+               // @todo Make these separate messages, since the message is written for both cases
+               if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) {
+                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'wrongpassword' ] );
+                       return Status::newFatal( $this->msg( 'resetpass-wrong-oldpass' ) );
+               }
+
+               // User is resetting their password to their old password
+               if ( $oldpass === $newpass ) {
+                       return Status::newFatal( $this->msg( 'resetpass-recycled' ) );
+               }
+
+               // Do AbortChangePassword after checking mOldpass, so we don't leak information
+               // by possibly aborting a new password before verifying the old password.
+               $abortMsg = 'resetpass-abort-generic';
+               if ( !Hooks::run( 'AbortChangePassword', [ $user, $oldpass, $newpass, &$abortMsg ] ) ) {
+                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'abortreset' ] );
+                       return Status::newFatal( $this->msg( $abortMsg ) );
+               }
+
+               // Please reset throttle for successful logins, thanks!
+               LoginForm::clearLoginThrottle( $this->mUserName );
+
+               try {
+                       $user->setPassword( $newpass );
+                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'success' ] );
+               } catch ( PasswordError $e ) {
+                       Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'error' ] );
+                       return Status::newFatal( new RawMessage( $e->getMessage() ) );
+               }
+
+               if ( $isSelf ) {
+                       // This is needed to keep the user connected since
+                       // changing the password also modifies the user's token.
+                       $remember = $this->getRequest()->getCookie( 'Token' ) !== null;
+                       $user->setCookies( null, null, $remember );
+               }
+               $user->saveSettings();
+               $this->resetPasswordExpiration( $user );
+               return Status::newGood();
+       }
+
+       public function requiresUnblock() {
+               return false;
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       /**
+        * For resetting user password expiration, until AuthManager comes along
+        * @param User $user
+        */
+       private function resetPasswordExpiration( User $user ) {
+               global $wgPasswordExpirationDays;
+               $newExpire = null;
+               if ( $wgPasswordExpirationDays ) {
+                       $newExpire = wfTimestamp(
+                               TS_MW,
+                               time() + ( $wgPasswordExpirationDays * 24 * 3600 )
+                       );
+               }
+               // Give extensions a chance to force an expiration
+               Hooks::run( 'ResetPasswordExpiration', [ $this, &$newExpire ] );
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       [ 'user_password_expires' => $dbw->timestampOrNull( $newExpire ) ],
+                       [ 'user_id' => $user->getId() ],
+                       __METHOD__
+               );
+       }
+
+       protected function getDisplayFormat() {
+               return 'ooui';
+       }
+}
diff --git a/includes/specials/pre-authmanager/SpecialCreateAccount.php b/includes/specials/pre-authmanager/SpecialCreateAccount.php
new file mode 100644 (file)
index 0000000..14f70b5
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
+ *
+ * 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
+ */
+
+/**
+ * Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
+ * @todo FIXME: This (and the rest of the login frontend) needs to die a horrible painful death
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialCreateAccountPreAuthManager extends SpecialRedirectToSpecial {
+       function __construct() {
+               parent::__construct(
+                       'CreateAccount',
+                       'Userlogin',
+                       'signup',
+                       [ 'returnto', 'returntoquery', 'uselang' ]
+               );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       // No reason to hide this link on Special:Specialpages
+       public function isListed() {
+               return true;
+       }
+
+       public function isRestricted() {
+               return !User::groupHasPermission( '*', 'createaccount' );
+       }
+
+       public function userCanExecute( User $user ) {
+               return $user->isAllowed( 'createaccount' );
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+}
diff --git a/includes/specials/pre-authmanager/SpecialPasswordReset.php b/includes/specials/pre-authmanager/SpecialPasswordReset.php
new file mode 100644 (file)
index 0000000..e8719a7
--- /dev/null
@@ -0,0 +1,378 @@
+<?php
+/**
+ * Implements Special:PasswordReset
+ *
+ * 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
+ */
+
+/**
+ * Special page for requesting a password reset email
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialPasswordResetPreAuthManager extends FormSpecialPage {
+       /**
+        * @var Message
+        */
+       private $email;
+
+       /**
+        * @var User
+        */
+       private $firstUser;
+
+       /**
+        * @var Status
+        */
+       private $result;
+
+       /**
+        * @var string $method Identifies which password reset field was specified by the user.
+        */
+       private $method;
+
+       public function __construct() {
+               parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       public function userCanExecute( User $user ) {
+               return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
+       }
+
+       public function checkExecutePermissions( User $user ) {
+               $error = $this->canChangePassword( $user );
+               if ( is_string( $error ) ) {
+                       throw new ErrorPageError( 'internalerror', $error );
+               } elseif ( !$error ) {
+                       throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
+               }
+
+               parent::checkExecutePermissions( $user );
+       }
+
+       protected function getFormFields() {
+               global $wgAuth;
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+               $a = [];
+               if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
+                       $a['Username'] = [
+                               'type' => 'text',
+                               'label-message' => 'passwordreset-username',
+                       ];
+
+                       if ( $this->getUser()->isLoggedIn() ) {
+                               $a['Username']['default'] = $this->getUser()->getName();
+                       }
+               }
+
+               if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
+                       $a['Email'] = [
+                               'type' => 'email',
+                               'label-message' => 'passwordreset-email',
+                       ];
+               }
+
+               if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
+                       $domains = $wgAuth->domainList();
+                       $a['Domain'] = [
+                               'type' => 'select',
+                               'options' => $domains,
+                               'label-message' => 'passwordreset-domain',
+                       ];
+               }
+
+               if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
+                       $a['Capture'] = [
+                               'type' => 'check',
+                               'label-message' => 'passwordreset-capture',
+                               'help-message' => 'passwordreset-capture-help',
+                       ];
+               }
+
+               return $a;
+       }
+
+       protected function getDisplayFormat() {
+               return 'ooui';
+       }
+
+       public function alterForm( HTMLForm $form ) {
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+
+               $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
+               $i = 0;
+               if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
+                       $i++;
+               }
+               if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
+                       $i++;
+               }
+               if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
+                       $i++;
+               }
+
+               $message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
+
+               $form->setHeaderText( $this->msg( $message, $i )->parseAsBlock() );
+               $form->setSubmitTextMsg( 'mailmypassword' );
+       }
+
+       /**
+        * Process the form.  At this point we know that the user passes all the criteria in
+        * userCanExecute(), and if the data array contains 'Username', etc, then Username
+        * resets are allowed.
+        * @param array $data
+        * @throws MWException
+        * @throws ThrottledError|PermissionsError
+        * @return bool|array
+        */
+       public function onSubmit( array $data ) {
+               global $wgAuth, $wgMinimalPasswordLength;
+
+               if ( isset( $data['Domain'] ) ) {
+                       if ( $wgAuth->validDomain( $data['Domain'] ) ) {
+                               $wgAuth->setDomain( $data['Domain'] );
+                       } else {
+                               $wgAuth->setDomain( 'invaliddomain' );
+                       }
+               }
+
+               if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) {
+                       // The user knows they don't have the passwordreset permission,
+                       // but they tried to spoof the form. That's naughty
+                       throw new PermissionsError( 'passwordreset' );
+               }
+
+               /**
+                * @var $firstUser User
+                * @var $users User[]
+                */
+
+               if ( isset( $data['Username'] ) && $data['Username'] !== '' ) {
+                       $method = 'username';
+                       $users = [ User::newFromName( $data['Username'] ) ];
+               } elseif ( isset( $data['Email'] )
+                       && $data['Email'] !== ''
+                       && Sanitizer::validateEmail( $data['Email'] )
+               ) {
+                       $method = 'email';
+                       $res = wfGetDB( DB_SLAVE )->select(
+                               'user',
+                               User::selectFields(),
+                               [ 'user_email' => $data['Email'] ],
+                               __METHOD__
+                       );
+
+                       if ( $res ) {
+                               $users = [];
+
+                               foreach ( $res as $row ) {
+                                       $users[] = User::newFromRow( $row );
+                               }
+                       } else {
+                               // Some sort of database error, probably unreachable
+                               throw new MWException( 'Unknown database error in ' . __METHOD__ );
+                       }
+               } else {
+                       // The user didn't supply any data
+                       return false;
+               }
+
+               // Check for hooks (captcha etc), and allow them to modify the users list
+               $error = [];
+               if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
+                       return [ $error ];
+               }
+
+               $this->method = $method;
+
+               if ( count( $users ) == 0 ) {
+                       if ( $method == 'email' ) {
+                               // Don't reveal whether or not an email address is in use
+                               return true;
+                       } else {
+                               return [ 'noname' ];
+                       }
+               }
+
+               $firstUser = $users[0];
+
+               if ( !$firstUser instanceof User || !$firstUser->getId() ) {
+                       // Don't parse username as wikitext (bug 65501)
+                       return [ [ 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ] ];
+               }
+
+               // Check against the rate limiter
+               if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
+                       throw new ThrottledError;
+               }
+
+               // Check against password throttle
+               foreach ( $users as $user ) {
+                       if ( $user->isPasswordReminderThrottled() ) {
+
+                               # Round the time in hours to 3 d.p., in case someone is specifying
+                               # minutes or seconds.
+                               return [ [
+                                       'throttled-mailpassword',
+                                       round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 )
+                               ] ];
+                       }
+               }
+
+               // All the users will have the same email address
+               if ( $firstUser->getEmail() == '' ) {
+                       // This won't be reachable from the email route, so safe to expose the username
+                       return [ [ 'noemail', wfEscapeWikiText( $firstUser->getName() ) ] ];
+               }
+
+               // We need to have a valid IP address for the hook, but per bug 18347, we should
+               // send the user's name if they're logged in.
+               $ip = $this->getRequest()->getIP();
+               if ( !$ip ) {
+                       return [ 'badipaddress' ];
+               }
+               $caller = $this->getUser();
+               Hooks::run( 'User::mailPasswordInternal', [ &$caller, &$ip, &$firstUser ] );
+               $username = $caller->getName();
+               $msg = IP::isValid( $username )
+                       ? 'passwordreset-emailtext-ip'
+                       : 'passwordreset-emailtext-user';
+
+               // Send in the user's language; which should hopefully be the same
+               $userLanguage = $firstUser->getOption( 'language' );
+
+               $passwords = [];
+               foreach ( $users as $user ) {
+                       $password = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
+                       $user->setNewpassword( $password );
+                       $user->saveSettings();
+                       $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password )
+                               ->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
+               }
+               $passwordBlock = implode( "\n\n", $passwords );
+
+               $this->email = $this->msg( $msg )->inLanguage( $userLanguage );
+               $this->email->params(
+                       $username,
+                       $passwordBlock,
+                       count( $passwords ),
+                       '<' . Title::newMainPage()->getCanonicalURL() . '>',
+                       round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 )
+               );
+
+               $title = $this->msg( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
+
+               $this->result = $firstUser->sendMail( $title->text(), $this->email->text() );
+
+               if ( isset( $data['Capture'] ) && $data['Capture'] ) {
+                       // Save the user, will be used if an error occurs when sending the email
+                       $this->firstUser = $firstUser;
+               } else {
+                       // Blank the email if the user is not supposed to see it
+                       $this->email = null;
+               }
+
+               if ( $this->result->isGood() ) {
+                       return true;
+               } elseif ( isset( $data['Capture'] ) && $data['Capture'] ) {
+                       // The email didn't send, but maybe they knew that and that's why they captured it
+                       return true;
+               } else {
+                       // @todo FIXME: The email wasn't sent, but we have already set
+                       // the password throttle timestamp, so they won't be able to try
+                       // again until it expires...  :(
+                       return [ [ 'mailerror', $this->result->getMessage() ] ];
+               }
+       }
+
+       public function onSuccess() {
+               if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
+                       // @todo Logging
+
+                       if ( $this->result->isGood() ) {
+                               $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
+                       } else {
+                               $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
+                                       $this->result->getMessage(), $this->firstUser->getName() );
+                       }
+
+                       $this->getOutput()->addHTML( Html::rawElement( 'pre', [], $this->email->escaped() ) );
+               }
+
+               if ( $this->method === 'email' ) {
+                       $this->getOutput()->addWikiMsg( 'passwordreset-emailsentemail' );
+               } else {
+                       $this->getOutput()->addWikiMsg( 'passwordreset-emailsentusername' );
+               }
+
+               $this->getOutput()->returnToMain();
+       }
+
+       protected function canChangePassword( User $user ) {
+               global $wgAuth;
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+
+               // Maybe password resets are disabled, or there are no allowable routes
+               if ( !is_array( $resetRoutes ) ||
+                       !in_array( true, array_values( $resetRoutes ) )
+               ) {
+                       return 'passwordreset-disabled';
+               }
+
+               // Maybe the external auth plugin won't allow local password changes
+               if ( !$wgAuth->allowPasswordChange() ) {
+                       return 'resetpass_forbidden';
+               }
+
+               // Maybe email features have been disabled
+               if ( !$this->getConfig()->get( 'EnableEmail' ) ) {
+                       return 'passwordreset-emaildisabled';
+               }
+
+               // Maybe the user is blocked (check this here rather than relying on the parent
+               // method as we have a more specific error message to use here
+               if ( $user->isBlocked() ) {
+                       return 'blocked-mailpassword';
+               }
+
+               return true;
+       }
+
+       /**
+        * Hide the password reset page if resets are disabled.
+        * @return bool
+        */
+       function isListed() {
+               if ( $this->canChangePassword( $this->getUser() ) === true ) {
+                       return parent::isListed();
+               }
+
+               return false;
+       }
+
+       protected function getGroupName() {
+               return 'users';
+       }
+}
diff --git a/includes/specials/pre-authmanager/SpecialUserlogin.php b/includes/specials/pre-authmanager/SpecialUserlogin.php
new file mode 100644 (file)
index 0000000..4af5cf6
--- /dev/null
@@ -0,0 +1,1842 @@
+<?php
+/**
+ * Implements Special:UserLogin
+ *
+ * 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
+ */
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LogLevel;
+use MediaWiki\Session\SessionManager;
+
+/**
+ * Implements Special:UserLogin
+ *
+ * @ingroup SpecialPage
+ */
+class LoginFormPreAuthManager extends SpecialPage {
+       const SUCCESS = 0;
+       const NO_NAME = 1;
+       const ILLEGAL = 2;
+       const WRONG_PLUGIN_PASS = 3;
+       const NOT_EXISTS = 4;
+       const WRONG_PASS = 5;
+       const EMPTY_PASS = 6;
+       const RESET_PASS = 7;
+       const ABORTED = 8;
+       const CREATE_BLOCKED = 9;
+       const THROTTLED = 10;
+       const USER_BLOCKED = 11;
+       const NEED_TOKEN = 12;
+       const WRONG_TOKEN = 13;
+       const USER_MIGRATED = 14;
+
+       public static $statusCodes = [
+               self::SUCCESS => 'success',
+               self::NO_NAME => 'no_name',
+               self::ILLEGAL => 'illegal',
+               self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass',
+               self::NOT_EXISTS => 'not_exists',
+               self::WRONG_PASS => 'wrong_pass',
+               self::EMPTY_PASS => 'empty_pass',
+               self::RESET_PASS => 'reset_pass',
+               self::ABORTED => 'aborted',
+               self::CREATE_BLOCKED => 'create_blocked',
+               self::THROTTLED => 'throttled',
+               self::USER_BLOCKED => 'user_blocked',
+               self::NEED_TOKEN => 'need_token',
+               self::WRONG_TOKEN => 'wrong_token',
+               self::USER_MIGRATED => 'user_migrated',
+       ];
+
+       /**
+        * Valid error and warning messages
+        *
+        * Special:Userlogin can show an error or warning message on the form when
+        * coming from another page. This is done via the ?error= or ?warning= GET
+        * parameters.
+        *
+        * This array is the list of valid message keys. All other values will be
+        * ignored.
+        *
+        * @since 1.24
+        * @var string[]
+        */
+       public static $validErrorMessages = [
+               'exception-nologin-text',
+               'watchlistanontext',
+               'changeemail-no-info',
+               'resetpass-no-info',
+               'confirmemail_needlogin',
+               'prefsnologintext2',
+       ];
+
+       public $mAbortLoginErrorMsg = null;
+       /**
+        * @var int How many seconds user is throttled for
+        * @since 1.27
+        */
+       public $mThrottleWait = '?';
+
+       protected $mUsername;
+       protected $mPassword;
+       protected $mRetype;
+       protected $mReturnTo;
+       protected $mCookieCheck;
+       protected $mPosted;
+       protected $mAction;
+       protected $mCreateaccount;
+       protected $mCreateaccountMail;
+       protected $mLoginattempt;
+       protected $mRemember;
+       protected $mEmail;
+       protected $mDomain;
+       protected $mLanguage;
+       protected $mSkipCookieCheck;
+       protected $mReturnToQuery;
+       protected $mToken;
+       protected $mStickHTTPS;
+       protected $mType;
+       protected $mReason;
+       protected $mRealName;
+       protected $mEntryError = '';
+       protected $mEntryErrorType = 'error';
+
+       private $mTempPasswordUsed;
+       private $mLoaded = false;
+       private $mSecureLoginUrl;
+
+       /** @var WebRequest */
+       private $mOverrideRequest = null;
+
+       /** @var WebRequest Effective request; set at the beginning of load */
+       private $mRequest = null;
+
+       /**
+        * @param WebRequest $request
+        */
+       public function __construct( $request = null ) {
+               global $wgUseMediaWikiUIEverywhere;
+               parent::__construct( 'Userlogin' );
+
+               $this->mOverrideRequest = $request;
+               // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
+               $wgUseMediaWikiUIEverywhere = true;
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       /**
+        * Returns an array of all valid error messages.
+        *
+        * @return array
+        */
+       public static function getValidErrorMessages() {
+               static $messages = null;
+               if ( !$messages ) {
+                       $messages = self::$validErrorMessages;
+                       Hooks::run( 'LoginFormValidErrorMessages', [ &$messages ] );
+               }
+
+               return $messages;
+       }
+
+       /**
+        * Loader
+        */
+       function load() {
+               global $wgAuth, $wgHiddenPrefs, $wgEnableEmail;
+
+               if ( $this->mLoaded ) {
+                       return;
+               }
+               $this->mLoaded = true;
+
+               if ( $this->mOverrideRequest === null ) {
+                       $request = $this->getRequest();
+               } else {
+                       $request = $this->mOverrideRequest;
+               }
+               $this->mRequest = $request;
+
+               $this->mType = $request->getText( 'type' );
+               $this->mUsername = $request->getText( 'wpName' );
+               $this->mPassword = $request->getText( 'wpPassword' );
+               $this->mRetype = $request->getText( 'wpRetype' );
+               $this->mDomain = $request->getText( 'wpDomain' );
+               $this->mReason = $request->getText( 'wpReason' );
+               $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
+               $this->mPosted = $request->wasPosted();
+               $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
+                       && $wgEnableEmail;
+               $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ) && !$this->mCreateaccountMail;
+               $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
+               $this->mAction = $request->getVal( 'action' );
+               $this->mRemember = $request->getCheck( 'wpRemember' );
+               $this->mFromHTTP = $request->getBool( 'fromhttp', false )
+                       || $request->getBool( 'wpFromhttp', false );
+               $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
+                       || $request->getBool( 'wpForceHttps', false );
+               $this->mLanguage = $request->getText( 'uselang' );
+               $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
+               $this->mToken = $this->mType == 'signup'
+                       ? $request->getVal( 'wpCreateaccountToken' )
+                       : $request->getVal( 'wpLoginToken' );
+               $this->mReturnTo = $request->getVal( 'returnto', '' );
+               $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
+
+               // Show an error or warning passed on from a previous page
+               $entryError = $this->msg( $request->getVal( 'error', '' ) );
+               $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
+               // bc: provide login link as a parameter for messages where the translation
+               // was not updated
+               $loginreqlink = Linker::linkKnown(
+                       $this->getPageTitle(),
+                       $this->msg( 'loginreqlink' )->escaped(),
+                       [],
+                       [
+                               'returnto' => $this->mReturnTo,
+                               'returntoquery' => $this->mReturnToQuery,
+                               'uselang' => $this->mLanguage,
+                               'fromhttp' => $this->mFromHTTP ? '1' : '0',
+                       ]
+               );
+
+               // Only show valid error or warning messages.
+               if ( $entryError->exists()
+                       && in_array( $entryError->getKey(), self::getValidErrorMessages() )
+               ) {
+                       $this->mEntryErrorType = 'error';
+                       $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
+
+               } elseif ( $entryWarning->exists()
+                       && in_array( $entryWarning->getKey(), self::getValidErrorMessages() )
+               ) {
+                       $this->mEntryErrorType = 'warning';
+                       $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
+               }
+
+               if ( $wgEnableEmail ) {
+                       $this->mEmail = $request->getText( 'wpEmail' );
+               } else {
+                       $this->mEmail = '';
+               }
+               if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
+                       $this->mRealName = $request->getText( 'wpRealName' );
+               } else {
+                       $this->mRealName = '';
+               }
+
+               if ( !$wgAuth->validDomain( $this->mDomain ) ) {
+                       $this->mDomain = $wgAuth->getDomain();
+               }
+               $wgAuth->setDomain( $this->mDomain );
+
+               # 1. When switching accounts, it sucks to get automatically logged out
+               # 2. Do not return to PasswordReset after a successful password change
+               #    but goto Wiki start page (Main_Page) instead ( bug 33997 )
+               $returnToTitle = Title::newFromText( $this->mReturnTo );
+               if ( is_object( $returnToTitle )
+                       && ( $returnToTitle->isSpecial( 'Userlogout' )
+                               || $returnToTitle->isSpecial( 'PasswordReset' ) )
+               ) {
+                       $this->mReturnTo = '';
+                       $this->mReturnToQuery = '';
+               }
+       }
+
+       function getDescription() {
+               if ( $this->mType === 'signup' ) {
+                       return $this->msg( 'createaccount' )->text();
+               } else {
+                       return $this->msg( 'login' )->text();
+               }
+       }
+
+       /**
+        * @param string|null $subPage
+        */
+       public function execute( $subPage ) {
+               // Make sure session is persisted
+               $session = SessionManager::getGlobalSession();
+               $session->persist();
+
+               $this->load();
+
+               // Check for [[Special:Userlogin/signup]]. This affects form display and
+               // page title.
+               if ( $subPage == 'signup' ) {
+                       $this->mType = 'signup';
+               }
+               $this->setHeaders();
+
+               // Make sure it's possible to log in
+               if ( $this->mType !== 'signup' && !$session->canSetUser() ) {
+                       throw new ErrorPageError(
+                               'cannotloginnow-title',
+                               'cannotloginnow-text',
+                               [
+                                       $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
+                * page. The use case scenario for this is when a user opens a large number
+                * of tabs, is redirected to the login page on all of them, and then logs
+                * in on one, expecting all the others to work properly.
+                *
+                * However, do show the form if it was visited intentionally (no 'returnto'
+                * is present). People who often switch between several accounts have grown
+                * accustomed to this behavior.
+                */
+               if (
+                       $this->mType !== 'signup' &&
+                       !$this->mPosted &&
+                       $this->getUser()->isLoggedIn() &&
+                       ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' )
+               ) {
+                       $this->successfulLogin();
+               }
+
+               // If logging in and not on HTTPS, either redirect to it or offer a link.
+               global $wgSecureLogin;
+               if ( $this->mRequest->getProtocol() !== 'https' ) {
+                       $title = $this->getFullTitle();
+                       $query = [
+                               'returnto' => $this->mReturnTo !== '' ? $this->mReturnTo : null,
+                               'returntoquery' => $this->mReturnToQuery !== '' ?
+                                       $this->mReturnToQuery : null,
+                               'title' => null,
+                               ( $this->mEntryErrorType === 'error' ? 'error' : 'warning' ) => $this->mEntryError,
+                       ] + $this->mRequest->getQueryValues();
+                       $url = $title->getFullURL( $query, false, PROTO_HTTPS );
+                       if ( $wgSecureLogin
+                               && wfCanIPUseHTTPS( $this->getRequest()->getIP() )
+                               && !$this->mFromHTTP ) // Avoid infinite redirect
+                       {
+                               $url = wfAppendQuery( $url, 'fromhttp=1' );
+                               $this->getOutput()->redirect( $url );
+                               // Since we only do this redir to change proto, always vary
+                               $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
+
+                               return;
+                       } else {
+                               // A wiki without HTTPS login support should set $wgServer to
+                               // http://somehost, in which case the secure URL generated
+                               // above won't actually start with https://
+                               if ( substr( $url, 0, 8 ) === 'https://' ) {
+                                       $this->mSecureLoginUrl = $url;
+                               }
+                       }
+               }
+
+               if ( !is_null( $this->mCookieCheck ) ) {
+                       $this->onCookieRedirectCheck( $this->mCookieCheck );
+
+                       return;
+               } elseif ( $this->mPosted ) {
+                       if ( $this->mCreateaccount ) {
+                               $this->addNewAccount();
+
+                               return;
+                       } elseif ( $this->mCreateaccountMail ) {
+                               $this->addNewAccountMailPassword();
+
+                               return;
+                       } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
+                               $this->processLogin();
+
+                               return;
+                       }
+               }
+               $this->mainLoginForm( $this->mEntryError, $this->mEntryErrorType );
+       }
+
+       /**
+        * @private
+        */
+       function addNewAccountMailPassword() {
+               if ( $this->mEmail == '' ) {
+                       $this->mainLoginForm( $this->msg( 'noemailcreate' )->escaped() );
+
+                       return;
+               }
+
+               $status = $this->addNewAccountInternal();
+               LoggerFactory::getInstance( 'authmanager' )->info(
+                       'Account creation attempt with mailed password',
+                       [ 'event' => 'accountcreation', 'status' => $status ]
+               );
+               if ( !$status->isGood() ) {
+                       $error = $status->getMessage();
+                       $this->mainLoginForm( $error->toString() );
+
+                       return;
+               }
+
+               /** @var User $u */
+               $u = $status->getValue();
+
+               // Wipe the initial password and mail a temporary one
+               $u->setPassword( null );
+               $u->saveSettings();
+               $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
+
+               Hooks::run( 'AddNewAccount', [ $u, true ] );
+               $u->addNewUserLogEntry( 'byemail', $this->mReason );
+
+               $out = $this->getOutput();
+               $out->setPageTitle( $this->msg( 'accmailtitle' ) );
+
+               if ( !$result->isGood() ) {
+                       $this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() );
+               } else {
+                       $out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
+                       $this->executeReturnTo( 'success' );
+               }
+       }
+
+       /**
+        * @private
+        * @return bool
+        */
+       function addNewAccount() {
+               global $wgContLang, $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector;
+
+               # Create the account and abort if there's a problem doing so
+               $status = $this->addNewAccountInternal();
+               LoggerFactory::getInstance( 'authmanager' )->info( 'Account creation attempt', [
+                       'event' => 'accountcreation',
+                       'status' => $status,
+               ] );
+
+               if ( !$status->isGood() ) {
+                       $error = $status->getMessage();
+                       $this->mainLoginForm( $error->toString() );
+
+                       return false;
+               }
+
+               $u = $status->getValue();
+
+               # Only save preferences if the user is not creating an account for someone else.
+               if ( $this->getUser()->isAnon() ) {
+                       # If we showed up language selection links, and one was in use, be
+                       # smart (and sensible) and save that language as the user's preference
+                       if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                               $u->setOption( 'language', $this->mLanguage );
+                       } else {
+
+                               # Otherwise the user's language preference defaults to $wgContLang,
+                               # but it may be better to set it to their preferred $wgContLang variant,
+                               # based on browser preferences or URL parameters.
+                               $u->setOption( 'language', $wgContLang->getPreferredVariant() );
+                       }
+                       if ( $wgContLang->hasVariants() ) {
+                               $u->setOption( 'variant', $wgContLang->getPreferredVariant() );
+                       }
+               }
+
+               $out = $this->getOutput();
+
+               # Send out an email authentication message if needed
+               if ( $wgEmailAuthentication && Sanitizer::validateEmail( $u->getEmail() ) ) {
+                       $status = $u->sendConfirmationMail();
+                       if ( $status->isGood() ) {
+                               $out->addWikiMsg( 'confirmemail_oncreate' );
+                       } else {
+                               $out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) );
+                       }
+               }
+
+               # Save settings (including confirmation token)
+               $u->saveSettings();
+
+               # If not logged in, assume the new account as the current one and set
+               # session cookies then show a "welcome" message or a "need cookies"
+               # message as needed
+               if ( $this->getUser()->isAnon() ) {
+                       $u->setCookies();
+                       $wgUser = $u;
+                       // This should set it for OutputPage and the Skin
+                       // which is needed or the personal links will be
+                       // wrong.
+                       $this->getContext()->setUser( $u );
+                       Hooks::run( 'AddNewAccount', [ $u, false ] );
+                       $u->addNewUserLogEntry( 'create' );
+                       if ( $this->hasSessionCookie() ) {
+                               $this->successfulCreation();
+                       } else {
+                               $this->cookieRedirectCheck( 'new' );
+                       }
+               } else {
+                       # Confirm that the account was created
+                       $out->setPageTitle( $this->msg( 'accountcreated' ) );
+                       $out->addWikiMsg( 'accountcreatedtext', $u->getName() );
+                       $out->addReturnTo( $this->getPageTitle() );
+                       Hooks::run( 'AddNewAccount', [ $u, false ] );
+                       $u->addNewUserLogEntry( 'create2', $this->mReason );
+               }
+
+               return true;
+       }
+
+       /**
+        * Make a new user account using the loaded data.
+        * @private
+        * @throws PermissionsError|ReadOnlyError
+        * @return Status
+        */
+       public function addNewAccountInternal() {
+               global $wgAuth, $wgAccountCreationThrottle, $wgEmailConfirmToEdit;
+
+               // If the user passes an invalid domain, something is fishy
+               if ( !$wgAuth->validDomain( $this->mDomain ) ) {
+                       return Status::newFatal( 'wrongpassword' );
+               }
+
+               // If we are not allowing users to login locally, we should be checking
+               // to see if the user is actually able to authenticate to the authenti-
+               // cation server before they create an account (otherwise, they can
+               // create a local account and login as any domain user). We only need
+               // to check this for domains that aren't local.
+               if ( 'local' != $this->mDomain && $this->mDomain != '' ) {
+                       if (
+                               !$wgAuth->canCreateAccounts() &&
+                               (
+                                       !$wgAuth->userExists( $this->mUsername ) ||
+                                       !$wgAuth->authenticate( $this->mUsername, $this->mPassword )
+                               )
+                       ) {
+                               return Status::newFatal( 'wrongpassword' );
+                       }
+               }
+
+               if ( wfReadOnly() ) {
+                       throw new ReadOnlyError;
+               }
+
+               # Request forgery checks.
+               $token = self::getCreateaccountToken();
+               if ( $token->wasNew() ) {
+                       return Status::newFatal( 'nocookiesfornew' );
+               }
+
+               # The user didn't pass a createaccount token
+               if ( !$this->mToken ) {
+                       return Status::newFatal( 'sessionfailure' );
+               }
+
+               # Validate the createaccount token
+               if ( !$token->match( $this->mToken ) ) {
+                       return Status::newFatal( 'sessionfailure' );
+               }
+
+               # Check permissions
+               $currentUser = $this->getUser();
+               $creationBlock = $currentUser->isBlockedFromCreateAccount();
+               if ( !$currentUser->isAllowed( 'createaccount' ) ) {
+                       throw new PermissionsError( 'createaccount' );
+               } elseif ( $creationBlock instanceof Block ) {
+                       // Throws an ErrorPageError.
+                       $this->userBlockedMessage( $creationBlock );
+
+                       // This should never be reached.
+                       return false;
+               }
+
+               # Include checks that will include GlobalBlocking (Bug 38333)
+               $permErrors = $this->getPageTitle()->getUserPermissionsErrors(
+                       'createaccount',
+                       $currentUser,
+                       true
+               );
+
+               if ( count( $permErrors ) ) {
+                       throw new PermissionsError( 'createaccount', $permErrors );
+               }
+
+               $ip = $this->getRequest()->getIP();
+               if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
+                       return Status::newFatal( 'sorbs_create_account_reason' );
+               }
+
+               # Now create a dummy user ($u) and check if it is valid
+               $u = User::newFromName( $this->mUsername, 'creatable' );
+               if ( !$u ) {
+                       return Status::newFatal( 'noname' );
+               }
+
+               $cache = ObjectCache::getLocalClusterInstance();
+               # Make sure the user does not exist already
+               $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $this->mUsername ) ) );
+               if ( !$lock ) {
+                       return Status::newFatal( 'usernameinprogress' );
+               } elseif ( $u->idForName( User::READ_LOCKING ) ) {
+                       return Status::newFatal( 'userexists' );
+               }
+
+               if ( $this->mCreateaccountMail ) {
+                       # do not force a password for account creation by email
+                       # set invalid password, it will be replaced later by a random generated password
+                       $this->mPassword = null;
+               } else {
+                       if ( $this->mPassword !== $this->mRetype ) {
+                               return Status::newFatal( 'badretype' );
+                       }
+
+                       # check for password validity, return a fatal Status if invalid
+                       $validity = $u->checkPasswordValidity( $this->mPassword, 'create' );
+                       if ( !$validity->isGood() ) {
+                               $validity->ok = false; // make sure this Status is fatal
+                               return $validity;
+                       }
+               }
+
+               # if you need a confirmed email address to edit, then obviously you
+               # need an email address.
+               if ( $wgEmailConfirmToEdit && strval( $this->mEmail ) === '' ) {
+                       return Status::newFatal( 'noemailtitle' );
+               }
+
+               if ( strval( $this->mEmail ) !== '' && !Sanitizer::validateEmail( $this->mEmail ) ) {
+                       return Status::newFatal( 'invalidemailaddress' );
+               }
+
+               # Set some additional data so the AbortNewAccount hook can be used for
+               # more than just username validation
+               $u->setEmail( $this->mEmail );
+               $u->setRealName( $this->mRealName );
+
+               $abortError = '';
+               $abortStatus = null;
+               if ( !Hooks::run( 'AbortNewAccount', [ $u, &$abortError, &$abortStatus ] ) ) {
+                       // Hook point to add extra creation throttles and blocks
+                       wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+                       if ( $abortStatus === null ) {
+                               // Report back the old string as a raw message status.
+                               // This will report the error back as 'createaccount-hook-aborted'
+                               // with the given string as the message.
+                               // To return a different error code, return a Status object.
+                               $abortError = new Message( 'createaccount-hook-aborted', [ $abortError ] );
+                               $abortError->text();
+
+                               return Status::newFatal( $abortError );
+                       } else {
+                               // For MediaWiki 1.23+ and updated hooks, return the Status object
+                               // returned from the hook.
+                               return $abortStatus;
+                       }
+               }
+
+               // Hook point to check for exempt from account creation throttle
+               if ( !Hooks::run( 'ExemptFromAccountCreationThrottle', [ $ip ] ) ) {
+                       wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook " .
+                               "allowed account creation w/o throttle\n" );
+               } else {
+                       if ( ( $wgAccountCreationThrottle && $currentUser->isPingLimitable() ) ) {
+                               $key = wfGlobalCacheKey( 'acctcreate', 'ip', $ip );
+                               $value = $cache->get( $key );
+                               if ( !$value ) {
+                                       $cache->set( $key, 0, $cache::TTL_DAY );
+                               }
+                               if ( $value >= $wgAccountCreationThrottle ) {
+                                       return Status::newFatal( 'acct_creation_throttle_hit', $wgAccountCreationThrottle );
+                               }
+                               $cache->incr( $key );
+                       }
+               }
+
+               if ( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+                       return Status::newFatal( 'externaldberror' );
+               }
+
+               self::clearCreateaccountToken();
+
+               return $this->initUser( $u, false );
+       }
+
+       /**
+        * Actually add a user to the database.
+        * Give it a User object that has been initialised with a name.
+        *
+        * @param User $u
+        * @param bool $autocreate True if this is an autocreation via auth plugin
+        * @return Status Status object, with the User object in the value member on success
+        * @private
+        */
+       function initUser( $u, $autocreate ) {
+               global $wgAuth;
+
+               $status = $u->addToDatabase();
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               if ( $wgAuth->allowPasswordChange() ) {
+                       $u->setPassword( $this->mPassword );
+               }
+
+               $u->setEmail( $this->mEmail );
+               $u->setRealName( $this->mRealName );
+               SessionManager::singleton()->invalidateSessionsForUser( $u );
+
+               Hooks::run( 'LocalUserCreated', [ $u, $autocreate ] );
+               $oldUser = $u;
+               $wgAuth->initUser( $u, $autocreate );
+               if ( $oldUser !== $u ) {
+                       wfWarn( get_class( $wgAuth ) . '::initUser() replaced the user object' );
+               }
+
+               $u->saveSettings();
+
+               // Update user count
+               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+
+               // Watch user's userpage and talk page
+               $u->addWatch( $u->getUserPage(), User::IGNORE_USER_RIGHTS );
+
+               return Status::newGood( $u );
+       }
+
+       /**
+        * Internally authenticate the login request.
+        *
+        * This may create a local account as a side effect if the
+        * authentication plugin allows transparent local account
+        * creation.
+        * @return int
+        */
+       public function authenticateUserData() {
+               global $wgUser, $wgAuth;
+
+               $this->load();
+
+               if ( $this->mUsername == '' ) {
+                       return self::NO_NAME;
+               }
+
+               // We require a login token to prevent login CSRF
+               // Handle part of this before incrementing the throttle so
+               // token-less login attempts don't count towards the throttle
+               // but wrong-token attempts do.
+
+               // If the user doesn't have a login token yet, set one.
+               $token = self::getLoginToken();
+               if ( $token->wasNew() ) {
+                       return self::NEED_TOKEN;
+               }
+               // If the user didn't pass a login token, tell them we need one
+               if ( !$this->mToken ) {
+                       return self::NEED_TOKEN;
+               }
+
+               $throttleCount = self::incrementLoginThrottle( $this->mUsername );
+               if ( $throttleCount ) {
+                       $this->mThrottleWait = $throttleCount['wait'];
+                       return self::THROTTLED;
+               }
+
+               // Validate the login token
+               if ( !$token->match( $this->mToken ) ) {
+                       return self::WRONG_TOKEN;
+               }
+
+               // Load the current user now, and check to see if we're logging in as
+               // the same name. This is necessary because loading the current user
+               // (say by calling getName()) calls the UserLoadFromSession hook, which
+               // potentially creates the user in the database. Until we load $wgUser,
+               // checking for user existence using User::newFromName($name)->getId() below
+               // will effectively be using stale data.
+               if ( $this->getUser()->getName() === $this->mUsername ) {
+                       wfDebug( __METHOD__ . ": already logged in as {$this->mUsername}\n" );
+
+                       return self::SUCCESS;
+               }
+
+               $u = User::newFromName( $this->mUsername );
+               if ( $u === false ) {
+                       return self::ILLEGAL;
+               }
+
+               $msg = null;
+               // Give extensions a way to indicate the username has been updated,
+               // rather than telling the user the account doesn't exist.
+               if ( !Hooks::run( 'LoginUserMigrated', [ $u, &$msg ] ) ) {
+                       $this->mAbortLoginErrorMsg = $msg;
+                       return self::USER_MIGRATED;
+               }
+
+               if ( !User::isUsableName( $u->getName() ) ) {
+                       return self::ILLEGAL;
+               }
+
+               $isAutoCreated = false;
+               if ( $u->getId() == 0 ) {
+                       $status = $this->attemptAutoCreate( $u );
+                       if ( $status !== self::SUCCESS ) {
+                               return $status;
+                       } else {
+                               $isAutoCreated = true;
+                       }
+               } else {
+                       $u->load();
+               }
+
+               // Give general extensions, such as a captcha, a chance to abort logins
+               $abort = self::ABORTED;
+               if ( !Hooks::run( 'AbortLogin', [ $u, $this->mPassword, &$abort, &$msg ] ) ) {
+                       if ( !in_array( $abort, array_keys( self::$statusCodes ), true ) ) {
+                               throw new Exception( 'Invalid status code returned from AbortLogin hook: ' . $abort );
+                       }
+                       $this->mAbortLoginErrorMsg = $msg;
+                       return $abort;
+               }
+
+               global $wgBlockDisablesLogin;
+               if ( !$u->checkPassword( $this->mPassword ) ) {
+                       if ( $u->checkTemporaryPassword( $this->mPassword ) ) {
+                               /**
+                                * The e-mailed temporary password should not be used for actu-
+                                * al logins; that's a very sloppy habit, and insecure if an
+                                * attacker has a few seconds to click "search" on someone's
+                                * open mail reader.
+                                *
+                                * Allow it to be used only to reset the password a single time
+                                * to a new value, which won't be in the user's e-mail ar-
+                                * chives.
+                                *
+                                * For backwards compatibility, we'll still recognize it at the
+                                * login form to minimize surprises for people who have been
+                                * logging in with a temporary password for some time.
+                                *
+                                * As a side-effect, we can authenticate the user's e-mail ad-
+                                * dress if it's not already done, since the temporary password
+                                * was sent via e-mail.
+                                */
+                               if ( !$u->isEmailConfirmed() && !wfReadOnly() ) {
+                                       $u->confirmEmail();
+                                       $u->saveSettings();
+                               }
+
+                               // At this point we just return an appropriate code/ indicating
+                               // that the UI should show a password reset form; bot inter-
+                               // faces etc will probably just fail cleanly here.
+                               $this->mAbortLoginErrorMsg = 'resetpass-temp-emailed';
+                               $this->mTempPasswordUsed = true;
+                               $retval = self::RESET_PASS;
+                       } else {
+                               $retval = ( $this->mPassword == '' ) ? self::EMPTY_PASS : self::WRONG_PASS;
+                       }
+               } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) {
+                       // If we've enabled it, make it so that a blocked user cannot login
+                       $retval = self::USER_BLOCKED;
+               } elseif ( $this->checkUserPasswordExpired( $u ) == 'hard' ) {
+                       // Force reset now, without logging in
+                       $retval = self::RESET_PASS;
+                       $this->mAbortLoginErrorMsg = 'resetpass-expired';
+               } else {
+                       Hooks::run( 'UserLoggedIn', [ $u ] );
+                       $oldUser = $u;
+                       $wgAuth->updateUser( $u );
+                       if ( $oldUser !== $u ) {
+                               wfWarn( get_class( $wgAuth ) . '::updateUser() replaced the user object' );
+                       }
+                       $wgUser = $u;
+                       // This should set it for OutputPage and the Skin
+                       // which is needed or the personal links will be
+                       // wrong.
+                       $this->getContext()->setUser( $u );
+
+                       // Please reset throttle for successful logins, thanks!
+                       self::clearLoginThrottle( $this->mUsername );
+
+                       if ( $isAutoCreated ) {
+                               // Must be run after $wgUser is set, for correct new user log
+                               Hooks::run( 'AuthPluginAutoCreate', [ $u ] );
+                       }
+
+                       $retval = self::SUCCESS;
+               }
+               Hooks::run( 'LoginAuthenticateAudit', [ $u, $this->mPassword, $retval ] );
+
+               return $retval;
+       }
+
+       /**
+        * Increment the login attempt throttle hit count for the (username,current IP)
+        * tuple unless the throttle was already reached.
+        *
+        * @since 1.27 Return value changed.
+        * @param string $username The user name
+        * @return bool|array false if below limit or an array if above limit
+        *   Array contains keys wait, count, and throttleIndex
+        */
+       public static function incrementLoginThrottle( $username ) {
+               global $wgPasswordAttemptThrottle, $wgRequest;
+               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
+
+               $throttleCount = 0;
+               if ( is_array( $wgPasswordAttemptThrottle ) ) {
+                       $throttleConfig = $wgPasswordAttemptThrottle;
+                       if ( isset( $wgPasswordAttemptThrottle['count'] ) ) {
+                               // old style. Convert for backwards compat.
+                               $throttleConfig = [ $wgPasswordAttemptThrottle ];
+                       }
+                       foreach ( $throttleConfig as $index => $specificThrottle ) {
+                               if ( isset( $specificThrottle['allIPs'] ) ) {
+                                       $ip = 'All';
+                               } else {
+                                       $ip = $wgRequest->getIP();
+                               }
+                               $throttleKey = wfGlobalCacheKey( 'password-throttle',
+                                       $index, $ip, md5( $username )
+                               );
+                               $count = $specificThrottle['count'];
+                               $period = $specificThrottle['seconds'];
+
+                               $cache = ObjectCache::getLocalClusterInstance();
+                               $throttleCount = $cache->get( $throttleKey );
+                               if ( !$throttleCount ) {
+                                       $cache->add( $throttleKey, 1, $period ); // start counter
+                               } elseif ( $throttleCount < $count ) {
+                                       $cache->incr( $throttleKey );
+                               } elseif ( $throttleCount >= $count ) {
+                                       $logMsg = 'Login attempt rejected because logins to '
+                                               . '{acct} from IP {ip} have been throttled for '
+                                               . '{period} seconds due to {count} failed attempts';
+                                       // If we are hitting a throttle for >= 50 attempts,
+                                       // it is much more likely to be an attack than someone
+                                       // simply forgetting their password, so log it at a
+                                       // higher level.
+                                       $level = $count >= 50 ? LogLevel::WARNING : LogLevel::INFO;
+                                       // It should be noted that once the throttle is hit,
+                                       // every attempt to login will generate the log message
+                                       // until the throttle expires, not just the attempt that
+                                       // puts the throttle over the top.
+                                       LoggerFactory::getInstance( 'password-throttle' )->log(
+                                               $level,
+                                               $logMsg,
+                                               [
+                                                       'ip' => $ip,
+                                                       'period' => $period,
+                                                       'acct' => $username,
+                                                       'count' => $count,
+                                                       'throttleIdentifier' => $index,
+                                                       'method' => __METHOD__
+                                               ]
+                                       );
+
+                                       return [
+                                               'throttleIndex' => $index,
+                                               'wait' => $period,
+                                               'count' => $count
+                                       ];
+                               }
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Increment the login attempt throttle hit count for the (username,current IP)
+        * tuple unless the throttle was already reached.
+        *
+        * @deprecated Use LoginForm::incrementLoginThrottle instead
+        * @param string $username The user name
+        * @return bool|int true if above throttle, or 0 (prior to 1.27, returned current count)
+        */
+       public static function incLoginThrottle( $username ) {
+               wfDeprecated( __METHOD__, "1.27" );
+               $res = self::incrementLoginThrottle( $username );
+               return is_array( $res ) ? true : 0;
+       }
+
+       /**
+        * Clear the login attempt throttle hit count for the (username,current IP) tuple.
+        * @param string $username The user name
+        * @return void
+        */
+       public static function clearLoginThrottle( $username ) {
+               global $wgRequest, $wgPasswordAttemptThrottle;
+               $username = User::getCanonicalName( $username, 'usable' ) ?: $username;
+
+               if ( is_array( $wgPasswordAttemptThrottle ) ) {
+                       $throttleConfig = $wgPasswordAttemptThrottle;
+                       if ( isset( $wgPasswordAttemptThrottle['count'] ) ) {
+                               // old style. Convert for backwards compat.
+                               $throttleConfig = [ $wgPasswordAttemptThrottle ];
+                       }
+                       foreach ( $throttleConfig as $index => $specificThrottle ) {
+                               if ( isset( $specificThrottle['allIPs'] ) ) {
+                                       $ip = 'All';
+                               } else {
+                                       $ip = $wgRequest->getIP();
+                               }
+                               $throttleKey = wfGlobalCacheKey( 'password-throttle', $index,
+                                       $ip, md5( $username )
+                               );
+                               ObjectCache::getLocalClusterInstance()->delete( $throttleKey );
+                       }
+               }
+       }
+
+       /**
+        * Attempt to automatically create a user on login. Only succeeds if there
+        * is an external authentication method which allows it.
+        *
+        * @param User $user
+        *
+        * @return int Status code
+        */
+       function attemptAutoCreate( $user ) {
+               global $wgAuth;
+
+               if ( $this->getUser()->isBlockedFromCreateAccount() ) {
+                       wfDebug( __METHOD__ . ": user is blocked from account creation\n" );
+
+                       return self::CREATE_BLOCKED;
+               }
+
+               if ( !$wgAuth->autoCreate() ) {
+                       return self::NOT_EXISTS;
+               }
+
+               if ( !$wgAuth->userExists( $user->getName() ) ) {
+                       wfDebug( __METHOD__ . ": user does not exist\n" );
+
+                       return self::NOT_EXISTS;
+               }
+
+               if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
+                       wfDebug( __METHOD__ . ": \$wgAuth->authenticate() returned false, aborting\n" );
+
+                       return self::WRONG_PLUGIN_PASS;
+               }
+
+               $abortError = '';
+               if ( !Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ] ) ) {
+                       // Hook point to add extra creation throttles and blocks
+                       wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" );
+                       $this->mAbortLoginErrorMsg = $abortError;
+
+                       return self::ABORTED;
+               }
+
+               wfDebug( __METHOD__ . ": creating account\n" );
+               $status = $this->initUser( $user, true );
+
+               if ( !$status->isOK() ) {
+                       $errors = $status->getErrorsByType( 'error' );
+                       $this->mAbortLoginErrorMsg = $errors[0]['message'];
+
+                       return self::ABORTED;
+               }
+
+               return self::SUCCESS;
+       }
+
+       function processLogin() {
+               global $wgLang, $wgSecureLogin, $wgInvalidPasswordReset;
+
+               $authRes = $this->authenticateUserData();
+               switch ( $authRes ) {
+                       case self::SUCCESS:
+                               # We've verified now, update the real record
+                               $user = $this->getUser();
+                               $user->touch();
+
+                               if ( $user->requiresHTTPS() ) {
+                                       $this->mStickHTTPS = true;
+                               }
+
+                               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+                                       $user->setCookies( $this->mRequest, false, $this->mRemember );
+                               } else {
+                                       $user->setCookies( $this->mRequest, null, $this->mRemember );
+                               }
+                               self::clearLoginToken();
+
+                               // Reset the throttle
+                               self::clearLoginThrottle( $this->mUsername );
+
+                               $request = $this->getRequest();
+                               if ( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
+                                       /* Replace the language object to provide user interface in
+                                        * correct language immediately on this first page load.
+                                        */
+                                       $code = $request->getVal( 'uselang', $user->getOption( 'language' ) );
+                                       $userLang = Language::factory( $code );
+                                       $wgLang = $userLang;
+                                       RequestContext::getMain()->setLanguage( $userLang );
+                                       $this->getContext()->setLanguage( $userLang );
+                                       // Reset SessionID on Successful login (bug 40995)
+                                       $this->renewSessionId();
+                                       if ( $this->checkUserPasswordExpired( $this->getUser() ) == 'soft' ) {
+                                               $this->resetLoginForm( $this->msg( 'resetpass-expired-soft' ) );
+                                       } elseif ( $wgInvalidPasswordReset
+                                               && !$user->isValidPassword( $this->mPassword )
+                                       ) {
+                                               $status = $user->checkPasswordValidity(
+                                                       $this->mPassword,
+                                                       'login'
+                                               );
+                                               $this->resetLoginForm(
+                                                       $status->getMessage( 'resetpass-validity-soft' )
+                                               );
+                                       } else {
+                                               $this->successfulLogin();
+                                       }
+                               } else {
+                                       $this->cookieRedirectCheck( 'login' );
+                               }
+                               break;
+
+                       case self::NEED_TOKEN:
+                               $error = $this->mAbortLoginErrorMsg ?: 'nocookiesforlogin';
+                               $this->mainLoginForm( $this->msg( $error )->parse() );
+                               break;
+                       case self::WRONG_TOKEN:
+                               $error = $this->mAbortLoginErrorMsg ?: 'sessionfailure';
+                               $this->mainLoginForm( $this->msg( $error )->text() );
+                               break;
+                       case self::NO_NAME:
+                       case self::ILLEGAL:
+                               $error = $this->mAbortLoginErrorMsg ?: 'noname';
+                               $this->mainLoginForm( $this->msg( $error )->text() );
+                               break;
+                       case self::WRONG_PLUGIN_PASS:
+                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
+                               $this->mainLoginForm( $this->msg( $error )->text() );
+                               break;
+                       case self::NOT_EXISTS:
+                               if ( $this->getUser()->isAllowed( 'createaccount' ) ) {
+                                       $error = $this->mAbortLoginErrorMsg ?: 'nosuchuser';
+                                       $this->mainLoginForm( $this->msg( $error,
+                                               wfEscapeWikiText( $this->mUsername ) )->parse() );
+                               } else {
+                                       $error = $this->mAbortLoginErrorMsg ?: 'nosuchusershort';
+                                       $this->mainLoginForm( $this->msg( $error,
+                                               wfEscapeWikiText( $this->mUsername ) )->text() );
+                               }
+                               break;
+                       case self::WRONG_PASS:
+                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword';
+                               $this->mainLoginForm( $this->msg( $error )->text() );
+                               break;
+                       case self::EMPTY_PASS:
+                               $error = $this->mAbortLoginErrorMsg ?: 'wrongpasswordempty';
+                               $this->mainLoginForm( $this->msg( $error )->text() );
+                               break;
+                       case self::RESET_PASS:
+                               $error = $this->mAbortLoginErrorMsg ?: 'resetpass_announce';
+                               $this->resetLoginForm( $this->msg( $error ) );
+                               break;
+                       case self::CREATE_BLOCKED:
+                               $this->userBlockedMessage( $this->getUser()->isBlockedFromCreateAccount() );
+                               break;
+                       case self::THROTTLED:
+                               $error = $this->mAbortLoginErrorMsg ?: 'login-throttled';
+                               $this->mainLoginForm( $this->msg( $error )
+                                       ->durationParams( $this->mThrottleWait )->text()
+                               );
+                               break;
+                       case self::USER_BLOCKED:
+                               $error = $this->mAbortLoginErrorMsg ?: 'login-userblocked';
+                               $this->mainLoginForm( $this->msg( $error, $this->mUsername )->escaped() );
+                               break;
+                       case self::ABORTED:
+                               $error = $this->mAbortLoginErrorMsg ?: 'login-abort-generic';
+                               $this->mainLoginForm( $this->msg( $error,
+                                               wfEscapeWikiText( $this->mUsername ) )->text() );
+                               break;
+                       case self::USER_MIGRATED:
+                               $error = $this->mAbortLoginErrorMsg ?: 'login-migrated-generic';
+                               $params = [];
+                               if ( is_array( $error ) ) {
+                                       $error = array_shift( $this->mAbortLoginErrorMsg );
+                                       $params = $this->mAbortLoginErrorMsg;
+                               }
+                               $this->mainLoginForm( $this->msg( $error, $params )->text() );
+                               break;
+                       default:
+                               throw new MWException( 'Unhandled case value' );
+               }
+
+               LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', [
+                       'event' => 'login',
+                       'successful' => $authRes === self::SUCCESS,
+                       'status' => LoginForm::$statusCodes[$authRes],
+               ] );
+       }
+
+       /**
+        * Show the Special:ChangePassword form, with custom message
+        * @param Message $msg
+        */
+       protected function resetLoginForm( Message $msg ) {
+               // Allow hooks to explain this password reset in more detail
+               Hooks::run( 'LoginPasswordResetMessage', [ &$msg, $this->mUsername ] );
+               $reset = new SpecialChangePasswordPreAuthManager();
+               $derivative = new DerivativeContext( $this->getContext() );
+               $derivative->setTitle( $reset->getPageTitle() );
+               $reset->setContext( $derivative );
+               if ( !$this->mTempPasswordUsed ) {
+                       $reset->setOldPasswordMessage( 'oldpassword' );
+               }
+               $reset->setChangeMessage( $msg );
+               $reset->execute( null );
+       }
+
+       /**
+        * @param User $u
+        * @param bool $throttle
+        * @param string $emailTitle Message name of email title
+        * @param string $emailText Message name of email text
+        * @return Status
+        */
+       function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle',
+               $emailText = 'passwordremindertext'
+       ) {
+               global $wgNewPasswordExpiry, $wgMinimalPasswordLength;
+
+               if ( $u->getEmail() == '' ) {
+                       return Status::newFatal( 'noemail', $u->getName() );
+               }
+               $ip = $this->getRequest()->getIP();
+               if ( !$ip ) {
+                       return Status::newFatal( 'badipaddress' );
+               }
+
+               $currentUser = $this->getUser();
+               Hooks::run( 'User::mailPasswordInternal', [ &$currentUser, &$ip, &$u ] );
+
+               $np = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
+               $u->setNewpassword( $np, $throttle );
+               $u->saveSettings();
+               $userLanguage = $u->getOption( 'language' );
+
+               $mainPage = Title::newMainPage();
+               $mainPageUrl = $mainPage->getCanonicalURL();
+
+               $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $mainPageUrl . '>',
+                       round( $wgNewPasswordExpiry / 86400 ) )->inLanguage( $userLanguage )->text();
+               $result = $u->sendMail( $this->msg( $emailTitle )->inLanguage( $userLanguage )->text(), $m );
+
+               return $result;
+       }
+
+       /**
+        * Run any hooks registered for logins, then HTTP redirect to
+        * $this->mReturnTo (or Main Page if that's undefined).  Formerly we had a
+        * nice message here, but that's really not as useful as just being sent to
+        * wherever you logged in from.  It should be clear that the action was
+        * successful, given the lack of error messages plus the appearance of your
+        * name in the upper right.
+        *
+        * @private
+        */
+       function successfulLogin() {
+               # Run any hooks; display injected HTML if any, else redirect
+               $currentUser = $this->getUser();
+               $injected_html = '';
+               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
+
+               if ( $injected_html !== '' ) {
+                       $this->displaySuccessfulAction( 'success', $this->msg( 'loginsuccesstitle' ),
+                               'loginsuccess', $injected_html );
+               } else {
+                       $this->executeReturnTo( 'successredirect' );
+               }
+       }
+
+       /**
+        * Run any hooks registered for logins, then display a message welcoming
+        * the user.
+        *
+        * @private
+        */
+       function successfulCreation() {
+               # Run any hooks; display injected HTML
+               $currentUser = $this->getUser();
+               $injected_html = '';
+               $welcome_creation_msg = 'welcomecreation-msg';
+
+               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
+
+               /**
+                * Let any extensions change what message is shown.
+                * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeWelcomeCreation
+                * @since 1.18
+                */
+               Hooks::run( 'BeforeWelcomeCreation', [ &$welcome_creation_msg, &$injected_html ] );
+
+               $this->displaySuccessfulAction(
+                       'signup',
+                       $this->msg( 'welcomeuser', $this->getUser()->getName() ),
+                       $welcome_creation_msg, $injected_html
+               );
+       }
+
+       /**
+        * Display a "successful action" page.
+        *
+        * @param string $type Condition of return to; see `executeReturnTo`
+        * @param string|Message $title Page's title
+        * @param string $msgname
+        * @param string $injected_html
+        */
+       private function displaySuccessfulAction( $type, $title, $msgname, $injected_html ) {
+               $out = $this->getOutput();
+               $out->setPageTitle( $title );
+               if ( $msgname ) {
+                       $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
+               }
+
+               $out->addHTML( $injected_html );
+
+               $this->executeReturnTo( $type );
+       }
+
+       /**
+        * Output a message that informs the user that they cannot create an account because
+        * there is a block on them or their IP which prevents account creation.  Note that
+        * User::isBlockedFromCreateAccount(), which gets this block, ignores the 'hardblock'
+        * setting on blocks (bug 13611).
+        * @param Block $block The block causing this error
+        * @throws ErrorPageError
+        */
+       function userBlockedMessage( Block $block ) {
+               # Let's be nice about this, it's likely that this feature will be used
+               # for blocking large numbers of innocent people, e.g. range blocks on
+               # schools. Don't blame it on the user. There's a small chance that it
+               # really is the user's fault, i.e. the username is blocked and they
+               # haven't bothered to log out before trying to create an account to
+               # evade it, but we'll leave that to their guilty conscience to figure
+               # out.
+               $errorParams = [
+                       $block->getTarget(),
+                       $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(),
+                       $block->getByName()
+               ];
+
+               if ( $block->getType() === Block::TYPE_RANGE ) {
+                       $errorMessage = 'cantcreateaccount-range-text';
+                       $errorParams[] = $this->getRequest()->getIP();
+               } else {
+                       $errorMessage = 'cantcreateaccount-text';
+               }
+
+               throw new ErrorPageError(
+                       'cantcreateaccounttitle',
+                       $errorMessage,
+                       $errorParams
+               );
+       }
+
+       /**
+        * Add a "return to" link or redirect to it.
+        * Extensions can use this to reuse the "return to" logic after
+        * inject steps (such as redirection) into the login process.
+        *
+        * @param string $type One of the following:
+        *    - error: display a return to link ignoring $wgRedirectOnLogin
+        *    - signup: display a return to link using $wgRedirectOnLogin if needed
+        *    - success: display a return to link using $wgRedirectOnLogin if needed
+        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
+        * @param string $returnTo
+        * @param array|string $returnToQuery
+        * @param bool $stickHTTPs Keep redirect link on HTTPs
+        * @since 1.22
+        */
+       public function showReturnToPage(
+               $type, $returnTo = '', $returnToQuery = '', $stickHTTPs = false
+       ) {
+               $this->mReturnTo = $returnTo;
+               $this->mReturnToQuery = $returnToQuery;
+               $this->mStickHTTPS = $stickHTTPs;
+               $this->executeReturnTo( $type );
+       }
+
+       /**
+        * Add a "return to" link or redirect to it.
+        *
+        * @param string $type One of the following:
+        *    - error: display a return to link ignoring $wgRedirectOnLogin
+        *    - signup: display a return to link using $wgRedirectOnLogin if needed
+        *    - success: display a return to link using $wgRedirectOnLogin if needed
+        *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
+        */
+       private function executeReturnTo( $type ) {
+               global $wgRedirectOnLogin, $wgSecureLogin;
+
+               if ( $type != 'error' && $wgRedirectOnLogin !== null ) {
+                       $returnTo = $wgRedirectOnLogin;
+                       $returnToQuery = [];
+               } else {
+                       $returnTo = $this->mReturnTo;
+                       $returnToQuery = wfCgiToArray( $this->mReturnToQuery );
+               }
+
+               // Allow modification of redirect behavior
+               Hooks::run( 'PostLoginRedirect', [ &$returnTo, &$returnToQuery, &$type ] );
+
+               $returnToTitle = Title::newFromText( $returnTo );
+               if ( !$returnToTitle ) {
+                       $returnToTitle = Title::newMainPage();
+               }
+
+               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+                       $options = [ 'http' ];
+                       $proto = PROTO_HTTP;
+               } elseif ( $wgSecureLogin ) {
+                       $options = [ 'https' ];
+                       $proto = PROTO_HTTPS;
+               } else {
+                       $options = [];
+                       $proto = PROTO_RELATIVE;
+               }
+
+               if ( $type == 'successredirect' ) {
+                       $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
+                       $this->getOutput()->redirect( $redirectUrl );
+               } else {
+                       $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
+               }
+       }
+
+       /**
+        * @param string $msg
+        * @param string $msgtype
+        * @throws ErrorPageError
+        * @throws Exception
+        * @throws FatalError
+        * @throws MWException
+        * @throws PermissionsError
+        * @throws ReadOnlyError
+        * @private
+        */
+       function mainLoginForm( $msg, $msgtype = 'error' ) {
+               global $wgEnableEmail, $wgEnableUserEmail;
+               global $wgHiddenPrefs, $wgLoginLanguageSelector;
+               global $wgAuth, $wgEmailConfirmToEdit;
+               global $wgSecureLogin, $wgPasswordResetRoutes;
+               global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
+
+               $titleObj = $this->getPageTitle();
+               $user = $this->getUser();
+               $out = $this->getOutput();
+
+               if ( $this->mType == 'signup' ) {
+                       // Block signup here if in readonly. Keeps user from
+                       // going through the process (filling out data, etc)
+                       // and being informed later.
+                       $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $user, true );
+                       if ( count( $permErrors ) ) {
+                               throw new PermissionsError( 'createaccount', $permErrors );
+                       } elseif ( $user->isBlockedFromCreateAccount() ) {
+                               $this->userBlockedMessage( $user->isBlockedFromCreateAccount() );
+
+                               return;
+                       } elseif ( wfReadOnly() ) {
+                               throw new ReadOnlyError;
+                       }
+               }
+
+               // Pre-fill username (if not creating an account, bug 44775).
+               if ( $this->mUsername == '' && $this->mType != 'signup' ) {
+                       if ( $user->isLoggedIn() ) {
+                               $this->mUsername = $user->getName();
+                       } else {
+                               $this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
+                       }
+               }
+
+               // Generic styles and scripts for both login and signup form
+               $out->addModuleStyles( [
+                       'mediawiki.ui',
+                       'mediawiki.ui.button',
+                       'mediawiki.ui.checkbox',
+                       'mediawiki.ui.input',
+                       'mediawiki.special.userlogin.common.styles'
+               ] );
+
+               if ( $this->mType == 'signup' ) {
+                       // Additional styles and scripts for signup form
+                       $out->addModules( [
+                               'mediawiki.special.userlogin.signup.js'
+                       ] );
+                       $out->addModuleStyles( [
+                               'mediawiki.special.userlogin.signup.styles'
+                       ] );
+
+                       $template = new UsercreateTemplate( $this->getConfig() );
+
+                       // Must match number of benefits defined in messages
+                       $template->set( 'benefitCount', 3 );
+
+                       $q = 'action=submitlogin&type=signup';
+                       $linkq = 'type=login';
+               } else {
+                       // Additional styles for login form
+                       $out->addModuleStyles( [
+                               'mediawiki.special.userlogin.login.styles'
+                       ] );
+
+                       $template = new UserloginTemplate( $this->getConfig() );
+
+                       $q = 'action=submitlogin&type=login';
+                       $linkq = 'type=signup';
+               }
+
+               if ( $this->mReturnTo !== '' ) {
+                       $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+                       if ( $this->mReturnToQuery !== '' ) {
+                               $returnto .= '&returntoquery=' .
+                                       wfUrlencode( $this->mReturnToQuery );
+                       }
+                       $q .= $returnto;
+                       $linkq .= $returnto;
+               }
+
+               # Don't show a "create account" link if the user can't.
+               if ( $this->showCreateOrLoginLink( $user ) ) {
+                       # Pass any language selection on to the mode switch link
+                       if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                               $linkq .= '&uselang=' . $this->mLanguage;
+                       }
+                       // Supply URL, login template creates the button.
+                       $template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
+               } else {
+                       $template->set( 'link', '' );
+               }
+
+               $resetLink = $this->mType == 'signup'
+                       ? null
+                       : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) );
+
+               $template->set( 'header', '' );
+               $template->set( 'formheader', '' );
+               $template->set( 'skin', $this->getSkin() );
+               $template->set( 'name', $this->mUsername );
+               $template->set( 'password', $this->mPassword );
+               $template->set( 'retype', $this->mRetype );
+               $template->set( 'createemailset', $this->mCreateaccountMail );
+               $template->set( 'email', $this->mEmail );
+               $template->set( 'realname', $this->mRealName );
+               $template->set( 'domain', $this->mDomain );
+               $template->set( 'reason', $this->mReason );
+
+               $template->set( 'action', $titleObj->getLocalURL( $q ) );
+               $template->set( 'message', $msg );
+               $template->set( 'messagetype', $msgtype );
+               $template->set( 'createemail', $wgEnableEmail && $user->isLoggedIn() );
+               $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs ) );
+               $template->set( 'useemail', $wgEnableEmail );
+               $template->set( 'emailrequired', $wgEmailConfirmToEdit );
+               $template->set( 'emailothers', $wgEnableUserEmail );
+               $template->set( 'canreset', $wgAuth->allowPasswordChange() );
+               $template->set( 'resetlink', $resetLink );
+               $template->set( 'canremember', $wgExtendedLoginCookieExpiration === null ?
+                       ( $wgCookieExpiration > 0 ) :
+                       ( $wgExtendedLoginCookieExpiration > 0 ) );
+               $template->set( 'usereason', $user->isLoggedIn() );
+               $template->set( 'remember', $this->mRemember );
+               $template->set( 'cansecurelogin', ( $wgSecureLogin === true ) );
+               $template->set( 'stickhttps', (int)$this->mStickHTTPS );
+               $template->set( 'loggedin', $user->isLoggedIn() );
+               $template->set( 'loggedinuser', $user->getName() );
+
+               if ( $this->mType == 'signup' ) {
+                       $template->set( 'token', self::getCreateaccountToken()->toString() );
+               } else {
+                       $template->set( 'token', self::getLoginToken()->toString() );
+               }
+
+               # Prepare language selection links as needed
+               if ( $wgLoginLanguageSelector ) {
+                       $template->set( 'languages', $this->makeLanguageSelector() );
+                       if ( $this->mLanguage ) {
+                               $template->set( 'uselang', $this->mLanguage );
+                       }
+               }
+
+               $template->set( 'secureLoginUrl', $this->mSecureLoginUrl );
+               // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
+               $usingHTTPS = $this->mRequest->getProtocol() == 'https';
+               $signupendHTTPS = $this->msg( 'signupend-https' );
+               if ( $usingHTTPS && !$signupendHTTPS->isBlank() ) {
+                       $template->set( 'signupend', $signupendHTTPS->parse() );
+               } else {
+                       $template->set( 'signupend', $this->msg( 'signupend' )->parse() );
+               }
+
+               // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
+               if ( $usingHTTPS ) {
+                       $template->set( 'fromhttp', $this->mFromHTTP );
+               }
+
+               // Give authentication and captcha plugins a chance to modify the form
+               $wgAuth->modifyUITemplate( $template, $this->mType );
+               if ( $this->mType == 'signup' ) {
+                       Hooks::run( 'UserCreateForm', [ &$template ] );
+               } else {
+                       Hooks::run( 'UserLoginForm', [ &$template ] );
+               }
+
+               $out->disallowUserJs(); // just in case...
+               $out->addTemplate( $template );
+       }
+
+       /**
+        * Whether the login/create account form should display a link to the
+        * other form (in addition to whatever the skin provides).
+        *
+        * @param User $user
+        * @return bool
+        */
+       private function showCreateOrLoginLink( &$user ) {
+               if ( $this->mType == 'signup' ) {
+                       return true;
+               } elseif ( $user->isAllowed( 'createaccount' ) ) {
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Check if a session cookie is present.
+        *
+        * This will not pick up a cookie set during _this_ request, but is meant
+        * to ensure that the client is returning the cookie which was set on a
+        * previous pass through the system.
+        *
+        * @private
+        * @return bool
+        */
+       function hasSessionCookie() {
+               global $wgDisableCookieCheck, $wgInitialSessionId;
+
+               return $wgDisableCookieCheck || (
+                       $wgInitialSessionId &&
+                       $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
+               );
+       }
+
+       /**
+        * Get the login token from the current session
+        * @since 1.27 returns a MediaWiki\Session\Token instead of a string
+        * @return MediaWiki\Session\Token
+        */
+       public static function getLoginToken() {
+               global $wgRequest;
+               return $wgRequest->getSession()->getToken( '', 'login' );
+       }
+
+       /**
+        * Formerly randomly generated a login token that would be returned by
+        * $this->getLoginToken().
+        *
+        * Since 1.27, this is a no-op. The token is generated as necessary by
+        * $this->getLoginToken().
+        *
+        * @deprecated since 1.27
+        */
+       public static function setLoginToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+       }
+
+       /**
+        * Remove any login token attached to the current session
+        */
+       public static function clearLoginToken() {
+               global $wgRequest;
+               $wgRequest->getSession()->resetToken( 'login' );
+       }
+
+       /**
+        * Get the createaccount token from the current session
+        * @since 1.27 returns a MediaWiki\Session\Token instead of a string
+        * @return MediaWiki\Session\Token
+        */
+       public static function getCreateaccountToken() {
+               global $wgRequest;
+               return $wgRequest->getSession()->getToken( '', 'createaccount' );
+       }
+
+       /**
+        * Formerly randomly generated a createaccount token that would be returned
+        * by $this->getCreateaccountToken().
+        *
+        * Since 1.27, this is a no-op. The token is generated as necessary by
+        * $this->getCreateaccountToken().
+        *
+        * @deprecated since 1.27
+        */
+       public static function setCreateaccountToken() {
+               wfDeprecated( __METHOD__, '1.27' );
+       }
+
+       /**
+        * Remove any createaccount token attached to the current session
+        */
+       public static function clearCreateaccountToken() {
+               global $wgRequest;
+               $wgRequest->getSession()->resetToken( 'createaccount' );
+       }
+
+       /**
+        * Renew the user's session id, using strong entropy
+        */
+       private function renewSessionId() {
+               global $wgSecureLogin, $wgCookieSecure;
+               if ( $wgSecureLogin && !$this->mStickHTTPS ) {
+                       $wgCookieSecure = false;
+               }
+
+               SessionManager::getGlobalSession()->resetId();
+       }
+
+       /**
+        * @param string $type
+        * @private
+        */
+       function cookieRedirectCheck( $type ) {
+               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+               $query = [ 'wpCookieCheck' => $type ];
+               if ( $this->mReturnTo !== '' ) {
+                       $query['returnto'] = $this->mReturnTo;
+                       $query['returntoquery'] = $this->mReturnToQuery;
+               }
+               $check = $titleObj->getFullURL( $query );
+
+               $this->getOutput()->redirect( $check );
+       }
+
+       /**
+        * @param string $type
+        * @private
+        */
+       function onCookieRedirectCheck( $type ) {
+               if ( !$this->hasSessionCookie() ) {
+                       if ( $type == 'new' ) {
+                               $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() );
+                       } elseif ( $type == 'login' ) {
+                               $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() );
+                       } else {
+                               # shouldn't happen
+                               $this->mainLoginForm( $this->msg( 'error' )->text() );
+                       }
+               } else {
+                       $this->successfulLogin();
+               }
+       }
+
+       /**
+        * Produce a bar of links which allow the user to select another language
+        * during login/registration but retain "returnto"
+        *
+        * @return string
+        */
+       function makeLanguageSelector() {
+               $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
+               if ( $msg->isBlank() ) {
+                       return '';
+               }
+               $langs = explode( "\n", $msg->text() );
+               $links = [];
+               foreach ( $langs as $lang ) {
+                       $lang = trim( $lang, '* ' );
+                       $parts = explode( '|', $lang );
+                       if ( count( $parts ) >= 2 ) {
+                               $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
+                       }
+               }
+
+               return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
+                       $this->getLanguage()->pipeList( $links ) )->escaped() : '';
+       }
+
+       /**
+        * Create a language selector link for a particular language
+        * Links back to this page preserving type and returnto
+        *
+        * @param string $text Link text
+        * @param string $lang Language code
+        * @return string
+        */
+       function makeLanguageSelectorLink( $text, $lang ) {
+               if ( $this->getLanguage()->getCode() == $lang ) {
+                       // no link for currently used language
+                       return htmlspecialchars( $text );
+               }
+               $query = [ 'uselang' => $lang ];
+               if ( $this->mType == 'signup' ) {
+                       $query['type'] = 'signup';
+               }
+               if ( $this->mReturnTo !== '' ) {
+                       $query['returnto'] = $this->mReturnTo;
+                       $query['returntoquery'] = $this->mReturnToQuery;
+               }
+
+               $attr = [];
+               $targetLanguage = Language::factory( $lang );
+               $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
+
+               return Linker::linkKnown(
+                       $this->getPageTitle(),
+                       htmlspecialchars( $text ),
+                       $attr,
+                       $query
+               );
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+
+       /**
+        * Private function to check password expiration, until AuthManager comes
+        * along to handle that.
+        * @param User $user
+        * @return string|bool
+        */
+       private function checkUserPasswordExpired( User $user ) {
+               global $wgPasswordExpireGrace;
+               $dbr = wfGetDB( DB_SLAVE );
+               $ts = $dbr->selectField( 'user', 'user_password_expires', [ 'user_id' => $user->getId() ] );
+
+               $expired = false;
+               $now = wfTimestamp();
+               $expUnix = wfTimestamp( TS_UNIX, $ts );
+               if ( $ts !== null && $expUnix < $now ) {
+                       $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
+               }
+               return $expired;
+       }
+
+       protected function getSubpagesForPrefixSearch() {
+               return [ 'signup' ];
+       }
+}
diff --git a/includes/specials/pre-authmanager/SpecialUserlogout.php b/includes/specials/pre-authmanager/SpecialUserlogout.php
new file mode 100644 (file)
index 0000000..6d6a714
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Implements Special:Userlogout
+ *
+ * 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
+ */
+
+/**
+ * Implements Special:Userlogout
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialUserlogoutPreAuthManager extends UnlistedSpecialPage {
+       function __construct() {
+               parent::__construct( 'Userlogout' );
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       function execute( $par ) {
+               /**
+                * Some satellite ISPs use broken precaching schemes that log people out straight after
+                * they're logged in (bug 17790). Luckily, there's a way to detect such requests.
+                */
+               if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
+                       wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
+                       throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
+               }
+
+               $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',
+                               [
+                                       $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
+                               ]
+                       );
+               }
+
+               $user = $this->getUser();
+               $oldName = $user->getName();
+               $user->logout();
+
+               $loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
+                       $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
+
+               $out = $this->getOutput();
+               $out->addWikiMsg( 'logouttext', $loginURL );
+
+               // Hook.
+               $injected_html = '';
+               Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
+               $out->addHTML( $injected_html );
+
+               $out->returnToMain();
+       }
+
+       protected function getGroupName() {
+               return 'login';
+       }
+}
index 3824be1..0a5aa61 100644 (file)
@@ -20,6 +20,8 @@
  *
  * @file
  * @ingroup Templates
+ * @deprecated Will be removed when AuthManager lands.
+ *   The signup form will be generated via HTMLForm.
  */
 
 class UsercreateTemplate extends BaseTemplate {
index c2b2df6..e816b62 100644 (file)
@@ -20,6 +20,8 @@
  *
  * @file
  * @ingroup Templates
+ * @deprecated Will be removed when AuthManager lands.
+ *   The login form will be generated via HTMLForm.
  */
 
 class UserloginTemplate extends BaseTemplate {
index 0e291ed..38e9ecd 100644 (file)
@@ -181,6 +181,37 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
                );
        }
 
+       /**
+        * @since 1.27
+        * @see TitleFormatter::getPrefixedDBkey()
+        * @param LinkTarget $target
+        * @return string
+        */
+       public function getPrefixedDBkey( LinkTarget $target ) {
+               $key = '';
+               if ( $target->isExternal() ) {
+                       $key .= $target->getInterwiki() . ':';
+               }
+               // Try to get a namespace name, but fallback
+               // to empty string if it doesn't exist
+               try {
+                       $nsName = $this->getNamespaceName(
+                               $target->getNamespace(),
+                               $target->getText()
+                       );
+               } catch ( InvalidArgumentException $e ) {
+                       $nsName = '';
+               }
+
+               if ( $target->getNamespace() !== 0 ) {
+                       $key .= $nsName . ':';
+               }
+
+               $key .= $target->getText();
+
+               return strtr( $key, ' ', '_' );
+       }
+
        /**
         * @see TitleFormatter::getText()
         *
index c081129..5177606 100644 (file)
@@ -68,6 +68,18 @@ interface TitleFormatter {
         */
        public function getPrefixedText( LinkTarget $title );
 
+       /**
+        * Return the title in prefixed database key form, with interwiki
+        * and namespace.
+        *
+        * @since 1.27
+        *
+        * @param LinkTarget $target
+        *
+        * @return string
+        */
+       public function getPrefixedDBkey( LinkTarget $target );
+
        /**
         * Returns the title formatted for display, with namespace and fragment.
         *
index 63c075f..597bf2f 100644 (file)
@@ -94,6 +94,15 @@ class TitleValue implements LinkTarget {
                return $this->namespace;
        }
 
+       /**
+        * @since 1.27
+        * @param int $ns
+        * @return bool
+        */
+       public function inNamespace( $ns ) {
+               return $this->namespace == $ns;
+       }
+
        /**
         * @return string
         */
diff --git a/includes/user/PasswordReset.php b/includes/user/PasswordReset.php
new file mode 100644 (file)
index 0000000..60144bb
--- /dev/null
@@ -0,0 +1,257 @@
+<?php
+/**
+ * User password reset helper for MediaWiki.
+ *
+ * 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
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
+
+/**
+ * Helper class for the password reset functionality shared by the web UI and the API.
+ *
+ * Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
+ * EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
+ * functionality) to be enabled.
+ */
+class PasswordReset {
+       /** @var Config */
+       protected $config;
+
+       /** @var AuthManager */
+       protected $authManager;
+
+       /**
+        * In-process cache for isAllowed lookups, by username. Contains pairs of StatusValue objects
+        * (for false and true value of $displayPassword, respectively).
+        * @var HashBagOStuff
+        */
+       private $permissionCache;
+
+       public function __construct( Config $config, AuthManager $authManager ) {
+               $this->config = $config;
+               $this->authManager = $authManager;
+               $this->permissionCache = new HashBagOStuff( [ 'maxKeys' => 1 ] );
+       }
+
+       /**
+        * Check if a given user has permission to use this functionality.
+        * @param User $user
+        * @param bool $displayPassword If set, also check whether the user is allowed to reset the
+        *   password of another user and see the temporary password.
+        * @return StatusValue
+        */
+       public function isAllowed( User $user, $displayPassword = false ) {
+               $statuses = $this->permissionCache->get( $user->getName() );
+               if ( $statuses ) {
+                       list ( $status, $status2 ) = $statuses;
+               } else {
+                       $resetRoutes = $this->config->get( 'PasswordResetRoutes' );
+                       $status = StatusValue::newGood();
+
+                       if ( !is_array( $resetRoutes ) ||
+                                !in_array( true, array_values( $resetRoutes ), true )
+                       ) {
+                               // Maybe password resets are disabled, or there are no allowable routes
+                               $status = StatusValue::newFatal( 'passwordreset-disabled' );
+                       } elseif (
+                               ( $providerStatus = $this->authManager->allowsAuthenticationDataChange(
+                                       new TemporaryPasswordAuthenticationRequest(), false ) )
+                               && !$providerStatus->isGood()
+                       ) {
+                               // Maybe the external auth plugin won't allow local password changes
+                               $status = StatusValue::newFatal( 'resetpass_forbidden-reason',
+                                       $providerStatus->getMessage() );
+                       } elseif ( !$this->config->get( 'EnableEmail' ) ) {
+                               // Maybe email features have been disabled
+                               $status = StatusValue::newFatal( 'passwordreset-emaildisabled' );
+                       } elseif ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
+                               // Maybe not all users have permission to change private data
+                               $status = StatusValue::newFatal( 'badaccess' );
+                       } elseif ( $user->isBlocked() ) {
+                               // Maybe the user is blocked (check this here rather than relying on the parent
+                               // method as we have a more specific error message to use here
+                               $status = StatusValue::newFatal( 'blocked-mailpassword' );
+                       }
+
+                       $status2 = StatusValue::newGood();
+                       if ( !$user->isAllowed( 'passwordreset' ) ) {
+                               $status2 = StatusValue::newFatal( 'badaccess' );
+                       }
+
+                       $this->permissionCache->set( $user->getName(), [ $status, $status2 ] );
+               }
+
+               if ( !$displayPassword || !$status->isGood() ) {
+                       return $status;
+               } else {
+                       return $status2;
+               }
+       }
+
+       /**
+        * Do a password reset. Authorization is the caller's responsibility.
+        *
+        * Process the form.  At this point we know that the user passes all the criteria in
+        * userCanExecute(), and if the data array contains 'Username', etc, then Username
+        * resets are allowed.
+        * @param User $performingUser The user that does the password reset
+        * @param string $username The user whose password is reset
+        * @param string $email Alternative way to specify the user
+        * @param bool $displayPassword Whether to display the password
+        * @return StatusValue Will contain the passwords as a username => password array if the
+        *   $displayPassword flag was set
+        * @throws LogicException When the user is not allowed to perform the action
+        * @throws MWException On unexpected DB errors
+        */
+       public function execute(
+               User $performingUser, $username = null, $email = null, $displayPassword = false
+       ) {
+               if ( !$this->isAllowed( $performingUser, $displayPassword )->isGood() ) {
+                       $action = $this->isAllowed( $performingUser )->isGood() ? 'display' : 'reset';
+                       throw new LogicException( 'User ' . $performingUser->getName()
+                               . ' is not allowed to ' . $action . ' passwords' );
+               }
+
+               $resetRoutes = $this->config->get( 'PasswordResetRoutes' )
+                       + [ 'username' => false, 'email' => false ];
+               if ( $resetRoutes['username'] && $username ) {
+                       $method = 'username';
+                       $users = [ User::newFromName( $username ) ];
+               } elseif ( $resetRoutes['email'] && $email ) {
+                       if ( !Sanitizer::validateEmail( $email ) ) {
+                               return StatusValue::newFatal( 'passwordreset-invalidemail' );
+                       }
+                       $method = 'email';
+                       $users = $this->getUsersByEmail( $email );
+               } else {
+                       // The user didn't supply any data
+                       return StatusValue::newFatal( 'passwordreset-nodata' );
+               }
+
+               // Check for hooks (captcha etc), and allow them to modify the users list
+               $error = [];
+               $data = [
+                       'Username' => $username,
+                       'Email' => $email,
+                       'Capture' => $displayPassword ? '1' : null,
+               ];
+               if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
+                       return StatusValue::newFatal( wfMessage( $error ) );
+               }
+
+               if ( !$users ) {
+                       if ( $method === 'email' ) {
+                               // Don't reveal whether or not an email address is in use
+                               return StatusValue::newGood( [] );
+                       } else {
+                               return StatusValue::newFatal( 'noname' );
+                       }
+               }
+
+               $firstUser = $users[0];
+
+               if ( !$firstUser instanceof User || !$firstUser->getId() ) {
+                       // Don't parse username as wikitext (bug 65501)
+                       return StatusValue::newFatal( wfMessage( 'nosuchuser', wfEscapeWikiText( $username ) ) );
+               }
+
+               // Check against the rate limiter
+               if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
+                       return StatusValue::newFatal( 'actionthrottledtext' );
+               }
+
+               // All the users will have the same email address
+               if ( !$firstUser->getEmail() ) {
+                       // This won't be reachable from the email route, so safe to expose the username
+                       return StatusValue::newFatal( wfMessage( 'noemail',
+                               wfEscapeWikiText( $firstUser->getName() ) ) );
+               }
+
+               // We need to have a valid IP address for the hook, but per bug 18347, we should
+               // send the user's name if they're logged in.
+               $ip = $performingUser->getRequest()->getIP();
+               if ( !$ip ) {
+                       return StatusValue::newFatal( 'badipaddress' );
+               }
+
+               Hooks::run( 'User::mailPasswordInternal', [ &$performingUser, &$ip, &$firstUser ] );
+
+               $result = StatusValue::newGood();
+               $reqs = [];
+               foreach ( $users as $user ) {
+                       $req = TemporaryPasswordAuthenticationRequest::newRandom();
+                       $req->username = $user->getName();
+                       $req->mailpassword = true;
+                       $req->hasBackchannel = $displayPassword;
+                       $req->caller = $performingUser->getName();
+                       $status = $this->authManager->allowsAuthenticationDataChange( $req, true );
+                       if ( $status->isGood() && $status->getValue() !== 'ignored' ) {
+                               $reqs[] = $req;
+                       } elseif ( $result->isGood() ) {
+                               // only record the first error, to avoid exposing the number of users having the
+                               // same email address
+                               if ( $status->getValue() === 'ignored' ) {
+                                       $status = StatusValue::newFatal( 'passwordreset-ignored' );
+                               }
+                               $result->merge( $status );
+                       }
+               }
+
+               if ( !$result->isGood() ) {
+                       return $result;
+               }
+
+               $passwords = [];
+               foreach ( $reqs as $req ) {
+                       $this->authManager->changeAuthenticationData( $req );
+                       // TODO record mail sending errors
+                       if ( $displayPassword ) {
+                               $passwords[$req->username] = $req->password;
+                       }
+               }
+
+               return StatusValue::newGood( $passwords );
+       }
+
+       /**
+        * @param string $email
+        * @return User[]
+        * @throws MWException On unexpected database errors
+        */
+       protected function getUsersByEmail( $email ) {
+               $res = wfGetDB( DB_SLAVE )->select(
+                       'user',
+                       User::selectFields(),
+                       [ 'user_email' => $email ],
+                       __METHOD__
+               );
+
+               if ( !$res ) {
+                       // Some sort of database error, probably unreachable
+                       throw new MWException( 'Unknown database error in ' . __METHOD__ );
+               }
+
+               $users = [];
+               foreach ( $res as $row ) {
+                       $users[] = User::newFromRow( $row );
+               }
+               return $users;
+       }
+}
index 3d7d71c..71023c0 100644 (file)
@@ -23,6 +23,9 @@
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\SessionManager;
 use MediaWiki\Session\Token;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthenticationRequest;
 
 /**
  * String Some punctuation to prevent editing from broken text-mangling proxies.
@@ -127,6 +130,7 @@ class User implements IDBAccessObject {
                'createpage',
                'createtalk',
                'delete',
+               'deletechangetags',
                'deletedhistory',
                'deletedtext',
                'deletelogentry',
@@ -498,7 +502,10 @@ class User implements IDBAccessObject {
                $data = $processCache->get( $key );
                if ( !is_array( $data ) ) {
                        $data = $cache->get( $key );
-                       if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) {
+                       if ( !is_array( $data )
+                               || !isset( $data['mVersion'] )
+                               || $data['mVersion'] < self::VERSION
+                       ) {
                                // Object is expired
                                return false;
                        }
@@ -674,8 +681,11 @@ class User implements IDBAccessObject {
         *  - steal: Whether to reset the account's password and email if it
         *    already exists, default false
         * @return User|null
+        * @since 1.27
         */
        public static function newSystemUser( $name, $options = [] ) {
+               global $wgDisableAuthManager;
+
                $options += [
                        'validate' => 'valid',
                        'create' => true,
@@ -687,13 +697,15 @@ class User implements IDBAccessObject {
                        return null;
                }
 
+               $fields = self::selectFields();
+               if ( $wgDisableAuthManager ) {
+                       $fields = array_merge( $fields, [ 'user_password', 'user_newpassword' ] );
+               }
+
                $dbw = wfGetDB( DB_MASTER );
                $row = $dbw->selectRow(
                        'user',
-                       array_merge(
-                               self::selectFields(),
-                               [ 'user_password', 'user_newpassword' ]
-                       ),
+                       $fields,
                        [ 'user_name' => $name ],
                        __METHOD__
                );
@@ -706,40 +718,50 @@ class User implements IDBAccessObject {
                // A user is considered to exist as a non-system user if it has a
                // password set, or a temporary password set, or an email set, or a
                // non-invalid token.
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               try {
-                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $password = PasswordFactory::newInvalidPassword();
-               }
-               try {
-                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newpassword = PasswordFactory::newInvalidPassword();
-               }
-               if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
-                       || $user->mEmail || $user->mToken !== self::INVALID_TOKEN
-               ) {
+               if ( !$user->mEmail && $user->mToken === self::INVALID_TOKEN ) {
+                       if ( $wgDisableAuthManager ) {
+                               $passwordFactory = new PasswordFactory();
+                               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                               try {
+                                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
+                               } catch ( PasswordError $e ) {
+                                       wfDebug( 'Invalid password hash found in database.' );
+                                       $password = PasswordFactory::newInvalidPassword();
+                               }
+                               try {
+                                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+                               } catch ( PasswordError $e ) {
+                                       wfDebug( 'Invalid password hash found in database.' );
+                                       $newpassword = PasswordFactory::newInvalidPassword();
+                               }
+                               $canAuthenticate = !$password instanceof InvalidPassword ||
+                                       !$newpassword instanceof InvalidPassword;
+                       } else {
+                               $canAuthenticate = AuthManager::singleton()->userCanAuthenticate( $name );
+                       }
+               }
+               if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN || $canAuthenticate ) {
                        // User exists. Steal it?
                        if ( !$options['steal'] ) {
                                return null;
                        }
 
-                       $nopass = PasswordFactory::newInvalidPassword()->toString();
+                       if ( $wgDisableAuthManager ) {
+                               $nopass = PasswordFactory::newInvalidPassword()->toString();
+                               $dbw->update(
+                                       'user',
+                                       [
+                                               'user_password' => $nopass,
+                                               'user_newpassword' => $nopass,
+                                               'user_newpass_time' => null,
+                                       ],
+                                       [ 'user_id' => $user->getId() ],
+                                       __METHOD__
+                               );
+                       } else {
+                               AuthManager::singleton()->revokeAccessForUser( $name );
+                       }
 
-                       $dbw->update(
-                               'user',
-                               [
-                                       'user_password' => $nopass,
-                                       'user_newpassword' => $nopass,
-                                       'user_newpass_time' => null,
-                               ],
-                               [ 'user_id' => $user->getId() ],
-                               __METHOD__
-                       );
                        $user->invalidateEmail();
                        $user->mToken = self::INVALID_TOKEN;
                        $user->saveSettings();
@@ -1078,8 +1100,9 @@ class User implements IDBAccessObject {
                }
 
                // Reject various classes of invalid names
-               global $wgAuth;
-               $name = $wgAuth->getCanonicalName( $t->getText() );
+               $name = AuthManager::callLegacyAuthPlugin(
+                       'getCanonicalName', [ $t->getText() ], $t->getText()
+               );
 
                switch ( $validate ) {
                        case false:
@@ -1404,7 +1427,7 @@ class User implements IDBAccessObject {
         * @see $wgAutopromoteOnce
         */
        public function addAutopromoteOnceGroups( $event ) {
-               global $wgAutopromoteOnceLogInRC, $wgAuth;
+               global $wgAutopromoteOnceLogInRC;
 
                if ( wfReadOnly() || !$this->getId() ) {
                        return [];
@@ -1425,7 +1448,7 @@ class User implements IDBAccessObject {
                }
                // update groups in external authentication database
                Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false ] );
-               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+               AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
 
                $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
 
@@ -2040,9 +2063,8 @@ class User implements IDBAccessObject {
                if ( $this->mLocked !== null ) {
                        return $this->mLocked;
                }
-               global $wgAuth;
-               $authUser = $wgAuth->getUserInstance( $this );
-               $this->mLocked = (bool)$authUser->isLocked();
+               $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+               $this->mLocked = $authUser && $authUser->isLocked();
                Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
                return $this->mLocked;
        }
@@ -2058,9 +2080,8 @@ class User implements IDBAccessObject {
                }
                $this->getBlockedStatus();
                if ( !$this->mHideName ) {
-                       global $wgAuth;
-                       $authUser = $wgAuth->getUserInstance( $this );
-                       $this->mHideName = (bool)$authUser->isHidden();
+                       $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+                       $this->mHideName = $authUser && $authUser->isHidden();
                        Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
                }
                return $this->mHideName;
@@ -2466,13 +2487,17 @@ class User implements IDBAccessObject {
         * wipes it, so the account cannot be logged in until
         * a new password is set, for instance via e-mail.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $str New password to set
         * @throws PasswordError On failure
         * @return bool
         */
        public function setPassword( $str ) {
-               global $wgAuth;
+               global $wgAuth, $wgDisableAuthManager;
+
+               if ( !$wgDisableAuthManager ) {
+                       return $this->setPasswordInternal( $str );
+               }
 
                if ( $str !== null ) {
                        if ( !$wgAuth->allowPasswordChange() ) {
@@ -2491,7 +2516,6 @@ class User implements IDBAccessObject {
 
                $this->setOption( 'watchlisttoken', false );
                $this->setPasswordInternal( $str );
-               SessionManager::singleton()->invalidateSessionsForUser( $this );
 
                return true;
        }
@@ -2499,18 +2523,21 @@ class User implements IDBAccessObject {
        /**
         * Set the password and reset the random token unconditionally.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string|null $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to log in
         *  through the web interface.
         */
        public function setInternalPassword( $str ) {
-               global $wgAuth;
+               global $wgAuth, $wgDisableAuthManager;
+
+               if ( !$wgDisableAuthManager ) {
+                       $this->setPasswordInternal( $str );
+               }
 
                if ( $wgAuth->allowSetLocalPassword() ) {
                        $this->setOption( 'watchlisttoken', false );
                        $this->setPasswordInternal( $str );
-                       SessionManager::singleton()->invalidateSessionsForUser( $this );
                }
        }
 
@@ -2520,31 +2547,68 @@ class User implements IDBAccessObject {
         * @param string|null $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to log in
         *  through the web interface.
+        * @return bool Success
         */
        private function setPasswordInternal( $str ) {
-               $id = self::idFromName( $this->getName(), self::READ_LATEST );
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+               global $wgDisableAuthManager;
+
+               if ( $wgDisableAuthManager ) {
+                       $id = self::idFromName( $this->getName(), self::READ_LATEST );
+                       if ( $id == 0 ) {
+                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+                       }
+
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->update(
+                               'user',
+                               [
+                                       'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
+                                       'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
+                                       'user_newpass_time' => $dbw->timestampOrNull( null ),
+                               ],
+                               [
+                                       'user_id' => $id,
+                               ],
+                               __METHOD__
+                       );
+
+                       // When the main password is changed, invalidate all bot passwords too
+                       BotPassword::invalidateAllPasswordsForUser( $this->getName() );
+               } else {
+                       $manager = AuthManager::singleton();
+
+                       // If the user doesn't exist yet, fail
+                       if ( !$manager->userExists( $this->getName() ) ) {
+                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+                       }
+
+                       $data = [
+                               'username' => $this->getName(),
+                               'password' => $str,
+                               'retype' => $str,
+                       ];
+                       $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
+                       $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+                       foreach ( $reqs as $req ) {
+                               $status = $manager->allowsAuthenticationDataChange( $req );
+                               if ( !$status->isOk() ) {
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Password change rejected: ' . $status->getWikiText() );
+                                       return false;
+                               }
+                       }
+                       foreach ( $reqs as $req ) {
+                               $manager->changeAuthenticationData( $req );
+                       }
+
+                       $this->setOption( 'watchlisttoken', false );
                }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       [
-                               'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
-                               'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
-                               'user_newpass_time' => $dbw->timestampOrNull( null ),
-                       ],
-                       [
-                               'user_id' => $id,
-                       ],
-                       __METHOD__
-               );
+               SessionManager::singleton()->invalidateSessionsForUser( $this );
 
-               // When the main password is changed, invalidate all bot passwords too
-               BotPassword::invalidateAllPasswordsForUser( $this->getName() );
+               return true;
        }
 
        /**
@@ -2606,63 +2670,74 @@ class User implements IDBAccessObject {
        /**
         * Set the password for a password reminder or new account email
         *
-        * @deprecated since 1.27, AuthManager is coming
+        * @deprecated Removed in 1.27. Use PasswordReset instead.
         * @param string $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to use it
         * @param bool $throttle If true, reset the throttle timestamp to the present
         */
        public function setNewpassword( $str, $throttle = true ) {
-               $id = $this->getId();
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
-               }
+               global $wgDisableAuthManager;
 
-               $dbw = wfGetDB( DB_MASTER );
+               if ( $wgDisableAuthManager ) {
+                       $id = $this->getId();
+                       if ( $id == 0 ) {
+                               throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
+                       }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $update = [
-                       'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
-               ];
+                       $dbw = wfGetDB( DB_MASTER );
 
-               if ( $str === null ) {
-                       $update['user_newpass_time'] = null;
-               } elseif ( $throttle ) {
-                       $update['user_newpass_time'] = $dbw->timestamp();
-               }
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $update = [
+                               'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
+                       ];
 
-               $dbw->update( 'user', $update, [ 'user_id' => $id ], __METHOD__ );
+                       if ( $str === null ) {
+                               $update['user_newpass_time'] = null;
+                       } elseif ( $throttle ) {
+                               $update['user_newpass_time'] = $dbw->timestamp();
+                       }
+
+                       $dbw->update( 'user', $update, [ 'user_id' => $id ], __METHOD__ );
+               } else {
+                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
+               }
        }
 
        /**
         * Has password reminder email been sent within the last
         * $wgPasswordReminderResendTime hours?
+        * @deprecated Removed in 1.27. See above.
         * @return bool
         */
        public function isPasswordReminderThrottled() {
-               global $wgPasswordReminderResendTime;
+               global $wgPasswordReminderResendTime, $wgDisableAuthManager;
 
-               if ( !$wgPasswordReminderResendTime ) {
-                       return false;
-               }
+               if ( $wgDisableAuthManager ) {
+                       if ( !$wgPasswordReminderResendTime ) {
+                               return false;
+                       }
 
-               $this->load();
+                       $this->load();
 
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-               $newpassTime = $db->selectField(
-                       'user',
-                       'user_newpass_time',
-                       [ 'user_id' => $this->getId() ],
-                       __METHOD__
-               );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
+                       $newpassTime = $db->selectField(
+                               'user',
+                               'user_newpass_time',
+                               [ 'user_id' => $this->getId() ],
+                               __METHOD__
+                       );
 
-               if ( $newpassTime === null ) {
-                       return false;
+                       if ( $newpassTime === null ) {
+                               return false;
+                       }
+                       $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
+                       return time() < $expiry;
+               } else {
+                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
                }
-               $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
-               return time() < $expiry;
        }
 
        /**
@@ -3403,12 +3478,14 @@ class User implements IDBAccessObject {
         * @since 1.28
         */
        public function isBot() {
-               $isBot = false;
-               if ( !Hooks::run( "UserIsBot", [ $this, &$isBot ] ) ) {
-                       return $isBot;
+               if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
+                       return true;
                }
 
-               return ( $isBot || in_array( 'bot', $this->getGroups() ) );
+               $isBot = false;
+               Hooks::run( "UserIsBot", [ $this, &$isBot ] );
+
+               return $isBot;
        }
 
        /**
@@ -4144,110 +4221,140 @@ class User implements IDBAccessObject {
 
        /**
         * Check to see if the given clear-text password is one of the accepted passwords
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $password User password
         * @return bool True if the given password is correct, otherwise False
         */
        public function checkPassword( $password ) {
-               global $wgAuth, $wgLegacyEncoding;
+               global $wgAuth, $wgLegacyEncoding, $wgDisableAuthManager;
 
-               $this->load();
+               if ( $wgDisableAuthManager ) {
+                       $this->load();
 
-               // Some passwords will give a fatal Status, which means there is
-               // some sort of technical or security reason for this password to
-               // be completely invalid and should never be checked (e.g., T64685)
-               if ( !$this->checkPasswordValidity( $password )->isOK() ) {
-                       return false;
-               }
+                       // Some passwords will give a fatal Status, which means there is
+                       // some sort of technical or security reason for this password to
+                       // be completely invalid and should never be checked (e.g., T64685)
+                       if ( !$this->checkPasswordValidity( $password )->isOK() ) {
+                               return false;
+                       }
 
-               // Certain authentication plugins do NOT want to save
-               // domain passwords in a mysql database, so we should
-               // check this (in case $wgAuth->strict() is false).
-               if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
-                       return true;
-               } elseif ( $wgAuth->strict() ) {
-                       // Auth plugin doesn't allow local authentication
-                       return false;
-               } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
-                       // Auth plugin doesn't allow local authentication for this user name
-                       return false;
-               }
+                       // Certain authentication plugins do NOT want to save
+                       // domain passwords in a mysql database, so we should
+                       // check this (in case $wgAuth->strict() is false).
+                       if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
+                               return true;
+                       } elseif ( $wgAuth->strict() ) {
+                               // Auth plugin doesn't allow local authentication
+                               return false;
+                       } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
+                               // Auth plugin doesn't allow local authentication for this user name
+                               return false;
+                       }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
 
-               try {
-                       $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
-                               'user', 'user_password', [ 'user_id' => $this->getId() ], __METHOD__
-                       ) );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $mPassword = PasswordFactory::newInvalidPassword();
-               }
+                       try {
+                               $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
+                                       'user', 'user_password', [ 'user_id' => $this->getId() ], __METHOD__
+                               ) );
+                       } catch ( PasswordError $e ) {
+                               wfDebug( 'Invalid password hash found in database.' );
+                               $mPassword = PasswordFactory::newInvalidPassword();
+                       }
 
-               if ( !$mPassword->equals( $password ) ) {
-                       if ( $wgLegacyEncoding ) {
-                               // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
-                               // Check for this with iconv
-                               $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
-                               if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
+                       if ( !$mPassword->equals( $password ) ) {
+                               if ( $wgLegacyEncoding ) {
+                                       // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+                                       // Check for this with iconv
+                                       $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
+                                       if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
+                                               return false;
+                                       }
+                               } else {
                                        return false;
                                }
-                       } else {
-                               return false;
                        }
-               }
 
-               if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
-                       $this->setPasswordInternal( $password );
-               }
+                       if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
+                               $this->setPasswordInternal( $password );
+                       }
 
-               return true;
+                       return true;
+               } else {
+                       $manager = AuthManager::singleton();
+                       $reqs = AuthenticationRequest::loadRequestsFromSubmission(
+                               $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
+                               [
+                                       'username' => $this->getName(),
+                                       'password' => $password,
+                               ]
+                       );
+                       $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+                       switch ( $res->status ) {
+                               case AuthenticationResponse::PASS:
+                                       return true;
+                               case AuthenticationResponse::FAIL:
+                                       // Hope it's not a PreAuthenticationProvider that failed...
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
+                                       return false;
+                               default:
+                                       throw new BadMethodCallException(
+                                               'AuthManager returned a response unsupported by ' . __METHOD__
+                                       );
+                       }
+               }
        }
 
        /**
         * Check if the given clear-text password matches the temporary password
         * sent by e-mail for password reset operations.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $plaintext
         * @return bool True if matches, false otherwise
         */
        public function checkTemporaryPassword( $plaintext ) {
-               global $wgNewPasswordExpiry;
+               global $wgNewPasswordExpiry, $wgDisableAuthManager;
 
-               $this->load();
+               if ( $wgDisableAuthManager ) {
+                       $this->load();
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
 
-               $row = $db->selectRow(
-                       'user',
-                       [ 'user_newpassword', 'user_newpass_time' ],
-                       [ 'user_id' => $this->getId() ],
-                       __METHOD__
-               );
-               try {
-                       $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newPassword = PasswordFactory::newInvalidPassword();
-               }
+                       $row = $db->selectRow(
+                               'user',
+                               [ 'user_newpassword', 'user_newpass_time' ],
+                               [ 'user_id' => $this->getId() ],
+                               __METHOD__
+                       );
+                       try {
+                               $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+                       } catch ( PasswordError $e ) {
+                               wfDebug( 'Invalid password hash found in database.' );
+                               $newPassword = PasswordFactory::newInvalidPassword();
+                       }
 
-               if ( $newPassword->equals( $plaintext ) ) {
-                       if ( is_null( $row->user_newpass_time ) ) {
-                               return true;
+                       if ( $newPassword->equals( $plaintext ) ) {
+                               if ( is_null( $row->user_newpass_time ) ) {
+                                       return true;
+                               }
+                               $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
+                               return ( time() < $expiry );
+                       } else {
+                               return false;
                        }
-                       $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
-                       return ( time() < $expiry );
                } else {
-                       return false;
+                       // Can't check the temporary password individually.
+                       return $this->checkPassword( $plaintext );
                }
        }
 
@@ -5103,6 +5210,7 @@ class User implements IDBAccessObject {
         * Add a newuser log entry for this user.
         * Before 1.19 the return value was always true.
         *
+        * @deprecated since 1.27, AuthManager handles logging
         * @param string|bool $action Account creation type.
         *   - String, one of the following values:
         *     - 'create' for an anonymous user creating an account for himself.
@@ -5115,14 +5223,13 @@ class User implements IDBAccessObject {
         *     - true will be converted to 'byemail'
         *     - false will be converted to 'create' if this object is the same as
         *       $wgUser and to 'create2' otherwise
-        *
         * @param string $reason User supplied reason
-        *
-        * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
+        * @return int|bool True if not $wgNewUserLog or not $wgDisableAuthManager;
+        *   otherwise ID of log item or 0 on failure
         */
        public function addNewUserLogEntry( $action = false, $reason = '' ) {
-               global $wgUser, $wgNewUserLog;
-               if ( empty( $wgNewUserLog ) ) {
+               global $wgUser, $wgNewUserLog, $wgDisableAuthManager;
+               if ( !$wgDisableAuthManager || empty( $wgNewUserLog ) ) {
                        return true; // disabled
                }
 
@@ -5163,6 +5270,7 @@ class User implements IDBAccessObject {
         * Used by things like CentralAuth and perhaps other authplugins.
         * Consider calling addNewUserLogEntry() directly instead.
         *
+        * @deprecated since 1.27, AuthManager handles logging
         * @return bool
         */
        public function addNewUserLogEntryAutoCreate() {
index 20047c4..0455740 100644 (file)
        "noname": "Nan ureuëng ngui nyang Droënueh peutamöng hana sah.",
        "loginsuccesstitle": "Meuhasé tamöng",
        "loginsuccess": "'''Droëneuh  jinoë ka neutamöng di {{SITENAME}} sibagoë \"$1\".'''",
-       "nosuchuser": "Hana ureuëng ngui ngön nan \"$1\".\nNan ureuëng ngui jipeubida haraih rayek.\nTulông neuparéksa keulayi neuija Droëneuh, atawa [[Special:UserLogin/signup|neudapeuta barô]].",
+       "nosuchuser": "Hana ureuëng ngui ngön nan \"$1\".\nNan ureuëng ngui jipeubida haraih rayek.\nTulông neuparéksa keulayi neuija Droëneuh, atawa [[Special:CreateAccount|neudapeuta barô]].",
        "nosuchusershort": "Hana ureuëng ngui ngön nan \"$1\".\nPréksa keulayi neu’ija Droëneuh.",
        "nouserspecified": "Neupasoë nan Droëneuh.",
        "login-userblocked": "Ureuëng ngui nyoë ka teublokir, hana idin/hanjeut tamöng.",
        "accmailtitle": "Lageuem rahsia ka meukirém",
        "newarticle": "(Barô)",
        "newarticletext": "Droëneuh ka neuseutöt peunawôt u laman nyang goh na.\nKeu neupeugöt laman nyan, neukeutik lam plôk di yup (eu [$1 laman beunantu] keu haba leubèh le).\nMeunyö droëneuh trôk keunoë hana neusaja, neuteugön tèk '''back''' bak ''browser'''droëneuh.",
-       "anontalkpagetext": "----''Nyoe nakeuh ôn marit ureueng ngui nyang hana tamöng atawa hana geungui.''\nSaweub nyan, kamoe payah meukubah alamat IP-geuh keu meuparéksa. \nAlamat IP mungkén jingui lé padum-padum droe ureueng.\nMeunyoe droeneuh ureueng nyang hana tamöng nyan, tulông [[Special:UserLogin/signup|peugöt nan ureueng ngui]] atawa [[Special:UserLogin|tamöng log]] mangat meuteugah nibak bhah nyang hana meuphôm bak uroe la'én.",
+       "anontalkpagetext": "----''Nyoe nakeuh ôn marit ureueng ngui nyang hana tamöng atawa hana geungui.''\nSaweub nyan, kamoe payah meukubah alamat IP-geuh keu meuparéksa. \nAlamat IP mungkén jingui lé padum-padum droe ureueng.\nMeunyoe droeneuh ureueng nyang hana tamöng nyan, tulông [[Special:CreateAccount|peugöt nan ureueng ngui]] atawa [[Special:UserLogin|tamöng log]] mangat meuteugah nibak bhah nyang hana meuphôm bak uroe la'én.",
        "noarticletext": "Hana naseukah jinoë lam laman nyoë.\nJi Droëneuh jeuët [[Special:Search/{{PAGENAME}}|neumita keu nan ôn nyoë]] bak ôn-ôn la’én, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} log nyang na hubôngan], atawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} neu'andam ôn nyoë]</span>.",
        "noarticletext-nopermission": "Hana asoë bak laman nyoë jinoë.\nDroëneuh jeuët [[Special:Search/{{PAGENAME}}|neumita keu nan ôn nyoë]] bak laman-laman la'én,\natawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} neumita log nyang na meuhubông]</span>, tapi Droëneuh hana idin keu neupeugöt laman nyoë",
        "userpage-userdoesnotexist-view": "Ureueng ngui \"$1\" hana teudapeuta.",
        "allpagesprefix": "Peuleumah laman ngön harah phôn:",
        "allpages-hide-redirects": "Peusom peuninah",
        "categories": "Dapeuta kawan",
-       "special-categories-sort-count": "atôe meunurôt jumeulah",
-       "special-categories-sort-abc": "atôe meunurôt seunurat",
        "deletedcontributions": "Beuneuri nyang geusampôh",
        "deletedcontributions-title": "Beuneuri nyang geusampôh",
        "sp-deletedcontributions-contribs": "beuneuri",
index 1ae670f..fac0302 100644 (file)
        "noname": "Ongeldige gebruikersnaam.",
        "loginsuccesstitle": "Suksesvolle aanmelding",
        "loginsuccess": "U is nou by {{SITENAME}} as \"$1\" ingeteken.",
-       "nosuchuser": "Die gebruiker \"$1\" bestaan nie.\nGebruikersname is gevoelig vir hoofletters.\nMaak seker dit is reg gespel of [[Special:UserLogin/signup|skep 'n nuwe rekening]].",
+       "nosuchuser": "Die gebruiker \"$1\" bestaan nie.\nGebruikersname is gevoelig vir hoofletters.\nMaak seker dit is reg gespel of [[Special:CreateAccount|skep 'n nuwe rekening]].",
        "nosuchusershort": "Daar is geen gebruikersnaam \"$1\" nie. Maak seker dit is reg gespel.",
        "nouserspecified": "U moet 'n gebruikersnaam spesifiseer.",
        "login-userblocked": "Hierdie gebruiker is geblokkeer.\nIntekening word verbied.",
        "accmailtext": "'n Lukrake wagwoord vir [[User talk:$1|$1]] is na $2 gestuur.\n\nDie wagwoord vir hierdie nuwe gebruiker kan op die ''[[Special:ChangePassword|verander wagwoord]]''-bladsy verander word nadat ingeteken is.",
        "newarticle": "(Nuut)",
        "newarticletext": "Hierdie bladsy bestaan nie.\nTik iets in die invoerboks hier onder om 'n nuwe bladsy te skep. Meer inligting is op die [$1 hulpbladsy] beskikbaar.\nAs u per ongeluk hier uitgekom het, gebruik u blaaier se '''terug'''-knoppie.",
-       "anontalkpagetext": "----''Hierdie is die besprekingsblad vir 'n anonieme gebruiker wat nog nie 'n gebruiker geskep het nie, of wat dit nie gebruik nie.\nDaarom moet ons sy/haar numeriese IP-adres vir identifikasie gebruik.\nSó 'n adres kan deur verskeie gebruikers gedeel word.\nIndien u 'n anonieme gebruiker is wat voel dat ontoepaslike kommentaar teen u gerig is, [[Special:UserLogin/signup|skep 'n gebruiker]] of [[Special:UserLogin|meld aan]] om verwarring met ander anonieme gebruikers te voorkom.''",
+       "anontalkpagetext": "----''Hierdie is die besprekingsblad vir 'n anonieme gebruiker wat nog nie 'n gebruiker geskep het nie, of wat dit nie gebruik nie.\nDaarom moet ons sy/haar numeriese IP-adres vir identifikasie gebruik.\nSó 'n adres kan deur verskeie gebruikers gedeel word.\nIndien u 'n anonieme gebruiker is wat voel dat ontoepaslike kommentaar teen u gerig is, [[Special:CreateAccount|skep 'n gebruiker]] of [[Special:UserLogin|meld aan]] om verwarring met ander anonieme gebruikers te voorkom.''",
        "noarticletext": "Hierdie bladsy bevat geen teks nie.\nU kan [[Special:Search/{{PAGENAME}}|vir die bladsytitel in ander bladsye soek]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} die verwante logboeke deursoek]\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} hierdie bladsy wysig]</span>.",
        "noarticletext-nopermission": "Hierdie bladsy bevat geen teks nie.\nU kan vir die term [[Special:Search/{{PAGENAME}}|in ander bladsye soek]] of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} die verwante logboeke deursoek]</span>, maar u kan nie die bladsy skep nie.",
        "missing-revision": "Die weergawe #$1 van die bladsy \"{{FULLPAGENAME}} bestaan nie.\n\nDit word meestal veroorsaak deur die volg van 'n verouderde verwysing na 'n bladsy wat verwyder is.\nMeer gegewens kan moontlik in die [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} skraplogboek] gevind word.",
index 9818253..dde3a06 100644 (file)
        "noname": "Nuk ke dhânë nofkë valide.",
        "loginsuccesstitle": "Kyçje e suksesshme",
        "loginsuccess": "'''Je kyçë në {{SITENAME}} si \"$1\".'''",
-       "nosuchuser": "Nuk ka përdorues me emnin \"$1\".\nEmnat janë senzitiv në madhësi të germës.\nKontrollo drejtshkrimin ose [[Special:UserLogin/signup|krijo llogari]].",
+       "nosuchuser": "Nuk ka përdorues me emnin \"$1\".\nEmnat janë senzitiv në madhësi të germës.\nKontrollo drejtshkrimin ose [[Special:CreateAccount|krijo llogari]].",
        "nosuchusershort": "Nuk ka përdorues me emnin \"$1\".\nKontrollo drejtshkrimin.",
        "nouserspecified": "Duhesh me dhânë nji nofkë.",
        "login-userblocked": "Ky përdorues është bllokuar. Identifikohu nuk lejohet",
        "accmailtext": "Nji fjalëkalim i krijuem rastësisht për [[User talk:$1|$1]] u dërgue në $2.\n\nFjalëkalimi për këtë llogari mundet me u ndryshue në faqen ''[[Special:ChangePassword|ndrysho fjalëkalimin]]'' mbas kyçjes.",
        "newarticle": "(I ri)",
        "newarticletext": "Ke ndjekë nji vegëz për te nji faqe që nuk ekziston.\nMe krijue kët faqe, shkruej në kutinë ma poshtë (shih [$1 faqen e ndihmës] për ma shum udhzime).\nNëse ke hy këtu gabimisht, klikoje sustën '''mbrapa''' në shfletues.",
-       "anontalkpagetext": "<em>Kjo është faqe diskutimi e një përdoruesi anonim, i cili nuk ka krijuar llogari, apo nuk e përdor atë.</em>\nPrandaj ne do të përdorim adresën numerike të IP së tij për ta identifikuar.\nAdresa IP mund të shfrytëzohet prej disa përdoruesve.\nNëse jeni përdorues anonim dhe keni përshtypjen se po ju drejtohen komente jorelevante, ju lutemi [[Special:UserLogin/signup|krijoni një llogari]] apo [[Special:UserLogin|identifikohuni]] për të ju shmangur ngatërrimev me përdorues të tjerë anonim.",
+       "anontalkpagetext": "<em>Kjo është faqe diskutimi e një përdoruesi anonim, i cili nuk ka krijuar llogari, apo nuk e përdor atë.</em>\nPrandaj ne do të përdorim adresën numerike të IP së tij për ta identifikuar.\nAdresa IP mund të shfrytëzohet prej disa përdoruesve.\nNëse jeni përdorues anonim dhe keni përshtypjen se po ju drejtohen komente jorelevante, ju lutemi [[Special:CreateAccount|krijoni një llogari]] apo [[Special:UserLogin|identifikohuni]] për të ju shmangur ngatërrimev me përdorues të tjerë anonim.",
        "noarticletext": "Momentalisht nuk ka tekst në këtë faqe.\nJu mundeni [[Special:Search/{{PAGENAME}}|me kërkue këtë titull]] në faqe tjera,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} me kërkue në regjistrat tematikisht të afërm],\napo [{{fullurl:{{FULLPAGENAME}}|action=edit}} me redaktue këtë faqe]</span>.",
        "userpage-userdoesnotexist": "Llogaria e përdoruesit \"<nowiki>$1</nowiki>\" nuk âsht regjistrue.\nJu lutemi kontrolloni nëse doni me krijue/redaktue këtë faqe.",
        "clearyourcache": "'''Shenim - Mbas ruejtjes, ka mundësi që duheni me shmângë memorizimin në cache për me i pâ ndryshimet.'''\n'''Mozilla / Firefox / Safari:''' mbani ''Shift'' tue klikue në ''Reload'', ose trusni ''Ctrl-F5'' ose ''Ctrl-R'' (''Command-R'' në Mac);\n'''Konqueror: '''klikoni ''Reload'' ose trusni ''F5'';\n'''Opera:''' fshini cachein në ''Tools → Preferences'';\n'''Internet Explorer:''' mbani ''Ctrl'' tue klikue në ''Refresh,'' ose trusni ''Ctrl-F5''.",
index 58c4a1a..65400e2 100644 (file)
        "noname": "የተወሰነው ብዕር ስም ትክክለኛ አይደለም።",
        "loginsuccesstitle": "መግባትዎ ተከናወነ!",
        "loginsuccess": "እንደ «$1» ሆነው አሁን {{SITENAME}}ን ገብተዋል።",
-       "nosuchuser": "«$1» የሚል ብዕር ስም አልተገኘም። አጻጻፉን ይመልከቱ ወይም [[Special:UserLogin/signup|አዲስ ብዕር ስም ያውጡ]]።",
+       "nosuchuser": "«$1» የሚል ብዕር ስም አልተገኘም። አጻጻፉን ይመልከቱ ወይም [[Special:CreateAccount|አዲስ ብዕር ስም ያውጡ]]።",
        "nosuchusershort": "«$1» የሚል ብዕር ስም አልተገኘም። አጻጻፉን ይመልከቱ።",
        "nouserspecified": "አንድ ብዕር ስም መጠቆም ያስፈልጋል።",
        "login-userblocked": "ተጠቃሚው አሁን የታገደ ነው። መግባት አልተፈቀደም።",
index 0af7f45..05000b1 100644 (file)
        "noname": "No ha escrito un nombre d'usuario correcto.",
        "loginsuccesstitle": "S'ha identificato correctament",
        "loginsuccess": "Ha encetato una sesión en {{SITENAME}} como \"$1\".",
-       "nosuchuser": "No bi ha garra usuario clamato \"$1\".\nOs nombres d'usuario son sensibles a las mayusclas.\nComprebe si ha escrito bien o nombre u [[Special:UserLogin/signup|creye una nueva cuenta d'usuario]].",
+       "nosuchuser": "No bi ha garra usuario clamato \"$1\".\nOs nombres d'usuario son sensibles a las mayusclas.\nComprebe si ha escrito bien o nombre u [[Special:CreateAccount|creye una nueva cuenta d'usuario]].",
        "nosuchusershort": "No bi ha garra usuario con o nombre \"$1\". Comprebe si o nombre ye bien escrito.",
        "nouserspecified": "Ha d'escribir un nombre d'usuario.",
        "login-userblocked": "Iste usuario ye bloqueyau. No se permite l'inicio de sesión.",
        "accmailtext": "S'ha ninviato a $2 una clau ta [[User talk:$1|$1]] chenerata aliatoriament.\n\nA clau ta ista nueva cuenta la puet cambiar en a pachina ''[[Special:ChangePassword|Cambiar a clau]]'' dimpués d'haber dentrato en ella.",
        "newarticle": "(Nuevo)",
        "newarticletext": "Ha siguito un vinclo ta una pachina que encara no existe.\nTa creyar a pachina, prencipie a escribir en a caixa d'abaixo (mire-se l'[$1 aduya] ta más información).\nSi ye plegau por error, punche o botón \"enta zaga\" d'o suyo navegador.",
-       "anontalkpagetext": "----''Ista ye a pachina de descusión d'un usuario anonimo que encara no ha creyato una cuenta, u no l'ha feito servir. Por ixo, hemos d'emplegar a suya adreza IP ta identificar-lo/a.\nDiferents usuarios pueden compartir una mesma adreza IP.\nSi vusté ye un usuario anonimo y creye que l'han escrito comentarios no relevants, [[Special:UserLogin/signup|creye una cuenta]] u [[Special:UserLogin/signup|identifique-se]] ta privar confusions futuras con atros usuarios anonimos.''",
+       "anontalkpagetext": "----''Ista ye a pachina de descusión d'un usuario anonimo que encara no ha creyato una cuenta, u no l'ha feito servir. Por ixo, hemos d'emplegar a suya adreza IP ta identificar-lo/a.\nDiferents usuarios pueden compartir una mesma adreza IP.\nSi vusté ye un usuario anonimo y creye que l'han escrito comentarios no relevants, [[Special:CreateAccount|creye una cuenta]] u [[Special:CreateAccount|identifique-se]] ta privar confusions futuras con atros usuarios anonimos.''",
        "noarticletext": "Por agora no bi ha garra texto en ista pachina. Puet [[Special:Search/{{PAGENAME}}|mirar o títol d'ista pachina]] en atras pachinas, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mirar os rechistros relacionatos] u [{{fullurl:{{FULLPAGENAME}}|action=edit}} escribir ista pachina]</span>.",
        "noarticletext-nopermission": "Por l'inte no i hai garra texto en ista pachina.\nPuede [[Special:Search/{{PAGENAME}}|mirar iste titol]] en atras pachinas,\nu bien <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mirar en os rechistros relacionatos]</span>, pero no tien permiso ta creyar ista pachina.",
        "userpage-userdoesnotexist": "A cuenta d'usuario \"<nowiki>$1</nowiki>\" no ye rechistrada. Piense si quiere creyar u editar ista pachina.",
index 016af24..b414d5e 100644 (file)
        "noname": "Þū nafast gewriten gengne brūcendes naman.",
        "loginsuccesstitle": "Inmeldung gesǣlde",
        "loginsuccess": "'''Þu eart nū inmeldod tō {{SITENAME}} tō \"$1\".'''",
-       "nosuchuser": "Þǣr nis nān brūcend þe hæfþ þone naman \"$1\".\nStafena micelnessa sind hefiga and ānlica on brūcendnamum.\nScēawa þīne wrītunge eft, oþþe [[Special:UserLogin/signup|sciepp nīwe reccinge]].",
+       "nosuchuser": "Þǣr nis nān brūcend þe hæfþ þone naman \"$1\".\nStafena micelnessa sind hefiga and ānlica on brūcendnamum.\nScēawa þīne wrītunge eft, oþþe [[Special:CreateAccount|sciepp nīwe reccinge]].",
        "nosuchusershort": "Þǣr nis nān brūcend mid þǣm naman \"$1\".  Scēawa þīne wrītunge.",
        "nouserspecified": "Þū scealt wrītan brūcendes naman.",
        "login-userblocked": "Þes brūcend is fortȳned. Inmeldung nis gelīfed.",
index ab9421f..9755531 100644 (file)
        "noname": "لم تحدد اسم مستخدم صحيح.",
        "loginsuccesstitle": "تم الدخول",
        "loginsuccess": "'''لقد سجلت الدخول ل{{SITENAME}} باسم \"$1\".'''",
-       "nosuchuser": "لا يوجد مستخدم بالاسم \"$1\".\nأسماء المستخدمين حساسة لحالة الحروف.\nتأكد من إملاء الاسم، أو [[Special:UserLogin/signup|قم بإنشاء حساب جديد]].",
+       "nosuchuser": "لا يوجد مستخدم بالاسم \"$1\".\nأسماء المستخدمين حساسة لحالة الحروف.\nتأكد من إملاء الاسم، أو [[Special:CreateAccount|قم بإنشاء حساب جديد]].",
        "nosuchusershort": "لا يوجد مستخدم باسم $1\".\nتأكد من إملاء الاسم.",
        "nouserspecified": "يجب عليك تحديد اسم مستخدم.",
        "login-userblocked": "هذا المستخدم ممنوع. لا يسمح بالولوج.",
        "accmailtext": "أُرسِلت كلمة سر مولدة عشوائيا ل[[User talk:$1|$1]] إلى $2. يمكن تغييرها في صفحة ''[[Special:ChangePassword|تغيير كلمة السر]]'' بعد تسجيل الدخول.",
        "newarticle": "(جديد)",
        "newarticletext": "لقد تبعت وصلة لصفحة لم يتم إنشائها بعد.\nلإنشاء هذه الصفحة ابدأ الكتابة في الصندوق بالأسفل (انظر في [$1 صفحة المساعدة] للمزيد من المعلومات).\nإذا كانت زيارتك لهذه الصفحة بالخطأ، اضغط على زر ''رجوع'' في متصفح الإنترنت لديك.",
-       "anontalkpagetext": "----''هذه صفحة نقاش لمستخدم مجهول لم يقم بإنشاء حساب بعد أو لا يستعمل ذلك الحساب.\nلذا فيجب علينا استعمال رقم الأيبي للتعرف عليه/عليها.\nمثل هذا العنوان يمكن أن يشترك فيه عدة مستخدمين.\nلو كنت مستخدما مجهولا وتشعر بأن تعليقات لا تخصك تم توجيهها إليك، من فضلك [[Special:UserLogin/signup|أنشئ حسابا]] أو [[Special:UserLogin|سجل الدخول]] لتجنب الارتباك المستقبلي مع مستخدمين مجهولين آخرين.''",
-       "noarticletext": "لا يوجد حاليا أي نص في هذه الصفحة.\nيمكنك [[Special:Search/{{PAGENAME}}|البحث عن عنوان هذه الصفحة]] في الصفحات الأخرى،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} البحث في السجلات المتعلقة]،\nأو [{{fullurl:{{FULLPAGENAME}}|action=edit}} تعديل هذه الصفحة]</span>.",
+       "anontalkpagetext": "----''هذه صفحة نقاش لمستخدم مجهول لم يقم بإنشاء حساب بعد أو لا يستعمل ذلك الحساب.\nلذا فيجب علينا استعمال رقم الأيبي للتعرف عليه/عليها.\nمثل هذا العنوان يمكن أن يشترك فيه عدة مستخدمين.\nلو كنت مستخدما مجهولا وتشعر بأن تعليقات لا تخصك تم توجيهها إليك، من فضلك [[Special:CreateAccount|أنشئ حسابا]] أو [[Special:UserLogin|سجل الدخول]] لتجنب الارتباك المستقبلي مع مستخدمين مجهولين آخرين.''",
+       "noarticletext": "الصفحة خالية. يمكنك [[Special:Search/{{PAGENAME}}|البحث عن عنوانها]] في الصفحات الأخرى أو\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} البحث في السجلات] (لتعرف إذا حذفت)،\nأو '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} إنشاؤها بنفسك]'''</span>.",
        "noarticletext-nopermission": "لا يوجد حاليا أي نص في هذه الصفحة.\nيمكنك [[Special:Search/{{PAGENAME}}|البحث عن عنوان هذه الصفحة]] في الصفحات الأخرى، أو <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} البحث في السجلات المتعلقة بها]</span>، لكنك لست مخولاً لإنشاء هذه الصفحة.",
        "missing-revision": "المراجعة #$1 من الصفحة المسماة \"{{FULLPAGENAME}}\" غير موجودة.\n\nهذا يحدث عادة عن طريق اتباع وصلة تاريخ قديمة لصفحة تم حذفها.\nالتفاصيل يمكن إيجادها في [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} سجل الحذف].",
        "userpage-userdoesnotexist": "حساب المستخدم \"<nowiki>$1</nowiki>\" غير مسجل.\nمن فضلك تأكد أنك تريد إنشاء/تعديل هذه الصفحة.",
        "log-action-filter-delete-event": "حذف السجلات",
        "log-action-filter-delete-revision": "حذف المراجعات",
        "log-action-filter-import-interwiki": "استيراد عابر للويكي",
+       "log-action-filter-managetags-create": "إنشاء الوسوم",
+       "log-action-filter-managetags-delete": "حذف الوسوم",
        "log-action-filter-newusers-autocreate": "إنشاء آلي",
        "log-action-filter-newusers-byemail": "الإنشاء بكلمة مرور مرسلة عبر البريد الإلكتروني",
        "log-action-filter-protect-protect": "حماية",
index 354c74b..44a3679 100644 (file)
        "noname": "ما مدّيتش سميّت` مستعملي مقبولة.",
        "loginsuccesstitle": "التوصال راه نجَح.",
        "loginsuccess": "<strong>راك مسجّل داخل ف {{SITENAME}} ب`السميّة \"$1\".</strong>",
-       "nosuchuser": "ما كاين حتا مستعملي ب`السميّة \"$1\".\nالسميّة تاع المستعملي راه حسّاسة ف تكسار الحروف (majuscule - minuscule).\nعاود أكّد على كيفاش كتبت الكلمات ولا [[Special:UserLogin/signup|اخلق حساب جديد]].",
+       "nosuchuser": "ما كاين حتا مستعملي ب`السميّة \"$1\".\nالسميّة تاع المستعملي راه حسّاسة ف تكسار الحروف (majuscule - minuscule).\nعاود أكّد على كيفاش كتبت الكلمات ولا [[Special:CreateAccount|اخلق حساب جديد]].",
        "nosuchusershort": "ما كاين حتا مستعملي ب` السميّة \"$1\".\nأكّد على الكتيبة تاعك.",
        "nouserspecified": "لازم لك تمدّ السميّة تاع المستعملي.",
        "login-userblocked": "هاد السميّة تاع المستعملي راهي مطرودة. تسجال` الدخول ماشي مسموح.",
        "accmailtext": "راهي انخلقت كلمت` سرّ مختارة على الزهَر لل مستعملي [[User talk:$1|$1]] و و انبعتت ل $2.\nتنجم تبدّلها فل پاجة<em>[[Special:ChangePassword|بدّل كلمت` السرّ]]</em> كي تتسجّل داخل.",
        "newarticle": "(جديد)",
        "newarticletext": "راك تبعت وصيلة لباجه لم ما تخدمتش بعد.\nباش تصنع هاذ الباجه ابدا الكتبه فالصندوق التحت (شوف في [$1  زياده باجه المساعده] لمعلومات).\nإذا كانت زيارتك لهاذ الباجه غلطه، ادرك على بوطون''ولى'' في نافيقاتور الإنترنت نتاعك.",
-       "anontalkpagetext": "----''هاذ الباجة نقاش لمستخدم مجهول ما قامش بإنشاء حساب بعد و الا ما يستعملش ذاك الحساب.\nلذا لازم علينا استعمال رقم الأيبي باش نتعرفو عليه/عليها.\nمثل هذا العنوان يمكنلو يشترك فيه مستخدمين بزاف.\nإذا كنت مستخدم مجهول وتشعر بلي التعليقات ما تخصكش وصلتلك ، من فضلك [[Special:UserLogin/signup|أصنع حساب]] ولا [[Special:UserLogin|سجل الدخول]] باش تتجنب الارتباك فالمستقبل مع مستخدمين مجهولين آخرين.''",
+       "anontalkpagetext": "----''هاذ الباجة نقاش لمستخدم مجهول ما قامش بإنشاء حساب بعد و الا ما يستعملش ذاك الحساب.\nلذا لازم علينا استعمال رقم الأيبي باش نتعرفو عليه/عليها.\nمثل هذا العنوان يمكنلو يشترك فيه مستخدمين بزاف.\nإذا كنت مستخدم مجهول وتشعر بلي التعليقات ما تخصكش وصلتلك ، من فضلك [[Special:CreateAccount|أصنع حساب]] ولا [[Special:UserLogin|سجل الدخول]] باش تتجنب الارتباك فالمستقبل مع مستخدمين مجهولين آخرين.''",
        "noarticletext": "حتا لضركا، ما كاين حتا نصّ ف هاذ الپاجة.\nتقدرو [[Special:Search/{{PAGENAME}}ترميو تفتيشة على هاذ العلوان]] فل پاجات لخرين,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} فتش فل عمليات المربوطة]\n ولا[{{fullurl:{{FULLPAGENAME}}|action=edit}} اصنع هاذ الپاجة]</span>.",
        "noarticletext-nopermission": "لحد الساعه ما كانش حتى نص في هاذ الباجه.\nتقدرو [[Special:Search/{{PAGENAME}}|ترميو تفتيشه على هاذ العنوان]] فالباجات لخرين,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} فتش فالعمليات المربوطه]\nو إلا[{{fullurl:{{FULLPAGENAME}}|action=edit}} أصنع هاذ الباجه]</span>.",
        "missing-revision": "المراجعة #$1 من الباجة اللي سموها \"{{FULLPAGENAME}}\" ما هيش كاينة.\n\nهذا يصرا فالعادة منين نتبعو وصيلة تاريخها قديم لباجة تنحات.\nالتفاصيل يمكن نصيبوها في [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} سجل المسح].",
index c996b3f..e9c1986 100644 (file)
        "noname": "Ma kṫebṫiċ ċi smiyṫ l-mosṫeĥdim mqadda.",
        "loginsuccesstitle": "dkhlti mzyan",
        "loginsuccess": "Dĥelṫi mezyan fe {{SITENAME}} be smiyṫ \"$1\".",
-       "nosuchuser": "ma kayn-ċ ċi mosṫĥdim smiṫo \"$1\".\ns-smiyyaṫ l-mosṫĥdimin raha ḫssasa lla caz (yĝni majuscule o-minuscule).\nċof l-kṫaḅa waċ hia hadik, wlla [[Special:UserLogin/signup|ṣayb ċi ḫisab jdid]].",
+       "nosuchuser": "ma kayn-ċ ċi mosṫĥdim smiṫo \"$1\".\ns-smiyyaṫ l-mosṫĥdimin raha ḫssasa lla caz (yĝni majuscule o-minuscule).\nċof l-kṫaḅa waċ hia hadik, wlla [[Special:CreateAccount|ṣayb ċi ḫisab jdid]].",
        "nosuchusershort": "ma kayn-ċ ċi mosṫĥdim smiṫo \"$1\".\nċof l-kṫaḅa waċ hia hadik.",
        "nouserspecified": "khassk tdkhl ism lmostakhdim.",
        "login-userblocked": "Had l-mosṫeĥdim meḫbos. Konnéksyon memnoĝa.",
        "accmailtext": "waḫd klmṫ s-srr mṣayba ĝċwa'iyyn ĝla ḫsab [[User talk:$1|$1]] ṫsiftaṫ l-$2.\n\nṫqdr ṫbddal klmṫ s-srr dialt had l-ḫisab j-jdid f-ṣṣfḫa ṫaĝ ''[[Special:ChangePassword|bḍḍel klmṫ s-srr]]'' mn bĝdmma ṫdĥol.",
        "newarticle": "(jdid)",
        "newarticletext": "Ṫbeĝṫi waḫed l-lyan li kayddi le waḫed ṣ-ṣefḫa li ṫṫemḫaṫ.\nBaċ ṫsayeb had ṣ-ṣefḫa, bda ṫekṫeb fe ṣ-ṣendoq li l-ṫeḫṫ (ċof ila bġiṫi [$1 ṣ-ṣefḫa de l-mosaĝada] le l-mazid de l-meĝlomaṫ).\nIla wṣelṫi hnaya ĝla ġefla, brek ĝla l-boton '''rjeĝ''' dyal n-navigaṫør internet dyalek.",
-       "anontalkpagetext": "----''hada niqaċ ṫaĝ waḫd l-mosṫĥdim anonim lli mazal ma ṣayb-ċ ċi ḫisab, wlla ma kayĥdam-ċ bih.\ndakċċi ĝlaċ raḫna ĥddamin b-ĝonwan l-IP ṛ-ṛqmi baċ nĝaṛṛfoh.\nbḫal had ĝanawin l-IP ymkn iṫċarko fiha bẓẓaf dl-mosṫĥdimin.\nina konṫi ḫṫṫa nṫa mosṫĥdim anonim o-ḫssiṫi billa wjjah lik ċi ḫadd ċi ṫĝalq ġalat,[[Special:UserLogin/signup|ṣayb ċi ḫisab]] wlla [[Special:UserLogin|dĥol]] baċ ṫqil raṣk mn ay ġalat fl-mosṫqbal.''",
+       "anontalkpagetext": "----''hada niqaċ ṫaĝ waḫd l-mosṫĥdim anonim lli mazal ma ṣayb-ċ ċi ḫisab, wlla ma kayĥdam-ċ bih.\ndakċċi ĝlaċ raḫna ĥddamin b-ĝonwan l-IP ṛ-ṛqmi baċ nĝaṛṛfoh.\nbḫal had ĝanawin l-IP ymkn iṫċarko fiha bẓẓaf dl-mosṫĥdimin.\nina konṫi ḫṫṫa nṫa mosṫĥdim anonim o-ḫssiṫi billa wjjah lik ċi ḫadd ċi ṫĝalq ġalat,[[Special:CreateAccount|ṣayb ċi ḫisab]] wlla [[Special:UserLogin|dĥol]] baċ ṫqil raṣk mn ay ġalat fl-mosṫqbal.''",
        "noarticletext": "Ma kayen fe had s-saĝa ḫṫa neṣ fe had ṣ-ṣefḫa.\nImken lek [[Special:Search/{{PAGENAME}}|ṫqelleb ĝla ṣefḫa be had l-ĝonwan]] fe ṣ-ṣefḫaṫ l-ĥrin,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ṫqelleb fe l-ĝamaliyaṫ l-mlaqyin]\nola [{{fullurl:{{FULLPAGENAME}}|action=edit}} ṫsayeb ṣ-ṣefḫa]</span>.",
        "noarticletext-nopermission": "Ma kayen fe had s-saĝa ḫṫa neṣ fe had ṣ-ṣefḫa.\nImken lek [[Special:Search/{{PAGENAME}}|ṫqelleb ĝla ṣefḫa be had l-ĝonwan]] fe ṣ-ṣefḫaṫ l-ĥrin,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ṫqelleb fe l-ĝamaliyaṫ l-mlaqyin]\nola [{{fullurl:{{FULLPAGENAME}}|action=edit}} ṫsayeb ṣ-ṣefḫa]</span>.",
        "userpage-userdoesnotexist": "ḫisab l-mosṫĥdim \"$1\" ma msjjal-ċ.\nċof waċ baġi ṫṣayb/ṫĝddal had ṣfḫa.",
index d1addb6..3138245 100644 (file)
        "noname": "انت ما حددتش اسم يوزر صحيح.",
        "loginsuccesstitle": "تم الدخول بشكل صحيح",
        "loginsuccess": "'''دخولك   {{SITENAME}} إتسجل بإسم \"$1\".'''",
-       "nosuchuser": "مافيش يوزر اسمه \"$1\".\nاسامى اليوزر بتبقى حساسه لحالة الحرف.\nاتأكد من التهجيه, او [[Special:UserLogin/signup|افتح حساب جديد]].",
+       "nosuchuser": "مافيش يوزر اسمه \"$1\".\nاسامى اليوزر بتبقى حساسه لحالة الحرف.\nاتأكد من التهجيه, او [[Special:CreateAccount|افتح حساب جديد]].",
        "nosuchusershort": "مافيش يوزر باسم $1\".\nاتاكد من تهجية الاسم.",
        "nouserspecified": "لازم تحدد اسم يوزر.",
        "login-userblocked": "اليوزر دا ممنوع من الدخول.",
        "accmailtext": "الباسورد العشوائيه اللى اتعملت لـ[[User talk:$1|$1]]  اتبعتت لـ $2.\n\nالباسورد بتاعة الحساب الجديد دا ممكن تتغير فى صفحة ''[[Special:ChangePassword|تغيير الباسورد]]''   بعد  تسجيل الدخول.",
        "newarticle": "(جديد)",
        "newarticletext": "انت وصلت لصفحه مابتدتش لسه.\nعلشان  تبتدى الصفحة ابتدى الكتابه فى الصندوق اللى تحت.\n(بص على [$1 صفحة المساعده] علشان معلومات اكتر)\nلو كانت زيارتك للصفحه دى بالغلط، دوس على زرار ''رجوع'' فى متصفح الإنترنت عندك.",
-       "anontalkpagetext": "----'' صفحة النقاش دى بتاعة يوزر مجهول لسة ما فتحش لنفسه حساب أو عنده واحد بس ما بيستعملوش.\nعلشان كدا لازم تستعمل رقم الأيبى علشان تتعرف عليه/عليها.\nالعنوان دا ممكن اكتر من واحد يكونو بيستعملوه.\nلو انت يوزر مجهول و حاسس  ان فى تعليقات بتتوجهلك مع انك مالكش دعوة بيها، من فضلك [[Special:UserLogin/signup|افتحلك حساب]] أو [[Special:UserLogin|سجل الدخول]] علشان تتجنب اللخبطة اللى ممكن تحصل فى المستقبل مع يوزرز مجهولين تانيين.''",
+       "anontalkpagetext": "----'' صفحة النقاش دى بتاعة يوزر مجهول لسة ما فتحش لنفسه حساب أو عنده واحد بس ما بيستعملوش.\nعلشان كدا لازم تستعمل رقم الأيبى علشان تتعرف عليه/عليها.\nالعنوان دا ممكن اكتر من واحد يكونو بيستعملوه.\nلو انت يوزر مجهول و حاسس  ان فى تعليقات بتتوجهلك مع انك مالكش دعوة بيها، من فضلك [[Special:CreateAccount|افتحلك حساب]] أو [[Special:UserLogin|سجل الدخول]] علشان تتجنب اللخبطة اللى ممكن تحصل فى المستقبل مع يوزرز مجهولين تانيين.''",
        "noarticletext": "مافيش دلوقتى اى نص فى الصفحه دى.\nممكن [[Special:Search/{{PAGENAME}}|تدور على عنوان الصفحه دى]] فى صفح تانيه,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} تدور فى السجلات اللى ليها علاقه],\nاو [{{fullurl:{{FULLPAGENAME}}|action=edit}} تعدل الصفحه دى]</span>.",
        "noarticletext-nopermission": "مافيش دلوقتى اى نص فى الصفحه دى.\nممكن [[Special:Search/{{PAGENAME}}|تدور على عنوان الصفحه دى]] فى صفح تانيه,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} تدور فى السجلات اللى ليها علاقه],\nاو [{{fullurl:{{FULLPAGENAME}}|action=edit}} تعدل الصفحه دى]</span>.",
        "userpage-userdoesnotexist": "حساب اليوزر \"<nowiki>$1</nowiki>\" مش متسجل. لو سمحت تشوف لو عايز تبتدي/تعدل الصفحة دي.",
index a57dbea..7a33b8c 100644 (file)
        "noname": "আপুনি বৈধ সদস্যনাম এটা দিয়া নাই।",
        "loginsuccesstitle": "প্ৰৱেশ অনুমোদিত হ'ল",
        "loginsuccess": "''' আপুনি {{SITENAME}}ত \"$1\" নামেৰে প্ৰৱেশ কৰিলে '''",
-       "nosuchuser": "\"$1\" নামৰ কোনো সদস্য নাই।\nসদস্য নাম আকাৰ সংবেদনশীল।\nআপোনাৰ বানানতো চাওক, বা  [[Special:UserLogin/signup|নতুন সদস্যভুক্তি কৰক]]।",
+       "nosuchuser": "\"$1\" নামৰ কোনো সদস্য নাই।\nসদস্য নাম আকাৰ সংবেদনশীল।\nআপোনাৰ বানানতো চাওক, বা  [[Special:CreateAccount|নতুন সদস্যভুক্তি কৰক]]।",
        "nosuchusershort": "\"$1\" এই নামৰ কোনো সদস্য নাই ।\nবানানতো আকৌ এবাৰ ভালদৰে চাওক ।",
        "nouserspecified": "সদস্যনাম দিয়া বাধ্যতামূলক।",
        "login-userblocked": "এই সদস্যক নিষেধ কৰা হৈছে। প্ৰৱেশ অসম্ভৱ।",
        "accmailtext": "[[User talk:$1|$1]]-ৰ কাৰণে যাদৃচ্ছিকভাৱে উৎপন্ন কৰা গুপ্তশব্দ $2লৈ পঠোৱা হ'ল । \nএই নতুন একাউন্টত প্ৰৱেশ কৰি ''[[Special:ChangePassword|গুপ্তশব্দ সলনি কৰক]]'' পৃষ্ঠাখনত শব্দতো সলনি কৰি ল’ব পাৰিব ।",
        "newarticle": "(নতুন)",
        "newarticletext": "আপুনি বিচৰা প্ৰবন্ধটো বিচাৰি পোৱা নগ'ল।\n\nইচ্ছা কৰিলে আপুনিয়েই এই প্ৰবন্ধটো লিখা আৰম্ভ কৰিব পাৰে। [$1 ইয়াত] সহায় পাব।\n\nআপুনি যদি ইয়ালৈ ভুলতে আহিছে, তেনেহলে আপোনাৰ ব্ৰাওজাৰৰ '''BACK''' বুটামত টিপা মাৰক।",
-       "anontalkpagetext": "----''এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট  সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ \n[[Special:UserLogin/signup|একাউন্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]] ।''",
+       "anontalkpagetext": "----''এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট  সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ \n[[Special:CreateAccount|একাউন্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]] ।''",
        "noarticletext": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই ।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}| এই শিৰোনামা সন্ধান কৰিব পাৰে]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে],\nবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পৃষ্ঠা সম্পাদনা কৰিব পাৰে]</span>",
        "noarticletext-nopermission": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\nবা <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে]</span>, কিন্তু এই পৃষ্ঠা সৃষ্টি কৰিবলৈ আপোনাৰ অনুমতি নাই।",
        "missing-revision": "\"{{FULLPAGENAME}}\" নামৰ পৃষ্ঠাৰ #$1 সংশোধনৰ অস্তিত্ব নাই।\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন ল'গ]ত অধিক তথ্য পাব।",
index 10feac9..a8ab056 100644 (file)
        "noname": "Nun conseñasti un nome d'usuariu válidu.",
        "loginsuccesstitle": "Identificáu",
        "loginsuccess": "'''Aniciasti sesión en {{SITENAME}} como «$1».'''",
-       "nosuchuser": "Nun hai nengún usuariu col nome «$1».\nLos nomes d'usuariu distinguen mayúscules y minúscules.\nMira que tea bien escritu o [[Special:UserLogin/signup|crea una cuenta nueva]].",
+       "nosuchuser": "Nun hai nengún usuariu col nome «$1».\nLos nomes d'usuariu distinguen mayúscules y minúscules.\nMira que tea bien escritu o [[Special:CreateAccount|crea una cuenta nueva]].",
        "nosuchusershort": "Nun hai nengún usuariu col nome «$1».\nMira que tea bien escritu.",
        "nouserspecified": "Has d'especificar un nome d'usuariu.",
        "login-userblocked": "Esti usuariu ta bloquiáu. Nun se permite l'aniciu de sesión.",
        "accmailtext": "Unvióse a $2 una contraseña xenerada al debalu pal usuariu [[User talk:$1|$1]]. Pue camudase na páxina ''[[Special:ChangePassword|camudar contraseña]]'' depués d'aniciar sesión.",
        "newarticle": "(Nuevu)",
        "newarticletext": "Siguisti un enllaz a un artículu qu'inda nun esiste.\nPa crear la páxina, empecipia a escribir nel cuadru d'embaxo (mira la [$1 páxina d'ayuda] pa más información).\nSi llegasti equí por enquivocu, calca nel botón <strong>atrás</strong> del to restolador.",
-       "anontalkpagetext": "----\n''Esta ye la páxina d'alderique pa un usuariu anónimu qu'inda nun creó una cuenta o que nun la usa.''\nPola mor d'ello ha usase la direición numbérica IP pa identificalu/la.\nTala IP pue compartise por varios usuarios.\nSi yes un usuariu anónimu y notes qu'hai comentarios irrelevantes empobinaos pa ti, por favor [[Special:UserLogin/signup|crea una cuenta]] o [[Special:UserLogin/signup|identifícate]] pa torgar futures confusiones con otros usuarios anónimos.",
+       "anontalkpagetext": "----\n''Esta ye la páxina d'alderique pa un usuariu anónimu qu'inda nun creó una cuenta o que nun la usa.''\nPola mor d'ello ha usase la direición numbérica IP pa identificalu/la.\nTala IP pue compartise por varios usuarios.\nSi yes un usuariu anónimu y notes qu'hai comentarios irrelevantes empobinaos pa ti, por favor [[Special:CreateAccount|crea una cuenta]] o [[Special:CreateAccount|identifícate]] pa torgar futures confusiones con otros usuarios anónimos.",
        "noarticletext": "Nestos momentos nun hai testu nesta páxina.\nPuedes [[Special:Search/{{PAGENAME}}|buscar esti títulu de páxina]] n'otres páxines,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar los rexistros rellacionaos],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear esta páxina]</span>.",
        "noarticletext-nopermission": "Nestos momentos nun hai testu nesta páxina.\nPue [[Special:Search/{{PAGENAME}}|buscar esti títulu de páxina]] n'otres páxines o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar los rexistros rellacionaos]</span>, pero nun tiene permisu pa crear esta páxina.",
        "missing-revision": "La revisión #$1 de la páxina llamada \"{{FULLPAGENAME}}\" nun esiste.\n\nDe vezu la causa d'esto ye siguir un enllaz antiguu del historial a una páxina que se desanició.\nSe puen alcontrar más detalles nel [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rexistru de desanicios].",
        "right-override-export-depth": "Esportar páxines, incluyendo páxines enllazaes fasta una fondura de 5",
        "right-sendemail": "Unviar corréu a otros usuarios",
        "right-passwordreset": "Ver los correos de reestablecimientu de conseña",
-       "right-managechangetags": "Crear y desaniciar [[Special:Tags|etiquetes]] dende la base de datos",
+       "right-managechangetags": "Crear y (des)activar [[Special:Tags|etiquetes]]",
        "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",
+       "right-deletechangetags": "Desaniciar [[Special:Tags|etiquetes]] de la base de datos",
        "grant-generic": "Conxuntu de drechos «$1»",
        "grant-group-page-interaction": "Interactuar con páxines",
        "grant-group-file-interaction": "Interactuar con multimedia",
        "action-viewmyprivateinfo": "ver la so información privada",
        "action-editmyprivateinfo": "editar la so información privada",
        "action-editcontentmodel": "editar el modelu de conteníu d'una páxina",
-       "action-managechangetags": "crear y desaniciar etiquetes dende la base de datos",
+       "action-managechangetags": "crear y (des)activar etiquetes",
        "action-applychangetags": "aplicar etiquetes xunto colos cambios",
        "action-changetags": "amestar y desaniciar etiquetes arbitraries en revisiones individuales y entraes del rexistru",
+       "action-deletechangetags": "desaniciar etiquetes de la base de datos",
        "nchanges": "{{PLURAL:$1|un cambiu|$1 cambios}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|dende la última visita}}",
        "enhancedrc-history": "historial",
        "upload-form-label-own-work": "Esti ye'l mio propiu trabayu",
        "upload-form-label-infoform-categories": "Categoríes",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Confirmo que xubo esti ficheru siguiendo les condiciones de serviciu y les polítiques de llicencies de {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Si nun puedes xubir esti ficheru baxo les polítiques de {{SITENAME}}, zarra esti diálogu y prueba otru métodu.",
-       "upload-form-label-not-own-work-local-local": "Quiciabes quieras probar tamién [[Special:Upload|la páxina predeterminada de xubíes]].",
-       "upload-form-label-own-work-message-default": "Entiendo que toi xubiendo esti ficheru a un depósitu compartíu. Confirmo que toi faciéndolo cumpliendo les condiciones de serviciu y les polítiques de llicencies d'esi sitiu.",
-       "upload-form-label-not-own-work-message-default": "Si nun puedes xubir esti ficheru baxo les polítiques del depósitu compartíu, zarra esti diálogu y prueba otru métodu.",
-       "upload-form-label-not-own-work-local-default": "Tamién pué interesate usar [[Special:Upload|la páxina de carga de {{SITENAME}}]] si esti ficheru pué xubise allí baxo les sos polítiques.",
-       "upload-form-label-own-work-message-shared": "Certifico que tengo los drechos d'autor d'esti ficheru, y aceuto irrevocablemente lliberalu a Wikimedia Commons baxo la llicencia [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], y aceuto les [https://wikimediafoundation.org/wiki/Terms_of_Use Condiciones d'usu].",
-       "upload-form-label-not-own-work-message-shared": "Si nun tienes los drechos d'autor d'esti ficheru, o quieres lliberalu baxo una llicencia diferente, considera usar el [https://commons.wikimedia.org/wiki/Special:UploadWizard Asistente de carga en Commons Upload].",
-       "upload-form-label-not-own-work-local-shared": "Tamién pué interesate usar [[Special:Upload|la páxina de carga de {{SITENAME}}]] si esti ficheru pué xubise allí baxo les sos polítiques.",
+       "upload-form-label-own-work-message-generic-local": "Confirmo que xubo esti ficheru siguiendo les condiciones de serviciu y les polítiques de llicencies de {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Si nun puedes xubir esti ficheru baxo les polítiques de {{SITENAME}}, zarra esti diálogu y prueba otru métodu.",
+       "upload-form-label-not-own-work-local-generic-local": "Quiciabes quieras probar tamién [[Special:Upload|la páxina predeterminada de xubíes]].",
+       "upload-form-label-own-work-message-generic-foreign": "Entiendo que toi xubiendo esti ficheru a un depósitu compartíu. Confirmo que toi faciéndolo cumpliendo les condiciones de serviciu y les polítiques de llicencies d'esi sitiu.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Si nun puedes xubir esti ficheru baxo les polítiques del depósitu compartíu, zarra esti diálogu y prueba otru métodu.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Tamién pué interesate usar [[Special:Upload|la páxina de carga de {{SITENAME}}]] si esti ficheru pué xubise allí baxo les sos polítiques.",
        "backend-fail-stream": "Nun se pudo tresmitir el ficheru $1.",
        "backend-fail-backup": "Nun se pudo facer copia de seguridá del ficheru $1.",
        "backend-fail-notexists": "El ficheru $1 nun esiste.",
        "changecontentmodel-success-text": "Cambióse'l tipu de conteníu de [[:$1]].",
        "changecontentmodel-cannot-convert": "El conteníu de [[:$1]] nun puede convertise a un tipu de $2.",
        "changecontentmodel-nodirectediting": "El modelu de conteníu $1 nun tien encontu pa edición direuta",
+       "changecontentmodel-emptymodels-title": "Nun hai modelos de conteníu disponibles",
+       "changecontentmodel-emptymodels-text": "El conteníu de [[:$1]] nun pue convertise a nengún tipu.",
        "log-name-contentmodel": "Rexistru de cambios del modelu de conteníu",
        "log-description-contentmodel": "Socesos rellacionaos colos modelos de conteníu d'una páxina",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|creó}} la páxina $3 usando un modelu de conteníu non predetermináu «$5»",
        "whatlinkshere-prev": "{{PLURAL:$1|anterior|anteriores $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|siguiente|siguientes $1}}",
        "whatlinkshere-links": "← enllaces",
-       "whatlinkshere-hideredirs": "$1 redireiciones",
-       "whatlinkshere-hidetrans": "$1 tresclusiones",
-       "whatlinkshere-hidelinks": "$1 enllaces",
-       "whatlinkshere-hideimages": "$1 los enllaces al ficheru",
+       "whatlinkshere-hideredirs": "Anubrir redireiciones",
+       "whatlinkshere-hidetrans": "Anubrir tresclusiones",
+       "whatlinkshere-hidelinks": "Tapecer enllaces",
+       "whatlinkshere-hideimages": "Anubrir los enllaces al ficheru",
        "whatlinkshere-filters": "Peñeres",
        "whatlinkshere-submit": "Dir",
        "autoblockid": "Autobloquiar #$1",
        "lockdbsuccesstext": "Candóse la base de datos.\n<br />Alcuérdate de [[Special:UnlockDB|descandala]] depués d'acabar el so mantenimientu.",
        "unlockdbsuccesstext": "La base de datos foi descandada.",
        "lockfilenotwritable": "L'archivu de candáu de la base de datos nun ye escribible. Pa candar o descandar la base de datos esti tien que poder ser modificáu pol sirvidor.",
+       "databaselocked": "La base de datos yá ta bloquiada.",
        "databasenotlocked": "La base de datos nun ta candada.",
        "lockedbyandtime": "(por $1 el $2 a les $3)",
        "move-page": "Treslladar $1",
        "invalidateemail": "Encaboxar confirmación de corréu electrónicu",
        "notificationemail_subject_changed": "Camudó la dirección de corréu electrónicu rexistrada de {{SITENAME}}",
        "notificationemail_subject_removed": "Desanicióse la dirección de corréu electrónicu rexistrada de {{SITENAME}}",
+       "notificationemail_body_changed": "Daquién, probablemente tu, dende la dirección IP $1,\ncamudó la dirección de corréu electrónicu de la cuenta \"$2\" a \"$3\" en {{SITENAME}}.\n\nSi nun fuisti tu, comunícate darréu con un alministrador del sitiu.",
+       "notificationemail_body_removed": "Daquién, probablemente tu, dende la dirección IP $1,\ndesanició la dirección de corréu electrónicu de la cuenta \"$2\" en {{SITENAME}}.\n\nSi nun fuisti tu, comunícate darréu con un alministrador del sitiu.",
        "scarytranscludedisabled": "[La tresclusión interwiki ta desactivada]",
        "scarytranscludefailed": "[Falló la recuperación de la plantía pa $1]",
        "scarytranscludefailed-httpstatus": "[Falló la recuperación de la plantía pa $1: HTTP $2]",
        "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\".",
+       "restricted-displaytitle": "<strong>Atención:</strong> El títulu a amosar \"$1\" inoróse porque nun ye equivaliente al títulu real de la páxina.",
        "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.",
        "version": "Versión",
        "version-extensions": "Estensiones instalaes",
        "tags-delete-not-found": "La etiqueta «$1» nun esiste.",
        "tags-delete-too-many-uses": "La etiqueta «$1» aplícase a más {{PLURAL:$2|d'una revisión|de $2 revisiones}}, lo que quier dicir que nun pue desaniciase.",
        "tags-delete-warnings-after-delete": "Desanicióse la etiqueta «$1», pero {{PLURAL:$2|alcontróse'l siguiente avisu|alcontráronse los siguientes avisos}}:",
+       "tags-delete-no-permission": "Nun tienes permisu pa desaniciar etiquetes de cambiu.",
        "tags-activate-title": "Activar etiqueta",
        "tags-activate-question": "Tas a piques d'activar la etiqueta «$1».",
        "tags-activate-reason": "Motivu:",
        "feedback-useragent": "Axente d'usuariu:",
        "searchsuggest-search": "Buscar",
        "searchsuggest-containing": "que contien...",
+       "api-error-autoblocked": "La to dirección IP bloquióse automáticamente porque la usó un usuariu bloquiáu.",
        "api-error-badaccess-groups": "Nun tienes permisu pa xubir ficheros a esta wiki.",
        "api-error-badtoken": "Fallu internu: token incorreutu.",
+       "api-error-blocked": "Tas bloquiáu pa editar.",
        "api-error-copyuploaddisabled": "Xubir d'una URL ta desactivao nesti sirvidor.",
        "api-error-duplicate": "Yá hai {{PLURAL:$1|otru ficheru|otros ficheros}} nesti sitiu col mesmu conteníu.",
        "api-error-duplicate-archive": "Había {{PLURAL:$1|otru ficheru|otros ficheros}} nesti sitiu col mesmu conteníu, pero se {{PLURAL:$1|desanició|desaniciaron}}.",
        "api-error-nomodule": "Fallu internu: nun se configuró dengún módulu de xubíes.",
        "api-error-ok-but-empty": "Fallu internu: nun hai respuesta del sirvidor.",
        "api-error-overwrite": "Nun ta permitío sobroscribir un ficheru esistente.",
+       "api-error-ratelimited": "Tas tentando xubir más ficheros nun espaciu de tiempu más pequeñu del que permite esta wiki.\nTéntalo otra vuelta en dellos minutos.",
        "api-error-stashfailed": "Fallu internu: el sirvidor nun pudo guardar el ficheru temporal.",
        "api-error-publishfailed": "Fallu internu: el sirvidor nun pudo espublizar el ficheru temporal.",
        "api-error-stasherror": "Hebo un error al xubir el ficheru al almacén.",
        "sessionprovider-nocookies": "Les cookies puen tar desactivaes. Asegúrate de tener activaes les cookies y vuelve a principiar.",
        "randomrootpage": "Páxina raíz al debalu",
        "log-action-filter-block": "Tipu de bloquéu:",
+       "log-action-filter-contentmodel": "Tipu de cambéu de modelu de conteníu:",
        "log-action-filter-delete": "Tipu de desaniciu:",
+       "log-action-filter-import": "Tipu d'importación:",
+       "log-action-filter-managetags": "Tipu d'acción d'alministración d'etiquetes:",
+       "log-action-filter-move": "Tipu de movimientu:",
+       "log-action-filter-newusers": "Tipu de creación de cuenta:",
        "log-action-filter-patrol": "Tipu de patrulla:",
        "log-action-filter-protect": "Tipu de proteición:",
+       "log-action-filter-rights": "Tipu de cambéu de permisos",
+       "log-action-filter-suppress": "Tipu de supresión",
        "log-action-filter-upload": "Tipu de carga:",
        "log-action-filter-all": "Too",
        "log-action-filter-block-block": "Bloquéu",
        "log-action-filter-block-reblock": "Cambiu de bloquéu",
        "log-action-filter-block-unblock": "Desbloquéu",
+       "log-action-filter-contentmodel-change": "Cambéu de modelu de conteníu",
+       "log-action-filter-contentmodel-new": "Creación de páxina con modelu de conteníu non estándar",
        "log-action-filter-delete-delete": "Desaniciu de páxines",
        "log-action-filter-delete-restore": "Restauración de páxines",
        "log-action-filter-delete-event": "Desaniciu de rexistros",
        "log-action-filter-delete-revision": "Desaniciu de revisión",
+       "log-action-filter-import-interwiki": "Importación ente wikis",
+       "log-action-filter-import-upload": "Importar cargando XML",
+       "log-action-filter-managetags-create": "Creación d'etiquetes",
+       "log-action-filter-managetags-delete": "Desaniciu d'etiquetes",
+       "log-action-filter-managetags-activate": "Activación d'etiquetes",
+       "log-action-filter-managetags-deactivate": "Desactivación d'etiquetes",
+       "log-action-filter-move-move": "Treslladar ensin sobreescribir les redireiciones",
+       "log-action-filter-move-move_redir": "Treslladar sobreescribiendo les redireiciones",
+       "log-action-filter-newusers-create": "Creación por usuariu anónimu",
+       "log-action-filter-newusers-create2": "Creación por usuariu rexistráu",
+       "log-action-filter-newusers-autocreate": "Creación automática",
+       "log-action-filter-newusers-byemail": "Creación cola contraseña unviada per corréu",
        "log-action-filter-patrol-patrol": "Patrulláu manual",
        "log-action-filter-patrol-autopatrol": "Patrulláu automáticu",
        "log-action-filter-protect-protect": "Proteición",
        "log-action-filter-protect-modify": "Cambiu na proteición",
        "log-action-filter-protect-unprotect": "Desproteición",
+       "log-action-filter-protect-move_prot": "Proteición treslladada",
+       "log-action-filter-rights-rights": "Cambéu manual",
+       "log-action-filter-rights-autopromote": "Cambéu automáticu",
+       "log-action-filter-suppress-event": "Supresión de rexistru",
+       "log-action-filter-suppress-revision": "Supresión de revisión",
+       "log-action-filter-suppress-delete": "Supresión de páxina",
+       "log-action-filter-suppress-block": "Supresión d'usuariu por bloquéu",
+       "log-action-filter-suppress-reblock": "Supresión d'usuariu por rebloquéu",
        "log-action-filter-upload-upload": "Nueva carga",
        "log-action-filter-upload-overwrite": "Recargar"
 }
index 71e1639..5c10f27 100644 (file)
        "noname": "Va favesikyolt gobazel.",
        "loginsuccesstitle": "Pilkomodanhara.",
        "loginsuccess": "Rin wetce « $1 » moe {{SITENAME}} til dogluyarakiraf.",
-       "nosuchuser": "« $1 » favesik me krulder.\nSutera va favesikyolt gotir eltaykoranhafa.\nAgeltal va rinaf suteks oke [[Special:UserLogin/signup|pataredura]].",
+       "nosuchuser": "« $1 » favesik me krulder.\nSutera va favesikyolt gotir eltaykoranhafa.\nAgeltal va rinaf suteks oke [[Special:CreateAccount|pataredura]].",
        "nosuchusershort": "Me tir favesik digis va « $1 » yolt. Va sutekaks ageltal.",
        "nouserspecified": "Va favesikyolt gobazel !",
        "login-userblocked": "Bat webesik tir elekayan. Dogluyara menovena.",
index 8cab3c9..2ac5aac 100644 (file)
        "noname": "आप सही सदस्यनाम नाइ दिहा गा है।",
        "loginsuccesstitle": "लॉग इन होइ गवा",
        "loginsuccess": "'''आप {{SITENAME}} में \"$1\" सदस्यनाम से लॉग इन होई {{GENDER:$1|चुके|चुकी|चुके}} हव।'''",
-       "nosuchuser": "\"$1\" नावँ कय कवनो सदस्य नाइ है।\nसदस्यनावँ में लघु औ दीर्घ अक्षरन् से फ़रक परत है।\nआपन अक्षर जाँचा जाय, या [[Special:UserLogin/signup|नवाँ खाता खोला जाय]]।",
+       "nosuchuser": "\"$1\" नावँ कय कवनो सदस्य नाइ है।\nसदस्यनावँ में लघु औ दीर्घ अक्षरन् से फ़रक परत है।\nआपन अक्षर जाँचा जाय, या [[Special:CreateAccount|नवाँ खाता खोला जाय]]।",
        "nosuchusershort": "\"$1\" नावँ कय कवनो सदस्य नाई है।\nकृपया आपन शब्द फिरसे जाँचा जाय।",
        "nouserspecified": "सदस्यनावँ देब जरुरी है।",
        "login-userblocked": "ई सदस्य प्रतिबन्धित है। सत्रारम्भ कय अनुमति नाई है।",
        "accmailtext": "[[User talk:$1|$1]] कय लिए एक यंत्र जनित गुप्त कुंजी $2 कय भेज दिहा गा है। लॉगिन करेक बाद एका '''[[Special:ChangePassword|गुप्त कुंजी बदला जाय]]'' वाला पन्नन् पे बदल सका जात है।",
        "newarticle": "(नँवा)",
        "newarticletext": "आप अईसन पन्ना पे आवा गा है जवन अभीन तक नाई बनावा है।\nपन्ना बनावेक लिये नीचे कय बौक्स में पाठ लिखा जाय। ढेर जानकारी कय लिये [$1 सहायता पन्ना] देखा जाय।\nअगर आप हिँया गलती से आवा गा हैं तव आपन ब्राउज़र कय बैक ('''back''') बटन पे क्लिक करा जाय।",
-       "anontalkpagetext": "----''ई बातचीत पन्ना उ बेनामी सदस्यन् कय खर्तीन होय जे या तव खाता नाई खोलें है या खाता कय प्रयोग नाइ करत हैं।\nइहिकै नाते वय लोगन कय पहिचान कय खर्तीन हम्मन कय वय लोगन कय आइ॰पी ठहर कय प्रयोग करेक परत है।\nआइ॰पी ठहर कयु सदस्यन् कय एक्कय होइ सकत है।\nयदि आप कवनो बेनामी सदस्य होआ जाय अव आप कय लागत है कि आप कय बारे में अप्रासंगिक टीका टिप्पणी कई गा है तव कृपया [[Special:UserLogin/signup|सदस्यता लिहा जाय]] या [[Special:UserLogin|सत्रारंभ करा जाय]] ताकि अउर बेनामी सदस्यन् में से आप कय अलग से पहिचान सका जाय।''",
+       "anontalkpagetext": "----''ई बातचीत पन्ना उ बेनामी सदस्यन् कय खर्तीन होय जे या तव खाता नाई खोलें है या खाता कय प्रयोग नाइ करत हैं।\nइहिकै नाते वय लोगन कय पहिचान कय खर्तीन हम्मन कय वय लोगन कय आइ॰पी ठहर कय प्रयोग करेक परत है।\nआइ॰पी ठहर कयु सदस्यन् कय एक्कय होइ सकत है।\nयदि आप कवनो बेनामी सदस्य होआ जाय अव आप कय लागत है कि आप कय बारे में अप्रासंगिक टीका टिप्पणी कई गा है तव कृपया [[Special:CreateAccount|सदस्यता लिहा जाय]] या [[Special:UserLogin|सत्रारंभ करा जाय]] ताकि अउर बेनामी सदस्यन् में से आप कय अलग से पहिचान सका जाय।''",
        "noarticletext": "अभीन इ पन्ना पे कवनो सामग्री नाई है।\nआप अउर पन्नन् में [[Special:Search/{{PAGENAME}}|इ शीर्षक कय खोज]] कई सका जात है,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} सम्बन्धित लॉग खोज सका जात है],\nया इस पृष्ठ को [{{fullurl:{{FULLPAGENAME}}|action=edit}} सम्पादित] कर सकते हैं</span>।",
        "noarticletext-nopermission": "अभीन इ पन्ना पे कवनो चिज नाई है।\nआप अउर पन्नन् में [[Special:Search/{{PAGENAME}}|इ शीर्षक कय खोज]] कई सका जात है,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} से सम्बन्धित लॉग खोज सका जात है] </span>लेकिन आप कय इ पन्ना बनावे कय अनुमति नाई है ।",
        "missing-revision": "\"{{FULLPAGENAME}}\" पन्ना कय अवतरण #$1 नाई है।\n\nखास कइकै ई एकठु मेटावल पन्ना कय पुरान लिंक पे क्लिक करय से होत है।\nढेर जानकारी कय खर्तिन आप [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटावे कय लॉग] देख सका जात है।",
index 3e1f6e7..0ace557 100644 (file)
        "noname": "Siz mövcud olan istifadəçi adı daxil etməmisiniz.",
        "loginsuccesstitle": "Daxil oldunuz",
        "loginsuccess": "'''\"$1\" adı ilə sistemə daxil oldunuz.'''",
-       "nosuchuser": "\"$1\" adlı istifadəçi mövcud deyil.\nİstifadəçi adları hərflərin böyük və ya kiçik olmasına həssasdırlar.\nDüzgün yazdığınıza əmin olun, yaxud [[Special:UserLogin/signup|yeni hesab açın]].",
+       "nosuchuser": "\"$1\" adlı istifadəçi mövcud deyil.\nİstifadəçi adları hərflərin böyük və ya kiçik olmasına həssasdırlar.\nDüzgün yazdığınıza əmin olun, yaxud [[Special:CreateAccount|yeni hesab açın]].",
        "nosuchusershort": "\"$1\" adında istifadəçi mövcud deyil.\nDüzgün yazdığına əmin ol.",
        "nouserspecified": "İstifadəçi adı daxil etməlisiniz.",
        "login-userblocked": "Bu istifadəçi bloklanıb. Sistemə giriş üçün icazə verilmir.",
        "accmailtext": "[[User talk:$1|$1]] üçün təsadüfi yolla yaradılmış parol $2 ünvanına göndərildi.\nHesabınıza daxil olduqdan sonra, parolunuzu ''[[Special:ChangePassword|parolu dəyiş]]'' səhifəsində dəyişdirə bilərsiniz.",
        "newarticle": "(Yeni)",
        "newarticletext": "Mövcud olmayan səhifəyə olan keçidi izlədiniz. Aşağıdakı sahəyə məzmununu yazaraq bu səhifəni '''siz''' yarada bilərsiniz. (əlavə məlumat üçün [$1 kömək səhifəsinə] baxın). Əgər bu səhifəyə səhvən gəlmisinizsə sadəcə olaraq brauzerin '''geri''' düyməsinə vurun.",
-       "anontalkpagetext": "----''Bu səhifə qeydiyyatdan keçməmiş və ya daxil olmamış anonim istifadəçiyə aid müzakirə səhifəsidir.\nOna görə bu istifadəçini rəqəmlərdən ibarət IP ünvanı ilə müəyyən etmək məcburiyyətindəyik.\nBelə IP ünvan bir neçə fərd tərəfindən istifadədə ola bilər.\nƏgər siz anonim istifadəçisinizsə və bu mesajın sizə aid olmadığını düşünürsünüzsə, onda  [[Special:UserLogin/signup|qeydiyyatdan keçin]] və ya [[Special:UserLogin|daxi olun]].''",
+       "anontalkpagetext": "----''Bu səhifə qeydiyyatdan keçməmiş və ya daxil olmamış anonim istifadəçiyə aid müzakirə səhifəsidir.\nOna görə bu istifadəçini rəqəmlərdən ibarət IP ünvanı ilə müəyyən etmək məcburiyyətindəyik.\nBelə IP ünvan bir neçə fərd tərəfindən istifadədə ola bilər.\nƏgər siz anonim istifadəçisinizsə və bu mesajın sizə aid olmadığını düşünürsünüzsə, onda  [[Special:CreateAccount|qeydiyyatdan keçin]] və ya [[Special:UserLogin|daxi olun]].''",
        "noarticletext": "Hal-hazırda bu səhifə boşdur. Başqa səhifələrdə eyni adda səhifəni [[Special:Search/{{PAGENAME}}| axtara]], əlaqəli qeydlərə\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} baxa] və ya səhifəni [{{fullurl:{{FULLPAGENAME}}|action=edit}} redaktə]</span> edə bilərsiniz.",
        "noarticletext-nopermission": "Hal-hazırda bu səhifə boşdur. Başqa səhifələrdə eyni adlı səhifəni [[Special:Search/{{PAGENAME}}| axtara]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} əlaqəli qeydlərə baxa] və ya səhifəni [{{fullurl:{{FULLPAGENAME}}|action=edit}} redaktə edə bilərsiniz]</span>, lakin sizin bu məqaləni yaratmaq hüququnuz yoxdur.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" istifadəçi adı qeydiyyata alınmayıb.\nƏgər siz bu səhifəni yaratmaq/redaktə etmək istəyirsinizsə, xahiş edirik bunu yoxlayın.",
index ad623cb..9771d45 100644 (file)
        "noname": "گئچرلی ایستیفاده‌چی آدی وئرمه‌دینیز.",
        "loginsuccesstitle": "گیریش اوغورلو",
        "loginsuccess": "'''سیز ایندی {{SITENAME}} سایتینا، «$1» آدی‌له گیرمیسینیز.'''",
-       "nosuchuser": "«$1» آدلا ایستیفاده‌چی یوخدور.\nایستیفاده‌چی آدلاری، حرفلرین بؤیوک/کیچیک‌لیگینه حساس‌دیلار.\nیازدیغینیزا یئنی‌دن باخین، یوخسا [[Special:UserLogin/signup|یئنی بیر حساب آچین]].",
+       "nosuchuser": "«$1» آدلا ایستیفاده‌چی یوخدور.\nایستیفاده‌چی آدلاری، حرفلرین بؤیوک/کیچیک‌لیگینه حساس‌دیلار.\nیازدیغینیزا یئنی‌دن باخین، یوخسا [[Special:CreateAccount|یئنی بیر حساب آچین]].",
        "nosuchusershort": "\"$1\" آدلا ایستیفاده‌چی یوخدور.\nدوزگون یازدیغینیزدان آرخایین اولون.",
        "nouserspecified": "بیر ایستیفاده‌چی آدی وئرمه‌لیسینیز.",
        "login-userblocked": "بو ایستیفاده چی باغلانیب‌دیر. گیریشه ایجازه یوخدور.",
        "accmailtext": "[[User talk:$1|$1]] اوچون بیر راست‌گله رمز یارادیلیب و $2-ه گؤندریلدی.\n\nبو یئنی حسابین رمزی، گیرندن سونرا <em>[[Special:ChangePassword|رمز دَییشدیرمه]]</em> صحیفه‌سیندن دَییشیله بیلر.",
        "newarticle": "(یئنی)",
        "newarticletext": "مؤوجود اوْلمايان صحیفه‌‌يه اوْلان کئچیدی ایزله‌دینیز. \nآشاغیداکی ساحه‌‌يه مظمونونو يازاراق بۇ صحیفه‌‌نی '''سیز''' يارادا بیلرسینیز. (علاوه‌‌ معلومات اۆچون [$1 کؤمک صحیفه‌‌سینه] باخین). اگر بۇ صحیفه‌‌يه سهون گلمیسینیزسه ساده‌جه اوْلاراق براوزئرین '''گئری''' دۆيمه‌سینه وۇرون.",
-       "anontalkpagetext": "''بو صحیفه قئیدیات‌دان کئچممیش و یا داخیل اولمامیش آنونیم ایستیفادچییه عایید موذاکیره صحیفه‌سی‌دیر.\nاونا گؤره بو ایستیفادچینی رقم‌لردن عبارت ایپ اونوانی ایله معین ائتمک مجبوریتیندییک.\nبئله ایپ اونوان بیر نئچه فرد طرفین‌دن ایستیفاده‌ده اولا بیلر.\nاگر سیز آنونیم ایستیفادچیسینیزسه و بو مئساژین سیزه عایید اولمادیغینی دوشونورسونوزسه، اوندا  [[Special:UserLogin/signup|قئیدیات‌دان کئچین]] و یا [[Special:UserLogin|داخی اولون]].''",
+       "anontalkpagetext": "''بو صحیفه قئیدیات‌دان کئچممیش و یا داخیل اولمامیش آنونیم ایستیفادچییه عایید موذاکیره صحیفه‌سی‌دیر.\nاونا گؤره بو ایستیفادچینی رقم‌لردن عبارت ایپ اونوانی ایله معین ائتمک مجبوریتیندییک.\nبئله ایپ اونوان بیر نئچه فرد طرفین‌دن ایستیفاده‌ده اولا بیلر.\nاگر سیز آنونیم ایستیفادچیسینیزسه و بو مئساژین سیزه عایید اولمادیغینی دوشونورسونوزسه، اوندا  [[Special:CreateAccount|قئیدیات‌دان کئچین]] و یا [[Special:UserLogin|داخی اولون]].''",
        "noarticletext": "ایندی بو صفحه‌ده یازی یوخدور.\nسیز آیری صفحه‌‌لرده [[Special:Search/{{PAGENAME}}|بو باشلیق اوچون آختارا بیلرسیز]]،\nیا دا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} باغلی قئیدلری آختارا بیلرسیز]،\nیا دا [{{fullurl:{{FULLPAGENAME}}|action=edit}} بو صفحه‌نی دَییشدیره بیلرسیز]</span>.",
        "noarticletext-nopermission": "بو صحیفه‌‌ ایندی بوشدور. \nباشقا صحیفه‌‌لرده عینی آددا صحیفه‌‌نی  [[Special:Search/{{PAGENAME}}| آختار]], علاقه‌‌لی قئيدلره \n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} باخا],\nو يا صحیفه‌‌نی  [{{fullurl:{{FULLPAGENAME}}|action=edit}} redaktə]</span> ائده بیلرسینیز.",
        "missing-revision": "«{{FULLPAGENAME}}» صحیفه‌سی اوچون $1 نومره‌لی نوسخه یوخدور.\n\nعموماُ بو ایشکال، واختی گئچمیش بیر باغلانتی ایله سیلینمیش بیر صحیفه‌یه گلنده، قاباغا گلر.\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} سیلمک سیاهی]‌سینده باشقا بیلگیلر اولا بیلر.",
index 1cb4117..38de629 100644 (file)
        "noname": "Ғәмәлдә булған ҡатнашыусы исемен керетмәнегеҙ.",
        "loginsuccesstitle": "Танышыу уңышлы үтте",
        "loginsuccess": "Хәҙер һеҙ $1 исеме менән эшләйһегеҙ.",
-       "nosuchuser": "$1 исемле ҡулланыусы юҡ.\nҠулланыусы исеме хәреф регистрына һиҙгер.\nИсемде тикшерегеҙ йәки [[Special:UserLogin/signup|яңы иҫәп яҙыуы асығыҙ]].",
+       "nosuchuser": "$1 исемле ҡулланыусы юҡ.\nҠулланыусы исеме хәреф регистрына һиҙгер.\nИсемде тикшерегеҙ йәки [[Special:CreateAccount|яңы иҫәп яҙыуы асығыҙ]].",
        "nosuchusershort": "$1 исемле ҡулланыусы юҡ. Исемде тикшерегеҙ.",
        "nouserspecified": "Һеҙ ҡатнашыусы исемен күрһәтергә тейеш.",
        "login-userblocked": "Был ҡатнашыусыға рөхсәт юҡ.  Исеме тыйылған.",
        "accmailtext": "[[User talk:$1|$1]] өсөн осраҡлы яһалған серһүҙ $2 адресына ебәрелде.\n\nТанылғандан һуң был иҫәп яҙмаһы өсөн серһүҙҙе ''[[Special:ChangePassword|серһүҙҙе үҙгәртеү өсөн махсус биттә үҙгәртә алаһығыҙ]]''.",
        "newarticle": "(Яңы)",
        "newarticletext": "Һеҙ һылтанма буйынса әлегә яһалмаған биткә күстегеҙ.\nЯңы бит яһар өсөн аҫтағы тәҙрәгә текст керетегеҙ (тулыраҡ мәғлүмәт өсөн [$1 ярҙам битен] ҡарағыҙ).\nӘгәр был биткә яңылыш килеп эләккән булһағыҙ, браузерығыҙҙың '''артҡа''' төймәһенә баҫығыҙ.",
-       "anontalkpagetext": "----\n<em>Был фекер алышыу бите, иҫәп яҙыуы булдырмаған йәки уны ҡулланмаған аноним ҡатнашыусының бите.</em>\nШуның өсөн ҡулланыусыны таныу өсөн IP-адресы ҡулланыла.\nӘгәр һеҙ аноним ҡулланыусы булһағыҙ һәм һеҙгә ебәрелмәгән хәбәрҙәр алдым тиһәгеҙ (бер IP-адрес күп ҡулланыусы өсөн булырға мөмкин) һәм башҡа бындай аңлашылмаусанлыҡтар килеп сыҡмаһын өсөн, зинар, [[Special:UserLogin|системаға керегеҙ]] йәки [[Special:UserLogin/signup|теркәлегеҙ]].",
+       "anontalkpagetext": "----\n<em>Был фекер алышыу бите, иҫәп яҙыуы булдырмаған йәки уны ҡулланмаған аноним ҡатнашыусының бите.</em>\nШуның өсөн ҡулланыусыны таныу өсөн IP-адресы ҡулланыла.\nӘгәр һеҙ аноним ҡулланыусы булһағыҙ һәм һеҙгә ебәрелмәгән хәбәрҙәр алдым тиһәгеҙ (бер IP-адрес күп ҡулланыусы өсөн булырға мөмкин) һәм башҡа бындай аңлашылмаусанлыҡтар килеп сыҡмаһын өсөн, зинар, [[Special:UserLogin|системаға керегеҙ]] йәки [[Special:CreateAccount|теркәлегеҙ]].",
        "noarticletext": "Хәҙерге ваҡытта был биттә текст юҡ.\nҺеҙ [[Special:Search/{{PAGENAME}}|был исемде башҡа биттәрҙә эҙләй]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} тап килгән журнал яҙмаларын таба]\nйәки '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} бындай исемле яңы бит яһай]'''</span> алаһығыҙ.",
        "noarticletext-nopermission": "Хәҙерге ваҡытта был биттә текст юҡ.\nҺеҙ башҡа биттәрҙә [[Special:Search/{{PAGENAME}}|был исемде]] йәки\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} журналдағы яҙмаларҙы] эҙләй алаһығыҙ, тик һеҙҙең бит яһау хоҡуғығыҙ юҡ.</span>",
        "missing-revision": "\"{{FULLPAGENAME}}\" исемле биттең $1 номерлы өлгөһө юҡ.\n\nБыл хәл, ғәҙәттә, юйылған биткә яһалған һылтанманын ваҡыты үтеүенән барлыҡҡа килә.\nТулыраҡ мәғлүмәт өсөн ҡарағыҙ: [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} юйыу яҙмалары].",
        "upload-form-label-own-work": "Был минең эш",
        "upload-form-label-infoform-categories": "Категориялар",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Тейәлгән файл  {{SITENAME}} лицензия сәйәсәтенә ярашлы икәнен раҫлайым.",
-       "upload-form-label-not-own-work-message-local": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү !с!н башҡа ысулды һайлағыҙ.",
-       "upload-form-label-not-own-work-local-local": "Ошонда эшләп ҡарағыҙ[[Special:Upload|килешеү буйынса тейәү бите]].",
-       "upload-form-label-own-work-message-default": "Был файлды дөйөм репозиторийға күсереүемде аңлайым. Быны ҡулланыусы килешеүе һәм лицензия сәйәсәтенә ярашлы эшләүемде раҫлайым.",
-       "upload-form-label-not-own-work-message-default": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү өсөн башҡа ысулды һайлағыҙ.",
-       "upload-form-label-not-own-work-local-default": "{{SITENAME}} талаптарына ярашлы файлы тейәп булһа,  [[Special:Upload|тейәү битен]] ҡарағыҙ.",
-       "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/ҡулланыу шарттары] менән килешәм.",
-       "upload-form-label-not-own-work-message-shared": "Әгәр ошо файлдың авторы түгелһегеҙ һәм уны икенсе лицензия аҫтында сығарырға теләйһегеҙ икән,  [https://commons.wikimedia.org/wiki/Special:UploadWizard Викискладҡа күсереү оҫталары] мөмкинлеген файҙаланығыҙ.",
-       "upload-form-label-not-own-work-local-shared": "{{SITENAME}} талаптарына ярашлы файлы тейәп булһа,  [[Special:Upload|тейәү битен]] ҡарағыҙ.",
+       "upload-form-label-own-work-message-generic-local": "Тейәлгән файл  {{SITENAME}} лицензия сәйәсәтенә ярашлы икәнен раҫлайым.",
+       "upload-form-label-not-own-work-message-generic-local": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү !с!н башҡа ысулды һайлағыҙ.",
+       "upload-form-label-not-own-work-local-generic-local": "Ошонда эшләп ҡарағыҙ[[Special:Upload|килешеү буйынса тейәү бите]].",
+       "upload-form-label-own-work-message-generic-foreign": "Был файлды дөйөм репозиторийға күсереүемде аңлайым. Быны ҡулланыусы килешеүе һәм лицензия сәйәсәтенә ярашлы эшләүемде раҫлайым.",
+       "upload-form-label-not-own-work-message-generic-foreign": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү өсөн башҡа ысулды һайлағыҙ.",
+       "upload-form-label-not-own-work-local-generic-foreign": "{{SITENAME}} талаптарына ярашлы файлы тейәп булһа,  [[Special:Upload|тейәү битен]] ҡарағыҙ.",
        "backend-fail-stream": "$1 файлын трансляциялап булмай.",
        "backend-fail-backup": "$1 файлының резерв күсермәһен эшләп булмай.",
        "backend-fail-notexists": "$1 файлы юҡ.",
        "pageinfo-not-current": "Ғәфү итегеҙ, был мәғлүмәтте иҫке версиялар өсөн күрһәтеп булмай.",
        "pageinfo-header-basic": "Төп мәғлүмәт",
        "pageinfo-header-edits": "Үҙгәртеүҙәр тарихы",
-       "pageinfo-header-restrictions": "Бите һаҡлау",
+       "pageinfo-header-restrictions": "Битте һаҡлау",
        "pageinfo-header-properties": "Биттең үҙенсәлектәре",
        "pageinfo-display-title": "Күренгән исем",
        "pageinfo-default-sort": "Ғәҙәттәге сортлау асҡысы",
index d228878..06a7cdb 100644 (file)
        "noname": "شما یک معتبرین نام کاربر مشخص نه کتت.",
        "loginsuccesstitle": "ورود موفقیت آمیز",
        "loginsuccess": "''''شما الان وارد {{SITENAME}} په عنوان \"$1\".'''",
-       "nosuchuser": "هچ کاربری گون نام \"$1\" نیستن.\nکاربری نام حرفش په هور و مزنی حساس انت.\nوتی املايا چک کنیت یا [[Special:UserLogin/signup|نوکین حسابی شرکنیت]].",
+       "nosuchuser": "هچ کاربری گون نام \"$1\" نیستن.\nکاربری نام حرفش په هور و مزنی حساس انت.\nوتی املايا چک کنیت یا [[Special:CreateAccount|نوکین حسابی شرکنیت]].",
        "nosuchusershort": "هچ کاربری گون نام  \"$1\"نیستن.\nوتی املايا کنترل کنیت",
        "nouserspecified": "شما باید یک نام کاربری مشخص کنیت.",
        "login-userblocked": "ائ کابر بلاک بیتگ. لاگین مان سیستمء اجازت نه انت.",
        "accmailtext": "یک پسوردء [[User talk:$1|$1]] پر $2 راهیگ بوت. بیت آئرا چه پیجء ''[[Special:ChangePassword|پسوردء ٹگل]]'' که لاگینء درگتء پیش دارگ بیت ٹگل دئیت.",
        "newarticle": "(نوکین)",
        "newarticletext": "شما رند چه یک لینکی په یک صفحه ی که هنو نیستند اتکگیت.\nپه شر کتن صفحه، شروع کن نوشتن ته جعبه جهلی(بچار  [$1 صفحه کمک]  په گیشترین اطلاعات).\nاگر شما اشتباهی ادانیت ته وتی بروزر دکمه ''Back'' بجن.",
-       "anontalkpagetext": "----'' ای صفحه بحث انت په یک ناشناس کاربری که هنگت یک حسابی شر نه کتت یا آی ا ستفاده نه کتت. اچه ما بایدن آدرس آی پی عددی په پچاه آرگ آیی استفاده کنین.\nچوشن آدرس آی پی گون چندین کاربر استفاده بیت.\nاگه شما یک کاربر ناشناس ایت وی حس کنیت بی ربطین نظر مربوط شمی هست، لطفا [[Special:UserLogin|وارد بیت ]] یا [[Special:UserLogin/signup|حسابی شرکن]] دان چه هور بییگ گون ناسناسین کاربران پرهیز بیت.''",
+       "anontalkpagetext": "----'' ای صفحه بحث انت په یک ناشناس کاربری که هنگت یک حسابی شر نه کتت یا آی ا ستفاده نه کتت. اچه ما بایدن آدرس آی پی عددی په پچاه آرگ آیی استفاده کنین.\nچوشن آدرس آی پی گون چندین کاربر استفاده بیت.\nاگه شما یک کاربر ناشناس ایت وی حس کنیت بی ربطین نظر مربوط شمی هست، لطفا [[Special:UserLogin|وارد بیت ]] یا [[Special:CreateAccount|حسابی شرکن]] دان چه هور بییگ گون ناسناسین کاربران پرهیز بیت.''",
        "noarticletext": "هنو هچ متنی ته ای صفحه نیست.\nشما تونیت [[Special:Search/{{PAGENAME}}|گردیت په عنوان صفحه]]  ته دگه صفحات یا<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} گردگ په مربوطین آمار],\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} اصلاح ای صفحه]</span>.",
        "noarticletext-nopermission": "ائ تاکء رء انیگ هچ سیاهگء نه انت .\nشما توانت مان ادگر تاکان [[Special:Search/{{PAGENAME}}|ائ عنوانء رء پٹوپول بکن ات]]،\nیانکه <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} مان یکپیمیگین سیاهگان شوهاز بکن ات]</span> بلئ شما رء پر ائ پیجء اڈ کتنء اجازت نه انت.",
        "missing-revision": "ویرایش #$1 چه پیجء «{{FULLPAGENAME}}» موجود نه انت.\n\nبلکین چه نوک نکتنء بابتء هزپ بیتگ انت.\nتوان ات گیشترین جزئیات رء مان [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} هزپانی سیاهگ] ودی بکن ات.",
index a9a1d75..16a3c7a 100644 (file)
        "noname": "Ika dae tabi nakapagkaag nin sarong balidong pangaran nin paragamit.",
        "loginsuccesstitle": "Matrayumpo an saimong paglaog",
        "loginsuccess": "'''Ika ngunyan nakalaog na sa {{SITENAME}} bilang si \"$1\".'''",
-       "nosuchuser": "Dae pang paragamit na ginagamit an pangaran na \"$1\".\nAn mga ngaran nin paragamit sensitibo gayo sa tipahan.\nPakireparo kan saimong espeling, o [[Special:UserLogin/signup|Magmukna nin bagong panindog]].",
+       "nosuchuser": "Dae pang paragamit na ginagamit an pangaran na \"$1\".\nAn mga ngaran nin paragamit sensitibo gayo sa tipahan.\nPakireparo kan saimong espeling, o [[Special:CreateAccount|Magmukna nin bagong panindog]].",
        "nosuchusershort": "Mayo po tabing paragamit na an pangaran \"$1\".\nPaki-tsek an saimong espeling.",
        "nouserspecified": "Kaipuhan mong magkaag nin sarong pangaran nin paragamit.",
        "login-userblocked": "An paragamit na ini pinagkubkob. An paglaog dae pinagtutugutan.",
        "accmailtext": "An purak na pinagpuyos na pasa-taramon para ki [[User talk:$1|$1]] ipinagpadara na sa $2. Ini mapupuwedeng pagribayan sa ''[[Special:ChangePassword|change password]]'' na pahina matapos na ika nakalaog na.",
        "newarticle": "(Bàgo)",
        "newarticletext": "Ika nakapagsunod sa sarong sugpon pasiring sa sarong pahina na bako pang eksistido. Tanganing makapagmukna nin pahina, magpoon sa pagpindot sa laog nin kahon sa ibaba (hilngon an [$1 pahina nin katabangan] para sa kadugangan na impormasyon).\nKun ika napasalang nakadigde, i-klik an  '''ibalik''' na pindutan kan saimong kilyawan.",
-       "anontalkpagetext": "----''Ini iyo an pahina kan orolayan para an sarong dae bistadong paragamit na dae pa nakapagmukna nin panindog, o dae pa nakapaggamit kaini.\nKaya kami kaipong gumamit nin numerikal na IP address sa pagbisto saiya.\nAn arog kaining IP address puwedeng maikapagheras sa nagkapirang mga paragamit.\nKun ika sarong dae pa bistadong paragamit asin mati mo na igwang irelebanteng sambit na pinanungod saimo, tabi paki [[Special:UserLogin/signup|mukna nin panindog]] or [[Special:UserLogin|maglaog ka]] tanganing malikayan an pagkaribong sa pag-iriba kan iba pang mga paragamit.''",
+       "anontalkpagetext": "----''Ini iyo an pahina kan orolayan para an sarong dae bistadong paragamit na dae pa nakapagmukna nin panindog, o dae pa nakapaggamit kaini.\nKaya kami kaipong gumamit nin numerikal na IP address sa pagbisto saiya.\nAn arog kaining IP address puwedeng maikapagheras sa nagkapirang mga paragamit.\nKun ika sarong dae pa bistadong paragamit asin mati mo na igwang irelebanteng sambit na pinanungod saimo, tabi paki [[Special:CreateAccount|mukna nin panindog]] or [[Special:UserLogin|maglaog ka]] tanganing malikayan an pagkaribong sa pag-iriba kan iba pang mga paragamit.''",
        "noarticletext": "Mayo tabi sa presente nin teksto sa pahinang ini.\nIka mapuwedeng [[Special:Search/{{PAGENAME}}|maghanap para sa titulo kan pahinang ini]] sa iba pang mga pahina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa magkasurundong mga talaan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} liwaton ining pahina]</span>.",
        "noarticletext-nopermission": "Mayong sa presente nin teksto an pahinang ini.\nIka mapuwedeng [[Special:Search/{{PAGENAME}}|hanapa para kaining titulo kan pahina]] sa iba pang mga pahina,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa magkasurundong mga talaan]</span>.",
        "missing-revision": "An rebisyon #$1 kan pahina pinagngaranan na \"{{FULLPAGENAME}}\" bakong eksistido.\n\nIni pirmihan na pinagkakausa sa paagi nin pagsusunod nin luwas na petsang historiya nin kasugpunan pasiring sa sarong pahinang pinagpura na.\nAn mga detalye matatagboan sa [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} pinagpura na talaan].",
index 58ec3a8..7a39fa8 100644 (file)
        "noname": "Вы пазначылі няслушнае імя ўдзельніка.",
        "loginsuccesstitle": "Увайшлі ў сыстэму",
        "loginsuccess": "<strong>Цяпер Вы ўвайшлі ў {{GRAMMAR:вінавальны|{{SITENAME}}}} як «$1».</strong>",
-       "nosuchuser": "Удзельніка «$1» не існуе.\nВялікія і малыя літары адрозьніваюцца ў імёнах удзельнікаў.\nПраверце напісаньне альбо [[Special:UserLogin/signup|стварыце новы рахунак]].",
+       "nosuchuser": "Удзельніка «$1» не існуе.\nВялікія і малыя літары адрозьніваюцца ў імёнах удзельнікаў.\nПраверце напісаньне альбо [[Special:CreateAccount|стварыце новы рахунак]].",
        "nosuchusershort": "Удзельніка зь іменем «$1» не існуе. Праверце напісаньне.",
        "nouserspecified": "Вы мусіце пазначыць імя ўдзельніка.",
        "login-userblocked": "{{GENDER:$1|Гэты ўдзельнік заблякаваны|Гэтая ўдзельніца заблякаваная}}. Уваход у сыстэму забаронены.",
        "mailmypassword": "Скінуць пароль",
        "passwordremindertitle": "Новы часовы пароль для {{GRAMMAR:родны|{{SITENAME}}}}",
        "passwordremindertext": "Нехта (магчыма Вы, з IP-адрасу $1) запытаў нас даслаць новы пароль для {{GRAMMAR:родны|{{SITENAME}}}} ($4). Для ўдзельніка «$2» быў створаны часовы пароль і ён цяпер «$3». Калі гэта была Вашая ініцыятыва, Вам трэба ўвайсьці ў сыстэму і адразу зьмяніць пароль. Тэрмін дзеяньня Вашага часовага паролю — $5 {{PLURAL:$5|дзень|дні|дзён}}.\n\nКалі гэты запыт адправіў нехта іншы, альбо Вы ўзгадалі свой пароль і ўжо не жадаеце яго зьмяніць, Вы можаце праігнараваць гэты ліст і працягваць карыстацца старым паролем.",
-       "noemail": "{{GENDER:$1|УдзелÑ\8cнÑ\96к Â«$1» Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\9e|УдзелÑ\8cнÑ\96Ñ\86а Â«$1» Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bла}} Ð½Ñ\96Ñ\8fкага Ð°Ð´Ñ\80аÑ\81Ñ\83 электроннай пошты.",
-       "noemailcreate": "Вы павінны пазначыць слушны адрас электроннай пошты",
+       "noemail": "{{GENDER:$1|УдзелÑ\8cнÑ\96к Â«$1» Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\9e|УдзелÑ\8cнÑ\96Ñ\86а Â«$1» Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bла}} Ð°Ð´Ñ\80аÑ\81 электроннай пошты.",
+       "noemailcreate": "Вы павінныя пазначыць слушны адрас электроннай пошты.",
        "passwordsent": "Новы пароль быў дасланы на адрас электроннай пошты ўдзельніка «$1».\nКалі ласка, увайдзіце ў сыстэму пасьля яго атрыманьня.",
        "blocked-mailpassword": "З Вашага IP-адрасу забароненыя рэдагаваньні. Каб пазьбегнуць злоўжываньняў, з гэтага IP-адрасу забаронена аднаўляць пароль.",
        "eauthentsent": "Пацьверджаньне было дасланае на пазначаны адрас электроннай пошты.\nУ лісьце ўтрымліваюцца інструкцыі, па выкананьні якіх Вы зможаце пацьвердзіць, што адрас сапраўды належыць Вам, і на гэты адрас будзе дасылацца пошта адсюль.",
        "accmailtext": "Створаны адвольны пароль для [[User talk:$1|$1]] быў адасланы па адрасе $2. Яго можна зьмяніць на старонцы ''[[Special:ChangePassword|зьмены паролю]]'' пасьля ўваходу.",
        "newarticle": "(Новая)",
        "newarticletext": "Вы прыйшлі па спасылцы на старонку, якая яшчэ не існуе.\nКаб стварыць яе, напішыце тэкст у полі ніжэй (глядзіце [$1 старонку дапамогі] для дадатковай інфармацыі).\nКалі Вы трапілі сюды памылкова, націсьніце кнопку «<strong>назад</strong>» у вашым браўзэры.",
-       "anontalkpagetext": "----''Гэта старонка гутарак ананімнага ўдзельніка, які яшчэ не стварыў сабе рахунак альбо не ўжывае яго. Таму мы вымушаныя ўжываць лічбавы IP-адрас дзеля ягонай ідэнтыфікацыі. Адзін IP-адрас можа выкарыстоўвацца некалькімі ўдзельнікамі. Калі Вы — ананімны ўдзельнік і лічыце, што атрымалі не прызначаныя Вам камэнтары, калі ласка, [[Special:UserLogin/signup|стварыце рахунак]] альбо [[Special:UserLogin|увайдзіце ў сыстэму]], каб у будучыні пазьбегнуць магчымай блытаніны зь іншымі ананімнымі ўдзельнікамі.''",
+       "anontalkpagetext": "----''Гэта старонка гутарак ананімнага ўдзельніка, які яшчэ не стварыў сабе рахунак альбо не ўжывае яго. Таму мы вымушаныя ўжываць лічбавы IP-адрас дзеля ягонай ідэнтыфікацыі. Адзін IP-адрас можа выкарыстоўвацца некалькімі ўдзельнікамі. Калі Вы — ананімны ўдзельнік і лічыце, што атрымалі не прызначаныя Вам камэнтары, калі ласка, [[Special:CreateAccount|стварыце рахунак]] альбо [[Special:UserLogin|увайдзіце ў сыстэму]], каб у будучыні пазьбегнуць магчымай блытаніны зь іншымі ананімнымі ўдзельнікамі.''",
        "noarticletext": "Цяпер тэкст на гэтай старонцы адсутнічае.\nВы можаце [[Special:Search/{{PAGENAME}}|пашукаць гэтую назву]] сярод іншых старонак, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пашукаць у адпаведных журналах падзеяў]\nальбо [{{fullurl:{{FULLPAGENAME}}|action=edit}} стварыць гэтую старонку]</span>.",
        "noarticletext-nopermission": "Цяпер на гэтай старонцы тэкст адсутнічае.\nВы можаце [[Special:Search/{{PAGENAME}}|пашукаць назву гэтай старонкі]] на іншых старонках, альбо <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пашукаць зьвязаныя запісы ў журналах]</span>, але ў вас няма дазволу ствараць гэтую старонку.",
        "missing-revision": "Вэрсія старонкі №$1 з назвай «{{FULLPAGENAME}}» не існуе.\n\nЗвычайна гэта здараецца з-за перахода па састарэлай спасылцы на старонку, якая была выдаленая.\nПадрабязнасьці можна знайсьці ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].",
        "right-override-export-depth": "экспартаваньне старонак, уключаючы зьвязаныя старонкі з глыбінёй да 5",
        "right-sendemail": "адпраўка электронных лістоў іншым удзельнікам",
        "right-passwordreset": "прагляд электронных лістоў з ачысткай паролю",
-       "right-managechangetags": "ствараць і выдаляць [[Special:Tags|меткі]] з базы зьвестак",
+       "right-managechangetags": "стварэньне і (дэ)актывацыя [[Special:Tags|метак]]",
        "right-applychangetags": "дадаваць [[Special:Tags|меткі]] пры рэдагаваньні",
        "right-changetags": "дадаваць і выдаляць адвольныя [[Special:Tags|меткі]] да асобных вэрсіяў і запісаў у журнале падзеяў",
+       "right-deletechangetags": "выдаленьне [[Special:Tags|метак]] з базы зьвестак",
        "grant-generic": "Набор правоў «$1»",
        "grant-group-page-interaction": "Узаемадзеяньне з старонкамі",
        "grant-group-file-interaction": "Узаемадзеяньне з мэдыяфайламі",
        "action-viewmyprivateinfo": "прагляд вашых прыватных зьвестак",
        "action-editmyprivateinfo": "рэдагаваньне вашых прыватных зьвестак",
        "action-editcontentmodel": "рэдагаваньне мадэлі зьместу старонкі",
-       "action-managechangetags": "стварэньне і выдаленьне метак з базы зьвестак",
+       "action-managechangetags": "стварэньне і (дэ)актывацыю метак",
        "action-applychangetags": "дадаваньне метак пры рэдагаваньні",
        "action-changetags": "дадаваньне і выдаленьне адвольных метак да асобных вэрсіяў і запісаў у журнале падзеяў",
        "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
        "upload-form-label-own-work": "Гэта мая ўласная праца",
        "upload-form-label-infoform-categories": "Катэгорыі",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Я пацьвярджаю, што загружаю гэты файл згодна з правіламі і ліцэнзійнай палітыкай {{GRAMMAR:родны|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "Калі вы ня можаце загрузіць файл у адпаведнасьці з правіламі {{GRAMMAR:родны|{{SITENAME}}}}, калі ласка, закрыйце гэтае акно і паспрабуйце іншы мэтад.",
-       "upload-form-label-not-own-work-local-local": "Вы таксама можаце паспрабаваць [[Special:Upload|старонку загрузкі па змоўчаньні]].",
-       "upload-form-label-own-work-message-default": "Я разумею, што загружаю гэты файл у агульнае сховішча. Я пацьвярджаю, што раблю гэта ў адпаведнасьці з умовамі выкарыстаньня і ліцэнзійнай палітыкай.",
-       "upload-form-label-not-own-work-message-default": "Калі вы ня можаце загрузіць гэты файл паводле правілаў агульнага сховішча, калі ласка, закрыйце гэты дыялёг і паспрабуйце іншы мэтад.",
-       "upload-form-label-not-own-work-local-default": "Вы можаце паспрабаваць скарыстацца [[Special:Upload|старонкай загрузкі {{GRAMMAR:родны|{{SITENAME}}}}]], калі гэты файл можна туды загрузіць згодна з правіламі.",
-       "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 умовамі выкарыстаньня].",
-       "upload-form-label-not-own-work-message-shared": "Калі вы не зьяўляецеся ўласьнікам аўтарскіх правоў на гэты файл, або вы жадаеце распаўсюджваць яго пад іншай ліцэнзіяй, можаце скарыстацца [https://commons.wikimedia.org/wiki/Special:UploadWizard Майстарам загрузкі ў Вікісховішча].",
-       "upload-form-label-not-own-work-local-shared": "Вы таксама можаце скарыстацца [[Special:Upload|старонкай загрузкі {{GRAMMAR:родны|{{SITENAME}}}}]], калі правілы сайту дазваляюць загрузку такога файлу.",
+       "upload-form-label-own-work-message-generic-local": "Я пацьвярджаю, што загружаю гэты файл згодна з правіламі і ліцэнзійнай палітыкай {{GRAMMAR:родны|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Калі вы ня можаце загрузіць файл у адпаведнасьці з правіламі {{GRAMMAR:родны|{{SITENAME}}}}, калі ласка, закрыйце гэтае акно і паспрабуйце іншы мэтад.",
+       "upload-form-label-not-own-work-local-generic-local": "Вы таксама можаце паспрабаваць [[Special:Upload|старонку загрузкі па змоўчаньні]].",
+       "upload-form-label-own-work-message-generic-foreign": "Я разумею, што загружаю гэты файл у агульнае сховішча. Я пацьвярджаю, што раблю гэта ў адпаведнасьці з умовамі выкарыстаньня і ліцэнзійнай палітыкай.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Калі вы ня можаце загрузіць гэты файл паводле правілаў агульнага сховішча, калі ласка, закрыйце гэты дыялёг і паспрабуйце іншы мэтад.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Вы можаце паспрабаваць скарыстацца [[Special:Upload|старонкай загрузкі {{GRAMMAR:родны|{{SITENAME}}}}]], калі гэты файл можна туды загрузіць згодна з правіламі.",
        "backend-fail-stream": "Немагчыма накіраваць файл $1.",
        "backend-fail-backup": "Немагчыма зрабіць рэзэрвовую копію файла $1.",
        "backend-fail-notexists": "Файл $1 не існуе.",
        "apisandbox-request-url-label": "URL-адрас запыту:",
        "apisandbox-request-time": "Час запыту: {{PLURAL:$1|$1 мс}}",
        "apisandbox-results-fixtoken": "Выпраўце токен і паўтарыце адпраўку",
+       "apisandbox-results-fixtoken-fail": "Памылка пры атрыманьні токену «$1».",
        "apisandbox-alert-page": "Палі на гэтай старонцы няслушныя.",
        "apisandbox-alert-field": "Значэньне гэтага поля зьяўляецца няслушным.",
        "booksources": "Крыніцы кніг",
        "changecontentmodel-title-label": "Назва старонкі",
        "changecontentmodel-model-label": "Новая мадэль зьместу",
        "changecontentmodel-reason-label": "Прычына:",
+       "changecontentmodel-submit": "Зьмяніць",
        "changecontentmodel-success-title": "Мадэль зьместу была зьмененая",
        "changecontentmodel-success-text": "Тып зьместу [[:$1]] быў зьменены.",
        "changecontentmodel-cannot-convert": "Зьмест [[:$1]] ня можа быць ператвораны ў тып $2.",
        "whatlinkshere-prev": "{{PLURAL:$1|папярэдняя|папярэднія}} $1",
        "whatlinkshere-next": "{{PLURAL:$1|наступная|наступныя}} $1",
        "whatlinkshere-links": "← спасылкі",
-       "whatlinkshere-hideredirs": "$1 перанакіраваньні",
-       "whatlinkshere-hidetrans": "$1 уключэньні",
-       "whatlinkshere-hidelinks": "$1 спасылкі",
-       "whatlinkshere-hideimages": "$1 спасылкі на выявы",
+       "whatlinkshere-hideredirs": "Схаваць перанакіраваньні",
+       "whatlinkshere-hidetrans": "Схаваць уключэньні",
+       "whatlinkshere-hidelinks": "Схаваць спасылкі",
+       "whatlinkshere-hideimages": "Схаваць спасылкі на файлы",
        "whatlinkshere-filters": "Фільтры",
        "whatlinkshere-submit": "Перайсьці",
        "autoblockid": "Аўтаматычнае блякаваньне №$1",
index f018af4..fceab4d 100644 (file)
        "noname": "Вы не вызначылі правільнага імя ўдзельніка.",
        "loginsuccesstitle": "Паспяховы ўваход у сістэму",
        "loginsuccess": "<strong>Цяпер Вы ўвайшлі на {{SITENAME}} як \"$1\".</strong>",
-       "nosuchuser": "Няма ўдзельніка з імем \"$1\". Праверце правільнасць напісання або [[Special:UserLogin/signup|стварыце новы рахунак]]. Вялікія і малыя літары ў такіх імёнах лічацца рознымі.",
+       "nosuchuser": "Няма ўдзельніка з імем \"$1\". Праверце правільнасць напісання або [[Special:CreateAccount|стварыце новы рахунак]]. Вялікія і малыя літары ў такіх імёнах лічацца рознымі.",
        "nosuchusershort": "Удзельніка з імем \"$1\" не існуе. Праверце яго напісанне.",
        "nouserspecified": "Вы мусіце вызначыць імя ўдзельніка.",
        "login-userblocked": "Гэты карыстальнік заблакаваны. Лагін не дапускаецца.",
        "minoredit": "Дробная праўка",
        "watchthis": "Назіраць за гэтай старонкай",
        "savearticle": "Запісаць",
+       "publishpage": "Апублікаваць старонку",
        "preview": "Перадпаказ",
        "showpreview": "Як будзе",
        "showdiff": "Розніца",
        "accmailtext": "На адрас $2 быў дасланы згенераваны пароль для [[User talk:$1|$1]]. Ён можа быць зменены на <em>[[Special:ChangePassword|старонцы змены пароля]]</em> пасля ўваходу ў сістэму.",
        "newarticle": "(Новы)",
        "newarticletext": "Вы перайшлі па спасылцы на старонку, якой яшчэ няма.\nКаб яе стварыць, набярыце яе тэкст у ніжэйпаказаным акне рэдагавання (падрабязнасці гл. ў [$1 даведцы]).\nКалі вы тут выпадкова, проста націсніце <strong>назад</strong> у браўзеры.",
-       "anontalkpagetext": "----''Гэта старонка размовы з ананімным удзельнікам, які або не мае свайго рахунка, або ім не карыстаўся. Таму дзеля яго ці яе ідэнтыфікацыі мы мусім выкарыстаць лічбавы IP-адрас. Такі адрас IP могуць дзяліць між сабою некалькі асоб. Калі вы ананімны ўдзельнік, і лічыце, што атрымліваеце няслушныя заўвагі,[[Special:UserLogin/signup|стварыце рахунак]] або [[Special:UserLogin|зайдзіце ў сістэму]], каб вас больш не блыталі з іншымі ананімнымі ўдзельнікамі.''",
+       "anontalkpagetext": "----''Гэта старонка размовы з ананімным удзельнікам, які або не мае свайго рахунка, або ім не карыстаўся. Таму дзеля яго ці яе ідэнтыфікацыі мы мусім выкарыстаць лічбавы IP-адрас. Такі адрас IP могуць дзяліць між сабою некалькі асоб. Калі вы ананімны ўдзельнік, і лічыце, што атрымліваеце няслушныя заўвагі,[[Special:CreateAccount|стварыце рахунак]] або [[Special:UserLogin|зайдзіце ў сістэму]], каб вас больш не блыталі з іншымі ананімнымі ўдзельнікамі.''",
        "noarticletext": "Старонка не ўтрымлівае тэксту. Вы можаце [[Special:Search/{{PAGENAME}}|пашукаць гэткую назву]] ў іншых старонках ці <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ў журналах],\nабо [{{fullurl:{{FULLPAGENAME}}|action=edit}} папрацаваць з гэтай старонкай]</span>.",
        "noarticletext-nopermission": "Старонка не ўтрымлівае тэксту.\nВы можаце [[Special:Search/{{PAGENAME}}|пашукаць гэткую назву]] ў іншых старонках,\nці <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ў журналах]</span>, але вы не маеце дазволу на стварэнне гэтай старонкі.",
        "missing-revision": "Няма версіі #$1 у старонкі з назвай \"{{FULLPAGENAME}}\".\n\nЗвычайна такое здараецца, калі прайсці па састарэлай спасылцы з гісторыі на старонку, якая была сцёрта.\nПадрабязнасці можна пабачыць у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале сціранняў].",
        "upload-form-label-own-work": "Гэта мая ўласная праца",
        "upload-form-label-infoform-categories": "Катэгорыі",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Я пацвярджаю, што ўкладваю гэты файл згодна з правіламі і ліцэнзійнай палітыкай {{GRAMMAR:родны|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "Калі вы не можаце ўкладваць гэты файл згодна з правіламі пляцоўкі {{SITENAME}}, калі ласка, закрыйце гэта акно і паспрабуйце іншы метад.",
-       "upload-form-label-not-own-work-local-local": "Вы таксама можаце паспрабаваць [[Special:Upload|прадвызначаную старонку ўкладвання]].",
-       "upload-form-label-own-work-message-default": "Я разумею, што ўкладваю гэты файл у агульнае сховішча. Я пацвярджаю, што раблю гэта ў адпаведнасці з умовамі выкарыстання і ліцэнзійнай палітыкай.",
-       "upload-form-label-not-own-work-message-default": "Калі вы не можаце ўкладваць гэты файл згодна з правіламі агульнага сховішча, калі ласка, закрыйце гэта акно і паспрабуйце іншы метад.",
-       "upload-form-label-not-own-work-local-default": "Вы таксама можаце паспрабаваць скарыстацца [[Special:Upload|старонкай укладанняў пляцоўкі {{SITENAME}}]], калі гэты файл можна укладваць туды згодна з іх палітыкай.",
+       "upload-form-label-own-work-message-generic-local": "Я пацвярджаю, што ўкладваю гэты файл згодна з правіламі і ліцэнзійнай палітыкай {{GRAMMAR:родны|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Калі вы не можаце ўкладваць гэты файл згодна з правіламі пляцоўкі {{SITENAME}}, калі ласка, закрыйце гэта акно і паспрабуйце іншы метад.",
+       "upload-form-label-not-own-work-local-generic-local": "Вы таксама можаце паспрабаваць [[Special:Upload|прадвызначаную старонку ўкладвання]].",
+       "upload-form-label-own-work-message-generic-foreign": "Я разумею, што ўкладваю гэты файл у агульнае сховішча. Я пацвярджаю, што раблю гэта ў адпаведнасці з умовамі выкарыстання і ліцэнзійнай палітыкай.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Калі вы не можаце ўкладваць гэты файл згодна з правіламі агульнага сховішча, калі ласка, закрыйце гэта акно і паспрабуйце іншы метад.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Вы таксама можаце паспрабаваць скарыстацца [[Special:Upload|старонкай укладанняў пляцоўкі {{SITENAME}}]], калі гэты файл можна укладваць туды згодна з іх палітыкай.",
        "backend-fail-stream": "Не атрымалася трансляваць файл $1.",
        "backend-fail-backup": "Немагчыма зрабіць рэзервную копію $1.",
        "backend-fail-notexists": "Файл $1 не існуе.",
index eb1e5ae..849daa1 100644 (file)
        "noname": "Не указахте валидно потребителско име.",
        "loginsuccesstitle": "Успешно влизане",
        "loginsuccess": "'''Влязохте в {{SITENAME}} като „$1“.'''",
-       "nosuchuser": "Не съществува потребител с име „$1“.\nПотребителските имена са чувствителни на малки и главни букви.\nПроверете изписването или [[Special:UserLogin/signup|създайте нова сметка]].",
+       "nosuchuser": "Не съществува потребител с име „$1“.\nПотребителските имена са чувствителни на малки и главни букви.\nПроверете изписването или [[Special:CreateAccount|създайте нова сметка]].",
        "nosuchusershort": "Не съществува потребител с името „$1“. Проверете изписването.",
        "nouserspecified": "Необходимо е да се посочи потребителско име.",
        "login-userblocked": "Този потребител е блокиран. Влизането в системата не е позволено.",
        "accmailtext": "Случайно генерирана парола за [[User talk:$1|$1]] беше изпратена на $2. Паролата може да бъде променена от страницата ''[[Special:ChangePassword|„Промяна на паролата“]]'' след влизане в системата.",
        "newarticle": "(нова)",
        "newarticletext": "Последвахте препратка към страница, която все още не съществува.\nЗа да я създадете, просто започнете да пишете в долната текстова кутия\n(вижте [$1 помощната страница] за повече информация).",
-       "anontalkpagetext": "----''Това е дискусионната страница на анонимен потребител, който все още няма регистрирана сметка или не я използва, затова се налага да използваме IP-адрес, за да го идентифицираме. Такъв адрес може да се споделя от няколко потребители.''\n\n''Ако сте анонимен потребител и мислите, че тези неуместни коментари са отправени към вас, [[Special:UserLogin/signup|регистрирайте се]] или [[Special:UserLogin|влезте в системата]], за да избегнете евентуално бъдещо объркване с други анонимни потребители.''",
+       "anontalkpagetext": "----''Това е дискусионната страница на анонимен потребител, който все още няма регистрирана сметка или не я използва, затова се налага да използваме IP-адрес, за да го идентифицираме. Такъв адрес може да се споделя от няколко потребители.''\n\n''Ако сте анонимен потребител и мислите, че тези неуместни коментари са отправени към вас, [[Special:CreateAccount|регистрирайте се]] или [[Special:UserLogin|влезте в системата]], за да избегнете евентуално бъдещо объркване с други анонимни потребители.''",
        "noarticletext": "Понастоящем няма текст на тази страница. Можете да [[Special:Search/{{PAGENAME}}|потърсите за заглавието на страницата]] в други страници, да <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} потърсите в съответните дневници] или [{{fullurl:{{FULLPAGENAME}}|action=edit}} да я създадете]</span>.",
        "noarticletext-nopermission": "Текущо в тази страница няма текст.\nМожете да [[Special:Search/{{PAGENAME}}|потърсите заглавието на тази страница ]] в други страници или да <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} потърсите в съответните дневници]</span>, но нямате права да създадете тази страница.",
        "missing-revision": "Версия #$1 на страницата „{{FULLPAGENAME}}“ не съществува.\n\nТова обикновено се дължи на препратка от историята на страницата, която е била изтрита.\nПодробности могат да бъдат открити в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневника на изтриванията].",
        "revdelete-show-file-submit": "Да",
        "revdelete-selected-text": "{{PLURAL:$1|Избрана версия|Избрани версии}} от [[:$2]]:",
        "logdelete-selected": "{{PLURAL:$1|Избрано събитие|Избрани събития}}:",
+       "revdelete-text-text": "Изтритите редакции ще продължат да се виждат в историята на страницата, но части от съдържанието ще бъдат публично недостъпни.",
+       "revdelete-text-others": "Другите администратори ще продължат да имат достъп до скритото съдържание и могат да го възстановят, освен ако не бъдат наложени допълнителни ограничения.",
        "revdelete-confirm": "Необходимо е да потвърдите, че желаете да извършите действието, разбирате последствията и го правите според [[{{MediaWiki:Policy-url}}|политиката]].",
        "revdelete-suppress-text": "Премахването трябва да се използва '''само''' при следните случаи:\n* Потенциално уязвима в правно отношение информация\n* Неподходяща лична информация\n*: ''домашни адреси и телефонни номера, номера за социално осигуряване и др.''",
        "revdelete-legend": "Задаване на ограничения:",
        "upload-form-label-own-work": "Това е моя собствена творба",
        "upload-form-label-infoform-categories": "Категории",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Потвърждавам, че качвам този файл в съответствие с правилата и лицензионната политика на сайта {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Ако не можете да заредите този файл в съответствие с правилата на сайта {{SITENAME}}, моля, затворете този прозорец и опитайте друг метод.",
+       "upload-form-label-own-work-message-generic-local": "Потвърждавам, че качвам този файл в съответствие с правилата и лицензионната политика на сайта {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Ако не можете да заредите този файл в съответствие с правилата на сайта {{SITENAME}}, моля, затворете този прозорец и опитайте друг метод.",
        "backend-fail-notexists": "Файлът $1 не съществува.",
        "backend-fail-delete": "Файлът $1 не може да бъде изтрит.",
        "backend-fail-alreadyexists": "Файлът $1 вече съществува.",
        "logempty": "Дневникът не съдържа записи, отговарящи на избрания критерий.",
        "log-title-wildcard": "Търсене на заглавия, започващи със",
        "showhideselectedlogentries": "Промяна на видимостта на избраните записи",
-       "checkbox-all": "Всички",
-       "checkbox-none": "Никои",
+       "checkbox-select": "Избери: $1",
+       "checkbox-all": "всички",
+       "checkbox-none": "никои",
+       "checkbox-invert": "обърни избора",
        "allpages": "Всички страници",
        "nextpage": "Следваща страница ($1)",
        "prevpage": "Предходна страница ($1)",
        "sqlite-no-fts": "$1 без поддръжка на пълнотекстово търсене",
        "logentry-delete-delete": "$1 {{GENDER:$2|изтри}} страницата $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|възстанови}} страницата $3",
+       "logentry-delete-revision": "$1 {{GENDER:$2|промени}} видимостта на {{PLURAL:$5|една редакция|$5 редакции}} в страница $3: $4",
+       "logentry-delete-event-legacy": "$1 {{GENDER:$2|промени}} видимостта на събитията от дневниците за страница $3",
        "logentry-delete-revision-legacy": "$1 {{GENDER:$2|промени}} видимостта на версиите на страница $3",
        "logentry-suppress-revision": "$1 тайно {{GENDER:$2|промени}} видимостта на {{PLURAL:$5|една версия|$5 версии}} на страницата $3: $4",
        "logentry-suppress-revision-legacy": "$1 тайно {{GENDER:$2|промени}} видимостта на версиите на страница $3",
index 3846953..a239396 100644 (file)
        "noname": "شما یک موتبرین کار زوروکی ئین نامی ئا مشخص نه کورته ئیت.",
        "loginsuccesstitle": "کامیابین لوگین",
        "loginsuccess": "'''شما انون گو «$1» ئی نا بی {{SITENAME}} ئی تا داخل بوته ایت.'''",
-       "nosuchuser": "کار زوروکئ گۆ «$1» ئی ناما موجود نه اینت.\nکار زورکئ نام گۆ گۆنڈی یا توُهی ئا هوروپ ئان هساس اینت.\nنامی املا ئا بگنیدیت، یا [[Special:UserLogin/signup|یک نوکین کار زورکی هسابئ جۆڑ بکنیت]].",
+       "nosuchuser": "کار زوروکئ گۆ «$1» ئی ناما موجود نه اینت.\nکار زورکئ نام گۆ گۆنڈی یا توُهی ئا هوروپ ئان هساس اینت.\nنامی املا ئا بگنیدیت، یا [[Special:CreateAccount|یک نوکین کار زورکی هسابئ جۆڑ بکنیت]].",
        "nosuchusershort": "هیچ کار زوروکئ بی نامئ  ''$1'' ئا وجود نداریت.\nوتئ املا ئا چیک بکنیت.",
        "nouserspecified": "باید یک کار زوروکئ نام مشخص بکنیت.",
        "login-userblocked": "ای کار زوروک بلاک بوته، په داخل بوتینا اجازه نه اینت.",
        "upload-form-label-own-work": "ائ ني جیندئ کار اینت",
        "upload-form-label-infoform-categories": "تهرئان",
        "upload-form-label-infoform-date": "تاریخ",
-       "upload-form-label-own-work-message-local": "من ایشیرا قبولا کنین که من ائ فایلا بُرزا کنین گۆ استفاده ئی شرایطان شه  جوازئ شینک  بوتینا و خدماتئ سیاستان به {{SITENAME}} تا.",
+       "upload-form-label-own-work-message-generic-local": "من ایشیرا قبولا کنین که من ائ فایلا بُرزا کنین گۆ استفاده ئی شرایطان شه  جوازئ شینک  بوتینا و خدماتئ سیاستان به {{SITENAME}} تا.",
        "backend-fail-stream": "نه توانن $1 ئی فایلا دیم دهین.",
        "backend-fail-backup": "نتنوانن پُشتوانی نخسه یی په $1 فایلا جۆڑ کنن.",
        "backend-fail-notexists": " $1 ئی فایل وجود نداریت.",
index cf66e2b..fa75a26 100644 (file)
        "noname": "रउआ उपयुक्त प्रयोगकर्ता नाम नईखीं निर्दिष्ट कईले।",
        "loginsuccesstitle": "खाता प्रवेश में सफल",
        "loginsuccess": "''' \"$1\" के रुप में रउआ {{SITENAME}} में अब प्रवेश कर चुकल बानी।'''",
-       "nosuchuser": "\"$1\" नाम से कौनो प्रयोगकर्ता नईखन।\nप्रयोगकर्ता नाम संवेदनशील मामला बा।\nशब्द-वर्तनी के जाँच करीं, आ चाहे [[Special:UserLogin/signup|एगो नया खाता बनाईं]]।",
+       "nosuchuser": "\"$1\" नाम से कौनो प्रयोगकर्ता नईखन।\nप्रयोगकर्ता नाम संवेदनशील मामला बा।\nशब्द-वर्तनी के जाँच करीं, आ चाहे [[Special:CreateAccount|एगो नया खाता बनाईं]]।",
        "nosuchusershort": "ई नाम से कौनो प्रयोगकर्ता नईखन \"$1\".\nआपन शब्द-वर्तनी के जाँच करीं।",
        "nouserspecified": "रउआ एगो प्रयोगकर्ता नाम निर्दिष्ट करे के बा।",
        "login-userblocked": "ई प्रयोगकर्ता के खाता निष्क्रिय हो चुकल बा। प्रवेश के आज्ञा नईखे।",
        "accmailtext": "[[User talk:$1|$1]] खातिर एगो यंत्र जनित गुप्तशब्द $2 के भेज दिहल गइल बा। खाता में प्रवेश कइला के बाद इ '''[[Special:ChangePassword|गुप्तशब्द बदल लीं]]'' वाला पन्ना पर बदलल जा सकत बा।",
        "newarticle": "(नया)",
        "newarticletext": "रउआ एगो अइसन कड़ी के पन्ना के अनुसरण कइले बानी जवन अभी तक उपलब्ध नइखे।\nपन्ना बनावे खातिर, नीचे के बाकस में टाइप करे के शुरु करीं (ज्यादा जानकारी खातिर देखीं [$1 मदद पन्ना])।\nयदि रउआ अहिजा गलती से आ गइल बानी त, आपन ब्राउजर के '''बैक''' (Back) बटन दबाईं!",
-       "anontalkpagetext": "----''इ वार्ता पन्ना उन अनाम सदस्यन खातिर बा जिन्हन के या त खाता नइखे खोलल गइल या खाता के प्रयोग नइखन करत।\nएहि खातिर उन्हन के पहिचान खातिर हमनी के उनकर आइ॰पी पता के प्रयोग करे के पड़ेला।\nआइ॰पी पता कई सदस्यन खातिर साझा हो सकत बा।\nयदि आप एगो अनाम सदस्य बानी अउर आपके लागत बा कि आपके बारे में अप्रासंगिक टीका टिप्पणी करल गइल बा त कृपया [[Special:UserLogin/signup|सदस्यता लिहीं]] या [[Special:UserLogin|सत्रारंभ करीं]] ताकि अन्य अनाम सदस्यन में से आपके अलग से पहिचानल जा सके।''",
+       "anontalkpagetext": "----''इ वार्ता पन्ना उन अनाम सदस्यन खातिर बा जिन्हन के या त खाता नइखे खोलल गइल या खाता के प्रयोग नइखन करत।\nएहि खातिर उन्हन के पहिचान खातिर हमनी के उनकर आइ॰पी पता के प्रयोग करे के पड़ेला।\nआइ॰पी पता कई सदस्यन खातिर साझा हो सकत बा।\nयदि आप एगो अनाम सदस्य बानी अउर आपके लागत बा कि आपके बारे में अप्रासंगिक टीका टिप्पणी करल गइल बा त कृपया [[Special:CreateAccount|सदस्यता लिहीं]] या [[Special:UserLogin|सत्रारंभ करीं]] ताकि अन्य अनाम सदस्यन में से आपके अलग से पहिचानल जा सके।''",
        "noarticletext": "ए पन्ना मे अभी ले कौनों सामग्री नइखे। \nरउआ दुसरा पन्ना में [[Special:Search/{{PAGENAME}}|ए टाइटिल के खोज]] कर सकत बानीं।",
        "noarticletext-nopermission": "ए पन्ना मे अभी कौनों सामग्री नइखे।\nरउआँ दुसरा पन्ना में [[Special:Search/{{PAGENAME}}|ए टाइटिल के खोज]] कर सकत बानीं,\nया <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} या संबंधित लॉग खोज सकत बानी]</span>, बाकी रउआ के ई पन्ना बनावे के परमीशन नइखे।",
        "missing-revision": "\"{{FULLPAGENAME}}\" पन्ना के संशोधन #$1 उपलब्ध नइखे।\n\nसाधारण रुप से इ एगो हटावल गइल पन्ना के पुरान लिंक पर क्लिक कइला से होखेला।\nअधिक जानकारी खातिर आप [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटावे के लॉग] देख सकत बानी।",
index a87d235..1980adf 100644 (file)
        "noname": "Ngaran pamakai nang Pian ajuakan kada sah.",
        "loginsuccesstitle": "Kulihan babuat log",
        "loginsuccess": "'''Pian parhatan ni babuat log dalam {{SITENAME}} sawagai \"$1\".'''",
-       "nosuchuser": "Kadada pamakai bangaran \"$1\".\nNgaran pamakai adalah kasus marinci.\nLihati pulang ijaan Pian, atawa [[Special:UserLogin/signup|ulah sabuting akun hanyar]]",
+       "nosuchuser": "Kadada pamakai bangaran \"$1\".\nNgaran pamakai adalah kasus marinci.\nLihati pulang ijaan Pian, atawa [[Special:CreateAccount|ulah sabuting akun hanyar]]",
        "nosuchusershort": "Kadada pamakai bangaran \"$1\".\nLihati pulang ijaan Pian.",
        "nouserspecified": "Pian harus ma'ajuakan sabuting ngaran pamakai.",
        "login-userblocked": "Pamakai naya diblukir. Babuat log kada dibulihakan.",
        "accmailtext": "Sabuting katasunduk babarang gasan [[User talk:$1|$1]] sudah dikirim ka $2.\n\nKatasunduk gasan pamakai hanyar nangini kawa diubah pintang tungkaran ''[[Special:ChangePassword|ubah katasunduk]]'' wayah babuat log.",
        "newarticle": "(Hanyar)",
        "newarticletext": "Pian maumpati sabuah tautan ka tungkaran nang baluman ada lagi. Gasan maulah tungkaran, mulai ja mangatik pada kutak di bawah (lihati [$1 tungkaran patulung] gasan panjalasan labih). Amun Pian ka sia cagaran tasalah, klik picikan '''back''' di panjalajah web Pian.",
-       "anontalkpagetext": "----''Ngini adalah tungkaran pamandiran gasan pamakai kada bangaran nang baluman ma-ulah akun pulang, atawa  kada mamakainya. Kami tapaksa mamakai numurik alamat IP hagan maminanduinya.\nAlamat IP nangkaini kawaai dipuruk ulih babarapa pamakai.\nAmun Pian adalah pamuruk kada bangaran wan marasa kumin nang kada pas ta ka Pian, muhun [[Special:UserLogin/signup|ulah sabuah akun]] or [[Special:UserLogin|babuat log]] hagan mahindari kabingungan awan pamuruk kada bangaran lain kaina.",
+       "anontalkpagetext": "----''Ngini adalah tungkaran pamandiran gasan pamakai kada bangaran nang baluman ma-ulah akun pulang, atawa  kada mamakainya. Kami tapaksa mamakai numurik alamat IP hagan maminanduinya.\nAlamat IP nangkaini kawaai dipuruk ulih babarapa pamakai.\nAmun Pian adalah pamuruk kada bangaran wan marasa kumin nang kada pas ta ka Pian, muhun [[Special:CreateAccount|ulah sabuah akun]] or [[Special:UserLogin|babuat log]] hagan mahindari kabingungan awan pamuruk kada bangaran lain kaina.",
        "noarticletext": "Parhatan ni kadada naskah di tungkaran ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|manggagai gasan judul ngini]] pintang tungkaran lain,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} manggagai log barait].</span>,\natawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} mambabak tungkaran ngini]</span>.",
        "noarticletext-nopermission": "Parhatan ni kadada naskah di tungkaran ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|manggagai gasan judul ngini]] pintang tungkaran lain,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} manggagai log barait].</span>.",
        "userpage-userdoesnotexist": "Akun pamakai \"<nowiki>$1</nowiki>\" kada tadaptar.\nMuhun pariksa/ditukui amun Pian handak maulah/mambabak tungkaran ngini.",
index 00e296a..18d2978 100644 (file)
        "noname": "আপনি সঠিক ব্যবহারকারী নাম নির্দিষ্ট করেননি।",
        "loginsuccesstitle": "প্রবেশ করেছেন",
        "loginsuccess": "<strong>আপনি এইমাত্র \"$1\" নামে {{SITENAME}}-তে প্রবেশ করেছেন।</strong>",
-       "nosuchuser": "\"$1\" নামে কোন ব্যবহারকারী নেই।\nব্যবহারকারী নামের আকার সংবেদনশীল।\nআপনার বানান পরীক্ষা করে দেখুন, অথবা [[Special:UserLogin/signup|নতুন একটি অ্যাকাউন্ট খুলুন]]।",
+       "nosuchuser": "\"$1\" নামে কোন ব্যবহারকারী নেই।\nব্যবহারকারী নামের আকার সংবেদনশীল।\nআপনার বানান পরীক্ষা করে দেখুন, অথবা [[Special:CreateAccount|নতুন একটি অ্যাকাউন্ট খুলুন]]।",
        "nosuchusershort": "\"$1\" নামের কোন ব্যবহারকারী নেই। নামের বানান পরীক্ষা করুন।",
        "nouserspecified": "আপনাকে অবশ্যই ব্যবহারকারী নাম নির্দিষ্ট করতে হবে।",
        "login-userblocked": "এই ব্যবহারকারীকে বাধা দেওয়া হয়েছে। প্রবেশ সম্ভব নয়।",
        "accmailtext": "[[User talk:$1|$1]] এর জন্য দৈব ভাবে উৎপন্ন শব্দ চাবি $2 এ পাঠানো হয়েছে।\nলগ-ইন করার পর ''[[Special:ChangePassword|পাসওয়ার্ড পরিবর্তন]]'' পাতা থেকে এটি পরিবর্তন করা যাব।",
        "newarticle": "(নতুন)",
        "newarticletext": "আপনি এমন একটি লিংক অনুসরণ করছেন, যা নেই।\nপাতাটি তৈরি করতে, নিচের বাক্সে তা টাইপ করা শুরু করুন (আরও তথ্য জানতে [$1 সহায়িকা পাতা] দেখুন)।\nআপনি যদি ভুল করে এখানে এসে থাকেন, তাহলে আপনার ব্রাউজারের '''back''' বোতাম ক্লিক করুন।",
-       "anontalkpagetext": "----''এটি একটি বেনামী ব্যবহারকারীর আলাপের পাতা, যিনি এখনও কোন অ্যাকাউন্ট তৈরি করেননি, কিংবা তিনি অ্যাকাউন্টটি ব্যবহার করছেন না।\nআমরা তাই সাংখ্যিক আইপি ঠিকানা ব্যবহার করে তাঁকে শনাক্ত করছি।\nএকাধিক ব্যবহারকারী এরকম একটি আইপি ঠিকানা ব্যবহার করতে পারেন।\nআপনি যদি একজন বেনামী ব্যবহারকারী হয়ে থাকেন এবং যদি অনুভব করেন যে আপনার প্রতি অপ্রাসঙ্গিক মন্তব্য করা হয়েছে, তাহলে অন্যান্য বেনামী ব্যবহারকারীর সাথে ভবিষ্যতে বিভ্রান্তি এড়াতে অনুগ্রহ করে [[Special:UserLogin/signup|একটি অ্যাকাউন্ট তৈরি করুন]] অথবা  [[Special:UserLogin|অ্যাকাউন্টে প্রবেশ করুন]]।''",
+       "anontalkpagetext": "----''এটি একটি বেনামী ব্যবহারকারীর আলাপের পাতা, যিনি এখনও কোন অ্যাকাউন্ট তৈরি করেননি, কিংবা তিনি অ্যাকাউন্টটি ব্যবহার করছেন না।\nআমরা তাই সাংখ্যিক আইপি ঠিকানা ব্যবহার করে তাঁকে শনাক্ত করছি।\nএকাধিক ব্যবহারকারী এরকম একটি আইপি ঠিকানা ব্যবহার করতে পারেন।\nআপনি যদি একজন বেনামী ব্যবহারকারী হয়ে থাকেন এবং যদি অনুভব করেন যে আপনার প্রতি অপ্রাসঙ্গিক মন্তব্য করা হয়েছে, তাহলে অন্যান্য বেনামী ব্যবহারকারীর সাথে ভবিষ্যতে বিভ্রান্তি এড়াতে অনুগ্রহ করে [[Special:CreateAccount|একটি অ্যাকাউন্ট তৈরি করুন]] অথবা  [[Special:UserLogin|অ্যাকাউন্টে প্রবেশ করুন]]।''",
        "noarticletext": "বর্তমানে এই পাতায় কোন লেখা নেই।\nআপনি চাইলে অন্যান্য পাতায় [[Special:Search/{{PAGENAME}}| এই শিরোনামটি অনুসন্ধান করতে পারেন]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} এ সম্পর্কিত লগ অনুসন্ধান করতে পারেন], \nকিংবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পাতাটি তৈরি করতে পারেন]</span>।",
        "noarticletext-nopermission": "বর্তমানে এই পাতায় কোন লেখা নেই।\nআপনি চাইলে অন্য পাতায় [[Special:Search/{{PAGENAME}}| শিরোনামটি অনুসন্ধান করতে পারেন]], অথবা <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পর্কিত লগ অনুসন্ধান করতে পারেন]</span>, কিন্তু আপনার এই পাতাটি তৈরী করার অনুমতি নেই।",
        "missing-revision": "\"{{FULLPAGENAME}}\" এর #$1তম সংস্করণটি প্রদর্শন সম্ভব নয়।\n\nসাধারণত মুছে ফেলা হয়েছে এমন পাতার মেয়াদ উত্তীর্ণ ইতিহাস পাতার লিংক ওপেন করার কারণে এটি হতে পারে। \n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} অপসারণ লগে] বিস্তারিত তথ্য জানা যাবে।",
        "recentchangeslinked-page": "পাতার নাম:",
        "recentchangeslinked-to": "প্রদত্ত পাতায় সংযুক্ত আছে এমন পাতাগুলোর পরিবর্তন দেখাও",
        "recentchanges-page-added-to-category": "বিষয়শ্রেণীতে [[:$1]] যোগ করা হয়েছে",
-       "recentchanges-page-added-to-category-bundled": "বিষয়শà§\8dরà§\87ণà§\80তà§\87 [[:$1]] à¦\93 [[Special:WhatLinksHere/$1|{{PLURAL:$2|à¦\8fà¦\95à¦\9fি à¦ªà¦¾à¦¤à¦¾|$2à¦\9fি à¦ªà¦¾à¦¤à¦¾}}]] à¦¯à§\8bà¦\97 à¦\95রা à¦¹à¦¯à¦¼à§\87à¦\9bà§\87",
+       "recentchanges-page-added-to-category-bundled": "বিষয়শà§\8dরà§\87ণà§\80তà§\87 [[:$1]] à¦¯à§\8bà¦\97 à¦¹à¦¯à¦¼à§\87à¦\9bà§\87, [[Special:WhatLinksHere/$1|à¦\8fà¦\87 à¦ªà¦¾à¦¤à¦¾à¦\9fি à¦\85নà§\8dয à¦ªà¦¾à¦¤à¦¾à¦\97à§\81লির à¦®à¦§à§\8dযà§\87 à¦\85নà§\8dতরà§\8dভà§\81à¦\95à§\8dতà¦\95à§\83ত]]",
        "recentchanges-page-removed-from-category": "বিষয়শ্রেণী থেকে [[:$1]] সরানো হয়েছে",
-       "recentchanges-page-removed-from-category-bundled": "বিষয়শ্রেণী থেকে [[:$1]] ও [[Special:WhatLinksHere/$1|{{PLURAL:$2|একটি পাতা|$2টি পাতা}}]] সরানো হয়েছে",
+       "recentchanges-page-removed-from-category-bundled": "বিষয়শ্রেণীতে [[:$1]] সরানো হয়েছে, [[Special:WhatLinksHere/$1|এই পাতাটি অন্য পাতাগুলির মধ্যে অন্তর্ভুক্তকৃত]]",
        "autochange-username": "মিডিয়াউইকি স্বয়ংক্রিয় পরিবর্তন",
        "upload": "আপলোড",
        "uploadbtn": "ফাইল আপলোড করুন",
        "upload-form-label-own-work": "এটি আমার নিজের কাজ",
        "upload-form-label-infoform-categories": "বিষয়শ্রেণীসমূহ",
        "upload-form-label-infoform-date": "তারিখ",
-       "upload-form-label-not-own-work-local-local": "এছাড়াও আপনি [[Special:Upload|ডিফল্ট আপলোডের পাতা]] চেষ্টা করতে পারেন।",
-       "upload-form-label-not-own-work-local-default": "এছাড়াও আপনি [[Special:Upload|{{SITENAME}}-এর আপলোডের পাতা]] ব্যবহার করার চেষ্টা করতে পারেন, যদি এই ফাইলটি তাদের নীতিমালা অধীনে সেখানে আপলোড করা যায়।",
-       "upload-form-label-own-work-message-shared": "আমি প্রত্যয়ন করছি যে আমি এই ফাইলের স্বত্তাধিকারী, এবং [https://creativecommons.org/licenses/by-sa/4.0/deed.bn ক্রিয়েটিভ কমন্স অ্যাট্রিবিউশন-শেয়ার অ্যালাইক ৪.০] লাইসেন্সের অধীনে এই ফাইলটি উইকিমিডিয়া কমন্সে অপরিবর্তনীয় প্রকাশে সম্মত হচ্ছি, এবং আমি [https://wikimediafoundation.org/wiki/Terms_of_Use ব্যবহারের শর্তাবলীর] সাথে সম্মত।",
-       "upload-form-label-not-own-work-message-shared": "যদি আপনি এই ফাইলের স্বত্তাধিকারী না হন, বা আপনি একটি ভিন্ন লাইসেন্সের আওতায় প্রকাশ করতে ইচ্ছুক থাকেন, তাহলে [https://commons.wikimedia.org/wiki/Special:UploadWizard?uselang=bn কমন্স আপলোড উইজার্ড] ব্যবহার করতে বিবেচনা করুন।",
-       "upload-form-label-not-own-work-local-shared": "এছাড়াও আপনি [[Special:Upload|{{SITENAME}}-এর আপলোডের পাতা]] ব্যবহার করার চেষ্টা করতে পারেন, যদি সাইটটি তাদের নীতিমালার অধীনে এই ফাইল আপলোড করার অনুমতি দেয়।",
+       "upload-form-label-not-own-work-local-generic-local": "এছাড়াও আপনি [[Special:Upload|ডিফল্ট আপলোডের পাতা]] চেষ্টা করতে পারেন।",
+       "upload-form-label-not-own-work-local-generic-foreign": "এছাড়াও আপনি [[Special:Upload|{{SITENAME}}-এর আপলোডের পাতা]] ব্যবহার করার চেষ্টা করতে পারেন, যদি এই ফাইলটি তাদের নীতিমালা অধীনে সেখানে আপলোড করা যায়।",
        "backend-fail-stream": "\"$1\" ফাইলের স্ট্রিম দেখানো যাচ্ছে না।",
        "backend-fail-backup": "\"$1\" ফাইলের ব্যাকআপ তৈরী সম্ভব নয়।",
        "backend-fail-notexists": "\"$1\" নামের কোনো ফাইল নেই।",
        "sp-contributions-logs": "লগসমূহ",
        "sp-contributions-talk": "আলোচনা",
        "sp-contributions-userrights": "ব্যবহারকারী অধিকার ব্যবস্থাপনা",
-       "sp-contributions-blocked-notice": "এই ব্যবহারকারী বর্তমানে বাধাদানকৃত অবস্থায় রয়েছেন।\nতথ্যসূত্র হিসেবে সাম্প্রতিক বাধাদান লগে ভুক্তিটি নিচে দেওয়া হলো:",
+       "sp-contributions-blocked-notice": "এই ব্যবহারকারী বর্তমানে বাধাদানকৃত অবস্থায় রয়েছেন।\nতথ্যসূত্র হিসেবে সাম্প্রতিক বাধাদান লগে ভুক্তিটি নিচে দেওয়া হলো:",
        "sp-contributions-blocked-notice-anon": "এই আইপি ঠিকানাটি বর্তমানে বাধাদানকৃত অবস্থায় রয়েছে।\nতথ্যসূত্র হিসেবে সাম্প্রতিক বাধাদান লগের ভুক্তিটি নিচে দেওয়া হলো:",
        "sp-contributions-search": "অবদানসমূহের জন্য অনুসন্ধান",
        "sp-contributions-username": "আইপি (IP) ঠিকানা অথবা ব্যবহারকারীর নাম:",
        "whatlinkshere-prev": "{{PLURAL:$1|পূর্ববর্তী|পূর্ববর্তী $1টি}}",
        "whatlinkshere-next": "{{PLURAL:$1|পরবর্তী|পরবর্তী $1টি}}",
        "whatlinkshere-links": "← সংযোগগুলি",
-       "whatlinkshere-hideredirs": "পুননির্দেশনা $1",
-       "whatlinkshere-hidetrans": "অন্তর্ভুক্তকরণ $1",
-       "whatlinkshere-hidelinks": "সংযোগ $1",
-       "whatlinkshere-hideimages": "$1 ফাইল সংযোগ",
+       "whatlinkshere-hideredirs": "পুননির্দেশনা আড়াল করো",
+       "whatlinkshere-hidetrans": "অন্তর্ভুক্তকরণ আড়াল করো",
+       "whatlinkshere-hidelinks": "সংযোগ আড়াল করো",
+       "whatlinkshere-hideimages": "ফাইল সংযোগ আড়াল করো",
        "whatlinkshere-filters": "ছাকনী",
        "whatlinkshere-submit": "চলো",
        "autoblockid": "স্বয়ংক্রিয় বাধা #$1",
index 299b757..a3c739a 100644 (file)
        "noname": "তি চুম্পা আতাকুরার নাঙহান না লেপ করিসত।",
        "loginsuccesstitle": "লগইনহান চুমিল",
        "loginsuccess": "'''এরে {{SITENAME}}ত তি \"$1\" হিসাবে না হমাসত।'''",
-       "nosuchuser": "এরে \"$1\" নাঙর কোন আতাকুরা নেই।\nআতাকুরার নাঙ কেইস সংবেদনশীল।\nতর বানানহান খিয়াল কর, নাইলে [[Special:UserLogin/signup|আরাক নুৱা একাউন্ট]]আহান হঙকর।",
+       "nosuchuser": "এরে \"$1\" নাঙর কোন আতাকুরা নেই।\nআতাকুরার নাঙ কেইস সংবেদনশীল।\nতর বানানহান খিয়াল কর, নাইলে [[Special:CreateAccount|আরাক নুৱা একাউন্ট]]আহান হঙকর।",
        "nosuchusershort": "এরে \"$1\" নাঙর কোন আতাকুরা নেই।\nতর বানানহান খিয়াল কর।",
        "nouserspecified": "তি আতাকুরার নাঙ আহান থনা লাগতই।",
        "login-userblocked": "আতাকুরাগরে থেপকরানি অসে। লগইন করে নারবে।",
        "accmailtext": "[[User talk:$1|$1]]র কা খন্তাচাবি(password) $2-রাঙ দিয়াপেঠুৱাদেনা ইল।\nলগ-ইন করানির পিসে তর নুৱা অ্যাকাউন্টর কা ''[[Special:ChangePassword|খন্তাচাবি সিলকরানি]]''র পাতাত্ব সালকরানি একরতই।",
        "newarticle": "(নুৱা)",
        "newarticletext": "তি বিসারার মিলাপ অহান নেয়সে।\nতি চেইলে তলর বক্সগত বিষয়হানর বারে খানি ইকরিয়া ইতুকরে পারর বারো নিবন্ধহান অকরে পারর (আরাকউ হারপানিরকা [$1 পাঙলাক পাতা] চা) পারর।\nযদি হারনাপেয়া এহাত আহিয়া থার অতা ইলে ব্রাউজারর ব্যাক গুতমগত ক্লিক করিয়া আগর পাতাত আল পারর।",
-       "anontalkpagetext": "----''এহান অচিনা অতার য়্যারির পাতাহান, যেগই কোন অ্যাকাউন্ট না খুলিসে, নাইলে ব্যবহার নাকরের।\nঅহানে আমি আইপি ঠিকানা (IP Address)ল অগরে দেখাদেরাঙতা।\nআক্কুসে এসাদে আকহান আইপি ব্যবহার করতে পারে।\nঅহানে তি নিশ্চকে এরে আইপি এহাত্ত উইকিপিডিয়াত হমিয়া কোন হবানেই য়্যারী দেখর, অহান তরে নিঙকরিয়া নাউ ইতে পারে। অহানে হাবিত্ত হবা অর, তি যদি [[Special:UserLogin|লগ-ইন]] করর, নাইলে [[Special:UserLogin/signup|নুৱা একাউন্ট খুলর]] অতা ইলে এসাদে ভুল বুঝাবুঝি নাইব।''",
+       "anontalkpagetext": "----''এহান অচিনা অতার য়্যারির পাতাহান, যেগই কোন অ্যাকাউন্ট না খুলিসে, নাইলে ব্যবহার নাকরের।\nঅহানে আমি আইপি ঠিকানা (IP Address)ল অগরে দেখাদেরাঙতা।\nআক্কুসে এসাদে আকহান আইপি ব্যবহার করতে পারে।\nঅহানে তি নিশ্চকে এরে আইপি এহাত্ত উইকিপিডিয়াত হমিয়া কোন হবানেই য়্যারী দেখর, অহান তরে নিঙকরিয়া নাউ ইতে পারে। অহানে হাবিত্ত হবা অর, তি যদি [[Special:UserLogin|লগ-ইন]] করর, নাইলে [[Special:CreateAccount|নুৱা একাউন্ট খুলর]] অতা ইলে এসাদে ভুল বুঝাবুঝি নাইব।''",
        "noarticletext": "এপাগা এরে পাতাত কোন লেখা নেই। তি মনেইলে হের পাতাহান [[Special:Search/{{PAGENAME}}|এরে চিঙনাঙল বিসারা পারর]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} এহানর বারে লগ বিসারা পারর], নাইলে [{{fullurl:{{FULLPAGENAME}}|action=edit}} এরে পাতা এহান পতা পারর।]",
        "noarticletext-nopermission": "এপাগাউ কোন মেয়েক নেই পাতা এহাত।\nতি [[Special:Search/{{PAGENAME}}|এরে পাতার চিঙনাঙ এহানল বিসারা]] হের পাতা অতাত,\nনাইলে <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} মিল আসে লগ অতাত বিসারা]</span>।",
        "userpage-userdoesnotexist": "আতাকুরা \"<nowiki>$1</nowiki>\"র অ্যাকাউন্টহান না হঙিসে।\nতি পাতা এহান হঙকরানি/পতানি চারাতানা কিতা খালকরিয়া চা।",
index 4a459fa..ae01322 100644 (file)
        "noname": "N'hoc'h eus lakaet anv implijer ebet.",
        "loginsuccesstitle": "Kevreet oc'h.",
        "loginsuccess": "'''Kevreet oc'h bremañ ouzh {{SITENAME}} evel \"$1\".'''",
-       "nosuchuser": "N'eus ket eus an implijer \"$1\".\nKizidik eo anv an implijer ouzh ar pennlizherennoù\nGwiriit eo bet skrivet mat an anv ganeoc'h pe [[Special:UserLogin/signup|krouit ur gont nevez]].",
+       "nosuchuser": "N'eus ket eus an implijer \"$1\".\nKizidik eo anv an implijer ouzh ar pennlizherennoù\nGwiriit eo bet skrivet mat an anv ganeoc'h pe [[Special:CreateAccount|krouit ur gont nevez]].",
        "nosuchusershort": "N'eus perzhiad ebet gantañ an anv « $1 ». Gwiriit ar reizhskrivadur.",
        "nouserspecified": "Ret eo deoc'h spisaat un anv implijer.",
        "login-userblocked": "Stanket eo an implijer-mañ. N'eo ket aotret da gevreañ.",
        "accmailtext": "Kaset ez eus bet ur ger-tremen dargouezhek evit [[User talk:$1|$1]] da $2. Cheñchet e c'hall ar ger-tremen evit ar gont nevez-mañ bezañ war ar bajenn ''[[Special:ChangePassword|cheñch ger-tremen]]'', ur wezh kevreet.",
        "newarticle": "(Nevez)",
        "newarticletext": "Heuliet hoc'h eus ul liamm a gas d'ur bajenn n'eo ket bet savet evit c'hoazh.\nA-benn krouiñ ar bajenn-se, krogit da skrivañ er prenestr skridaozañ dindan (gwelet ar [$1 bajenn skoazell] evit gouzout hiroc'h).\nM'emaoc'h en em gavet amañ dre fazi, klikit war bouton '''kent''' ho merdeer evit mont war ho kiz.",
-       "anontalkpagetext": "---- ''Homañ eo ar bajenn gaozeal evit un implijer(ez) dizanv n'eus ket krouet kont ebet evit c'hoazh pe na implij ket anezhi.\nSetu perak e rankomp ober gant ar chomlec'h IP niverel evit anavezout anezhañ/i.\nGallout a ra ur chomlec'h a seurt-se bezañ rannet etre meur a implijer(ez).\nMa'z oc'h un implijer(ez) dizanv ha ma stadit ez eus bet kaset deoc'h kemennadennoù na sellont ket ouzhoc'h, gallout a rit [[Special:UserLogin/signup|krouiñ ur gont]]pe [[Special:UserLogin|kevreañ]] kuit a vagañ muioc'h a gemmesk gant implijerien dizanv all.",
+       "anontalkpagetext": "---- ''Homañ eo ar bajenn gaozeal evit un implijer(ez) dizanv n'eus ket krouet kont ebet evit c'hoazh pe na implij ket anezhi.\nSetu perak e rankomp ober gant ar chomlec'h IP niverel evit anavezout anezhañ/i.\nGallout a ra ur chomlec'h a seurt-se bezañ rannet etre meur a implijer(ez).\nMa'z oc'h un implijer(ez) dizanv ha ma stadit ez eus bet kaset deoc'h kemennadennoù na sellont ket ouzhoc'h, gallout a rit [[Special:CreateAccount|krouiñ ur gont]]pe [[Special:UserLogin|kevreañ]] kuit a vagañ muioc'h a gemmesk gant implijerien dizanv all.",
        "noarticletext": "N'eus tamm skrid ebet war ar bajenn-mañ evit poent.\nGallout a rit [[Special:Search/{{PAGENAME}}|klask an titl anezhi]] e pajennoù all,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} klask en oberiadennoù liammet], pe [{{fullurl:{{FULLPAGENAME}}|action=edit}} krouiñ ar bajenn]</span>.",
        "noarticletext-nopermission": "N'eus, evit ar mare, tamm testenn ebet war ar bajenn-mañ.\nGallout a rit [[Special:Search/{{PAGENAME}}|klask titl ar bajenn-mañ]] war pajennoù all,\npe <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} klask er marilhoù kar]</span>, met n'oc'h ket aotreet da grouiñ ar bajenn-mañ.",
        "missing-revision": "N'eus ket eus adwel niv. $1 eus ar bajenn anvet « {{FULLPAGENAME}} ».\n\nC'hoarvezout a ra peurliesañ pa vez heuliet ul liamm istorel dispredet war-zu ur bajenn zo bet dilamet.\nGallout a reot kavout muioc'h a vunudoù e [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} renabl an dilamadurioù].",
index 9aefe5b..e4ed30f 100644 (file)
        "noname": "Niste izabrali ispravno korisničko ime.",
        "loginsuccesstitle": "Prijavljen",
        "loginsuccess": "'''Sad ste prijavljeni na {{SITENAME}} kao \"$1\".'''",
-       "nosuchuser": "Ne postoji korisnik s imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite Vaš unos ili [[Special:UserLogin/signup|napravite novi korisnički račun]].",
+       "nosuchuser": "Ne postoji korisnik s imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite Vaš unos ili [[Special:CreateAccount|napravite novi korisnički račun]].",
        "nosuchusershort": "Ne postoji korisnik s imenom \"$1\".\nProvjerite jeste li dobro ukucali.",
        "nouserspecified": "Morate izabrati korisničko ime.",
        "login-userblocked": "Ovaj korisnik je blokiran. Prijava nije dopuštena.",
        "accmailtext": "Nasumično odabrana šifra za [[User talk:$1|$1]] je poslata na adresu $2.\n\nŠifra/lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|izmjene šifre]]'' nakon prijave.",
        "newarticle": "(Novi)",
        "newarticletext": "Došli ste na stranicu koja još nema sadržaja.\n*Ako želite unijeti sadržaj, počnite tipkati u prozor ispod ovog teksta.\n*Ako Vam treba pomoć, idite na [$1 stranicu za pomoć].\n*Ako ste ovamo dospjeli slučajno, kliknite na dugme \"Nazad\" (''Back'') u Vašem internetskom pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio nalog ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:UserLogin/signup|napravite nalog]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
+       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio nalog ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite nalog]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
        "noarticletext": "Na ovoj stranici trenutno nema teksta.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] na drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tražiti u povezanim zapisnicima] ili [{{fullurl:{{FULLPAGENAME}}|action=edit}} napraviti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Trenutno nema teksta na ovoj stranici.\nMožete [[Special:Search/{{PAGENAME}}|tražiti ovaj naslov stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane zapisnike]</span>, ali nemate dozvolu da napravite ovu stranicu.",
        "missing-revision": "Uređivanje broj $1 na stranici \"{{FULLPAGENAME}}\" ne postoji.\n\nOvo se obično dešava kad pratite zastarjelu vezu na stranicu koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protokolu brisanja].",
        "upload-form-label-own-work": "Ovo je moje djelo",
        "upload-form-label-infoform-categories": "Kategorije",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Potvrđujem da postavljam ovu datoteku u skladu s uslovima korištenja i pravilima o licenciranju na {{GRAMMAR:dativ|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "Ako niste u stanju postaviti ovu datoteku pod pravilima {{GRAMMAR:genitiv|{{SITENAME}}}}, molimo zatvorite ovaj prozor i pokušajte drugom metodom.",
-       "upload-form-label-not-own-work-local-local": "Također možete pokušati [[Special:Upload|na standardnoj stranici za postavljanje]].",
-       "upload-form-label-own-work-message-default": "Shvatam da postavljam ovu datoteku na zajedničko spremište. Potvrđujem da to činim u skladu s uslovima korištenja i ovdašnjim pravilima licenciranja.",
-       "upload-form-label-not-own-work-message-default": "Ako niste u stanju postaviti ovu datoteku pod pravilima zajedničkog skladišta, molimo zatvorite ovaj prozor i pokušajte drugom metodom.",
-       "upload-form-label-not-own-work-local-default": "Možete također pokušati koristeći  [[Special:Upload|stranicu za postavljanje na  {{GRAMMAR:dativ|{{SITENAME}}}}]], ako se ova datoteka može postaviti pod tamošnjim pravilima.",
-       "upload-form-label-own-work-message-shared": "Potvrđujem da posjedujem autorska prava za ovu datoteku i slažem se da ću je neopozivo postaviti na Wikimedia Commons pod licencom [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], te se slažem s [https://wikimediafoundation.org/wiki/Terms_of_Use uslovima korištenja].",
-       "upload-form-label-not-own-work-message-shared": "Ako ne posjedujete autorska prava za ovu datoteku ili je želite postaviti pod drugom licencom, imajte na umu da možete koristiti [https://commons.wikimedia.org/wiki/Special:UploadWizard čarobnjak za postavljanje datoteka na Commonsu].",
-       "upload-form-label-not-own-work-local-shared": "Također možete koristiti [[Special:Upload|stranicu za postavljanje datoteka na {{GRAMMAR:dativ|{{SITENAME}}}}]] ako pravila te stranice dozvoljavaju postavljanje ove datoteke.",
+       "upload-form-label-own-work-message-generic-local": "Potvrđujem da postavljam ovu datoteku u skladu s uslovima korištenja i pravilima o licenciranju na {{GRAMMAR:dativ|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Ako niste u stanju postaviti ovu datoteku pod pravilima {{GRAMMAR:genitiv|{{SITENAME}}}}, molimo zatvorite ovaj prozor i pokušajte drugom metodom.",
+       "upload-form-label-not-own-work-local-generic-local": "Također možete pokušati [[Special:Upload|na standardnoj stranici za postavljanje]].",
+       "upload-form-label-own-work-message-generic-foreign": "Shvatam da postavljam ovu datoteku na zajedničko spremište. Potvrđujem da to činim u skladu s uslovima korištenja i ovdašnjim pravilima licenciranja.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Ako niste u stanju postaviti ovu datoteku pod pravilima zajedničkog skladišta, molimo zatvorite ovaj prozor i pokušajte drugom metodom.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Možete također pokušati koristeći  [[Special:Upload|stranicu za postavljanje na  {{GRAMMAR:dativ|{{SITENAME}}}}]], ako se ova datoteka može postaviti pod tamošnjim pravilima.",
        "backend-fail-stream": "Ne mogu emitirati datoteku $1.",
        "backend-fail-backup": "Ne može sigurnosno kopirati datoteku $1.",
        "backend-fail-notexists": "Datoteka $1 ne postoji.",
index cfe09b6..31ccf1d 100644 (file)
@@ -97,7 +97,7 @@
        "tog-ccmeonemails": "Envia’m còpies dels missatges que enviï als altres usuaris",
        "tog-diffonly": "Amaga el contingut de la pàgina davall de la taula de diferències",
        "tog-showhiddencats": "Mostra les categories ocultes",
-       "tog-norollbackdiff": "Omet la pàgina de diferències després de realitzar una reversió",
+       "tog-norollbackdiff": "No mostris les diferències després de realitzar una reversió",
        "tog-useeditwarning": "Avisa'm quan surti d'una pàgina d'edició amb canvis sense desar",
        "tog-prefershttps": "Utilitza sempre una connexió segura en iniciar una sessió",
        "underline-always": "Sempre",
        "noname": "No heu especificat un nom vàlid d'usuari.",
        "loginsuccesstitle": "Sessió iniciada",
        "loginsuccess": "Heu iniciat la sessió a {{SITENAME}} com a «$1».",
-       "nosuchuser": "No hi ha cap usuari anomenat «$1».\nReviseu-ne l'ortografia (recordeu que es distingeixen les majúscules i minúscules), o [[Special:UserLogin/signup|creeu un compte d'usuari nou]].",
+       "nosuchuser": "No hi ha cap usuari anomenat «$1».\nReviseu-ne l'ortografia (recordeu que es distingeixen les majúscules i minúscules), o [[Special:CreateAccount|creeu un compte d'usuari nou]].",
        "nosuchusershort": "No hi ha cap usuari anomenat «$1». Comproveu que ho hàgiu escrit correctament.",
        "nouserspecified": "Heu d'especificar un nom d'usuari.",
        "login-userblocked": "Aquest usuari està bloquejat. Inici de sessió no permès.",
        "botpasswords-created-title": "S'ha creat la contrasenya del bot",
        "botpasswords-created-body": "S'ha creat la contrasenya per al bot «$1» de l'usuari «$2».",
        "botpasswords-updated-title": "Contrasenya de bot actualitzada",
+       "botpasswords-not-exist": "L'usuari «$1» no té una contrasenya de bot anomenada «$2».",
        "resetpass_forbidden": "No poden canviar-se les contrasenyes",
        "resetpass-no-info": "Heu d'estar registrats en un compte per a poder accedir directament a aquesta pàgina.",
        "resetpass-submit-loggedin": "Canvia la contrasenya",
        "minoredit": "Aquesta és una modificació menor",
        "watchthis": "Vigila aquesta pàgina",
        "savearticle": "Desa la pàgina",
+       "publishpage": "Publica la pàgina",
        "preview": "Previsualització",
        "showpreview": "Mostra una previsualització",
        "showdiff": "Mostra els canvis",
        "accmailtext": "S'ha enviat una contrasenya aleatòria a $2 per a {{GENDER:$1|l'usuari|la usuària}} [[User talk:$1|$1]].\n\nLa contrasenya per aquest nou compte pot ser canviada a la pàgina de ''[[Special:ChangePassword|canvi de contrasenya]]'' un cop connectat.",
        "newarticle": "(Nou)",
        "newarticletext": "Heu seguit un enllaç a una pàgina que encara no existeix.\nPer a crear-la, comenceu a escriure en l'espai de sota\n(vegeu l'[$1 ajuda] per a més informació).\nSi sou ací per error, simplement cliqueu al botó «Enrere» del vostre navegador.",
-       "anontalkpagetext": "----''Aquesta és la pàgina de discussió d'un usuari anònim que encara no ha creat un compte o que no fa servir el seu nom registrat. Per tant, hem de fer servir la seua adreça IP numèrica per a identificar-lo. Una adreça IP pot ser compartida per molts usuaris. Si sou un usuari anònim, i trobeu que us han adreçat comentaris inoportuns, si us plau, [[Special:UserLogin/signup|creeu-vos un compte]], o [[Special:UserLogin|entreu en el vostre compte]] si ja en teniu un, per a evitar futures confusions amb altres usuaris anònims.''",
+       "anontalkpagetext": "----''Aquesta és la pàgina de discussió d'un usuari anònim que encara no ha creat un compte o que no fa servir el seu nom registrat. Per tant, hem de fer servir la seua adreça IP numèrica per a identificar-lo. Una adreça IP pot ser compartida per molts usuaris. Si sou un usuari anònim, i trobeu que us han adreçat comentaris inoportuns, si us plau, [[Special:CreateAccount|creeu-vos un compte]], o [[Special:UserLogin|entreu en el vostre compte]] si ja en teniu un, per a evitar futures confusions amb altres usuaris anònims.''",
        "noarticletext": "Actualment no hi ha text en aquesta pàgina.\nPodeu [[Special:Search/{{PAGENAME}}|cercar aquest títol]] en altres pàgines,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercar en els registres]\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear-la ara]</span>.",
        "noarticletext-nopermission": "Actualment no hi ha text en aquesta pàgina.\nPodeu [[Special:Search/{{PAGENAME}}|cercar aquest títol]] en altres pàgines o bé <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercar en els registres relacionats]</span>, però no teniu permisos per crear la pàgina.",
        "missing-revision": "La revisió núm. $1 de la pàgina anomenada «{{FULLPAGENAME}}» no existeix.\n\nNormalment això ocorre quan seguiu un enllaç d’historial desactualitzat que apunta a una pàgina que s’ha suprimit.\nPodeu trobar detalls en el [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registre de supressions].",
        "upload-form-label-own-work": "Això és el meu propi treball",
        "upload-form-label-infoform-categories": "Categories",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-not-own-work-local-local": "També podeu provar [[Special:Upload|la pàgina de càrrega per defecte]].",
-       "upload-form-label-own-work-message-default": "Entenc que esteu carregant el fitxer en un repositori compartit. Confirmo que ho estic fent seguint les condicions d'ús i les polítiques de llicenciament que s'hi apliquen.",
+       "upload-form-label-not-own-work-local-generic-local": "També podeu provar [[Special:Upload|la pàgina de càrrega per defecte]].",
+       "upload-form-label-own-work-message-generic-foreign": "Entenc que esteu carregant el fitxer en un repositori compartit. Confirmo que ho estic fent seguint les condicions d'ús i les polítiques de llicenciament que s'hi apliquen.",
        "backend-fail-stream": "No s'ha pogut transmetre el fitxer $1.",
        "backend-fail-backup": "No s'ha pogut fer una còpia de seguretat del fitxer $1.",
        "backend-fail-notexists": "El fitxer $1 no existeix.",
        "whatlinkshere-prev": "{{PLURAL:$1|anterior|anteriors $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|següent|següents $1}}",
        "whatlinkshere-links": "← enllaços",
-       "whatlinkshere-hideredirs": "$1 redireccions",
-       "whatlinkshere-hidetrans": "$1 inclusions",
-       "whatlinkshere-hidelinks": "$1 enllaços",
+       "whatlinkshere-hideredirs": "Amaga les redireccions",
+       "whatlinkshere-hidetrans": "Amagar transclusions",
+       "whatlinkshere-hidelinks": "Amagar enllaços",
        "whatlinkshere-hideimages": "$1 enllaços de fitxers",
        "whatlinkshere-filters": "Filtres",
        "whatlinkshere-submit": "Vés-hi",
        "exif-compression-6": "JPEG (antic)",
        "exif-copyrighted-true": "Sotmesa a drets d'autor",
        "exif-copyrighted-false": "No s'ha definit l'estat de copyright",
+       "exif-photometricinterpretation-1": "Negra i blanc (negre és 0)",
        "exif-unknowndate": "Data desconeguda",
        "exif-orientation-1": "Normal",
        "exif-orientation-2": "Invertit horitzontalment",
        "confirmemail_body_set": "Algú, probablement vós, des de l'adreça IP $1, \nha establert aquesta adreça de correu electrònic com la del compte «$2» del lloc {{SITENAME}}. \n\nPer confirmar que aquest compte realment us pertany i reactivar \nles facilitats de correu electrònic a {{SITENAME}}, cal que obriu al navegador aquest enllaç:\n\n$3\n\nSi el compte *no* us pertany, cancel·leu l'adreça de correu electrònic seguint aquest enllaç: \n\n$5\n\nAquest codi de confirmació caducarà el $4.",
        "confirmemail_invalidated": "Confirmació d'adreça electrònica cancel·lada",
        "invalidateemail": "Cancel·lació d'adreça electrònica",
+       "notificationemail_subject_changed": "L'adreça registrada de {{SITENAME}} ha canviat",
+       "notificationemail_subject_removed": "L'adreça registrada de {{SITENAME}} ha estat suprimida",
        "scarytranscludedisabled": "[S'ha inhabilitat la transclusió interwiki]",
        "scarytranscludefailed": "[Ha fallat la recuperació de la plantilla per a $1]",
        "scarytranscludefailed-httpstatus": "[Ha fallat la recuperació de la plantilla per a $1: HTTP $2]",
index cf650ea..df8d3a4 100644 (file)
        "noname": "汝未指定蜀萆合法其用戶名。",
        "loginsuccesstitle": "躒入成功",
        "loginsuccess": "'''汝現在已經「$1」其成功躒入{{SITENAME}}了。'''",
-       "nosuchuser": "無總款其用戶名「$1」。\n用户名是大小写敏感其。\n检查汝其拼写,或者覷蜀覷[[Special:UserLogin/signup|開新賬戶]]。",
+       "nosuchuser": "無總款其用戶名「$1」。\n用户名是大小写敏感其。\n检查汝其拼写,或者覷蜀覷[[Special:CreateAccount|開新賬戶]]。",
        "nosuchusershort": "無總款其用戶名「$1」。\n檢查汝其拼寫。",
        "nouserspecified": "汝著指定蜀萆用戶名。",
        "login-userblocked": "茲隻用戶已經乞封鎖去了。登錄是𣍐允許其。",
        "accmailtext": "共[[User talk:$1|$1]]用戶隨機生成其密碼已經發遘$2了。汝登錄以後會使敆[[Special:ChangePassword|修改密碼]]頁面修改茲蜀萆密碼。",
        "newarticle": "(新)",
        "newarticletext": "汝已經跟鏈接跟遘無存在其頁面了。\n卜想創建頁面,敆下底其框框𡅏拍字(覷蜀覷[$1 幫助頁面]有無更更価其幫助)。\n如果汝是無注意來遘茲蜀萆頁面,篤囇汝其瀏覽器上其「返回」按鈕。",
-       "anontalkpagetext": "<em>茲是未登錄其用戶討論頁面。</em>\n故此儂家著使數字IP來確定伊。\n總款其IP地址會乞雅価用戶共享。\n如果蜀隻未登錄其用戶見覺無關係其評論指向汝,起動[[Special:UserLogin/signup|開賬戶]]或者[[Special:UserLogin|登錄]]來避免以後共其它未登錄其用戶混蜀堆。",
+       "anontalkpagetext": "<em>茲是未登錄其用戶討論頁面。</em>\n故此儂家著使數字IP來確定伊。\n總款其IP地址會乞雅価用戶共享。\n如果蜀隻未登錄其用戶見覺無關係其評論指向汝,起動[[Special:CreateAccount|開賬戶]]或者[[Special:UserLogin|登錄]]來避免以後共其它未登錄其用戶混蜀堆。",
        "noarticletext": "現在敆茲蜀頁𡅏無文字。汝會使敆其它其頁面𡅏[[Special:Search/{{PAGENAME}}|討蜀討茲蜀萆標題]],<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 討相關其記錄],或者[{{fullurl:{{FULLPAGENAME}}|action=edit}}編輯茲蜀頁]</span>。",
        "clearyourcache": "'''注意:'''保存以後,汝可能固著刷新汝其瀏覽器緩存來看遘變化。\n* '''火狐/Safari:'''擪下''Shift''篤蜀篤''重新載入'',或者擪蜀擪''Ctrl+F5''或者''Ctrl+R'' (''⌘-R''敆Mac懸頂)\n* '''Google Chrome:'''擪''Ctrl+Shift+R''(敆Mac𡅏使''⌘-Shift-R'')\n* '''Internet Explorer:'''擪''Ctrl''其時候篤蜀篤''刷新'',或者擪''Ctrl+F5''\n* '''Opera:'''敆''工具→首選項''𡅏清除緩存",
        "note": "<strong>注意:</strong>",
index 77dff52..0a4fdf3 100644 (file)
        "noname": "Ахьа декъашхочун цӀе лартӀахь язйина яц.",
        "loginsuccesstitle": "Хьан пароль тӀеэца, марша догӀила Википеди чу!",
        "loginsuccess": "Хlинца ахьа болх бó оцу цlарца $1.",
-       "nosuchuser": "Иштта $1 цӀе йолуш декъашхочун дӀаяздар дац.\nДекъашхой цӀерш хаалуш ю дӀаяздарца элпаш.\nНийса юьй хьажа цӀе я [[Special:UserLogin/signup|дӀаяздар кхолла керла]].",
+       "nosuchuser": "Иштта $1 цӀе йолуш декъашхочун дӀаяздар дац.\nДекъашхой цӀерш хаалуш ю дӀаяздарца элпаш.\nНийса юьй хьажа цӀе я [[Special:CreateAccount|дӀаяздар кхолла керла]].",
        "nosuchusershort": "Ишта «$1» цӀе йолу декъашхо вац/яц. Хьажа цӀе нийса язйина юй.",
        "nouserspecified": "Ахьа декъашхочун цӀе язъян езаш ю.",
        "login-userblocked": "ХӀара декъашхо блоктоьхна ву/ю. Системин чувала/яла магийна дац.",
        "accmailtext": "Пароль декъашхочуьнан [[User talk:$1|$1]], йина ша шех хитта делла чу элпашах, дӀаяхийтина хьокху $2 адрес тӀе.\n\nЧуваьлла/яла чул тӀехьа , кху гӀирса чохь шуьга хийцалур ю ''[[Special:ChangePassword|шай пароль]]''.",
        "newarticle": "(Kерла)",
        "newarticletext": "ХӀара агӀо хӀинца йоцаш ю.\nНагахь иза кхолла лаахь, хӀотта де лахо гуш долу корехь йоза (мадарра хьажа. [$1 гӀон агӀон чу]).\nНагахь гӀалат даьлла цахууш кхечехь кхузе, атта тӀе тӀаӀа йе '''юха йоккхуриг''' хьай браузеран тӀехь.",
-       "anontalkpagetext": "----''ХӀара дийцаре агӀо къайлаха декъашхочуьна ю, хӀинца дӀаяздар доцу, я лелош воцуш/йоцуш.\nЦундела иза вовзийта/йовзийта лелош ду терахьца IP-адрес.\nИза терахь долу меттиг хила мега кхечу декъашхойчух терра.\nНагахь хьо къайлах волу декъашхо валахь хьайна хаам кхаьчна аьлла хеташн, хьуна хьажийна доцуш, дехар до, кхолла хьай меттиг кху чохь[[Special:UserLogin/signup|дӀаяздар кхоллар]] я [[Special:UserLogin|системин чугӀо]],",
+       "anontalkpagetext": "----''ХӀара дийцаре агӀо къайлаха декъашхочуьна ю, хӀинца дӀаяздар доцу, я лелош воцуш/йоцуш.\nЦундела иза вовзийта/йовзийта лелош ду терахьца IP-адрес.\nИза терахь долу меттиг хила мега кхечу декъашхойчух терра.\nНагахь хьо къайлах волу декъашхо валахь хьайна хаам кхаьчна аьлла хеташн, хьуна хьажийна доцуш, дехар до, кхолла хьай меттиг кху чохь[[Special:CreateAccount|дӀаяздар кхоллар]] я [[Special:UserLogin|системин чугӀо]],",
        "noarticletext": "ХӀокху хан чохь кху агӀонца йоза дац.\nШуьга далундерг [[Special:Search/{{PAGENAME}}|лахар ишта хьехош йолу цӀе]] кхечу яззамашкахь,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} лахар кхечу тептаршкахь],\nя '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} кхолла ишта цӀе йолу агӀо]'''</span>.",
        "noarticletext-nopermission": "ХӀокху хан чохь кху агӀонца йоза дац.\nШуьга далундерг [[Special:Search/{{PAGENAME}}|лахар ишта хьехош йолу цӀе]] кхечу яззамашкахь,\nя <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} лаха оцуьнах терадерг кхечу тептаршкахь].</span>",
        "missing-revision": "АгӀона «{{FULLPAGENAME}}» верси $1 яц.\n\nИшта хуьйла ширелла дӀаяьккхина агӀонан хьажораган дихьа делча.\nМа-дара хила мега [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дӀайаьхарш йолу тептар] чохь.",
        "upload-form-label-own-work": "ХӀара сан долара болх бу",
        "upload-form-label-infoform-categories": "Категореш",
        "upload-form-label-infoform-date": "Терахь",
-       "upload-form-label-not-own-work-local-default": "ХӀара файл {{SITENAME}} сайтан бакъонашца чуйоккхила делахь, хьайн таро ю [[Special:Upload|хӀара агӀо]] лелаян.",
-       "upload-form-label-own-work-message-shared": "Аса со хӀокху файлан авторан бакъонаш долахь ерг хилар бакъдо, цундела хӀара файл [https://creativecommons.org/licenses/by-sa/4.0/deed.ru Creative Commons Attribution-ShareAlike 4.0] лицензица Викигуламан чуяккха бакъо ю, цул совнах [https://wikimediafoundation.org/wiki/ хӀокху хьолаца лело] а мега.",
-       "upload-form-label-not-own-work-message-shared": "ХӀокху файлан авторан бакъонаш хьай яцахь, я хьайна кхечу лицензица яржо лууш делахь хьажа [https://commons.wikimedia.org/wiki/Special:UploadWizard Викигуламера чуяхаран говзанча] лелон тароне.",
-       "upload-form-label-not-own-work-local-shared": "ХӀара файл {{SITENAME}} сайтан бакъонашца чуйоккхила делахь, хьайн таро ю [[Special:Upload|хӀара агӀо]] лелаян.",
+       "upload-form-label-not-own-work-local-generic-foreign": "ХӀара файл {{SITENAME}} сайтан бакъонашца чуйоккхила делахь, хьайн таро ю [[Special:Upload|хӀара агӀо]] лелаян.",
        "backend-fail-stream": "ДӀаяккха цатарло файл «$1».",
        "backend-fail-backup": "Таро яц файлан $1 тӀаьхьалонан копиян.",
        "backend-fail-notexists": "Файл $1 яц.",
index 17e3f08..31f3cf0 100644 (file)
        "noname": "Wala ikaw mag-specify og valid nga user name.",
        "loginsuccesstitle": "Malamposon ang pagpaila",
        "loginsuccess": "'''Nailhan ka na sa {{SITENAME}} isip \"$1\".'''",
-       "nosuchuser": "Walay gumagamit nga may pangalang \"$1\".\nCase sensitive ang mga user name.\nI-tsek ang imong espeling, o [[Special:UserLogin/signup|paghimo og bag-ong akawnt]].",
+       "nosuchuser": "Walay gumagamit nga may pangalang \"$1\".\nCase sensitive ang mga user name.\nI-tsek ang imong espeling, o [[Special:CreateAccount|paghimo og bag-ong akawnt]].",
        "nosuchusershort": "Walay gumagamit nga may pangalang \"$1\".\nI-tsek ang imong espeling.",
        "nouserspecified": "Kinahanglan mag-specify ka og username.",
        "wrongpassword": "Sayop nga pasword ang naentra.\nPalihog sulayi'g usab.",
        "allpagessubmit": "Sige",
        "categories": "Mga kategoriya",
        "categoriespagetext": "Ang mosunod nga mga kategoriya adunay sulod nga panid o medya.\n[[Special:UnusedCategories|Unused categories]] are not shown here.\nAlso see [[Special:WantedCategories|wanted categories]].",
-       "special-categories-sort-abc": "han-aya nga paalpabetikal",
        "linksearch": "Mga sumpay sa gawas",
        "listgrouprights-members": "(talaan sa mga miyembro)",
        "emailuser": "I-email kaning gumagamit",
index 696fffe..6abe392 100644 (file)
        "noname": "ناوی بەکارهێنەرییەکی گۆنجاوت دیاری نەکردووه.",
        "loginsuccesstitle": "چوویە ناوەوە",
        "loginsuccess": "'''ئێستا بە ناوی «$1»ەوە لە {{SITENAME}} چوویتەتەژوورەوە.'''",
-       "nosuchuser": "بەکارھێنەرێک بە ناوی «$1» نیە.\nناوی بەکارھێنەر بە گەورە و بچووک بوونی پیتەکان ھەستیارە.\nڕێنووسەکەت چاولێکەرەوە، یان [[Special:UserLogin/signup|ھەژمارێکی نوێ دروست بکە]].",
+       "nosuchuser": "بەکارھێنەرێک بە ناوی «$1» نیە.\nناوی بەکارھێنەر بە گەورە و بچووک بوونی پیتەکان ھەستیارە.\nڕێنووسەکەت چاولێکەرەوە، یان [[Special:CreateAccount|ھەژمارێکی نوێ دروست بکە]].",
        "nosuchusershort": "بەکارھێنەرێک بە ناوی «$1»ەوە نیە.\nبە نووسراوەکەتدا بچۆرەوە.",
        "nouserspecified": "دەبێت ناوێکی بەکارھێنەری دەستنیشان بکەیت.",
        "login-userblocked": "ئەم بەکارهێنەرە بڵۆک کراوە. چوونەژوورەوە ڕێگەپێنەدراوە.",
        "accmailtext": "تێپەڕوشەیەک کە بە هەڕەمەکی بۆ [[User talk:$1|$1]] دروست کرا، نێررا بۆ $2. دەتوانیت لە پەڕەی <em>[[Special:ChangePassword|گۆڕینی تێپەڕوشەدا]]</em> لە کاتی چوونەژوورەوەدا بیگۆڕی.",
        "newarticle": "(نوێ)",
        "newarticletext": "بە دوای بەستەری پەڕەیەک کەوتووی کە ھێشتا دروست نەکراوە.\nبۆ دروست کردنی پەڕەکە، لە چوارچێوەکەی خوارەوە دەست بکە بە تایپ کردن. (بۆ زانیاری زورتر\n[$1 یارمەتی] ببینە).\nئەگەر بە ھەڵەوە ھاتویتە ئێرە، لە سەر دوگمەی '''back'''ی وێبگەڕەکەت کلیک بکە.",
-       "anontalkpagetext": "----''ئەمە لاپەڕەی وتووێژە بۆ بەکارهێنەرێکی نەناسراوە کە هێشتا هەژمارەی درووست‌نەکردووه یان کەڵکی‌ لێ وەرناگرێ .\nلەبەر ئەوە مەجبوورین ئای‌پی ئەدرەسەکی ژمارەیی بۆ ناساندنی بەکار بێنین.\nئای‌پی ئەدرەسی وا لەوانەیه لە لایەن چەندین بەکارهێنەروە بەکاربێت.\nئەگەر تۆ بەکارهێنەرێکی نەناسراوی و هەست ئەکەی ئەم لێدوانە پەیوەندی بە تۆوە نیە تکایە [[Special:UserLogin/signup|ھەژمارێکی نوێ دروست بکە]] یان [[Special:UserLogin|بچۆ ژوورەوە]] لەبەر ئەوەی لەداهاتوودا دەگەڵ بەکارهێنەرانی‌ نەناسراوی دی تێکەڵ نەکرێیت. ''",
+       "anontalkpagetext": "----''ئەمە لاپەڕەی وتووێژە بۆ بەکارهێنەرێکی نەناسراوە کە هێشتا هەژمارەی درووست‌نەکردووه یان کەڵکی‌ لێ وەرناگرێ .\nلەبەر ئەوە مەجبوورین ئای‌پی ئەدرەسەکی ژمارەیی بۆ ناساندنی بەکار بێنین.\nئای‌پی ئەدرەسی وا لەوانەیه لە لایەن چەندین بەکارهێنەروە بەکاربێت.\nئەگەر تۆ بەکارهێنەرێکی نەناسراوی و هەست ئەکەی ئەم لێدوانە پەیوەندی بە تۆوە نیە تکایە [[Special:CreateAccount|ھەژمارێکی نوێ دروست بکە]] یان [[Special:UserLogin|بچۆ ژوورەوە]] لەبەر ئەوەی لەداهاتوودا دەگەڵ بەکارهێنەرانی‌ نەناسراوی دی تێکەڵ نەکرێیت. ''",
        "noarticletext": "ھەنووکە ھیچ دەقێک لەم پەڕەیەدا نییە.\nدەتوانیت لە پەڕەکانی تردا [[Special:Search/{{PAGENAME}}|بۆ ئەم سەرناوە بگەڕێیت]]،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} لە لۆگە پەیوەندیدارەکاندا بگەڕێیت]،\nیان [{{fullurl:{{FULLPAGENAME}}|action=edit}} ئەم پەڕەیە دروست بکەیت]</span>.",
        "noarticletext-nopermission": "ھەنووکە ھیچ دەقێک لەم پەڕەیەدا نییە.\nدەتوانی لە پەڕەکانی تردا [[Special:Search/{{PAGENAME}}|بۆ ئەم ناوە بگەڕێی]]، یان <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} لە لۆگە پەیوەندیدارەکاندا بگەڕێی]</span>، بەڵام ناتوانی ئەم پەڕەیە دروست بکەی.",
        "userpage-userdoesnotexist": "ھەژماری بەکارھێنەریی «$1» تۆمار نەکراوە.\nتکایە دڵنیا ببەرەوە ئەگەر دەتھەوێت ئەم پەڕەیە دروست یان دەستکاری بکەیت.",
index 089bfc3..bfa212d 100644 (file)
        "noname": "Догъру къулланыджы адыны кирсетмединъиз.",
        "loginsuccesstitle": "Кириш япылды",
        "loginsuccess": "'''$1 адынен {{SITENAME}} сайтында чалышып оласынъыз.'''",
-       "nosuchuser": "«$1» адлы къулланыджы ёкъ.\nКъулланыджы адларында буюк ве кичик арифлер арасында фаркъ бар.\nДогъру язгъанынъызны тешкеринъиз я да [[Special:UserLogin/signup|янъы къулланыджы эсабыны ачынъыз]].",
+       "nosuchuser": "«$1» адлы къулланыджы ёкъ.\nКъулланыджы адларында буюк ве кичик арифлер арасында фаркъ бар.\nДогъру язгъанынъызны тешкеринъиз я да [[Special:CreateAccount|янъы къулланыджы эсабыны ачынъыз]].",
        "nosuchusershort": "«$1» адлы къулланыджы тапыламады. Адынъызны догъру язгъанынъыздан эмин олунъыз.",
        "nouserspecified": "Къулланыджы адыны кирсетмек керексинъиз.",
        "login-userblocked": "Бу къулланыджы блок этильген. Кирмеге рухсет этильмей.",
        "accmailtext": "[[User talk:$1|$1]] ичюн тесадуфий ишаретлерден яратылгъан пароль $2 адресине ёлланды.\n\nБу янъы эсап ичюн пароль, кириш япкъандан сонъ ''[[Special:ChangePassword|парольни денъиштир]]'' болюгинде денъиштирилип олур.",
        "newarticle": "(Янъы)",
        "newarticletext": "Сиз бу багълантынен шимдилик ёкъ олгъан саифеге авуштынъыз. Янъы бир саифе яратмакъ ичюн ашагъыда булунгъан пенджереге метин язынъыз (тафсилятлы малюмат алмакъ ичюн [$1 ярдым саифесине] бакъынъыз). Бу саифеге тесадюфен авушкъан олсанъыз, браузеринъиздеки '''кери''' дёгмесине басынъыз.",
-       "anontalkpagetext": "----''Бу музакере саифеси шимдилик къайд олунмагъан я да отурымыны ачмагъан адсыз (аноним) къулланыджыгъа менсюптир. Идентификация ичюн IP адрес ишлетиле. Бир IP адресинден бир къач къулланыджы файдаланып ола.\nЭгер сиз аноним къулланыджы олсанъыз ве сизге кельген беянатларны янълыштан кельгенини беллесенъиз, лютфен, артыкъ бунынъ киби къарышыкълыкъ олмасын деп [[Special:UserLogin/signup|къайд олунъыз]] я да [[Special:UserLogin|отурым ачынъыз]].''",
+       "anontalkpagetext": "----''Бу музакере саифеси шимдилик къайд олунмагъан я да отурымыны ачмагъан адсыз (аноним) къулланыджыгъа менсюптир. Идентификация ичюн IP адрес ишлетиле. Бир IP адресинден бир къач къулланыджы файдаланып ола.\nЭгер сиз аноним къулланыджы олсанъыз ве сизге кельген беянатларны янълыштан кельгенини беллесенъиз, лютфен, артыкъ бунынъ киби къарышыкълыкъ олмасын деп [[Special:CreateAccount|къайд олунъыз]] я да [[Special:UserLogin|отурым ачынъыз]].''",
        "noarticletext": "Бу саифе шимди боштыр. Бу серлеваны башкъа саифелерде [[Special:Search/{{PAGENAME}}|къыдырып оласынъыз]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} багълы журнал къайдларыны къыдырып оласынъыз] я да бу саифени озюнъиз [{{fullurl:{{FULLPAGENAME}}|action=edit}} язып оласынъыз]</span>.",
        "noarticletext-nopermission": "Бу саифе шимди боштыр. Бу серлеваны башкъа саифелерде [[Special:Search/{{PAGENAME}}|къыдыра биле]] я да <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} багълы журналларны козьден кечире билесинъиз]</span>.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" адлы къулланыджы ёкътыр. Тамам бу саифени денъиштирмеге истегенинъизни тешкеринъиз.",
        "allpages-bad-ns": "{{SITENAME}} сайтында «$1» исим фезасы ёкътыр.",
        "categories": "Саифе категориялары",
        "categoriespagetext": "Ашагъыдаки {{PLURAL:$1|1=категорияда|категорияларда}} саифе я да медиа файллар бар.\n[[Special:UnusedCategories|Къулланылмагъан категориялар]] мында косьтерильмеген.\nАйрыджа [[Special:WantedCategories|талап этильген категорияларнынъ джедвелине]] де бакъынъыз.",
-       "special-categories-sort-count": "сайыларына коре сырала",
-       "special-categories-sort-abc": "элифбе сырасынен сырала",
        "linksearch": "Тыш багълантылар",
        "linksearch-pat": "Къыдырув ичюн шаблон:",
        "linksearch-ns": "Исим фезасы:",
        "watchlisttools-raw": "Козетюв джедвелини адий метин оларакъ денъиштир",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|музакере]])",
        "version": "Версия",
-       "fileduplicatesearch-legend": "Дубликатны къыдыр",
        "fileduplicatesearch-filename": "Файл ады:",
        "fileduplicatesearch-submit": "Къыдыр",
        "fileduplicatesearch-info": "$1 × $2 пиксел<br />Файл буюклиги: $3<br />MIME чешити: $4",
index ffdd83c..fc114e5 100644 (file)
        "noname": "Doğru qullanıcı adını kirsetmediñiz.",
        "loginsuccesstitle": "Kiriş yapıldı",
        "loginsuccess": "'''$1 adınen {{SITENAME}} saytında çalışıp olasıñız.'''",
-       "nosuchuser": "\"$1\" adlı qullanıcı yoq.\nQullanıcı adlarında büyük ve kiçik arifler arasında farq bar.\nDoğru yazğanıñıznı teşkeriñiz ya da [[Special:UserLogin/signup|yañı qullanıcı esabını açıñız]].",
+       "nosuchuser": "\"$1\" adlı qullanıcı yoq.\nQullanıcı adlarında büyük ve kiçik arifler arasında farq bar.\nDoğru yazğanıñıznı teşkeriñiz ya da [[Special:CreateAccount|yañı qullanıcı esabını açıñız]].",
        "nosuchusershort": "\"$1\" adlı qullanıcı tapılamadı. Adıñıznı doğru yazğanıñızdan emin oluñız.",
        "nouserspecified": "Qullanıcı adını kirsetmek kereksiñiz.",
        "login-userblocked": "Bu qullanıcı blok etilgen. Kirmege ruhset etilmey.",
        "accmailtext": "[[User talk:$1|$1]] içün tesadufiy işaretlerden yaratılğan parol $2 adresine yollandı.\n\nBu yañı esap içün parol, kiriş yapqandan soñ ''[[Special:ChangePassword|parolni deñiştir]]'' bölüginde deñiştirilip olur.",
        "newarticle": "(Yañı)",
        "newarticletext": "Siz bu bağlantınen şimdilik yoq olğan saifege avuştıñız. Yañı bir saife yaratmaq içün aşağıda bulunğan pencerege metin yazıñız (tafsilâtlı malümat almaq içün [$1 yardım saifesine] baqıñız). Bu saifege tesadüfen avuşqan olsañız, brauzeriñizdeki '''keri''' dögmesine basıñız.",
-       "anontalkpagetext": "----''Bu muzakere saifesi şimdilik qayd olunmağan ya da oturımını açmağan adsız (anonim) qullanıcığa mensüptir. İdentifikatsiya içün IP adres işletile.\nBir IP adresinden bir qaç qullanıcı faydalanıp ola.\nEger siz anonim qullanıcı olsañız ve sizge kelgen beyanatlarnı yañlıştan kelgenini belleseñiz, lütfen, artıq bunıñ kibi qarışıqlıq olmasın dep [[Special:UserLogin/signup|qayd oluñız]] ya da [[Special:UserLogin|oturım açıñız]].''",
+       "anontalkpagetext": "----''Bu muzakere saifesi şimdilik qayd olunmağan ya da oturımını açmağan adsız (anonim) qullanıcığa mensüptir. İdentifikatsiya içün IP adres işletile.\nBir IP adresinden bir qaç qullanıcı faydalanıp ola.\nEger siz anonim qullanıcı olsañız ve sizge kelgen beyanatlarnı yañlıştan kelgenini belleseñiz, lütfen, artıq bunıñ kibi qarışıqlıq olmasın dep [[Special:CreateAccount|qayd oluñız]] ya da [[Special:UserLogin|oturım açıñız]].''",
        "noarticletext": "Bu saife şimdi boştır. Bu serlevanı başqa saifelerde [[Special:Search/{{PAGENAME}}|qıdırıp olasıñız]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} bağlı jurnal qaydlarını qıdırıp olasıñız] ya da bu saifeni özüñiz [{{fullurl:{{FULLPAGENAME}}|action=edit}} yazıp olasıñız]</span>.",
        "noarticletext-nopermission": "Bu saife şimdi boştır. Bu serlevanı başqa saifelerde [[Special:Search/{{PAGENAME}}|qıdıra bile]] ya da <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} bağlı jurnallarnı közden keçire bilesiñiz]</span>.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" adlı qullanıcı yoqtır. Tamam bu saifeni deñiştirmege istegeniñizni teşkeriñiz.",
        "allpages-bad-ns": "{{SITENAME}} saytında \"$1\" isim fezası yoqtır.",
        "categories": "Saife kategoriyaları",
        "categoriespagetext": "Aşağıdaki {{PLURAL:$1|kategoriyada|kategoriyalarda}} saife ya da media fayllar bar.\n[[Special:UnusedCategories|Qullanılmağan kategoriyalar]] mında kösterilmegen.\nAyrıca [[Special:WantedCategories|talap etilgen kategoriyalarnıñ cedveline]] de baqıñız.",
-       "special-categories-sort-count": "sayılarına köre sırala",
-       "special-categories-sort-abc": "elifbe sırasınen sırala",
        "linksearch": "Tış bağlantılar",
        "linksearch-pat": "Qıdıruv içün şablon:",
        "linksearch-ns": "İsim fezası:",
        "watchlisttools-raw": "Közetüv cedvelini adiy metin olaraq deñiştir",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|muzakere]])",
        "version": "Versiya",
-       "fileduplicatesearch-legend": "Dublikatnı qıdır",
        "fileduplicatesearch-filename": "Fayl adı:",
        "fileduplicatesearch-submit": "Qıdır",
        "fileduplicatesearch-info": "$1 × $2 piksel<br />Fayl büyükligi: $3<br />MIME çeşiti: $4",
index 98b5cee..732d94b 100644 (file)
        "noname": "{{GENDER:|Nezadal|Nezadala|Nezadali}} jste platné uživatelské jméno.",
        "loginsuccesstitle": "Přihlášení bylo úspěšné",
        "loginsuccess": "<strong>Nyní jste na {{grammar:6sg|{{SITENAME}}}} {{GENDER:$1|přihlášen jako uživatel|přihlášena jako uživatelka}} „$1“.</strong>",
-       "nosuchuser": "Neexistuje uživatel se jménem „$1“. U uživatelských jmen se rozlišují malá/velká písmena. Zkontrolujte zápis, nebo si [[Special:UserLogin/signup|vytvořte nový účet]].",
+       "nosuchuser": "Neexistuje uživatel se jménem „$1“. U uživatelských jmen se rozlišují malá/velká písmena. Zkontrolujte zápis, nebo si [[Special:CreateAccount|vytvořte nový účet]].",
        "nosuchusershort": "Neexistuje uživatel se jménem „$1“. Zkontrolujte zápis.",
        "nouserspecified": "Musíte zadat uživatelské jméno.",
        "login-userblocked": "{{GENDER:$1|Tento uživatel je zablokován|Tato uživatelka je zablokována}}. Přihlášení není dovoleno.",
        "accmailtext": "Náhodně vygenerované heslo pro uživatele [[User talk:$1|$1]] bylo odesláno na $2. Po přihlášení ho bude možno změnit na [[Special:ChangePassword|stránce pro změnu hesla]].",
        "newarticle": "(Nový)",
        "newarticletext": "Následovali jste odkaz na stránku, která dosud neexistuje.\nPokud ji chcete vytvořit, začněte psát do rámečku níže (více informací najdete v [$1 nápovědě]).\nPokud jste zde omylem, stiskněte ve svém prohlížeči tlačítko <strong>Zpět</strong>.",
-       "anontalkpagetext": "----\n<em>Toto je diskusní stránka anonymního uživatele, který si dosud nevytvořil účet nebo ho nepoužívá.</em>\nK&nbsp;jeho identifikaci proto musíme používat číselnou IP adresu. Takovou IP adresu může sdílet několik uživatelů. Pokud jste anonymní uživatel a&nbsp;máte za to, že jsou vám adresovány irelevantní komentáře, prosíme, [[Special:UserLogin/signup|vytvořte si účet]] nebo [[Special:UserLogin|se přihlaste]], vyhnete se tím budoucí záměně s&nbsp;jinými anonymními uživateli.",
+       "anontalkpagetext": "----\n<em>Toto je diskusní stránka anonymního uživatele, který si dosud nevytvořil účet nebo ho nepoužívá.</em>\nK&nbsp;jeho identifikaci proto musíme používat číselnou IP adresu. Takovou IP adresu může sdílet několik uživatelů. Pokud jste anonymní uživatel a&nbsp;máte za to, že jsou vám adresovány irelevantní komentáře, prosíme, [[Special:CreateAccount|vytvořte si účet]] nebo [[Special:UserLogin|se přihlaste]], vyhnete se tím budoucí záměně s&nbsp;jinými anonymními uživateli.",
        "noarticletext": "Tato stránka zatím neobsahuje žádný text.\nMůžete [[Special:Search/{{PAGENAME}}|zkusit tento název vyhledat]] na jiných stránkách, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} prohlédnout si příslušné protokolovací záznamy] nebo [{{fullurl:{{FULLPAGENAME}}|action=edit}} tuto stránku založit]</span>.",
        "noarticletext-nopermission": "Tato stránka zatím neobsahuje žádný text.\nMůžete [[Special:Search/{{PAGENAME}}|zkusit tento název vyhledat]] na jiných stránkách nebo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} si prohlédnout příslušné protokolovací záznamy]</span>, ale na založení této stránky nemáte oprávnění.",
        "missing-revision": "Revize #$1 stránky s názvem „{{FULLPAGENAME}}“ neexistuje.\n\nToto je obvykle způsobeno tím, že jste následovali zastaralý odkaz na historickou verzi stránky, jež byla smazána.\nPodrobnosti mohou být uvedeny v [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} knize smazaných stránek].",
        "right-override-export-depth": "Exportovat stránky včetně odkazovaných stránek až do hloubky 5",
        "right-sendemail": "Odesílání e-mailů ostatním uživatelům",
        "right-passwordreset": "Prohlížení e-mailů pro znovunastavení hesla",
-       "right-managechangetags": "Vytváření [[Special:Tags|značek]] a jejich mazání z databáze",
+       "right-managechangetags": "Vytváření a (de)aktivace [[Special:Tags|značek]]",
        "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í",
+       "right-deletechangetags": "Mazání [[Special:Tags|značek]] z databáze",
        "grant-generic": "Balíček oprávnění „$1“",
        "grant-group-page-interaction": "Interakce se stránkami",
        "grant-group-file-interaction": "Interakce se soubory",
        "action-viewmyprivateinfo": "prohlížet si své soukromé údaje",
        "action-editmyprivateinfo": "změnit své soukromé údaje",
        "action-editcontentmodel": "editovat model obsahu stránky",
-       "action-managechangetags": "vytvářet a mazat značky z databáze",
+       "action-managechangetags": "vytvářet a (de)aktivovat značky",
        "action-applychangetags": "přidávat značky k vlastním změnám",
        "action-changetags": "přidávat libovolné značky na jednotlivé revize a protokolovací záznamy a odebírat je",
+       "action-deletechangetags": "mazat značky z databáze",
        "nchanges": "$1 {{PLURAL:$1|změna|změny|změn}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od poslední návštěvy}}",
        "enhancedrc-history": "historie",
        "upload-form-label-own-work": "Je to mé vlastní dílo",
        "upload-form-label-infoform-categories": "Kategorie",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Potvrzuji, že tento soubor načítám v souladu s podmínkami užití a licenčními pravidly na {{grammar:6sg|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "Pokud nemůžete tento soubor načíst v souladu s pravidly {{grammar:2sg|{{SITENAME}}}}, zavřete prosím tento dialog a zkuste jiný způsob.",
-       "upload-form-label-not-own-work-local-local": "Také můžete zkusit [[Special:Upload|standardní stránku pro načítání souborů]].",
-       "upload-form-label-own-work-message-default": "Chápu, že soubor načítám na sdílené úložiště. Potvrzuji, že tak činím v souladu s tamními podmínkami užití a licenčními pravidly.",
-       "upload-form-label-not-own-work-message-default": "Pokud nemůžete tento soubor načíst v souladu s pravidly sdíleného úložiště, zavřete prosím tento dialog a zkuste jiný způsob.",
-       "upload-form-label-not-own-work-local-default": "Také můžete zkusit [[Special:Upload|stránku pro načítání souborů na {{grammar:6sg|{{SITENAME}}}}]], pokud tam lze soubor podle tamních pravidel načíst.",
-       "upload-form-label-own-work-message-shared": "Potvrzuji, že jsem držitelem autorských práv k tomuto souboru a neodvolatelně souhlasím s jeho zveřejněním na Wikimedia Commons pod licencí [https://creativecommons.org/licenses/by-sa/4.0/deed.cs Creative Commons Uveďte původ – Zachovejte licenci 4.0] a souhlasím s [https://wikimediafoundation.org/wiki/Terms_of_Use/cs Podmínkami užití].",
-       "upload-form-label-not-own-work-message-shared": "Pokud nejste držiteli autorských práv k tomuto souboru nebo si ho přejete zveřejnit pod jinou licencí, zvažte použití [https://commons.wikimedia.org/wiki/Special:UploadWizard Průvodce načítáním souborů na Commons].",
-       "upload-form-label-not-own-work-local-shared": "Také můžete zkusit [[Special:Upload|stránku pro načítání souborů na {{grammar:6sg|{{SITENAME}}}}]], pokud tamní pravidla načtení tohoto souboru dovolují.",
+       "upload-form-label-own-work-message-generic-local": "Potvrzuji, že tento soubor načítám v souladu s podmínkami užití a licenčními pravidly na {{grammar:6sg|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Pokud nemůžete tento soubor načíst v souladu s pravidly {{grammar:2sg|{{SITENAME}}}}, zavřete prosím tento dialog a zkuste jiný způsob.",
+       "upload-form-label-not-own-work-local-generic-local": "Také můžete zkusit [[Special:Upload|standardní stránku pro načítání souborů]].",
+       "upload-form-label-own-work-message-generic-foreign": "Chápu, že soubor načítám na sdílené úložiště. Potvrzuji, že tak činím v souladu s tamními podmínkami užití a licenčními pravidly.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Pokud nemůžete tento soubor načíst v souladu s pravidly sdíleného úložiště, zavřete prosím tento dialog a zkuste jiný způsob.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Také můžete zkusit [[Special:Upload|stránku pro načítání souborů na {{grammar:6sg|{{SITENAME}}}}]], pokud tam lze soubor podle tamních pravidel načíst.",
        "backend-fail-stream": "Soubor $1 nelze streamovat.",
        "backend-fail-backup": "Soubor $1 nelze zazálohovat.",
        "backend-fail-notexists": "Soubor $1 neexistuje.",
        "whatlinkshere-prev": "{{PLURAL:$1|předchozí|předchozí $1|předchozích $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|následující|následující $1|následujících $1}}",
        "whatlinkshere-links": "← odkazy",
-       "whatlinkshere-hideredirs": "$1 přesměrování",
-       "whatlinkshere-hidetrans": "$1 vložení",
-       "whatlinkshere-hidelinks": "$1 odkazy",
-       "whatlinkshere-hideimages": "$1 vložení souboru",
+       "whatlinkshere-hideredirs": "Skrýt přesměrování",
+       "whatlinkshere-hidetrans": "Skrýt vložení",
+       "whatlinkshere-hidelinks": "Skrýt odkazy",
+       "whatlinkshere-hideimages": "Skrýt vložení souboru",
        "whatlinkshere-filters": "Filtry",
        "whatlinkshere-submit": "Přejít",
        "autoblockid": "Autoblok #$1",
        "timezone-local": "místní čas",
        "duplicate-defaultsort": "Upozornění: Implicitní klíč řazení (DEFAULTSORTKEY) „$2“ přepisuje dříve nastavenou hodnotu „$1“.",
        "duplicate-displaytitle": "<strong>Upozornění:</strong> Předchozí zobrazovaný název „$1“ je nahrazen zobrazovaným názvem „$2“.",
+       "restricted-displaytitle": "<strong>Upozornění:</strong> Zobrazovaný název „$1“ byl ignorován, neboť není ekvivalentní skutečnému názvu stránky.",
        "invalid-indicator-name": "<strong>Chyba:</strong> Atribut <code>name</code> indikátoru stavu stránky nesmí být prázdný.",
        "version": "Verze",
        "version-extensions": "Nainstalovaná rozšíření",
        "tags-delete-not-found": "Značka „$1“ neexistuje.",
        "tags-delete-too-many-uses": "Značkou „$1“ {{PLURAL:$2|je označena více než $2 revize|jsou označeny více než $2 revize|je označeno více než $2 revizí}}, což znamená, že ji nelze smazat.",
        "tags-delete-warnings-after-delete": "Značka „$1“ byla smazána, ale {{PLURAL:$2|bylo zjištěno|byla zjištěna}} následující varování:",
+       "tags-delete-no-permission": "Nemáte oprávnění mazat značky pro změny.",
        "tags-activate-title": "Aktivovat značku",
        "tags-activate-question": "Chystáte se aktivovat značku „$1“.",
        "tags-activate-reason": "Důvod:",
index aca1e80..99eac43 100644 (file)
        "loginerror": "Fela logòwaniô",
        "loginsuccesstitle": "ùdałé logòwanié",
        "loginsuccess": "Të jes wlogòwóny do {{SITENAME}} jakno \"$1\".",
-       "nosuchuser": "Nie dô brëkòwnika ò mionie \"$1\".\nSprôwdzë pisënk abò [[Special:UserLogin/signup|ùsôdzë nowé kònto]].",
+       "nosuchuser": "Nie dô brëkòwnika ò mionie \"$1\".\nSprôwdzë pisënk abò [[Special:CreateAccount|ùsôdzë nowé kònto]].",
        "nouserspecified": "Mùszisz pòdac miono brëkòwnika.",
        "wrongpassword": "Lëchô parola.\nSpróbùjë znowa.",
        "wrongpasswordempty": "Wpisónô parola je pùstô\nSpróbùjë znowa.",
        "accmailtext": "Przëtrôfkòwò wëgenerowónô parola dlô [[User talk:$1|$1]] òsta wësłónô do $2.\n\nParolã dlô negò nowégò kònta mòże zmienic pò wlogòwaniu na starnie ''[[Special:ChangePassword|zjinaka parolë]]''.",
        "newarticle": "(Nowi)",
        "newarticletext": "Môsz przëszłi z lënkù do starnë jaka jesz nie òbstoji.\nBë ùsôdzëc artikel, naczni pisac w kastce niżi (òb. [$1 starnã pòmòcë]\ndlô wicy wëdowiédzë).\nJeżlë jes të tuwò bez zmiłkã, le klëkni w swòjim przezérnikù knąpã '''nazôd'''.",
-       "anontalkpagetext": "----''To je starna dyskùsëji anonimòwiégò brëkòwnika, chtëren nie ùsôdzëł jesz swòjegò kòntae, abò gò nie brëkùje.\nAbë gò rozpòznac, ùżëwómë adresów IP.\nTakô adresa IP, mòże bëc równak brëkòwónô przez wiele lëdzy.\nJeżlë jes anonimòwim brëkòwnikã ë ùwôżôsz, że ne wiadła nie są do ce sczerowóne, tedë [[Special:UserLogin/signup|ùsôdzë nowé kònto]] abò [[Special:UserLogin|wlogùjë sã]], bë niechac niezrozmeiniô z jinyma anonimòwima brëkòwnikama.''",
+       "anontalkpagetext": "----''To je starna dyskùsëji anonimòwiégò brëkòwnika, chtëren nie ùsôdzëł jesz swòjegò kòntae, abò gò nie brëkùje.\nAbë gò rozpòznac, ùżëwómë adresów IP.\nTakô adresa IP, mòże bëc równak brëkòwónô przez wiele lëdzy.\nJeżlë jes anonimòwim brëkòwnikã ë ùwôżôsz, że ne wiadła nie są do ce sczerowóne, tedë [[Special:CreateAccount|ùsôdzë nowé kònto]] abò [[Special:UserLogin|wlogùjë sã]], bë niechac niezrozmeiniô z jinyma anonimòwima brëkòwnikama.''",
        "noarticletext": "Felëje starna ò tim titlu.\nMòżesz [[Special:Search/{{PAGENAME}}|szëkac za {{PAGENAME}} na jinych starnach]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} szëkac w logù] abò [{{fullurl:{{FULLPAGENAME}}|action=edit}} ùsôdzëc nã starnã]</span>",
        "clearyourcache": "'''Bôczë: Pò zapisanim, mòże bãdzesz mùszôł òminąc pamiãc przezérnika bë òbaczëc zmianë.'''\n'''Mozilla / Firefox / Safari:''' przëtrzëmôj ''Shift'' òbczas klëkaniô na ''Zladëjë znowa'', abò wcësni ''Ctrl-F5'' abò ''Ctrl-R'' (''Command-R'' na kòmpùtrach Mac);\n'''Konqueror:''': klëkni na knąpã ''Zladëjë znowa'', abò wcësni ''F5'';\n'''Opera:''' wëczëszczë pòdrãczną pamiãc w ''Tools→Preferences'';\n'''Internet Explorer:'''przëtrzëmôj ''Ctrl'' òbczas klëkaniô na ''Zladëjë znowa'', abò wcësni ''Ctrl-F5''.",
        "updated": "(Zaktualnioné)",
        "uploadwarning": "Òstrzega ò wladënkù",
        "savefile": "Zapiszë lôpk",
        "uploaddisabled": "Przeprôszómë! Mòżlëwòta wladënkù lopków na nen serwer òsta wëłączonô.",
-       "upload-success-subj": "Wladënk darzëł sã",
        "listfiles": "Lësta òbrôzków",
        "listfiles_user": "Brëkòwnik",
        "file-anchor-link": "Òbrôzk",
        "linksearch": "Bùtnowé lënczi",
        "listgrouprights-members": "(lësta nôlëżników karna)",
        "emailuser": "Wëslë e-maila do negò brëkòwnika",
-       "emailpage": "Sélajë e-mail do brëkòwnika",
        "defemailsubject": "E-mail òd {{SITENAME}}",
        "noemailtitle": "Felënk email-adresë",
        "emailfrom": "Òd:",
        "move-page-legend": "Przeniesë starnã",
        "movepagetext": "Z pòmòcą ùiższegò fòrmùlôra zjinaczisz miono starnë, przenosząc równoczasno ji historëjã.\nPòd stôrim titlã bãdze ùsôdzonô przeczérowùjącô starna.\nMòżesz aùtomatno zaktualniac przeczérowania wskazëwôjące titel przed zjinaką.\nJeżlë nie wëbiérzesz ti òptacëji, ùgwësni sã pò przenieseniu starnë, czë nie òstałé ùsôdzoné [[Special:DoubleRedirects|dëbeltné]] abò [[Special:BrokenRedirects|zerwóné przeczérowania]].\nJes òdpòwiedzalny za to, abë lënczi dali robiłë tam dze mają.\n\nStarna '''ni''' bãdze przeniosłô, jeżlë starna ò nowim mionie ju je, chòba że je òna pùstô abò je przeczérowaniém ë mô pùstą historëjã edicëji.\nTo òznôczô, że lëchą òperacëjã zjinaczi miona mòże doprowôdzëc bezpieczno nazôd, zjinaczając nowé miono starnë nawczasniészą, ë że ni mòże nadpisac stranë chtërną ju dô.\n\n'''BÔCZËNK!'''\nTo mòże bëc drasticznô abò nieprzewidëwólnô zjinaka w przëtrôfkù pòpùlarnych starnów.\nÙgwësni sã co do skùtków ti òperacëji, niglë to zrobisz.",
        "movepagetalktext": "Sparłãczonô starna diskùsëji, jeżlë ju je, to bãdze przeniosłô aùtomatno, chòba że:\n*niepùstô starna diskùsëji ju je z nowim mionã\n*rëmniész nacéchòwanié z niższegò pòla wëbiérkù\n\nW taczich przëtrôfkach zamkłosc diskùsëji mòże przeniesc blós rãczno.",
-       "movearticle": "Przeniesë artikel",
        "newtitle": "Nowi titel:",
        "move-watch": "Ùzérôj tã starnã",
        "movepagebtn": "Przeniesë starnã",
        "movelogpage": "Przeniosłé",
        "movereason": "Przëczëna:",
        "revertmove": "copnij",
-       "delete_and_move": "Rëmôj ë przeniesë",
        "delete_and_move_confirm": "Jo, rëmôj ną starnã",
        "export": "Ekspòrt starnów",
        "allmessages": "Wszëtczé systemòwé ògłosë",
        "specialpages": "Specjalné starnë",
        "revdelete-restricted": "nastôwi ògrańczenia dlô sprôwników",
        "revdelete-unrestricted": "rëmôj ògrańczenia dlô sprôwników",
-       "revdelete-summary": "òpisënk zjinaczi"
+       "revdelete-summary": "òpisënk zjinaczi",
+       "special-characters-group-ipa": "IPA",
+       "special-characters-group-symbols": "Céchë",
+       "special-characters-group-greek": "Grecczi",
+       "special-characters-group-cyrillic": "Cërylica",
+       "special-characters-group-arabic": "Arabsczi",
+       "special-characters-group-hebrew": "Hebrajsczi"
 }
index 4cbca02..43b294c 100644 (file)
        "noname": "Dydych chi ddim wedi cynnig enw defnyddiwr dilys.",
        "loginsuccesstitle": "Llwyddodd y mewngofnodi",
        "loginsuccess": "'''Yr ydych wedi mewngofnodi i {{SITENAME}} fel \"$1\".'''",
-       "nosuchuser": "Does dim defnyddiwr o'r enw \"$1\".\nMae'r rhaglen yn gwahaniaethu rhwng llythrennau bach a mawr.\nSicrhewch eich bod chi wedi sillafu'r enw'n gywir, neu [[Special:UserLogin/signup|crëwch gyfrif newydd]].",
+       "nosuchuser": "Does dim defnyddiwr o'r enw \"$1\".\nMae'r rhaglen yn gwahaniaethu rhwng llythrennau bach a mawr.\nSicrhewch eich bod chi wedi sillafu'r enw'n gywir, neu [[Special:CreateAccount|crëwch gyfrif newydd]].",
        "nosuchusershort": "Does dim defnyddiwr o'r enw \"$1\". Gwiriwch eich sillafu.",
        "nouserspecified": "Mae'n rhaid nodi enw defnyddiwr.",
        "login-userblocked": "Mae'r defnyddiwr hwn wedi ei flocio. Ni ellir mewngofnodi.",
        "accmailtext": "Anfonwyd cyfrinair a grewyd ar hap ar gyfer [[User talk:$1|$1]] at $2. Gellir newid y cyfrinair hwn ar y dudalen ''[[Special:ChangePassword|newid cyfrinair]]'', wrth fewngofnodi.",
        "newarticle": "(Newydd)",
        "newarticletext": "Rydych chi wedi dilyn cysylltiad i dudalen sydd heb gael ei chreu eto.\nI greu'r dudalen, dechreuwch deipio yn y blwch isod (gweler y [$1 dudalen gymorth] am fwy o wybodaeth).\nOs daethoch yma ar ddamwain, cliciwch botwm '''n&ocirc;l''' y porwr.",
-       "anontalkpagetext": "----''Dyma dudalen sgwrs ar gyfer defnyddiwr anhysbys sydd heb greu cyfrif eto, neu nad yw'n ei ddefnyddio. Felly mae'n rhaid inni ddefnyddio'r cyfeiriad IP i'w (h)adnabod. Mae cyfeiriadau IP yn gallu cael eu rhannu rhwng nifer o ddefnyddwyr. Os ydych chi'n ddefnyddiwr anhysbys ac yn teimlo'ch bod wedi derbyn sylwadau amherthnasol, [[Special:UserLogin/signup|crëwch gyfrif]] neu [[Special:UserLogin|mewngofnodwch]] i osgoi cael eich drysu gyda defnyddwyr anhysbys eraill o hyn ymlaen.''",
+       "anontalkpagetext": "----''Dyma dudalen sgwrs ar gyfer defnyddiwr anhysbys sydd heb greu cyfrif eto, neu nad yw'n ei ddefnyddio. Felly mae'n rhaid inni ddefnyddio'r cyfeiriad IP i'w (h)adnabod. Mae cyfeiriadau IP yn gallu cael eu rhannu rhwng nifer o ddefnyddwyr. Os ydych chi'n ddefnyddiwr anhysbys ac yn teimlo'ch bod wedi derbyn sylwadau amherthnasol, [[Special:CreateAccount|crëwch gyfrif]] neu [[Special:UserLogin|mewngofnodwch]] i osgoi cael eich drysu gyda defnyddwyr anhysbys eraill o hyn ymlaen.''",
        "noarticletext": "Mae'r dudalen hon yn wag ar hyn o bryd.\nGallwch [[Special:Search/{{PAGENAME}}|chwilio am y teitl hwn]] ar ddalennau eraill, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chwilio drwy'r logiau perthnasol], neu [{{fullurl:{{FULLPAGENAME}}|action=edit}} fe allwch greu'r ddalen hon]</span>.",
        "noarticletext-nopermission": "Mae'r dudalen hon yn wag ar hyn o bryd.\nGallwch [[Special:Search/{{PAGENAME}}|chwilio am y teitl hwn]] ar dudalennau eraill, neu gallwch <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chwilio drwy'r logiau perthnasol]</span>, ond nid yw'r gallu gennych i ddechrau'r dudalen o'r dechrau cyntaf.",
        "missing-revision": "Nid yw'r diwygiad #$1 o'r dudalen \"{{FULLPAGENAME}}\" ar gael.\n\nFel arfer, fe ddigwydd hyn wrth ddilyn hen gyswllt i dudalen sydd wedi ei dileu.\nGallwch weld y manylion yn y [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} lòg dileu].",
        "upload-form-label-own-work": "Dyma fy ngwaith fy hun",
        "upload-form-label-infoform-categories": "Categoriau",
        "upload-form-label-infoform-date": "Dyddiad",
-       "upload-form-label-own-work-message-local": "Rwy'n cadarnhau fy mod yn uwchlwytho'r ffeil yma gan ddilyn amodau a pholisiau trwyddedu {{SITENAME}}.",
-       "upload-form-label-not-own-work-local-local": "Efallai y carwch hefyd roi gynnig ar [[Special:Upload|y ddalen uwchlwytho diofyn]].",
-       "upload-form-label-own-work-message-default": "Rwy'n cadarnhau fy mod yn uwchlwytho'r ffeil yma i fan sy'n cael ei rannu gan nifer. Rwy'n cadarnhau hefyd y gwnaf hyn gan ddilyn yr amodau a'r polisiau trwyddedu sydd yno.",
-       "upload-form-label-own-work-message-shared": "Cadarnhaf mai fi yw perchennog hawlfraint y ffeil hon, a chytunaf  yn ddi-droi'n ôl i ryddhau'r ffeil hon i Gomin Wicimedia dan drwydded [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], a chytunaf gyda'r[https://wikimediafoundation.org/wiki/Terms_of_Use Amodau Defnyddio].",
+       "upload-form-label-own-work-message-generic-local": "Rwy'n cadarnhau fy mod yn uwchlwytho'r ffeil yma gan ddilyn amodau a pholisiau trwyddedu {{SITENAME}}.",
+       "upload-form-label-not-own-work-local-generic-local": "Efallai y carwch hefyd roi gynnig ar [[Special:Upload|y ddalen uwchlwytho diofyn]].",
+       "upload-form-label-own-work-message-generic-foreign": "Rwy'n cadarnhau fy mod yn uwchlwytho'r ffeil yma i fan sy'n cael ei rannu gan nifer. Rwy'n cadarnhau hefyd y gwnaf hyn gan ddilyn yr amodau a'r polisiau trwyddedu sydd yno.",
        "backend-fail-stream": "Wedi methu ffrydio'r ffeil $1.",
        "backend-fail-backup": "Wedi methu gwneud copi wrth gefn o'r ffeil $1.",
        "backend-fail-notexists": "Nid yw'r ffeil $1 ar gael.",
index 8c40276..7a43083 100644 (file)
        "noname": "Du har ikke angivet et gyldigt brugernavn.",
        "loginsuccesstitle": "Du er nu logget på",
        "loginsuccess": "'''Du er nu logget på {{SITENAME}} som \"$1\".'''",
-       "nosuchuser": "Der er ingen bruger med navnet \"$1\".\nDer skelnes mellem store og bogstaver i brugernavne.\nKontrollér stavemåden, eller [[Special:UserLogin/signup|opret en ny konto]].",
+       "nosuchuser": "Der er ingen bruger med navnet \"$1\".\nDer skelnes mellem store og bogstaver i brugernavne.\nKontrollér stavemåden, eller [[Special:CreateAccount|opret en ny konto]].",
        "nosuchusershort": "Der er ingen bruger ved navn \"$1\". Tjek din stavning.",
        "nouserspecified": "Angiv venligst et brugernavn.",
        "login-userblocked": "Denne bruger er blokeret. Det er ikke tilladt at logge på.",
        "accmailtext": "En tilfældigt dannet adgangskode for [[User talk:$1|$1]] er sendt til $2. Den kan ændres på siden ''[[Special:ChangePassword|skift adgangskode]]'', når du logger på.",
        "newarticle": "(Ny)",
        "newarticletext": "Du har fulgt en henvisning til en side som endnu ikke findes.\nFor at oprette siden skal du begynde at skrive i boksen nedenfor\n(se [$1 hjælpesiden] for yderligere information).\nHvis du er her ved en fejl, så tryk på din browsers '''tilbage'''-knap.",
-       "anontalkpagetext": "---- ''Dette er en diskussionsside for en anonym bruger, der ikke har oprettet en konto endnu eller ikke bruger den.\nVi er derfor nødt til at bruge den numeriske IP-adresse til at identificere ham eller hende.\nEn IP-adresse kan være delt mellem flere brugere.\nHvis du er en anonym bruger og synes, at du har fået irrelevante kommentarer på sådan en side, så vær venlig at [[Special:UserLogin/signup|oprette en brugerkonto]] og [[Special:UserLogin|logge på]], så vi undgår fremtidige forvekslinger med andre anonyme brugere.''",
+       "anontalkpagetext": "---- ''Dette er en diskussionsside for en anonym bruger, der ikke har oprettet en konto endnu eller ikke bruger den.\nVi er derfor nødt til at bruge den numeriske IP-adresse til at identificere ham eller hende.\nEn IP-adresse kan være delt mellem flere brugere.\nHvis du er en anonym bruger og synes, at du har fået irrelevante kommentarer på sådan en side, så vær venlig at [[Special:CreateAccount|oprette en brugerkonto]] og [[Special:UserLogin|logge på]], så vi undgår fremtidige forvekslinger med andre anonyme brugere.''",
        "noarticletext": "Der er i øjeblikket ikke nogen tekst på denne side.\nDu kan [[Special:Search/{{PAGENAME}}|søge efter sidenavnet]] på andre sider,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søge i relaterede logger]\neller [{{fullurl:{{FULLPAGENAME}}|action=edit}} oprette siden]</span>.",
        "noarticletext-nopermission": "Der er i øjeblikket ikke nogen tekst på denne side.\nDu kan [[Special:Search/{{PAGENAME}}|søge efter sidenavnet]] på andre sider,\neller <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søge i relaterede loglister]</span>,\nmen du har ikke tilladelse til at oprette denne side.",
        "missing-revision": "Revision #$1 af siden med navnet \"{{FULLPAGENAME}}\" eksisterer ikke.\n\nDette skyldes normalt at et forældet historik-link er fulgt til en side der er slettet.\nDetaljer kan findes i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} sletningsloggen].",
        "upload-form-label-usage-filename": "Filnavn",
        "upload-form-label-infoform-categories": "Kategorier",
        "upload-form-label-infoform-date": "Dato",
-       "upload-form-label-own-work-message-local": "Jeg bekræfter at jeg uploader filen i overenstemmelse med betingelser for brug og licenseringspoltikken på {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Hvis du ikke kan uploade filen under politikerne på {{SITENAME}}, skal du lukke dialogboksen og prøve en anden metode.",
-       "upload-form-label-not-own-work-local-local": "Du kan også vælge at prøve [[Special:Upload|den almindelige uploadside]].",
+       "upload-form-label-own-work-message-generic-local": "Jeg bekræfter at jeg uploader filen i overenstemmelse med betingelser for brug og licenseringspoltikken på {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Hvis du ikke kan uploade filen under politikerne på {{SITENAME}}, skal du lukke dialogboksen og prøve en anden metode.",
+       "upload-form-label-not-own-work-local-generic-local": "Du kan også vælge at prøve [[Special:Upload|den almindelige uploadside]].",
        "backend-fail-stream": "Kunne ikke streame filen $1.",
        "backend-fail-backup": "Kunne ikke lave sikkerhedskopi af filen $1.",
        "backend-fail-notexists": "Filen $1 findes ikke.",
        "whatlinkshere-prev": "{{PLURAL:$1|forrige|forrige $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|næste|næste $1}}",
        "whatlinkshere-links": "← henvisninger",
-       "whatlinkshere-hideredirs": "$1 omdirigeringer",
-       "whatlinkshere-hidetrans": "$1 inkluderinger",
-       "whatlinkshere-hidelinks": "$1 henvisninger",
+       "whatlinkshere-hideredirs": "Skjul omdirigeringer",
+       "whatlinkshere-hidetrans": "Skjul inkluderinger",
+       "whatlinkshere-hidelinks": "Skjul henvisninger",
        "whatlinkshere-hideimages": "$1 filhenvisninger",
        "whatlinkshere-filters": "Filtre",
        "autoblockid": "Autoblock #$1",
        "revdelete-restricted": "tilføjede begrænsninger for administratorer",
        "revdelete-unrestricted": "fjernede begrænsninger for administratorer",
        "logentry-block-block": "$1 {{GENDER:$2|blokerede}} {{GENDER:$4|$3}} med en udløbstid på $5 $6",
+       "logentry-block-unblock": "$1 {{GENDER:$2|ophævede blokering af}} {{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1 {{GENDER:$2|ændrede}} blokeringsindstillinger for {{GENDER:$4|$3}} med en udløbstid på $5 $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|ændrede}} blokeringsindstillinger for {{GENDER:$4|$3}} med en udløbstid på $5 $6",
        "logentry-move-move": "$1 {{GENDER:$2|flyttede}} siden $3 til $4",
index 05a1720..f3d076b 100644 (file)
        "databaseerror-query": "Abfrage: $1",
        "databaseerror-function": "Funktion: $1",
        "databaseerror-error": "Fehler: $1",
-       "transaction-duration-limit-exceeded": "Um eine hohe Nachbildungsverzögerung zu vermeiden, wurde diese Transaktion abgebrochen, da die Schreibdauer ($1) die Grenze von {{PLURAL:$2|einer Sekunde|$2 Sekunden}} überschritten hat. Falls du viele Objekte auf einmal änderst, versuche stattdessen, mehrere kleine Operationen auszuführen.",
+       "transaction-duration-limit-exceeded": "Um eine große Verzögerung in der Datenreplikation zu vermeiden, wurde diese Transaktion abgebrochen. Die Schreibdauer ($1) hat die Grenze von {{PLURAL:$2|einer Sekunde|$2 Sekunden}} überschritten. Falls du viele Objekte auf einmal änderst, versuche stattdessen, die Änderungen auf mehrere Operationen aufzuteilen.",
        "laggedslavemode": "<strong>Achtung:</strong> Die angezeigte Seite könnte unter Umständen nicht die letzten Bearbeitungen enthalten.",
        "readonly": "Datenbank gesperrt",
        "enterlockreason": "Bitte gib einen Grund ein, warum die Datenbank gesperrt werden soll und eine Abschätzung über die Dauer der Sperrung",
        "noname": "Du musst einen gültigen Benutzernamen angeben.",
        "loginsuccesstitle": "Angemeldet",
        "loginsuccess": "<strong>Du bist jetzt als „$1“ bei {{SITENAME}} angemeldet.</strong>",
-       "nosuchuser": "Der Benutzername „$1“ existiert nicht.\nÜberprüfe die Schreibweise (Groß-/Kleinschreibung beachten) oder [[Special:UserLogin/signup|lege ein neues Benutzerkonto an]].",
+       "nosuchuser": "Der Benutzername „$1“ existiert nicht.\nÜberprüfe die Schreibweise (Groß-/Kleinschreibung beachten) oder [[Special:CreateAccount|lege ein neues Benutzerkonto an]].",
        "nosuchusershort": "Der Benutzername „$1“ ist nicht vorhanden. Bitte überprüfe die Schreibweise.",
        "nouserspecified": "Bitte gib einen Benutzernamen an.",
        "login-userblocked": "{{GENDER:$1|Dieser Benutzer|Diese Benutzerin}} ist gesperrt. Die Anmeldung ist nicht erlaubt.",
        "accmailtext": "Ein zufällig generiertes Passwort für [[User talk:$1|$1]] wurde an $2 versandt. Es kann auf der Seite ''[[Special:ChangePassword|Passwort ändern]]'' nach der Anmeldung geändert werden.",
        "newarticle": "(Neu)",
        "newarticletext": "Du bist einem Link zu einer Seite gefolgt, die nicht vorhanden ist.\nUm diese Seite anzulegen, trage deinen Text in das untenstehende Bearbeitungsfeld ein (weitere Informationen auf der [$1 Hilfeseite]).\nSofern du fälschlicherweise hier bist, klicke auf die Schaltfläche '''Zurück''' deines Browsers.",
-       "anontalkpagetext": "----''Diese Seite dient dazu, einem nicht angemeldeten Benutzer Nachrichten zu hinterlassen. Es wird seine IP-Adresse zur Identifizierung verwendet. IP-Adressen können von mehreren Benutzern gemeinsam verwendet werden. Wenn du mit den Kommentaren auf dieser Seite nichts anfangen kannst, richten sie sich vermutlich an einen früheren Inhaber deiner IP-Adresse und du kannst sie ignorieren. Du kannst dir auch ein [[Special:UserLogin/signup|Benutzerkonto erstellen]] oder dich [[Special:UserLogin|anmelden]], um künftig Verwechslungen mit anderen anonymen Benutzern zu vermeiden.''",
+       "anontalkpagetext": "----''Diese Seite dient dazu, einem nicht angemeldeten Benutzer Nachrichten zu hinterlassen. Es wird seine IP-Adresse zur Identifizierung verwendet. IP-Adressen können von mehreren Benutzern gemeinsam verwendet werden. Wenn du mit den Kommentaren auf dieser Seite nichts anfangen kannst, richten sie sich vermutlich an einen früheren Inhaber deiner IP-Adresse und du kannst sie ignorieren. Du kannst dir auch ein [[Special:CreateAccount|Benutzerkonto erstellen]] oder dich [[Special:UserLogin|anmelden]], um künftig Verwechslungen mit anderen anonymen Benutzern zu vermeiden.''",
        "noarticletext": "Diese Seite enthält momentan noch keinen Text.\nDu kannst sie <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} erstellen]</span>,\nihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]]\noder die zugehörigen <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten]</span>.",
        "noarticletext-nopermission": "Diese Seite enthält momentan noch keinen Text und du bist auch nicht dazu berechtigt, diese Seite zu erstellen.\nDu kannst ihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]] oder die zugehörigen <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten].</span>",
        "missing-revision": "Die Version $1 der Seite namens „{{FULLPAGENAME}}“ ist nicht vorhanden.\n\nDieser Fehler wird normalerweise von einem veralteten Link zur Versionsgeschichte einer Seite verursacht, die zwischenzeitlich gelöscht wurde.\nEinzelheiten sind im [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Lösch-Logbuch] einsehbar.",
        "right-override-export-depth": "Exportiere Seiten einschließlich verlinkter Seiten bis zu einer Tiefe von 5",
        "right-sendemail": "E-Mails an andere Benutzer senden",
        "right-passwordreset": "Passwort eines Benutzers zurücksetzen und die dazu verschickte E-Mail einsehen",
-       "right-managechangetags": "[[Special:Tags|Markierungen]] erstellen und aus der Datenbank löschen",
+       "right-managechangetags": "[[Special:Tags|Markierungen]] erstellen und (de)aktivieren",
        "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",
+       "right-deletechangetags": "[[Special:Tags|Markierungen]] aus der Datenbank löschen",
        "grant-generic": "Rechtegruppe „$1“",
        "grant-group-page-interaction": "Mit Seiten interagieren",
        "grant-group-file-interaction": "Mit Medien interagieren",
        "action-viewmyprivateinfo": "deine privaten Informationen einzusehen",
        "action-editmyprivateinfo": "deine privaten Informationen zu bearbeiten",
        "action-editcontentmodel": "das Inhaltsmodell einer Seite zu bearbeiten",
-       "action-managechangetags": "Markierungen zu erstellen und aus der Datenbank zu löschen",
+       "action-managechangetags": "Markierungen zu erstellen und zu (de)aktivieren",
        "action-applychangetags": "Markierungen zusammen mit deinen Änderungen anzuwenden",
        "action-changetags": "beliebige Markierungen zu einzelnen Versionen und Logbucheinträgen hinzuzufügen und zu entfernen",
+       "action-deletechangetags": "Markierungen aus der Datenbank zu löschen",
        "nchanges": "$1 {{PLURAL:$1|Änderung|Änderungen}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|seit dem letzten Besuch}}",
        "enhancedrc-history": "Versionsgeschichte",
        "upload-form-label-own-work": "Dies ist mein eigenes Werk",
        "upload-form-label-infoform-categories": "Kategorien",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Ich bestätige, dass ich diese Datei gemäß den Nutzungsbedingungen und Lizenzrichtlinien von {{SITENAME}} hochlade.",
-       "upload-form-label-not-own-work-message-local": "Wenn du diese Datei nicht unter den Richtlinien von {{SITENAME}} hochladen kannst, schließe bitte diesen Dialog und versuche eine andere Methode.",
-       "upload-form-label-not-own-work-local-local": "Du kannst auch [[Special:Upload|die Standard-Hochladeseite]] ausprobieren.",
-       "upload-form-label-own-work-message-default": "Ich verstehe, dass ich diese Datei auf ein gemeinsames Repositorium hochlade. Ich bestätige, dass ich dies gemäß den dortigen Nutzungs- und Lizenzbedingungen tue.",
-       "upload-form-label-not-own-work-message-default": "Falls du diese Datei nicht unter den Bedingungen des gemeinsamen Repositoriums hochladen kannst, schließe bitte diesen Dialog und versuche eine andere Methode.",
-       "upload-form-label-not-own-work-local-default": "Du kannst auch [[Special:Upload|die Hochladeseite auf {{SITENAME}}]] ausprobieren, falls diese Datei dort unter ihren Richtlinien hochgeladen werden kann.",
-       "upload-form-label-own-work-message-shared": "Ich bestätige, dass ich das Urheberrecht für diese Datei besitze und stimme unwiderruflich der Veröffentlichung dieser Datei auf Wikimedia Commons unter der Lizenz [https://creativecommons.org/licenses/by-sa/4.0/deed.de „Creative Commons Namensnennung – Weitergabe unter gleichen Bedingungen 4.0 International“] sowie den [https://wikimediafoundation.org/wiki/Terms_of_Use/de Nutzungsbedingungen] zu.",
-       "upload-form-label-not-own-work-message-shared": "Falls du nicht das Urheberrecht für diese Datei besitzt oder du diese Datei unter einer anderen Lizenz veröffentlichen möchtest, ziehe [https://commons.wikimedia.org/wiki/Special:UploadWizard den Hochladeassistenten auf Wikimedia Commons] in Erwägung.",
-       "upload-form-label-not-own-work-local-shared": "Du kannst auch [[Special:Upload|die Hochladeseite auf {{SITENAME}}]] ausprobieren, falls die Website das Hochladen dieser Datei unter ihren Richtlinien erlaubt.",
+       "upload-form-label-own-work-message-generic-local": "Ich bestätige, dass ich diese Datei gemäß den Nutzungsbedingungen und Lizenzrichtlinien von {{SITENAME}} hochlade.",
+       "upload-form-label-not-own-work-message-generic-local": "Wenn du diese Datei nicht unter den Richtlinien von {{SITENAME}} hochladen kannst, schließe bitte diesen Dialog und versuche eine andere Methode.",
+       "upload-form-label-not-own-work-local-generic-local": "Du kannst auch [[Special:Upload|die Standard-Hochladeseite]] ausprobieren.",
+       "upload-form-label-own-work-message-generic-foreign": "Ich verstehe, dass ich diese Datei auf ein gemeinsames Repositorium hochlade. Ich bestätige, dass ich dies gemäß den dortigen Nutzungs- und Lizenzbedingungen tue.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Falls du diese Datei nicht unter den Bedingungen des gemeinsamen Repositoriums hochladen kannst, schließe bitte diesen Dialog und versuche eine andere Methode.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Du kannst auch [[Special:Upload|die Hochladeseite auf {{SITENAME}}]] ausprobieren, falls diese Datei dort unter ihren Richtlinien hochgeladen werden kann.",
        "backend-fail-stream": "Die Datei $1 konnte nicht übertragen werden.",
        "backend-fail-backup": "Die Datei $1 konnte nicht gesichert werden.",
        "backend-fail-notexists": "Die Datei $1 ist nicht vorhanden.",
        "whatlinkshere-prev": "{{PLURAL:$1|vorheriger|vorherige $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|nächster|nächste $1}}",
        "whatlinkshere-links": "← Links",
-       "whatlinkshere-hideredirs": "Weiterleitungen $1",
-       "whatlinkshere-hidetrans": "Vorlageneinbindungen $1",
-       "whatlinkshere-hidelinks": "Links $1",
-       "whatlinkshere-hideimages": "Dateilinks $1",
+       "whatlinkshere-hideredirs": "Weiterleitungen ausblenden",
+       "whatlinkshere-hidetrans": "Vorlageneinbindungen ausblenden",
+       "whatlinkshere-hidelinks": "Links ausblenden",
+       "whatlinkshere-hideimages": "Dateilinks ausblenden",
        "whatlinkshere-filters": "Filter",
        "whatlinkshere-submit": "Los",
        "autoblockid": "Automatische Sperrung #$1",
        "size-megabytes": "$1 MB",
        "size-gigabytes": "$1 GB",
        "lag-warn-normal": "Bearbeitungen der letzten {{PLURAL:$1|Sekunde|$1 Sekunden}} werden in dieser Liste noch nicht angezeigt.",
-       "lag-warn-high": "Auf Grund hoher Datenbankauslastung werden die Bearbeitungen der letzten {{PLURAL:$1|Sekunde|$1 Sekunden}} noch nicht in dieser Liste angezeigt.",
+       "lag-warn-high": "Aufgrund hoher Datenbankauslastung werden die Bearbeitungen der letzten {{PLURAL:$1|Sekunde|$1 Sekunden}} noch nicht in dieser Liste angezeigt.",
        "watchlistedit-normal-title": "Beobachtungsliste bearbeiten",
        "watchlistedit-normal-legend": "Einträge von der Beobachtungsliste entfernen",
        "watchlistedit-normal-explain": "Dies sind die Einträge deiner Beobachtungsliste. Um Einträge zu entfernen, markiere die Kästchen neben den Einträgen und klicke am Ende der Seite auf „{{int:Watchlistedit-normal-submit}}“. Du kannst deine Beobachtungsliste auch im [[Special:EditWatchlist/raw|Listenformat bearbeiten]].",
        "timezone-local": "Lokal",
        "duplicate-defaultsort": "Achtung: Der Sortierungsschlüssel „$2“ überschreibt den vorher verwendeten Schlüssel „$1“.",
        "duplicate-displaytitle": "<strong>Warnung:</strong> Der Anzeigetitel „$2“ überschreibt den früheren Anzeigetitel „$1“.",
+       "restricted-displaytitle": "<strong>Warnung:</strong> Der Anzeigetitel „$1“ wurde ignoriert, da er nicht mit dem tatsächlichen Seitentitel gleichwertig ist.",
        "invalid-indicator-name": "<strong>Fehler:</strong> Das Attribut <code>name</code> des Seitenstatusindikators darf nicht leer sein.",
        "version": "Version",
        "version-extensions": "Installierte Erweiterungen",
        "tags-delete-not-found": "Die Markierung „$1“ ist nicht vorhanden.",
        "tags-delete-too-many-uses": "Die Markierung „$1“ wird bei mehr als {{PLURAL:$2|einer Version|$2 Versionen}} verwendet und kann deshalb nicht gelöscht werden.",
        "tags-delete-warnings-after-delete": "Die Markierung „$1“ wurde gelöscht, aber die {{PLURAL:$2|folgende Warnung ist|folgenden Warnungen sind}} aufgetreten:",
+       "tags-delete-no-permission": "Du hast keine Berechtigung, Änderungsmarkierungen zu löschen.",
        "tags-activate-title": "Markierung aktivieren",
        "tags-activate-question": "Du bist dabei, die Markierung „$1“ zu aktivieren.",
        "tags-activate-reason": "Grund:",
index ba4d82b..88c2891 100644 (file)
        "hidden-category-category": "Kategoriyê nımıtey",
        "category-subcat-count": "{{PLURAL:$2|Na kategoriya de $1 bınkategoriyay estê.|$2 kategoriyan ra $1 bınkategoriyay asenê.}}",
        "category-subcat-count-limited": "Na kategoriye de {{PLURAL:$1|ena kategoriya bınêne esta|enê $1 kategoriyê bınêni estê}}.",
-       "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ena pele esta.|Ebe $2 ra pêro piya {{PLURAL:$1|ena pela na kategoriye dera|$1 enê peli na kategoriye derê.}}",
+       "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ena pele esta.|Ebe $2 ra pêro piya {{PLURAL:$1|ena pela na kategoriye dera|$1 enê peli na kategoriye derê.}}}}",
        "category-article-count-limited": "{{PLURAL:$1|Pela cêrêne|$1 Pelê cêrêni}} na kategoriye derê.",
        "category-file-count": "<noinclude>{{PLURAL:$2|Na kategoriye tenya dosyayanê cêrênan muhtewa kena.}}</noinclude>\n*Na kategoriye de $2 dosyayan ra {{PLURAL:$1|yew dosya tenêka esta| $1 dosyey asenê}}.",
        "category-file-count-limited": "{{PLURAL:$1|Dosya cêrêne|$1 Dosyê cêrêni}} na kategoriye derê.",
        "search": "Cı geyre",
        "searchbutton": "Cı geyre",
        "go": "Şo",
-       "searcharticle": "Şo",
+       "searcharticle": "So",
        "history": "Tarixê pele",
        "history_short": "Tarix",
        "updatedmarker": "cıkewtena mına peyêne ra dıme biyo rocane",
-       "printableversion": "Asayışê çapkerdışi",
+       "printableversion": "Asaena çapkerdene",
        "permalink": "Gıreyo jûqere",
        "print": "Çap ke",
        "view": "Bıvêne",
        "lastmodifiedat": "Ena pele tewr peyên roca $2, $1 de biya rocaniye.",
        "viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.",
        "protectedpage": "Pela pawıtiye",
-       "jumpto": "Şo:",
+       "jumpto": "Şo be:",
        "jumptonavigation": "Pusula",
        "jumptosearch": "cı geyre",
        "view-pool-error": "Qaytê qısuri mekerên, serverê ma enıka zêde bar gırewto xo ser.\nHedê xo ra zêde karberi kenê ke seyrê na pele bıkerê.\nŞıma rê zehmet, tenê vınderên, heta ke reyna kenê ke ena pele kewê.\n\n$1",
        "pool-errorunknown": "Xeta nêzanıtiye",
        "poolcounter-usage-error": "Xırab karyayış:$1",
        "aboutsite": "Heqa {{SITENAME}} de",
-       "aboutpage": "Project:Heqa {{SITENAME}} de",
+       "aboutpage": "Project:Heqa",
        "copyright": "Zerrekacı $1 bındı not biya.",
        "copyrightpage": "{{ns:project}}:Heqa telifi",
        "currentevents": "Veng û vac",
        "mainpage": "Pela Seri",
        "mainpage-description": "Pela seri",
        "policy-url": "Project:Terzê hereketi",
-       "portal": "Portalê cemaeti",
-       "portal-url": "Project:Portalê cemaeti",
-       "privacy": "Madeyê dızdiye",
+       "portal": "Portalê kome",
+       "portal-url": "Project:Portalê kome",
+       "privacy": "Madê dızdêni",
        "privacypage": "Project:Xısusiyetê nımtışi",
        "badaccess": "Xeta mısadey",
        "badaccess-group0": "Heqa şıma çıniya, karo ke şıma waşt, bıkerê.",
        "feed-invalid": "Qeydey cıresnayışê  beğşi nêvêreno.",
        "feed-unavailable": "Cıresnayışê şebekey çıniyê",
        "site-rss-feed": "$1 Cıresnayışê RSSi",
-       "site-atom-feed": "$1 Cıresnayışê atomi",
+       "site-atom-feed": "$1 Wari kerdrna Atomi",
        "page-rss-feed": "\"$1\" Cıresnayışê RSSi",
        "page-atom-feed": "\"$1\" Cıresnayışê atomi",
        "feed-atom": "Atom",
        "noname": "Yew nameyo maqbul bınuse.",
        "loginsuccesstitle": "Hesab abıya",
        "loginsuccess": "'''{{SITENAME}} dı name dê \"$1\" şıma hesab akerdo.'''",
-       "nosuchuser": "Ebe namey \"$1\"i yew karber çıniyo.\nNuştışê namanê karberan de herfa pil u qıce rê diqet kerên.\nNuştışê xo qonrol kerên, ya zi [[Special:UserLogin/signup|yew hesabo newe akerên]].",
+       "nosuchuser": "Ebe namey \"$1\"i yew karber çıniyo.\nNuştışê namanê karberan de herfa pil u qıce rê diqet kerên.\nNuştışê xo qonrol kerên, ya zi [[Special:CreateAccount|yew hesabo newe akerên]].",
        "nosuchusershort": "No \"$1\" name de yew ten çino. Kontrolê nuştışi bıkere.",
        "nouserspecified": "Şıma gani yew name bıde.",
        "login-userblocked": "No karber/na karbere blokekerdeyo/blokekerdiya. Cıkewtışi rê musade çıniyo.",
        "accmailtext": "[[User talk:$1|$1]] parolayo ke raşt ameyo şırawiyo na adres $2.\n\nQey na hesabê newe parola, cıkewtış dıma şıma eşkeni na qısım de ''[[Special:ChangePassword|parola bıvurn]]'' bıvurni.",
        "newarticle": "(Newe)",
        "newarticletext": "To yew gıre tıkna be ra yew pela ke hewna çıniya.\nSeba afernayışê pele ra, qutiya metnê cêrêni bıgurene (seba melumati qaytê [$1 pela peşti] ke).\nEke be ğeletine ameya tiya, wa gocega <strong>peyser</strong>i programê xo de bıtıkne.",
-       "anontalkpagetext": "----''No pel, pel o karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres şuxulneni û ney IP adresan herkes eşkeno bıvino. Eke şıma qayil niye ina bo xo ri [[Special:UserLogin/signup|yew hesab bıvıraze]] veyaxut [[Special:UserLogin|hesab akere]].''",
+       "anontalkpagetext": "----''No pel, pel o karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres şuxulneni û ney IP adresan herkes eşkeno bıvino. Eke şıma qayil niye ina bo xo ri [[Special:CreateAccount|yew hesab bıvıraze]] veyaxut [[Special:UserLogin|hesab akere]].''",
        "noarticletext": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.",
        "noarticletext-nopermission": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê na pele cı geyre]], ya zi <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre]</span>, ema destur çıniyo ke na pele vırazê.",
        "missing-revision": "Rewizyonê name dê pela da #$1 \"{{FULLPAGENAME}}\" dı çıniyo.\n\nNo normal de tarix dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.",
        "whatlinkshere-prev": "{{PLURAL:$1|veror|veror $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|verni|verni $1}}",
        "whatlinkshere-links": "← gırey",
-       "whatlinkshere-hideredirs": "Hetenayışê $1",
-       "whatlinkshere-hidetrans": "Açarnayışê $1",
-       "whatlinkshere-hidelinks": "Greyê $1",
+       "whatlinkshere-hideredirs": "Hetenayışan bınımne",
+       "whatlinkshere-hidetrans": "Gırêyanê açarnayışan bınımne",
+       "whatlinkshere-hidelinks": "Gırêyan bınımne",
        "whatlinkshere-hideimages": "Gıreyê dosya $1",
        "whatlinkshere-filters": "Avrêci",
        "autoblockid": "Otomatik vındarnayış #$1",
        "tooltip-ca-move": "Ena pele bere",
        "tooltip-ca-watch": "Ena pele lista xoya seyrkerdışi ke",
        "tooltip-ca-unwatch": "Ena pele lista xoya seyrkerdışi ra vece",
-       "tooltip-search": "{{SITENAME}} miyan de bıvin",
+       "tooltip-search": "{{SITENAME}} de bıvin",
        "tooltip-search-go": "Ebe nê namey tami şo yew pela ke esta",
-       "tooltip-search-fulltext": "Nê  metni peran dı cı geyre",
-       "tooltip-p-logo": "Şo pela seri",
+       "tooltip-search-fulltext": "Pela miyan dı bı geyr ena metin",
+       "tooltip-p-logo": "pela seri ziyaret ke",
        "tooltip-n-mainpage": "Şo pela seri",
        "tooltip-n-mainpage-description": "Şo pela seri",
        "tooltip-n-portal": "Heqa proceyi de, çı şenay bıkerê, çı koti vêniyeno",
        "tooltip-feed-atom": "Qe ena pele atom feed",
        "tooltip-t-contributions": "Yew lista iştırakanê {{GENDER:$1|nê karberi}}",
        "tooltip-t-emailuser": "Ena karber ri yew email bışırav",
-       "tooltip-t-upload": "Dosya bar ke",
+       "tooltip-t-upload": "Dosyey bar ke",
        "tooltip-t-specialpages": "Yew lista pelanê xasanê pêroyinan",
        "tooltip-t-print": "Hewl versiyona ploğnayışa na perer",
        "tooltip-t-permalink": "Gırêyo daimi be ena versiyonê pele",
        "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.",
        "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.",
        "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.",
-       "specialpages": "Pelê xısusiyi",
+       "specialpages": "Pelê xasi",
        "specialpages-note-top": "Kıtabek",
        "specialpages-note": "* Pelê xasê normali.\n* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
        "specialpages-group-maintenance": "Raporê pawıtışi",
index f11718e..0cbc2e8 100644 (file)
        "noname": "Njejsy žedno płaśece wužywarske mě zapódał.",
        "loginsuccesstitle": "Pśizjawjenje wuspěšne",
        "loginsuccess": "'''Sy něnto ako „$1” w {{GRAMMAR:lokatiw|{{SITENAME}}}} pśizjawjony.'''",
-       "nosuchuser": "Wužywaŕ z mjenim „$1“ njeeksistěrujo. Wužywarske mjenja źiwaju na wjelikopisanje.\nPśeglěduj pšawopis abo [[Special:UserLogin/signup|załož nowe konto]].",
+       "nosuchuser": "Wužywaŕ z mjenim „$1“ njeeksistěrujo. Wužywarske mjenja źiwaju na wjelikopisanje.\nPśeglěduj pšawopis abo [[Special:CreateAccount|załož nowe konto]].",
        "nosuchusershort": "Wužywarske mě „$1“ njeeksistěrujo. Pśeglěduj pšawopis.",
        "nouserspecified": "Pšosym pódaj wužywarske mě.",
        "login-userblocked": "Toś ten wužywaŕ jo blokěrowany. Pśizjawjenje njejo dowólone.",
        "accmailtext": "Pśipadnje napórane gronidło za [[User talk:$1|$1]] jo se pósłało k $2. Dajo se na boku ''[[Special:ChangePassword|Gronidło změniś]]'' pśi pśizjawjenju změniś.",
        "newarticle": "(Nowy nastawk)",
        "newarticletext": "Sy slědował wótkaz na bok, kótaryž hyšći njeeksistěrujo.\nAby bok napórał, zapiš do kašćika dołojce (glědaj [$1 bok pomocy] za dalšne informacije). Jolic sy zamólnje how, klikni na tłocašk '''Slědk''' w swójom wobglědowaku.",
-       "anontalkpagetext": "---- ''Toś jo diskusijny bok za anonymnego wužywarja, kótaryž njejo dotychměst žedno wužywarske konto załožył abo swójo konto njewužywa. Togodla dejmy numerisku IP-adresu wužywaś, aby jogo/ju identificěrowali. Taka IP-adresa dajo se wót wšakich wužywarjow wužywaś. Jolic sy anonymny wužywaŕ a se mysliš, až su se njerelewantne komentary na tebje měrili, [[Special:UserLogin/signup|załož konto]] abo [[Special:UserLogin|pśizjaw se]], aby se w pśichoźe zmuśenje z drugimi anonymnymi wužywarjami wobinuł.''",
+       "anontalkpagetext": "---- ''Toś jo diskusijny bok za anonymnego wužywarja, kótaryž njejo dotychměst žedno wužywarske konto załožył abo swójo konto njewužywa. Togodla dejmy numerisku IP-adresu wužywaś, aby jogo/ju identificěrowali. Taka IP-adresa dajo se wót wšakich wužywarjow wužywaś. Jolic sy anonymny wužywaŕ a se mysliš, až su se njerelewantne komentary na tebje měrili, [[Special:CreateAccount|załož konto]] abo [[Special:UserLogin|pśizjaw se]], aby se w pśichoźe zmuśenje z drugimi anonymnymi wužywarjami wobinuł.''",
        "noarticletext": "Dotychměst toś ten bok hyšći njewopśimujo žeden tekst. Móžoš w drugich bokach [[Special:Search/{{PAGENAME}}|titel togo boka pytaś]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wótpowědne protokole pytaś] abo [{{fullurl:{{FULLPAGENAME}}|action=edit}} toś ten bok wobźěłaś]</span>.",
        "noarticletext-nopermission": "Tuchylu njejo žeden tekst na toś tom boku.\nMóžoš [[Special:Search/{{PAGENAME}}|toś ten bokowy titel]] na drugich bokach pytaś\nabo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wótpowědne protokole pytaś]</span>, ale njamaš pšawo, toś ten bok napóraś.",
        "missing-revision": "Wersija #$1 boka z mjenim \"{{FULLPAGENAME}}\" njeeksistěrujo.\n\nPśicyna jo zwětšego zestarjony wótkaz w historiji k bokoju, kótaryž jo se wulašował.\nDrobnostki móžoš w  [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protokolu wulašowanjow] namakaś.",
index 1b9381e..b79dcda 100644 (file)
        "noname": "Awu nu pinosuang ngaran momomoguno di pasagaon.",
        "loginsuccesstitle": "Nokosuang log.",
        "loginsuccess": "'''Nokosuang log ko noh id {{SITENAME}} sobaagi do \"$1\".'''",
-       "nosuchuser": "Ingaa momoguno pinungaranan \"$1\".\nNgaran momoguno nopo nga kowoyo pimato.\nIntaai ijaannu, toi [[Special:UserLogin/signup|pomonsoi do takaun kawawagu]].",
+       "nosuchuser": "Ingaa momoguno pinungaranan \"$1\".\nNgaran momoguno nopo nga kowoyo pimato.\nIntaai ijaannu, toi [[Special:CreateAccount|pomonsoi do takaun kawawagu]].",
        "nosuchusershort": "Ingaa momoguno kingaran  \"$1\".\nIntaai ijaannu.",
        "nouserspecified": "Maai posuango ngaran momoguno.",
        "login-userblocked": "Nantaban momoguno diti. Oduhan do sumuang log.",
        "accmailtext": "Kaatalib somporimonsoi montok [[User talk:$1|$1]] nakaatod no hilo $2.\n\nKaatalib montok akaun wagu milo do alanan hilo id bolikon ''[[Special:ChangePassword|pangalanan kaatalib]]'' soira do kosuang log.",
        "newarticle": "(Wagu)",
        "newarticletext": "Nakatanudko di noputan kumaa di bolikon di awu po nokosuang.\nMomonsoi ko nopo do bolikon diti, timpuun do manaip id kutak siriba (intaai [$1 bolikon ponokodung] montok lobi gumu kointalangan).\nNung nakaansau ko do hiti, tonsoko ot butang '''gumuli''' id pogigihumnu.",
-       "anontalkpagetext": "----''Iti nopo nga bolikon pogibabarasan montok di momomoguno poinlisok it awu poh nokopomonsoi do takaun, toi it awu momoguno dilo.\nMantad dilo, asansagan yahai do poposurat numbur kinoyonon IP do mongintutun dau.\nKinoyonon IP pogialasan do pipiro momomoguno.\nNung momomoguno koh do ingaa ngaran om karamit koh do komin di awu kapadan, mangai [[Special:UserLogin/signup|wonsoyo takaun]] toi [[Special:UserLogin|sumuang log]] do magalai kounsuhan di momomoguno suai di tingaa ngaran.''",
+       "anontalkpagetext": "----''Iti nopo nga bolikon pogibabarasan montok di momomoguno poinlisok it awu poh nokopomonsoi do takaun, toi it awu momoguno dilo.\nMantad dilo, asansagan yahai do poposurat numbur kinoyonon IP do mongintutun dau.\nKinoyonon IP pogialasan do pipiro momomoguno.\nNung momomoguno koh do ingaa ngaran om karamit koh do komin di awu kapadan, mangai [[Special:CreateAccount|wonsoyo takaun]] toi [[Special:UserLogin|sumuang log]] do magalai kounsuhan di momomoguno suai di tingaa ngaran.''",
        "noarticletext": "Maso po do ingaa tik id bolikon diti.\nMilo ko nogi do [[Special:Search/{{PAGENAME}}|mogihum do tuluhon bolikon diti]] id bolikon suai,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mogihum kokomoi log-log],\ntoi [{{fullurl:{{FULLPAGENAME}}|action=edit}} mongidit bolikon diti]</span>.",
        "noarticletext-nopermission": "Maso po do ingaa tik id bolikon diti.\nMilo ko nogi do [[Special:Search/{{PAGENAME}}|mogihum do tuluhon bolikon diti]] id bolikon suai, toi\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mogihum kokomoi log-log] </span>, nga ingaa kapasagaannu do momonsoi do bolikon diti.",
        "userpage-userdoesnotexist": "Akaun momomoguno \"<nowiki>$1</nowiki>\" awu nokorijisto.\nMangai intangai nung mumang ko pomonsoi/mongidit do bolikon diti.",
        "allpages-bad-ns": "{{SITENAME}} aiso pinungaranan do \"$1\"",
        "categories": "Kakatogori",
        "categoriesfrom": "Pokitono kakatogori tinimpuun do:",
-       "special-categories-sort-abc": "uludo mompimato",
        "deletedcontributions": "Pugaso pinototoluod do momoguno",
        "deletedcontributions-title": "Pugaso pinototoluod do momoguno",
        "sp-deletedcontributions-contribs": "Totoluod",
index 29c1cc1..051f1a8 100644 (file)
@@ -13,6 +13,7 @@
        "tog-hideminor": "अहिलका मामूली सम्पादनलाई लुकाउन्या",
        "tog-hidepatrolled": "गस्ती(patrolled)सम्पादनलाई लुकाउन्या",
        "tog-newpageshidepatrolled": "गस्ती गरिया पानानलाई नयाँ पाना  सूचीबठेई लुकाउन्या",
+       "tog-hidecategorization": "पृष्ठहरुको श्रेणीकरण हटाया",
        "tog-extendwatchlist": "निगरानी सूचीलाई सबै परिवर्तन धेकुन्या गरी बढुन्या , ऐईलका बाहेक",
        "tog-usenewrc": "पानाका अहिलका  परिवर्तन र अवलोकन सूचीका आधारमी सामूहिक परिवर्तनहरू",
        "tog-numberheadings": "शीर्षकहरूलाई स्वत:अङ्कित गर",
@@ -23,6 +24,7 @@
        "tog-watchdefault": "मुइले सम्पादन गरयाको पाना र फाइल ध्यान सूचीमाई थप्दया",
        "tog-watchmoves": "मुइले सार्या पानाहरू र फाइलहरूलाई ध्यान सूचीमी थप्द्या",
        "tog-watchdeletion": "मुइले हटायाका पानाहरू र चित्रहरूलाई ध्यान सूचीमी थप्द्या",
+       "tog-watchuploads": "नयाँ फाईल थप|मेरा अवलोकसुचिमी अपलोड गर",
        "tog-watchrollback": "मुइले रोलब्याक गर्याका पानाहरू मेरो ध्यानसूचीमी थप्द्या ।",
        "tog-minordefault": "सबै सम्पादनहरूलाई पहिल्लई निर्धारित रुपमी सामान्य चिनो लगाउन्या",
        "tog-previewontop": "सम्पादन बाकस अगि पहिलाकोरूप देखाउन्या",
        "nstab-template": "ढाँचा",
        "nstab-help": "सहायता पानो",
        "nstab-category": "श्रेणी",
+       "mainpage-nstab": "मुख्य पानो",
        "nosuchaction": "यसो काम हैन",
        "nosuchactiontext": "URL ले खुलाएको काम मान्य छैन ।\nतमीले URL गलत टाइपगरेका हौ , वा गलत लिंकक पछाडी लागेका हुनसक्देहौ ।\nयो {{SITENAME}}ले सफ्टवेयरमी भयाको गल्ति देखायाको लै हुनसक्छ ।",
        "nosuchspecialpage": "तसो विशेष पानो छैन",
        "nologin": "तमरो खाता छैन? $1।",
        "nologinlink": "नयाँ खाता खोल",
        "createaccount": "खाता खोल",
+       "gotaccount": "तमरो खाता छनोई छ? $1।",
        "gotaccountlink": "प्रवेश",
        "userlogin-resetlink": "प्रवेश सम्बन्धी विवरणहरू बिसरया भयो?",
        "userlogin-resetpassword-link": "पासवर्ड भुलिगया?",
        "accmailtitle": "पासवर्ड पठाइयो",
        "newarticle": "(नयाँ)",
        "newarticletext": "तमले अहिलसम्म नभयाका पानाको लिंङ्क पहिल्यायाका छौ ।\nयो पानो बनौनाखी तल्तिरको कोष्ठमी टाइप गरि । (और जाण्णाखीलेखा [$1 help page] हेर )।\nताखाइ सुधिसार आइपुग्या हौ भण्या, ब्राउजरको  '''back''' बटन थिचिहाल ।",
-       "anontalkpagetext": "----''यो कुरडी पानो अज्ञात प्रयोगकर्ताको हो जनले अहिलसम्म खाता बनायाकै छैन, अथवा जनले यै पानाको उपयोग गर्दैन।\nयस कारण हामीले उनलाई उनरो आइ पी (IP) ठेगानाले चिन्न सकन्छौ। \nयस्तो आइ पी (IP) ठेगाना धेरै प्रयोगकर्तानको साझा हुनसकन्छ ।\nयदि तमी अज्ञात प्रयोगकर्ता हौ र तमलाई अचाहिँदो टिप्पणी भयाको अनुभव गद्दा छौ भण्या भविष्यमी अन्य अज्ञात प्रयोगकर्तासँगको भ्रमबाट बाँच्न कृपया [[Special:UserLogin/signup|खाता खोल]] अथवा [[Special:UserLogin|प्रवेश गर]] ''",
+       "anontalkpagetext": "----''यो कुरडी पानो अज्ञात प्रयोगकर्ताको हो जनले अहिलसम्म खाता बनायाकै छैन, अथवा जनले यै पानाको उपयोग गर्दैन।\nयस कारण हामीले उनलाई उनरो आइ पी (IP) ठेगानाले चिन्न सकन्छौ। \nयस्तो आइ पी (IP) ठेगाना धेरै प्रयोगकर्तानको साझा हुनसकन्छ ।\nयदि तमी अज्ञात प्रयोगकर्ता हौ र तमलाई अचाहिँदो टिप्पणी भयाको अनुभव गद्दा छौ भण्या भविष्यमी अन्य अज्ञात प्रयोगकर्तासँगको भ्रमबाट बाँच्न कृपया [[Special:CreateAccount|खाता खोल]] अथवा [[Special:UserLogin|प्रवेश गर]] ''",
        "noarticletext": "यै लेखमी अहिल क्यै पन पाठ नाइथी  ।\nतमले और पृष्ठमी\n[[Special:Search/{{PAGENAME}}|यस पृष्ठको शीर्षककी लेखा खोज]] गद्द सकन्छौ ।\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} पाना सम्बन्धित ढड्डामी खोज],\nवा [{{fullurl:{{FULLPAGENAME}}|action=edit}}  यै पानालाई सम्पादन गद्या]</span>.",
        "noarticletext-nopermission": "यै लेखमी अहिल केइ पन पाठ नाइथी  ।\nतमले और पानामी\n[[Special:Search/{{PAGENAME}}|यै पानाको शीर्षककी लेखा खोज]] गद्द सकन्छौ ।\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} पाना सम्बन्धित ढड्डामी खोज्न],\nवा [{{fullurl:{{FULLPAGENAME}}|action=edit}}  यै पानालाई सम्पादन गद्द] सकन्छौ</span>.",
        "userinvalidcssjstitle": "<strong>चेतावनी:</strong> यहाँ कोइपनि \"$1\" नामको खोल नाइथिन् ।\nप्रचलित .css तथा .js पानाहरूले निम्नपद शीर्षक प्रयोग गद्दान्, जस्तै {{ns:user}}:Foo/Vector.css को सट्टामी {{ns:user}}:Foo/vector.css",
index 388ad61..adb3dab 100644 (file)
        "noname": "Al nòm utèint scrét an n'é mìa vâlid.",
        "loginsuccesstitle": "Ingrès fât.",
        "loginsuccess": "''' T'é stê coleghê al terminêl {{SITENAME}} cun al nòm utèint '''$1'''.",
-       "nosuchuser": "An n'é mìa registrê nisûn utèint cun al nòm \"$1\". I nòm utèin în sensébil al lètri grândi. Veréfica al nòm scrét o [[Special:UserLogin/signup|fà un nōv ingrès]].",
+       "nosuchuser": "An n'é mìa registrê nisûn utèint cun al nòm \"$1\". I nòm utèin în sensébil al lètri grândi. Veréfica al nòm scrét o [[Special:CreateAccount|fà un nōv ingrès]].",
        "nosuchusershort": "An gh'é mìa registrê un utèint ciamê ''$1''. Veréfica al nòm scrét.",
        "nouserspecified": "L'é necesâri precişêr un nòm utèint.",
        "login-userblocked": "Cl'utèinsa ché l'é bluchêda. An n'é pusébil fêr l'ingrès.",
        "accmailtext": "'Na cêva 'd ingrés l'è stêda fâta a chêş per [[User talk:$1|$1]] e l'è stêda spidîda a $2. Cla cêva 'd ingrès ché la pōl èser cambiêda int la pàgina per ''[[Special:ChangePassword|cambiêr la cêva 'd ingrès]]'' subét dôp avèir fât l'ingrès.",
        "newarticle": "(Nōv)",
        "newarticletext": "Al colegamèint apèina fât al cumbîna cun 'na pàgina ch' an n'é mìa incòra stêda fâta. S'ét vō fêr la pàgina adès, l'é asê cumincêr a scréver al tèst int la caşèla ché sòt (per vedèr infurmasiòun pió precîşi guêrda la [$1 pàgina 'd ajót]). Se al colegamèint  l'é stê avêrt per erōr, l'é asê clichêr al pulsânt \"Indrē\" dal tó navigadōr.",
-       "anontalkpagetext": "----'' Còsta l'è la pàgina 'd discusiòun ed 'n utèint sèinsa nòm, ch' an n' à mìa incòra fât 'n' utèinsa o in tót al manēri an n'è mìa drē druvêrla. Per arcgnòsrel l'è dòunca necesâri druvê al só indirés IP. J indirés IP a pōlen èser spartî cun êter utèint. Se t'è un utèint sèinsa nòm e 't pèins che i cumèint in cla pàgina ché an riguêrden mìa tè, [[Special:UserLogin/signup|fa 'n' utèinsa nōva]] o [[Special:UserLogin|vîn dèinter cun còla ch' ét gh'ê bèle]] per schivşêr, in futûr,  'd èser cunfûş cun 'd j êter utèint sèinsa nòm.''",
+       "anontalkpagetext": "----'' Còsta l'è la pàgina 'd discusiòun ed 'n utèint sèinsa nòm, ch' an n' à mìa incòra fât 'n' utèinsa o in tót al manēri an n'è mìa drē druvêrla. Per arcgnòsrel l'è dòunca necesâri druvê al só indirés IP. J indirés IP a pōlen èser spartî cun êter utèint. Se t'è un utèint sèinsa nòm e 't pèins che i cumèint in cla pàgina ché an riguêrden mìa tè, [[Special:CreateAccount|fa 'n' utèinsa nōva]] o [[Special:UserLogin|vîn dèinter cun còla ch' ét gh'ê bèle]] per schivşêr, in futûr,  'd èser cunfûş cun 'd j êter utèint sèinsa nòm.''",
        "noarticletext": "In cól mumèint ché la pàgina serchêda l'é vōda. L'é pusébil [[Special:Search/{{PAGENAME}}|serchêr sté tétol]] int al j êtri pàgini dal sît, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serchêr int i regéster coleghê] opór  [{{fullurl:{{FULLPAGENAME}}|action=edit}} mudifichêr la pàgina adèsa]</span>.",
        "noarticletext-nopermission": "In cól mumèint ché la pàgina serchêda l'é vōda. L'é pusébil [[Special:Search/{{PAGENAME}}|serchêr sté tétol]] int al j êtri pàgini dal sît o<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serchêr int i regéster coleghê] <span>, mó an 't gh'ê mìa al permès ed fêr cla pàgina ché.",
        "missing-revision": "La revişiòun #$1 'd la pagina \"{{FULLPAGENAME}}\" l' an gh'è mìa. Còst, ed sôlit, a sucēd mèint'r as va drē a 'n colegamèint a 'na pàgina scanşlêda, in 'na stòria, di lavōr fât, mìa arnuvêda. I particulêr a 's pōlen catêr int al [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} regéster dal scanşladûri].",
index 379bbec..2500688 100644 (file)
        "site-atom-feed": "$1 ροή Atom",
        "page-rss-feed": "Ροή RSS «$1»",
        "page-atom-feed": "Ροή Atom «$1»",
-       "feed-atom": "Άτομο",
+       "feed-atom": "Atom",
        "red-link-title": "$1 (η σελίδα δεν υπάρχει)",
        "sort-descending": "Φθίνουσα ταξινόμηση",
        "sort-ascending": "Αύξουσα ταξινόμηση",
        "noname": "Το όνομα χρήστη που έχετε καθορίσει δεν είναι έγκυρο.",
        "loginsuccesstitle": "Επιτυχής σύνδεση",
        "loginsuccess": "Είστε συνδεδεμένος(-η) στο {{SITENAME}} ως \"$1\".",
-       "nosuchuser": "Δεν υπάρχει χρήστης με το όνομα \"$1\".\nΤα ονόματα χρηστών είναι ευαίσθητα σε κεφαλαιογράμματη και μικρογράμματη γραφή.\nΕλέγξτε την ορθογραφία ή [[Special:UserLogin/signup|δημιουργήστε ένα νέο λογαριασμό]].",
+       "nosuchuser": "Δεν υπάρχει χρήστης με το όνομα \"$1\".\nΤα ονόματα χρηστών είναι ευαίσθητα σε κεφαλαιογράμματη και μικρογράμματη γραφή.\nΕλέγξτε την ορθογραφία ή [[Special:CreateAccount|δημιουργήστε ένα νέο λογαριασμό]].",
        "nosuchusershort": "Δεν υπάρχει χρήστης με το όνομα \"$1\". Παρακαλούμε ελέγξτε την ορθογραφία.",
        "nouserspecified": "Πρέπει να ορίσετε ένα όνομα χρήστη.",
        "login-userblocked": "Αυτός ο χρήστης έχει αποκλειστεί. Δεν επιτρέπεται σύνδεση.",
        "accmailtext": "Ένας τυχαία παρηγμένος κωδικός για {{GENDER:$1|τον|την}} [[User talk:$1|$1]] έχει σταλεί στο $2.\n\nΜπορεί να αλλαχθεί από την σελίδα ''[[Special:ChangePassword|αλλαγή κωδικού]]'' μετά τη σύνδεση.",
        "newarticle": "(Νέο)",
        "newarticletext": "Ακολουθήσατε ένα σύνδεσμο προς μια σελίδα που δεν υπάρχει ακόμα. \nΓια να δημιουργήσετε τη σελίδα, αρχίστε να πληκτρολογείτε στο παρακάτω πλαίσιο (δείτε τη [$1 σελίδα βοήθειας] για περισσότερες πληροφορίες).\nΑν έχετε βρεθεί εδώ κατά λάθος, πατήστε το κουμπί '''πίσω''' στον περιηγητή σας.",
-       "anontalkpagetext": "----''Αυτή η σελίδα συζήτησης προορίζεται για ανώνυμο χρήστη που δεν έχει δημιουργήσει ακόμα λογαριασμό ή που δεν τον χρησιμοποιεί. Έτσι για την ταυτοποίηση ενός ανώνυμου χρήστη χρησιμοποιείται η διεύθυνση IP του. Είναι όμως πιθανόν η διεύθυνση αυτή να είναι κοινή για πολλούς διαφορετικούς χρήστες.  Αν είστε ανώνυμος χρήστης και νομίζετε ότι άσχετα σχόλια απευθύνθηκαν σε σας, παρακαλούμε να [[Special:UserLogin/signup|δημιουργήσετε ένα λογαριασμό]] ή να  [[Special:UserLogin|συνδεθείτε]] για να αποφεύγεται η μελλοντική σύγχυση με άλλους ανώνυμους χρήστες.''",
+       "anontalkpagetext": "----''Αυτή η σελίδα συζήτησης προορίζεται για ανώνυμο χρήστη που δεν έχει δημιουργήσει ακόμα λογαριασμό ή που δεν τον χρησιμοποιεί. Έτσι για την ταυτοποίηση ενός ανώνυμου χρήστη χρησιμοποιείται η διεύθυνση IP του. Είναι όμως πιθανόν η διεύθυνση αυτή να είναι κοινή για πολλούς διαφορετικούς χρήστες.  Αν είστε ανώνυμος χρήστης και νομίζετε ότι άσχετα σχόλια απευθύνθηκαν σε σας, παρακαλούμε να [[Special:CreateAccount|δημιουργήσετε ένα λογαριασμό]] ή να  [[Special:UserLogin|συνδεθείτε]] για να αποφεύγεται η μελλοντική σύγχυση με άλλους ανώνυμους χρήστες.''",
        "noarticletext": "Δεν υπάρχει προς το παρόν κείμενο σε αυτή τη σελίδα. \nΜπορείτε να [[Special:Search/{{PAGENAME}}|αναζητήσετε αυτόν τον τίτλο σελίδας]] σε άλλες σελίδες,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} να αναζητήσετε τις σχετικές καταγραφές],\nή να [{{fullurl:{{FULLPAGENAME}}|action=edit}} δημιουργήσετε αυτή τη σελίδα]</span>.",
        "noarticletext-nopermission": "Δεν υπάρχει προς το παρόν κείμενο σε αυτή τη σελίδα.\nΜπορείτε να [[Special:Search/{{PAGENAME}}|αναζητήσετε αυτόν τον τίτλο σελίδας]] σε άλλες σελίδες, ή <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} να ψάξετε στις σχετικές καταγραφές]</span>, αλλά δεν έχετε την άδεια να δημιουργήσετε αυτή τη σελίδα.",
        "missing-revision": "Δεν υπάρχει αναθεώρηση με αριθμό $1 για τη σελίδα με όνομα «{{FULLPAGENAME}}».\n\nΑυτό συνήθως προκαλείται από παλιό σύνδεσμο ιστορικού προς σελίδα που έχει διαγραφεί.\nΛεπτομέρειες θα βρείτε στο [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ημερολόγιο καταγραφής διαγραφών].",
        "upload-form-label-own-work": "Αυτό είναι το δικό μου έργο",
        "upload-form-label-infoform-categories": "Κατηγορίες",
        "upload-form-label-infoform-date": "Ημερομηνία",
-       "upload-form-label-own-work-message-local": "Επιβεβαιώνω ότι επιφορτώνω  αυτό το αρχείο κατά τους όρους της υπηρεσίας και πολιτικές αδειοδότησης για τον ιστότοπο {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Εάν δεν είστε σε θέση να ανεβάσετε αυτό το αρχείο στο πλαίσιο των πολιτικών της  {{SITENAME}}, παρακαλώ κλείστε αυτό το παράθυρο διαλόγου και να επιχειρήσετε μια άλλη μέθοδος.",
-       "upload-form-label-not-own-work-local-local": "Επίσης, μπορεί να θέλετε να δοκιμάσετε [[Special:Upload|την προεπιλεγμένη σελίδα επιφόρτωσης]].",
-       "upload-form-label-own-work-message-default": "Καταλαβαίνω ότι είμαι φόρτωμα αυτό το αρχείο σε ένα κοινόχρηστο αρχείο. Επιβεβαιώνω ότι είμαι τόσο ακόλουθες τους όρους της υπηρεσίας και πολιτικές αδειοδότησης.",
-       "upload-form-label-not-own-work-message-default": "Εάν δεν είστε σε θέση να ανεβάσετε αυτό το αρχείο στο πλαίσιο των πολιτικών της shared repository, παρακαλώ κλείστε αυτό το παράθυρο διαλόγου και να επιχειρήσετε μια άλλη μέθοδος.",
-       "upload-form-label-not-own-work-local-default": "Επίσης, μπορεί να θέλετε να δοκιμάσετε χρησιμοποιώντας το [[Special:Upload|τη σελίδα ανεβάσματος για το {{SITENAME}}]], αν αυτό το αρχείο μπορεί να φορτωθεί κάτω σύμφωνα με τις πολιτικές τους.",
-       "upload-form-label-own-work-message-shared": "Δηλώνω ότι κατέχω τα πνευματικά δικαιώματα για αυτό το αρχείο, και συμφωνώ αμετάκλητα στην απελευθέρωση  αυτού του  αρχείου στο Wikimedia Commons με άδεια  [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], και συμφωνώ με την [https://wikimediafoundation.org/wiki/Terms_of_Use Όρους Χρήσης].",
-       "upload-form-label-not-own-work-message-shared": "Αν δεν κατέχει τα πνευματικά δικαιώματα για αυτό το αρχείο, ή επιθυμείτε να το δημοσιεύσετε υπό μια διαφορετική άδεια χρήσης, μπορείτε να χρησιμοποιήσετε τον [https://commons.wikimedia.org/wiki/Special:UploadWizard Οδηγό Ανεβάσματος των Wikimedia Commons].",
-       "upload-form-label-not-own-work-local-shared": "Επίσης, μπορεί να θέλετε να δοκιμάσετε να χρησιμοποιήσετε  το [[Special:Upload|τη σελίδα ανεβάσματος για το {{SITENAME}}]], αν αυτό το αρχείο μπορεί να φορτωθεί σύμφωνα με  τις πολιτικές τους.",
+       "upload-form-label-own-work-message-generic-local": "Επιβεβαιώνω ότι επιφορτώνω  αυτό το αρχείο κατά τους όρους της υπηρεσίας και πολιτικές αδειοδότησης για τον ιστότοπο {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Εάν δεν είστε σε θέση να ανεβάσετε αυτό το αρχείο στο πλαίσιο των πολιτικών της  {{SITENAME}}, παρακαλώ κλείστε αυτό το παράθυρο διαλόγου και να επιχειρήσετε μια άλλη μέθοδος.",
+       "upload-form-label-not-own-work-local-generic-local": "Επίσης, μπορεί να θέλετε να δοκιμάσετε [[Special:Upload|την προεπιλεγμένη σελίδα επιφόρτωσης]].",
+       "upload-form-label-own-work-message-generic-foreign": "Καταλαβαίνω ότι είμαι φόρτωμα αυτό το αρχείο σε ένα κοινόχρηστο αρχείο. Επιβεβαιώνω ότι είμαι τόσο ακόλουθες τους όρους της υπηρεσίας και πολιτικές αδειοδότησης.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Εάν δεν είστε σε θέση να ανεβάσετε αυτό το αρχείο στο πλαίσιο των πολιτικών της shared repository, παρακαλώ κλείστε αυτό το παράθυρο διαλόγου και να επιχειρήσετε μια άλλη μέθοδος.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Επίσης, μπορεί να θέλετε να δοκιμάσετε χρησιμοποιώντας το [[Special:Upload|τη σελίδα ανεβάσματος για το {{SITENAME}}]], αν αυτό το αρχείο μπορεί να φορτωθεί κάτω σύμφωνα με τις πολιτικές τους.",
        "backend-fail-stream": "Αδύνατη η μετάδοση του αρχείου $1.",
        "backend-fail-backup": "Αδύνατη η δημιουργία αντίγραφου ασφαλείας του αρχείου $1.",
        "backend-fail-notexists": "Το αρχείο $1 δεν υπάρχει.",
        "apisandbox-dynamic-parameters-add-placeholder": "Ονομασία παραμέτρου",
        "apisandbox-dynamic-error-exists": "Η παράμετρος με την ονομασία \"$1\" υπάρχει ήδη",
        "apisandbox-submit-invalid-fields-title": "Κάποια από τα πεδία δεν είναι έγκυρα",
-       "apisandbox-submit-invalid-fields-message": "ΠαÏ\81ακαλÏ\8e Î´Î¹Î¿Ï\81θÏ\8eÏ\83Ï\84ε Ï\84α Ï\83ημειÏ\89μένα Ï\80εδία ÎºÎ±Î¹ Ï\80Ï\81οÏ\83Ï\80αθείστε ξανά.",
+       "apisandbox-submit-invalid-fields-message": "ΠαÏ\81ακαλÏ\8e Î´Î¹Î¿Ï\81θÏ\8eÏ\83Ï\84ε Ï\84α Ï\83ημειÏ\89μένα Ï\80εδία ÎºÎ±Î¹ Ï\80Ï\81οÏ\83Ï\80αθήστε ξανά.",
        "apisandbox-results": "Αποτελέσματα",
        "apisandbox-sending-request": "Αποστολή αιτήματος API...",
        "apisandbox-loading-results": "Λήψη αποτελεσμάτων API...",
        "whatlinkshere-prev": "{{PLURAL:$1|προηγούμενη|προηγούμενες $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|επόμενη|επόμενες $1}}",
        "whatlinkshere-links": "← σύνδεσμοι",
-       "whatlinkshere-hideredirs": "$1 ανακατευθύνσεων",
-       "whatlinkshere-hidetrans": "$1 ενσωματώσεων",
-       "whatlinkshere-hidelinks": "$1 συνδέσμων",
+       "whatlinkshere-hideredirs": "Απόκρυψη ανακατευθύνσεων",
+       "whatlinkshere-hidetrans": "Απόκρυψη ενσωματώσεων",
+       "whatlinkshere-hidelinks": "Απόκρυψη συνδέσμων",
        "whatlinkshere-hideimages": "$1 σύνδεσμοι αρχείων",
        "whatlinkshere-filters": "Φίλτρα",
        "whatlinkshere-submit": "Μετάβαση",
index e7fa4c7..8b9fefe 100644 (file)
        "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.",
        "login": "Log in",
+       "login-security": "Verify your identity",
        "nav-login-createaccount": "Log in / create account",
        "loginprompt": "",
        "userlogin": "Log in / create account",
        "helplogin-url": "https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Logging_in",
        "userlogin-helplink2": "Help with logging in",
        "userlogin-loggedin": "You are already logged in as {{GENDER:$1|$1}}.\nUse the form below to log in as another user.",
+       "userlogin-reauth": "You must log in again to verify that you are {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Create another account",
        "createacct-emailrequired": "Email address",
        "createacct-emailoptional": "Email address (optional)",
        "createacct-email-ph": "Enter your email address",
        "createacct-another-email-ph": "Enter email address",
        "createaccountmail": "Use a temporary random password and send it to the specified email address",
+       "createaccountmail-help": "Can be used to create account for another person without learning the password.",
        "createacct-realname": "Real name (optional)",
        "createaccountreason": "Reason:",
        "createacct-reason": "Reason",
        "createacct-reason-ph": "Why you are creating another account",
+       "createacct-reason-help": "Message shown in the account creation log",
        "createacct-imgcaptcha-help": "",
        "createacct-submit": "Create your account",
        "createacct-another-submit": "Create account",
+       "createacct-continue-submit": "Continue account creation",
+       "createacct-another-continue-submit": "Continue account creation",
        "createacct-benefit-heading": "{{SITENAME}} is made by people like you.",
        "createacct-benefit-icon1": "icon-edits",
        "createacct-benefit-head1": "{{NUMBEROFEDITS}}",
        "nocookieslogin": "{{SITENAME}} uses cookies to log in users.\nYou have cookies disabled.\nPlease enable them and try again.",
        "nocookiesfornew": "The user account was not created, as we could not confirm its source.\nEnsure you have cookies enabled, reload this page and try again.",
        "nocookiesforlogin": "{{int:nocookieslogin}}",
+       "createacct-loginerror": "The account was successfully created but you could not be logged in automatically. Please proceed to [[Special:UserLogin|manual login]].",
        "noname": "You have not specified a valid username.",
        "loginsuccesstitle": "Logged in",
        "loginsuccess": "<strong>You are now logged in to {{SITENAME}} as \"$1\".</strong>",
-       "nosuchuser": "There is no user by the name \"$1\".\nUsernames are case sensitive.\nCheck your spelling, or [[Special:UserLogin/signup|create a new account]].",
+       "nosuchuser": "There is no user by the name \"$1\".\nUsernames are case sensitive.\nCheck your spelling, or [[Special:CreateAccount|create a new account]].",
        "nosuchusershort": "There is no user by the name \"$1\".\nCheck your spelling.",
        "nouserspecified": "You have to specify a username.",
        "login-userblocked": "This user is blocked. Login not allowed.",
        "createacct-another-realname-tip": "Real name is optional.\nIf you choose to provide it, this will be used for giving the user attribution for their work.",
        "pt-login": "Log in",
        "pt-login-button": "Log in",
+       "pt-login-continue-button": "Continue login",
        "pt-createaccount": "Create account",
        "pt-userlogout": "Log out",
        "pear-mail-error": "$1",
        "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_forbidden-reason": "Passwords cannot be changed: $1",
        "resetpass-no-info": "You must be logged in to access this page directly.",
        "resetpass-submit-loggedin": "Change password",
        "resetpass-submit-cancel": "Cancel",
        "passwordreset-emailsentusername": "If there is an email address associated with this username, then a password reset email will be sent.",
        "passwordreset-emailsent-capture": "A password reset email has been sent, which is shown below.",
        "passwordreset-emailerror-capture": "A password reset email was generated, which is shown below, but sending it to the {{GENDER:$2|user}} failed: $1",
+       "passwordreset-emailsent-capture2": "The password reset {{PLURAL:$1|email has|emails have}} been sent. The {{PLURAL:$1|username and password|list of usernames and passwords}} is shown below.",
+       "passwordreset-emailerror-capture2": "Emailing the {{GENDER:$2|user}} failed: $1 The {{PLURAL:$3|username and password|list of usernames and passwords}} is shown below.",
+       "passwordreset-nocaller": "A caller must be provided",
+       "passwordreset-nosuchcaller": "Caller does not exist: $1",
+       "passwordreset-ignored": "The password reset was not handled. Maybe no provider was configured?",
+       "passwordreset-invalideamil": "Invalid email address",
+       "passwordreset-nodata": "Neither a username nor an email address was supplied",
        "changeemail": "Change or remove email address",
        "changeemail-summary": "",
        "changeemail-header": "Complete this form to change your email address. If you would like to remove the association of any email address from your account, leave the new email address blank when submitting the form.",
        "image_tip": "Embedded file",
        "media_sample": "Example.ogg",
        "media_tip": "File link",
+       "sig-text": "--$1",
        "sig_tip": "Your signature with timestamp",
        "hr_tip": "Horizontal line (use sparingly)",
        "summary": "Summary:",
        "newarticletext": "You have followed a link to a page that does not exist yet.\nTo create the page, start typing in the box below (see the [$1 help page] for more info).\nIf you are here by mistake, click your browser's <strong>back</strong> button.",
        "newarticletextanon": "{{int:newarticletext|$1}}",
        "talkpagetext": "<!-- MediaWiki:talkpagetext -->",
-       "anontalkpagetext": "----\n<em>This is the discussion page for an anonymous user who has not created an account yet, or who does not use it.</em>\nWe therefore have to use the numerical IP address to identify him/her.\nSuch an IP address can be shared by several users.\nIf you are an anonymous user and feel that irrelevant comments have been directed at you, please [[Special:UserLogin/signup|create an account]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous users.",
+       "anontalkpagetext": "----\n<em>This is the discussion page for an anonymous user who has not created an account yet, or who does not use it.</em>\nWe therefore have to use the numerical IP address to identify him/her.\nSuch an IP address can be shared by several users.\nIf you are an anonymous user and feel that irrelevant comments have been directed at you, please [[Special:CreateAccount|create an account]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous users.",
        "noarticletext": "There is currently no text in this page.\nYou can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs],\nor [{{fullurl:{{FULLPAGENAME}}|action=edit}} create this page]</span>.",
        "noarticletext-nopermission": "There is currently no text in this page.\nYou can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages, or <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>, but you do not have permission to create this page.",
        "noarticletextanon": "{{int:noarticletext}}",
        "right-override-export-depth": "Export pages including linked pages up to a depth of 5",
        "right-sendemail": "Send email to other users",
        "right-passwordreset": "View password reset emails",
-       "right-managechangetags": "Create and delete [[Special:Tags|tags]] from the database",
+       "right-managechangetags": "Create and (de)activate [[Special:Tags|tags]]",
        "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",
+       "right-deletechangetags": "Delete [[Special:Tags|tags]] from the database",
        "grant-generic": "\"$1\" rights bundle",
        "grant-group-page-interaction": "Interact with pages",
        "grant-group-file-interaction": "Interact with media",
        "action-viewmyprivateinfo": "view your private information",
        "action-editmyprivateinfo": "edit your private information",
        "action-editcontentmodel": "edit the content model of a page",
-       "action-managechangetags": "create and delete tags from the database",
+       "action-managechangetags": "create and (de)activate tags",
        "action-applychangetags": "apply tags along with your changes",
        "action-changetags": "add and remove arbitrary tags on individual revisions and log entries",
+       "action-deletechangetags": "delete tags from the database",
        "nchanges": "$1 {{PLURAL:$1|change|changes}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|since last visit}}",
        "enhancedrc-history": "history",
        "upload-form-label-own-work": "This is my own work",
        "upload-form-label-infoform-categories": "Categories",
        "upload-form-label-infoform-date": "Date",
-       "upload-form-label-own-work-message-local": "I confirm that I am uploading this file following the terms of service and licensing policies on {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "If you are not able to upload this file under the policies of {{SITENAME}}, please close this dialog and try another method.",
-       "upload-form-label-not-own-work-local-local": "You may also want to try [[Special:Upload|the default upload page]].",
-       "upload-form-label-own-work-message-default": "I understand that I am uploading this file to a shared repository. I confirm that I am doing so following the terms of service and licensing policies there.",
-       "upload-form-label-not-own-work-message-default": "If you are not able to upload this file under the policies of the shared repository, please close this dialog and try another method.",
-       "upload-form-label-not-own-work-local-default": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if this file can be uploaded there under their policies.",
-       "upload-form-label-own-work-message-shared": "I attest that I own the copyright on this file, and agree to irrevocably release this file to Wikimedia Commons under the [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] license, and I agree to the [https://wikimediafoundation.org/wiki/Terms_of_Use Terms of Use].",
-       "upload-form-label-not-own-work-message-shared": "If you do not own the copyright on this file, or you wish to release it under a different license, consider using the [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard].",
-       "upload-form-label-not-own-work-local-shared": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if the site allows the upload of this file under their policies.",
+       "upload-form-label-own-work-message-generic-local": "I confirm that I am uploading this file following the terms of service and licensing policies on {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "If you are not able to upload this file under the policies of {{SITENAME}}, please close this dialog and try another method.",
+       "upload-form-label-not-own-work-local-generic-local": "You may also want to try [[Special:Upload|the default upload page]].",
+       "upload-form-label-own-work-message-generic-foreign": "I understand that I am uploading this file to a shared repository. I confirm that I am doing so following the terms of service and licensing policies there.",
+       "upload-form-label-not-own-work-message-generic-foreign": "If you are not able to upload this file under the policies of the shared repository, please close this dialog and try another method.",
+       "upload-form-label-not-own-work-local-generic-foreign": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if this file can be uploaded there under their policies.",
        "backend-fail-stream": "Could not stream file \"$1\".",
        "backend-fail-backup": "Could not backup file \"$1\".",
        "backend-fail-notexists": "The file $1 does not exist.",
        "timezone-local": "Local",
        "duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",
        "duplicate-displaytitle": "<strong>Warning:</strong> Display title \"$2\" overrides earlier display title \"$1\".",
+       "restricted-displaytitle": "<strong>Warning:</strong> Display title \"$1\" was ignored since it is not equivalent to the page's actual title.",
        "invalid-indicator-name": "<strong>Error:</strong> Page status indicators' <code>name</code> attribute must not be empty.",
        "version": "Version",
        "version-summary": "",
        "tags-delete-not-found": "The tag \"$1\" does not exist.",
        "tags-delete-too-many-uses": "The tag \"$1\" is applied to more than $2 {{PLURAL:$2|revision|revisions}}, which means it cannot be deleted.",
        "tags-delete-warnings-after-delete": "The tag \"$1\" was deleted, but the following {{PLURAL:$2|warning was|warnings were}} encountered:",
+       "tags-delete-no-permission": "You do not have permission to delete change tags.",
        "tags-activate-title": "Activate tag",
        "tags-activate-question": "You are about to activate the tag \"$1\".",
        "tags-activate-reason": "Reason:",
        "log-action-filter-suppress-block": "User supppression by block",
        "log-action-filter-suppress-reblock": "User suppression by reblock",
        "log-action-filter-upload-upload": "New upload",
-       "log-action-filter-upload-overwrite": "Reupload"
+       "log-action-filter-upload-overwrite": "Reupload",
+       "authmanager-authn-not-in-progress": "Authentication is not in progress or session data has been lost. Please start again from the beginning.",
+       "authmanager-authn-no-primary": "The supplied credentials could not be authenticated.",
+       "authmanager-authn-no-local-user": "The supplied credentials are not associated with any user on this wiki.",
+       "authmanager-authn-no-local-user-link": "The supplied credentials are valid but are not associated with any user on this wiki. Login in a different way, or create a new user, and you will have an option to link your previous credentials to that account.",
+       "authmanager-authn-autocreate-failed": "Auto-creation of a local account failed: $1",
+       "authmanager-change-not-supported": "The supplied credentials cannot be changed, as nothing would use them.",
+       "authmanager-create-disabled": "Account creation is disabled.",
+       "authmanager-create-from-login": "To create your account, please fill in the fields below.",
+       "authmanager-create-not-in-progress": "Account creation is not in progress or session data has been lost. Please start again from the beginning.",
+       "authmanager-create-no-primary": "The supplied credentials could not be used for account creation.",
+       "authmanager-link-no-primary": "The supplied credentials could not be used for account linking.",
+       "authmanager-link-not-in-progress": "Account linking is not in progress or session data has been lost. Please start again from the beginning.",
+       "authmanager-authplugin-setpass-failed-title": "Password change failed",
+       "authmanager-authplugin-setpass-failed-message": "The authentication plugin denied the password change.",
+       "authmanager-authplugin-create-fail": "The authentication plugin denied the account creation.",
+       "authmanager-authplugin-setpass-denied": "The authentication plugin does not allow changing passwords.",
+       "authmanager-authplugin-setpass-bad-domain": "Invalid domain.",
+       "authmanager-autocreate-noperm": "Automatic account creation is not allowed.",
+       "authmanager-autocreate-exception": "Automatic account creation temporarily disabled due to prior errors.",
+       "authmanager-userdoesnotexist": "User account \"$1\" is not registered.",
+       "authmanager-userlogin-remembermypassword-help": "Whether the password should be remembered for longer than the length of the session.",
+       "authmanager-username-help": "Username for authentication.",
+       "authmanager-password-help": "Password for authentication.",
+       "authmanager-domain-help": "Domain for external authentication.",
+       "authmanager-retype-help": "Password again to confirm.",
+       "authmanager-email-label": "Email",
+       "authmanager-email-help": "Email address",
+       "authmanager-realname-label": "Real name",
+       "authmanager-realname-help": "Real name of the user",
+       "authmanager-provider-password": "Password-based authentication",
+       "authmanager-provider-password-domain": "Password- and domain-based authentication",
+       "authmanager-account-password-domain": "$1@$2",
+       "authmanager-provider-temporarypassword": "Temporary password",
+       "authprovider-confirmlink-message": "Based on your recent login attempts, the following accounts can be linked to your wiki account. Linking them enables logging in via those accounts. Please select which ones should be linked.",
+       "authprovider-confirmlink-option": "$1 ($2)",
+       "authprovider-confirmlink-request-label": "Accounts which should be linked",
+       "authprovider-confirmlink-request-help": "",
+       "authprovider-confirmlink-success-line": "$1: Linked successfully.",
+       "authprovider-confirmlink-failed-line": "$1: $2",
+       "authprovider-confirmlink-failed": "Account linking did not fully succeed: $1",
+       "authprovider-confirmlink-ok-help": "Continue after displaying linking failure messages.",
+       "authprovider-resetpass-skip-label": "Skip",
+       "authprovider-resetpass-skip-help": "Skip resetting the password.",
+       "authform-nosession-login": "The authentication was successful, but your browser cannot \"remember\" being logged in.\n\n$1",
+       "authform-nosession-signup": "The account was created, but your browser cannot \"remember\" being logged in.\n\n$1",
+       "authform-newtoken": "Missing token. $1",
+       "authform-notoken": "Missing token",
+       "authform-wrongtoken": "Wrong token",
+       "specialpage-securitylevel-not-allowed-title": "Not allowed",
+       "specialpage-securitylevel-not-allowed": "Sorry, you are not allowed to use this page because your identity could not be verified.",
+       "authpage-cannot-login": "Unable to start login.",
+       "authpage-cannot-login-continue": "Unable to continue login. Your session most likely timed out.",
+       "authpage-cannot-create": "Unable to start account creation.",
+       "authpage-cannot-create-continue": "Unable to continue account creation. Your session most likely timed out.",
+       "authpage-cannot-link": "Unable to start account linking.",
+       "authpage-cannot-link-continue": "Unable to continue account linking. Your session most likely timed out.",
+       "cannotauth-not-allowed-title": "Permission denied",
+       "cannotauth-not-allowed": "You are not allowed to use this page",
+       "changecredentials" : "Change credentials",
+       "changecredentials-submit": "Change",
+       "changecredentials-submit-cancel": "Cancel",
+       "changecredentials-invalidsubpage": "$1 is not a valid credential type.",
+       "changecredentials-success": "Your credentials have been changed.",
+       "removecredentials" : "Remove credentials",
+       "removecredentials-submit": "Remove",
+       "removecredentials-submit-cancel": "Cancel",
+       "removecredentials-invalidsubpage": "$1 is not a valid credential type.",
+       "removecredentials-success": "Your credentials have been removed.",
+       "credentialsform-provider": "Credentials type:",
+       "credentialsform-account": "Account name:",
+       "cannotlink-no-provider-title": "There are no linkable accounts",
+       "cannotlink-no-provider": "There are no linkable accounts.",
+       "linkaccounts": "Link accounts",
+       "linkaccounts-success-text": "The account was linked.",
+       "linkaccounts-submit": "Link accounts",
+       "unlinkaccounts": "Unlink accounts",
+       "unlinkaccounts-success": "The account was unlinked."
 }
index 6168265..7277348 100644 (file)
        "noname": "Vi ne tajpis validan salutnomon.",
        "loginsuccesstitle": "Ensalutis",
        "loginsuccess": "Vi ensalutis ĉe {{SITENAME}} kiel uzanto \"$1\".",
-       "nosuchuser": "Neniu uzanto havas nomon \"$1\".\nNomoj por uzantoj estas usklecodistingaj.\nKontrolu vian literumadon, aŭ [[Special:UserLogin/signup|kreu novan konton]].",
+       "nosuchuser": "Neniu uzanto havas nomon \"$1\".\nNomoj por uzantoj estas usklecodistingaj.\nKontrolu vian literumadon, aŭ [[Special:CreateAccount|kreu novan konton]].",
        "nosuchusershort": "Ne ekzistas uzanto kun la nomo \"$1\". Bonvolu kontroli vian ortografion.",
        "nouserspecified": "Vi devas entajpi salutnomon.",
        "login-userblocked": "Ĉi tiu uzanto estas forbarita. Ensalutado ne estas permesita.",
        "accmailtext": "Hazarde generita pasvorto por [[User talk:$1|$1]] estis sendita al $2.\n\nLa pasvorto por ĉi tiu nova konto povas esti ŝanĝita en la paĝo ''[[Special:ChangePassword|ŝanĝi pasvorton]]'' dum ensalutado.",
        "newarticle": "(Nova)",
        "newarticletext": "Vi sekvis ligilon al paĝo ankoraŭ ne ekzistanta. Se vi volas krei ĝin, ektajpu malsupre (vidu la [$1 helpopaĝon] por klarigoj.) Se vi malintence alvenis ĉi tien, simple alklaku la retrobutonon de via retumilo.",
-       "anontalkpagetext": "---- ''Jen diskutopaĝo por anonima kontribuanto kiu ne jam kreis konton aŭ ne uzas ĝin.\nNi tial devas uzi la cifran IP-adreson por identigi lin/ŝin.\nĈi tia IP-adreso povas esti uzata de pluraj uzantoj.\nSe vi estas anonimulo kaj preferus eviti tiajn mistrafajn komentojn al vi, bonvolu [[Special:UserLogin/signup|krei konton]] aŭ [[Special:UserLogin|ensaluti]] por eviti estontan konfuzon pro aliaj anonimaj uzantoj.''",
+       "anontalkpagetext": "---- ''Jen diskutopaĝo por anonima kontribuanto kiu ne jam kreis konton aŭ ne uzas ĝin.\nNi tial devas uzi la cifran IP-adreson por identigi lin/ŝin.\nĈi tia IP-adreso povas esti uzata de pluraj uzantoj.\nSe vi estas anonimulo kaj preferus eviti tiajn mistrafajn komentojn al vi, bonvolu [[Special:CreateAccount|krei konton]] aŭ [[Special:UserLogin|ensaluti]] por eviti estontan konfuzon pro aliaj anonimaj uzantoj.''",
        "noarticletext": "Mankas teksto en ĉi tiu paĝo.\nVi povas [[Special:Search/{{PAGENAME}}|serĉi ĉi tiun paĝtitolon]] en aliaj paĝoj,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serĉi la rilatajn protokolojn],\naŭ [{{fullurl:{{FULLPAGENAME}}|action=edit}} krei ĉi tiun paĝon]</span>.",
        "noarticletext-nopermission": "Estas neniom da teksto en ĉi tiu paĝo.\nVi povas [[Special:Search/{{PAGENAME}}|serĉi ĉi tiun paĝan titolon]] en aliaj paĝoj,\naŭ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serĉi la rilatajn protokolojn]</span>, sed vi ne rajtas krei ĉi tiun paĝon.",
        "missing-revision": "La revizio n-ro $1 de la paĝo nomata \"{{FULLPAGENAME}}\" ne ekzistas.\n\nTio kutime estas kaŭzata per sekvado de malaktuala historio-ligilo al paĝo forigita.\nDetaloj troveblos en la [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de forigoj].",
        "right-managechangetags": "Kreado kaj forigado de [[Special:Tags|etikedoj]] de datumbazo",
        "right-applychangetags": "Aldoni [[Special:Tags|etikedojn]] al propraj ŝanĝoj",
        "right-changetags": "Aldoni kaj forigi arbitrajn [[Special:Tags|etikedojn]] ĉe unuopaj revizioj kaj protokoleroj",
+       "right-deletechangetags": "Forigi [[Special:Tags|etikedojn]] de la datenbazo",
        "grant-generic": "\"$1\" rajtaro",
        "grant-group-page-interaction": "Interagi paĝojn",
        "grant-group-file-interaction": "Interagi aŭdvidaĵajn dosierojn",
        "action-viewmyprivateinfo": "vidi viajn privatajn informojn",
        "action-editmyprivateinfo": "redakti viajn privatajn informojn",
        "action-editcontentmodel": "redakti paĝan enhavmodelon",
-       "action-managechangetags": "krei kaj forigi etikedojn de datumbazo",
+       "action-managechangetags": "Krei kaj (mal)aktivigi etikedojn",
        "action-applychangetags": "aldoni etikedojn al viaj propraj ŝanĝoj",
        "action-changetags": "aldoni kaj forigi arbitrajn etikedojn ĉe unuopaj revizioj kaj protokoleroj",
+       "action-deletechangetags": "Forigi etikedojn de la datenbazo.",
        "nchanges": "$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ekde lasta vizito}}",
        "enhancedrc-history": "historio",
        "upload-form-label-own-work": "Tio estas mia propra laboro",
        "upload-form-label-infoform-categories": "Kategorioj",
        "upload-form-label-infoform-date": "Dato",
-       "upload-form-label-own-work-message-local": "Mi konfirmas ke mi alŝutas tiun dosieron respektante pri la uzadokondiĉoj kaj permesopolitikoj de  {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Se vi ne eblas alŝuti tiun dosieron respektante de politikoj de {{SITENAME}}, bonvolu fermi tiun dialogon kaj provi denove kun alia metodo.",
-       "upload-form-label-not-own-work-local-local": "Vi eble ŝatu egale pravi [[Special:Upload|la defaŭltan paĝon]].",
-       "upload-form-label-own-work-message-default": "Mi komprenas ke mi alŝutas tiun dosieron al komunigita deponejo. Mi konfirmas ke mi faras tiun respektante de la uzadtermoj kaj de la permisilopolitikoj tie.",
-       "upload-form-label-not-own-work-message-default": "Se vi ne eblas alŝuti tiun dosieron respektante de politikoj de komuna deponejo, bonvolu fermi tiun dialogon kaj provi denove kun alia metodo.",
+       "upload-form-label-own-work-message-generic-local": "Mi konfirmas ke mi alŝutas tiun dosieron respektante pri la uzadokondiĉoj kaj permesopolitikoj de  {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Se vi ne eblas alŝuti tiun dosieron respektante de politikoj de {{SITENAME}}, bonvolu fermi tiun dialogon kaj provi denove kun alia metodo.",
+       "upload-form-label-not-own-work-local-generic-local": "Vi eble ŝatu egale pravi [[Special:Upload|la defaŭltan paĝon]].",
+       "upload-form-label-own-work-message-generic-foreign": "Mi komprenas ke mi alŝutas tiun dosieron al komunigita deponejo. Mi konfirmas ke mi faras tiun respektante de la uzadtermoj kaj de la permisilopolitikoj tie.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Se vi ne eblas alŝuti tiun dosieron respektante de politikoj de komuna deponejo, bonvolu fermi tiun dialogon kaj provi denove kun alia metodo.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Vi ankaŭ eble dezirus provi per uzi [[Special:Upload|la alŝutan paĝon sur {{SITENAME}}]], se ĉi tiu dosiero povas esti alŝutita tie respektante de iliaj politikoj.",
        "backend-fail-stream": "Ne povis fluigi dosieron $1.",
        "backend-fail-backup": "Ne povis enarkivigi dosieron $1.",
        "backend-fail-notexists": "La dosiero $1 ne ekzistas.",
        "apihelp": "Helpo pri API",
        "apihelp-no-such-module": "Modulo \"$1\" ne estis trovita.",
        "apisandbox": "API testejo",
+       "apisandbox-jsonly": "JavaScript estas postulita por uzi la API provejon.",
        "apisandbox-api-disabled": "API estas malŝalta en ĉi tiu retejo.",
-       "apisandbox-intro": "Uzu tiun ĉi paĝon por eksperimenti kun '''MediaWiki API'''.\nVidu [//www.mediawiki.org/wiki/API:Main_page la API-dokumentadon] por pli da detaloj pri la uzo de API. Ekz-e: [//www.mediawiki.org/wiki/API#A_simple_example atingi la enhavon de la Ĉefpaĝo]. Elektu agon por vidi pliajn ekzemplojn.\n\nNotu ke, kvankam ĉi tiu estas provejo, agoj kiun vi faros en ĉi tiu paĝo povas modifi la vikion.",
+       "apisandbox-intro": "Uzu tiun ĉi paĝon por eksperimenti kun <strong>Mediavikia retserva API</strong>.\nVidu [[mw:API:Main page|la API-dokumentadon]] por pli da detaloj pri la uzo de API. Ekz-e: [//www.mediawiki.org/wiki/API#A_simple_example atingi la enhavon de la Ĉefpaĝo]. Elektu agon por vidi pliajn ekzemplojn.\n\nNotu ke, kvankam ĉi tiu estas provejo, agoj kiun vi faros en ĉi tiu paĝo povas modifi la vikion.",
+       "apisandbox-fullscreen": "Etendi panelon",
+       "apisandbox-fullscreen-tooltip": "Etendi la proveja panelo por plenigi la retumilan fenestron.",
        "apisandbox-unfullscreen": "Montri paĝon",
+       "apisandbox-unfullscreen-tooltip": "Maletendi la provejan panelon, tiel Mediavikiaj navigadaj ligoj estas haveblaj.",
        "apisandbox-submit": "Fari mendon",
        "apisandbox-reset": "Nuligi",
        "apisandbox-retry": "Reprovi",
+       "apisandbox-loading": "Ŝutas informon de la APIa modulo je \"$1\"…",
+       "apisandbox-load-error": "Eraro okazis dum ŝutis informon por APIa modulo je \"$1\": $2",
+       "apisandbox-no-parameters": "Ĉi tiu APIa modulo ne havas parametron.",
        "apisandbox-helpurls": "Ligiloj pri helpo",
        "apisandbox-examples": "Ekzemploj",
        "apisandbox-dynamic-parameters": "Aldonaj parametroj",
        "apisandbox-submit-invalid-fields-message": "Bonvolu ĝustigi la markitajn kampojn kaj provi denove.",
        "apisandbox-results": "Rezultoj",
        "apisandbox-sending-request": "Sendanta aplikprograminterfacan peton…",
+       "apisandbox-loading-results": "Ricevas APIajn rezultojn…",
+       "apisandbox-results-error": "Eraro okazis dum ŝutis la APIan petan respondon: $1.",
        "apisandbox-request-url-label": "Mendi URL-on.",
        "apisandbox-request-time": "Tempo de peto:{{PLURAL:$1|$1 ms}}",
+       "apisandbox-results-fixtoken": "Korekti ĵetonon kaj resendi",
+       "apisandbox-results-fixtoken-fail": "Malsukcese venigis ĵetonon je \"$1\".",
+       "apisandbox-alert-page": "Kampoj de ĉi tiu paĝo ne estas validaj.",
+       "apisandbox-alert-field": "La valoro de ĉi tiu kampo ne estas valida.",
        "booksources": "Librofontoj",
        "booksources-search-legend": "Serĉi librofontojn",
        "booksources-search": "Serĉi",
        "activeusers-hidebots": "kaŝi robotojn",
        "activeusers-hidesysops": "Kaŝi administrantojn",
        "activeusers-noresult": "Neniuj uzantoj trovitaj.",
+       "activeusers-submit": "Montri la agemajn uzantojn",
        "listgrouprights": "Gruprajtoj de uzantoj",
        "listgrouprights-summary": "Jen listo de uzanto-grupoj difinitaj en ĉi tiu vikio, kun ties asociaj atingrajtoj.\nEstas [[{{MediaWiki:Listgrouprights-helppage}}|aldona informo]] pri individuaj rajtoj.",
        "listgrouprights-key": "* <span class=\"listgrouprights-granted\">Donita rajto</span>\n* <span class=\"listgrouprights-revoked\">Forigita rajto</span>",
        "listgrouprights-namespaceprotection-namespace": "Nomspaco",
        "listgrouprights-namespaceprotection-restrictedto": "Rajtoj, kiuj permesas al uzanto redakti",
        "listgrants": "Rajtoj donitaj",
+       "listgrants-grant": "Aljuĝoj",
+       "listgrants-rights": "Rajtoj",
        "trackingcategories": "Kategorioj por kontrolado",
        "trackingcategories-summary": "Ĉi tiu paĝo listigas kategoriojn por kontrolado, aŭtomate farita de la Mediavikia programaro. Ties nomoj estas ŝanĝebla, ŝanĝante la paran sistemmesaĝon en la nomspaco {{ns:8}}.",
        "trackingcategories-msg": "Kategorio pri kontrolado",
        "emailccsubject": "Kopio de via mesaĝo al $1: $2",
        "emailsent": "Retmesaĝo sendita",
        "emailsenttext": "Via retmesaĝo estas sendita.",
-       "emailuserfooter": "Ĉi tiun retpoŝton sendis $1 al $2 per la funkcio \"{{int:emailuser}}\" ĉe {{SITENAME}}.",
+       "emailuserfooter": "Ĉi tiun retpoŝton estis sendita far $1 al $2 per la funkcio \"{{int:emailuser}}\" el {{SITENAME}}.",
        "usermessage-summary": "Lasanta sisteman mesaĝon.",
        "usermessage-editor": "Mesaĝanto de sistemo",
        "watchlist": "Mia atentaro",
        "watchlistanontext": "Bonvolu ensaluti por vidi aŭ redakti vian atentaron.",
        "watchnologin": "Ne ensalutinta",
        "addwatch": "Aldoniĝi al atentaro",
-       "addedwatchtext": "La paĝo \"[[:$1]]\" aldoniĝis al via [[Special:Watchlist|atentaro]]. Estontaj ŝanĝoj de tiu paĝo kaj de ĝia rilata diskutpaĝo aperos tie.",
+       "addedwatchtext": "\"[[:$1]]\" kaj ĝia diskutpaĝo estis aldonitaj al via [[Special:Watchlist|atentaro]].",
        "addedwatchtext-short": "La paĝo \"$1\" estis aldonita al via atento-listo.",
        "removewatch": "Forigi el atentaro",
-       "removedwatchtext": "La paĝo \"[[:$1]]\" estas forigita el via [[Special:Watchlist|atentaro]].",
+       "removedwatchtext": "\"[[:$1]]\" kaj ĝia diskutpaĝo estis forigita el via [[Special:Watchlist|atentaro]].",
        "removedwatchtext-short": "La paĝo \"$1\" estis forigita el via atento-listo.",
        "watch": "Atenti",
        "watchthispage": "Priatenti paĝon",
        "wlshowlast": "Montri el lastaj $1 horoj $2 tagoj",
        "watchlist-hide": "Kaŝi",
        "watchlist-submit": "Montri",
+       "wlshowtime": "Vidigenda tempodaŭro:",
        "wlshowhideminor": "Etaj redaktoj",
        "wlshowhidebots": "robotoj",
        "wlshowhideliu": "registritaj uzantoj",
        "rollback-success": "Restaris redaktojn de $1; ŝanĝis al lasta versio de $2.",
        "sessionfailure-title": "Seanco malsukcesis",
        "sessionfailure": "Ŝajnas, ke estas problemo kun via ensalutado;\nĈi ago estis nuligita por malhelpi fiensalutadon.\nBonvolu alklaki la reirbutonon kaj reŝargi la paĝon el kiu vi venas, kaj provu denove.",
+       "changecontentmodel": "Ŝanĝi la enhavomodelon de paĝo",
+       "changecontentmodel-legend": "Ŝanĝi la enhavomodelon",
        "changecontentmodel-title-label": "Titolo de paĝo",
+       "changecontentmodel-model-label": "Nova enhavomodelo",
        "changecontentmodel-reason-label": "Kialo:",
        "changecontentmodel-submit": "Ŝanĝi",
+       "changecontentmodel-success-title": "Enhavomodelo estis ŝanĝigita",
+       "changecontentmodel-success-text": "La enhavotipo de [[:$1]] estis ŝanĝigita.",
+       "changecontentmodel-cannot-convert": "La enhavo en [[:$1]] ne transtipeblas al $2.",
+       "changecontentmodel-nodirectediting": "La enhavomodelo $1 ne permesas la rektan redaktadon",
+       "changecontentmodel-emptymodels-title": "Neniu disponebla enhavomodelo",
+       "changecontentmodel-emptymodels-text": "La enhavo en [[:$1]] ne transtipeblas al iu ajn tipo.",
+       "log-name-contentmodel": "Ŝanĝprotokolo de enhavomodelo",
+       "log-description-contentmodel": "Eventoj rilataj kun la enhavomodeloj de paĝo",
+       "logentry-contentmodel-new": "$1 {{GENDER:$2|kreis}} la paĝo $3 per uzado de ne-defaŭlta enhavomodelo \"$5\"",
+       "logentry-contentmodel-change": "$1 {{GENDER:$2|ŝanĝis}} la enhavomodelon de la paĝo $3 el \"$4\" al \"$5\"",
        "logentry-contentmodel-change-revertlink": "restarigi",
        "logentry-contentmodel-change-revert": "restarigi",
        "protectlogpage": "Protokolo pri protektoj",
        "protect-locked-blocked": "Vi ne povas ŝanĝi prokekto-nivelojn dum forbarita. Jen la nunaj ecoj de la paĝo '''$1''':",
        "protect-locked-dblock": "Ne povas ŝanĝi nivelojn de protekto pro aktiva datumbaza ŝlosado.\nJen la nunaj agordoj de la paĝo '''$1''':",
        "protect-locked-access": "Via konto ne havas rajton ŝanĝi protekto-nivelojn.\nJen la aktualaj valoroj por la paĝo '''$1''':",
-       "protect-cascadeon": "Ĉi paĝo estas nun protektita kontraŭ redaktado ĉar ĝi estas inkluzivita en {{PLURAL:$1|jena paĝo, kiu mem estas protektita|jenaj paĝoj, kiuj mem estas protektitaj}} per kaskada protekto.\nŜanĝoj de ĝia protektonivelo ne influos la kaskadan protekton.",
+       "protect-cascadeon": "Ĉi paĝo estas nun protektita kontraŭ redaktado ĉar ĝi estas transenhavigita en {{PLURAL:$1|jena paĝo, kiu mem estas protektita|jenaj paĝoj, kiuj mem estas protektitaj}} per kaskada protekto.\nŜanĝoj de ĝia protektonivelo ne influos la kaskadan protekton.",
        "protect-default": "Permesigi ĉiujn uzantojn",
        "protect-fallback": "Permesi nur uzantojn kun la rajto  \"$1\"",
        "protect-level-autoconfirmed": "Permesi nur aŭtomate konfirmitajn uzantojn",
        "undeletepagetext": "La {{PLURAL:$1|jena paĝo estis forigita|jenaj paĝoj estis forigitaj}}, sed ankoraŭ restas {{PLURAL:$1|arkivita|arkivitaj}} kaj {{PLURAL:$1|restarigebla|restarigeblaj}}.\nLa arkivo povas esti malplenigita periode.",
        "undelete-fieldset-title": "Malforigi versiojn",
        "undeleteextrahelp": "Por restarigi la tutan kronologion de la paĝo, lasu ĉiujn markobutonoj malŝaltitaj kaj klaku la butonon '''''{{int:undeletebtn}}'''''.\nPor restarigi selektitajn versiojn de la paĝo, marku la butonojn konformajn al la dezirataj versioj, kaj klaku la butonon '''''{{int:undeletebtn}}'''''.",
-       "undeleterevisions": "$1 {{PLURAL:$1|versio arkivita|versioj arkivitaj}}",
+       "undeleterevisions": "$1 {{PLURAL:$1|versio forigita|versioj forigitaj}}",
        "undeletehistory": "Se vi restarigos la paĝon, ĉiuj versioj estos restarigitaj en la historio.\nSe nova paĝo kun la sama nomo estis kreita post la forigo, la restarigitaj versioj aperos antaŭe en la antaŭa historio.",
        "undeleterevdel": "Restarigo ne estos farita se ĝi rezultos en la supera paĝa aŭ dosiera versio estonte parte forigita. Tiuzake, vi malmarku aŭ malkaŝu la plej novajn forigitajn versiojn.",
        "undeletehistorynoadmin": "Ĉi tiu artikolo estis forigita. La kaŭzo por la forigo estas montrata en la malsupra resumo, kune kun detaloj pri la uzantoj, kiuj redaktis ĉi tiun paĝon antaŭ la forigo. La aktuala teksto de ĉi tiuj forigitaj versioj estas atingebla nur de administrantoj.",
        "whatlinkshere-prev": "{{PLURAL:$1|antaŭa|antaŭaj $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|posta|postaj $1}}",
        "whatlinkshere-links": "← ligiloj",
-       "whatlinkshere-hideredirs": "$1 alidirektilojn",
-       "whatlinkshere-hidetrans": "$1 transinkluzivaĵojn",
-       "whatlinkshere-hidelinks": "$1 ligilojn",
-       "whatlinkshere-hideimages": "$1 ligiloj al bildo",
+       "whatlinkshere-hideredirs": "Kaŝi alidirektilojn",
+       "whatlinkshere-hidetrans": "Kaŝi transinkluzivaĵojn",
+       "whatlinkshere-hidelinks": "Kaŝi ligilojn",
+       "whatlinkshere-hideimages": "Malvidigi ligojn al dosiero",
        "whatlinkshere-filters": "Filtriloj",
        "whatlinkshere-submit": "Ek",
        "autoblockid": "Aŭtomata forbaro #$1",
        "block-log-flags-hiddenname": "salutnomo kaŝita",
        "range_block_disabled": "La ebleco de administranto krei forbaritajn intervalojn da IP-adresoj estas malebligita.",
        "ipb_expiry_invalid": "Nevalida blokdaŭro.",
+       "ipb_expiry_old": "Limdato antaŭas la nuntempon.",
        "ipb_expiry_temp": "Kaŝitaj salutnomaj blokoj estu daŭraj.",
        "ipb_hide_invalid": "Ne povas subpremi ĉi tiun konton; ĝi havas {{PLURAL:$1|unu redakton|$1 redaktojn}}.",
        "ipb_already_blocked": "\"$1\" estas jam forbarita",
        "lockdbsuccesstext": "La datumaro de {{SITENAME}} estas ŝlosita.\n<br />Ne forgesu malŝlosi ĝin post kiam vi finos la riparadon.",
        "unlockdbsuccesstext": "La datumaro de {{SITENAME}} estas malŝlosita.",
        "lockfilenotwritable": "La datumbaza dosiero pri ŝlosado ne estas skribebla. Por ŝlosi aŭ malŝlosi la datumbazon, ĉi devas esti skribebla de la TTT-servilo.",
+       "databaselocked": "Datenbazo jam estas ŝlosita.",
        "databasenotlocked": "La datumbazo ne estas ŝlosita.",
        "lockedbyandtime": "(de {{GENDER:$1|$1}} je $2, $3)",
        "move-page": "Alinomi $1",
        "export-download": "Konservi kiel dosieron",
        "export-templates": "Inkluzivi ŝablonojn",
        "export-pagelinks": "Inkluzivi ligitajn paĝoj al profundo de:",
+       "export-manual": "Aldoni paĝojn per uzanta efiko",
        "allmessages": "Ĉiuj mesaĝoj",
        "allmessagesname": "Nomo",
        "allmessagesdefault": "Defaŭlta teksto",
        "tooltip-pt-preferences": "{{GENDER:|Viaj}} preferoj",
        "tooltip-pt-watchlist": "Listo de paĝoj kies ŝanĝojn vi priatentas.",
        "tooltip-pt-mycontris": "Listo de viaj kontribuoj",
+       "tooltip-pt-anoncontribs": "Listo de redaktoj faritaj el ĉi tiu IPa adreso",
        "tooltip-pt-login": "Vi estas invitita ensaluti, tamen ne estas devige.",
        "tooltip-pt-logout": "Elsaluti",
        "tooltip-pt-createaccount": "Ni rekomendas al vi kreon de uzantokonto kaj ensaluto; tamen, tio ne estas deviga",
        "tooltip-feed-rss": "RSS-fonto por tiu ĉi paĝo",
        "tooltip-feed-atom": "Atom-fonto por ĉi tiu paĝo",
        "tooltip-t-contributions": "Listo de kontribuoj de {{GENDER:$1|ĉi tiu uzanto}}",
-       "tooltip-t-emailuser": "Sendi retmesaĝon al tiu ĉi uzanto",
+       "tooltip-t-emailuser": "Sendi retmesaĝon al {{GENDER:$1|tiu ĉi uzantiĉo|tiu ĉi uzantino|tiu ĉi uzanto}}",
        "tooltip-t-info": "Pli da informo pri ĉi tiu paĝo",
        "tooltip-t-upload": "Alŝuti dosierojn",
        "tooltip-t-specialpages": "Listo de ĉiuj specialaj paĝoj",
        "tooltip-ca-nstab-category": "Vidi la paĝon de la kategorio",
        "tooltip-minoredit": "Marki tiun ŝanĝon kiel etan",
        "tooltip-save": "Konservi viajn ŝanĝojn",
+       "tooltip-publish": "Publikigi viajn ŝanĝojn",
        "tooltip-preview": "Antaŭrigardi viajn ŝanĝojn. Bonvolu uzi tion antaŭ ol konservi ilin!",
        "tooltip-diff": "Montri la ŝanĝojn kiujn vi faris de la teksto.",
        "tooltip-compareselectedversions": "Rigardi la malsamojn inter ambaŭ selektitaj versioj de ĉi tiu paĝo.",
        "lastmodifiedatby": "Ĉi paĝo estis laste ŝanĝita je $2, $1 de $3.",
        "othercontribs": "Bazita sur la laboro de $1.",
        "others": "aliaj",
-       "siteusers": "{{PLURAL:$2|uzanto|uzantoj}} de {{SITENAME}} $1",
+       "siteusers": "{{GENDER:$2|uzantiĉo|uzantino|uzanto}}{{PLURAL:$2||j}} $1 de {{SITENAME}}",
        "anonusers": "{{SITENAME}}-{{PLURAL:$2|anonimulo|anonimuloj}} $1",
        "creditspage": "Atribuoj de paĝo",
        "nocredits": "Ne estas informo pri atribuoj por ĉi paĝo.",
        "pageinfo-robot-index": "Permesata",
        "pageinfo-robot-noindex": "Malpermesata",
        "pageinfo-watchers": "Nombro de paĝatentantoj",
+       "pageinfo-visiting-watchers": "Nombro de paĝoatendantoj kiuj vizitis la ĵusajn redaktitojn",
        "pageinfo-few-watchers": "Malpli ol $1 {{PLURAL:$1|atentanto|atentantoj}}",
+       "pageinfo-few-visiting-watchers": "Eble estas aŭ eble eble ne estas paĝatendanta uzanto kiu vizitas ĵusajn redaktitojn",
        "pageinfo-redirects-name": "Nombro da alidirektiloj al ĉi tiu paĝo",
        "pageinfo-subpages-name": "Subpaĝoj de ĉi tiu paĝo",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|alidirektilo|alidirektiloj}}; $3 {{PLURAL:$3|ne-alidirektilo|ne-alidirektiloj}})",
        "pageinfo-category-files": "Nombro de dosieroj",
        "markaspatrolleddiff": "Marki kiel patrolitan",
        "markaspatrolledtext": "Marki ĉi tiun paĝon patrolita",
+       "markaspatrolledtext-file": "Marki ĉi tiu versio de dosiero kiel patrolita",
        "markedaspatrolled": "Markita kiel patrolita",
        "markedaspatrolledtext": "La elektita versio [[:$1]] estas markita kiel patrolita.",
        "rcpatroldisabled": "Patrolado de lastaj ŝanĝoj malaktivigita",
        "svg-long-error": "Malvalida SVG-dosiero: $1",
        "show-big-image": "Fonta dosiero",
        "show-big-image-preview": "Grandeco de ĉi antaŭvido: $1.",
+       "show-big-image-preview-differ": "Grando de tiu $3 antaŭprezento de tiu $2a dosiero: $1.",
        "show-big-image-other": "{{PLURAL:$2|Alia distingivo|Aliaj distingivoj}}: $1.",
        "show-big-image-size": "$1 × $2 rastrumeroj",
        "file-info-gif-looped": "ripeta GIF",
        "newimages-legend": "Dosiernomo",
        "newimages-label": "Dosiernomo (aŭ parto de ĝi):",
        "newimages-showbots": "Montri alŝutojn per robotoj",
+       "newimages-hidepatrolled": "Malvidigi la patrolitajn alŝutitojn",
        "noimages": "Nenio videbla.",
        "ilsubmit": "Serĉi",
        "bydate": "laŭ dato",
        "confirmemail_body_set": "Iu, supozeble vi mem, de IP-adreso $1,\nagordis la retpoŝadreson de konto \"$2\" al ĉi tiu adreso je {{SITENAME}}.\n\nPor konfirmi ke ĉi tiu konto vere apertenas al vi kaj refunkciigi la retpoŝtfunkciojn je {{SITENAME}}, bonvolu\nmalfermi la jenon ligon per via foliumilo:\n\n$3\n\nSe ĉi tiu konto *ne* apertenas al vi, bonvolu sekvi jenan ligon por nuligi la konfirmon pir la retpoŝadreso:\n\n$5\n\nĈi tiu konfirma kodo eksvalidiĝos je $4",
        "confirmemail_invalidated": "Konfirmado de retadreso estas nuligita",
        "invalidateemail": "Nuligi konfirmadon de retadreso",
+       "notificationemail_subject_changed": "La retpôstadreso registrita de {{SITENAME}} estis ŝanĝita",
+       "notificationemail_subject_removed": "La retpôstadreso registrita de {{SITENAME}} estis forigita",
+       "notificationemail_body_changed": "Iu, verŝajne vi, el IPa adreso $1, ŝanĝis la retpoŝtan adreson de la konto \"$2\" al \"$3\" por {{SITENAME}}.\n\nSe tio ne estis vi, kontaktu retejan administranton tuj.",
+       "notificationemail_body_removed": "Iu, verŝajne vi, el IPa adreso $1, forigis la retpoŝtan adreson de la konto \"$2\" por  {{SITENAME}}.\n\nSe tio ne estis vi, kontaktu retejan administranton tuj.",
        "scarytranscludedisabled": "[Intervikia transinkluzivado estas malebligita.]",
        "scarytranscludefailed": "[Akiro de ŝablono $1 malsukcesis.]",
        "scarytranscludefailed-httpstatus": "[Malsukcesis akiri la ŝablonon $1 : HTTP  $2 ]",
        "scarytranscludetoolong": "[URL-o estas tro longa]",
        "deletedwhileediting": "'''Averto''': Ĉi tiu paĝo estis forigita post vi ekredaktis!",
-       "confirmrecreate": "Uzanto [[User:$1|$1]] ([[User talk:$1|diskuto]]) forigis ĉi tiun paĝon post vi ekredaktis ĝin kun kialo:\n: ''$2''\nBonvolu konfirmi ke vi ja volas rekrei la paĝon.",
+       "confirmrecreate": "Uzanto [[User:$1|$1]] ([[User talk:$1|diskuto]]) forigis ĉi tiun paĝon post vi ekredaktis ĝin pro kialo:\n: <em>$2</em>\nBonvolu konfirmi ke vi ja volas rekrei la paĝon.",
        "confirmrecreate-noreason": "Uzanto [[User:$1|$1]] ([[User talk:$1|diskuto-paĝo]]) forigis ĉi tiun paĝon post vi ekredaktis ĝin.\nBonvolu konfirmi ke vi ja volas rekrei la paĝon.",
        "recreate": "Rekrei",
        "confirm_purge_button": "Ek!",
        "timezone-local": "Loka",
        "duplicate-defaultsort": "'''Averto:''' Defaŭlta ordiga ŝlosilo \"$2\" anstataŭigas pli fruan defaŭltan ordigan ŝlosilon \"$1\".",
        "duplicate-displaytitle": "<strong>Atentigo:</strong> La montrata titolo \"$2\" transpasas la antaŭan titolon \"$1\".",
+       "restricted-displaytitle": "<strong>Averto:</strong> Vidiga titolo \"$1\" estis ignorita pro tio ke ĝi ne estas ekvivalenta al la efektiva titolo de la paĝo.",
        "invalid-indicator-name": "<strong>Eraro:</strong> Atributo de la paĝstata indikilo <code>name</code> maldevas esti malplena.",
        "version": "Versio",
        "version-extensions": "Instalitaj kromprogramoj",
        "version-libraries-license": "Permesilo",
        "version-libraries-description": "Priskribo",
        "version-libraries-authors": "Aŭtoroj",
-       "redirect": "Alidirektilo laŭ dosiero, uzanto, paĝo aŭ identigilo de revizio.",
+       "redirect": "Alidirektilo laŭ dosiera, uzanta, paĝa, revizia aŭ protokola identigilo.",
        "redirect-summary": "Tiu ĉi paĝo alidirektas al dosiero (laŭ ĝia nomo), paĝo (laŭ ĝia revizio-numero aŭ paĝo-identigilo) aŭ al uzantopaĝo (laŭ numera uzanto-identigilo). Uzado: [[{{#Special:Redirect}}/file/Ekzemplo.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], aŭ [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "Ek",
        "redirect-lookup": "Traserĉi:",
        "redirect-page": "Paĝa identigo",
        "redirect-revision": "Revizio de la paĝo",
        "redirect-file": "Dosiernomo",
+       "redirect-logid": "Protokola identigilo",
        "redirect-not-exists": "Valoro ne trovita",
        "fileduplicatesearch": "Serĉu duplikatajn dosierojn",
        "fileduplicatesearch-summary": "Serĉi duplikatajn dosierojn bazite de haketvaloro.",
        "tags-deactivate": "malaktivigi",
        "tags-hitcount": "$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}",
        "tags-manage-no-permission": "Vi ne havas la rajton prilabori markilojn.",
+       "tags-manage-blocked": "Vi ne povas administri ŝanĝajn etikedojn dum blokita.",
        "tags-create-heading": "Krei novan markilon",
        "tags-create-explanation": "Nove kreitaj etikedoj estos defaŭlte disponeblaj por uzado de uzantoj kaj robotoj.",
        "tags-create-tag-name": "Nomo de etikedo:",
        "tags-delete-not-allowed": "Ne eblas forigi etikedojn difinitajn de etendaĵoj, krom se la etendaĵo eksplice permesas tion.",
        "tags-delete-not-found": "La etikedo \"$1\" ne ekzistas.",
        "tags-delete-too-many-uses": "La etikedo \"$1\" estas aldonita al pli ol $2 {{PLURAL:$2|revizio|revizioj}}, kio signifas, ke ĝi ne povas esti forigita.",
-       "tags-delete-warnings-after-delete": "LA etikedo \"$1\" estis sukcese forigita, sed estis {{PLURAL:$2|trafita jena atentigo|trafitaj jenaj atentigoj}}:",
+       "tags-delete-warnings-after-delete": "La etikedo \"$1\" estis forigita, sed trafis {{PLURAL:$2|jenan averton|jenajn avertojn}}:",
+       "tags-delete-no-permission": "Vi ne havas permeson por forigi tiujn etikedojn de ŝanĝo.",
        "tags-activate-title": "Aktivigi markilon",
        "tags-activate-question": "Vi estas aktivigonta la markilon \"$1\".",
        "tags-activate-reason": "Kialo:",
        "tags-deactivate-not-allowed": "Ne eblas malaktivigi la etikedon \"$1\".",
        "tags-deactivate-submit": "Malaktivigi",
        "tags-apply-no-permission": "Vi ne havas permeson por aldoni ŝanĝo-etikedojn al viaj ŝanĝoj.",
+       "tags-apply-blocked": "Vi ne povas apliki etikedojn de ŝanĝo kune kun viaj ŝanĝoj dum blokita.",
        "tags-apply-not-allowed-one": "La etikedon \"$1\" ne eblas aldoni permane.",
        "tags-apply-not-allowed-multi": "Ne estas permesite permane aldoni {{PLURAL:$2|jenan etikedon|jenajn etikedojn}}: $1",
        "tags-update-no-permission": "Vi ne havas permeson por aldoni aŭ forigi ŝanĝo-etikedojn de unuopaj revizioj aŭ protokoleroj.",
+       "tags-update-blocked": "Vi ne povas aldoni aŭ forigi etikedojn de ŝanĝo dum blokita.",
        "tags-update-add-not-allowed-one": "Ne estas permesite permane aldoni la etikedon \"$1\".",
        "tags-update-add-not-allowed-multi": "Ne estas permesite permane aldoni {{PLURAL:$2|jenan etikedon|jenajn etikedojn}}: $1",
        "tags-update-remove-not-allowed-one": "Ne estas permesite permane forigi la etikedon \"$1\".",
        "tags-edit-reason": "Kialo:",
        "tags-edit-revision-submit": "Apliki ŝanĝojn al {{PLURAL:$1|tiu ĉi revizio|$1 revizioj}}",
        "tags-edit-logentry-submit": "Apliki ŝanĝojn al {{PLURAL:$1|tiu ĉi protokolero|$1 protokoleroj}}",
-       "tags-edit-success": "La ŝanĝoj estis sukcese aplikitaj.",
+       "tags-edit-success": "Aplikis la ŝanĝojn.",
        "tags-edit-failure": "La ŝanĝojn ne eblis apliki:\n$1",
        "tags-edit-nooldid-title": "Nevalida cela revizio",
        "tags-edit-nooldid-text": "Vi specifis neniun celan revizion por efektivigi la funkcion aŭ la specifita revizio ne ekzistas.",
index 296b22e..7fe4263 100644 (file)
                        "Eloy",
                        "Lemondoge",
                        "Jdforrester",
-                       "Indiralena"
+                       "Indiralena",
+                       "Rubentl134",
+                       "Codynguyen1116"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "tog-watchdefault": "Añadir las páginas y archivos que edite a mi lista de seguimiento",
        "tog-watchmoves": "Añadir las páginas y archivos que mueva a mi lista de seguimiento",
        "tog-watchdeletion": "Añadir las páginas y archivos que borre a mi lista de seguimiento",
-       "tog-watchuploads": "Agregar nuevos archivos puedo subir a mi lista de favoritos",
+       "tog-watchuploads": "Agregar los archivos nuevos que suba a mi lista de seguimiento",
        "tog-watchrollback": "Añadir las páginas donde haya realizado una reversión a mi lista de seguimiento",
        "tog-minordefault": "Marcar todas las ediciones como menores de manera predeterminada",
        "tog-previewontop": "Mostrar previsualización antes del cuadro de edición",
        "noname": "No se ha especificado un nombre de usuario válido.",
        "loginsuccesstitle": "Has accedido",
        "loginsuccess": "<strong>Has accedido a {{SITENAME}} como «$1».</strong>",
-       "nosuchuser": "No existe ningún usuario llamado «$1».\nLos nombres de usuario son sensibles a las mayúsculas.\nRevisa tu ortografía, o [[Special:UserLogin/signup|crea una cuenta nueva]].",
+       "nosuchuser": "No existe ningún usuario llamado «$1».\nLos nombres de usuario son sensibles a las mayúsculas.\nRevisa tu ortografía, o [[Special:CreateAccount|crea una cuenta nueva]].",
        "nosuchusershort": "No existe ningún usuario llamado «$1». Comprueba que lo has escrito correctamente.",
        "nouserspecified": "Debes especificar un nombre de usuario.",
        "login-userblocked": "No puedes iniciar sesión porque tu cuenta está bloqueada.",
        "accmailtext": "Se ha enviado a $2 una contraseña generada aleatoriamente para [[User talk:$1|$1]]. Una vez iniciada la sesión, se puede cambiar en la página [[Special:ChangePassword|destinada para ello]].",
        "newarticle": "(Nuevo)",
        "newarticletext": "Has seguido un enlace a una página que aún no existe.\nPara crear esta página, escribe en el cuadro que aparece a continuación. Para más información, consulta la [$1 página de ayuda].\nSi llegaste aquí por error, vuelve a la página anterior.",
-       "anontalkpagetext": "---- ''Esta es la página de discusión de un usuario anónimo que aún no ha creado una cuenta, o no la usa. Por lo tanto, tenemos que usar su dirección IP para identificarlo. Puede que varios usuarios compartan una misma dirección IP. Si eres un usuario anónimo y crees que se han dirigido a ti con comentarios improcedentes, por favor [[Special:UserLogin/signup|crea una cuenta]] o, si ya la tienes, [[Special:UserLogin|identifícate]] para evitar confusiones futuras con otros usuarios anónimos.''",
+       "anontalkpagetext": "---- ''Esta es la página de discusión de un usuario anónimo que aún no ha creado una cuenta, o no la usa. Por lo tanto, tenemos que usar su dirección IP para identificarlo. Puede que varios usuarios compartan una misma dirección IP. Si eres un usuario anónimo y crees que se han dirigido a ti con comentarios improcedentes, por favor [[Special:CreateAccount|crea una cuenta]] o, si ya la tienes, [[Special:UserLogin|identifícate]] para evitar confusiones futuras con otros usuarios anónimos.''",
        "noarticletext": "En este momento no hay texto en esta página.\nPuedes [[Special:Search/{{PAGENAME}}|buscar el título de esta página]] en otras páginas,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar en los registros relacionados],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear esta página]</span>.",
        "noarticletext-nopermission": "Actualmente no hay texto en esta página.\nPuedes [[Special:Search/{{PAGENAME}}|buscar este título de página]] en otras páginas, o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar en los registros relacionados]</span>, pero no tienes permiso para crear esta página.",
        "missing-revision": "La revisión n.º $1 de la página «{{FULLPAGENAME}}» no existe.\n\nEsto suele ocurrir cuando se sigue un enlace de historial obsoleto que apunta a una página ya borrada.\nPuedes encontrar detalles en el [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de borrados].",
        "right-override-export-depth": "Exportar páginas, incluyendo aquellas enlazadas hasta una profundidad de 5",
        "right-sendemail": "Enviar correo electrónico a otros usuarios",
        "right-passwordreset": "Ver los mensajes de restablecimiento de contraseña",
-       "right-managechangetags": "Crear y eliminar [[Special:Tags|etiquetas]] en la base de datos",
+       "right-managechangetags": "Crear y (des)activar [[Special:Tags|etiquetas]]",
        "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",
+       "right-deletechangetags": "Eliminar [[Special:Tags|tags]] de la base de datos",
        "grant-generic": "Paquete de permisos \"$1\"",
        "grant-group-page-interaction": "Interactuar con páginas",
        "grant-group-file-interaction": "Interactuar con multimedia",
        "action-viewmyprivateinfo": "ver tu información privada",
        "action-editmyprivateinfo": "editar tu información privada",
        "action-editcontentmodel": "editar el modelo de contenido de una página",
-       "action-managechangetags": "crear y eliminar etiquetas en la base de datos",
+       "action-managechangetags": "crear y (des)activar etiquetas",
        "action-applychangetags": "aplicar etiquetas junto con los cambios",
        "action-changetags": "agregar y quitar etiquetas arbitrarias a revisiones individuales y entradas del registro",
+       "action-deletechangetags": "eliminar etiquetas de la base de datos",
        "nchanges": "$1 {{PLURAL:$1|cambio|cambios}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde la última visita}}",
        "enhancedrc-history": "historial",
        "upload-form-label-own-work": "Esto es mi trabajo propio",
        "upload-form-label-infoform-categories": "Categorías",
        "upload-form-label-infoform-date": "Fecha",
-       "upload-form-label-own-work-message-local": "Confirmo que estoy subiendo este archivo siguiendo los términos del servicio y las políticas de concesión de licencias en {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Si no es capaz de subir este archivo bajo las políticas de {{SITENAME}}, por favor cierre este cuadro de diálogo e intente otro método.",
-       "upload-form-label-not-own-work-local-local": "Quizás también quieras probar [[Special:Upload|la página predeterminada de subidas]].",
-       "upload-form-label-own-work-message-default": "Entiendo que voy a subir este archivo a un repositorio compartido. Confirmo que estoy haciéndolo que siguiendo los términos de servicio y políticas de licenciamiento de allí.",
-       "upload-form-label-not-own-work-message-default": "Si usted no es capaz de cargar este archivo en virtud de las políticas del repositorio compartido, por favor cierre este cuadro de diálogo y probar con otro método.",
-       "upload-form-label-not-own-work-local-default": "Puede que también quieras usar [[Special:Upload|la página de subidas en {{SITENAME}}]], si se puede subir este archivo bajo sus políticas.",
-       "upload-form-label-own-work-message-shared": "Doy fe que soy dueño de los derechos de autor de este archivo, y acepto irrevocablemente liberar este archivo a Wikimedia Commons bajo la licencia [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], y acepto los [https://wikimediafoundation.org/wiki/Terms_of_Use Términos de Uso].",
-       "upload-form-label-not-own-work-message-shared": "Si no es dueño de los derechos de autor de este archivo, o desea publicarlo bajo una licencia diferentes, considere usar el [https://commons.wikimedia.org/wiki/Special:UploadWizard Asistente de Carga de Commons].",
-       "upload-form-label-not-own-work-local-shared": "Puede que también quieras usar [[Special:Upload|la página de subidas en {{SITENAME}}]], si el sitio permite la subida de este archivo bajo sus políticas.",
+       "upload-form-label-own-work-message-generic-local": "Confirmo que estoy subiendo este archivo siguiendo los términos del servicio y las políticas de concesión de licencias en {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Si no es capaz de subir este archivo bajo las políticas de {{SITENAME}}, por favor cierre este cuadro de diálogo e intente otro método.",
+       "upload-form-label-not-own-work-local-generic-local": "Quizás también quieras probar [[Special:Upload|la página predeterminada de subidas]].",
+       "upload-form-label-own-work-message-generic-foreign": "Entiendo que voy a subir este archivo a un repositorio compartido. Confirmo que estoy haciéndolo que siguiendo los términos de servicio y políticas de licenciamiento de allí.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Si usted no es capaz de cargar este archivo en virtud de las políticas del repositorio compartido, por favor cierre este cuadro de diálogo y probar con otro método.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Puede que también quieras usar [[Special:Upload|la página de subidas en {{SITENAME}}]], si se puede subir este archivo bajo sus políticas.",
        "backend-fail-stream": "No se pudo transmitir el archivo «$1».",
        "backend-fail-backup": "No se pudo hacer copia de seguridad del archivo «$1».",
        "backend-fail-notexists": "El archivo  $1  no existe.",
        "changecontentmodel-success-text": "Se ha cambiado el tipo de contenido de [[:$1]].",
        "changecontentmodel-cannot-convert": "El contenido de [[:$1]] no se puede convertir a un tipo de $2.",
        "changecontentmodel-nodirectediting": "El modelo de contenido $1 no admite la edición directa",
+       "changecontentmodel-emptymodels-title": "No hay modelos de contenido disponibles",
        "log-name-contentmodel": "Registro de cambios del modelo de contenido",
        "log-description-contentmodel": "Eventos relacionados con los modelos de contenido de una página",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|creó}} la página $3 usando un modelo de contenido no predeterminado \"$5\"",
        "whatlinkshere-prev": "{{PLURAL:$1|previa|previas $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|siguiente|siguientes $1}}",
        "whatlinkshere-links": "← enlaces",
-       "whatlinkshere-hideredirs": "$1 redirecciones",
-       "whatlinkshere-hidetrans": "$1 inclusiones",
-       "whatlinkshere-hidelinks": "$1 enlaces",
-       "whatlinkshere-hideimages": "$1 enlaces a archivos",
+       "whatlinkshere-hideredirs": "Ocultar redirecciones",
+       "whatlinkshere-hidetrans": "Ocultar transclusiones",
+       "whatlinkshere-hidelinks": "Ocultar enlaces",
+       "whatlinkshere-hideimages": "Ocultar los vínculos de archivo",
        "whatlinkshere-filters": "Filtros",
        "whatlinkshere-submit": "Ir",
        "autoblockid": "Bloqueo automático #$1",
        "lockdbsuccesstext": "La base de datos de {{SITENAME}} ha sido bloqueada.\n<br />Recuerde retirar el bloqueo después de completar las tareas de mantenimiento.",
        "unlockdbsuccesstext": "La base de datos de {{SITENAME}} ha sido desbloqueada.",
        "lockfilenotwritable": "El archivo-cerrojo de la base de datos no tiene permiso de escritura. Para bloquear o desbloquear la base de datos, este archivo tiene que ser escribible por el servidor web.",
+       "databaselocked": "La base de datos ya está bloqueada.",
        "databasenotlocked": "La base de datos no está bloqueada.",
        "lockedbyandtime": "(por {{GENDER:$1|$1}} el $2 a las $3)",
        "move-page": "Trasladar $1",
        "tags-delete-not-found": "La etiqueta «$1» no existe.",
        "tags-delete-too-many-uses": "No se puede borrar la etiqueta \"$1\" porque se ha aplicado a más de {{PLURAL:$2|una revisión|$2 revisiones}}.",
        "tags-delete-warnings-after-delete": "La etiqueta \"$1\" se borró, pero con {{PLURAL:$2|la siguiente advertencia|las siguientes advertencias}}:",
+       "tags-delete-no-permission": "No tienes permiso para eliminar las etiquetas de cambios.",
        "tags-activate-title": "Activar etiqueta",
        "tags-activate-question": "Estás a punto de activar la etiqueta «$1».",
        "tags-activate-reason": "Motivo:",
index aae6430..02ddf8f 100644 (file)
        "noname": "Sa ei sisestanud kasutajanime lubataval kujul.",
        "loginsuccesstitle": "Sisselogimine õnnestus",
        "loginsuccess": "Oled sisse loginud. Sinu kasutajanimi on \"$1\".",
-       "nosuchuser": "Kasutajat \"$1\" pole.\nKasutajanimed on tõstutundlikud.\nKontrolli kirjapilti või [[Special:UserLogin/signup|loo uus konto]].",
+       "nosuchuser": "Kasutajat \"$1\" pole.\nKasutajanimed on tõstutundlikud.\nKontrolli kirjapilti või [[Special:CreateAccount|loo uus konto]].",
        "nosuchusershort": "Kasutajat nimega \"$1\" pole.\nKontrolli kirjapilti.",
        "nouserspecified": "Kasutajanimi puudub.",
        "login-userblocked": "See kasutaja on blokeeritud. Sisselogimine pole lubatud.",
        "accmailtext": "Kasutajale [[User talk:$1|$1]] genereeritud juhuslik parool saadeti aadressile $2.\n\nSeda saab pärast sisselogimist muuta ''[[Special:ChangePassword|parooli muutmise]]'' leheküljel.",
        "newarticle": "(Uus)",
        "newarticletext": "Lehekülge, kuhu link sind suunas, pole veel.\nEt lehekülg luua, alusta allolevas kastis kirjutamist (lisateave [$1 juhendist]).\nKui sattusid siia kogemata, klõpsa veebilehitseja ''tagasi''-nupule.",
-       "anontalkpagetext": "----''See on anonüümse kasutaja arutelulehekülg. See kasutaja pole kontot loonud või ei kasuta seda. Sellepärast tuleb meil kasutaja tuvastamiseks kasutada tema IP-aadressi. Sellist IP-aadressi võib kasutada mitu kasutajat. Kui oled osutatud IP-aadressi kasutaja ning leiad, et siinsed kommentaarid ei puutu kuidagi sinusse, [[Special:UserLogin/signup|loo palun kasutajakonto]] või [[Special:UserLogin|logi sisse]], et sind edaspidi teiste anonüümsete kasutajatega segi ei aetaks.''",
+       "anontalkpagetext": "----''See on anonüümse kasutaja arutelulehekülg. See kasutaja pole kontot loonud või ei kasuta seda. Sellepärast tuleb meil kasutaja tuvastamiseks kasutada tema IP-aadressi. Sellist IP-aadressi võib kasutada mitu kasutajat. Kui oled osutatud IP-aadressi kasutaja ning leiad, et siinsed kommentaarid ei puutu kuidagi sinusse, [[Special:CreateAccount|loo palun kasutajakonto]] või [[Special:UserLogin|logi sisse]], et sind edaspidi teiste anonüümsete kasutajatega segi ei aetaks.''",
        "noarticletext": "Käesoleval leheküljel hetkel teksti ei ole.\nVõid [[Special:Search/{{PAGENAME}}|otsida pealkirjaks olevat fraasi]] teistelt lehtedelt,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uurida asjassepuutuvaid logisid] või [{{fullurl:{{FULLPAGENAME}}|action=edit}} puuduva lehekülje ise luua]</span>.",
        "noarticletext-nopermission": "Sellel leheküljel pole praegu teksti.\nSaad [[Special:Search/{{PAGENAME}}|otsida selle lehekülje pealkirja]] teistelt lehekülgedelt või <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} otsida seonduvatest logidest]</span>, aga sul pole õigust seda lehekülge alustada.",
        "missing-revision": "Lehekülje \"{{FULLPAGENAME}}\" redaktsiooni $1 pole.\n\nHarilikult tähendab see seda, et sind siia juhatanud link on vananenud ja siin asunud lehekülg on kustutatud.\nÜksikasjad leiad [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} kustutamislogist].",
        "upload-form-label-own-work": "See on minu enda töö",
        "upload-form-label-infoform-categories": "Kategooriad",
        "upload-form-label-infoform-date": "Kuupäev",
-       "upload-form-label-own-work-message-local": "Kinnitan, et seda faili üles laadides järgin saidi {{SITENAME}} kasutustingimusi ja litsentsipõhimõtteid.",
-       "upload-form-label-not-own-work-message-local": "Kui sul pole võimalik laadida seda faili üles kooskõlas saidi {{SITENAME}} reeglitega, siis palun sule see dialoog ja proovi teisiti toimida.",
-       "upload-form-label-not-own-work-local-local": "Võimalik, et soovid kasutada [[Special:Upload|harilikku üleslaadimislehekülge]].",
-       "upload-form-label-own-work-message-default": "Saan aru, et laadin selle faili jagatud varamusse. Kinnitan, et teen seda kooskõlas sealsete kasutustingimuste ja litsentsipõhimõtetega.",
-       "upload-form-label-not-own-work-message-default": "Kui sul pole võimalik laadida seda faili üles kooskõlas jagatud varamu reeglitega, siis palun sule see dialoog ja proovi teisiti toimida.",
-       "upload-form-label-not-own-work-local-default": "Võimalik, et soovid [[Special:Upload|laadida selle faili üles saidil {{SITENAME}}]], kui seda on võimalik teha kooskõlas siinsete reeglitega.",
-       "upload-form-label-own-work-message-shared": "Kinnitan, et olen selle faili autoriõiguse valdaja ja nõustun faili avaldamisega Wikimedia Commonsis pöördumatult Creative Commonsi litsentsi \"[https://creativecommons.org/licenses/by-sa/4.0/deed.et Autorile viitamine + jagamine samadel tingimustel 4.0]\" all. Samuti nõustun [https://wikimediafoundation.org/wiki/Terms_of_Use kasutustingimustega].",
-       "upload-form-label-not-own-work-message-shared": "Kui sa pole selle faili autoriõiguse valdaja või kui soovid avaldada seda teise litsentsi all, siis on sul võimalik kasutada [https://commons.wikimedia.org/wiki/Special:UploadWizard Commonsi üleslaadimisviisardit].",
-       "upload-form-label-not-own-work-local-shared": "Võimalik, et soovid kasutada [[Special:Upload|saidi {{SITENAME}} üleslaadimislehekülge]], kui seda faili on lubatud üles laadida kooskõlas siinsete reeglitega.",
+       "upload-form-label-own-work-message-generic-local": "Kinnitan, et seda faili üles laadides järgin saidi {{SITENAME}} kasutustingimusi ja litsentsipõhimõtteid.",
+       "upload-form-label-not-own-work-message-generic-local": "Kui sul pole võimalik laadida seda faili üles kooskõlas saidi {{SITENAME}} reeglitega, siis palun sule see dialoog ja proovi teisiti toimida.",
+       "upload-form-label-not-own-work-local-generic-local": "Võimalik, et soovid kasutada [[Special:Upload|harilikku üleslaadimislehekülge]].",
+       "upload-form-label-own-work-message-generic-foreign": "Saan aru, et laadin selle faili jagatud varamusse. Kinnitan, et teen seda kooskõlas sealsete kasutustingimuste ja litsentsipõhimõtetega.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Kui sul pole võimalik laadida seda faili üles kooskõlas jagatud varamu reeglitega, siis palun sule see dialoog ja proovi teisiti toimida.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Võimalik, et soovid [[Special:Upload|laadida selle faili üles saidil {{SITENAME}}]], kui seda on võimalik teha kooskõlas siinsete reeglitega.",
        "backend-fail-stream": "Faili $1 ei saanud edastada.",
        "backend-fail-backup": "Faili $1 ei saanud varundada.",
        "backend-fail-notexists": "Faili $1 pole olemas.",
index 4926942..bb7c10d 100644 (file)
        "noname": "Ez duzu baliozko erabiltzaile izen bat zehaztu.",
        "loginsuccesstitle": "Saio hasiera egina",
        "loginsuccess": "'''Saioa hasi duzu {{SITENAME}}(e)n \"$1\" izenarekin.'''",
-       "nosuchuser": "Ez dago \"$1\" izena duen lankiderik.\nLankide izenak zehatza izan behar du.\nEgiaztatu ondo idatzi duzun, edo [[Special:UserLogin/signup|kontu berria sor ezazu]].",
+       "nosuchuser": "Ez dago \"$1\" izena duen lankiderik.\nLankide izenak zehatza izan behar du.\nEgiaztatu ondo idatzi duzun, edo [[Special:CreateAccount|kontu berria sor ezazu]].",
        "nosuchusershort": "Ez dago \"$1\" izena duen erabiltzailerik. Egiaztatu ongi idatzi duzula.",
        "nouserspecified": "Erabiltzaile izena zehaztu beharra daukazu.",
        "login-userblocked": "Erabiltzailea blokeatua dago. Ezin du saioa hasi.",
        "accmailtext": "[[User talk:$1|$1]]-entzako ausaz sortutako pasahitza $2-(r)a bidali da.\n\n''[[Special:ChangePassword|pasahitz aldaketa]]'' orrialdean alda daiteke, behin barruan sartuta.",
        "newarticle": "(Berria)",
        "newarticletext": "Orrialde hau ez da existitzen oraindik. Orrialde sortu nahi baduzu, beheko koadroan idazten hasi zaitezke (ikus [$1 laguntza orrialdea] informazio gehiagorako). Hona nahi gabe etorri bazara, nabigatzaileko '''atzera''' botoian klik egin.",
-       "anontalkpagetext": "----''Orrialde hau konturik sortu ez edo erabiltzen ez duen erabiltzaile anonimo baten eztabaida orria da.\nBere IP helbidea erabili beharko da beraz identifikatzeko.\nErabiltzaile batek baino gehiagok IP bera erabil dezakete ordea.\nErabiltzaile anonimoa bazara eta zurekin zerikusirik ez duten mezuak jasotzen badituzu, mesedez [[Special:UserLogin/signup|Izena eman]] edo [[Special:UserLogin|saioa hasi]] etorkizunean horrelakoak gerta ez daitezen.''",
+       "anontalkpagetext": "----''Orrialde hau konturik sortu ez edo erabiltzen ez duen erabiltzaile anonimo baten eztabaida orria da.\nBere IP helbidea erabili beharko da beraz identifikatzeko.\nErabiltzaile batek baino gehiagok IP bera erabil dezakete ordea.\nErabiltzaile anonimoa bazara eta zurekin zerikusirik ez duten mezuak jasotzen badituzu, mesedez [[Special:CreateAccount|Izena eman]] edo [[Special:UserLogin|saioa hasi]] etorkizunean horrelakoak gerta ez daitezen.''",
        "noarticletext": "Oraindik ez dago testurik orri honetan.\nEdukiz hornitzeko, aukera hauek dituzu: beste orri batzuetan [[Special:Search/{{PAGENAME}}|orri izenburu hau bilatzea]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} lotutako logak bilatzea],\nedo [{{fullurl:{{FULLPAGENAME}}|action=edit}} orri hau sortzea]</span>.",
        "noarticletext-nopermission": "Une honetan ez dago testurik orrialde honetan.\nBeste orrialdeetan [[Special:Search/{{PAGENAME}}|izenburu hau bilatu dezakezu]],\nedo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} erlazionatutako erregistroak bilatu]</span>, baina ez duzu orrialde hau sortzeko baimenik.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" lankidea ez dago erregistatuta. Mesedez, konprobatu orri hau editatu/sortu nahi duzun.",
        "whatlinkshere-links": "← loturak",
        "whatlinkshere-hideredirs": "$1 birzuzenketak",
        "whatlinkshere-hidetrans": "$1 transklusioak",
-       "whatlinkshere-hidelinks": "$1 loturak",
+       "whatlinkshere-hidelinks": "Ezkutatu loturak",
        "whatlinkshere-hideimages": "$1 irudi loturak",
        "whatlinkshere-filters": "Iragazleak",
        "whatlinkshere-submit": "Joan",
index 93d25bf..50d0a8c 100644 (file)
        "noname": "Nu as escrebiu un nombri d'usuáriu corretu.",
        "loginsuccesstitle": "Yeu, lo cúmu va esu?",
        "loginsuccess": "'''Acabihas d'entral en {{SITENAME}} con el nombri \"$1\".'''",
-       "nosuchuser": "Nu dessisti dengún usuáriu anombrau \"$1\".\nCompreba que lo aigas escritu bien, u [[Special:UserLogin/signup|cria una cuenta nueva]].",
+       "nosuchuser": "Nu dessisti dengún usuáriu anombrau \"$1\".\nCompreba que lo aigas escritu bien, u [[Special:CreateAccount|cria una cuenta nueva]].",
        "nosuchusershort": "Nu ai dengún usuáriu llamau \"$1\". Compreba qu'esté bien escritu.",
        "nouserspecified": "Ebis escribil un nombri d'usuáriu.",
        "wrongpassword": "La consínia escrebia nu es correta. Pol favol, preba otra vezi.",
index d60607c..99ac9c4 100644 (file)
        "noname": "شما نام کاربری معتبری مشخص نکرده‌اید.",
        "loginsuccesstitle": "ورود به سامانه",
        "loginsuccess": "'''شما اکنون با نام «$1» به {{SITENAME}} وارد شده‌اید.'''",
-       "nosuchuser": "کاربری با نام «$1» وجود ندارد.\nنام کاربری به بزرگی و کوچکی حروف حساس است.\nاملای نام را بررسی کنید، یا [[Special:UserLogin/signup|یک حساب کاربری تازه بسازید]].",
+       "nosuchuser": "کاربری با نام «$1» وجود ندارد.\nنام کاربری به بزرگی و کوچکی حروف حساس است.\nاملای نام را بررسی کنید، یا [[Special:CreateAccount|یک حساب کاربری تازه بسازید]].",
        "nosuchusershort": "هیچ کاربری با نام ''$1'' وجود ندارد.\nاملایتان را وارسی کنید.",
        "nouserspecified": "باید یک نام کاربری مشخص کنید.",
        "login-userblocked": "این کاربر بسته شده‌است. ورود به سامانه مجاز نیست.",
        "accmailtext": "یک گذرواژهٔ تصادفی برای [[User talk:$1|$1]] به $2 فرستاده شد. می‌توان آن را از صفحهٔ ''[[Special:ChangePassword|تغییر گذرواژه]]'' که هنگام ثبت ورود نمایش می‌یابد تغییر داد.",
        "newarticle": "(تازه)",
        "newarticletext": "شما پیوندی را دنبال کرده‌اید و به صفحه‌ای رسیده‌اید که هنوز وجود ندارد.\nبرای ایجاد صفحه، در مستطیل زیر شروع به نوشتن کنید (برای اطلاعات بیشتر به [$1 صفحهٔ راهنما] مراجعه کنید).\nاگر به اشتباه اینجا آمده‌اید، دکمهٔ «بازگشت» مرورگرتان را بزنید.",
-       "anontalkpagetext": "----''این صفحهٔ بحث برای کاربر گمنامی است که هنوز حسابی درست نکرده است یا از آن استفاده نمی‌کند.\nبنا بر این برای شناسایی‌اش مجبوریم از نشانی آی‌پی عددی استفاده کنیم.\nچنین نشانی‌های آی‌پی ممکن است توسط چندین کاربر به شکل مشترک استفاده شود.\nاگر شما کاربر گمنامی هستید و تصور می‌کنید اظهار نظرات نامربوط به شما صورت گرفته است، لطفاً برای پیشگیری از اشتباه گرفته شدن با کاربران گمنام دیگر در آینده [[Special:UserLogin/signup|حسابی ایجاد کنید]] یا [[Special:UserLogin|به سامانه وارد شوید]].''",
+       "anontalkpagetext": "----''این صفحهٔ بحث برای کاربر گمنامی است که هنوز حسابی درست نکرده است یا از آن استفاده نمی‌کند.\nبنا بر این برای شناسایی‌اش مجبوریم از نشانی آی‌پی عددی استفاده کنیم.\nچنین نشانی‌های آی‌پی ممکن است توسط چندین کاربر به شکل مشترک استفاده شود.\nاگر شما کاربر گمنامی هستید و تصور می‌کنید اظهار نظرات نامربوط به شما صورت گرفته است، لطفاً برای پیشگیری از اشتباه گرفته شدن با کاربران گمنام دیگر در آینده [[Special:CreateAccount|حسابی ایجاد کنید]] یا [[Special:UserLogin|به سامانه وارد شوید]].''",
        "noarticletext": "این صفحه هم‌اکنون دارای هیچ متنی نیست.\nشما می‌توانید در صفحه‌های دیگر [[Special:Search/{{PAGENAME}}|عنوان این صفحه را جستجو کنید]]،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} سیاهه‌های مرتبط را جستجو کنید]،\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} این صفحه را ایجاد کنید]</span>.",
        "noarticletext-nopermission": "این صفحه هم‌اکنون متنی ندارد.\nشما می‌توانید در دیگر صفحات [[Special:Search/{{PAGENAME}}|این عنوان را جستجو کنید]]،\nیا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} سیاهه‌های مرتبط را بگردید]</span> ولی شما اجازه ایجاد این صفحه را ندارید.",
        "missing-revision": "ویرایش #$1 از صفحهٔ «{{FULLPAGENAME}}» موجود نیست.\n\nمعمولاً در اثر پیوند به تاریخچهٔ به‌روز نشدهٔ صفحهٔ حذف شده است.\nمی‌توانید جزئیات بیشتر را در [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} سیاههٔ حذف] بیابید.",
        "right-managechangetags": "ایجاد و حذف [[Special:Tags|برچسب‌ها]] از پایگاه داده",
        "right-applychangetags": "تائید [[Special:Tags|برچسب]] بر روی تغییرات یک نفر",
        "right-changetags": "افزودن یا حذف [[Special:Tags|برچسب]] قراردادی بر روی نسخه یا سیاهه ورودی‌ها",
+       "right-deletechangetags": "حذف [[Special:Tags|برچسب‌ها]] از پایگاه داده",
        "grant-generic": "\" $1 \" حقوق بسته",
        "grant-group-page-interaction": "تعامل با صفحات",
        "grant-group-file-interaction": "تعامل با رسانه",
        "action-viewmyprivateinfo": "اطلاعات خصوصی خود را ببینید",
        "action-editmyprivateinfo": "اطلاعات خصوصی خود را ویرایش کنید",
        "action-editcontentmodel": "ویرایش مدل محتوای یک صفحه",
-       "action-managechangetags": "ایجاد و حذف تگ‌ها از پایگاه داده",
+       "action-managechangetags": "ایجاد و (غیر)فعال‌سازی برچسب‌ها",
        "action-applychangetags": "اعمال برچسب بر روی تغییرات شما",
        "action-changetags": "افزودن یا حذف برچسب قراردادی بر روی نسخه یا سیاهه ورودی‌ها",
+       "action-deletechangetags": "حذف برچسب‌ها از پایگاه داده",
        "nchanges": "$1 تغییر",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|از آخرین بازدید}}",
        "enhancedrc-history": "تاریخچه",
        "recentchangeslinked-page": "نام صفحه:",
        "recentchangeslinked-to": "نمایش تغییرات صفحه‌هایی که به صفحهٔ داده‌شده پیوند دارند",
        "recentchanges-page-added-to-category": "[[:$1]] به رده اضافه شد",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] و [[Special:WhatLinksHere/$1|{{PLURAL:$2|یک صفحه|$2 صفحه}}]]دیگر به رده اضافه شدند",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] به رده افزوده شد، [[Special:WhatLinksHere/$1|این صفحه در صفحه‌های دیگر قرار گرفته است.]]",
        "recentchanges-page-removed-from-category": "[[:$1]] از رده حذف شد",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] و [[Special:WhatLinksHere/$1|{{PLURAL:$2|یک صفحه|$2 صفحه}}]] دیگر از رده حذف شدند",
        "autochange-username": "تغییرات خودکار مدیاویکی",
        "upload-form-label-own-work": "این کار خودم است",
        "upload-form-label-infoform-categories": "رده‌ها",
        "upload-form-label-infoform-date": "تاریخ",
-       "upload-form-label-own-work-message-local": "تائید می کنم که این پرونده را تحت مجوزها و سیاست‌های {{SITENAME}} بارگذاری می‌کنم.",
-       "upload-form-label-not-own-work-message-local": "اگر امکان بارگذاری پرونده تحت سیاست‌های {{SITENAME}} را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
-       "upload-form-label-not-own-work-local-local": "ممکن بخواهید از [[Special:Upload|پنجرهٔ بارگذاری پیش‌فرض]] استفاده کنید.",
-       "upload-form-label-own-work-message-default": "متوجهم که این پرونده را بر روی مخزن مشترک بارگذاری می‌کنم و تائید می‌کنم که تحت سیاست‌ها و مجوزهای آنجا عمل می‌کنم.",
-       "upload-form-label-not-own-work-message-default": "اگر امکان بارگذاری پرونده تحت سیاست‌ها و مجوزهای مخزن اشتراک‌گذاری را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
-       "upload-form-label-not-own-work-local-default": "در صورتی که نمی‌شود پرونده را تحت سیاست‌ها بارگذاری کنید ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
-       "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 سیاست نحوهٔ استفاده] هستم.",
-       "upload-form-label-not-own-work-message-shared": "اگر مالک حق تکثیر این پرونده نیستید یا قصد بارگذاری تحت مجوز دیگری دارید، از [https://commons.wikimedia.org/wiki/Special:UploadWizard جادوگر بارگذاری ویکی‌انبار] استفاده کنید.",
-       "upload-form-label-not-own-work-local-shared": "در صورتی که سایت امکان بارگذاری پرونده را تحت سیاست‌ها بارگذاری می‌دهد ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
+       "upload-form-label-own-work-message-generic-local": "تائید می کنم که این پرونده را تحت مجوزها و سیاست‌های {{SITENAME}} بارگذاری می‌کنم.",
+       "upload-form-label-not-own-work-message-generic-local": "اگر امکان بارگذاری پرونده تحت سیاست‌های {{SITENAME}} را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
+       "upload-form-label-not-own-work-local-generic-local": "ممکن بخواهید از [[Special:Upload|پنجرهٔ بارگذاری پیش‌فرض]] استفاده کنید.",
+       "upload-form-label-own-work-message-generic-foreign": "متوجهم که این پرونده را بر روی مخزن مشترک بارگذاری می‌کنم و تائید می‌کنم که تحت سیاست‌ها و مجوزهای آنجا عمل می‌کنم.",
+       "upload-form-label-not-own-work-message-generic-foreign": "اگر امکان بارگذاری پرونده تحت سیاست‌ها و مجوزهای مخزن اشتراک‌گذاری را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
+       "upload-form-label-not-own-work-local-generic-foreign": "در صورتی که نمی‌شود پرونده را تحت سیاست‌ها بارگذاری کنید ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
        "backend-fail-stream": "نمی‌توان پروندهٔ $1 را ارسال کرد.",
        "backend-fail-backup": "نمی‌توان نسخهٔ پشتیبان برای پروندهٔ $1 ایجاد کرد.",
        "backend-fail-notexists": "پروندهٔ $1 وجود ندارد.",
        "whatlinkshere-prev": "{{PLURAL:$1|قبلی|$1 مورد قبلی}}",
        "whatlinkshere-next": "{{PLURAL:$1|بعدی|$1 مورد بعدی}}",
        "whatlinkshere-links": "→ پیوندها",
-       "whatlinkshere-hideredirs": "$1 تغییرمسیر",
-       "whatlinkshere-hidetrans": "$1 تراگنجانش‌ها",
-       "whatlinkshere-hidelinks": "$1 پیوند",
+       "whatlinkshere-hideredirs": "پنهان‌کردن تغییرمسیرها",
+       "whatlinkshere-hidetrans": "پنهان‌کردن تراگنجانش‌ها",
+       "whatlinkshere-hidelinks": "پنهان‌کردن پیوندها",
        "whatlinkshere-hideimages": "$1 پیوندهای پرونده",
        "whatlinkshere-filters": "پالایه‌ها",
        "whatlinkshere-submit": "برو",
        "lockdbsuccesstext": "پایگاه داده قفل شد.\n<br />فراموش نکنید که پس از اتمام نگهداری قفل را بردارید.",
        "unlockdbsuccesstext": "پایگاه داده از قفل در آمد.",
        "lockfilenotwritable": "قفل پایگاه داده نوشتنی نیست. برای این که بتوانید پایگاه داده را قفل یا باز کنید، باید این پرونده نوشتنی باشد.",
+       "databaselocked": "در حال حاضر، پایگاه داده قفل است.",
        "databasenotlocked": "پایگاه داده قفل نیست.",
        "lockedbyandtime": "(به وسیلهٔ $1 در $2 ساعت $3)",
        "move-page": "انتقال $1",
        "tags-delete-not-found": "تگ «$1» وجود ندارد.",
        "tags-delete-too-many-uses": "برچسب \"$1\" در بیش از $2 نسخه اعمال شده است و نمی‌توان آن را حذف نمود.",
        "tags-delete-warnings-after-delete": "برچسب \"$1\" حذف شد، اما با {{PLURAL:$2|خطای|خطاهای}} زیر همراه بود:",
+       "tags-delete-no-permission": "شما اجازهٔ حذف برچسب‌های تغییر را ندارید.",
        "tags-activate-title": "فعال‌سازی برچسب",
        "tags-activate-question": "شما در حال فعال‌سازی تگ «$1» هستید.",
        "tags-activate-reason": "دلیل:",
        "logentry-protect-protect-cascade": "$1 $3 $4 {{GENDER:$2|محافظت کرد}} [آبشاری]",
        "logentry-protect-modify": "$1 سطح محافظت $3 را {{GENDER:$2|تغییر داد}} $4",
        "logentry-protect-modify-cascade": "$1 سطح حفاظت برای $3 $4 را {{GENDER:$2|تغییر داد}}[آبشاری]",
-       "logentry-rights-rights": "$1 Ø¹Ø¶Ù\88Û\8cت $3 Ø±Ø§ Ø§Ø² Ú¯Ø±Ù\88Ù\87 $4 Ø¨Ù\87 $5 {{GENDER:$2|تغÛ\8cÛ\8cر Ø¯Ø§Ø¯}}",
+       "logentry-rights-rights": "$1 Ø¯Ø³ØªØ±Ø³Û\8c $3 Ø±Ø§ Ø§Ø² Ú¯Ø±Ù\88Ù\87 $4 Ø¨Ù\87 $5 ØªØºÛ\8cÛ\8cر Ø¯Ø§Ø¯",
        "logentry-rights-rights-legacy": "$1 گروه عضویت $3 را {{GENDER:$2|تغییر داد}}",
        "logentry-rights-autopromote": "$1 به طور خودکار از $4 به $5 {{GENDER:$2|ارتقاء داد}}",
        "logentry-upload-upload": "$1 $3 را {{GENDER:$2|بارگذاری کرد}}",
        "feedback-useragent": "رابط کاربر:",
        "searchsuggest-search": "جستجو",
        "searchsuggest-containing": "صفحه‌های دربردارنده...",
+       "api-error-autoblocked": "نشانی آی‌پی شما به صورت خودکار بسته شده‌است، چون توسط یک کاربر بسته‌شده استفاده می‌شد.",
        "api-error-badaccess-groups": "شما اجازهٔ بارگذاری پرونده‌ها را در این ویکی ندارید.",
        "api-error-badtoken": "خطای داخلی: کد امنیتی اشتباه (Bad token).",
        "api-error-blocked": "شما از ویرایش بسته شده‌اید.",
        "log-action-filter-managetags-delete": "حذف کردن تگ",
        "log-action-filter-managetags-activate": "فعالسازی تگ",
        "log-action-filter-managetags-deactivate": "تغییر تگ",
+       "log-action-filter-move-move": "انتقال بدون بازنویسی تغییر مسیرها",
+       "log-action-filter-move-move_redir": "انتقال با بازنویسی تغییر مسیرها",
        "log-action-filter-newusers-create": "ایجاد شده توسط کاربر ناشناس",
        "log-action-filter-newusers-create2": "ایجاد شده توسط کاربر ثبت نام شده",
        "log-action-filter-newusers-autocreate": "ایجاد خودکار",
index 89834e6..7c6a371 100644 (file)
        "noname": "Et ole määritellyt kelvollista käyttäjänimeä.",
        "loginsuccesstitle": "Olet kirjautunut sisään",
        "loginsuccess": "'''Olet kirjautunut sivustolle {{SITENAME}} käyttäjänä $1.'''",
-       "nosuchuser": "Käyttäjää ”$1” ei ole olemassa. Nimet ovat kirjainkoosta riippuvaisia. Tarkista kirjoititko nimen oikein, tai [[Special:UserLogin/signup|luo uusi käyttäjätunnus]].",
+       "nosuchuser": "Käyttäjää ”$1” ei ole olemassa. Nimet ovat kirjainkoosta riippuvaisia. Tarkista kirjoititko nimen oikein, tai [[Special:CreateAccount|luo uusi käyttäjätunnus]].",
        "nosuchusershort": "Käyttäjää nimeltä ”$1” ei ole. Kirjoititko nimen oikein?",
        "nouserspecified": "Käyttäjätunnusta ei ole määritelty.",
        "login-userblocked": "Tämä käyttäjä on estetty. Kirjautuminen ei ole sallittua.",
        "accmailtext": "Satunnaisesti generoitu salasana käyttäjälle [[User talk:$1|$1]] on lähetetty osoitteeseen $2. Sen voi vaihtaa kirjautumisen jälkeen [[Special:ChangePassword|asetussivulla]].",
        "newarticle": "(Uusi)",
        "newarticletext": "Linkki toi sivulle, jota ei vielä ole.\nVoit luoda sivun kirjoittamalla alla olevaan kenttään (katso [$1 ohjesivulta] lisätietoja).\nJos et halua luoda sivua, käytä selaimen paluutoimintoa.",
-       "anontalkpagetext": "----''Tämä on nimettömän käyttäjän keskustelusivu. Hän ei ole joko luonut itselleen käyttäjätunnusta tai ei käytä sitä. Siksi hänet tunnistetaan nyt numeerisella IP-osoitteella. Kyseinen IP-osoite voi olla useamman henkilön käytössä. Jos olet nimetön käyttäjä, ja sinusta tuntuu, että aiheettomia kommentteja on ohjattu sinulle, [[Special:UserLogin/signup|luo itsellesi käyttäjätunnus]] tai [[Special:UserLogin|kirjaudu sisään]] välttääksesi jatkossa sekaannukset muiden nimettömien käyttäjien kanssa.''",
+       "anontalkpagetext": "----''Tämä on nimettömän käyttäjän keskustelusivu. Hän ei ole joko luonut itselleen käyttäjätunnusta tai ei käytä sitä. Siksi hänet tunnistetaan nyt numeerisella IP-osoitteella. Kyseinen IP-osoite voi olla useamman henkilön käytössä. Jos olet nimetön käyttäjä, ja sinusta tuntuu, että aiheettomia kommentteja on ohjattu sinulle, [[Special:CreateAccount|luo itsellesi käyttäjätunnus]] tai [[Special:UserLogin|kirjaudu sisään]] välttääksesi jatkossa sekaannukset muiden nimettömien käyttäjien kanssa.''",
        "noarticletext": "Tällä hetkellä tällä sivulla ei ole tekstiä.\nVoit [[Special:Search/{{PAGENAME}}|etsiä sivun nimellä]] muilta sivuilta,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheeseen liittyviä lokeja]\ntai [{{fullurl:{{FULLPAGENAME}}|action=edit}} luoda tämän sivun]</span>.",
        "noarticletext-nopermission": "Tällä hetkellä tällä sivulla ei ole tekstiä.\nVoit [[Special:Search/{{PAGENAME}}|etsiä sivun nimellä]] muilta sivuilta tai <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheeseen liittyviä lokeja]</span>, mutta sinulla ei ole oikeutta luoda tätä sivua.",
        "missing-revision": "Sivusta \"{{FULLPAGENAME}}\" ei ole olemassa versiota $1.\n\nYleensä tämä johtuu vanhentuneesta historialinkistä sivulle, joka on poistettu.\nTarkempia tietoja löytyy [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} poistolokista].",
        "right-override-export-depth": "Viedä sivuja sisältäen viitatut sivut viiden syvyydellä",
        "right-sendemail": "Lähettää sähköpostia muille käyttäjille",
        "right-passwordreset": "Tarkastella salasanan alustusviestejä",
-       "right-managechangetags": "Luoda ja poistaa [[Special:Tags|merkkauksia]] tietokannasta",
+       "right-managechangetags": "Luoda ja ottaa käyttöön [[Special:Tags|merkkauksia]]",
        "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ä",
+       "right-deletechangetags": "Poistaa [[Special:Tags|merkkauksia]] tietokannasta",
        "grant-generic": "\"$1\" oikeuksien joukko",
        "grant-group-page-interaction": "Ole vuorovaikutuksessa sivujen kanssa",
        "grant-group-file-interaction": "Ole vuorovaikutuksessa mediatiedostojen kanssa",
        "action-viewmyprivateinfo": "katsoa omia yksityisiä tietojasi",
        "action-editmyprivateinfo": "muokata omia yksityisiä tietojasi",
        "action-editcontentmodel": "muokata sivun sisältömallia",
-       "action-managechangetags": "luoda ja poistaa merkkauksia tietokannasta",
+       "action-managechangetags": "luoda ja ottaa käyttöön merkkauksia",
        "action-applychangetags": "käyttää merkkauksia muutostesi yhteydessä",
        "action-changetags": "lisätä ja poistaa satunnaisia merkkauksia yksittäisissä sivuversioissa ja lokimerkinnöissä",
+       "action-deletechangetags": "poistaa merkkauksia tietokannasta",
        "nchanges": "$1 {{PLURAL:$1|muutos|muutosta}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|viimeisen käynnin jälkeen}}",
        "enhancedrc-history": "historia",
        "upload-form-label-own-work": "Tämä on oma työni",
        "upload-form-label-infoform-categories": "Luokat",
        "upload-form-label-infoform-date": "Päivämäärä",
-       "upload-form-label-own-work-message-local": "Vakuutan, että tallennan tämän tiedoston noudattaen sivustolla {{SITENAME}} voimassa olevia käyttöehtoja sekä lisenssejä koskevia käytäntöjä.",
-       "upload-form-label-not-own-work-message-local": "Jos et kykene tallentamaan tätä tiedostoa noudattaen niitä käytäntöjä, jotka ovat voimassa sivustolla {{SITENAME}}, sulje tämä dialogi ja kokeile jotain toista menetelmää.",
-       "upload-form-label-not-own-work-local-local": "Voit myös kokeilla [[Special:Upload|yleistä tallentamista]].",
-       "upload-form-label-own-work-message-default": "Ymmärrän, että olen tallentamassa tätä tiedostoa yhteiseen mediasäilöön. Vakuutan, että teen tämän noudattaen asiaankuuluvia käyttöehtoja ja lisenssejä koskevia käytäntöjä.",
-       "upload-form-label-not-own-work-message-default": "Jos et kykene tallentamaan tätä tiedostoa noudattaen niitä käytäntöjä, jotka ovat voimassa yhteisessä mediasäilössä, sulje tämä dialogi ja kokeile jotain toista menetelmää.",
-       "upload-form-label-not-own-work-local-default": "Voit myös kokeilla [[Special:Upload|tallennussivua sivustolla {{SITENAME}}]]. Saattaa olla, että tämän tiedoston tallentaminen sinne on mahdollista siellä voimassa olevien käytäntöjen mukaisesti.",
-       "upload-form-label-own-work-message-shared": "Vakuutan, että minä omistan tämän tiedoston tekijänoikeudet, ja sitoudun siihen, että luovutan peruuttamattomasti tämän tiedoston kohteeseen Wikimedia Commons niillä ehdoilla, jotka liittyvät lisenssiin [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0]. Sitoudun myös noudattamaan [https://wikimediafoundation.org/wiki/Terms_of_Use käyttöehtoja].",
-       "upload-form-label-not-own-work-message-shared": "Jos sinulla ei ole tähän tiedostoon tekijänoikeutta tai jos haluat luovuttaa tiedoston käyttäen jotain toista lisenssiä, voit käyttää erityistä [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard] -toimintoa.",
-       "upload-form-label-not-own-work-local-shared": "Voit myös kokeilla [[Special:Upload|tallennussivua sivustolla {{SITENAME}}]]. Saattaa olla, että sivusto antaa tallentaa tämän tiedoston sinne siellä voimassa olevien käytäntöjen mukaisesti.",
+       "upload-form-label-own-work-message-generic-local": "Vakuutan, että tallennan tämän tiedoston noudattaen sivustolla {{SITENAME}} voimassa olevia käyttöehtoja sekä lisenssejä koskevia käytäntöjä.",
+       "upload-form-label-not-own-work-message-generic-local": "Jos et kykene tallentamaan tätä tiedostoa noudattaen niitä käytäntöjä, jotka ovat voimassa sivustolla {{SITENAME}}, sulje tämä dialogi ja kokeile jotain toista menetelmää.",
+       "upload-form-label-not-own-work-local-generic-local": "Voit myös kokeilla [[Special:Upload|yleistä tallentamista]].",
+       "upload-form-label-own-work-message-generic-foreign": "Ymmärrän, että olen tallentamassa tätä tiedostoa yhteiseen mediasäilöön. Vakuutan, että teen tämän noudattaen asiaankuuluvia käyttöehtoja ja lisenssejä koskevia käytäntöjä.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Jos et kykene tallentamaan tätä tiedostoa noudattaen niitä käytäntöjä, jotka ovat voimassa yhteisessä mediasäilössä, sulje tämä dialogi ja kokeile jotain toista menetelmää.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Voit myös kokeilla [[Special:Upload|tallennussivua sivustolla {{SITENAME}}]]. Saattaa olla, että tämän tiedoston tallentaminen sinne on mahdollista siellä voimassa olevien käytäntöjen mukaisesti.",
        "backend-fail-stream": "Tiedoston $1 virtauttaminen epäonnistui.",
        "backend-fail-backup": "Tiedostoa $1 ei voitu varmuuskopioida.",
        "backend-fail-notexists": "Tiedostoa $1 ei ole olemassa.",
        "changecontentmodel-success-text": "Sisältötyyppiä kohteessa [[:$1]] on muutettu.",
        "changecontentmodel-cannot-convert": "Sisältöä sivulla [[:$1]] ei voida muuntaa tyypiksi $2.",
        "changecontentmodel-nodirectediting": "Sisältömalli $1 ei tue suoraa muokkaamista",
+       "changecontentmodel-emptymodels-title": "Mitään sisältömallia ei ole saatavilla",
+       "changecontentmodel-emptymodels-text": "Sisältöä sivulla [[:$1]] ei voida muuntaa mihinkään muotoon.",
        "log-name-contentmodel": "Sisältömallin muutosloki",
        "log-description-contentmodel": "Tapahtumat, jotka liittyvät sivun sisältömalleihin",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|loi}} sivun $3 käyttäen normaalista poikkeavaa sisältömallia \"$5\"",
        "whatlinkshere-prev": "← {{PLURAL:$1|edellinen sivu|$1 edellistä sivua}}",
        "whatlinkshere-next": "{{PLURAL:$1|seuraava sivu|$1 seuraavaa sivua}} →",
        "whatlinkshere-links": "viittaukset",
-       "whatlinkshere-hideredirs": "$1 ohjaukset",
-       "whatlinkshere-hidetrans": "$1 sisällytykset",
-       "whatlinkshere-hidelinks": "$1 linkit",
-       "whatlinkshere-hideimages": "$1 tiedostolinkit",
+       "whatlinkshere-hideredirs": "Piilota ohjaukset",
+       "whatlinkshere-hidetrans": "Piilota sisällytykset",
+       "whatlinkshere-hidelinks": "Piilota linkit",
+       "whatlinkshere-hideimages": "Piilota tiedostolinkit",
        "whatlinkshere-filters": "Suotimet",
        "whatlinkshere-submit": "Siirry",
        "autoblockid": "Automaattinen esto #$1",
        "lockdbsuccesstext": "Tietokanta on lukittu.<br />\nMuista [[Special:UnlockDB|poistaa tietokannan lukitus]] kun huolto on tehty.",
        "unlockdbsuccesstext": "Tietokannan lukitus on poistettu.",
        "lockfilenotwritable": "Tietokannan lukitustiedostoa ei voi kirjoittaa. Tarkista oikeudet.",
+       "databaselocked": "Tietokanta on jo lukittu.",
        "databasenotlocked": "Tietokantaa ei ole lukittu.",
        "lockedbyandtime": "(lukinnut {{GENDER:$1|$1}} $2 kello $3)",
        "move-page": "Siirrä $1",
        "tags-delete-not-found": "Merkkausta \"$1\" ei ole olemassa.",
        "tags-delete-too-many-uses": "Tämä merkkaus \"$1\" on käytössä useammassa kuin $2 sivuversiossa, joten sitä ei voi poistaa.",
        "tags-delete-warnings-after-delete": "Merkkaus \"$1\" poistettiin, mutta toimenpide sai aikaan {{PLURAL:$2|seuraavan varoituksen|seuraavat varoitukset}}:",
+       "tags-delete-no-permission": "Sinulla ei ole oikeutta poistaa muutoksien yhteydessä olevia merkkauksia.",
        "tags-activate-title": "Aktivoi merkkaus",
        "tags-activate-question": "Olet nyt aktivoimassa merkkausta \"$1\".",
        "tags-activate-reason": "Syy:",
        "feedback-useragent": "User agent:",
        "searchsuggest-search": "Hae",
        "searchsuggest-containing": "sisältää...",
+       "api-error-autoblocked": "Sinun IP-osoitteesi on estetty automaattisesti, koska sitä on käyttänyt estetty käyttäjätunnus.",
        "api-error-badaccess-groups": "Sinulla ei ole oikeutta tallentaa tiedostoja tähän wikiin.",
        "api-error-badtoken": "Sisäinen virhe: virheellinen tarkistussumma.",
+       "api-error-blocked": "Sinut on estetty muokkaamasta.",
        "api-error-copyuploaddisabled": "Tallentaminen URL-osoitteesta ei ole käytössä.",
        "api-error-duplicate": "Samansisältöisiä tiedostoja löytyi {{PLURAL:$1|yksi kappale|useampia kappaleita}}.",
        "api-error-duplicate-archive": "Sivustolla oli aiemmin {{PLURAL:$1|toinen samansisältöinen tiedosto|toisia samansisältöisiä tiedostoja}}, mutta {{PLURAL:$1|se|ne}} poistettiin.",
index d12e340..247ec5f 100644 (file)
        "noname": "Tú hevur ikki skrivað eitt gyldugt brúkaranavn.",
        "loginsuccesstitle": "Innritan væleydnað",
        "loginsuccess": "'''Tú hevur nú ritað inn í {{SITENAME}} sum \"$1\".'''",
-       "nosuchuser": "Eingin brúkari er við navninum \"$1\". \nBrúkaranøvn eru følsom fyri stórum og lítlum bókstavum.\nEftirkanna um tú hevur stavað rætt, ella [[Special:UserLogin/signup|stovna eina nýggja konto]].",
+       "nosuchuser": "Eingin brúkari er við navninum \"$1\". \nBrúkaranøvn eru følsom fyri stórum og lítlum bókstavum.\nEftirkanna um tú hevur stavað rætt, ella [[Special:CreateAccount|stovna eina nýggja konto]].",
        "nosuchusershort": "Eingin brúkari er við navninum \"$1\". Kanna stavseting.",
        "nouserspecified": "Tú mást skriva eitt brúkaranavn.",
        "login-userblocked": "Hesin brúkarin er blokkaður. Tað er ikki loyvt at logga á.",
        "accmailtext": "Eitt tilvildarliga valt loyniorð fyri brúkaran [[User talk:$1|$1]] er blivið  sent til $2.\nTað kann broytast á ''[[Special:ChangePassword|broyt loyniorð]]'' síðuni tá tú ritar inn.",
        "newarticle": "(Nýggj)",
        "newarticletext": "Tú ert komin eftir eini slóð til eina síðu, ið ikki er til enn. Skriva í kassan niðanfyri, um tú vilt byrja uppá hesa síðuna.\n(Sí [$1 hjálparsíðuna] um tú ynskir fleiri upplýsingar).\nErt tú komin higar av einum mistaki, kanst tú trýsta á '''aftur'''-knøttin á kagaranum.",
-       "anontalkpagetext": "----''Hetta er ein kjaksíða hjá einum dulnevndum brúkara, sum ikki hevur stovnað eina kontu enn, ella ikki brúkar hana. \nTí noyðast vit at brúka nummerisku IP-adressuna hjá honum ella henni.\nEin slík IP-adressa kann verða brúkt av fleiri brúkarum.\nErt tú ein dulnevndur brúkari, og meinar, at óviðkomandi viðmerkingar eru vendar til tín, so er best fyri teg at [[Special:UserLogin/signup|stovna eina kontu]] ella [[Special:UserLogin|rita inn]] fyri at sleppa undan samanblanding við aðrar dulnevndar brúkarar í framtíðini.''",
+       "anontalkpagetext": "----''Hetta er ein kjaksíða hjá einum dulnevndum brúkara, sum ikki hevur stovnað eina kontu enn, ella ikki brúkar hana. \nTí noyðast vit at brúka nummerisku IP-adressuna hjá honum ella henni.\nEin slík IP-adressa kann verða brúkt av fleiri brúkarum.\nErt tú ein dulnevndur brúkari, og meinar, at óviðkomandi viðmerkingar eru vendar til tín, so er best fyri teg at [[Special:CreateAccount|stovna eina kontu]] ella [[Special:UserLogin|rita inn]] fyri at sleppa undan samanblanding við aðrar dulnevndar brúkarar í framtíðini.''",
        "noarticletext": "Tað er í løtuni ongin tekstur á hesi síðu.\nTú kanst [[Special:Search/{{PAGENAME}}|leita eftir hesum síðu heitinum]] á øðrum síðum,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leita í líknandi loggum],\nella [{{fullurl:{{FULLPAGENAME}}|action=edit}} rætta hesa síðu]</span>.",
        "noarticletext-nopermission": "Tað er í løtuni ongin tekstur á hesi síðu.\nTú kanst [[Special:Search/{{PAGENAME}}|leita eftir hesum síðuheiti]] á øðrum síðum, \nella <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leita eftir viðkomandi loggum]</span>, men tú hevur ikki loyvi til at stovna hesa síðu.",
        "missing-revision": "Endurskoðan #$1 av síðuni við heitinum \"{{FULLPAGENAME}}\" er ikki til.\n\nHetta skyldast vanliga tað, at tú fylgir einari gamlari søguslóð til eina síðu, sum er blivin slettað. \nNærri frágreiðing kanst tú finna í [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} strikingar logginum].",
        "categories": "Bólkar",
        "categoriespagetext": "Fylgjandi {{PLURAL:$1|bólkur inniheldur|bólkar innihalda}} síður ella miðlar (media).\n[[Special:UnusedCategories|Ikki brúktir bólkar]] eru ikki vístar her.\nSí eisini [[Special:WantedCategories|ynsktir bólkar]].",
        "categoriesfrom": "Vís bólkar, byrja við:",
-       "special-categories-sort-count": "sortera eftir stødd",
-       "special-categories-sort-abc": "uppdeil í bókstavarøð",
        "deletedcontributions": "Slettaði brúkaraíkøst",
        "deletedcontributions-title": "Slettaði brúkaraíkøst",
        "sp-deletedcontributions-contribs": "íkøst",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|versjón|versjónir}}",
        "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|versjón|versjónir}} frá $2",
        "javascripttest": "Royndarkoyring av JavaScript",
-       "javascripttest-pagetext-noframework": "Henda síðan er løgd av til at koyra JavaScript royndir.",
-       "javascripttest-pagetext-skins": "Vel eina útsjónd at koyra royndirnar við:",
        "tooltip-pt-userpage": "Tín brúkarasíða",
        "tooltip-pt-anonuserpage": "Brúkarasíðan fyri IP adressuna, sum tú rættar frá",
        "tooltip-pt-mytalk": "Tín kjaksíða",
index 9788eb5..416ed94 100644 (file)
                        "Frigory",
                        "Lemondoge",
                        "Jdforrester",
-                       "Yasten"
+                       "Yasten",
+                       "Psychoslave"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "noname": "Vous n’avez pas saisi un nom d’utilisateur valide.",
        "loginsuccesstitle": "Connecté",
        "loginsuccess": "<strong>Vous êtes maintenant connecté{{GENDER:$1||e|(e)}} à {{SITENAME}} en tant que « $1 ».</strong>",
-       "nosuchuser": "L'utilisateur « $1 » n’existe pas.\nLes noms d’utilisateurs sont sensibles à la casse.\nVérifiez l’orthographe, ou [[Special:UserLogin/signup|créez un nouveau compte]].",
+       "nosuchuser": "L'utilisateur « $1 » n’existe pas.\nLes noms d’utilisateurs sont sensibles à la casse.\nVérifiez l’orthographe, ou [[Special:CreateAccount|créez un nouveau compte]].",
        "nosuchusershort": "Il n’y a pas de contributeur avec le nom « $1 ».\nVeuillez vérifier l’orthographe.",
        "nouserspecified": "Vous devez saisir un nom d’utilisateur.",
        "login-userblocked": "Cet utilisateur est bloqué. Connexion non autorisée.",
        "accmailtext": "Un mot de passe généré aléatoirement pour [[User talk:$1|$1]] a été envoyé à $2.\nIl peut être modifié sur la page ''[[Special:ChangePassword|Changement de mot de passe]]'' après connexion.",
        "newarticle": "(Nouveau)",
        "newarticletext": "Vous avez suivi un lien vers une page qui n’existe pas encore. \nAfin de créer cette page, entrez votre texte dans la boîte ci-après (vous pouvez consulter [$1 la page d’aide] pour plus d’informations). \nSi vous êtes arrivé{{GENDER:||e}} ici par erreur, cliquez sur le bouton '''retour''' de votre navigateur.",
-       "anontalkpagetext": "---- ''Vous êtes sur la page de discussion d'un utilisateur anonyme qui n'a pas encore créé de compte ou qui n'en utilise pas. Pour cette raison, nous devons utiliser son adresse IP pour l'identifier. Une adresse IP peut être partagée par plusieurs utilisateurs. Si vous êtes un{{GENDER:||e|}} utilisat{{GENDER:|eur|rice|eur}} anonyme et si vous constatez que des commentaires qui ne vous concernent pas vous ont été adressés, vous pouvez [[Special:UserLogin/signup|créer un compte]] ou [[Special:UserLogin|vous connecter]] afin d'éviter toute confusion future avec d'autres contributeurs anonymes.''",
+       "anontalkpagetext": "---- ''Vous êtes sur la page de discussion d'un utilisateur anonyme qui n'a pas encore créé de compte ou qui n'en utilise pas. Pour cette raison, nous devons utiliser son adresse IP pour l'identifier. Une adresse IP peut être partagée par plusieurs utilisateurs. Si vous êtes un{{GENDER:||e|}} utilisat{{GENDER:|eur|rice|eur}} anonyme et si vous constatez que des commentaires qui ne vous concernent pas vous ont été adressés, vous pouvez [[Special:CreateAccount|créer un compte]] ou [[Special:UserLogin|vous connecter]] afin d'éviter toute confusion future avec d'autres contributeurs anonymes.''",
        "noarticletext": "Il n’y a pour l’instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|lancer une recherche sur ce titre]] dans les autres pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les opérations liées]\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} créer cette page]</span>.",
        "noarticletext-nopermission": "Il n'y a pour l'instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|faire une recherche sur ce titre]] dans les autres pages,\nou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les journaux associés]</span>.",
        "missing-revision": "La révision n° $1 de la page intitulée « {{FULLPAGENAME}} » n'existe pas.\n\nCela survient en général en suivant un lien historique obsolète vers une page qui a été supprimée.\nVous pouvez trouver plus de détails dans le [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} journal des suppressions].",
        "right-override-export-depth": "Exporter les pages en incluant les pages liées jusqu'à une profondeur de 5 niveaux",
        "right-sendemail": "Envoyer un courriel aux autres utilisateurs",
        "right-passwordreset": "Voir les courriels de réinitialisation des mots de passe",
-       "right-managechangetags": "Créer et supprimer des [[Spécial:Balises|balises]] de la base de données",
+       "right-managechangetags": "Créer et (dés)activer des [[Special:Tags|balises]]",
        "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",
+       "right-deletechangetags": "Supprimer des [[Special:Tags|balises]] de la base de données",
        "grant-generic": "ensemble de droits « $1 »",
        "grant-group-page-interaction": "Interagir avec des pages",
        "grant-group-file-interaction": "Interagir avec des médias",
        "action-viewmyprivateinfo": "voir vos informations personnelles",
        "action-editmyprivateinfo": "modifier vos informations personnelles",
        "action-editcontentmodel": "modifier le modèle de contenu d’une page",
-       "action-managechangetags": "créer et supprimer des balises de la base de données",
+       "action-managechangetags": "créer et (dés)activer des balises",
        "action-applychangetags": "appliquer les balises avec vos modifications",
        "action-changetags": "ajouter et supprimer de façon arbitraire des balises sur des révisions individuelles et des entrées de journal",
+       "action-deletechangetags": "supprimer des balises de la base de données",
        "nchanges": "$1 modification{{PLURAL:$1||s}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|depuis la dernière visite}}",
        "enhancedrc-history": "historique",
        "upload-form-label-own-work": "Je suis l’auteur de cette œuvre",
        "upload-form-label-infoform-categories": "Catégories",
        "upload-form-label-infoform-date": "Date",
-       "upload-form-label-own-work-message-local": "Je confirme que je télécharge ce fichier suivant les conditions et les politiques de licence de {{SITENAME}}",
-       "upload-form-label-not-own-work-message-local": "Si vous ne pouvez pas télécharger ce fichier d’après les politiques de {{SITENAME}}, veuillez fermer cette boîte de dialogue et essayer une autre méthode.",
-       "upload-form-label-not-own-work-local-local": "Vous pouvez aussi essayer [[Special:Upload|la page de téléchargement par défaut]].",
-       "upload-form-label-own-work-message-default": "Je comprends que je téléverse ce fichier vers un dépôt partagé. Je confirme agir en accord avec les conditions d’utilisation et les règles relatives aux licences de celui-ci.",
-       "upload-form-label-not-own-work-message-default": "Si vous n’êtes pas en mesure de téléverser ce fichier de façon conforme aux règles de ce dépôt partagé, veuillez fermer cette boîte de dialogue et essayer une autre méthode.",
-       "upload-form-label-not-own-work-local-default": "Vous pouvez également essayer d’utiliser [[Special:Upload|la page de téléversement de {{SITENAME}}]], si les règles du site autorisent le téléversement du fichier.",
-       "upload-form-label-own-work-message-shared": "Je certifie être le détenteur des droits d’auteur sur ce fichier, j’accepte de publier ce fichier sur Wikimedia Commons en le plaçant irrévocablement sous licence [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] et j’accepte les [https://wikimediafoundation.org/wiki/Terms_of_Use conditions d’utilisation].",
-       "upload-form-label-not-own-work-message-shared": "Si vous n’êtes pas le détenteur des droits d’auteur sur ce fichier ou que vous voulez le publier sous une licence différente, vous pouvez utiliser l’[https://commons.wikimedia.org/wiki/Special:UploadWizard assistant d’import].",
-       "upload-form-label-not-own-work-local-shared": "Vous pouvez également essayer d’utiliser [[Special:Upload|la page de téléversement de {{SITENAME}}]], si les règles du site autorisent le téléversement du fichier.",
+       "upload-form-label-own-work-message-generic-local": "Je confirme que je télécharge ce fichier suivant les conditions et les politiques de licence de {{SITENAME}}",
+       "upload-form-label-not-own-work-message-generic-local": "Si vous ne pouvez pas télécharger ce fichier d’après les politiques de {{SITENAME}}, veuillez fermer cette boîte de dialogue et essayer une autre méthode.",
+       "upload-form-label-not-own-work-local-generic-local": "Vous pouvez aussi essayer [[Special:Upload|la page de téléchargement par défaut]].",
+       "upload-form-label-own-work-message-generic-foreign": "Je comprends que je téléverse ce fichier vers un dépôt partagé. Je confirme agir en accord avec les conditions d’utilisation et les règles relatives aux licences de celui-ci.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Si vous n’êtes pas en mesure de téléverser ce fichier de façon conforme aux règles de ce dépôt partagé, veuillez fermer cette boîte de dialogue et essayer une autre méthode.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Vous pouvez également essayer d’utiliser [[Special:Upload|la page de téléversement de {{SITENAME}}]], si les règles du site autorisent le téléversement du fichier.",
        "backend-fail-stream": "Impossible de lire le fichier $1.",
        "backend-fail-backup": "Impossible de sauvegarder le fichier $1.",
        "backend-fail-notexists": "Le fichier $1 n’existe pas.",
        "whatlinkshere-prev": "{{PLURAL:$1|précédente|$1 précédentes}}",
        "whatlinkshere-next": "{{PLURAL:$1|suivante|$1 suivantes}}",
        "whatlinkshere-links": "← liens",
-       "whatlinkshere-hideredirs": "$1 les redirections",
-       "whatlinkshere-hidetrans": "$1 les inclusions",
-       "whatlinkshere-hidelinks": "$1 les liens",
-       "whatlinkshere-hideimages": "$1 les liens vers le fichier",
+       "whatlinkshere-hideredirs": "Masquer les redirections",
+       "whatlinkshere-hidetrans": "Masquer les inclusions",
+       "whatlinkshere-hidelinks": "Masquer les liens",
+       "whatlinkshere-hideimages": "Masquer les liens vers le fichier",
        "whatlinkshere-filters": "Filtres",
        "whatlinkshere-submit": "Lister",
        "autoblockid": "Blocage automatique #$1",
        "timezone-local": "Local",
        "duplicate-defaultsort": "Attention : la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
        "duplicate-displaytitle": "<strong>Attention :</strong> Le titre d'affichage « $2 » remplace l'ancien titre d'affichage « $1 ».",
+       "restricted-displaytitle": "<strong>Avertissement :</strong> le titre d’affichae \"$1\" a été ignoré car il n'est pas équivalent au titre effectif de la page.",
        "invalid-indicator-name": "<strong>Erreur :</strong> L’attribut <code>name</code> des indicateurs d’état de la page ne doit pas être vide.",
        "version": "Version",
        "version-extensions": "Extensions installées",
        "tags-delete-not-found": "La balise « $1 » n’existe pas.",
        "tags-delete-too-many-uses": "La balise « $1 » est appliquée à plus de $2 {{PLURAL:$2|révision|révisions}}, ce qui signifie qu'elle ne peut pas être supprimée.",
        "tags-delete-warnings-after-delete": "La balise « $1 » a été supprimée, mais {{PLURAL:$2|l’avertissement suivant a|les avertissements suivants ont}} été rencontré{{PLURAL:$2||s}} :",
+       "tags-delete-no-permission": "Vous n’avez pas le droit de supprimer des balises de changement.",
        "tags-activate-title": "Activer la balise",
        "tags-activate-question": "Vous êtes sur le point d'activer la balise « $1 ».",
        "tags-activate-reason": "Motif :",
index e86bd46..32459c7 100644 (file)
        "noname": "Vos éd pas spècifiâ un nom d’utilisator justo.",
        "loginsuccesstitle": "Branchiê",
        "loginsuccess": "<strong>Ora vos éte branchiê{{GENDER:$1||e}} a {{SITENAME}} coment « $1 ».</strong>",
-       "nosuchuser": "Y at gins d’utilisator avouéc lo nom « $1 ».\nLos noms d’utilisator sont sensiblos a la câssa.\nSe vos plét, controlâd l’ortografia ou ben [[Special:UserLogin/signup|féte un comptio novél]].",
+       "nosuchuser": "Y at gins d’utilisator avouéc lo nom « $1 ».\nLos noms d’utilisator sont sensiblos a la câssa.\nSe vos plét, controlâd l’ortografia ou ben [[Special:CreateAccount|féte un comptio novél]].",
        "nosuchusershort": "Y at gins d’utilisator avouéc lo nom « $1 ».\nSe vos plét, controlâd l’ortografia.",
        "nouserspecified": "Vos dête spècifiar un nom d’utilisator.",
        "login-userblocked": "Cél utilisator est blocâ. Branchement pas ôtorisâ.",
        "accmailtext": "Un contresegno fêt a l’hasârd por [[User talk:$1|$1]] est étâ mandâ a $2. Pôt étre changiê dessus la pâge de <em>[[Special:ChangePassword|changement de contresegno]]</em> aprés branchement.",
        "newarticle": "(Novél)",
        "newarticletext": "Vos éd siuvu un lim de vers na pâge qu’ègziste p’oncor.\nPor fâre cela pâge, buchiéd voutron tèxto dedens la bouèta ce-desot (vêde la [$1 pâge d’éde] por més d’enformacions).\nSe vos éte arrevâ{{GENDER:||ye}} ique per fôta, cllicâd dessus lo boton <strong>Devant</strong> de voutron navegator.",
-       "anontalkpagetext": "----\n<em>O est la pâge de discussion d’un utilisator anonimo qu’at p’oncor fêt un comptio ou ben que nen emplèye pas.</em>\nPor cen, nos devens empleyér son adrèce IP numerica por o identifiar.\nUn’adrèce IP d’ense pôt étre partagiêe per un mouél d’utilisators.\nSe vos éte {{GENDER:|un utilisator|un’utilisatrice}} anonim{{GENDER:|o|a}} et pués se vos constatâd que de comentèros que vos regârdont pas vos sont étâs adrèciês, se vos plét [[Special:UserLogin/signup|féte un comptio]] ou ben [[Special:UserLogin|branchiéd-vos]] por èvitar tota confusion que vint avouéc d’ôtros utilisators anonimos.",
+       "anontalkpagetext": "----\n<em>O est la pâge de discussion d’un utilisator anonimo qu’at p’oncor fêt un comptio ou ben que nen emplèye pas.</em>\nPor cen, nos devens empleyér son adrèce IP numerica por o identifiar.\nUn’adrèce IP d’ense pôt étre partagiêe per un mouél d’utilisators.\nSe vos éte {{GENDER:|un utilisator|un’utilisatrice}} anonim{{GENDER:|o|a}} et pués se vos constatâd que de comentèros que vos regârdont pas vos sont étâs adrèciês, se vos plét [[Special:CreateAccount|féte un comptio]] ou ben [[Special:UserLogin|branchiéd-vos]] por èvitar tota confusion que vint avouéc d’ôtros utilisators anonimos.",
        "noarticletext": "Ora y at gins de tèxto dedens cela pâge.\nVos pouede [[Special:Search/{{PAGENAME}}|fâre na rechèrche sur cél titro]] dedens les ôtres pâges,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechèrchiér dedens los jornâls liyês]\nou ben [{{fullurl:{{FULLPAGENAME}}|action=edit}} fâre cela pâge]</span>.",
        "noarticletext-nopermission": "Ora y at gins de tèxto dedens cela pâge.\nVos pouede [[Special:Search/{{PAGENAME}}|fâre na rechèrche sur cél titro]] dedens les ôtres pâges ou ben <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechèrchiér dedens los jornâls liyês]</span>, mas vos éd pas la pèrmission de fâre cela pâge.",
        "missing-revision": "La vèrsion n° $1 de la pâge apelâye « {{FULLPAGENAME}} » ègziste pas.\n\nEn g·ènèrâl cen arreve en siuvent un lim dèpassâ d’un historico de vers na pâge qu’est étâye suprimâye.\nVos pouede trovar més de dètalys dessus lo [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} jornâl de les suprèssions].",
        "upload-form-label-own-work": "Su l’ôtor de cel’ôvra",
        "upload-form-label-infoform-categories": "Catègories",
        "upload-form-label-infoform-date": "Dâta",
-       "upload-form-label-own-work-message-local": "Confirmo que tèlèchârjo cél fichiér d’aprés les condicions d’usâjo et les politiques de licence de {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Se vos pouede pas tèlèchargiér cél fichiér d’aprés les politiques de {{SITENAME}}, se vos plét cllôde cela bouèta de dialogo et pués èprovâd un’ôtra mètoda.",
-       "upload-form-label-not-own-work-local-local": "Vos pouede asse-ben èprovar la [[Special:Upload|pâge de tèlèchargement per dèfôt]].",
-       "upload-form-label-own-work-message-default": "Compregno que tèlèchârjo cél fichiér vers un dèpôt partagiê. Confirmo qu’o fé d’aprés les condicions d’usâjo et les politiques de licence de ceti.",
-       "upload-form-label-not-own-work-message-default": "Se vos pouede pas tèlèchargiér cél fichiér d’aprés les politiques du dèpôt partagiê, se vos plét cllôde cela bouèta de dialogo et pués èprovâd un’ôtra mètoda.",
-       "upload-form-label-not-own-work-local-default": "Vos pouede asse-ben èprovar d’empleyér la [[Special:Upload|pâge de tèlèchargement de {{SITENAME}}]], se cél fichiér y pôt étre tèlèchargiê d’aprés lors politiques.",
-       "upload-form-label-own-work-message-shared": "Cèrtifio étre lo dètentior des drêts d’ôtor sur cél fichiér, et j’accèpto de publeyér cél fichiér dessus Wikimedia Commons en lo betent irrèvocâblament desot licence [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Atribucion - Partâjo dens les Mémes Condicions 4.0] et pués j’accèpto les [https://wikimediafoundation.org/wiki/Terms_of_Use condicions d’usâjo].",
-       "upload-form-label-not-own-work-message-shared": "Se vos éte pas lo dètentior des drêts d’ôtor sur cél fichiér ou ben que vos lo voléd publeyér desot na licence difèrenta, vos pouede empleyér l’[https://commons.wikimedia.org/wiki/Special:UploadWizard assistent de tèlèchargement de Commons].",
-       "upload-form-label-not-own-work-local-shared": "Vos pouede asse-ben èprovar d’empleyér la [[Special:Upload|pâge de tèlèchargement de {{SITENAME}}]], se cél fichiér y pôt étre tèlèchargiê d’aprés lors politiques.",
+       "upload-form-label-own-work-message-generic-local": "Confirmo que tèlèchârjo cél fichiér d’aprés les condicions d’usâjo et les politiques de licence de {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Se vos pouede pas tèlèchargiér cél fichiér d’aprés les politiques de {{SITENAME}}, se vos plét cllôde cela bouèta de dialogo et pués èprovâd un’ôtra mètoda.",
+       "upload-form-label-not-own-work-local-generic-local": "Vos pouede asse-ben èprovar la [[Special:Upload|pâge de tèlèchargement per dèfôt]].",
+       "upload-form-label-own-work-message-generic-foreign": "Compregno que tèlèchârjo cél fichiér vers un dèpôt partagiê. Confirmo qu’o fé d’aprés les condicions d’usâjo et les politiques de licence de ceti.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Se vos pouede pas tèlèchargiér cél fichiér d’aprés les politiques du dèpôt partagiê, se vos plét cllôde cela bouèta de dialogo et pués èprovâd un’ôtra mètoda.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Vos pouede asse-ben èprovar d’empleyér la [[Special:Upload|pâge de tèlèchargement de {{SITENAME}}]], se cél fichiér y pôt étre tèlèchargiê d’aprés lors politiques.",
        "backend-fail-stream": "Y at pas moyen de tramandar lo fichiér « $1 ».",
        "backend-fail-backup": "Y at pas moyen d’encartar lo fichiér « $1 ».",
        "backend-fail-notexists": "Lo fichiér $1 ègziste pas.",
index c52df0a..27ac2b2 100644 (file)
        "noname": "Dü skel en rochten brükernööm uundu.",
        "loginsuccesstitle": "Uunmeldin hää loket.",
        "loginsuccess": "'''Dü beest nü üs „$1“ bi {{SITENAME}} uunmeldet.'''",
-       "nosuchuser": "Di brükernööm „$1“ jaft at ei. Aachte üüb det skriiwwiis (an uk üüb grat- an letjskriiwang), an do [[Special:UserLogin/signup|melde di nei uun]].",
+       "nosuchuser": "Di brükernööm „$1“ jaft at ei. Aachte üüb det skriiwwiis (an uk üüb grat- an letjskriiwang), an do [[Special:CreateAccount|melde di nei uun]].",
        "nosuchusershort": "Diar as nään brüker mä di nööm \"$1\".\nHeest dü ham uk rocht skrewen?",
        "nouserspecified": "Dü skel en brükernööm uundu.",
        "login-userblocked": "Didiar brüker as speret wurden. Hi mut ham ei uunmelde.",
        "accmailtext": "En tufelag iinracht paaswurd för [[User talk:$1|$1]] as tu $2 ferschüürd wurden. Det koon üüb det spezial-sidj ''[[Special:ChangePassword|Paaswurd anre]]'' feranert wurd, wan dü uunmeldet beest.",
        "newarticle": "(Nei)",
        "newarticletext": "Dü beest en ferwisang tu en sidj fulagt, diar't noch ei jaft.\nAm det sidj iinturachten, skriiw dan tekst uun det fial för't bewerkin iin.\nÜüb det [$1 halepsidj] fanjst dü halep.\nWan dü ütj fersen heer beest, trak ianfach üüb di <strong>turag</strong>-knoop faan dan browser.",
-       "anontalkpagetext": "----''Üüb detheer sidj könst dü en ünbekäänden brüker en nooracht du. Det lääpt auer sin IP adres. IP adresen kön faan flook brükern brükt wurd. Wan dü mä detheer nooracht niks began könst, do as det ferlicht för hoker ööders mend weesen. Dü säärst niks widjer onernem. Wan dü en aanj [[Special:UserLogin/signup|brükerkonto iinrachst]] of di [[Special:UserLogin|uunmeldest]], komt sowat ei weder föör.",
+       "anontalkpagetext": "----''Üüb detheer sidj könst dü en ünbekäänden brüker en nooracht du. Det lääpt auer sin IP adres. IP adresen kön faan flook brükern brükt wurd. Wan dü mä detheer nooracht niks began könst, do as det ferlicht för hoker ööders mend weesen. Dü säärst niks widjer onernem. Wan dü en aanj [[Special:CreateAccount|brükerkonto iinrachst]] of di [[Special:UserLogin|uunmeldest]], komt sowat ei weder föör.",
        "noarticletext": "Üüb detdiar sidj stäänt noch niks.\nDü könst didiar tiitel üüb ööder sidjen [[Special:Search/{{PAGENAME}}|schük]],\n<span class=\"plainlinks\">uun [{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} logbuken schük] of detdiar sidj [{{fullurl:{{FULLPAGENAME}}|action=edit}} bewerke]</span>.",
        "noarticletext-nopermission": "Üüb detdiar sidj stäänt noch niks, oober dü mutst diar uk niks iinskriiw.\nDü könst diar üüb ööder sidjen efter [[Special:Search/{{PAGENAME}}|schük]] of a <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAME}}}} logbuken uunluke].</span>",
        "missing-revision": "Det werjuun #$1 faan det sidj \"{{FULLPAGENAME}}\" jaft at ei.\n\nDet komt diar miast faan, dat en ual ferwisang stregen wurden as.\nDü könst det uun't [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} logbuk faan stregen sidjen] efterlees.",
index 42da004..46ec76f 100644 (file)
        "noname": "No tu âs inserît un non utent valit.",
        "loginsuccesstitle": "Jentrât cun sucès",
        "loginsuccess": "Cumò tu sês jentrât te {{SITENAME}} sicu \"$1\".",
-       "nosuchuser": "Non è registrato alcun utente di nome \"$1\".\nI nomi utente sono sensibili alle maiuscole.\nVerificare il nome inserito o [[Special:UserLogin/signup|creare una nuova utenza]].",
+       "nosuchuser": "Non è registrato alcun utente di nome \"$1\".\nI nomi utente sono sensibili alle maiuscole.\nVerificare il nome inserito o [[Special:CreateAccount|creare una nuova utenza]].",
        "nosuchusershort": "Nol esist nissun utent cul non \"$1\". Controle di no vê sbaliât di scrivi.",
        "nouserspecified": "Tu scugnis specificâ un non utent.",
        "wrongpassword": "La peraule clâf zontade no je juste. Torne par plasê a provâ.",
        "categories": "Categoriis",
        "categoriespagetext": "Lis categoriis ca sot a àn dentri pagjinis o elements multimediâi.\nLis [[Special:UnusedCategories|categoriis no dopradis]] no son mostradis culì.\nCjale ancje lis [[Special:WantedCategories|categoriis desideradis]].",
        "categoriesfrom": "Mostre lis categoriis scomençant di:",
-       "special-categories-sort-count": "met in ordin par numar",
-       "special-categories-sort-abc": "met in ordin alfabetichementri",
        "deletedcontributions": "Contribûts dal utent eliminâts",
        "deletedcontributions-title": "Contribûts dal utent eliminâts",
        "sp-deletedcontributions-contribs": "contribûts",
index 41368ee..0146cb2 100644 (file)
        "noname": "Jo moatte in meidognamme opjaan.",
        "loginsuccesstitle": "Oanmelden slagge.",
        "loginsuccess": "<strong>Jo binne no oanmelden op de {{SITENAME}} as: \"$1.\"</strong>",
-       "nosuchuser": "Der is gjin meidogger \"$1\".\nKontrolearje de stavering, of [[Special:UserLogin/signup|meitsje in nije meidogger oan]].",
+       "nosuchuser": "Der is gjin meidogger \"$1\".\nKontrolearje de stavering, of [[Special:CreateAccount|meitsje in nije meidogger oan]].",
        "nosuchusershort": "Der is gjin meidogger mei de namme \"$1\". It is goed skreaun?",
        "nouserspecified": "Jo moatte in brûkersnamme opjaan.",
        "wrongpassword": "Meidochnamme en wachtwurd hearre net by elkoar. Besykje op 'e nij, of fier it wachtwurd twa kear yn en meitsje nije meidoggersynstellings.",
        "accmailtext": "It wachtwurd foar \"$1\" is ferstjoerd nei $2.",
        "newarticle": "(Nij)",
        "newarticletext": "Jo hawwe in keppeling folge nei in side dêr't noch gjin tekst op stiet.\nOm sels tekst te meistjsen kinne jo dy gewoan yntype in dit bewurkingsfjild\n([$1 Mear ynformaasje oer bewurkjen].)\nOars kinne jo tebek mei de tebek-knop fan jo blêder.",
-       "anontalkpagetext": "----''Dit is de oerlisside fan in ûnbekende meidogger; in meidogger dy't him/har net oanmeld hat. Om't der gjin namme bekend is, wurdt it ynternet-adres brûkt om oan te jaan wa. Mar faak is it sa dat sa'n adres net altyd troch deselde persoan brûkt wurdt. As jo it idee hawwe dat jo as ûnbekende meidogger opmerkings foar in oar krije, dan kinne jo jo [[Special:UserLogin/signup|registrearje]], of jo [[Special:UserLogin|oanmelde]]. Fan in oanmelde meidogger is it ynternet-adres net sichtber, en as oanmelde meidogger krije jo allinnich opmerkings dy't foar josels bedoeld binne.''",
+       "anontalkpagetext": "----''Dit is de oerlisside fan in ûnbekende meidogger; in meidogger dy't him/har net oanmeld hat. Om't der gjin namme bekend is, wurdt it ynternet-adres brûkt om oan te jaan wa. Mar faak is it sa dat sa'n adres net altyd troch deselde persoan brûkt wurdt. As jo it idee hawwe dat jo as ûnbekende meidogger opmerkings foar in oar krije, dan kinne jo jo [[Special:CreateAccount|registrearje]], of jo [[Special:UserLogin|oanmelde]]. Fan in oanmelde meidogger is it ynternet-adres net sichtber, en as oanmelde meidogger krije jo allinnich opmerkings dy't foar josels bedoeld binne.''",
        "noarticletext": "Der stjit noch gjin tekst op dizze side. Jo kinne\n[[Special:Search/{{PAGENAME}}|hjirboppe nei dy tekst sykje]], of [{{fullurl:{{FULLPAGENAME}}|action=edit}} de side skriuwe].",
        "userpage-userdoesnotexist": "Jo bewurkje in brûkersside fan in brûker dy't net bestiet (brûker \"<nowiki>$1</nowiki>\").\nKontrolearje oft jo dizze side wol oanmeitsje/bewurkje wolle.",
        "clearyourcache": "<strong>Opmerking:</strong> Nei it fêstlizzen kin it nedich wêze de oerslach fan dyn blêder te leegjen foardat de wizigings te sjen binne.\n* <strong>Firefox / Safari:</strong> Hâld <em>Shift</em> yntreaun wylst jo op <em>Dizze side fernije</em> klikke, of typ <em>Ctrl-F5</em> of <em>Ctrl-R</em> (<em>⌘-R</em> op in Mac)\n* <strong>Google Chrome:</strong> Typ <em>CTRL-Shift-R</em> (<em>⌘-Shift-R</em> op in Mac)\n* <strong>Internet Explorer:</strong> Hâld <em>Ctrl</em> yntreaun wylst jo <em>Vernieuwen'' klikke of typ <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Leegje jo cache yn <em>Extra → Voorkeuren</em>",
index 06cbd5c..fd2344e 100644 (file)
        "noname": "Níor thug tú ainm úsáideora bailí.",
        "loginsuccesstitle": "Logáladh isteach thú",
        "loginsuccess": "'''Tá tú logáilte isteach anois sa {{SITENAME}} mar \"<nowiki>$1</nowiki>\".'''",
-       "nosuchuser": "Níl aon úsáideoir ann leis an ainm \"$1\".\nTá ainmneacha úsáideoir cásíogair.\nCinntigh do litriú, nó [[Special:UserLogin/signup|bain úsáid as an foirm thíos]] chun cuntas úsáideora nua a chruthú.",
+       "nosuchuser": "Níl aon úsáideoir ann leis an ainm \"$1\".\nTá ainmneacha úsáideoir cásíogair.\nCinntigh do litriú, nó [[Special:CreateAccount|bain úsáid as an foirm thíos]] chun cuntas úsáideora nua a chruthú.",
        "nosuchusershort": "Níl aon úsáideoir ann leis an ainm \"$1\". Cinntigh do litriú.",
        "nouserspecified": "Caithfidh ainm úsáideoir a shonrú.",
        "login-userblocked": "Tá an t-úsáideoir seo faoi bhac. Níl cead aige/aici logáil isteach.",
index 1fcf778..d8f5d4d 100644 (file)
        "noname": "Cha do thagh thu ainm-cleachdaiche dligheach.",
        "loginsuccesstitle": "Rinn thu logadh a-steach",
        "loginsuccess": "<strong>Rinn thu logadh a-steach air {{SITENAME}} mar \"$1\".</strong>",
-       "nosuchuser": "Chan eil cleachdaiche ann air a bheil \"$1\".\nTha ainmean chleachdaichean mothaichail do litrichean mòra 's beaga.\nThoir sùil air an litreachadh no [[Special:UserLogin/signup|cruthaich cunntas ùr]].",
+       "nosuchuser": "Chan eil cleachdaiche ann air a bheil \"$1\".\nTha ainmean chleachdaichean mothaichail do litrichean mòra 's beaga.\nThoir sùil air an litreachadh no [[Special:CreateAccount|cruthaich cunntas ùr]].",
        "nosuchusershort": "Chan eil cleachdaiche ann leis an ainm \"$1\".\nCuir sùil air an litreachadh.",
        "nouserspecified": "Tha agad ri ainm-cleachdaiche a chur ann.",
        "login-userblocked": "Chaidh an cleachdaiche seo a chasgadh. Chan eil logadh a-steach ceadaichte dhaibh.",
        "accmailtext": "Chaidh facal-faire a chruthachadh air thuaiream airson [[User talk:$1|$1]] 's a chur gu $2.\n\nGabhaidh am facal-faire airson a' chunntais ùir seo atharrachadh air an fo <em>[[Special:ChangePassword|atharraich facal-faire]]</em> às dèidh dhan chleachdaiche logadh a-steach.",
        "newarticle": "(Ùr)",
        "newarticletext": "Lean thu ri ceangal gu duilleag nach eil ann fhathast.\nCuir teacs sa bhogsa gu h-ìosal gus an duilleag seo a chruthachadh (seall air [$1 duilleag na cobharach] airson barrachd fiosrachaidh).\nMura robh dùil agad ris an duilleag seo a ruigsinn, briog air a' phutan <strong>air ais</strong> 'nad bhrabhsair.",
-       "anontalkpagetext": "----\n<em>Seo an duilleag deasbaireachd aig cleachdaiche gun urra nach do chruthaich cunntas fhathast no nach eil 'ga chleachdadh.</em>\nFeumaidh sinn an àireamh IP aca a chleachdadh air sgàth sin.\nFaodadh grunn chleachdaichean seòladh IP mar a chleachdadh còmhla.\nMas e cleachdaiche gun urra a tha annad 's ma tha thu dhen bheachd nach eil na beachdan seo a' buntainn riut, nach [[Special:UserLogin/signup|clàraich thu]] no nach dèan thu [[Special:UserLogin|logadh a-steach]] gus bùrach mar seo a sheachnadh san àm ri teachd?",
+       "anontalkpagetext": "----\n<em>Seo an duilleag deasbaireachd aig cleachdaiche gun urra nach do chruthaich cunntas fhathast no nach eil 'ga chleachdadh.</em>\nFeumaidh sinn an àireamh IP aca a chleachdadh air sgàth sin.\nFaodadh grunn chleachdaichean seòladh IP mar a chleachdadh còmhla.\nMas e cleachdaiche gun urra a tha annad 's ma tha thu dhen bheachd nach eil na beachdan seo a' buntainn riut, nach [[Special:CreateAccount|clàraich thu]] no nach dèan thu [[Special:UserLogin|logadh a-steach]] gus bùrach mar seo a sheachnadh san àm ri teachd?",
        "noarticletext": "Chan eil teacsa sam bith anns an duilleag seo an-dràsta.\n'S urrainn dhut [[Special:Search/{{PAGENAME}}|an tiotal seo a lorg]] ann an duilleagan eile,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} na logaichean co-cheangailte a rannsachadh],\nno [{{fullurl:{{FULLPAGENAME}}|action=edit}} an duilleag seo a chruthachadh]</span>.",
        "noarticletext-nopermission": "Chan eil teacsa sam bith san duilleag seo an-dràsta.\n'S urrainn dhut [[Special:Search/{{PAGENAME}}|tiotal na duilleige seo a lorg]] ann an duilleagan eile, no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} na logaichean co-cheangailte a rannsachadh]</span> ach chan eil cead agad an duilleag seo a chruthachadh.",
        "missing-revision": "Chan eil mùthadh #$1 na duilleige \"{{FULLPAGENAME}}\" ann.\n\nMar is trice, tachraidh seo ma leanas tu ceangal san eachdraidh a tha fìor aosta 's a tha a' dol gu duilleag a chaidh a sguabadh às.\nGheibh thu mion-fhiosrachadh ann an [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} loga nan duilleagan a chaidh a sguabadh às].",
index 6499e8e..0fda562 100644 (file)
        "noname": "Non especificou un nome de usuario válido.",
        "loginsuccesstitle": "Conectado",
        "loginsuccess": "<strong>Accedeu ao sistema de {{SITENAME}} como \"$1\".</strong>",
-       "nosuchuser": "Non existe ningún usuario chamado \"$1\".\nOs nomes de usuario diferencian entre maiúsculas e minúsculas.\nVerifique o nome que inseriu ou [[Special:UserLogin/signup|cree unha nova conta]].",
+       "nosuchuser": "Non existe ningún usuario chamado \"$1\".\nOs nomes de usuario diferencian entre maiúsculas e minúsculas.\nVerifique o nome que inseriu ou [[Special:CreateAccount|cree unha nova conta]].",
        "nosuchusershort": "Non existe ningún usuario chamado \"$1\".\nVerifique o nome que inseriu.",
        "nouserspecified": "Cómpre especificar un nome de usuario.",
        "login-userblocked": "Este usuario está bloqueado. Acceso non autorizado.",
        "accmailtext": "Un contrasinal xerado ao chou para [[User talk:$1|$1]] foi enviado a $2. Pode modificarse na páxina de [[Special:ChangePassword|cambio de contrasinais]] tras acceder ao sistema.",
        "newarticle": "(Novo)",
        "newarticletext": "Seguiu unha ligazón a unha páxina que aínda non existe.\nPara crear a páxina, comece a escribir na caixa inferior (consulte a [$1 páxina de axuda] para obter máis información).\nSe chegou aquí por erro, simplemente prema no botón '''atrás''' do seu navegador.",
-       "anontalkpagetext": "----''Esta é a páxina de conversa dun usuario anónimo que aínda non creou unha conta ou que non a usa. Polo tanto, empregamos o enderezo IP para a súa identificación. Este enderezo IP pódeno compartir varios usuarios distintos. Se pensa que foron dirixidos contra a súa persoa comentarios inadecuados, por favor, [[Special:UserLogin/signup|cree unha conta]] ou [[Special:UserLogin|acceda ao sistema]] para evitar futuras confusións con outros usuarios anónimos.''",
+       "anontalkpagetext": "----''Esta é a páxina de conversa dun usuario anónimo que aínda non creou unha conta ou que non a usa. Polo tanto, empregamos o enderezo IP para a súa identificación. Este enderezo IP pódeno compartir varios usuarios distintos. Se pensa que foron dirixidos contra a súa persoa comentarios inadecuados, por favor, [[Special:CreateAccount|cree unha conta]] ou [[Special:UserLogin|acceda ao sistema]] para evitar futuras confusións con outros usuarios anónimos.''",
        "noarticletext": "Actualmente non hai ningún texto nesta páxina.\nPode [[Special:Search/{{PAGENAME}}|procurar polo título desta páxina]] noutras páxinas,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ollar os rexistros relacionados]\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear a páxina]</span>.",
        "noarticletext-nopermission": "Actualmente non hai ningún texto nesta páxina.\nPode [[Special:Search/{{PAGENAME}}|procurar polo título desta páxina]] noutras páxinas ou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ollar os rexistros relacionados]</span>, pero non ten os permisos necesarios para crear esta páxina.",
        "missing-revision": "A revisión nº$1 da páxina chamada \"{{FULLPAGENAME}}\" non existe.\n\nA miúdo, isto está provocado por seguir unha ligazón de historial obsoleta cara a unha páxina que foi borrada.\nO [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rexistro de borrados] contén máis detalles.",
        "right-override-export-depth": "Exportar páxinas incluíndo as páxinas ligadas ata unha profundidade de 5",
        "right-sendemail": "Enviar correos electrónicos a outros usuarios",
        "right-passwordreset": "Ver os correos electrónicos de restablecemento de contrasinais",
-       "right-managechangetags": "Crear e borrar [[Special:Tags|tags]] da base de datos",
+       "right-managechangetags": "Crear e (des)activar [[Special:Tags|tags]]",
        "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",
+       "right-deletechangetags": "Suprimir as [[Special:Tags|etiquetas]] da base de datos",
        "grant-generic": "conxunto de dereitos \"$1\"",
        "grant-group-page-interaction": "Interactuar con páxinas",
        "grant-group-file-interaction": "Interactuar con ficheiros multimedia",
        "action-viewmyprivateinfo": "ver a súa información privada",
        "action-editmyprivateinfo": "editar a súa información privada",
        "action-editcontentmodel": "editar o modelo de contido dunha páxina",
-       "action-managechangetags": "crear e borrar etiquetas da base de datos",
+       "action-managechangetags": "crear e (des)activar etiquetas",
        "action-applychangetags": "aplicar etiquetas xunto cos cambios",
        "action-changetags": "engadir e quitar etiquetas arbitrarias a revisións individuais e entradas do rexistro",
+       "action-deletechangetags": "borrar etiquetas da base de datos",
        "nchanges": "$1 {{PLURAL:$1|modificación|modificacións}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde a última visita}}",
        "enhancedrc-history": "historial",
        "upload-form-label-own-work": "Isto é o meu propio traballo",
        "upload-form-label-infoform-categories": "Categorías",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Confirmo que estou a cargar este ficheiro seguindo os termos de uso e políticas de licenza de {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Se non é capaz de cargar este ficheiro baixo as políticas de {{SITENAME}}, por favor peche este diálogo e intente outro método.",
-       "upload-form-label-not-own-work-local-local": "Quizais tamén queira probar [[Special:Upload|a páxina predeterminada de subidas]].",
-       "upload-form-label-own-work-message-default": "Comprendo que estou a cargar este ficheiro nun repositorio compartido. Confirmo que fago isto seguindo os termos de uso e políticas de licenza existentes alí.",
-       "upload-form-label-not-own-work-message-default": "Se non é capaz de cargar este ficheiro baixo as políticas do repositorio compartido, por favor peche este diálogo e intente outro método.",
-       "upload-form-label-not-own-work-local-default": "Tamén pode interesarlle usar [[Special:Upload|a páxina de carga en {{SITENAME}}]], se este ficheiro pode ser cargado alí baixo as súas políticas.",
-       "upload-form-label-own-work-message-shared": "Certifico que son o propietario dos dereitos de autor deste ficheiro, e que concordo a liberar irrevocablemente este ficheiro a Wikimedia Commons baixo a licenza [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], e que concordo cos [https://wikimediafoundation.org/wiki/Terms_of_Use Termos de uso].",
-       "upload-form-label-not-own-work-message-shared": "Se non posúe os dereitos de autor deste ficheiro, ou quere liberalo baixo unha licenza diferente, considere usar o [https://commons.wikimedia.org/wiki/Special:UploadWizard Asistente de subas de Commons].",
-       "upload-form-label-not-own-work-local-shared": "Tamén pode interesarlle usar [[Special:Upload|a páxina de carga en {{SITENAME}}]], se o sitio permite a suba deste ficheiro nas súas políticas.",
+       "upload-form-label-own-work-message-generic-local": "Confirmo que estou a cargar este ficheiro seguindo os termos de uso e políticas de licenza de {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Se non é capaz de cargar este ficheiro baixo as políticas de {{SITENAME}}, por favor peche este diálogo e intente outro método.",
+       "upload-form-label-not-own-work-local-generic-local": "Quizais tamén queira probar [[Special:Upload|a páxina predeterminada de subidas]].",
+       "upload-form-label-own-work-message-generic-foreign": "Comprendo que estou a cargar este ficheiro nun repositorio compartido. Confirmo que fago isto seguindo os termos de uso e políticas de licenza existentes alí.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Se non é capaz de cargar este ficheiro baixo as políticas do repositorio compartido, por favor peche este diálogo e intente outro método.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Tamén pode interesarlle usar [[Special:Upload|a páxina de carga en {{SITENAME}}]], se este ficheiro pode ser cargado alí baixo as súas políticas.",
        "backend-fail-stream": "Non se puido transmitir o ficheiro \"$1\".",
        "backend-fail-backup": "Non se puido facer unha copia de seguridade do ficheiro \"$1\".",
        "backend-fail-notexists": "O ficheiro \"$1\" non existe.",
        "whatlinkshere-prev": "{{PLURAL:$1|anterior|$1 anteriores}}",
        "whatlinkshere-next": "{{PLURAL:$1|seguinte|$1 seguintes}}",
        "whatlinkshere-links": "← ligazóns",
-       "whatlinkshere-hideredirs": "$1 as redireccións",
-       "whatlinkshere-hidetrans": "$1 as inclusións",
-       "whatlinkshere-hidelinks": "$1 as ligazóns",
-       "whatlinkshere-hideimages": "$1 as ligazóns ao ficheiro",
+       "whatlinkshere-hideredirs": "Ocultar as redireccións",
+       "whatlinkshere-hidetrans": "Ocultar as inclusións",
+       "whatlinkshere-hidelinks": "Ocultar as ligazóns",
+       "whatlinkshere-hideimages": "Ocultar as ligazóns ao ficheiro",
        "whatlinkshere-filters": "Filtros",
        "whatlinkshere-submit": "Ir",
        "autoblockid": "Bloqueo automático nº$1",
        "tags-delete-not-found": "A páxina \"$1\" non existe.",
        "tags-delete-too-many-uses": "A etiqueta \"$1\" aplícase a máis de $2 {{PLURAL:$2|revisión|revisións}}; isto significa que non se pode borrar.",
        "tags-delete-warnings-after-delete": "A etiqueta \"$1\" borrouse; con todo, {{PLURAL:$2|atopouse o seguinte aviso|atopáronse os seguintes avisos}}:",
+       "tags-delete-no-permission": "Non ten permisos para borrar etiquetas de cambio.",
        "tags-activate-title": "Activar unha etiqueta",
        "tags-activate-question": "Está a piques de activar a etiqueta\"$1\".",
        "tags-activate-reason": "Motivo:",
index 530a48c..d81df3d 100644 (file)
        "compareselectedversions": "वेंचिल्ल्या पुनर्नियाळांची तुळा करात",
        "editundo": "केल्लें परतावचें",
        "diff-multi-sameuser": "(ह्या वांगड्या सयत {{PLURAL:$1|केल्लें मदलें एक अवतरण दाखोवंक ना|केल्लें मदलें $1 अवतरण दाखोवंक ना}})",
-       "searchresults": "सà¥\8bदाà¤\9aà¥\87 à¤ªà¤°à¤¿à¤£à¤¾à¤®à¤¾à¤\82",
+       "searchresults": "सà¥\8bदाà¤\9aà¥\8b à¤¨à¤¿à¤\95ाल",
        "searchresults-title": "\"$1\" हाच्या सोदाचे परिणामां",
        "prevn": "आदलें{{PLURAL:$1|$1}}",
        "nextn": "दुसरें {{PLURAL:$1|$1}}",
index 1b3138d..06bfece 100644 (file)
        "noname": "Οὐ καθὠρισας ἔγκυρόν τι ὄνομα χρωμένου.",
        "loginsuccesstitle": "Ἐπιτυχῶς συνεδέθης",
        "loginsuccess": "'''συνδέδεσαι ἤδη τῷ {{SITENAME}} ὡς \"$1\".'''",
-       "nosuchuser": "Οὐκ ἐστὶ χρώμενος ὀνόματι \"$1\".\nΤὰ γράμματα τῶν ὀνομάτων χρωμένων διακρίνονται εἰς κεφαλαῖα καὶ μικρά.\nΣκόπει τὴν τῶν γραμμάτων ἀκριβείαν ἢ [[Special:UserLogin/signup|λογισμὸν νέον ποίει]].",
+       "nosuchuser": "Οὐκ ἐστὶ χρώμενος ὀνόματι \"$1\".\nΤὰ γράμματα τῶν ὀνομάτων χρωμένων διακρίνονται εἰς κεφαλαῖα καὶ μικρά.\nΣκόπει τὴν τῶν γραμμάτων ἀκριβείαν ἢ [[Special:CreateAccount|λογισμὸν νέον ποίει]].",
        "nosuchusershort": "Οὐκ ἐστὶ χρώμενος ἔχων τὸ ὄνομα \"$1\".\nἜλεγξον τὴν ὀρθογραφίαν σου.",
        "nouserspecified": "Ὄνομα χρωμένου καθοριστέον ὑποχρεωτικώς.",
        "wrongpassword": "Εἰσηγμένον σύνθημα ἐσφαλμένον. Πείρασον πάλιν.",
index 149b65e..55120ea 100644 (file)
        "noname": "Du muesch e Benutzername aagee.",
        "loginsuccesstitle": "Aamäldig erfolgrych",
        "loginsuccess": "'''Du bisch jetz als \"$1\" bi {{SITENAME}} aagmäldet.'''",
-       "nosuchuser": "Dr Benutzername \"$1\" git s nit.\n\nIberprief d Schrybwys, oder mäld Di as [[Special:UserLogin/signup|neje Benutzer aa]].",
+       "nosuchuser": "Dr Benutzername \"$1\" git s nit.\n\nIberprief d Schrybwys, oder mäld Di as [[Special:CreateAccount|neje Benutzer aa]].",
        "nosuchusershort": "S git kei Benutzername „$1“. Bitte iberprief d Schrybwys.",
        "nouserspecified": "Bitte gib e Benutzername yy.",
        "login-userblocked": "Dää Benutzer isch gsperrt. Aamäldig nit erlaubt.",
        "accmailtext": "E zuefällig generiert Passwort fir [[User talk:$1|$1]] isch an $2 gschickt wore.\n\nS Passwort fir des nej Benutzerkonto cha uf dr Spezialsyte „[[Special:ChangePassword|Passwort ändere]]“ gänderet wäre.",
        "newarticle": "(Nej)",
        "newarticletext": "Du bisch eme Link nogange zuen ere Syte, wu s nid git.\nZum die Syte aalege, chasch do in däm Chaschte unte aafange schrybe (lueg [$1 Hilfe] fir meh Informatione).\nWänn do nid hesch welle aane goh, no druck in Dyynem Browser uf '''Zruck'''.",
-       "anontalkpagetext": "----''Des isch e Diskussionssyte vun eme anonyme Benutzer, wu kei Zuegang aagleit het oder wu ne nit bruucht. Sälleweg mien mir di numerisch IP-Adräss bruuche zum ihn oder si z identifiziere. So ne IP-Adräss cha au vu mehrere Benutzer teilt wäre. Wenn Du ne anonyme Benutzer bisch un s Gfiel hesch, ass do irrelevanti Kommentar an di grichtet wäre, derno [[Special:UserLogin/signup|leg e Konto aa]] oder [[Special:UserLogin|mäld di aa]] zum in Zuekumft Verwirrige mit andere anonyme Benutzer z vermyyde.''",
+       "anontalkpagetext": "----''Des isch e Diskussionssyte vun eme anonyme Benutzer, wu kei Zuegang aagleit het oder wu ne nit bruucht. Sälleweg mien mir di numerisch IP-Adräss bruuche zum ihn oder si z identifiziere. So ne IP-Adräss cha au vu mehrere Benutzer teilt wäre. Wenn Du ne anonyme Benutzer bisch un s Gfiel hesch, ass do irrelevanti Kommentar an di grichtet wäre, derno [[Special:CreateAccount|leg e Konto aa]] oder [[Special:UserLogin|mäld di aa]] zum in Zuekumft Verwirrige mit andere anonyme Benutzer z vermyyde.''",
        "noarticletext": "Uf däre Syte het s no kei Täxt. Du chasch uf andere Syte [[Special:Search/{{PAGENAME}}|dä Yytrag sueche]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} dr Logbuechyytrag sueche, wo dezue ghert],\noder [{{fullurl:{{FULLPAGENAME}}|action=edit}} die Syte bearbeite]</span>.",
        "noarticletext-nopermission": "In däre Syte het s zur Zyt no kei Text.\nDu chasch dää Titel uf andre Syte [[Special:Search/{{PAGENAME}}|sueche]]\noder <span class=\"plainlinks\">in dr zuegherige [{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbiecher sueche].</span> Du derfsch aber die Syte nit aalege.",
        "missing-revision": "D Version $1 vu dr Syte mit Name „{{FULLPAGENAME}}“ git s nit.\n\nDää Fähler chunnt normalerwyys dur e veraltete Link zue dr Versionsgschicht vun ere Syte, wu in dr Zwischezyt glescht woren isch.\nEinzelheite chasch im [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Lesch-Logbuech] bschaue.",
        "upload-form-label-own-work": "Das han i sälber gmacht.",
        "upload-form-label-infoform-categories": "Kategorië",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "I bestätige, das i die Datei under de Nutzigsbedingigen und Lizänzrichtlinje vo {{SITENAME}} ufelade.",
-       "upload-form-label-not-own-work-message-local": "We du die Datei nid under de Nutzigsbedingigen und Lizänzrichtlinje vo {{SITENAME}} chasch ufelade, de schliess bitte dä Dialog u probier’s uf’nen anderi Art.",
-       "upload-form-label-not-own-work-local-local": "Du chasch es ou mit der [[Special:Upload|Standardsyte zum Ufelade]] probiere.",
-       "upload-form-label-own-work-message-default": "I bi mer bewusst, das i die Datei in es gmeinsams Repository ufelade. I bestätige, das mi derby a d Nutzigs- und Lizänzbedingige dört halte.",
-       "upload-form-label-not-own-work-message-default": "Bitte schliess dä Dialog und versuech’s mit eren andere Methode, falls du die Datei nid under de Bedingige vom gmeinsame Repository chasch ufelade.",
-       "upload-form-label-not-own-work-local-default": "Du chasch es ou mit der [[Special:Upload|Syte zum Ufeladen uf {{SITENAME}}]] probiere, falls du die Datei dört under denen irne Bedingige chasch ufelade.",
-       "upload-form-label-own-work-message-shared": "I bestätige, das ds Copyright vo dere Datei mir ghört. I stimmen unwiderruefflech zue, das die Datei uf Wikimedia Commons under der Lizänz [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] veröffentlecht wird. I bi mit de [https://wikimediafoundation.org/wiki/Terms_of_Use/de Nutzigsbedingigen] yverstande.",
-       "upload-form-label-not-own-work-message-shared": "Falls ds Copyright vo dere Datei nid dir ghört oder falls du sen under eren andere Lizänz wosch veröffentleche, de chönntsch der [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard] bruuche.",
-       "upload-form-label-not-own-work-local-shared": "Du chasch es ou mit der [[Special:Upload|Syte zum Ufeladen uf {{SITENAME}}]] probiere, falls dä Website ds Ufelade vo der Datei under syne Bedingige zuelat.",
+       "upload-form-label-own-work-message-generic-local": "I bestätige, das i die Datei under de Nutzigsbedingigen und Lizänzrichtlinje vo {{SITENAME}} ufelade.",
+       "upload-form-label-not-own-work-message-generic-local": "We du die Datei nid under de Nutzigsbedingigen und Lizänzrichtlinje vo {{SITENAME}} chasch ufelade, de schliess bitte dä Dialog u probier’s uf’nen anderi Art.",
+       "upload-form-label-not-own-work-local-generic-local": "Du chasch es ou mit der [[Special:Upload|Standardsyte zum Ufelade]] probiere.",
+       "upload-form-label-own-work-message-generic-foreign": "I bi mer bewusst, das i die Datei in es gmeinsams Repository ufelade. I bestätige, das mi derby a d Nutzigs- und Lizänzbedingige dört halte.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Bitte schliess dä Dialog und versuech’s mit eren andere Methode, falls du die Datei nid under de Bedingige vom gmeinsame Repository chasch ufelade.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Du chasch es ou mit der [[Special:Upload|Syte zum Ufeladen uf {{SITENAME}}]] probiere, falls du die Datei dört under denen irne Bedingige chasch ufelade.",
        "backend-fail-stream": "D Datei $1 het nit chenne ibertrait wäre.",
        "backend-fail-backup": "D Datei $1 het nit chenne gsicheret wäre.",
        "backend-fail-notexists": "D Datei $1 git s nit.",
index c478e9d..2e12a73 100644 (file)
        "noname": "તમે પ્રમાણભૂત સભ્યનામ જણાવેલ નથી.",
        "loginsuccesstitle": "પ્રવેશ સફળ",
        "loginsuccess": "'''તમે હવે {{SITENAME}}માં \"$1\" તરીકે પ્રવેશી ચુક્યા છો.'''",
-       "nosuchuser": "\"$1\" નામ ધરાવતો કોઇ સભ્ય અસ્તિત્વમાં નથી.\n\nસભ્યનામો અક્ષરસંવેદી (કેસ સેન્સિટીવ) હોય છે.\n\nકૃપા કરી સ્પેલીંગ/જોડણી ચકાસો અથવા [[Special:UserLogin/signup|નવું ખાતુ ખોલો]].",
+       "nosuchuser": "\"$1\" નામ ધરાવતો કોઇ સભ્ય અસ્તિત્વમાં નથી.\n\nસભ્યનામો અક્ષરસંવેદી (કેસ સેન્સિટીવ) હોય છે.\n\nકૃપા કરી સ્પેલીંગ/જોડણી ચકાસો અથવા [[Special:CreateAccount|નવું ખાતુ ખોલો]].",
        "nosuchusershort": "\"$1\" નામનો કોઇ સભ્ય નથી, તમારી જોડણી તપાસો.",
        "nouserspecified": "તમારે સભ્ય નામ દર્શાવવાની જરૂર છે.",
        "login-userblocked": "આ યુઝર પ્રતિબંધિત છે. પ્રવેશ વર્જીત.",
        "accmailtext": "[[User talk:$1|$1]] માટે રચેલ ગુપ્તસંજ્ઞા $2 ને મોકલાવી દેવાઇ છે. આ નવા ખાતાનીગુપ્તસંજ્ઞા પ્રવેશ કર્યા બાદ ''[[Special:ChangePassword|ગુપ્તસંજ્ઞા બદલો]]'' વાપરીને બદલી શકાશે.",
        "newarticle": "(નવીન)",
        "newarticletext": "<div style=\"background: #F9F9F9; margin-top: 1em; padding: 1em; border: 1px solid #ccc; border-right: 2px solid #ccc; border-bottom: 2px solid #ccc\"><center>'''આ વિકિ પ્રકલ્પ પર આ પ્રકારનો લેખ હાલમાં નથી'''</center>\n----\n* [[Image:Searchtool.svg|25px|alt=|link=]] '''[[{{ns:special}}:Search/{{PAGENAME}}|“{{PAGENAME}}”]]''' માટે શોધો.\n* [[Image:Nuvola apps fonts.png|25px|alt=|link=]] જે લેખોનું શીર્ષક આ પ્રત્યય સાથે શરુ થાય છે તેનો  [[{{ns:special}}:Prefixindex/{{FULLPAGENAME}}|ઉપસર્ગ]] જૂઓ.\n* [[Image:Nuvola apps ksig.png|25px|alt=|link=]] <span class=\"plainlinks\">'''[{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=edit}} આ શીર્ષકનું નવું પૃષ્ઠ બનાવો.]'''</span> \n* [[Image:WP-TranslationProject TwoFlags.svg|25px|alt=|link=]] ''વૈશ્વિક લેખ યોજના'' દ્વારા આ લેખને [//vs.aka-online.de/cgi-bin/globalwpsearch.pl?search={{PAGENAMEE}} અન્ય ભાષાઓમાં શોધો.]\n<div style=\"margin: 1em 2em 1em 3em; font-size: 90%;\">\nયોગદાનકર્તાઓ માટે:\n* જો આપ હાલમાં આ લેખ બનાવી રહ્યા છો તો પોતાના બ્રાઉઝરની <span class=\"plainlinks\">[{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=purge}} કૈશ ખાલી]</span> કરો, અથવા થોડી વધુ રાહ જૂઓ, પછી કામ આગળ વધારો.\n* કદાચ આ પાનું દૂર કરાયું છે, આ જોવા માટે કૃપા કરીને <span class=\"plainlinks\">[{{fullurl:Special:Log|type=delete&page={{FULLPAGENAMEE}}}} આ પાનાનો વિલોપન ઇતિહાસ]</span> જૂઓ.\n* આપ આ લેખ બનાવવા માગો છો તો ક્રુપા કરીને નીચે આપેલા ખાનામાં લખવાનું શરુ કરો.\n* જો આપ ભૂલમાં અહીં આવી ગયા છો તો આપના બ્રાઉઝરના બૅક બટન પર ક્લિક કરીને પરત ફરો.</div></div>",
-       "anontalkpagetext": "----\n<em>આ એક અજ્ઞાત સભ્યનું ચર્ચા પાનું છે જેમણે ક્યાં તો પોતાનું ખાતું ખોલ્યું નથી અથવા તો તેને વાપરતા નથી.</em>\nઆથી તેમને ઓળખવા માટે અમારે સાંખ્યિક IP સરનામાની મદદ લેવી પડી છે.\nઆવું IP સરનામું ઘણાં અન્ય સભ્યો પણ વાપરતા હોઇ શકે.\nજો તમે અજ્ઞાત સભ્ય હોવ અને માનતા હોવ કે અસંધિત ટિપ્પણીઓ તમારી તરફ વાળવામાં આવી છે, તો કૃપયા  [[Special:UserLogin/signup|create an account]] અથવા [[Special:UserLogin|log in]]નો ઉપયોગ કરશો જેથી તમને કોઈ અજ્ઞાત સભ્ય સમજવાની ભૂલ ભવિષ્યમાં ટાળી શકાય.",
+       "anontalkpagetext": "----\n<em>આ એક અજ્ઞાત સભ્યનું ચર્ચા પાનું છે જેમણે ક્યાં તો પોતાનું ખાતું ખોલ્યું નથી અથવા તો તેને વાપરતા નથી.</em>\nઆથી તેમને ઓળખવા માટે અમારે સાંખ્યિક IP સરનામાની મદદ લેવી પડી છે.\nઆવું IP સરનામું ઘણાં અન્ય સભ્યો પણ વાપરતા હોઇ શકે.\nજો તમે અજ્ઞાત સભ્ય હોવ અને માનતા હોવ કે અસંધિત ટિપ્પણીઓ તમારી તરફ વાળવામાં આવી છે, તો કૃપયા  [[Special:CreateAccount|create an account]] અથવા [[Special:UserLogin|log in]]નો ઉપયોગ કરશો જેથી તમને કોઈ અજ્ઞાત સભ્ય સમજવાની ભૂલ ભવિષ્યમાં ટાળી શકાય.",
        "noarticletext": "આ પાનામાં હાલમાં કોઇ માહિતિ નથી.\nતમે  [[Special:Search/{{PAGENAME}}|આ શબ્દ]] ધરાવતાં અન્ય લેખો શોધી શકો છો, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} સંલગ્ન માહિતિ પત્રકોમાં શોધી શકો છો],\nઅથવા  [{{fullurl:{{FULLPAGENAME}}|action=edit}} આ પાનામાં ફેરફાર કરી] માહિતિ ઉમેરવાનું શરૂ કરી શકો છો</span>.",
        "noarticletext-nopermission": "આ પાનામાં હાલમાં કોઇ માહિતિ નથી.\nતમે  [[Special:Search/{{PAGENAME}}|આ શબ્દ]] ધરાવતાં અન્ય લેખો શોધી શકો છો, અથવા <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} સંલગ્ન માહિતિ પત્રકોમાં શોધી શકો છો], પરંતુ તમને આ પાનું બનાવવાની મંજૂરી નથી.",
        "userpage-userdoesnotexist": "સભ્ય ખાતું \"<nowiki>$1</nowiki>\"ની નોંધણીનથી થઈ.\nશું તમે ખરેખર આ પાનાની રચના કે ફેરફાર કરવા માંગો છો",
index 679b35a..c1757c9 100644 (file)
        "noname": "Cha honree uss ennym ymmydeyr fondagh.",
        "loginsuccesstitle": "T'ou loggit stiagh",
        "loginsuccess": "'''T'ou loggit stiagh ayns {{SITENAME}} myr \"$1\".'''",
-       "nosuchuser": "Cha nel ymmydeyr ayn lesh yn ennym \"$1\".<br />\nTa case ny lettyryn ayns enmyn ymmydeyr dendeaysagh.<br />\nCur streean er dty lettraghey, ny [[Special:UserLogin/signup|croo coontys noa]].",
+       "nosuchuser": "Cha nel ymmydeyr ayn lesh yn ennym \"$1\".<br />\nTa case ny lettyryn ayns enmyn ymmydeyr dendeaysagh.<br />\nCur streean er dty lettraghey, ny [[Special:CreateAccount|croo coontys noa]].",
        "nosuchusershort": "Cha nel ymmydeyr ayn lesh yn ennym \"$1\".\nCur streean er dty lettraghey.",
        "nouserspecified": "Shegin diu ennym ymmydeyr y honraghey.",
        "login-userblocked": "Ta'n ymmydeyr shoh fo ghlass.  Cha lowit {{GENDER:|da|jee|da}} loggal stiagh.",
        "allpagessubmit": "Gow",
        "allpagesprefix": "Taishbyn ny duillagyn lesh roie-ockle:",
        "categories": "Ronnaghyn",
-       "special-categories-sort-count": "sorçhaghey rere coontey",
-       "special-categories-sort-abc": "sorçhaghey rere lettyr",
        "linksearch": "Ronsaghey kianglaghyn çheumooie",
        "linksearch-ok": "Ronsee",
        "linksearch-line": "Ta kiangley ayn veih $2 gys $1",
index 9f3e157..128b824 100644 (file)
        "noname": "汝還吂輸入一隻有效嘅用戶名。",
        "loginsuccesstitle": "登入成功",
        "loginsuccess": "汝今下以\"$1\"嘅身份在{{SITENAME}}登入。",
-       "nosuchuser": "尋毋到用戶 \"$1\"。\n用戶名稱係有大小寫區分嘅。\n檢查汝嘅拼寫,或者用下面嘅表格[[Special:UserLogin/signup|建立一隻新賬號]]。",
+       "nosuchuser": "尋毋到用戶 \"$1\"。\n用戶名稱係有大小寫區分嘅。\n檢查汝嘅拼寫,或者用下面嘅表格[[Special:CreateAccount|建立一隻新賬號]]。",
        "nosuchusershort": "無有喊做“$1”嘅用戶。請檢查汝輸入嘅文字係毋係有差錯。",
        "nouserspecified": "汝愛指定一隻用戶名。",
        "login-userblocked": "邇隻用戶已分封鎖。毋做得登入。",
index 65176b1..2de6b59 100644 (file)
        "createaccountreason": "Kumu:",
        "createacct-reason": "Kumu",
        "createacct-reason-ph": "No ke aha mai ke kāinoa nei i kekahi moʻokāki ʻē aʻe",
-       "createacct-captcha": "Hōʻoiana palekana",
-       "createacct-imgcaptcha-ph": "E kikokiko i ke kikokikona  i luna",
        "createacct-submit": "Kāinoa",
        "createacct-another-submit": "Kāinoa hou",
        "createacct-benefit-heading": "Hana ʻia ʻo {{SITENAME}} e nā kanaka e like me ʻoe.",
        "noname": "ʻAʻole hana ʻoe i kekahi inoa mea hoʻohana pono.",
        "loginsuccesstitle": "ʻEʻe kūleʻa",
        "loginsuccess": "<strong>Ua ʻeʻe ʻo \"$1\" iā {{SITENAME}}.</strong>",
-       "nosuchuser": "Loaʻa ʻole i ka inoa mea hoʻohana ʻo \"$1\".\nMakaʻala maʻaka ka inoa mea hoʻohana.\nHōʻoia i kāu kepela ʻana, aiʻole [[Special:UserLogin/signup|kāinoa i kekahi mōʻaukaki hou]].",
+       "nosuchuser": "Loaʻa ʻole i ka inoa mea hoʻohana ʻo \"$1\".\nMakaʻala maʻaka ka inoa mea hoʻohana.\nHōʻoia i kāu kepela ʻana, aiʻole [[Special:CreateAccount|kāinoa i kekahi mōʻaukaki hou]].",
        "nosuchusershort": "Loaʻa ʻole i ka Loaʻa ʻole i ka inoa mea hoʻohana ʻo \"$1\".\nHōʻoia i kāu kepela ʻana.",
        "nouserspecified": "Pono ʻoe e kāhuakomo i ka inoa mea hoʻohana.",
        "login-userblocked": "Pale ʻia kēia mea hoʻohana. ʻAʻole hiki ke ʻeʻe.",
        "resetpass-temp-password": "ʻŌlelo hūnā kūikawā:",
        "resetpass-expired": "Ua pau kāu ʻōlelo hūnā. E ʻoluʻolu, e loaʻa kahi ʻōlelo hūnā hou no ka ʻeʻe ʻana.",
        "passwordreset": "Kāinoa hou i ka ʻōlelo hūnā",
-       "passwordreset-legend": "Kāinoa hou i ka ʻōlelo hūnā",
        "passwordreset-username": "Inoa mea ho'ohana:",
        "passwordreset-email": "Wahinoho lekauila:",
        "changeemail": "Hoʻololi i ka wahinoho lekauila",
+       "changeemail-header": "Hoʻololi i ka wahinoho lekauila moʻokāki",
        "changeemail-oldemail": "Wahinoho lekauila hananei:",
        "changeemail-newemail": "Wahinoho lekauila hou:",
        "changeemail-none": "(ʻaʻohe)",
        "accmailtitle": "Ua ho‘ouna ‘ia ka ‘ōlelo hūnā",
        "newarticle": "(Hou)",
        "newarticletext": "Ua hāhai ʻoe i kekahi loulou i kekahi ʻaoʻao e haku ʻole.\nNo ka haku ʻana i ka ʻaoʻao, kikokiko i loko o ka pahu i lalo (ʻike i ka [$1 ʻaoʻao kōkua] no nā ʻike ʻē aʻe).\nInā hewa kou hele ʻana, kāomi i ka pihi <strong>hoʻi</strong>.",
-       "anontalkpagetext": "----\n<em>ʻO kēia ka ʻaoʻao kūkākūkā no kekahi mea ho‘ohana me ka inoa ʻole.</em>\nNo laila, pono mākou e ho‘ohana i ka IP no ka hōʻoia ʻana iā ia a hiki i kekahi mau mea hoʻohana ke hoʻokaʻana i kēia  IP.\nInā he mea ho‘ohana inoa ʻole ʻoe a loaʻa kekahi mau manaʻo nāuʻole, e ʻoluʻolu [[Special:UserLogin/signup|e kāinoa]] a i ʻole [[Special:UserLogin|e ʻeʻe]].''",
+       "anontalkpagetext": "----\n<em>ʻO kēia ka ʻaoʻao kūkākūkā no kekahi mea ho‘ohana me ka inoa ʻole.</em>\nNo laila, pono mākou e ho‘ohana i ka IP no ka hōʻoia ʻana iā ia a hiki i kekahi mau mea hoʻohana ke hoʻokaʻana i kēia  IP.\nInā he mea ho‘ohana inoa ʻole ʻoe a loaʻa kekahi mau manaʻo nāuʻole, e ʻoluʻolu [[Special:CreateAccount|e kāinoa]] a i ʻole [[Special:UserLogin|e ʻeʻe]].''",
        "noarticletext": "ʻAʻohe kikokikona a kēia ʻaoʻao.\nHiki iā ʻoe ke [[Special:Search/{{PAGENAME}}|huli no kēia inoa ʻaoʻao]] i nā ʻaoʻao ʻē aʻe, <span class=\"plainlinks\">[{{fullurl:SpecialLog|page={{FULLPAGENAMEE}}}} huli i nā moʻolelo pili], a i ʻole [{{fullurl:{{FULLPAGENAME}}|action=edit}} hoʻololi i kēia ʻaoʻao]</span>.",
        "noarticletext-nopermission": "ʻAʻohe kikokikona a kēia ʻaoʻao.\nHiki iā ʻoe ke [[Special:Search/{{PAGENAME}}|huli no kēia inoa ʻaoʻao]] i nā ʻaoʻao ʻē aʻe aiʻole <span class=\"plainlinks\">[{{fullurl:SpecialLog|page={{FULLPAGENAMEE}}}} huli i nā moʻolelo pili]</span>, akā hiki ʻole iā ʻoe ke hoʻololi i kēia ʻaoʻao.",
        "userpage-userdoesnotexist-view": "ʻAʻole kāinoa ʻia ka moʻokāki mea hoʻohana ʻo \"$1\".",
        "upload-description": "Hōʻike ʻano waihona",
        "upload-options": "Koho hoʻouka",
        "watchthisupload": "E kiaʻi i kēia waihona",
-       "upload-warning-subj": "Akahele hoʻouka",
        "upload-file-error": "Hewa kūloko",
        "upload-http-error": "Ua loaʻa kekahi hewa HTTP: $1",
        "license": "Laikini:",
        "blocklogentry": "ua hoʻopale ʻia ʻo [[$1]] no ka manawa o $2 $3",
        "block-log-flags-nocreate": "ua hoʻopale ʻia ke kāinoa moʻokāki ʻana",
        "move-page-legend": "Hoʻoneʻe i ka ʻaoʻao",
-       "movearticle": "E hoʻoneʻe i ka ʻaoʻao:",
        "newtitle": "I ka inoa hou:",
        "move-watch": "Kiaʻi i ka ʻaoʻao kumu a me ka ʻaoʻao māka",
        "movepagebtn": "Hoʻoneʻe i ka ʻaoʻao",
        "movelogpage": "Hoʻoneʻe i ka moʻolelo",
        "movereason": "Kumu:",
        "revertmove": "hoʻihoʻi",
-       "delete_and_move": "Holoi a hoʻoneʻe",
        "delete_and_move_confirm": "‘Ae, e holoi i ka ‘ao‘ao",
        "export": "Kāpuka ʻaoʻao",
        "export-addcat": "Ho‘ohui",
        "version-poweredby-translators": "nā mea unuhi ma translatewiki.net",
        "version-software-version": "Mana",
        "redirect": "Kiahou e ka waihona, ka mea hoʻohana, ka ʻaoʻao aiʻole ka ID loihape",
-       "redirect-legend": "Kiahou i kekahi waihona aiʻole kekahi ʻaoʻao",
        "redirect-summary": "Kiahou kēia ʻaoʻao kūikawā i kekahi waihona (inā hāʻawi ʻia kekahi inoa waihona), kekahi ʻaoʻao (inā hāʻawi ʻia kekahi ID loihape aiʻole ID ʻaoʻao) aiʻole kekahi ʻaoʻao mea hoʻohana (inā hāʻawi ʻia ka ID mea hoʻohana helu). Ka hana ʻana: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], aiʻole [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "Huli",
        "redirect-lookup": "Huli no:",
index 1abe8a9..9196f2d 100644 (file)
@@ -66,7 +66,7 @@
        "tog-fancysig": "התייחסות לחתימה כקוד ויקי (ללא קישור אוטומטי)",
        "tog-uselivepreview": "שימוש בתצוגה מקדימה מהירה",
        "tog-forceeditsummary": "הצגת אזהרה בעת הכנסת תקציר עריכה ריק",
-       "tog-watchlisthideown": "×\94סתרת ×\94ער×\99×\9b×\95ת ×©×\9c×\99 ×\91רש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchlisthideown": "הסתרת עריכות שלי ברשימת המעקב",
        "tog-watchlisthidebots": "הסתרת עריכות של בוטים ברשימת המעקב",
        "tog-watchlisthideminor": "הסתרת עריכות משניות ברשימת המעקב",
        "tog-watchlisthideliu": "הסתרת עריכות של משתמשים רשומים ברשימת המעקב",
        "mar": "מרץ",
        "apr": "אפר'",
        "may": "מאי",
-       "jun": "יונ'",
-       "jul": "יול'",
+       "jun": "יוני",
+       "jul": "יולי",
        "aug": "אוג'",
        "sep": "ספט'",
        "oct": "אוק'",
        "category_header": "דפים בקטגוריה \"$1\"",
        "subcategories": "קטגוריות משנה",
        "category-media-header": "קובצי מדיה בקטגוריה \"$1\"",
-       "category-empty": "'''קטגוריה זו אינה כוללת דפים או קובצי מדיה.'''",
+       "category-empty": "<strong>קטגוריה זו אינה כוללת דפים או קובצי מדיה.</strong>",
        "hidden-categories": "{{PLURAL:$1|קטגוריה מוסתרת|קטגוריות מוסתרות}}",
        "hidden-category-category": "קטגוריות מוסתרות",
        "category-subcat-count": "{{PLURAL:$2|קטגוריה זו כוללת את קטגוריית המשנה הבאה בלבד.|קטגוריה זו כוללת את {{PLURAL:$1|קטגוריית המשנה המוצגת להלן|$1 קטגוריות המשנה המוצגות להלן}}, וכוללת בסך־הכול $2 קטגוריות משנה.}}",
        "redirectedfrom": "(הופנה מהדף $1)",
        "redirectpagesub": "דף הפניה",
        "redirectto": "הפניה ל:",
-       "lastmodifiedat": "×\93×£ ×\96×\94 ×©×\81×\95Ö¼× ×\94 ×\9c×\90×\97ר×\95× ×\94 ×\91Ö¾$1, ×\91שע×\94 $2.",
+       "lastmodifiedat": "דף זה שוּנה לאחרונה ב־$1, בשעה $2.",
        "viewcount": "דף זה נצפה {{PLURAL:$1|פעם אחת|פעמיים|$1 פעמים}}.",
        "protectedpage": "דף מוגן",
        "jumpto": "קפיצה אל:",
        "perfcachedts": "המידע הבא הוא עותק שמור בזיכרון המטמון של המידע, שעודכן לאחרונה ב־$1. לכל היותר {{PLURAL:$4|תוצאה אחת נשמרת|$4 תוצאות נשמרות}} בזיכרון המטמון.",
        "querypage-no-updates": "העדכונים לדף זה כרגע מופסקים, והמידע לא יעודכן באופן שוטף.",
        "viewsource": "הצגת מקור",
-       "viewsource-title": "הצגת המקור של $1",
+       "viewsource-title": "הצגת המקור של הדף \"$1\"",
        "actionthrottled": "הפעולה הוגבלה",
        "actionthrottledtext": "כאמצעי נגד שימוש לרעה, קיימת מגבלה על ביצוע פעולה זו פעמים רבות מדי בזמן קצר, וחרגת מהמגבלה הזאת.\nנא לנסות שוב בעוד מספר דקות.",
        "protectedpagetext": "דף זה מוגן כדי למנוע עריכה ופעולות אחרות.",
        "noname": "לא הכנסת שם משתמש תקין",
        "loginsuccesstitle": "נכנסת לחשבון",
        "loginsuccess": "'''נכנסת ל{{grammar:תחילית|{{SITENAME}}}} בשם \"$1\".'''",
-       "nosuchuser": "אין משתמש בשם \"$1\".\nאנא ודאו שהאיות נכון (כולל אותיות רישיות וקטנות), או [[Special:UserLogin/signup|צרו חשבון חדש]].",
+       "nosuchuser": "אין משתמש בשם \"$1\".\nאנא ודאו שהאיות נכון (כולל אותיות רישיות וקטנות), או [[Special:CreateAccount|צרו חשבון חדש]].",
        "nosuchusershort": "אין משתמש בשם \"$1\".\nאנא ודאו שהאיות נכון.",
        "nouserspecified": "יש לציין שם משתמש.",
        "login-userblocked": "משתמש זה חסום. אינכם מורשים להיכנס לחשבון.",
        "subject": "נושא:",
        "minoredit": "זוהי עריכה משנית",
        "watchthis": "מעקב אחרי דף זה",
-       "savearticle": "ש×\9e×\99ר×\94",
+       "savearticle": "ש×\9e×\99רת ×\94×\93×£",
        "publishpage": "פרסום הדף",
        "preview": "תצוגה מקדימה",
        "showpreview": "תצוגה מקדימה",
        "accmailtext": "סיסמה אקראית עבור [[User talk:$1|$1]] נשלחה אל $2. ניתן לשנותה בדף '''[[Special:ChangePassword|שינוי הסיסמה]]''' לאחר הכניסה.",
        "newarticle": "(חדש)",
        "newarticletext": "הגעתם לדף שעדיין אינו קיים.\nכדי ליצור את הדף הזה, התחילו להקליד בתיבת הטקסט שלמטה (ראו את [$1 דף העזרה] למידע נוסף).\nאם הגעתם לכאן בטעות, לחצו על כפתור ה<strong>חזרה</strong> (Back) בדפדפן שלכם.",
-       "anontalkpagetext": "----\n<em>זהו דף שיחה של משתמש אנונימי שעדיין לא יצר חשבון במערכת, או שהוא לא משתמש בו.</em>\nלכן עלינו להשתמש בכתובת ה־IP המספרית כדי לזהותו.\nייתכן שכתובת IP זו תהיה משותפת למספר משתמשים.\nאם אתם משתמשים אנונימיים ומרגישים שקיבלתם הודעות בלתי רלוונטיות, אנא [[Special:UserLogin/signup|צרו חשבון]] או [[Special:UserLogin|היכנסו לחשבון]] כדי להימנע מבלבול עתידי עם משתמשים אנונימיים נוספים.",
+       "anontalkpagetext": "----\n<em>זהו דף שיחה של משתמש אנונימי שעדיין לא יצר חשבון במערכת, או שהוא לא משתמש בו.</em>\nלכן עלינו להשתמש בכתובת ה־IP המספרית כדי לזהותו.\nייתכן שכתובת IP זו תהיה משותפת למספר משתמשים.\nאם אתם משתמשים אנונימיים ומרגישים שקיבלתם הודעות בלתי רלוונטיות, אנא [[Special:CreateAccount|צרו חשבון]] או [[Special:UserLogin|היכנסו לחשבון]] כדי להימנע מבלבול עתידי עם משתמשים אנונימיים נוספים.",
        "noarticletext": "אין כרגע טקסט בדף הזה.\nבאפשרותך [[Special:Search/{{PAGENAME}}|לחפש את כותרת הדף]] בדפים אחרים,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} לחפש את הדף ביומנים],\nאו [{{fullurl:{{FULLPAGENAME}}|action=edit}} ליצור את הדף]</span>.",
        "noarticletext-nopermission": "אין כרגע טקסט בדף הזה.\nבאפשרותך [[Special:Search/{{PAGENAME}}|לחפש את כותרת הדף]] בדפים אחרים או <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} לחפש את הדף ביומנים]</span>, אך אין לך הרשאה ליצור את הדף.",
        "missing-revision": "גרסה #$1 של הדף \"{{FULLPAGENAME}}\" אינה קיימת.\n\nזה נגרם בדרך כלל על־ידי לחיצה על קישור ישן לגרסה קודמת של דף שנמחק.\nאפשר למצוא פרטים ב[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} יומן המחיקות].",
-       "userpage-userdoesnotexist": "חשבון המשתמש \"$1\" אינו רשום.\nאנא בִּדקו אם ברצונכם ליצור/לערוך את הדף הזה.",
+       "userpage-userdoesnotexist": "חשבון המשתמש \"$1\" אינו רשום.\nאנא בִדקו אם ברצונכם ליצור/לערוך את הדף הזה.",
        "userpage-userdoesnotexist-view": "חשבון המשתמש \"$1\" אינו רשום.",
        "blocked-notice-logextract": "{{GENDER:$1|המשתמש הזה חסום|המשתמשת הזו חסומה}} כרגע.\nהפעולה האחרונה ביומן החסימות מוצגת להלן:",
        "clearyourcache": "<strong>הערה:</strong> לאחר השמירה, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.\n* <strong>פיירפוקס / ספארי:</strong> להחזיק את המקש <em>Shift</em> בעת לחיצה על <strong>טעינה מחדש</strong> (Reload), או ללחוץ על צירוף המקשים <em>Ctrl-F5</em> או <em>Ctrl-R</em> (במחשב מק: <em dir=\"ltr\">⌘-R</em>)\n* <strong>גוגל כרום:</strong> ללחוץ על צירוף המקשים <em>Ctrl-Shift-R</em> (במחשב מק: <em dir=\"ltr\">⌘-Shift-R</em>)\n* <strong>אינטרנט אקספלורר:</strong> ללחוץ ולהחזיק את המקש <em>Ctrl</em> בעת לחיצה על <strong>רענן</strong> (Refresh), או ללחוץ על צירוף המקשים <em>Ctrl-F5</em>\n* <strong>אופרה:</strong> לפתוח <em>תפריט ← הגדרות</em> (במחשב מק <em>Opera ← העדפות</em>) ואז ללחוץ על <em>פרטיות ואבטחה ← מחק היסטוריית גלישה ← Cached images and files</em>.",
        "editingsection": "עריכת הדף $1 (פסקה)",
        "editingcomment": "עריכת הדף $1 (פסקה חדשה)",
        "editconflict": "התנגשות עריכה: $1",
-       "explainconflict": "×\9eשת×\9eש ×\90×\97ר ×©×\99× ×\94 ×\90ת ×\94×\93×£ ×\9e×\90×\96 ×©×\94ת×\97×\9cת×\9d ×\9cער×\95×\9a ×\90×\95ת×\95.\n×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f ×\9e×\9b×\99×\9c ×\90ת ×\94×\98קס×\98 ×\91×\93×£ ×\9bפ×\99 ×©×\94×\95×\90 ×¢×ª×\94.\n×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9e×\95צ×\92×\99×\9d ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94ת×\97ת×\95×\9f.\n×¢×\9c×\99×\9b×\9d ×\9c×\9e×\96×\92 ×\90ת ×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9cת×\95×\9a ×\94×\98קס×\98 ×\94ק×\99×\99×\9d.\n'''רק''' הטקסט בחלון העריכה העליון יישמר כשתלחצו על \"{{int:savearticle}}\".",
+       "explainconflict": "×\9eשת×\9eש ×\90×\97ר ×©×\99× ×\94 ×\90ת ×\94×\93×£ ×\9e×\90×\96 ×©×\94ת×\97×\9cת×\9d ×\9cער×\95×\9a ×\90×\95ת×\95.\n×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f ×\9eצ×\99×\92 ×\90ת ×\94×\98קס×\98 ×\91×\93×£ ×\9bפ×\99 ×©×\94×\95×\90 ×\9bר×\92×¢.\n×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9e×\95צ×\92×\99×\9d ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94ת×\97ת×\95×\9f.\n×¢×\9c×\99×\9b×\9d ×\9c×\9e×\96×\92 ×\90ת ×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9cת×\95×\9a ×\94×\98קס×\98 ×\94ק×\99×\99×\9d.\n<strong>רק</strong> הטקסט בחלון העריכה העליון יישמר כשתלחצו על \"{{int:savearticle}}\".",
        "yourtext": "הטקסט שלך",
        "storedversion": "גרסה שמורה",
        "nonunicodebrowser": "'''אזהרה: הדפדפן שלך אינו תואם לתקן יוניקוד.'''\nכדי למנוע בעיות הנוצרות כתוצאה מכך ולאפשר לך לערוך דפים בבטחה, תווים שאינם ב־ASCII יוצגו בתיבת העריכה כקודים הקסדצימליים.",
-       "editingold": "'''אזהרה: אתם עורכים גרסה לא עדכנית של דף זה.'''\nאם תשמרו את הדף, כל השינויים שנעשו מאז גרסה זו יאבדו.",
+       "editingold": "<strong>אזהרה: אתם עורכים גרסה לא עדכנית של דף זה.</strong>\nאם תשמרו את הדף, כל השינויים שנעשו מאז גרסה זו יאבדו.",
        "yourdiff": "הבדלים",
        "copyrightwarning": "'''שימו לב:''' תרומתכם ל{{grammar:תחילית|{{SITENAME}}}} תפורסם תחת תנאי הרישיון $2 (ראו $1 לפרטים נוספים). אם אינכם רוצים שעבודתכם תהיה זמינה לעריכה על־ידי אחרים, שתופץ לעיני כול, ושאחרים יוכלו להעתיק ממנה בציון המקור – אל תפרסמו אותה פה. כמו־כן, אתם מבטיחים לנו כי כתבתם את הטקסט הזה בעצמכם, או העתקתם אותו ממקור שאינו מוגן בזכויות יוצרים. '''אל תעשו שימוש בחומר המוגן בזכויות יוצרים ללא רשות!'''",
        "copyrightwarning2": "'''שימו לב:''' תורמים אחרים עשויים לערוך או אף להסיר את תרומתכם ל{{grammar:תחילית|{{SITENAME}}}}. אם אינכם רוצים שעבודתכם תהיה זמינה לעריכה על־ידי אחרים, אל תפרסמו אותה פה. כמו־כן, אתם מבטיחים לנו כי כתבתם את הטקסט הזה בעצמכם, או העתקתם אותו ממקור שאינו מוגן בזכויות יוצרים (ראו $1 לפרטים נוספים). '''אל תעשו שימוש בחומר המוגן בזכויות יוצרים ללא רשות!'''",
        "revdelete-unsuppress": "הסרת הגבלות בגרסאות המשוחזרות",
        "revdelete-log": "סיבה:",
        "revdelete-submit": "ביצוע על {{PLURAL:$1|הגרסה שנבחרה|הגרסאות שנבחרו}}",
-       "revdelete-success": "×\9eצ×\91 ×\94תצ×\95×\92×\94 ×©×\9c ×\94×\92רס×\94 ×©×\81×\95Ö¼× ×\94.",
+       "revdelete-success": "מצב התצוגה של הגרסה שוּנה.",
        "revdelete-failure": "לא ניתן היה לשנות את מצב התצוגה של הגרסה:\n$1",
-       "logdelete-success": "×\9eצ×\91 ×\94תצ×\95×\92×\94 ×©×\9c ×¤×¢×\95×\9cת ×\94×\99×\95×\9e×\9f ×©×\81×\95Ö¼× ×\94.",
+       "logdelete-success": "מצב התצוגה של פעולת היומן שוּנה.",
        "logdelete-failure": "לא ניתן היה לשנות את מצב התצוגה של היומן:\n$1",
        "revdel-restore": "שינוי מצב התצוגה",
        "pagehist": "היסטוריית הדף",
        "prefs-rc": "שינויים אחרונים",
        "prefs-watchlist": "רשימת המעקב",
        "prefs-editwatchlist": "עריכת רשימת המעקב",
-       "prefs-editwatchlist-label": "עריכת דפים ברשימת המעקב שלך:",
-       "prefs-editwatchlist-edit": "הצגה או הסרה של דפים מרשימת המעקב שלך",
+       "prefs-editwatchlist-label": "עריכת דפים ברשימת המעקב:",
+       "prefs-editwatchlist-edit": "הצגה או הסרה של דפים מרשימת המעקב",
        "prefs-editwatchlist-raw": "עריכת רשימת המעקב הגולמית",
-       "prefs-editwatchlist-clear": "ניקוי רשימת המעקב שלך",
+       "prefs-editwatchlist-clear": "ניקוי רשימת המעקב",
        "prefs-watchlist-days": "מספר הימים שמוצגים ברשימת המעקב:",
        "prefs-watchlist-days-max": "לכל היותר {{PLURAL:$1|יום אחד|יומיים|$1 ימים}}",
        "prefs-watchlist-edits": "המספר המרבי של העריכות שמוצגות ברשימת המעקב המורחבת:",
        "right-override-export-depth": "ייצוא דפים כולל דפים מקושרים עד עומק של חמישה",
        "right-sendemail": "שליחת דואר אלקטרוני למשתמשים אחרים",
        "right-passwordreset": "צפייה בדואר אלקטרוני של איפוס סיסמה",
-       "right-managechangetags": "×\99צ×\99רת ×\95×\9e×\97×\99קת [[Special:Tags|ת×\92×\99×\95ת]] ×\9e×\91ס×\99ס ×\94נת×\95× ×\99×\9d",
+       "right-managechangetags": "×\99צ×\99ר×\94, ×\94פע×\9c×\94 ×\95×\91×\99×\98×\95×\9c ×©×\9c [[Special:Tags|ת×\92×\99×\95ת]]",
        "right-applychangetags": "החלת [[Special:Tags|תגיות]] יחד עם שינויים",
        "right-changetags": "הוספת והסרה של [[Special:Tags|תגיות]] כלשהן לגרסאות מסוימות ולרשומות יומן",
+       "right-deletechangetags": "מחיקת [[Special:Tags|תגיות]] מבסיס הנתונים",
        "grant-generic": "חבילת ההרשאות \"$1\"",
        "grant-group-page-interaction": "אינטראקציה עם דפים",
        "grant-group-file-interaction": "אינטראקציה עם קבצים",
        "action-viewmyprivateinfo": "לצפות במידע הפרטי שלך",
        "action-editmyprivateinfo": "לערוך את המידע הפרטי שלך",
        "action-editcontentmodel": "לערוך את מודל התוכן של דף",
-       "action-managechangetags": "ליצור ולמחוק תגיות מבסיס הנתונים",
+       "action-managechangetags": "ליצור, להפעיל ולבטל תגיות",
        "action-applychangetags": "להחיל תגיות יחד עם השינויים שלכם",
        "action-changetags": "להוסיף ולהסיר תגיות כלשהן לגרסאות מסוימות ולרשומות יומן",
+       "action-deletechangetags": "למחוק תגיות מבסיס הנתונים",
        "nchanges": "{{PLURAL:$1|שינוי אחד|$1 שינויים}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|מאז ביקורך האחרון}}",
        "enhancedrc-history": "היסטוריה",
        "recentchanges-label-unpatrolled": "עריכה זו טרם נבדקה",
        "recentchanges-label-plusminus": "גודל הדף השתנה במספר זה של בתים",
        "recentchanges-legend-heading": "<strong>מקרא:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ראו גם [[Special:NewPages|רשימת דפים חדשים]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ראו גם את [[Special:NewPages|רשימת הדפים החדשים]])",
        "recentchanges-legend-plusminus": "(''±123'')",
        "recentchanges-submit": "הצגה",
        "rcnotefrom": "להלן {{PLURAL:$5|השינוי שבוצע|השינויים שבוצעו}} מאז <strong>$3, $4</strong> (מוצגים עד <strong>$1</strong>).",
        "recentchangeslinked": "שינויים בדפים המקושרים",
        "recentchangeslinked-feed": "שינויים בדפים המקושרים",
        "recentchangeslinked-toolbox": "שינויים בדפים המקושרים",
-       "recentchangeslinked-title": "שינויים בדפים המקושרים מהדף $1",
+       "recentchangeslinked-title": "שינויים בדפים המקושרים מהדף \"$1\"",
        "recentchangeslinked-summary": "בדף מיוחד זה רשומים השינויים האחרונים בדפים המקושרים מתוך הדף (או בדפים הכלולים בקטגוריה).\nדפים ב[[Special:Watchlist|רשימת המעקב שלכם]] מוצגים ב'''הדגשה'''.",
        "recentchangeslinked-page": "שם הדף:",
        "recentchangeslinked-to": "הצגת השינויים בדפים המקשרים לדף הנתון במקום זאת",
        "recentchanges-page-removed-from-category": "הדף [[:$1]] הוסר מקטגוריה",
        "recentchanges-page-removed-from-category-bundled": "הדף [[:$1]] הוסר מקטגוריה, [[Special:WhatLinksHere/$1|והוא מוכלל בדפים אחרים]]",
        "autochange-username": "שינוי אוטומטי של מדיה־ויקי",
-       "upload": "העלאת קובץ לשרת",
-       "uploadbtn": "×\94×¢×\9c×\90×\94",
+       "upload": "העלאת קובץ",
+       "uploadbtn": "×\94×¢×\9c×\90ת ×\94ק×\95×\91×¥",
        "reuploaddesc": "ביטול ההעלאה וחזרה לטופס העלאת קבצים לשרת",
        "upload-tryagain": "שליחת התיאור החדש של הקובץ",
        "uploadnologin": "לא נכנסת לחשבון",
        "upload-form-label-own-work": "אני יצרתי את הקובץ",
        "upload-form-label-infoform-categories": "קטגוריות",
        "upload-form-label-infoform-date": "תאריך",
-       "upload-form-label-own-work-message-local": "ההעלאה מבוצעת בהתאם לתנאי השירות ולמדיניות הרישיונות ב{{grammar:תחילית|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "אם אין באפשרותך להעלות את הקובץ הזה לפי המדיניות של {{SITENAME}}, עליך לסגור את התיבה הנוכחית ולנסות שיטה אחרת.",
-       "upload-form-label-not-own-work-local-local": "באפשרותך לנסות להשתמש ב[[Special:Upload|דף ברירת המחדל להעלאת קבצים]].",
-       "upload-form-label-own-work-message-default": "ידוע לי שאני מעלה את הקובץ הזה למאגר משותף. ההעלאה מבוצעת בהתאם לתנאי השירות ולמדיניות הרישיונות שם.",
-       "upload-form-label-not-own-work-message-default": "אם אין באפשרותך להעלות את הקובץ הזה לפי המדיניות של המאגר המשותף, עליך לסגור את התיבה הנוכחית ולנסות שיטה אחרת.",
-       "upload-form-label-not-own-work-local-default": "באפשרותך לנסות להשתמש ב[[Special:Upload|דף העלאת הקבצים ב{{grammar:תחילית|{{SITENAME}}}}]], אם ניתן להעלות את הקובץ הזה לשם לפי מדיניות האתר.",
-       "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 תנאי השימוש].",
-       "upload-form-label-not-own-work-message-shared": "אם זכויות היוצרים על הקובץ הזה אינן בבעלותך, או שברצונך לשחרר אותו תחת רישיון אחר, באפשרותך להשתמש ב[https://commons.wikimedia.org/wiki/Special:UploadWizard אשף ההעלאה לוויקישיתוף].",
-       "upload-form-label-not-own-work-local-shared": "באפשרותך לנסות להשתמש ב[[Special:Upload|דף העלאת הקבצים ב{{grammar:תחילית|{{SITENAME}}}}]], אם ניתן להעלות את הקובץ הזה לשם לפי מדיניות האתר.",
+       "upload-form-label-own-work-message-generic-local": "ההעלאה מבוצעת בהתאם לתנאי השירות ולמדיניות הרישיונות ב{{grammar:תחילית|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "אם אין באפשרותך להעלות את הקובץ הזה לפי המדיניות של {{SITENAME}}, עליך לסגור את התיבה הנוכחית ולנסות שיטה אחרת.",
+       "upload-form-label-not-own-work-local-generic-local": "באפשרותך לנסות להשתמש ב[[Special:Upload|דף ברירת המחדל להעלאת קבצים]].",
+       "upload-form-label-own-work-message-generic-foreign": "ידוע לי שאני מעלה את הקובץ הזה למאגר משותף. ההעלאה מבוצעת בהתאם לתנאי השירות ולמדיניות הרישיונות שם.",
+       "upload-form-label-not-own-work-message-generic-foreign": "אם אין באפשרותך להעלות את הקובץ הזה לפי המדיניות של המאגר המשותף, עליך לסגור את התיבה הנוכחית ולנסות שיטה אחרת.",
+       "upload-form-label-not-own-work-local-generic-foreign": "באפשרותך לנסות להשתמש ב[[Special:Upload|דף העלאת הקבצים ב{{grammar:תחילית|{{SITENAME}}}}]], אם ניתן להעלות את הקובץ הזה לשם לפי מדיניות האתר.",
        "backend-fail-stream": "לא הייתה אפשרות להזרים את הקובץ \"$1\".",
        "backend-fail-backup": "לא הייתה אפשרות לגבות את הקובץ \"$1\".",
        "backend-fail-notexists": "הקובץ \"$1\" אינו קיים.",
        "enotif_body": "לכבוד $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nתקציר העריכה: $PAGESUMMARY $PAGEMINOREDIT\n\nבאפשרותכם ליצור קשר עם העורך:\nבדואר האלקטרוני: $PAGEEDITOR_EMAIL\nבאתר: $PAGEEDITOR_WIKI\n\nלא תהיינה הודעות על פעולות נוספות עד שתבקרו בדף כשאתם מחוברים לחשבון. באפשרותכם גם לאפס את דגלי ההודעות בכל הדפים שברשימת המעקב.\n\nמערכת ההודעות של {{SITENAME}}\n\n--\nכדי לשנות את ההגדרות של הודעות הדוא\"ל הנשלחות אליכם, בקרו בדף\n{{canonicalurl:{{#special:Preferences}}}}\n\nכדי לשנות את הגדרות רשימת המעקב, בקרו בדף\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nכדי למחוק את הדף מרשימת המעקב שלכם, בקרו בדף\n$UNWATCHURL\n\nלמשוב ולעזרה נוספת:\n$HELPPAGE",
        "created": "נוצר",
        "changed": "שונה",
-       "deletepage": "×\9e×\97×\99ק×\94",
+       "deletepage": "×\9e×\97×\99קת ×\94×\93×£",
        "confirm": "אישור",
        "excontent": "התוכן היה: \"$1\"",
        "excontentauthor": "התוכן היה: \"$1\", {{GENDER:$2|והתורם היחיד היה|והתורמת היחידה הייתה}} \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|שיחה]])",
        "exbeforeblank": "התוכן לפני שרוקן היה: \"$1\"",
-       "delete-confirm": "מחיקת \"$1\"",
+       "delete-confirm": "מחיקת הדף \"$1\"",
        "delete-legend": "מחיקה",
        "historywarning": "<strong>אזהרה:</strong> לדף שאתם עומדים למחוק יש היסטוריית שינויים של {{PLURAL:$1|גרסה אחת|$1 גרסאות}}:",
        "historyaction-submit": "הצגה",
        "sp-contributions-newonly": "הצגת עריכות שהן יצירות של דפים בלבד",
        "sp-contributions-submit": "חיפוש",
        "whatlinkshere": "דפים המקושרים לכאן",
-       "whatlinkshere-title": "דפים המקשרים לדף $1",
+       "whatlinkshere-title": "דפים המקשרים לדף \"$1\"",
        "whatlinkshere-page": "דף:",
        "linkshere": "הדפים שלהלן מקושרים לדף '''[[:$1]]''':",
        "nolinkshere": "אין דפים המקושרים לדף '''[[:$1]]'''.",
        "whatlinkshere-prev": "{{PLURAL:$1|הקודם|$1 הקודמים}}",
        "whatlinkshere-next": "{{PLURAL:$1|הבא|$1 הבאים}}",
        "whatlinkshere-links": "→ קישורים",
-       "whatlinkshere-hideredirs": "$1 הפניות",
-       "whatlinkshere-hidetrans": "$1 הכללות",
-       "whatlinkshere-hidelinks": "$1 קישורים",
-       "whatlinkshere-hideimages": "$1 קישורים לקובץ",
+       "whatlinkshere-hideredirs": "הסתרת הפניות",
+       "whatlinkshere-hidetrans": "הסתרת הכללות",
+       "whatlinkshere-hidelinks": "הסתרת קישורים",
+       "whatlinkshere-hideimages": "הסתרת קישורי קבצים",
        "whatlinkshere-filters": "מסננים",
        "whatlinkshere-submit": "הצגה",
        "autoblockid": "חסימה אוטומטית #$1",
        "ipb-hardblock": "ביטול האפשרות של משתמשים רשומים לערוך מכתובת IP זו",
        "ipbcreateaccount": "חסימה של יצירת חשבונות",
        "ipbemailban": "חסימה של שליחת דואר אלקטרוני",
-       "ipbenableautoblock": "×\97ס×\99×\9e×\94 ×\90×\95×\98×\95×\9e×\98×\99ת ×\92×\9d ×©×\9c ×\9bת×\95×\91ת ×\94Ö¾IP ×\94×\90×\97ר×\95× ×\94 ×©×\94שת×\9eש ×\91×\94 ×\95של כל כתובת IP שינסה להשתמש בה בעתיד",
-       "ipbsubmit": "×\97ס×\99×\9e×\94",
+       "ipbenableautoblock": "×\97ס×\99×\9e×\94 ×\90×\95×\98×\95×\9e×\98×\99ת ×©×\9c ×\9bת×\95×\91ת ×\94Ö¾IP ×\94×\90×\97ר×\95× ×\94 ×©×\94×\9eשת×\9eש ×¢×©×\94 ×\91×\94 ×©×\99×\9e×\95ש ×\95×\92×\9d של כל כתובת IP שינסה להשתמש בה בעתיד",
+       "ipbsubmit": "×\97ס×\99×\9eת ×\94×\9eשת×\9eש",
        "ipbother": "זמן אחר:",
        "ipboptions": "שעתיים:2 hours,יום:1 day,שלושה ימים:3 days,שבוע:1 week,שבועיים:2 weeks,חודש:1 month,שלושה חודשים:3 months,שישה חודשים:6 months,שנה:1 year,זמן בלתי מוגבל:infinite",
        "ipbhidename": "הסתרת שם המשתמש מהעריכות ומהרשימות",
        "ipb-confirmhideuser": "אתם עומדים לחסום משתמש עם האפשרות \"הסתרת משתמש\". זה יעלים את שם המשתמש בכל הרשימות ופעולות היומן. האם אתם בטוחים שברצונכם לעשות זאת?",
        "ipb-confirmaction": "אם אתם באמת בטוחים שברצונכם לעשות זאת, אנא סמנו את השדה \"{{int:ipb-confirm}}\" שבתחתית.",
        "ipb-edit-dropdown": "עריכת סיבות החסימה",
-       "ipb-unblock-addr": "×\94סרת חסימה של $1",
+       "ipb-unblock-addr": "×\91×\99×\98×\95×\9c חסימה של $1",
        "ipb-unblock": "הסרת חסימה של שם משתמש או כתובת IP",
        "ipb-blocklist": "הצגת החסימות הנוכחיות",
-       "ipb-blocklist-contribs": "×\94תר×\95×\9e×\95ת ×©×\9c {{GENDER:$1|$1}}",
+       "ipb-blocklist-contribs": "תרומות של {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "נותרו $1",
        "unblockip": "שחרור חסימה",
        "unblockiptext": "השתמשו בטופס שלהלן כדי להחזיר את הרשאות הכתיבה למשתמש או כתובת IP חסומים.",
        "blocklist-tempblocks": "הסתרת חסימות זמניות",
        "blocklist-addressblocks": "הסתרת חסימות IP בודד",
        "blocklist-rangeblocks": "הסתרת חסימות טווחים",
-       "blocklist-timestamp": "×\96×\9e×\9f",
+       "blocklist-timestamp": "ת×\90ר×\99×\9a ×\95שע×\94",
        "blocklist-target": "יעד",
        "blocklist-expiry": "זמן פקיעה",
-       "blocklist-by": "×\9eפע×\99×\9c ×\97×\95ס×\9d",
-       "blocklist-params": "הגדרות חסימה",
+       "blocklist-by": "× ×\97ס×\9d ×¢×\9cÖ¾×\99×\93×\99",
+       "blocklist-params": "×\94×\92×\93ר×\95ת ×\94×\97ס×\99×\9e×\94",
        "blocklist-reason": "סיבה",
        "ipblocklist-submit": "חיפוש",
        "ipblocklist-localblock": "חסימה מקומית",
        "databaselocked": "בסיס הנתונים כבר נעול.",
        "databasenotlocked": "בסיס הנתונים אינו נעול.",
        "lockedbyandtime": "(על־ידי $1 ב־$3, $2)",
-       "move-page": "העברת $1",
+       "move-page": "העברת הדף \"$1\"",
        "move-page-legend": "העברת דף",
        "movepagetext": "שימוש בטופס שלהלן ישנה את שמו של דף, ויעביר את כל ההיסטוריה שלו לשם חדש.\nהשם הישן יהפוך לדף הפניה אל הדף עם השם החדש.\nבאפשרותכם לעדכן אוטומטית דפי הפניה לכותרת המקורית.\nאם תבחרו לא לעשות זאת, אנא ודאו שאין [[Special:DoubleRedirects|הפניות כפולות]] או [[Special:BrokenRedirects|שבורות]].\nאתם אחראים לוודא שכל הקישורים ימשיכו להצביע למקום שאליו הם אמורים להצביע.\n\nשימו לב: הדף <strong>לא</strong> יועבר אם כבר יש דף תחת השם החדש, אלא אם הדף השני הוא הפניה ואין לו היסטוריית עריכות קודמות.\nפירוש הדבר שאפשר לשנות חזרה את שמו של דף לשם המקורי אם נעשתה טעות, ושלא ניתן לדרוס דף קיים.\n\n<strong>לתשומת לבכם:</strong>\nשינוי זה עשוי להיות שינוי דרסטי ובלתי צפוי לדף פופולרי;\nאנא ודאו שאתם מבינים את השלכות המעשה לפני שאתם ממשיכים.",
        "movepagetext-noredirectfixer": "שימוש בטופס שלהלן ישנה את שמו של דף, ויעביר את כל ההיסטוריה שלו לשם חדש.\nהשם הישן יהפוך לדף הפניה אל הדף עם השם החדש.\nאנא ודאו שאין [[Special:DoubleRedirects|הפניות כפולות]] או [[Special:BrokenRedirects|שבורות]].\nאתם אחראים לוודא שכל הקישורים ימשיכו להצביע למקום שאליו הם אמורים להצביע.\n\nשימו לב: הדף <strong>לא</strong> יועבר אם כבר יש דף תחת השם החדש, אלא אם הדף הזה הוא הפניה ואין לו היסטוריית עריכות קודמות.\nפירוש הדבר שאפשר לשנות חזרה את שמו של דף לשם המקורי אם נעשתה טעות, ושלא ניתן לדרוס דף קיים.\n\n<strong>לתשומת לבכם:</strong>\nשינוי זה עשוי להיות שינוי דרסטי ובלתי צפוי לדף פופולרי;\nאנא ודאו שאתם מבינים את השלכות המעשה לפני שאתם ממשיכים.",
        "cant-move-to-category-page": "אין לך הרשאה להעביר דף לדף קטגוריה.",
        "newtitle": "השם החדש:",
        "move-watch": "מעקב אחרי דף המקור ואחרי דף היעד",
-       "movepagebtn": "×\94×¢×\91ר×\94",
+       "movepagebtn": "×\94×¢×\91רת ×\94×\93×£",
        "pagemovedsub": "ההעברה הושלמה בהצלחה",
        "movepage-moved": "הדף \"$1\" הועבר לשם \"$2\".",
        "movepage-moved-redirect": "נוצרה הפניה.",
        "tooltip-pt-mytalk": "דף השיחה שלך",
        "tooltip-pt-anontalk": "שיחה על תרומות המשתמש האנונימי",
        "tooltip-pt-preferences": "ההעדפות שלך",
-       "tooltip-pt-watchlist": "רשימת הדפים שאתם עוקבים אחרי השינויים בהם",
+       "tooltip-pt-watchlist": "רשימת הדפים ש{{GENDER:|אתה עוקב|את עוקבת}} אחרי השינויים בהם",
        "tooltip-pt-mycontris": "רשימת התרומות שלך",
        "tooltip-pt-anoncontribs": "רשימת העריכות שנעשו מכתובת ה־IP הזאת",
        "tooltip-pt-login": "מומלץ להיכנס לחשבון; עם זאת, אין חובה לעשות זאת",
        "tooltip-save": "שמירת השינויים שלך",
        "tooltip-publish": "פרסום השינויים שלך",
        "tooltip-preview": "תצוגה מקדימה של השינויים שלך. נא להשתמש באפשרות זו לפני השמירה.",
-       "tooltip-diff": "צפ×\99×\99×\94 ×\91ש×\99× ×\95×\99×\99×\9d ×©×¢×¨×\9bת×\9d בטקסט",
+       "tooltip-diff": "×\94צ×\92ת ×\94ש×\99× ×\95×\99×\99×\9d ×©×\91×\99צעת בטקסט",
        "tooltip-compareselectedversions": "צפייה בהשוואת שתי גרסאות של דף זה",
        "tooltip-watch": "הוספת דף זה לרשימת המעקב שלך",
        "tooltip-watchlistedit-normal-submit": "הסרת הדפים",
        "anonymous": "{{PLURAL:$1|משתמש אנונימי|משתמשים אנונימיים}} של {{SITENAME}}",
        "siteuser": "משתמש {{SITENAME}} $1",
        "anonuser": "משתמש אנונימי של {{SITENAME}} $1",
-       "lastmodifiedatby": "דף זה שונה לאחרונה ב־$2, $1 על־ידי $3.",
+       "lastmodifiedatby": "דף זה שוּנה לאחרונה ב־$2, $1 על־ידי $3.",
        "othercontribs": "מבוסס על העבודה של $1.",
        "others": "אחרים",
        "siteusers": "{{PLURAL:$2|{{GENDER:$1|משתמש}}|משתמשי}} {{SITENAME}} $1",
        "spam_blanking": "כל הגרסאות כוללות קישורים ל־$1, מרוקן את הדף",
        "spam_deleting": "כל הגרסאות כוללות קישורים ל־$1, מוחק את הדף",
        "simpleantispam-label": "בדיקת אנטי־ספאם.\n<strong>אל</strong> תמלאו שדה זה!",
-       "pageinfo-title": "מידע על \"$1\"",
+       "pageinfo-title": "מידע על הדף \"$1\"",
        "pageinfo-not-current": "מצטערים, לא ניתן להציג את המידע הזה לגרסאות ישנות.",
        "pageinfo-header-basic": "מידע בסיסי",
        "pageinfo-header-edits": "היסטוריית עריכות",
        "markedaspatrollederror": "לא ניתן לסמן כבדוק",
        "markedaspatrollederrortext": "יש לציין גרסה שברצונך לסמן כבדוקה.",
        "markedaspatrollederror-noautopatrol": "אינך מורשה לסמן שינויים של עצמך כבדוקים.",
-       "markedaspatrollednotify": "ש×\99× ×\95×\99 ×\96×\94 ×\9c\"$1\" ×¡×\95×\9e×\9f ×\9b×\91×\93×\95ק.",
-       "markedaspatrollederrornotify": "ס×\99×\9e×\95×\9f ×\94ש×\99× ×\95×\99 ×\9b×\91×\93×\95ק נכשל.",
+       "markedaspatrollednotify": "ער×\99×\9b×\94 ×\96×\95 ×\91×\93×£ \"$1\" ×¡×\95×\9e× ×\94 ×\9b×\91×\93×\95ק×\94.",
+       "markedaspatrollederrornotify": "ס×\99×\9e×\95×\9f ×\94ער×\99×\9b×\94 ×\9b×\91×\93×\95ק×\94 נכשל.",
        "patrol-log-page": "יומן שינויים בדוקים",
        "patrol-log-header": "יומן זה מציג גרסאות שנבדקו.",
        "log-show-hide-patrol": "$1 יומן שינויים בדוקים",
        "timezone-local": "מקומי",
        "duplicate-defaultsort": "'''אזהרה:''' המיון הרגיל \"$2\" דורס את המיון הרגיל המוקדם ממנו \"$1\".",
        "duplicate-displaytitle": "<strong>אזהרה:</strong> כותרת התצוגה \"$2\" דורסת את כותרת התצוגה הקודמת \"$1\".",
+       "restricted-displaytitle": "<strong>אזהרה:</strong> כותרת התצוגה \"$1\" לא הופעלה כי היא אינה תואמת לכותרת האמיתית של הדף.",
        "invalid-indicator-name": "<strong>שגיאה:</strong> התכונה <code>name</code> של מצייני מצב הדף אינה יכולה להיות ריקה.",
        "version": "גרסת התוכנה",
        "version-extensions": "הרחבות מותקנות",
        "tags-delete-not-found": "התגית \"$1\" אינה קיימת.",
        "tags-delete-too-many-uses": "התגית \"$1\" מוחלת על יותר {{PLURAL:$2|מגרסה אחת|מ־$2 גרסאות}}, ולכן לא ניתן למחוק אותה.",
        "tags-delete-warnings-after-delete": "התגית \"$1\" נמחקה, אבל {{PLURAL:$2|התקבלה האזהרה הבאה|התקבלו האזהרות הבאות}}:",
+       "tags-delete-no-permission": "אין לך הרשאה למחוק תגיות שינויים.",
        "tags-activate-title": "הפעלת תגית",
        "tags-activate-question": "אתם עומדים להפעיל את התגית \"$1\".",
        "tags-activate-reason": "הסבר:",
index eb11fb4..8d8024f 100644 (file)
        "noname": "आपने वैध सदस्यनाम नहीं दिया है।",
        "loginsuccesstitle": "प्रवेश हुआ",
        "loginsuccess": "'''आप {{SITENAME}} में \"$1\" सदस्यनाम से लॉग इन हो {{GENDER:$1|चुके|चुकी|चुके}} हैं।'''",
-       "nosuchuser": "\"$1\" नाम का कोई सदस्य नहीं है।\nसदस्यनाम में लघु और दीर्घ अक्षरों से फ़र्क पड़ता है।\nअपनी वर्तनी जाँचें, या [[Special:UserLogin/signup|नया खाता खोलें]]।",
+       "nosuchuser": "\"$1\" नाम का कोई सदस्य नहीं है।\nसदस्यनाम में लघु और दीर्घ अक्षरों से फ़र्क पड़ता है।\nअपनी वर्तनी जाँचें, या [[Special:CreateAccount|नया खाता खोलें]]।",
        "nosuchusershort": "\"$1\" नाम का कोई सदस्य नहीं है।\nकृपया अपनी दी हुई वर्तनी जाँचें।",
        "nouserspecified": "सदस्यनाम देना अनिवार्य है।",
        "login-userblocked": "यह सदस्य प्रतिबन्धित है। सत्रारम्भ की अनुमति नहीं है।",
        "accmailtext": "[[User talk:$1|$1]] के लिए एक यंत्र जनित कूटशब्द $2 को भेज दिया गया है। लॉगिन करने के बाद इसे '''[[Special:ChangePassword|कूटशब्द बदलें]]'' वाले पृष्ठ पर बदला जा सकता है।",
        "newarticle": "(नया)",
        "newarticletext": "आप ऐसे पृष्ठ पर आए हैं जो अभी तक बनाया नहीं गया है।\nपृष्ठ बनाने के लिये नीचे के बौक्स में पाठ लिखें। अधिक जानकारी के लिये [$1 सहायता पृष्ठ] देखें।\nअगर आप यहाँ पर गलती से आए हैं तो अपने ब्राउज़र के बैक ('''back''') बटन पर क्लिक करें।",
-       "anontalkpagetext": "----''यह वार्ता पृष्ठ उन बेनामी सदस्यों के लिये है जिन्होंने या तो खाता नहीं खोला है या खाते का प्रयोग नहीं कर रहे हैं।\nइसलिये उनकी पहचान के लिये हमें उनका आइ॰पी पता प्रयोग करना पड़ता है।\nआइ॰पी पता कई सदस्यों के लिए साझा हो सकता है।\nयदि आप एक बेनामी सदस्य हैं और आपको लगता है कि आपके बारे में अप्रासंगिक टीका टिप्पणी की गई है तो कृपया [[Special:UserLogin/signup|सदस्यता लें]] या [[Special:UserLogin|सत्रारंभ करें]] ताकि अन्य बेनामी सदस्यों में से आपको अलग से पहचाना जा सके।''",
+       "anontalkpagetext": "----''यह वार्ता पृष्ठ उन बेनामी सदस्यों के लिये है जिन्होंने या तो खाता नहीं खोला है या खाते का प्रयोग नहीं कर रहे हैं।\nइसलिये उनकी पहचान के लिये हमें उनका आइ॰पी पता प्रयोग करना पड़ता है।\nआइ॰पी पता कई सदस्यों के लिए साझा हो सकता है।\nयदि आप एक बेनामी सदस्य हैं और आपको लगता है कि आपके बारे में अप्रासंगिक टीका टिप्पणी की गई है तो कृपया [[Special:CreateAccount|सदस्यता लें]] या [[Special:UserLogin|सत्रारंभ करें]] ताकि अन्य बेनामी सदस्यों में से आपको अलग से पहचाना जा सके।''",
        "noarticletext": "फ़िलहाल इस पृष्ठ पर कोई सामग्री नहीं है।\nआप अन्य पृष्ठों में [[Special:Search/{{PAGENAME}}|इस शीर्षक की खोज]] कर सकते हैं,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} सम्बन्धित लॉग खोज सकते हैं],\nया इस पृष्ठ को [{{fullurl:{{FULLPAGENAME}}|action=edit}} सम्पादित] कर सकते हैं</span>।",
        "noarticletext-nopermission": "फ़िलहाल इस पृष्ठ पर कोई सामग्री नहीं है।\nआप अन्य पृष्ठों में [[Special:Search/{{PAGENAME}}|इस शीर्षक की खोज]] कर सकते हैं,\nया <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} संबंधित लॉग खोज सकते हैं]</span>, परन्तु आपको यह पृष्ठ बनाने की अनुमति नहीं है।",
        "missing-revision": "\"{{FULLPAGENAME}}\" पृष्ठ का अवतरण #$1 मौजूद नहीं है।\n\nआम तौर पर यह एक हटाए गए पृष्ठ के पुराने लिंक पर क्लिक करने से होता है।\nअधिक जानकारी के लिए आप [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटाने का लॉग] देख सकते हैं।",
        "upload-form-label-own-work": "यह मेरा कार्य है",
        "upload-form-label-infoform-categories": "श्रेणियाँ",
        "upload-form-label-infoform-date": "दिनांक",
-       "upload-form-label-own-work-message-local": "मैं यह सत्यापित करता हूँ कि मेरे द्वारा डाला गया फ़ाइल {{SITENAME}} सेवा के शर्तों और अधिकार नियम के अनुकूल है।",
-       "upload-form-label-not-own-work-message-local": "यदि आप {{SITENAME}} के नियमों के अंतर्गत फ़ाइल नहीं डाल सकते, तो आप इसे हटा कर किसी दूसरे विधि का उपयोग करें।",
-       "upload-form-label-not-own-work-local-local": "आप [[Special:Upload|मूल डालने वाले पृष्ठ]] का भी उपयोग कर सकते हो।",
-       "upload-form-label-own-work-message-default": "मैं यह समझता हूँ कि यहाँ सभी फ़ाइल सांझा होते हैं। मैं यह सत्यापित करता हूँ कि में सेवा के शर्तों और नियम के अनुरूप ही कार्य कर रहा हूँ।",
-       "upload-form-label-not-own-work-message-default": "यदि आप इस नियम के अंतर्गत फ़ाइल नहीं डालना चाहते तो अभी इसे बन्द कर दें और कोई दूसरे विधि को खोजें।",
-       "upload-form-label-not-own-work-local-default": "यदि आप चाहें तो आप [[Special:Upload|{{SITENAME}} के पृष्ठ]] पर फ़ाइल डाल सकते हैं, यदि यह फ़ाइल वहाँ के नियम के अंतर्गत हो तो।",
-       "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 विकि उपयोग की शर्तों] का भी पालन करता है।",
-       "upload-form-label-not-own-work-message-shared": "यदि आपके पास इस फ़ाइल का प्रतिकृति अधिकार नहीं है और आप इसे किसी और अधिकार के तहत प्रदर्शित करना चाहते हैं तो आप [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard] का उपयोग करें।",
-       "upload-form-label-not-own-work-local-shared": "यदि आप चाहें तो आप [[Special:Upload|{{SITENAME}} के पृष्ठ]] पर फ़ाइल डाल सकते हैं, यदि यह फ़ाइल वहाँ के नियम के अंतर्गत हो तो।",
+       "upload-form-label-own-work-message-generic-local": "मैं यह सत्यापित करता हूँ कि मेरे द्वारा डाला गया फ़ाइल {{SITENAME}} सेवा के शर्तों और अधिकार नियम के अनुकूल है।",
+       "upload-form-label-not-own-work-message-generic-local": "यदि आप {{SITENAME}} के नियमों के अंतर्गत फ़ाइल नहीं डाल सकते, तो आप इसे हटा कर किसी दूसरे विधि का उपयोग करें।",
+       "upload-form-label-not-own-work-local-generic-local": "आप [[Special:Upload|मूल डालने वाले पृष्ठ]] का भी उपयोग कर सकते हो।",
+       "upload-form-label-own-work-message-generic-foreign": "मैं यह समझता हूँ कि यहाँ सभी फ़ाइल सांझा होते हैं। मैं यह सत्यापित करता हूँ कि में सेवा के शर्तों और नियम के अनुरूप ही कार्य कर रहा हूँ।",
+       "upload-form-label-not-own-work-message-generic-foreign": "यदि आप इस नियम के अंतर्गत फ़ाइल नहीं डालना चाहते तो अभी इसे बन्द कर दें और कोई दूसरे विधि को खोजें।",
+       "upload-form-label-not-own-work-local-generic-foreign": "यदि आप चाहें तो आप [[Special:Upload|{{SITENAME}} के पृष्ठ]] पर फ़ाइल डाल सकते हैं, यदि यह फ़ाइल वहाँ के नियम के अंतर्गत हो तो।",
        "backend-fail-stream": "फ़ाइल $1 स्ट्रीम नहीं हो पाई।",
        "backend-fail-backup": "फ़ाइल $1 बैकअप नहीं हो पाई।",
        "backend-fail-notexists": "फ़ाइल $1 मौजूद नहीं है।",
index 6586e89..6da450d 100644 (file)
        "noname": "Aap achchha user name ke nai specify karaa hai.",
        "loginsuccesstitle": "Login safal bhais",
        "loginsuccess": "'''Aap \"$1\" ke naam pe {{SITENAME}} me logged in hai.'''",
-       "nosuchuser": "\"$1\" naam ke koi sadasya nai hai.\nSadasya ke naam case sensitive hai.\nAapan spelling check karo nai to [[Special:UserLogin/signup|nawaa account banao]].",
+       "nosuchuser": "\"$1\" naam ke koi sadasya nai hai.\nSadasya ke naam case sensitive hai.\nAapan spelling check karo nai to [[Special:CreateAccount|nawaa account banao]].",
        "nosuchusershort": "\"$1\" naam ke koi sadasya nai hai.\nAapan spelling check karo.",
        "nouserspecified": "Aap ke aapan username de ke parri.",
        "login-userblocked": "Ii sadasya ke rok dewa gais hae.  Login kare ke ijajat nai hae.",
        "accmailtext": "Ek randomly banawal password ke [[User talk:$1|$1]] ke khatir $2 ke lage bhaja gais hai.\nIi nawa account ke password ke ''[[Special:ChangePassword|change password]]''  panna pe badla jaae sake hai jab aap login karta hai.",
        "newarticle": "(Nawaa)",
        "newarticletext": "Aap ek panna ke jorr ke follow kara hae jon ki abhi nai hae.\nIi panna banae khatir, niche box me type karo (see the [$1 help page] for more info).\nAgar jo aap hian par galti se aae hai tab aapan browser ke '''back''' button pe click karo.",
-       "anontalkpagetext": "----''Ii salah kare waala panna uu anonymous sadasya ke baare me jon abhi account nai banais hai, nai to account ke kaam me nai lawe hai.\nIi kaaran se ham log ke IP address kaam me lae ke ii sadasya ke jaana jae hai.\n\nIi rakam ke IP address ke dher sadasya kaam me lae sake hai.\nAgar aap ek anonymous user hai aur ii sochta hai ki bekar baat aap ke baare me karaa gais hai, tab\n[[Special:UserLogin/signup|create an account]] or [[Special:UserLogin|log in]] aage ke garrbarri roke khatir aur duusra anonymous users se mistake nai kare ke khatir .''",
+       "anontalkpagetext": "----''Ii salah kare waala panna uu anonymous sadasya ke baare me jon abhi account nai banais hai, nai to account ke kaam me nai lawe hai.\nIi kaaran se ham log ke IP address kaam me lae ke ii sadasya ke jaana jae hai.\n\nIi rakam ke IP address ke dher sadasya kaam me lae sake hai.\nAgar aap ek anonymous user hai aur ii sochta hai ki bekar baat aap ke baare me karaa gais hai, tab\n[[Special:CreateAccount|create an account]] or [[Special:UserLogin|log in]] aage ke garrbarri roke khatir aur duusra anonymous users se mistake nai kare ke khatir .''",
        "noarticletext": "Abhi ii panna me kuchh likhaa nai hai.\nAap saktaa hai [[Special:Search/{{PAGENAME}}|ii panna ke title khoje]] duusra panna me,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs],\nnai to [{{fullurl:{{FULLPAGENAME}}|action=edit}} ii panna ke badlo]</span>.",
        "noarticletext-nopermission": "Abhi ii panna me koi chij likha nai hae.\nAap sakta hae [[Special:Search/{{PAGENAME}}|ii panna ke title ke khoje]] duusra panna me,\nnai to <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>, lekin aap ke ii panna ke banae ke ijaaja tnai hae.",
        "missing-revision": "Panna \"{{FULLPAGENAME}}\" me #$1 badlao nai hae.\nIske kaaran ii hoe sake hae ki ek mitawa gais panna se link karaa jaawe hae.\nIske baare me aur jaankari [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log] me paawa jaae sake hae.",
index 333b58c..1e7f53f 100644 (file)
        "noname": "Wala ka nagbutang sang tood-tood nga gamit-pangalan.",
        "loginsuccesstitle": "Madinalag-on nga pagsulod",
        "loginsuccess": "'''Nakasulod ka na subong sa {{SITENAME}} bilang si \"$1\".'''",
-       "nosuchuser": "Wala sang manug-gamit nga iya pangalan \"$1\".\nAng mga gamit-pangalan sensitibo sa kadakoon sang letra.\nSiguradoha ang pagbaybay sang pumolongon, ukon  [[Special:UserLogin/signup|magbuhat sang bag-o nga akawnt]].",
+       "nosuchuser": "Wala sang manug-gamit nga iya pangalan \"$1\".\nAng mga gamit-pangalan sensitibo sa kadakoon sang letra.\nSiguradoha ang pagbaybay sang pumolongon, ukon  [[Special:CreateAccount|magbuhat sang bag-o nga akawnt]].",
        "nosuchusershort": "Wala sang manug-gamit nga iya pangalan \"$1\".\nSiguradoha ang pagbaybay sang pumolongon.",
        "nouserspecified": "Kinahanglan mo magbutang sa gamit-pangalan.",
        "login-userblocked": "Ang ini nga manug-gamit ginapungga. Indi ka mahimo nga magsulod.",
        "accmailtext": "May pasword nga wala ginpilian nga ginhimo para kay [[User talk:$1|$1]] nga ginpadala sa $2.\n\nAng pasword para sa sini nga bag-o nga akawnt mahimo ma-ilisan sa ''[[Special:ChangePassword|ilisan ang pasword]]'' nga panid pagkatapos magsulod.",
        "newarticle": "(Bag-o)",
        "newarticletext": "Nagbukas ka sang isa ka tabid padulong sa isa ka panid nga wala pa nahimo.\nAgud mahimo ang panid, magsugod ka lang sa pagsulat sa sulod sang kahon nga makit-an mo sa idalum (tan-awa ang [$1 bulig nga pahina] para sa dugang nga ihibalo).\nUgaling kon ikaw nagtalang lamang diri, palihog lang tum-uka ang pityong nga <strong>balik</strong> sa imo nga palalayagan.",
-       "anontalkpagetext": "----''Ini ang panid para sa pagtalakay sa wala makilala-an nga manuggamit nga wala pa nakatuga sang akawnt, ukon wala nagagamit sang isa.\nAmo nga kinahanglan naton mag-gamit sang IP adres nga de numero agod nga mahibaluan naton siya.\nAng amo sini nga adres sang IP mahimo nga pagasaluhan sang madamo nga manuggamit.\nKon ikaw manuggamit nga wala makilal-i kag nabatyagan mo nga may mga komento nga wala man sing labot nga ginapakadto sa imo, palihog [[Special:UserLogin/signup|maghimo ka sang akawnt]] ukon [[Special:UserLogin|magsulod]] para malikawan ang iban pa nga pagsala sa iban pa nga wala makilal-an nga manuggamit.''",
+       "anontalkpagetext": "----''Ini ang panid para sa pagtalakay sa wala makilala-an nga manuggamit nga wala pa nakatuga sang akawnt, ukon wala nagagamit sang isa.\nAmo nga kinahanglan naton mag-gamit sang IP adres nga de numero agod nga mahibaluan naton siya.\nAng amo sini nga adres sang IP mahimo nga pagasaluhan sang madamo nga manuggamit.\nKon ikaw manuggamit nga wala makilal-i kag nabatyagan mo nga may mga komento nga wala man sing labot nga ginapakadto sa imo, palihog [[Special:CreateAccount|maghimo ka sang akawnt]] ukon [[Special:UserLogin|magsulod]] para malikawan ang iban pa nga pagsala sa iban pa nga wala makilal-an nga manuggamit.''",
        "noarticletext": "Wala unod ining panid.\nSarang ka [[Special:Search/{{PAGENAME}}|magpangita sining panig-ulo]] sa iban nga mga panid,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pangitaa sa nagakaangot nga pagkitan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} ilisan ining panid]</span>.",
        "noarticletext-nopermission": "Wala unod ining panid.\nSarang ka mag-[[Special:Search/{{PAGENAME}}|pangitaa ang ini nga panig-ulo]] sa iban nga mga panid,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pangitaa sa nagakaangot nga pagkitan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} ilisan ining panid]</span>, ugaling wala ka ginapahanugutan nga maghimo sining panid.",
        "missing-revision": "Ang ini nga pag-ilis nga #$1 sang panid nga ginhinanglan nga \"{{FULLPAGENAME}}\" wala naga-eksister.\n\nIni kalabanan ginabuhat sang nagasunod nga wala na mabag-o nga link sang hisayranay sa isa ka panid nga gindula na.\nAng mga detalye mahimo nga makita sa [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].",
        "import-rootpage-nosubpage": "Ang espasyo sang ngalan nga \"$1\" nga gingikanang panid indi ginapasugtan ang kaupod nga mga panid.",
        "importlogpage": "Listahan sang mga importe",
        "importlogpagetext": "Mga importeng administratibo sang mga panid nga may maragtas sang pagbag-o halin sa iban nga wiki.",
-       "javascripttest-pagetext-frameworks": "Palihug pilion ang isa sa mga masunod nga mga testing frameworks: $1",
-       "javascripttest-pagetext-skins": "Pilion ang isa ka panit para magdalagan sa imo nga eksamin:",
        "tooltip-pt-userpage": "Ang imo kaugalingon nga pahina",
        "tooltip-pt-anonuserpage": "Ang panid sang tiggamit para sa IP address imo ginbag-o bilang",
        "tooltip-pt-mytalk": "Ang imo pahina sang paghisayranay",
index fa1b19f..209dd30 100644 (file)
        "noname": "Niste unijeli valjano suradničko ime.",
        "loginsuccesstitle": "Prijava uspješna",
        "loginsuccess": "Prijavili ste se na wiki kao \"$1\".",
-       "nosuchuser": "Ne postoji suradnik s imenom \"$1\".\nSuradnička imena su osjetljiva na veličinu slova.\nProvjerite jeste li točno upisali, ili [[Special:UserLogin/signup|otvorite novi suradnički račun]].",
+       "nosuchuser": "Ne postoji suradnik s imenom \"$1\".\nSuradnička imena su osjetljiva na veličinu slova.\nProvjerite jeste li točno upisali, ili [[Special:CreateAccount|otvorite novi suradnički račun]].",
        "nosuchusershort": "Ne postoji suradnik s imenom \"$1\". Provjerite Vaš unos.",
        "nouserspecified": "Molimo navedite suradničko ime.",
        "login-userblocked": "Ovaj je suradnik blokiran. Prijava nije dopuštena.",
        "accmailtext": "Nova lozinka za [[User talk:$1|$1]] je poslana na $2.\n\nNakon prijave, lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|promijeni lozinku]]'' nakon prijave.",
        "newarticle": "(Novo)",
        "newarticletext": "Došli ste na stranicu koja još ne postoji.\nAko želite stvoriti tu stranicu, počnite tipkati u prozor ispod ovog teksta (pogledajte [$1 stranicu za pomoć]).\nAko ste ovamo dospjeli slučajno, kliknite gumb '''natrag''' (back) u svom pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor s neprijavljenim suradnikom koji još nije otvorio suradnički račun ili se njime ne koristi. Zbog toga se moramo služiti brojčanom IP adresom kako bismo ga identificirali. Takvu adresu često može dijeliti više ljudi. Ako ste neprijavljeni suradnik i smatrate da su Vam upućeni irelevantni komentari, molimo Vas da [[Special:UserLogin/signup|otvorite suradnički račun]] ili [[Special:UserLogin|se prijavite]] te tako u budućnosti izbjegnete zamjenu s drugim neprijavljenim suradnicima.''",
+       "anontalkpagetext": "----''Ovo je stranica za razgovor s neprijavljenim suradnikom koji još nije otvorio suradnički račun ili se njime ne koristi. Zbog toga se moramo služiti brojčanom IP adresom kako bismo ga identificirali. Takvu adresu često može dijeliti više ljudi. Ako ste neprijavljeni suradnik i smatrate da su Vam upućeni irelevantni komentari, molimo Vas da [[Special:CreateAccount|otvorite suradnički račun]] ili [[Special:UserLogin|se prijavite]] te tako u budućnosti izbjegnete zamjenu s drugim neprijavljenim suradnicima.''",
        "noarticletext": "Na ovoj stranici trenutačno nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|potražiti ovaj naslov]] na drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane evidencije]\nili [{{fullurl:{{FULLPAGENAME}}|action=edit}} stvoriti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Ova stranica nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane evidencije]</span>, ali ne možete stvoriti ovu stranicu.",
        "missing-revision": "Uređivanje broj $1 na stranici \"{{FULLPAGENAME}}\" ne postoji.\n\nOvo je obično uzrokovano kada kliknete na zastarjelu poveznicu na stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].",
        "upload-form-label-own-work": "Ovo je moje djelo",
        "upload-form-label-infoform-categories": "Kategorije",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-not-own-work-local-default": "Možete pokušati [[Special:Upload|postaviti datoteku na projektu {{SITENAME}}]], pod uvjetom da može biti tamo postavljena, sukladno pravilima projekta.",
-       "upload-form-label-own-work-message-shared": "Potvrđujem da posjedujem autorska prava ove datoteke i slažem se da je nepozivo postavljam na Zajednički poslužitelj pod licencijom  [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], i pristajem na [https://wikimediafoundation.org/wiki/Terms_of_Use Uvjete uporabe].",
-       "upload-form-label-not-own-work-message-shared": "Ako ne posjedujete autorska prava za ovu datoteku, ili je želite objaviti pod drugom licencijom, razmislite o uporabi [https://commons.wikimedia.org/wiki/Special:UploadWizard Čarobnjaka za postavljanje] na Zajedničkom poslužitelju.",
-       "upload-form-label-not-own-work-local-shared": "Možete pokušati [[Special:Upload|postaviti datoteku na projektu {{SITENAME}}]], pod uvjetom da je dopušteno postavljanje ove datoteke, sukladno pravilima projekta.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Možete pokušati [[Special:Upload|postaviti datoteku na projektu {{SITENAME}}]], pod uvjetom da može biti tamo postavljena, sukladno pravilima projekta.",
        "backend-fail-stream": "Ne mogu prikazati datoteku $1.",
        "backend-fail-backup": "Izrada sigurnosne kopije datoteke \"$1\" nije uspjela.",
        "backend-fail-notexists": "Datoteka $1 ne postoji.",
        "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|promjena|promjene|promjena}} od $2",
        "javascripttest": "Testiranje JavaScripta",
        "javascripttest-qunit-intro": "Pogledajte [$1 testnu dokumentaciju] na mediawiki.org.",
-       "tooltip-pt-userpage": "Stranica suradnika {{GENDER:|Your user}}",
+       "tooltip-pt-userpage": "Moja suradnička stranica",
        "tooltip-pt-anonuserpage": "Suradnička stranica za IP adresu pod kojom uređujete",
        "tooltip-pt-mytalk": "Vaša stranica za razgovor",
        "tooltip-pt-anontalk": "Razgovor o suradnicima s ove IP adrese",
        "mw-widgets-dateinput-placeholder-day": "GGGG-MM-DD",
        "mw-widgets-dateinput-placeholder-month": "GGGG-MM",
        "mw-widgets-titleinput-description-new-page": "stranica još ne postoji",
-       "mw-widgets-titleinput-description-redirect": "preusmjeravanje na $1"
+       "mw-widgets-titleinput-description-redirect": "preusmjeravanje na $1",
+       "log-action-filter-upload": "Vrsta postavljanja:",
+       "log-action-filter-all": "sve",
+       "log-action-filter-upload-upload": "novo postavljanje",
+       "log-action-filter-upload-overwrite": "ponovno postavljanje"
 }
index af3840c..4821bb6 100644 (file)
        "noname": "Du musst en gültiche Benutzernoome oongewe.",
        "loginsuccesstitle": "Oonmeldung erfollichgreich",
        "loginsuccess": "Du bist jetzt als \"$1\" bei {{SITENAME}} oongemeldt.",
-       "nosuchuser": "Der Benutzernoome „$1“ existiert net.\nÜwerprüf die Schreibweis (Gross-/Klenschreibung beachte) orrer [[Special:UserLogin/signup|meld dich als neier Benutzer an]].",
+       "nosuchuser": "Der Benutzernoome „$1“ existiert net.\nÜwerprüf die Schreibweis (Gross-/Klenschreibung beachte) orrer [[Special:CreateAccount|meld dich als neier Benutzer an]].",
        "nosuchusershort": "Der Benutzernoome \"$1\" ist net voarhand. Bitte üwerprüf die Schreibweis.",
        "nouserspecified": "Bittschön geb en Benutzernoome an.",
        "login-userblocked": "{{GENDER:$1|Der Benutzer|Die Benutzrin|Der Benutzer}} ist gesperrt. Die Oonmeldung ist net erlaubt.",
        "accmailtext": "En zufällich generiertes Passwort für [[User talk:$1|$1]] woard an $2 versandt. Es kann uff der Seit ''[[Special:ChangePassword|Passwort ännre]]'' noh der Oonmeldung geännert sin.",
        "newarticle": "(Nei)",
        "newarticletext": "Du bist en Link zu en Seit noh gang, wo net voarhand ist.\nUm die Seit oonzulehn, trooh dein Text in das unnestehend Beoorbeitungsfeld ren (weitre Informatione uff der [$1 Hellefseit]).\nSoweit du fälschlicherweise hier bist, klick uff dein Browser sein Schaltfläch '''Retuar'''.",
-       "anontalkpagetext": "----''Dies Seit dient dozu, enem net oongemeldete Benutzer Nachrichte zu hinnerlosse. Es weard sein IP-Adress zur Identifizierung verwenndt. IP-Adresse könne von mehrere Benutzer gemeinsam verwendt sin. Wenn du mit den Kommentare uff der Seit nix oonfänge kannst, richte die sich vermutlich an en frühre Inhaber von deiner IP-Adresse und du kannst se ignoriere. Du kannst dir ooch en [[Special:UserLogin/signup|Benutzerkonto erstelle]] orrer dich [[Special:UserLogin|oonmelde]], um künftich Verwechslunge mit annre anonyme Benutzer zu vermeide.''",
+       "anontalkpagetext": "----''Dies Seit dient dozu, enem net oongemeldete Benutzer Nachrichte zu hinnerlosse. Es weard sein IP-Adress zur Identifizierung verwenndt. IP-Adresse könne von mehrere Benutzer gemeinsam verwendt sin. Wenn du mit den Kommentare uff der Seit nix oonfänge kannst, richte die sich vermutlich an en frühre Inhaber von deiner IP-Adresse und du kannst se ignoriere. Du kannst dir ooch en [[Special:CreateAccount|Benutzerkonto erstelle]] orrer dich [[Special:UserLogin|oonmelde]], um künftich Verwechslunge mit annre anonyme Benutzer zu vermeide.''",
        "noarticletext": "Die Seit dohie enthält momentan noch ken Text.\nDu kannst sie <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} beoorbeite]</span>,\nehre Titel uff annre Seite [[Special:Search/{{PAGENAME}}|suche]]\norrer die zugehöriche <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachte]</span>.",
        "noarticletext-nopermission": "Die Seit dohie enthält momentan noch ken Text.\nDu kannst sie <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} beoorbeite]</span>,\nehre Titel uff annre Seite [[Special:Search/{{PAGENAME}}|suche]]\norrer die zugehöriche <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachte]</span>.",
        "missing-revision": "Die Version $1 von der Seit mit der noomen \"{{FULLPAGENAME}}\" ist net voarhand.\n\nDer Fehler weard normalerweis von enem veraltete Link zur Versionsgeschicht von en Seit verursacht, wo zwischichzeitlich gelöscht woard.\nEnzelheite sind im [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Lösch-Logbuch] einsehbar (visibel).",
index f8069bf..ac27ce7 100644 (file)
        "noname": "Njejsy płaćiwe wužiwarske mjeno podał.",
        "loginsuccesstitle": "Přizjewjenje wuspěšne",
        "loginsuccess": "'''Sy nětko jako \"$1\" w {{GRAMMAR:lokatiw|{{SITENAME}}}} {{GENDER:|přizjewjeny|přizjewjena|přizjewjene}}.'''",
-       "nosuchuser": "Njeje wužiwar z mjenom \"$1\".\nWužiwarske mjena wobkedźbuja wulkopisanje.\nPřepruwuj swój prawopis abo [[Special:UserLogin/signup|wutwor nowe konto]].",
+       "nosuchuser": "Njeje wužiwar z mjenom \"$1\".\nWužiwarske mjena wobkedźbuja wulkopisanje.\nPřepruwuj swój prawopis abo [[Special:CreateAccount|wutwor nowe konto]].",
        "nosuchusershort": "Wužiwarske mjeno „$1” njeeksistuje. Prošu skontroluj prawopis.",
        "nouserspecified": "Dyrbiš wužiwarske mjeno podać",
        "login-userblocked": "Tutón wužiwar je zablokowany. Přizjewjenje njedowolene.",
        "accmailtext": "Připadnje spłodźene hesło za [[User talk:$1|$1]] bu na $2 pósłane. Daj so na stronje ''[[Special:ChangePassword|hesło změnić]]'' při přizjewjenju změnić.",
        "newarticle": "(Nowy)",
        "newarticletext": "Sy wotkaz k stronje slědował, kotraž hišće njeeksistuje. Zo by stronu wutworił, wupjelń slědowace tekstowe polo (hlej [$1 stronu pomocy] za dalše informacije). Jeli sy zmylnje tu, klikń prosće na tłóčatko <b>Wróćo</b> we swojim wobhladowaku.",
-       "anontalkpagetext": "---- ''To je diskusijna strona za anonymneho wužiwarja, kiž hišće konto wutworił njeje abo je njewužiwa. Dyrbimy tohodla numerisku IP-adresu wužiwać, zo bychmy jeho/ju identifikowali. Tajka IP-adresa hodźi so wot wjacorych wužiwarjow zhromadnje wužiwać. Jeli sy anonymny wužiwar a měniš, zo buchu irelewantne komentary k tebi pósłane, [[Special:UserLogin/signup|wutwor prošu konto]] abo [[Special:UserLogin|přizjew so]], zo by přichodnu šmjatańcu z anonymnymi wužiwarjemi wobešoł.''",
+       "anontalkpagetext": "---- ''To je diskusijna strona za anonymneho wužiwarja, kiž hišće konto wutworił njeje abo je njewužiwa. Dyrbimy tohodla numerisku IP-adresu wužiwać, zo bychmy jeho/ju identifikowali. Tajka IP-adresa hodźi so wot wjacorych wužiwarjow zhromadnje wužiwać. Jeli sy anonymny wužiwar a měniš, zo buchu irelewantne komentary k tebi pósłane, [[Special:CreateAccount|wutwor prošu konto]] abo [[Special:UserLogin|přizjew so]], zo by přichodnu šmjatańcu z anonymnymi wužiwarjemi wobešoł.''",
        "noarticletext": "Tuchwilu tuta strona žadyn tekst njewobsahuje. Móžeš [[Special:Search/{{PAGENAME}}|tutón titul strony na druhich stronach pytać]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wotpowědne protokole pytać] abo [{{fullurl:{{FULLPAGENAME}}|action=edit}} tutu stronu wobdźěłać]</span>.",
        "noarticletext-nopermission": "Tuchwilu žadyn tekst na tutej stronje njeje.\nMóžeš [[Special:Search/{{PAGENAME}}|tutón titul strony]] na druhich stronach pytać abo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pytaj wotpowědne protokole]</span>, ale nimaš prawo, strou wutworić.",
        "missing-revision": "Wersija #$1 strony z mjenom \"{{FULLPAGENAME}}\" njeeksistuje.\n\nPřičina je zwjetša zestarjeny wotkaz w stawiznach k stronje, kotraž je so zhašała.\nPodrobnosće móžeš w  [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protokolu wušmórnjenjow] namakać.",
index 140f56a..193b5bb 100644 (file)
        "otherlanguages": "Nan lòt lang yo",
        "redirectedfrom": "(Redirije depi $1)",
        "redirectpagesub": "Paj pou redireksyon",
-       "redirectto": "Voye sou:",
+       "redirectto": "Redireksyon sou&nbsp;:",
        "lastmodifiedat": "Paj sa te modifye pou dènye fwa $1 a $2.<br />",
        "viewcount": "Paj sa te konsilte {{PLURAL:$1|yon fwa|$1 fwa}}.",
        "protectedpage": "Paj pwoteje",
        "noname": "Ou pa bay sistèm an yon non itilizatè ki bon.",
        "loginsuccesstitle": "Ou byen konekte nan sistèm la",
        "loginsuccess": "Ou konekte kounye a nan {{SITENAME}} ak idantifyan sa a « $1 ».",
-       "nosuchuser": "Itilizatè \"$1\" pa ekziste.\nMajiskil ak miniskil chanje non itilizatè.\nByen gade ke ou te byen ekri non ou, oubyen [[Special:UserLogin/signup|kreye yon nouvo kont]].",
+       "nosuchuser": "Itilizatè \"$1\" pa ekziste.\nMajiskil ak miniskil chanje non itilizatè.\nByen gade ke ou te byen ekri non ou, oubyen [[Special:CreateAccount|kreye yon nouvo kont]].",
        "nosuchusershort": "Pa genyen itilizatè ak non « $1 » sa a. Byen gade lòtograf ou an.",
        "nouserspecified": "Ou dwe mete non itilizatè ou an.",
        "login-userblocked": "Itilizatè sa bloke.  Li pa gendwa konekte.",
        "accmailtext": "Nou fè yon mopas o aza pou [[User talk:$1|$1]] epi nou voye l nan adrès $2.\nOu ka chanje mopas pou kont sa a nan paj ''[[Special:ChangePassword|chanje mopas]]'' aprè ou konekte ou.",
        "newarticle": "(Nouvo)",
        "newarticletext": "Ou swiv on lyen pou yon paj ki poko egziste.\nPou ou kapab kreye paj sa a, komanse ekri nan bwat ki anba (gade [$1 paj èd nan] pou konnen plis, pou plis enfòmasyon).\nSi se paske ou fè yon erè ke ou rive nan paj sa a, klike anlè bouton '''fè back''' nan navigatè ou a.",
-       "anontalkpagetext": "---- ''Ou nan paj diskisyon yon itilizatè anonim, ki pa gen non, ki poko kreye yon kont oubyen ki pa itilize pyès kont nan sistèm sa. Pou rezon sa, nou dwe itilize adrès IP l pou nou kapab lokalize l, sitye l, montre kote l rete, idantifye l. Yon adrès IP kapab pataje ant plizyè moun, plizyè itilizatè. Si ou se yon itilizatè anonim e si ou wè ke ou resevwa komantè ki pa t pou ou, ou mèt [[Special:UserLogin/signup|kreye yon kont]] oubyen [[Special:UserLogin|konekte ou]] pou ou kapab anpeche konfizyon ak kontribitè anonim yo.''",
+       "anontalkpagetext": "---- ''Ou nan paj diskisyon yon itilizatè anonim, ki pa gen non, ki poko kreye yon kont oubyen ki pa itilize pyès kont nan sistèm sa. Pou rezon sa, nou dwe itilize adrès IP l pou nou kapab lokalize l, sitye l, montre kote l rete, idantifye l. Yon adrès IP kapab pataje ant plizyè moun, plizyè itilizatè. Si ou se yon itilizatè anonim e si ou wè ke ou resevwa komantè ki pa t pou ou, ou mèt [[Special:CreateAccount|kreye yon kont]] oubyen [[Special:UserLogin|konekte ou]] pou ou kapab anpeche konfizyon ak kontribitè anonim yo.''",
        "noarticletext": "Poko genyen tèks nan paj sa a.\nOu mèt [[Special:Search/{{PAGENAME}}|fè yon rechèch, fouye ak non paj sa a]] nan lòt paj yo, oubyen <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chache jounal modifikasyon yo ki an relasyon ak paj sa] oubyen tou [{{fullurl:{{FULLPAGENAME}}|action=edit}} modifye paj sa]</span>.",
        "noarticletext-nopermission": "Poko genyen tèks nan paj sa a.\nOu mèt [[Special:Search/{{PAGENAME}}|fè yon rechèch, fouye ak non paj sa a]] nan lòt paj yo, oubyen <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chache jounal modifikasyon yo ki an relasyon ak paj sa].",
        "userpage-userdoesnotexist": "Kont itilizatè « <nowiki>$1</nowiki> » sa pa anrejistre. Verifye toutbon ke ou vle kreye paj sa.",
        "whatlinkshere-page": "Paj :",
        "linkshere": "Paj yo ki anba ap mene nan <b>[[:$1]]</b> :",
        "nolinkshere": "Pyès paj genyen lyen pou paj sa a <b>[[:$1]]</b>.",
-       "isredirect": "Paj redireksyon",
+       "isredirect": "paj redireksyon",
        "istemplate": "anndan",
        "isimage": "lyen fichye a",
        "whatlinkshere-prev": "{{PLURAL:$1|presedan|$1 presedan yo}}",
index 5e56627..f477ccb 100644 (file)
        "noname": "Érvénytelen szerkesztőnevet adtál meg.",
        "loginsuccesstitle": "Sikeres bejelentkezés",
        "loginsuccess": "'''Sikeresen bejelentkeztél a(z) {{SITENAME}} wikibe „$1” néven.'''",
-       "nosuchuser": "Nem létezik „$1” nevű szerkesztő.\nA szerkesztőnevek kis- és nagybetű-érzékenyek.\nEllenőrizd, hogy helyesen írtad-e be, vagy [[Special:UserLogin/signup|hozz létre egy új fiókot]].",
+       "nosuchuser": "Nem létezik „$1” nevű szerkesztő.\nA szerkesztőnevek kis- és nagybetű-érzékenyek.\nEllenőrizd, hogy helyesen írtad-e be, vagy [[Special:CreateAccount|hozz létre egy új fiókot]].",
        "nosuchusershort": "Nem létezik „$1” nevű szerkesztő.\nEllenőrizd, hogy helyesen írtad-e be.",
        "nouserspecified": "Meg kell adnod a felhasználói nevet.",
        "login-userblocked": "Ez a szerkesztő blokkolva van, a bejelentkezés nem engedélyezett.",
        "accmailtext": "A(z) [[User talk:$1|$1]] fiókhoz egy véletlenszerűen generált jelszót küldünk a(z) $2 címre.\n\nAz új fiók jelszava a ''[[Special:ChangePassword|jelszó megváltoztatása]]'' lapon módosítható a bejelentkezés után.",
        "newarticle": "(Új)",
        "newarticletext": "Egy olyan lapra mutató hivatkozást követtél, ami még nem létezik.\nA lap létrehozásához csak gépeld be a szövegét a lenti szövegdobozba. Ha kész vagy, az „Előnézet megtekintése” gombbal ellenőrizheted, hogy úgy fog-e kinézni, ahogy szeretnéd, és a „Lap mentése” gombbal tudod elmenteni. (További információkat a [$1 súgólapon] találsz).\nHa tévedésből jutottál ide, kattints a böngésződ '''vissza''' vagy '''back''' gombjára.",
-       "anontalkpagetext": "----''Ez egy olyan anonim szerkesztő vitalapja, aki még nem regisztrált, vagy csak nem jelentkezett be.\nEzért az IP-címét használjuk az azonosítására.\nUgyanazon az IP-címen számos szerkesztő osztozhat az idők folyamán.\nHa úgy látod, hogy az üzenetek, amiket ide kapsz, nem neked szólnak, [[Special:UserLogin/signup|regisztrálj]] vagy ha már regisztráltál, [[Special:UserLogin|jelentkezz be]], hogy ne keverjenek össze másokkal.''",
+       "anontalkpagetext": "----''Ez egy olyan anonim szerkesztő vitalapja, aki még nem regisztrált, vagy csak nem jelentkezett be.\nEzért az IP-címét használjuk az azonosítására.\nUgyanazon az IP-címen számos szerkesztő osztozhat az idők folyamán.\nHa úgy látod, hogy az üzenetek, amiket ide kapsz, nem neked szólnak, [[Special:CreateAccount|regisztrálj]] vagy ha már regisztráltál, [[Special:UserLogin|jelentkezz be]], hogy ne keverjenek össze másokkal.''",
        "noarticletext": "Ez a lap jelenleg nem tartalmaz szöveget.\n[[Special:Search/{{PAGENAME}}|Rákereshetsz erre a címszóra]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} megtekintheted a kapcsolódó naplókat],\nvagy [{{fullurl:{{FULLPAGENAME}}|action=edit}} létrehozhatod a lapot].</span>",
        "noarticletext-nopermission": "Ez a lap jelenleg nem tartalmaz szöveget.\n[[Special:Search/{{PAGENAME}}|Rákereshetsz a lap címére]] más lapok tartalmában, vagy <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} megtekintheted a kapcsolódó naplófájlokat]</span>.",
        "missing-revision": "A(z) \"{{FULLPAGENAME}}\" nevű oldal #$1 változata nem létezik.\n\nEzt általában egy elavult, törölt oldalra mutató laptörténeti hivatkozás használata okozza. Részletek a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} törlési naplóban] találhatóak.",
        "upload-form-label-own-work": "Ez a saját munkám",
        "upload-form-label-infoform-categories": "Kategóriák",
        "upload-form-label-infoform-date": "Dátum",
-       "upload-form-label-own-work-message-local": "Kijelentem, hogy a fájlt a(z) {{SITENAME}} következő felhasználási feltételei és licencirányelvei alapján töltöm fel.",
-       "upload-form-label-not-own-work-message-local": "Ha nem tudod feltölteni a fájlt a(z) {{SITENAME}} irányelvei értelmében, zárd be ezt a panelt és próbálkozz egy másik módszerrel.",
-       "upload-form-label-not-own-work-local-local": "Az [[Special:Upload|alapértelmezett feltöltőoldalt]] is kipróbálhatod.",
-       "upload-form-label-own-work-message-default": "Megértettem, hogy a megosztott tárhelyre töltöm fel a fájlt. Kijelentem, hogy az ottani felhasználási feltételek és licencirányelvek szerint teszem ezt.",
-       "upload-form-label-not-own-work-message-default": "Ha nem tudod feltölteni a fájlt a megosztott tárhely irányelvei értelmében, zárd be ezt a panelt és próbálkozz egy másik módszerrel.",
-       "upload-form-label-not-own-work-local-default": "Megpróbálhatod használni [[Special:Upload|a(z) {{SITENAME}} feltöltési lapját]] is, ha ezt a fájlt fel lehet tölteni az itteni irányelvek szerint.",
-       "upload-form-label-own-work-message-shared": "Igazolom, hogy én birtoklom a fájl szerzői jogait, és egyetértek azzal, hogy visszavonhatatlanul közzéteszem a fájlt a Wikimédia Commonson a [https://creativecommons.org/licenses/by-sa/4.0/deed.hu Creative Commons Nevezd meg! – Így add tovább! 4.0] licenc alatt, és egyetértek a [https://wikimediafoundation.org/wiki/Terms_of_Use felhasználási feltételekkel].",
-       "upload-form-label-not-own-work-message-shared": "Ha nem birtoklod a fájl szerzői jogait vagy más licenc alatt szeretnéd közzétenni, fontold meg a [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Feltöltésvarázslójának] használatát.",
-       "upload-form-label-not-own-work-local-shared": "Megpróbálhatod használni [[Special:Upload|a(z) {{SITENAME}} feltöltési lapját]] is, ha ezt a fájlt fel lehet tölteni az itteni irányelvek szerint.",
+       "upload-form-label-own-work-message-generic-local": "Kijelentem, hogy a fájlt a(z) {{SITENAME}} következő felhasználási feltételei és licencirányelvei alapján töltöm fel.",
+       "upload-form-label-not-own-work-message-generic-local": "Ha nem tudod feltölteni a fájlt a(z) {{SITENAME}} irányelvei értelmében, zárd be ezt a panelt és próbálkozz egy másik módszerrel.",
+       "upload-form-label-not-own-work-local-generic-local": "Az [[Special:Upload|alapértelmezett feltöltőoldalt]] is kipróbálhatod.",
+       "upload-form-label-own-work-message-generic-foreign": "Megértettem, hogy a megosztott tárhelyre töltöm fel a fájlt. Kijelentem, hogy az ottani felhasználási feltételek és licencirányelvek szerint teszem ezt.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Ha nem tudod feltölteni a fájlt a megosztott tárhely irányelvei értelmében, zárd be ezt a panelt és próbálkozz egy másik módszerrel.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Megpróbálhatod használni [[Special:Upload|a(z) {{SITENAME}} feltöltési lapját]] is, ha ezt a fájlt fel lehet tölteni az itteni irányelvek szerint.",
        "backend-fail-stream": "Nem sikerült sugározni ezt a fájlt: $1.",
        "backend-fail-backup": "Nem lehet elmenteni ezt a fájlt: $1.",
        "backend-fail-notexists": "Ez a fájl nem létezik: $1 .",
index 65e4c76..20a247b 100644 (file)
        "noname": "Դուք չեք նշել թույլատրելի մասնակցային անուն։",
        "loginsuccesstitle": "Բարեհաջող մուտք",
        "loginsuccess": "'''Դուք մուտք գործեցիք {{SITENAME}}, որպես \"$1\"։'''",
-       "nosuchuser": "$1 անունով մասնակից գոյություն չունի։\nՄասնակիցների անունները զգայուն են մեծատառերի նկատմամբ։\nՍտուգեք ձեր ուղղագրությունը կամ [[Special:UserLogin/signup|ստեղծեք նոր մասնակցի հաշիվ]]։",
+       "nosuchuser": "$1 անունով մասնակից գոյություն չունի։\nՄասնակիցների անունները զգայուն են մեծատառերի նկատմամբ։\nՍտուգեք ձեր ուղղագրությունը կամ [[Special:CreateAccount|ստեղծեք նոր մասնակցի հաշիվ]]։",
        "nosuchusershort": "$1 անունով մասնակից գոյություն չունի։ Ստուգեք ձեր ուղղագրությունը։",
        "nouserspecified": "Հարկավոր է նշել մասնակցային անուն։",
        "login-userblocked": "Այս մասնակիցը արգելափակված է: Մուտքը արգելված է:",
        "accmailtext": "[[User talk:$1|$1]] մասնակցի համար պատահական նշերից կազմված գաղտնաբառը ուղարկված է $2 հասցեին։\n\nՀամակարգ մուտք գործելուն պես կարող եք ''[[Special:ChangePassword|փոխել գաղտնաբառը]]''։",
        "newarticle": "(Նոր)",
        "newarticletext": "Դուք հղվել եք դեռևս գոյություն չունեցող էջի։ \nՆոր էջ ստեղծելու համար ստորև գտնվող խմբագրման դաշտում ավելացրեք տեքստ, այնուհետև սեղմեք '''Հիշել էջը''' (այցելեք [$1 օգնության էջը]՝ մանրամասն տեղեկությունների համար)։ \n\nԵթե դուք սխալմամբ եք այստեղ հայտնվել, ապա սեղմեք ձեր դիտարկչի '''հետ''' (back) կոճակը։",
-       "anontalkpagetext": "----\n''Այս քննարկման էջը պատկանում է անանուն մասնակցին, որը դեռ չի ստեղծել մասնակցային հաշիվ կամ չի մտել համակարգ մասնակցի անունով։''\nԱյդ իսկ պատճառով օգտագործվում է թվային IP-հասցեն։\nՆման IP-հասցեից կարող են օգտվել մի քանի մասնակիցներ։\nԵթե դուք անանուն մասնակից եք, բայց կարծում եք, որ ուրիշներին վերաբերող դիտողությունները արվում են ձեր հասցեով, ապա խնդրում ենք պարզապես [[Special:UserLogin/signup|գրանցվել]] կամ [[Special:UserLogin|մտնել համակարգ]], որպեսզի հետագայում ձեզ չշփոթեն այլ անանուն մասնակիցների հետ։",
+       "anontalkpagetext": "----\n''Այս քննարկման էջը պատկանում է անանուն մասնակցին, որը դեռ չի ստեղծել մասնակցային հաշիվ կամ չի մտել համակարգ մասնակցի անունով։''\nԱյդ իսկ պատճառով օգտագործվում է թվային IP-հասցեն։\nՆման IP-հասցեից կարող են օգտվել մի քանի մասնակիցներ։\nԵթե դուք անանուն մասնակից եք, բայց կարծում եք, որ ուրիշներին վերաբերող դիտողությունները արվում են ձեր հասցեով, ապա խնդրում ենք պարզապես [[Special:CreateAccount|գրանցվել]] կամ [[Special:UserLogin|մտնել համակարգ]], որպեսզի հետագայում ձեզ չշփոթեն այլ անանուն մասնակիցների հետ։",
        "noarticletext": "Ներկայումս այս էջում որևէ տեքստ չկա։\nԴուք կարող եք [[Special:Search/{{PAGENAME}}|որոնել այս անվանումը]] այլ էջերում, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} որոնել համապատասխան տեղեկամատյանները] կամ [{{fullurl:{{FULLPAGENAME}}|action=edit}} ստեղծել նոր էջ այս անվանմամբ]</span>։",
        "noarticletext-nopermission": "Ներկայումս այս էջում որևէ տեքստ չկա։\nԴուք կարող եք [[Special:Search/{{PAGENAME}}|որոնել այս անվանունը]] այլ էջերում կամ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} որոնել այն տեղեկամատյաններում]</span>։ Դուք չունեք թույլտվություն ստեղծել այս էջը։",
        "userpage-userdoesnotexist": "«<nowiki>$1</nowiki>» անվանմամբ մասնակից գոյություն չունի։\nԽնդրում ենք հավաստիանալ նրանում, թե արդյոք ուզում եք ստեղծել/խմբագրել այս էջը։",
        "sp-contributions-search": "Որոնել ներդրումները",
        "sp-contributions-username": "IP-հասցե կամ մասնակցի անուն.",
        "sp-contributions-toponly": "Ցույց տալ միայն այն խմբագրումները, որոնք վերջին փոփոխություն են",
-       "sp-contributions-newonly": "Õ\91Õ¸Ö\82ÕµÖ\81 Õ¿Õ¡Õ¬ Õ´Õ«Õ¡ÕµÕ¶ Õ¡ÕµÕ¶ Õ­Õ´Õ¢Õ¡Õ£Ö\80Õ¸Ö\82Õ´Õ¶Õ¥Ö\80Õ¨, Õ¸Ö\80Õ¸Õ¶Ö\84 Õ§Õ» Õ¥Õ¶ Õ½Õ¿Õ¥Õ²Õ®Õ¥Õ¬",
+       "sp-contributions-newonly": "Õ\91Õ¸Ö\82ÕµÖ\81 Õ¿Õ¡Õ¬ Õ´Õ«Õ¡ÕµÕ¶ Õ¶Õ¸Ö\80 Õ§Õ»Õ¥Ö\80Õ« Õ­Õ´Õ¢Õ¡Õ£Ö\80Õ¸Ö\82Õ´Õ¶Õ¥Ö\80Õ¨",
        "sp-contributions-submit": "Որոնել",
        "whatlinkshere": "Այստեղ հղվող էջերը",
        "whatlinkshere-title": "Էջեր, որոնք հղում են դեպի «$1»",
index 0f60d82..ca80da8 100644 (file)
@@ -32,6 +32,7 @@
        "tog-watchdefault": "Adder le paginas e files que io modifica a mi observatorio",
        "tog-watchmoves": "Adder le paginas e files que io renomina a mi observatorio",
        "tog-watchdeletion": "Adder le paginas e files que io dele a mi observatorio",
+       "tog-watchuploads": "Adder le nove files que io incarga a mi observatorio",
        "tog-watchrollback": "Adder a mi observatorio le paginas in que io ha effectuate un revocation",
        "tog-minordefault": "Marcar omne modificationes initialmente como minor",
        "tog-previewontop": "Monstrar previsualisation ante le quadro de modification",
@@ -56,7 +57,7 @@
        "tog-ccmeonemails": "Inviar me copias del messages de e-mail que io invia a altere usatores",
        "tog-diffonly": "Non monstrar le contento del pagina sub le comparation de duo versiones",
        "tog-showhiddencats": "Monstrar categorias celate",
-       "tog-norollbackdiff": "Omitter le diff post le execution de un revocation",
+       "tog-norollbackdiff": "Non monstrar differentias post exequer un revocation",
        "tog-useeditwarning": "Advertir me quando io quita un pagina de modification sin publicar le cambiamentos",
        "tog-prefershttps": "Sempre usar un connexion secur in session aperte",
        "underline-always": "Sempre",
        "noname": "Tu non specificava un nomine de usator valide.",
        "loginsuccesstitle": "Session aperite",
        "loginsuccess": "'''Tu es ora authenticate in {{SITENAME}} como \"$1\".'''",
-       "nosuchuser": "Non existe un usator con le nomine \"$1\".\nIn le nomines de usator se distingue inter majusculas e minusculas.\nVerifica le orthographia, o [[Special:UserLogin/signup|crea un nove conto]].",
+       "nosuchuser": "Non existe un usator con le nomine \"$1\".\nIn le nomines de usator se distingue inter majusculas e minusculas.\nVerifica le orthographia, o [[Special:CreateAccount|crea un nove conto]].",
        "nosuchusershort": "Non existe un usator con le nomine \"$1\".\nVerifica le orthographia.",
        "nouserspecified": "Tu debe specificar un nomine de usator.",
        "login-userblocked": "Iste usator es blocate. Apertura de session non permittite.",
        "minoredit": "Isto es un modification minor",
        "watchthis": "Observar iste pagina",
        "savearticle": "Publicar pagina",
+       "publishpage": "Publicar pagina",
        "preview": "Previsualisation",
        "showpreview": "Monstrar previsualisation",
        "showdiff": "Detaliar modificationes",
        "accmailtext": "Un contrasigno generate aleatorimente pro [[User talk:$1|$1]] ha essite inviate a $2. Illo pote esser cambiate in le pagina ''[[Special:ChangePassword|Cambiar contrasigno]]'' post que tu ha aperite un session.",
        "newarticle": "(Nove)",
        "newarticletext": "Tu ha sequite un ligamine verso un pagina que non existe ancora.\nPro crear iste pagina, comencia a scriber in le quadro infra (consulta le [$1 pagina de adjuta] pro plus informationes).\nSi tu ha arrivate a iste pagina per error, clicca le button '''Retornar''' de tu navigator.",
-       "anontalkpagetext": "---- ''Isto es le pagina de discussion pro un usator anonyme qui non ha ancora create un conto, o qui non lo usa. Consequentemente nos debe usar le adresse IP numeric pro identificar le/la.\nUn tal adresse IP pote esser usate in commun per varie personas.\nSi tu es un usator anonyme e pensa que commentos irrelevante ha essite dirigite a te, per favor [[Special:UserLogin/signup|crea un conto]] o [[Special:UserLogin|aperi un session]] pro evitar futur confusiones con altere usatores anonyme.''",
+       "anontalkpagetext": "---- ''Isto es le pagina de discussion pro un usator anonyme qui non ha ancora create un conto, o qui non lo usa. Consequentemente nos debe usar le adresse IP numeric pro identificar le/la.\nUn tal adresse IP pote esser usate in commun per varie personas.\nSi tu es un usator anonyme e pensa que commentos irrelevante ha essite dirigite a te, per favor [[Special:CreateAccount|crea un conto]] o [[Special:UserLogin|aperi un session]] pro evitar futur confusiones con altere usatores anonyme.''",
        "noarticletext": "Al momento il non ha texto in iste pagina.\nTu pote [[Special:Search/{{PAGENAME}}|cercar le titulo de iste pagina]] in altere paginas,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercar in le registros pertinente],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear iste pagina]</span>.",
        "noarticletext-nopermission": "In iste momento il non ha texto in iste pagina.\nTu pote [[Special:Search/{{PAGENAME}}|cercar le titulo de iste pagina]] in altere paginas,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercar in le registros pertinente], ma tu non ha le permission de crear iste pagina.</span>",
        "missing-revision": "Le version №$1 del pagina nominate \"{{FULLPAGENAME}}\" non existe.\n\nIsto es generalmente causate per sequer un ligamine de historia obsolete a un pagina que ha essite delite.\nDetalios se trova in le [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de deletiones].",
        "userpage-userdoesnotexist": "Le conto de usator \"<nowiki>$1</nowiki>\" non es registrate. Per favor verifica que tu vole crear/modificar iste pagina.",
        "userpage-userdoesnotexist-view": "Le conto de usator \"$1\" non es registrate.",
        "blocked-notice-logextract": "Iste usator es actualmente blocate.\nLe ultime entrata del registro de blocadas es reproducite ci infra pro information:",
-       "clearyourcache": "'''Nota:''' Post confirmar, il pote esser necessari refrescar le ''cache'' de tu navigator pro vider le cambiamentos.\n* '''Firefox / Safari:''' Tenente ''Shift'' clicca ''Reload (Recargar)'', o preme ''Ctrl-F5'' o ''Ctrl-R'' (''⌘-R'' sur Mac)\n* '''Google Chrome:''' Preme ''Ctrl-Shift-R'' (''⌘-Shift-R'' sur Mac)\n* '''Internet Explorer:''' Tenente ''Ctrl'' clicca ''Refresh (Refrescar)'', o preme ''Ctrl-F5'' \n* '''Opera:''' Vacua le ''cache'' in ''Tools → Preferences (Utensiles → Preferentias)''",
+       "clearyourcache": "<strong>Nota:</strong> Post confirmar, il pote esser necessari refrescar le <em>cache</em> de tu navigator pro vider le cambiamentos.\n* <strong>Firefox / Safari:</strong> Tenente <em>Shift</em> clicca <em>Reload (Recargar)</em>, o preme <em>Ctrl-F5</em> o <em>Ctrl-R</em> (<em>⌘-R</em> sur Mac)\n* <strong>Google Chrome:</strong> Preme <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> sur Mac)\n* <strong>Internet Explorer:</strong> Tenente <em>Ctrl</em> clicca <em>Refresh (Refrescar)</em>, o preme <em>Ctrl-F5</em> \n* <strong>Opera:</strong> Vade a <em>Menu → Configurationes</em> (<em>Opera → Preferentias</em> sur un Mac) e alora a <em>Privacy & securitate → Rader datos de navigation → Files e imagines in cache</em>.",
        "usercssyoucanpreview": "'''Consilio:''' Usa le button \"{{int:showpreview}}\" pro testar tu nove CSS ante de salveguardar lo.",
        "userjsyoucanpreview": "'''Consilio:''' Usa le button \"{{int:showpreview}}\" pro testar tu nove JavaScript ante de salveguardar lo.",
        "usercsspreview": "'''Non oblida que isto es solmente un previsualisation de tu CSS personalisate.'''\n'''Le modificationes non ha ancora essite salveguardate!'''",
        "right-override-export-depth": "Exportar paginas includente paginas ligate usque a un profunditate de 5",
        "right-sendemail": "Inviar e-mail a altere usatores",
        "right-passwordreset": "Vider le e-mails pro reinitialisar le contrasigno",
-       "right-managechangetags": "Crear e deler [[Special:Tags|etiquettas]] in le base de datos",
+       "right-managechangetags": "Crear e (de)activar [[Special:Tags|etiquettas]]",
        "right-applychangetags": "Applicar [[Special:Tags|etiquettas]] al proprie modificationes",
        "right-changetags": "Adder e remover qualcunque [[Special:Tags|etiquettas]] sur individual versiones e entratas de registro",
+       "right-deletechangetags": "Deler [[Special:Tags|etiquettas]] del base de datos",
        "grant-generic": "Gruppo de derectos \"$1\"",
        "grant-group-page-interaction": "Interager con paginas",
        "grant-group-file-interaction": "Interager con multimedia",
        "action-viewmyprivateinfo": "vider le proprie information private",
        "action-editmyprivateinfo": "modificar le proprie information private",
        "action-editcontentmodel": "modificar le modello de contento de un pagina",
-       "action-managechangetags": "crear e deler etiquettas in le base de datos",
+       "action-managechangetags": "crear e (de)activar etiquettas",
        "action-applychangetags": "applicar etiquettas al proprie modificationes",
        "action-changetags": "adder e remover qualcunque etiquettas sur individual versiones e entratas de registro",
+       "action-deletechangetags": "deler etiquettas del base de datos",
        "nchanges": "$1 {{PLURAL:$1|modification|modificationes}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde le ultime visita}}",
        "enhancedrc-history": "historia",
        "recentchangeslinked-page": "Nomine del pagina:",
        "recentchangeslinked-to": "Monstrar modificationes in paginas con ligamines al pagina specificate",
        "recentchanges-page-added-to-category": "[[:$1]] addite al categoria",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] e [[Special:WhatLinksHere/$1|{{PLURAL:$2|un pagina|$2 paginas}}]] addite al categoria",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] addite al categoria, [[Special:WhatLinksHere/$1|iste pagina es includite in altere paginas]]",
        "recentchanges-page-removed-from-category": "[[:$1]] removite del categoria",
-       "recentchanges-page-removed-from-category-bundled": "[[:$1]] e [[Special:WhatLinksHere/$1|{{PLURAL:$2|un pagina|$2 paginas}}]] removite del categoria",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] removite del categoria, [[Special:WhatLinksHere/$1|iste pagina es includite in altere paginas]]",
        "autochange-username": "Cambiamento automatic de MediaWiki",
        "upload": "Incargar file",
        "uploadbtn": "Incargar file",
        "upload-form-label-own-work": "Iste es mi proprie obra",
        "upload-form-label-infoform-categories": "Categorias",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Io confirma que io incarga iste file secundo le conditiones de servicio e politicas de licentia de {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Si tu non pote incargar iste file in concordantia con le politicas de {{SITENAME}}, per favor claude iste dialogo e essaya un altere methodo.",
-       "upload-form-label-not-own-work-local-local": "Tu pote etiam essayar [[Special:Upload|le pagina de incargamento normal]].",
-       "upload-form-label-own-work-message-default": "Io comprende que io incarga iste file in un repositorio commun. Io confirma que io lo face secundo le conditiones de servicio e politicas de licentia de illo.",
-       "upload-form-label-not-own-work-message-default": "Si tu non pote incargar iste file in concordantia con le politicas del repositorio commun, per favor claude iste dialogo e essaya un altere methodo.",
-       "upload-form-label-not-own-work-local-default": "Tu pote anque probar [[Special:Upload|le pagina de incargamento in {{SITENAME}}]], si le politicas de ille sito permitte incargar iste file.",
-       "upload-form-label-own-work-message-shared": "Io certifica que io possede le derecto de autor sur iste file, io consenti le publication irrevocabile de iste file a Wikimedia Commons sub le licentia [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], e io accepta le [https://wikimediafoundation.org/wiki/Terms_of_Use conditiones de uso].",
-       "upload-form-label-not-own-work-message-shared": "Si tu non possede le derecto de autor sur iste file, o si tu prefere publicar lo sub un altere licentia, considera usar le [https://commons.wikimedia.org/wiki/Special:UploadWizard assistente de incargamento de Commons].",
-       "upload-form-label-not-own-work-local-shared": "Tu pote anque probar [[Special:Upload|le pagina de incargamento in {{SITENAME}}]], si le politicas de ille sito permitte incargar iste file.",
+       "upload-form-label-own-work-message-generic-local": "Io confirma que io incarga iste file secundo le conditiones de servicio e politicas de licentia de {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Si tu non pote incargar iste file in concordantia con le politicas de {{SITENAME}}, per favor claude iste dialogo e essaya un altere methodo.",
+       "upload-form-label-not-own-work-local-generic-local": "Tu pote etiam essayar [[Special:Upload|le pagina de incargamento normal]].",
+       "upload-form-label-own-work-message-generic-foreign": "Io comprende que io incarga iste file in un repositorio commun. Io confirma que io lo face secundo le conditiones de servicio e politicas de licentia de illo.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Si tu non pote incargar iste file in concordantia con le politicas del repositorio commun, per favor claude iste dialogo e essaya un altere methodo.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Tu pote anque probar [[Special:Upload|le pagina de incargamento in {{SITENAME}}]], si le politicas de ille sito permitte incargar iste file.",
        "backend-fail-stream": "Non poteva transmitter le file $1.",
        "backend-fail-backup": "Non poteva facer un copia de reserva del file $1.",
        "backend-fail-notexists": "Le file $1 non existe.",
        "changecontentmodel-success-text": "Le typo de contento de [[:$1]] ha essite cambiate.",
        "changecontentmodel-cannot-convert": "Le contento de [[:$1]] non pote esser convertite a un typo de $2.",
        "changecontentmodel-nodirectediting": "Le modello de contento $1 non supporta le modification directe",
+       "changecontentmodel-emptymodels-title": "Nulle modello de contento disponibile",
+       "changecontentmodel-emptymodels-text": "Le contento in [[:$1]] non pote esser convertite in alcun typo.",
        "log-name-contentmodel": "Registro de cambiamentos de modello de contento",
        "log-description-contentmodel": "Eventos relative al modellos de contento de un pagina",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|creava}} le pagina $3 con le modello de contento non predefinite \"$5\"",
        "whatlinkshere-prev": "{{PLURAL:$1|precedente|precedente $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|sequente|sequente $1}}",
        "whatlinkshere-links": "← ligamines",
-       "whatlinkshere-hideredirs": "$1 redirectiones",
-       "whatlinkshere-hidetrans": "$1 transclusiones",
-       "whatlinkshere-hidelinks": "$1 ligamines",
-       "whatlinkshere-hideimages": "$1 le ligamines a files",
+       "whatlinkshere-hideredirs": "Celar redirectiones",
+       "whatlinkshere-hidetrans": "Celar transclusiones",
+       "whatlinkshere-hidelinks": "Celar ligamines",
+       "whatlinkshere-hideimages": "Celar le ligamines a files",
        "whatlinkshere-filters": "Filtros",
        "whatlinkshere-submit": "Va",
        "autoblockid": "Auto-blocada №$1",
        "lockdbsuccesstext": "Le base de datos de {{SITENAME}} ha essite blocate.\n<br />Rememora te de disblocar lo post completar tu mantenentia.",
        "unlockdbsuccesstext": "Le base de datos de {{SITENAME}} ha essite disblocate.",
        "lockfilenotwritable": "Impossibile scriber al file de blocada del base de datos.\nPro blocar o disblocar le base de datos, le servitor web debe poter scriber a iste file.",
+       "databaselocked": "Le base de datos es jam blocate.",
        "databasenotlocked": "Le base de datos non es blocate.",
        "lockedbyandtime": "(per $1 le $2 a $3)",
        "move-page": "Renominar $1",
        "tooltip-ca-nstab-category": "Vider le pagina del categoria",
        "tooltip-minoredit": "Marcar iste modification como minor",
        "tooltip-save": "Confirmar tu modificationes",
+       "tooltip-publish": "Publicar tu cambiamentos",
        "tooltip-preview": "Per favor verifica tu modificationes ante que tu los publica!",
        "tooltip-diff": "Detaliar le modificationes que tu ha facite in le texto.",
        "tooltip-compareselectedversions": "Vider le differentias inter le seligite duo versiones de iste pagina.",
        "confirmemail_body_set": "Un persona, probabilemente tu, usante le adresse IP $1,\nha specificate que iste adresse de e-mail pertine al conto \"$2\" in {{SITENAME}}.\n\nPro confirmar que iste conto vermente pertine a te, e pro activar le functionalitate\nde e-mail in {{SITENAME}}, visita iste ligamine in tu navigator:\n\n$3\n\nSi le conto *non* pertine a te, seque iste ligamine\npro cancellar le confirmation del adresse de e-mail:\n\n$5\n\nIste codice de confirmation expirara le $6 a $7.",
        "confirmemail_invalidated": "Confirmation del adresse de e-mail cancellate",
        "invalidateemail": "Cancellar confirmation del adresse de e-mail",
+       "notificationemail_subject_changed": "Le adresse de e-mail registrate sur {{SITENAME}} ha essite cambiate",
+       "notificationemail_subject_removed": "Le adresse de e-mail registrate sur {{SITENAME}} ha essite removite",
+       "notificationemail_body_changed": "Qualcuno, probabilemente tu, ab le adresse IP $1, ha cambiate le adresse de e-mail del conto \"$2\" in \"$3\" sur {{SITENAME}}.\n\nSi isto non esseva tu, contacta immediatemente un administrator del sito.",
+       "notificationemail_body_removed": "Qualcuno, probabilemente tu, ab le adresse IP $1, ha removite le adresse de e-mail del conto \"$2\" sur {{SITENAME}}.\n\nSi isto non esseva tu, contacta immediatemente un administrator del sito.",
        "scarytranscludedisabled": "[Le transclusion interwiki es disactivate]",
        "scarytranscludefailed": "[Falleva de obtener le patrono pro $1]",
        "scarytranscludefailed-httpstatus": "[Obtention de patrono fallite pro $1: HTTP $2]",
        "watchlistedit-raw-done": "Tu observatorio ha essite actualisate.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 titulo|$1 titulos}} ha essite addite:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 titulo|$1 titulos}} ha essite removite:",
-       "watchlistedit-clear-title": "Observatorio radite",
+       "watchlistedit-clear-title": "Rader observatorio",
        "watchlistedit-clear-legend": "Rader observatorio",
        "watchlistedit-clear-explain": "Tote le titulos essera removite de tu observatorio",
        "watchlistedit-clear-titles": "Titulos:",
        "timezone-local": "Local",
        "duplicate-defaultsort": "Attention: Le clave de ordination predefinite \"$2\" supplanta le anterior clave de ordination predefinite \"$1\".",
        "duplicate-displaytitle": "<strong>Attention:</strong> Le titulo a monstrar \"$2\" supplanta le ancian titulo a monstrar \"$1\".",
+       "restricted-displaytitle": "<strong>Attention:</strong> Le titulo a monstrar \"$1\" ha essite ignorate perque illo non es equivalente al titulo real del pagina.",
        "invalid-indicator-name": "<strong>Error:</strong> Le attributo <code>name</code> del indicatores del stato del pagina non pote esser vacue.",
        "version": "Version",
        "version-extensions": "Extensiones installate",
        "version-libraries-description": "Description",
        "version-libraries-authors": "Autores",
        "redirect": "Rediriger per ID de file, usator, pagina, version o registro",
-       "redirect-summary": "Iste pagina special redirige a un file (si es date le nomine de un file), a un pagina (si es date un ID de version o ID de pagina) o a un pagina de usator (si es date un ID de usator numeric). Usage: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]] o [[{{#Special:Redirect}}/user/101]].",
+       "redirect-summary": "Iste pagina special redirige a un file (si es date le nomine de un file), a un pagina (si es date un ID de version o ID de pagina), a un pagina de usator (si es date un ID de usator numeric) o a un entrata de registro (si es date le ID de un registro). Usage: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] o [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Va",
        "redirect-lookup": "Cercar:",
        "redirect-value": "Valor:",
        "redirect-page": "ID del pagina",
        "redirect-revision": "Version de pagina",
        "redirect-file": "Nomine de file",
+       "redirect-logid": "ID de registro",
        "redirect-not-exists": "Valor non trovate",
        "fileduplicatesearch": "Cercar files duplicate",
        "fileduplicatesearch-summary": "Cercar files duplicate a base de lor summas de verification ''(hash).''",
        "tags-deactivate": "disactivar",
        "tags-hitcount": "$1 {{PLURAL:$1|modification|modificationes}}",
        "tags-manage-no-permission": "Tu non ha le permission de gerer le etiquettas de modification.",
+       "tags-manage-blocked": "Tu non pote gerer etiquettas de cambiamento durante que tu es blocate.",
        "tags-create-heading": "Crear un nove etiquetta",
        "tags-create-explanation": "Per configuration predefinite, le etiquettas novemente create essera disponibile pro le uso per usatores e robots.",
        "tags-create-tag-name": "Nomine del etiquetta:",
        "tags-delete-not-found": "Le etiquetta \"$1\" non existe.",
        "tags-delete-too-many-uses": "Le etiquetta \"$1\" es applicate a plus de $2 {{PLURAL:$2|version|versiones}}, e per isto non pote esser delite.",
        "tags-delete-warnings-after-delete": "Le etiquetta \"$1\" ha essite delite, ma le sequente {{PLURAL:$2|advertimento|advertimentos}} ha essite incontrate:",
+       "tags-delete-no-permission": "Tu non ha le permission de deler etiquettas de modification.",
        "tags-activate-title": "Activar etiquetta",
        "tags-activate-question": "Tu es sur le puncto de activar le etiquetta \"$1\".",
        "tags-activate-reason": "Motivo:",
        "tags-deactivate-not-allowed": "Non es possibile disactivar le etiquetta \"$1\".",
        "tags-deactivate-submit": "Disactivar",
        "tags-apply-no-permission": "Tu non ha le permission de adjunger etiquettas de cambiamento a tu cambiamentos.",
+       "tags-apply-blocked": "Tu non pote applicar etiquettas de cambiamento con tu cambiamentos durante que tu es blocate.",
        "tags-apply-not-allowed-one": "Non es permittite applicar le etiquetta \"$1\" manualmente.",
        "tags-apply-not-allowed-multi": "Le sequente {{PLURAL:$2|etiquetta|etiquettas}} non es autorisate a esser manualmente applicate: $1",
        "tags-update-no-permission": "Tu non ha le permission de adder o remover etiquettas de cambiamento sur individual versiones o entratas de registro.",
+       "tags-update-blocked": "Tu non pote adder o remover etiquettas de cambiamento durante que tu es blocate.",
        "tags-update-add-not-allowed-one": "Non es permittite adjunger le etiquetta \"$1\" manualmente.",
        "tags-update-add-not-allowed-multi": "Le sequente {{PLURAL:$2|etiquetta|etiquettas}} non es autorisate a esser manualmente adjungite: $1",
        "tags-update-remove-not-allowed-one": "Non es permittite remover le etiquetta \"$1\".",
        "tags-edit-revision-legend": "Adder o remover etiquettas de {{PLURAL:$1|iste version|tote le $1 versiones}}",
        "tags-edit-logentry-legend": "Adder o remover etiquettas de {{PLURAL:$1|iste entrata|tote le $1 entratas}} de registro",
        "tags-edit-existing-tags": "Etiquettas existente:",
-       "tags-edit-existing-tags-none": "\"Nulle\"",
+       "tags-edit-existing-tags-none": "<em>Nulle</em>",
        "tags-edit-new-tags": "Nove etiquettas:",
        "tags-edit-add": "Adder iste etiquettas:",
        "tags-edit-remove": "Remover iste etiquettas:",
        "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protegeva}} $3 $4 [in cascada]",
        "logentry-protect-modify": "$1 {{GENDER:$2|cambiava}} le nivello de protection de $3 $4",
        "logentry-protect-modify-cascade": "$1 {{GENDER:$2|cambiava}} le nivello de protection de $3 $4 [in cascada]",
-       "logentry-rights-rights": "$1 {{GENDER:$2|cambiava}} le appertinentia a gruppos pro $3 de $4 a $5",
+       "logentry-rights-rights": "$1 {{GENDER:$2|cambiava}} le appertinentia a gruppos pro {{GENDER:$6|$3}} de $4 a $5",
        "logentry-rights-rights-legacy": "$1 {{GENDER:$2|cambiava}} le appertinentia a gruppos pro $3",
        "logentry-rights-autopromote": "$1 ha essite automaticamente {{GENDER:$2|promovite}} de $4 a $5",
        "logentry-upload-upload": "$1 {{GENDER:$2|ha incargate}} $3",
        "feedback-useragent": "Agente usator:",
        "searchsuggest-search": "Cercar",
        "searchsuggest-containing": "continente...",
+       "api-error-autoblocked": "Tu adresse IP ha essite blocate automaticamente, perque illo ha essite usate per un usator blocate.",
        "api-error-badaccess-groups": "Tu non ha le permission de incargar files in iste wiki.",
        "api-error-badtoken": "Error interne: indicio invalide.",
+       "api-error-blocked": "Le modification ha essite blocate pro te.",
        "api-error-copyuploaddisabled": "Le incargamentos per URL es disactivate in iste servitor.",
        "api-error-duplicate": "Existe jam {{PLURAL:$1|un altere file|altere files}} in le wiki con le mesme contento.",
        "api-error-duplicate-archive": "Il habeva jam {{PLURAL:$1|un altere file|altere files}} in le sito con le mesme contento, ma {{PLURAL:$1|illo|illos}} ha essite delite.",
        "api-error-nomodule": "Error interne: nulle modulo de incargamento definite.",
        "api-error-ok-but-empty": "Error interne: nulle responsa del servitor.",
        "api-error-overwrite": "Superscriber un file existente non es permittite.",
+       "api-error-ratelimited": "Tu tenta incargar plus files in curte tempore que iste wiki permitte.\nPer favor, reproba in alcun minutas.",
        "api-error-stashfailed": "Error interne: le servitor non poteva immagazinar le file temporari.",
        "api-error-publishfailed": "Error interne: le servitor non poteva publicar le file temporari.",
        "api-error-stasherror": "Un error ha occurrite durante le incargamento del file in \"stash\".",
        "api-error-unknownerror": "Error incognite: \"$1\".",
        "api-error-uploaddisabled": "Le incargamento es disactivate in iste wiki.",
        "api-error-verification-error": "Le file pote esser corrumpite o su nomine pote haber un extension errate.",
+       "api-error-was-deleted": "Un file con iste nomine ha jam essite incargate e postea delite.",
        "duration-seconds": "$1 {{PLURAL:$1|secunda|secundas}}",
        "duration-minutes": "$1 {{PLURAL:$1|minuta|minutas}}",
        "duration-hours": "$1 {{PLURAL:$1|hora|horas}}",
        "expand_templates_generate_xml": "Monstrar arbore syntactic XML",
        "expand_templates_generate_rawhtml": "Monstrar HTML brute",
        "expand_templates_preview": "Previsualisation",
-       "expand_templates_preview_fail_html": "<em>Perque {{SITENAME}} ha HTML crude activate e il habeva un perdita de datos de session, le previsualisation es celate como precaution contra attaccos con JavaScript.</em>\n\n<strong>Si isto es un tentativa de previsualisation legitime, per favor essaya lo de novo.</strong>\nSi illo ancora non functiona, essaya [[Special:UserLogout|clauder le session]] e aperir un nove session.",
+       "expand_templates_preview_fail_html": "<em>Perque {{SITENAME}} ha HTML crude activate e il habeva un perdita de datos de session, le previsualisation es celate como precaution contra attaccos con JavaScript.</em>\n\n<strong>Si isto es un tentativa de previsualisation legitime, per favor essaya lo de novo.</strong>\nSi illo ancora non functiona, essaya [[Special:UserLogout|clauder le session]] e aperir un nove session, e verifica que tu navigator permitte le cookies de iste sito.",
        "expand_templates_preview_fail_html_anon": "<em>Perque {{SITENAME}} ha HTML crude activate e tu non ha aperite session, le previsualisation es celate como precaution contra attaccos con JavaScript.</em>\n\n<strong>Si isto es un tentativa de previsualisation legitime, per favor [[Special:UserLogin|aperi session]] e essaya lo de novo.</strong>",
-       "pagelanguage": "Selector de lingua de pagina",
+       "expand_templates_input_missing": "Tu debe scriber alcun texto de entrata.",
+       "pagelanguage": "Cambiar lingua del pagina",
        "pagelang-name": "Pagina",
        "pagelang-language": "Lingua",
        "pagelang-use-default": "Usar lingua predefinite",
        "pagelang-select-lang": "Selige lingua",
+       "pagelang-submit": "Submitter",
        "right-pagelang": "Cambiar lingua del pagina",
        "action-pagelang": "cambiar le lingua del pagina",
        "log-name-pagelang": "Registro de cambios de lingua",
        "log-description-pagelang": "Isto es un registro de cambios de lingua in paginas.",
-       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiava}} le lingua del pagina $3 de $4 a $5.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiava}} le lingua de $3 de $4 a $5.",
        "default-skin-not-found": "Attention! Le apparentia predefinite de tu wiki, definite in <code dir=\"ltr\">$wgDefaultSkin</code> como <code>$1</code>, non es disponibile.\n\nLe installation pare includer le sequente {{PLURAL:$4|apparentia|apparentias}}. Vide [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] pro saper como activar {{PLURAL:$4|lo|los e seliger le predefinite}}.\n\n$2\n\n; Si tu ha justo installate MediaWiki:\n: Tu lo ha probabilemente installate a partir de git, o directemente del codice fonte con un altere methodo. Isto es normal. Essaya installar alcun apparentias desde [https://www.mediawiki.org/wiki/Category:All_skins le directorio de apparentias de mediawiki.org], per:\n:* Discargar le [https://www.mediawiki.org/wiki/Download archivo tar del installator], que include plure apparentias e extensiones. Tu pote copiar e collar le directorio <code>skins/</code> de illo.\n:* Discargar archivos tar con apparentias indidivual ab [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usar Git pro discargar apparentias].\n: Facer isto non deberea interferer con tu repositorio git si tu es un disveloppator de MediaWiki.\n\n; Si tu ha justo actualisate MediaWiki:\n: MediaWiki a partir del version 1.24 non plus activa automaticamente le apparentias installate (vide [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Tu pote collar le sequente {{PLURAL:$5|linea|lineas}} in <code>LocalSettings.php</code> pro activar {{PLURAL:$5|le apparentia|tote le apparentias}} actualmente installate:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Si tu ha justo modificate <code>LocalSettings.php</code>:\n: Verifica meticulosemente que le nomines del apparentias non ha errores.",
        "default-skin-not-found-no-skins": "Attention! Le apparentia predefinite de tu wiki, definite in <code>$wgDefaultSkin</code> como <code>$1</code>, non es disponibile.\n\nTu non ha apparentias installate.\n\n; Si tu ha justo installate o actualisate MediaWiki:\n: Tu lo ha probabilemente installate a partir de git, o directemente del codice fonte con un altere methodo. Isto es normal. Essaya installar alcun apparentias desde [https://www.mediawiki.org/wiki/Category:All_skins le directorio de apparentias de mediawiki.org], per:\n:* Discargar le [https://www.mediawiki.org/wiki/Download archivo tar del installator], que include plure apparentias e extensiones. Tu pote copiar e collar le directorio <code>skins/</code> de illo.\n:* Discargar archivos tar con apparentias individual ab [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usar Git pro discargar apparentias].\n: Facer isto non deberea interferer con tu repositorio git si tu es un disveloppator de MediaWiki. Vide [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] pro saper como activar apparentias e seliger le predefinite.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (activate)",
        "mediastatistics": "Statisticas de multimedia",
        "mediastatistics-summary": "Statisticas sur le typos de file incargate. Isto include solmente le version le plus recente de un file. Versiones ancian o delite de files es excludite.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%)",
+       "mediastatistics-bytespertype": "Dimension total de files pro iste section: {{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%).",
+       "mediastatistics-allbytes": "Dimension total de tote le files: {{PLURAL:$1|$1 byte|$1 bytes}} ($2).",
        "mediastatistics-table-mimetype": "Typo MIME",
        "mediastatistics-table-extensions": "Extensiones possibile",
        "mediastatistics-table-count": "Numero de files",
        "mediastatistics-header-text": "Textual",
        "mediastatistics-header-executable": "Executabiles",
        "mediastatistics-header-archive": "Formatos comprimite",
+       "mediastatistics-header-total": "Tote le files",
        "json-warn-trailing-comma": "$1 {{PLURAL:$1|comma|commas}} final ha essite removite de JSON",
        "json-error-unknown": "Il habeva un problema con le JSON. Error: $1",
        "json-error-depth": "Le profunditate maxime del pila ha essite excedite",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Symbolos",
        "special-characters-group-greek": "Greco",
+       "special-characters-group-greekextended": "Greco extense",
        "special-characters-group-cyrillic": "Cyrillic",
        "special-characters-group-arabic": "Arabe",
        "special-characters-group-arabicextended": "Arabe extendite",
        "mw-widgets-titleinput-description-new-page": "pagina non existe ancora",
        "mw-widgets-titleinput-description-redirect": "redirection a $1",
        "api-error-blacklisted": "Per favor elige un altere titulo, plus descriptive.",
-       "randomrootpage": "Pagina-radice aleatori"
+       "sessionmanager-tie": "Impossibile combinar plure typos de authentication de requesta: $1.",
+       "sessionprovider-generic": "sessiones $1",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "sessiones basate sur cookies",
+       "sessionprovider-nocookies": "Le cookies pote esser disactivate. Assecura te de haber activate le cookies e recomencia.",
+       "randomrootpage": "Pagina radice aleatori",
+       "log-action-filter-block": "Typo de blocada:",
+       "log-action-filter-contentmodel": "Typo de modification de modello de contento:",
+       "log-action-filter-delete": "Typo de deletion:",
+       "log-action-filter-import": "Typo de importation:",
+       "log-action-filter-managetags": "Typo de action de gestion de etiquettas:",
+       "log-action-filter-move": "Typo de renomination:",
+       "log-action-filter-newusers": "Typo de creation de conto:",
+       "log-action-filter-patrol": "Typo de patrulia:",
+       "log-action-filter-protect": "Typo de protection:",
+       "log-action-filter-rights": "Typo de cambio de derecto",
+       "log-action-filter-suppress": "Typo de suppression",
+       "log-action-filter-upload": "Typo de incargamento:",
+       "log-action-filter-all": "Toto",
+       "log-action-filter-block-block": "Blocar",
+       "log-action-filter-block-reblock": "Modification de blocada",
+       "log-action-filter-block-unblock": "Disblocar",
+       "log-action-filter-contentmodel-change": "Cambio de modello de contento",
+       "log-action-filter-contentmodel-new": "Creation de pagina con modello de contento non standard",
+       "log-action-filter-delete-delete": "Deletion de pagina",
+       "log-action-filter-delete-restore": "Restauration de pagina",
+       "log-action-filter-delete-event": "Deletion de registro",
+       "log-action-filter-delete-revision": "Deletion de version",
+       "log-action-filter-import-interwiki": "Importation trans wiki",
+       "log-action-filter-import-upload": "Importation per incargamento XML",
+       "log-action-filter-managetags-create": "Creation de etiquetta",
+       "log-action-filter-managetags-delete": "Deletion de etiquetta",
+       "log-action-filter-managetags-activate": "Activation de etiquetta",
+       "log-action-filter-managetags-deactivate": "Disactivation de etiquetta",
+       "log-action-filter-move-move": "Renomination sin superscriber redirectiones",
+       "log-action-filter-move-move_redir": "Renomination superscribente redirectiones",
+       "log-action-filter-newusers-create": "Creation per usator anonyme",
+       "log-action-filter-newusers-create2": "Creation per usator registrate",
+       "log-action-filter-newusers-autocreate": "Creation automatic",
+       "log-action-filter-newusers-byemail": "Creation con contrasigno inviate per e-mail",
+       "log-action-filter-patrol-patrol": "Patrulia manual",
+       "log-action-filter-patrol-autopatrol": "Patrulia automatic",
+       "log-action-filter-protect-protect": "Protection",
+       "log-action-filter-protect-modify": "Modification de protection",
+       "log-action-filter-protect-unprotect": "Disprotection",
+       "log-action-filter-protect-move_prot": "Protection displaciate",
+       "log-action-filter-rights-rights": "Cambiamento manual",
+       "log-action-filter-rights-autopromote": "Cambiamento automatic",
+       "log-action-filter-suppress-event": "Suppression de registro",
+       "log-action-filter-suppress-revision": "Suppression de version",
+       "log-action-filter-suppress-delete": "Suppression de pagina",
+       "log-action-filter-suppress-block": "Suppression de usator per blocada",
+       "log-action-filter-suppress-reblock": "Suppression de usator per re-blocada",
+       "log-action-filter-upload-upload": "Nove file incargate",
+       "log-action-filter-upload-overwrite": "File re-incargate"
 }
index 8b0af46..20abcbd 100644 (file)
        "noname": "Nama pengguna yang Anda masukkan tidak sah.",
        "loginsuccesstitle": "Berhasil masuk log",
        "loginsuccess": "'''Anda sekarang masuk log di {{SITENAME}} sebagai \"$1\".'''",
-       "nosuchuser": "Tidak ada pengguna dengan nama \"$1\".\nNama pengguna membedakan kapitalisasi.\nPeriksa kembali ejaan Anda, atau [[Special:UserLogin/signup|buat akun baru]].",
+       "nosuchuser": "Tidak ada pengguna dengan nama \"$1\".\nNama pengguna membedakan kapitalisasi.\nPeriksa kembali ejaan Anda, atau [[Special:CreateAccount|buat akun baru]].",
        "nosuchusershort": "Tidak ada pengguna dengan nama \"$1\".\nSilakan periksa kembali ejaan Anda.",
        "nouserspecified": "Anda harus memasukkan nama pengguna.",
        "login-userblocked": "Pengguna ini diblokir. Tidak diizinkan/diperbolehkan untuk masuk log.",
        "changepassword-success": "Kata sandi Anda telah diubah!",
        "changepassword-throttled": "Anda terlalu sering mencoba masuk log.\nMohon tunggu $1 sebelum mencoba lagi.",
        "botpasswords": "Kata sandi bot",
+       "botpasswords-summary": "<em>Kata sandi bot</em> memungkinkan akses ke akun pengguna menggunakan API tanpa menggunakan kredensial masuk log utama akun tersebut. Hak pengguna yang tersedia ketika masuk log dengan kata sandi bot mungkin akan dibatasi.\n\nJika Anda tidak tahu kenapa Anda ingin melakukan hal ini, sebaiknya jangan lakukan. Semestinya tidak ada orang lain yang boleh meminta Anda untuk menciptakan dan menyerahkan kata sandi bot ini kepadanya.",
        "botpasswords-disabled": "Kata sandi bot dinonaktifkan.",
        "botpasswords-no-central-id": "Untuk menggunakan kata sandi bot, Anda harus masuk log ke akun yang telah tersentralisasi.",
        "botpasswords-existing": "Kata sandi bot tersedia",
        "botpasswords-label-delete": "Hapus",
        "botpasswords-label-resetpassword": "Setel ulang kata sandi",
        "botpasswords-label-grants": "Akses yang dapat diberikan:",
+       "botpasswords-help-grants": "Tiap izin memberikan akses ke hak-hak pengguna yang telah dimiliki suatu akun pengguna. Lihat [[Special:ListGrants|tabel izin]] untuk informasi lebih lanjut.",
        "botpasswords-label-restrictions": "Batasan penggunaan:",
        "botpasswords-label-grants-column": "Izin diberikan",
        "botpasswords-bad-appid": "Nama bot \"$1\" tidak valid.",
        "botpasswords-insert-failed": "Gagal menambah nama bot \"$1\". Apakah sudah ditambahkan sebelum ini?",
        "botpasswords-update-failed": "Gagal memperbarui nama bot \"$1\". Apakah sebelumnya sudah pernah dihapus?",
        "botpasswords-created-title": "Kata sandi bot dibuat",
-       "botpasswords-created-body": "Kata sandi bot \"$1\" sukses dibuat.",
+       "botpasswords-created-body": "Kata sandi bot \"$1\" berhasil dibuat.",
        "botpasswords-updated-title": "Kata sandi bot diperbarui",
-       "botpasswords-updated-body": "Kata sandi bot \"$1\" sukses diperbarui.",
+       "botpasswords-updated-body": "Kata sandi bot \"$1\" berhasil diperbarui.",
        "botpasswords-deleted-title": "Kata sandi bot dihapus",
        "botpasswords-deleted-body": "Kata sandi bot \"$1\" telah dihapus.",
        "botpasswords-newpassword": "Kata sandi baru untuk masuk log dengan '''$1''' adalah '''$2'''. ''Mohon simpan untuk referensi di kemudian hari.''",
        "resetpass-no-info": "Anda harus masuk log untuk mengakses halaman ini secara langsung.",
        "resetpass-submit-loggedin": "Ganti kata sandi",
        "resetpass-submit-cancel": "Batalkan",
-       "resetpass-wrong-oldpass": "Kata sandi tidak sah.\nAnda mungkin telah berhasil mengganti kata sandi Anda atau telah meminta kata sandi sementara yang baru.",
+       "resetpass-wrong-oldpass": "Kata sandi tidak sah.\nAnda mungkin telah mengganti kata sandi Anda atau telah meminta kata sandi sementara yang baru.",
        "resetpass-recycled": "Mohon menyetel ulang kata sandi Anda ke sesuatu yang berbeda dari kata sandi Anda sekarang.",
        "resetpass-temp-emailed": "Anda masuk log dengan kode sementara yang disurel.\nUntuk menyelesaikan masuk log, Anda harus mengatur sandi baru di sini:",
        "resetpass-temp-password": "Kata sandi sementara:",
        "minoredit": "Ini adalah suntingan kecil.",
        "watchthis": "Pantau halaman ini",
        "savearticle": "Simpan halaman",
+       "publishpage": "Terbitkan halaman",
        "preview": "Pratayang",
        "showpreview": "Lihat pratayang",
        "showdiff": "Lihat perubahan",
        "accmailtext": "Sebuah kata sandi acak untuk [[User talk:$1|$1]] telah dikirimkan ke $2.\n\nKata sandi untuk akun baru ini dapat diubah di halaman ''[[Special:ChangePassword|pengubahan kata sandi]]'' setelah masuk log.",
        "newarticle": "(Baru)",
        "newarticletext": "Anda mengikuti pranala ke halaman yang belum tersedia. Untuk membuat halaman tersebut, ketiklah isi halaman di kotak di bawah ini (lihat [$1 halaman bantuan] untuk informasi lebih lanjut). Jika Anda tanpa sengaja sampai ke halaman ini, klik tombol '''back''' di penjelajah web Anda.",
-       "anontalkpagetext": "----''Ini adalah halaman pembicaraan seorang pengguna anonim yang belum membuat akun atau tidak menggunakannya.\nDengan demikian, kami terpaksa harus memakai alamat IP yang bersangkutan untuk mengidentifikasikannya.\nAlamat IP seperti ini mungkin dipakai bersama oleh beberapa pengguna yang berbeda.\nJika Anda adalah seorang pengguna anonim dan merasa mendapatkan komentar-komentar yang tidak relevan yang ditujukan langsung kepada Anda, silakan [[Special:UserLogin/signup|membuat akun]] atau [[Special:UserLogin|masuk log]] untuk menghindari kerancuan dengan pengguna anonim lainnya di lain waktu.''",
+       "anontalkpagetext": "----''Ini adalah halaman pembicaraan seorang pengguna anonim yang belum membuat akun atau tidak menggunakannya.\nDengan demikian, kami terpaksa harus memakai alamat IP yang bersangkutan untuk mengidentifikasikannya.\nAlamat IP seperti ini mungkin dipakai bersama oleh beberapa pengguna yang berbeda.\nJika Anda adalah seorang pengguna anonim dan merasa mendapatkan komentar-komentar yang tidak relevan yang ditujukan langsung kepada Anda, silakan [[Special:CreateAccount|membuat akun]] atau [[Special:UserLogin|masuk log]] untuk menghindari kerancuan dengan pengguna anonim lainnya di lain waktu.''",
        "noarticletext": "Saat ini tidak ada teks di halaman ini.\nAnda dapat [[Special:Search/{{PAGENAME}}|melakukan pencarian untuk judul halaman ini]] di halaman-halaman lain, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mencari log terkait], atau [{{fullurl:{{FULLPAGENAME}}|action=edit}} membuat halaman ini]</span>.",
        "noarticletext-nopermission": "!Saat ini tidak ada teks di halaman ini.\nAnda dapat [[Special:Search/{{PAGENAME}}|melakukan pencarian untuk judul halaman ini]] di halaman-halaman lain, atau <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mencari log terkait]</span>, tapi Anda tidak memiliki izin untuk membuat halaman ini",
        "missing-revision": "Revisi #$1 halaman berjudul \"{{FULLPAGENAME}}\" tidak eksis.\n\nHal ini biasanya disebabkan oleh tautan versi terdahulu menuju halaman yang sudah dihapus.\nRinciannya dapat ditemukan di [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} log penghapusan].",
        "userpage-userdoesnotexist": "Akun pengguna \"<nowiki>$1</nowiki>\" tidak terdaftar.",
        "userpage-userdoesnotexist-view": "Pengguna \"$1\" tidak terdaftar.",
        "blocked-notice-logextract": "Pengguna ini sedang diblokir.\nEntri log pemblokiran terakhir tersedia di bawah ini sebagai rujukan:",
-       "clearyourcache": "'''Catatan:''' Setelah menyimpan, Anda mungkin harus memintas singgahan peramban Anda untuk melihat perubahan.\n* '''Firefox / Safari:''' Tahan ''Shift'' sambil mengeklik ''Reload'', atau tekan ''Ctrl-F5'' atau ''Ctrl-R'' (''⌘-R'' di Mac)\n* '''Google Chrome:''' Tekan ''Ctrl-Shift-R'' (''⌘-Shift-R'' di Mac)\n* '''Internet Explorer:''' Tahan ''Ctrl'' sambl mengeklik ''Refresh'', atau tekan ''Ctrl-F5''\n* '''Opera:''' Bersihkan tembolok di ''Tools → Preferences''",
+       "clearyourcache": "'''Catatan:''' Setelah menyimpan, Anda mungkin harus memintas isi singgahan peramban Anda untuk melihat perubahan.\n* '''Firefox / Safari:''' Tahan ''Shift'' sambil mengeklik ''Reload'', atau tekan ''Ctrl-F5'' atau ''Ctrl-R'' (''⌘-R'' di Mac)\n* '''Google Chrome:''' Tekan ''Ctrl-Shift-R'' (''⌘-Shift-R'' di Mac)\n* '''Internet Explorer:''' Tahan ''Ctrl'' sambl mengeklik ''Refresh'', atau tekan ''Ctrl-F5''\n* '''Opera:''' Buka <em> Menu → Settings</em> (<em>Opera → Preferences</em>Privacy & Security  → Clear browsing data  → Cached images and files</em>.",
        "usercssyoucanpreview": "'''Tips:''' Gunakan tombol \"{{int:showpreview}}\" untuk menguji CSS baru Anda sebelum menyimpannya.",
        "userjsyoucanpreview": "'''Tips:''' Gunakan tombol \"{{int:showpreview}}\" untuk menguji JS baru Anda sebelum menyimpannya.",
        "usercsspreview": "'''Ingatlah bahwa Anda sedang menampilkan pratayang dari CSS Anda.\nPratayang ini belum disimpan!'''",
        "userrights-unchangeable-col": "Kelompok yang tidak dapat Anda ubah",
        "userrights-irreversible-marker": "$1*",
        "userrights-conflict": "Konflik perubahan hak pengguna! Silakan tinjau ulang dan konfirmasi perubahan Anda.",
-       "userrights-removed-self": "Anda berhasil mencabut hak-hak Anda. Anda tidak bisa lagi mengakses halaman ini.",
+       "userrights-removed-self": "Anda telah mencabut hak-hak Anda sendiri. Anda tidak bisa lagi mengakses halaman ini.",
        "group": "Kelompok:",
        "group-user": "Pengguna",
        "group-autoconfirmed": "Pengguna terkonfirmasi otomatis",
        "uploaded-script-svg": "Terdapat elemen terskrip \"$1\" dalam berkas SVG yang diunggah.",
        "uploaded-hostile-svg": "Terdapat CSS yang tidak aman dalam elemen gaya berkas SVG yang diunggah.",
        "uploaded-event-handler-on-svg": "Penetapan atribut <i>event-handler</i> $1=\"$2\" tidak diizinkan dalam berkas SVG.",
+       "uploaded-href-attribute-svg": "Atribut href pada berkas SVG hanya dibolehkan untuk ditautkan ke target http:// atau https://, ditemukan <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-href-unsafe-target-svg": "Menemukan href ke data tidak aman: URI menarget <code>&lt;$1 $2=\"$3\"&gt;</code> dalam berkas SVG yang diunggah.",
        "uploaded-setting-event-handler-svg": "Penyetelan atribut event-handler diblokir, menemukan <code>&lt;$1 $2=\"$3\"&gt;</code> dalam berkas SVG yang diunggah.",
        "uploadscriptednamespace": "Berkas SVG ini memuat ruang nama ilegal \"$1\"",
        "uploadinvalidxml": "XML dalam berkas yang diunggah tidak bisa diuraikan.",
        "upload-options": "Opsi pengunggahan",
        "watchthisupload": "Pantau berkas ini",
        "filewasdeleted": "Suatu berkas dengan nama ini pernah dimuat dan selanjutnya dihapus. Harap cek $1 sebelum memuat lagi berkas tersebut.",
+       "filename-thumb-name": "Tampaknya ini adalah judul gambar mini. Mohon jangan mengunggah gambar mini kembali ke wiki yang sama. Bila tidak, silakan perbaiki nama berkas sehingga lebih bermakna, dan tidak memiliki awalan seperti judul gambar mini.",
        "filename-bad-prefix": "Nama berkas yang Anda muat diawali dengan '''\"$1\"''', yang merupakan nama non-deskriptif yang biasanya diberikan secara otomatis oleh kamera digital. Harap pilih nama lain yang lebih deskriptif untuk berkas Anda.",
        "filename-prefix-blacklist": " #<!-- biarkan baris ini seperti adanya --> <pre>\n# Contohnya sebagai berikut:\n#   * Semuanya dari karekter \"#\" sampai akhir baris ini adalah komentar\n#   * Setiap garis \"_\" adalah awalan untuk nama file khas yang diberikan secara otomatis oleh kamera digital\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # beberapa model telpon seluler\nIMG # generik\nJD # Jenoptik\nMGP # Pentax\nPICT # lainnya.\n #</pre> <!-- biarkan baris ini seperti adanya -->",
        "upload-proto-error": "Protokol tak tepat",
        "uploadstash-summary": "Halaman ini memberikan akses terhadap berkas yang diunggah (atau dalam proses pengunggahan), namun belum diterbitkan ke wiki. Berkas-berkas ini tidak dapat dilihat oleh siapa pun kecuali pengunggahnya.",
        "uploadstash-clear": "Hapus berkas simpanan",
        "uploadstash-nofiles": "Anda tidak memiliki berkas simpanan.",
-       "uploadstash-badtoken": "Pelaksanaan tindakan tersebut gagal. Mungkin karena hak penyuntingan Anda telah kedaluwarsa. Coba lagi.",
+       "uploadstash-badtoken": "Pelaksanaan tindakan tersebut gagal. Mungkin karena hak penyuntingan Anda telah kedaluwarsa. Silakan coba lagi.",
        "uploadstash-errclear": "Penghapusan berkas gagal.",
        "uploadstash-refresh": "Segarkan daftar berkas.",
+       "uploadstash-thumbnail": "lihat miniatur",
        "invalid-chunk-offset": "Ofset potongan tidak valid",
        "img-auth-accessdenied": "Akses ditolak",
        "img-auth-nopathinfo": "PATH_INFO hilang.\nServer Anda tidak diatur untuk melewatkan informasi ini.\nServer tersebut mungkin berbasis CGI dan tidak dapat mendukung img_auth.\nLihat https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "changecontentmodel-title-label": "Judul halaman",
        "changecontentmodel-model-label": "Model konten baru",
        "changecontentmodel-reason-label": "Alasan:",
+       "changecontentmodel-submit": "Ubah",
        "changecontentmodel-success-title": "Model konten ini telah diubah",
        "changecontentmodel-success-text": "Jenis konten [[:$1]] telah diubah",
        "changecontentmodel-cannot-convert": "Isi pada [[:$1]] tidak dapat ditukar kepada jenis $2.",
        "whatlinkshere-prev": "{{PLURAL:$1|sebelumnya $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|selanjutnya $1}}",
        "whatlinkshere-links": "← pranala",
-       "whatlinkshere-hideredirs": "$1 pengalihan",
-       "whatlinkshere-hidetrans": "$1 transklusi",
-       "whatlinkshere-hidelinks": "$1 pranala",
-       "whatlinkshere-hideimages": "$1 pranala berkas",
+       "whatlinkshere-hideredirs": "Sembunyikan pengalihan",
+       "whatlinkshere-hidetrans": "Sembunyikan transklusi",
+       "whatlinkshere-hidelinks": "Sembunyikan pranala",
+       "whatlinkshere-hideimages": "Sembunyikan pranala berkas",
        "whatlinkshere-filters": "Penyaring",
        "whatlinkshere-submit": "Tuju ke",
        "autoblockid": "Blokir otomatis #$1",
        "ipb-unblock": "Hilangkan blokir seorang pengguna atau suatu alamat IP",
        "ipb-blocklist": "Lihat blokir yang diterapkan",
        "ipb-blocklist-contribs": "Kontribusi untuk {{GENDER:$1|$1}}",
+       "ipb-blocklist-duration-left": "Tersisa $1",
        "unblockip": "Buka blokir pengguna",
        "unblockiptext": "Gunakan formulir di bawah untuk mengembalikan kemampuan menulis sebuah alamat IP atau pengguna yang sebelumnya telah diblokir.",
        "ipusubmit": "Hilangkan blokir ini",
        "lockdbsuccesstext": "Basis data telah dikunci.<br />\nPastikan Anda [[Special:UnlockDB|membuka kuncinya]] setelah pemeliharaan selesai.",
        "unlockdbsuccesstext": "Kunci basis data telah dibuka.",
        "lockfilenotwritable": "Berkas kunci basis data tidak dapat ditulis. Untuk mengunci atau membuka basis data, berkas ini harus dapat ditulis oleh server web.",
+       "databaselocked": "Basis data telah terkunci.",
        "databasenotlocked": "Basis data tidak terkunci.",
        "lockedbyandtime": "(oleh $1 pada $2 $3)",
        "move-page": "Pindahkan $1",
        "tooltip-ca-nstab-category": "Lihat halaman kategori",
        "tooltip-minoredit": "Tandai ini sebagai suntingan kecil",
        "tooltip-save": "Simpan perubahan Anda",
+       "tooltip-publish": "Terbitkan perubahan Anda",
        "tooltip-preview": "Pratayang perubahan Anda, harap gunakan ini sebelum menyimpan!",
        "tooltip-diff": "Lihat perubahan yang telah Anda lakukan.",
        "tooltip-compareselectedversions": "Lihat perbedaan antara dua versi halaman yang dipilih.",
        "watchlistedit-raw-done": "Daftar pantauan Anda telah diperbarui.",
        "watchlistedit-raw-added": "{{PLURAL:$1|$1 judul telah}} ditambahkan:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|$1 judul telah}} dikeluarkan:",
-       "watchlistedit-clear-title": "Daftar pantauan dihapus",
+       "watchlistedit-clear-title": "Hapus daftar pantauan",
        "watchlistedit-clear-legend": "Hapus daftar pantauan",
        "watchlistedit-clear-explain": "Semua judul akan dihapus dari daftar pantauan Anda",
        "watchlistedit-clear-titles": "Judul:",
        "tags-create-invalid-chars": "Nama tag tidak boleh mengandung koma (<code>,</code>) atau garis miring (<code>/</code>).",
        "tags-create-invalid-title-chars": "Nama tag tidak boleh mengandung karakter yang tidak bisa digunakan dalam judul halaman.",
        "tags-create-already-exists": "Tag \"$1\" sudah ada.",
+       "tags-create-warnings-below": "Apakah Anda ingin melanjutkan pembuatan tanda ini?",
        "tags-delete-reason": "Alasan:",
        "tags-activate-reason": "Alasan:",
        "tags-activate-submit": "Aktifkan",
        "feedback-bugornote": "Jika Anda sudah siap untuk mendeskripsikan masalah teknis secara rinci silakan [$1 melaporkan bug].\nJika tidak, Anda dapat menggunakan formulir mudah di bawah ini. Komentar Anda akan ditambahkan ke halaman \"[$3 $2]\", bersama dengan nama pengguna Anda dan apa browser yang Anda gunakan.",
        "feedback-cancel": "Batal",
        "feedback-close": "Selesai",
+       "feedback-dialog-title": "Kirimkan saran dan tanggapan",
+       "feedback-dialog-intro": "Anda bisa menggunakan formulir sederhana di bawah untuk mengirimkan saran dan masukan. Komentar Anda akan ditambahkan pada laman \"$1\" bersama nama pengguna Anda.",
        "feedback-error-title": "Kesalahan",
        "feedback-error1": "Galat: Hasil tidak dikenal dari API",
        "feedback-error2": "Galat: Penyuntingan gagal",
        "api-error-blacklisted": "Pilih judul lain yang deskriptif",
        "randomrootpage": "Halaman dasar sembarang",
        "log-action-filter-block": "Jenis pemblokiran:",
+       "log-action-filter-all": "Semua",
        "log-action-filter-block-block": "Blokir",
        "log-action-filter-suppress-block": "Perahasiaan pengguna menurut pemblokiran"
 }
index e88379c..2dff4b6 100644 (file)
        "noname": "Saanmo a nainaganan ti umisu a nagan ti agar-aramat.",
        "loginsuccesstitle": "Nakastrek",
        "loginsuccess": "<strong>Nakastrekkan iti {{SITENAME}} a kas ni \"$1\".</strong>",
-       "nosuchuser": "Awan ti agar-aramat nga agnagan iti \"$1\". \n\nDagiti nagan ti agar-aramat ket sensitibo ti kadakkel ti letra.\n\nKitaem ti panangiletram, wenno [[Special:UserLogin/signup|agpartuat iti baro a pakabilangan]].",
+       "nosuchuser": "Awan ti agar-aramat nga agnagan iti \"$1\". \n\nDagiti nagan ti agar-aramat ket sensitibo ti kadakkel ti letra.\n\nKitaem ti panangiletram, wenno [[Special:CreateAccount|agpartuat iti baro a pakabilangan]].",
        "nosuchusershort": "Awan ti agar-aramat nga agnagan iti \"$1\".\nKitaem ti panangiletram.",
        "nouserspecified": "Nasken nga inaganam ti nagan ti agar-aramat.",
        "login-userblocked": "Naserraan daytoy nga agar-aramat. Saan a mapalubosan ti sumrek.",
        "accmailtext": "Ti pugto a napartuat a kontrasenias para kenni [[User talk:$1|$1]] ket naipatuloden iti $2. Mabalin a masukatan iti\n<em>[[Special:ChangePassword|pagsukatan ti kontrasenias]]</em> a panid no sumrekka.",
        "newarticle": "(Baro)",
        "newarticletext": "Nasurotmo ti silpo ti awan pay a panid. \nTi mangpartuat ti panid, rugiamon ti agmakinilia iti kahon dita baba (kitaen ti [$1 panid ti tulong] para iti adu pay a pakaammo). \nNo addaka ditoy babaen ti biddut, pindutem ti buton ti <strong>back</strong> ti pagbasabasam.",
-       "anontalkpagetext": "----\n<em>Daytoy ti pakitungtungan a panid para iti di ammo nga agar-aramat a saan pay a nakapartuat iti pakabilangan, wenno saanna nga us-usaren.</em>\nIsu nga agusarkami ti numero nga IP a pagtaengan tapno mailasin isuda a lalaki/babai.\nTi kastoy nga IP a pagtaengan ket us-usaren a bingayan babaen ti nadumaduma nga agar-aramat.\nNo sika ket maysa a di ammo nga agar-aramat ken dagiti awan ti pategna a komentario ket napaitudo kenka, pangngaasi nga [[Special:UserLogin/signup|agpartuatka iti pakabilangam]] wenno [[Special:UserLogin|sumrekka]] \ntapno maliklikan ti pannakaiyallilaw kadagiti sabali a di ammo nga agar-aramat.",
+       "anontalkpagetext": "----\n<em>Daytoy ti pakitungtungan a panid para iti di ammo nga agar-aramat a saan pay a nakapartuat iti pakabilangan, wenno saanna nga us-usaren.</em>\nIsu nga agusarkami ti numero nga IP a pagtaengan tapno mailasin isuda a lalaki/babai.\nTi kastoy nga IP a pagtaengan ket us-usaren a bingayan babaen ti nadumaduma nga agar-aramat.\nNo sika ket maysa a di ammo nga agar-aramat ken dagiti awan ti pategna a komentario ket napaitudo kenka, pangngaasi nga [[Special:CreateAccount|agpartuatka iti pakabilangam]] wenno [[Special:UserLogin|sumrekka]] \ntapno maliklikan ti pannakaiyallilaw kadagiti sabali a di ammo nga agar-aramat.",
        "noarticletext": "Awan ti agdama a teksto iti daytoy a panid.\nMabalinmo ti [[Special:Search/{{PAGENAME}}|agbiruk iti kastoy a titulo ti panid]] kadagiti sabali a panid,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} agbiruk kadagiti mainaig a listaan],\nwenno [{{fullurl:{{NAMESPACE}}:{{PAGENAME}}|action=edit}} partuaten daytoy a panid]</span>.",
        "noarticletext-nopermission": "Awan ti agdama  a linaon daytoy a panid.\nMabalinmo ti [[Special:Search/{{PAGENAME}}|agbiruk para iti titulo ti daytoy a panid]] kadagiti sabali a panid, wenno <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} agbiruk kadagiti mainaig a listaan]</span>, ngem awan ti pammalubosmo a mangpartuat iti daytoy a panid.",
        "missing-revision": "Ti panagbalbaliw ti #$1 iti daytoy a panid a nanaganan ti \"{{FULLPAGENAME}}\" ket awan.\n\nDaytoy ket kadawyan a gapuanan babaen ti sumaganad a silpo ti baak a pakasaritaan iti maysa a naikkaten a panid.\nDagiti salaysay ket mabalin a mabirukan idiay [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} listaan ti panagikkat].",
        "upload-form-label-own-work": "Daytoy ket bukodko nga obra",
        "upload-form-label-infoform-categories": "Katkategoria",
        "upload-form-label-infoform-date": "Petsa",
-       "upload-form-label-not-own-work-local-local": "Mabalinmo pay a padasen [[Special:Upload|ti kasisigud a pagikargaan a panid]].",
+       "upload-form-label-not-own-work-local-generic-local": "Mabalinmo pay a padasen [[Special:Upload|ti kasisigud a pagikargaan a panid]].",
        "backend-fail-stream": "Saan a maipan ti papeles $1.",
        "backend-fail-backup": "Saan a makaidulin ti kapada ti papeles ti $1.",
        "backend-fail-notexists": "Awan ti papeles ti $1.",
index ac588bf..8ac3982 100644 (file)
        "tog-hidecategorization": "Къайлаяха оагӀонай категореш",
        "tog-extendwatchlist": "Хьашеръяь йола зем бара список, массадола хувцамаш ше чулоацаш, т|ехьара даь хувцамаш хинна ца Iеш.",
        "tog-usenewrc": "Керда хувцамашка а хьат|аяздара зембаккхарга а эргадаккхараш тоабаде (JavaScript эша)",
-       "tog-numberheadings": "Ð\9aеÑ\80Ñ\82аÑ\88каÑ\88Ñ\82а Ð°Ð»Ð°Ð½Ð·Ð° Ñ\82аÑ\8cÑ\80аÑ\85Ñ\8cа Ñ\85оÑ\82Ñ\82а",
-       "tog-showtoolbar": "Г|алатнийcдара г|ирсагартакх хьахьокха (JavaScript)",
-       "tog-editondblclick": "Шозза Ð´|аÑ\82о|амÑ\86a oаг|Ñ\83в Ñ\85Ñ\83вÑ\86а (JavaScript)",
-       "tog-editsectiononrightclick": "РалÑ\81декÑ\8aаÑ\80аÑ\88 Ñ\85Ñ\83вÑ\86а Ð´Ð°Ñ\85каÑ\86а Ð°Ñ\8cÑ\82Ñ\82а Ð´|аÑ\82о|амÑ\86а  ÐºÐµÑ\80Ñ\82аÑ\88ка Ñ\82|а (JavaScript)",
-       "tog-watchcreations": "Tеркама хьат|аяздар т|а аз яь оаг|онаши чуяьккха паьлаши т|атоха",
-       "tog-watchdefault": "Tеркама хьат|аяздар т|а аз хийца оаг|онаши паьлаша кустяздараши т|атоха",
-       "tog-watchmoves": "Tеркама хьат|аяздар т|а аз ц|ихийца оагӀонаши паьлаши т|атоха",
-       "tog-watchdeletion": "Tеркама хьат|аяздар т|а аз д|аяьккха оагӀонаши паьлаши т|атоха",
-       "tog-minordefault": "ТеÑ\80камза Ñ\85Ñ\83вÑ\86амаÑ\88Ñ\82а Ð»Ð¾Ð°Ñ\80Ñ\85Ó\80амза Ð¼Ð¾ Ð±ÐµÐ»Ð³Ð°Ð»Ð¾ Ñ\85оÑ\82Ñ\82а",
-       "tog-previewontop": "Ð\93Ó\80алаÑ\82нийÑ\81даÑ\80а ÐºÐ¾Ñ\80а Ñ\85Ñ\8cалÑ\85е Ð±Ó\80аÑ\80гÑ\82аÑ\81Ñ\81ам Ð¾Ñ\82Ñ\82ае",
-       "tog-previewonfirst": "Ð\93Ó\80алаÑ\82нийÑ\81даÑ\80е Ð´ÐµÑ\85Ñ\8cавоалаÑ\88/йоалаÑ\88 Ð±Ó\80аÑ\80гÑ\82аÑ\81Ñ\81ам гойта",
+       "tog-numberheadings": "Ð\90вÑ\82омаÑ\82иÑ\87еÑ\81ки Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ°Ñ\88Ñ\82а Ð½Ñ\83меÑ\80аÑ\86и Ñ\85Ñ\8cае",
+       "tog-showtoolbar": "ГIирсай панель хьахьокха хувцам беч хана",
+       "tog-editondblclick": "Ð\9dиÑ\81Ñ\8aе Ð¾Ð°Ð³Ó\80онаÑ\88 Ñ\88озза IоÑ\82Ó\80аÑ\82оÓ\80аеÑ\87а (JavaScript)",
+       "tog-editsectiononrightclick": "Ð\9dийÑ\81де Ð´Ð°ÐºÑ\8aа Ñ\88озза Ð´Ð°Ñ\85ка Ð°Ñ\8cÑ\82Ñ\82а Ñ\82оIаеÑ\80 Ñ\82Ó\80аÑ\82оÓ\80айиÑ\87а Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ° Ñ\82Iа (JavaScript)",
+       "tog-watchcreations": "Зем бара списка т|атоха аз хьаяь оаг|онаши чуяьккха файлаши",
+       "tog-watchdefault": "Зем бара списка т|атоха аз хийца оаг|онаши файлай йоазонца сурташ оттадари",
+       "tog-watchmoves": "Зем бара списка т|атоха аз цIи хийца оаг|онаши файлаши",
+       "tog-watchdeletion": "Зем бара списка т|атоха аз дIаяьккха оаг|онаши файлаши",
+       "tog-minordefault": "Ð\9cаÑ\81Ñ\81аза Ð·Iамига Ð´Ð°Ñ\80аÑ\88 Ñ\81анна Ð±ÐµÐ»Ð³Ð°Ð»Ð´Ðµ Ñ\85Ñ\83вÑ\86амаÑ\88.",
+       "tog-previewontop": "Ð¥Ñ\8cалÑ\85Ñ\85е Ð±Ó\80аÑ\80гÑ\82оÑ\85аÑ\80 Ñ\85Ñ\8cагойÑ\82а Ñ\85Ñ\83вÑ\86ама ÐºÐ¾Ñ\80а Ñ\85Ñ\8cалÑ\85аÑ\88каÑ\85Ñ\8c",
+       "tog-previewonfirst": "Ð¥Ñ\83вÑ\86ама Ð´ÐµÑ\85Ñ\8cавоалаÑ\88 Ñ\85ан Ñ\85Ñ\8cалÑ\85Ñ\85е Ð±Ó\80аÑ\80гÑ\82оÑ\85аÑ\80 Ñ\85Ñ\8cагойта",
        "tog-enotifwatchlistpages": "Tеркама хьат|аяздар чура оаг|онаши паьлаши хувцамаех лаьца д-хоамне т|а дайта хьа",
        "tog-enotifusertalkpages": "Са дувцама оаг|он т|а хувцамаш хилча, д-хоамнец хьахоам бе",
        "tog-enotifminoredits": "Геттара з|амига хувцамаш хилча а, д-хоамнец хьахоам бе",
@@ -69,9 +69,9 @@
        "sun": "КIиранди",
        "mon": "Ор",
        "tue": "Шин",
-       "wed": "Кха",
+       "wed": "Кх",
        "thu": "Ер",
-       "fri": "П|аь",
+       "fri": "ПI",
        "sat": "Шоа",
        "january": "АгIой бутт",
        "february": "Саь-кур бутт",
@@ -87,7 +87,7 @@
        "december": "Чан-тар бутт",
        "january-gen": "АгIой бетт",
        "february-gen": "Саь-кур бетт",
-       "march-gen": "Муттхьол бетт",
+       "march-gen": "Мутт-хьал бетт",
        "april-gen": "Тушоли бетт",
        "may-gen": "Села бетт",
        "june-gen": "Этинга бетт",
@@ -96,7 +96,7 @@
        "september-gen": "Тов\\Михий бетт",
        "october-gen": "Ардарий\\АьрхIий бетт",
        "november-gen": "Лай чилла бетт",
-       "december-gen": "Чантар бетт",
+       "december-gen": "Чан-тар бетт",
        "jan": "АгIой",
        "feb": "Саь-кур",
        "mar": "Мутт-хьал",
        "period-pm": "ДТ",
        "pagecategories": "{{PLURAL:$1|1=Категори|Категореш}}",
        "category_header": "«$1» категори чура оагIонаш",
-       "subcategories": "ЧÑ\83Ñ\80акаÑ\82агаш",
+       "subcategories": "Ð\9aIалкаÑ\82егоÑ\80еш",
        "category-media-header": "\"$1\" Категори чура файлаш",
        "category-empty": "''Ер категори хӀанза яьсса я (цхьаккха оагIонаш е файлаш йоацаш).''",
        "hidden-categories": "{{PLURAL:$1|1=Къайла категори|Къайла категореш}}",
        "categorypage": "Катага оаг|oн т|а б|аргтасса",
        "viewtalkpage": "Дувцамага б|аргтасса",
        "otherlanguages": "Кхыча меттаех",
-       "redirectedfrom": "($1 Ñ\82IaÑ\80а Ñ\85Ñ\8cаÑ\85Ñ\8cожадаÑ\8c Ð´Ð°)",
+       "redirectedfrom": "($1 Ñ\82IaÑ\80а Ñ\83кÑ\85аз Ñ\85Ñ\8cаÑ\85Ñ\8cожаÑ\8fÑ\8c Ñ\8f)",
        "redirectpagesub": "Д|а-хьа дайта оаг|ув",
-       "redirectto": "ТIахьожадар укхаза:",
+       "redirectto": "Ð\94Iа-Ñ\81ахьожадар укхаза:",
        "lastmodifiedat": "Укх оагIoн тIеххьара хувцам: $2, $1.",
        "viewcount": "Укх оаг|oн т|а б|аргтассаб {{PLURAL:$1|цхьааца\n|$1 times}}. {{PLURAL:$1|1=цхьазза|$1за}}.",
        "protectedpage": "Лорама оаг|ув",
        "page-atom-feed": "«$1» — Atom-мугI",
        "red-link-title": "$1 (укх тайпара оагӀув яц)",
        "nstab-main": "Йоазув",
-       "nstab-user": "Ð\94акÑ\8aалаÑ\8cÑ\86аÑ\80хо",
+       "nstab-user": "Ð\94оакÑ\8aаÑ\88хо",
        "nstab-media": "Медифаг",
        "nstab-special": "Балха оагӀув",
        "nstab-project": "Проектах лаьца",
        "virus-unknownscanner": "довзашдоаца мазаундохьалург:",
        "welcomeuser": "Маьрша доаг|алд, $1!",
        "yourname": "Дакъалаьцархочунна цӀи:",
-       "userlogin-yourname": "Доакъашхочунна цӀи",
-       "userlogin-yourname-ph": "Чуйоалае доакъашхочун цӀи",
+       "userlogin-yourname": "Доакъашхочун цӀи",
+       "userlogin-yourname-ph": "Iочуязъе хьай учёта яздара (доакъашхочун) цӀи",
        "createacct-another-username-ph": "Чуйоалае доакъашхочун цӀи",
        "yourpassword": "КъайладIоагӀа:",
        "userlogin-yourpassword": "Пароль",
        "notloggedin": "Оаш шоай цӀи хьааьннадац",
        "nologin": "Леламе дIаяздар дац? '''$1'''.",
        "nologinlink": "Леламе дIаяздар кхолла",
-       "createaccount": "Учёта яздар кхолла",
+       "createaccount": "Учёта яздар хьакхолла",
        "gotaccount": "Укхаза дӀаязабенна дий шо? '''$1'''.",
        "gotaccountlink": "Чувала/яла",
        "userlogin-resetlink": "Чувала/яла цӀии дIоагӀаи дийцаденнадий?",
        "resetpass-submit-cancel": "Юхавал/ялa",
        "passwordreset-username": "Дакъалаьцархочунна цӀи:",
        "passwordreset-email": "Д-хоамни моттиг:",
-       "bold_sample": "Сома Ñ\8fздам",
-       "bold_tip": "Сома Ñ\8fздам",
-       "italic_sample": "Ð\9aÑ\83лга Ñ\8fздам",
-       "italic_tip": "Ð\9aÑ\83лга Ñ\8fздам",
-       "link_sample": "Ӏинка кортале",
-       "link_tip": "Чура хьожаярг",
-       "extlink_sample": "Ӏинка кортале http://www.example.com",
-       "extlink_tip": "Ð\90Ñ\80ен Ó\80инка (http:// Ñ\82амагÓ\80аÑ\85 Ð´Ð¸Ð¹Ñ\86а Ð¼Ð° Ð»Ðµ)",
-       "headline_sample": "Ð\9aоÑ\80Ñ\82ален Ñ\8fздам",
-       "headline_tip": "2-гӀа лагӀарлен кортале",
-       "nowiki_sample": "Укхаза кийчаде дезаш доаца яздам оттаде",
-       "nowiki_tip": "Ð\9cаÑ\81Ñ\81а-бÑ\83Ñ\81Ñ\82амлоÑ\80г Ñ\82еÑ\80камза Ð´Ð¸Ñ\82а",
-       "image_tip": "ЧÑ\83Ñ\8fÑ\8cккÑ\85а Ð¿Ð°Ñ\8cла",
-       "media_tip": "Файлан тIахьожавар",
-       "sig_tip": "ШÑ\83н кулгаяздар а, хӀанзара ха а",
-       "hr_tip": "Ð\9cÑ\83Ñ\85ала Ð¼Ñ\83гÓ\80 (могаÑ\88 Ñ\82айпаÑ\80а Ðº|еззига Ñ\85айÑ\80аде)",
-       "summary": "Ð¥Ñ\83вÑ\86амий Ð±ÐµÐ»Ð³Ð°Ð»Ð´Ðµр",
+       "bold_sample": "Ð\90Ñ\85\81ома Ñ\82екÑ\81Ñ\82",
+       "bold_tip": "Ð\90Ñ\85\81ома Ñ\82екÑ\81Ñ\82",
+       "italic_sample": "СиÑ\85а Ð¹Ð¾Ð°Ð·Ð¾Ð½ Ñ\82екÑ\81Ñ\82",
+       "italic_tip": "СиÑ\85а Ð¹Ð¾Ð°Ð·Ð¾Ð½ Ñ\82екÑ\81Ñ\82",
+       "link_sample": "ТIахьожаярга заголовок",
+       "link_tip": "ЧÑ\83Ñ\80а Ñ\82IаÑ\85Ñ\8cожаÑ\8fÑ\80г",
+       "extlink_sample": "http://www.example.com тIахьожаярга заголовок",
+       "extlink_tip": "Ð\90Ñ\80аÑ\85Ñ\8cаÑ\80а Ñ\82IаÑ\85Ñ\8cожаÑ\8fÑ\80г (йиÑ\86 Ð¼Ð° Ñ\8fлийÑ\82Ñ\82а Ð¿Ñ\80еÑ\84икÑ\81 http://)",
+       "headline_sample": "Ð\97аголовка Ñ\82екÑ\81Ñ\82",
+       "headline_tip": "2-гӀа лагӀа заголовок",
+       "nowiki_sample": "Укхаза хувца езаш йоаца текст хьачуоттае",
+       "nowiki_tip": "ТеÑ\80кал Ð¼Ð° Ðµ Ð²Ð¸ÐºÐ¸-Ñ\84оÑ\80маÑ\82иÑ\80овани",
+       "image_tip": "Ð\94IаÑ\87Ñ\83оÑ\82Ñ\82аÑ\8fÑ\8c Ñ\84айл",
+       "media_tip": "Файла тIахьожавар",
+       "sig_tip": "Ð¥Ñ\8cа кулгаяздар а, хӀанзара ха а",
+       "hr_tip": "Ð\9fÑ\85Ñ\8cоÑ\80агIен Ñ\82ака (Ñ\86оÑ\85 Ð¿Ð°Ð¹Ð´Ð° Ñ\8dÑ\86аÑ\80 Ñ\82IеÑ\85даÑ\8cнна ÐºÐ°Ñ\81Ñ\82Ñ\82а Ð¼Ð° де)",
+       "summary": "Ð¥Ñ\83вÑ\86амай Ñ\81Ñ\83Ñ\80Ñ\82 Ð¾Ñ\82Ñ\82адар",
        "subject": "БӀагал/кортале:",
        "minoredit": "ЗӀамига хувцам",
        "watchthis": "Зем бе укх оагӀон",
        "savearticle": "ОагӀув дIаязъе",
        "preview": "Хьалхе бӀаргтассар",
-       "showpreview": "Хьалххе хьажар",
+       "showpreview": "Хьалххе бIаргтохар",
        "showdiff": "Даь хувцамаш",
        "anoneditwarning": "<strong>Теркам бе!</strong> Хьо автор хинна система чуваьннавац. Нагахьа санна Iа моллагIа хувцам бой, Хьа IP-адрес дийла массанен бIаргагуш хургда. Нагахьа санна Хьо <strong>[$1 хьачувоале]</strong> е <strong>[$2 учёта яздар хьакхолле]</strong>, нийсдараш (хувцамаш) бувзам болаш хургда Хьа доакъашхой цIерца, иштта кхыдола толажагIи гIойленагIи дола дикаьш хургда Хьона.",
        "summary-preview": "Лоацам ба:",
        "newarticle": "(Kерда)",
        "newarticletext": "Шо хьожаяргаца дехьадаьннад йоаца оагӀон тӀа.\nИз кхолларгьйолаш кӀалхагӀа доала корачу текст Iочуязаде (нагахьа кхетаде хала дале [$1 новкъосталан оагӀонга] хьажа).\nЦа ховш укхаза нийсденнадале, шоай браузера '''Юха''' (назад) кнопка тӀа пӀелга тоӀабе.",
        "noarticletext": "ХIанз укх оагӀув тӀа текст яц.\nШун аьттув ба [[Special:Search/{{PAGENAME}}|цу тайпара цӀи хьоаяр кораде]] кхыйола йоазуваш чу, иштта\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} тара дола тептарий яздаьраш], е\n'''[{{fullurl:{{FULLPAGENAME}}|action=edit}} изза мо цӀи йолаш оагӀув кхолла]'''</span>.",
-       "noarticletext-nopermission": "ХIанз укх оагӀув тӀа яздам дац.\nШун йиш я, кхыдола йоазувнашках [[Special:Search/{{PAGENAME}}|дола цӀерий хаттам корае]] е <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} нийсамий тептара йоазувнаш корае].</span>",
+       "noarticletext-nopermission": "ХIанз укх оагӀон тӀа текст яц.\nШун аьттув ба [[Special:Search/{{PAGENAME}}|цу тайпара цӀи белгалъяр хьалаха]] кхыйола оагIонаш тIа, иштта\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} цун тара дола тептарай яздаьраш].</span> Иштта йола (Ер) оагӀув хьакхолла Хьа бокъо яц.",
        "note": "'''ХӀамоалар:'''",
        "previewnote": "'''Хьалхе б|аргтассам мара бац.'''\nЯздам кхы яздаь дац!",
        "editing": "Хувцам: $1",
        "hiddencategories": "Ер оагIув {{PLURAL:$1|$1 къайла категориех|1=цаI къайла категорех}} я:",
        "permissionserrorstext-withaction": "Ер $2 де Хьа бокъо яц {{PLURAL:$1|1=из бахьан долаш|из бахьанаш долаш}}:",
        "recreate-moveddeleted-warn": "'''Зем бе! Шо хьалххе дIайоаккхаш хинна оагӀув хьае гӀерта.'''\n\nХьажа, бокъонцахь езаш йолга.\nКӀалхагIа укх оагӀуви дӀадаккхами цӀи хувцами тептараш хьекха да.",
-       "moveddeleted-notice": "Ер оагӀув дӀаяккха хиннай.\nНовкъостала, кӀалха хьахьекха да дӀадаккхама а хувцама а тептарашкара дIаяздараш.",
+       "moveddeleted-notice": "Ð\95Ñ\80 Ð¾Ð°Ð³Ó\80Ñ\83в Ð´Ó\80аÑ\8fккÑ\85а Ñ\85иннай.\nÐ\9dовкÑ\8aоÑ\81Ñ\82ала, ÐºÓ\80алÑ\85а Ñ\85Ñ\8cаÑ\85Ñ\8cекÑ\85а Ð´Ð° Ð´Ó\80адаккÑ\85ама Ð° Ñ\85Ñ\83вÑ\86ама Ð° Ñ\82епÑ\82аÑ\80аÑ\88каÑ\80а Ð´IаÑ\8fздаÑ\8cÑ\80аÑ\88.",
        "log-fulllog": "Деррига таптара бӀаргтасса",
        "edit-conflict": "Хувцамий къовсам.",
        "post-expand-template-inclusion-warning": "Зембаккхам: жамIан чIабалаш чулоаца дустам геттара доккха да.\nЦхьадола чIабалаш чулоацалургдац.",
        "post-expand-template-inclusion-category": "Чулоаца чIабала мегаш дола дустам дукхалена тӀехьайоала оагӀувнаш",
        "post-expand-template-argument-warning": "Зем бе! Ер оагӀув цаӀ куцкепа |аьлдош мара чулоацац, юхадастара сел доккха дустам йолаш.\nЦу тайпара |аьлдешаш ӀокӀаладаькха да.",
        "post-expand-template-argument-category": "Куцкепий теркамза |аьлдешаш чулоаца оагӀувнаш",
-       "viewpagelogs": "Укх оагӀон тептараш хьокха",
+       "viewpagelogs": "УкÑ\85 Ð¾Ð°Ð³Ó\80он Ñ\82епÑ\82аÑ\80аÑ\88 Ñ\85Ñ\8cаÑ\85Ñ\8cокÑ\85а",
        "currentrev-asof": "тӀеххьара верси $1",
        "revisionasof": "Верси $1",
        "revision-info": "Верси $1; {{GENDER:$6|$2}}$7",
        "shown-title": "Хьóкха $1 {{PLURAL:$1|даь йоазо|даь йоазонаш}} укх оáгIувна тIа",
        "viewprevnext": "ДIахьажа ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''Укх масса-хьахьоадайтамач ер оаг|ув \"[[:$1]]\" я'''",
-       "searchmenu-new": "<strong>Ð\9aÑ\85олла Ð¾Ð°Ð³IÑ\83в Â«[[:$1]]» Ñ\83кÑ\85 Ð²Ð¸ÐºÐ¸-пÑ\80оекÑ\82е!</strong>\n{{PLURAL:$2|0=|Ð\98Ñ\88Ñ\82Ñ\82а Ñ\85Ñ\8cажа Ñ\85Ñ\8cай Ð»Ð¸Ð¹Ñ\85а Ð¾Ð°Ð³IÑ\83внага.|Иштта хьажа хьай лахара хьахиннарашка.}}",
+       "searchmenu-new": "<strong>Ð¥Ñ\8cакÑ\85олла Ð¾Ð°Ð³IÑ\83в Â«[[:$1]]» Ñ\83кÑ\85 Ð²Ð¸ÐºÐ¸-пÑ\80оекÑ\82е!</strong>\n{{PLURAL:$2|0=|Ð\98Ñ\88Ñ\82Ñ\82а Ñ\85Ñ\8cажа IайÑ\85а Ð»Ð¸Ð¹Ñ\85а Ð¾Ð°Ð³Iонга.|Иштта хьажа хьай лахара хьахиннарашка.}}",
        "searchprofile-articles": "Кертера оагIонаш",
        "searchprofile-images": "Мультимедиа",
        "searchprofile-everything": "Массанахьа",
        "search-result-size": "$1 ({{PLURAL:$2|$2 дош|$2 дешаш}})",
        "search-result-category-size": "{{PLURAL:$1|1=$1 дакъа|$1 дакъаш}} ({{PLURAL:$2|1=$2 кIалцатег|$2 кIалцатегаш}}, {{PLURAL:$3|1=$3 паьла|$3 паьлий}})",
        "search-redirect": "(дIа-сахьожадар $1 тIара)",
-       "search-section": " (дакъа $1)",
+       "search-section": "(дакъа «$1»)",
        "search-suggest": "Хьона эшар ер хила мега: $1",
        "search-interwiki-caption": "Гаргалон хьахьоадайтамаш",
        "search-interwiki-default": "$1 хьахиннараш:",
        "powersearch-toggleall": "Деррига",
        "powersearch-togglenone": "Цхьаккха",
        "preferences": "Оттамаш",
-       "mypreferences": "Ð\9eÑ\82Ñ\82амаш",
+       "mypreferences": "Ð\93IиÑ\80Ñ\81аш",
        "prefs-skin": "БIагала куц",
        "skin-preview": "Хьажа",
        "prefs-personal": "Хьа хьай далам",
        "enhancedrc-history": "истори",
        "recentchanges": "Керда хувцамаш",
        "recentchanges-legend": "Керда хувцамай оттамаш",
-       "recentchanges-summary": "КIалхагIа лоарамий доаламе тIехьара оагIувний хувцамаш дIаязадаь да {{grammar:genitive|{{SITENAME}}}}.",
+       "recentchanges-summary": "КIалхагIа ханашца нийсдаь дIаяьздаь да {{grammar:genitive|{{SITENAME}}}}  оагIонай тIеххьара хувцамаш.",
        "recentchanges-feed-description": "Укх ларамца тIехьара массахувцамашт теркам бе.",
        "recentchanges-label-newpage": "Укх хувцамаца керда оагIув кхелла хиннай",
        "recentchanges-label-minor": "Ер зIамига хувцам ба",
        "rcnotefrom": "КIалхагIа хувцамаш хьахьекха я <strong>$2</strong> денза (<strong>$1</strong> кхачалца).",
        "rclistfrom": "$3 $2 денза даь хувцамаш хьахьокха",
        "rcshowhideminor": "$1 зIамига нийсдараш",
-       "rcshowhideminor-hide": "Ð\9aÑ\8aайлдаккха",
+       "rcshowhideminor-hide": "Ð\94IакÑ\8aайладаккха",
        "rcshowhidebots": "$1 боташ",
        "rcshowhidebots-show": "Хьахьокха",
        "rcshowhideliu": "$1 бовзийтарчара доакъашхой",
        "rcshowhideanons-hide": "Къайлабаха",
        "rcshowhidepatr": "$1 теркам даь хувцамаш",
        "rcshowhidemine": "$1 хьа нийсдараш",
-       "rcshowhidemine-hide": "Ð\9aÑ\8aайлдаккха",
+       "rcshowhidemine-hide": "Ð\94IакÑ\8aайладаккха",
        "rclinks": "Хьахьокха $2 дийнахь даь хинна тIеххьара $1 хувцамаш\n<br />$3",
        "diff": "башхало",
        "hist": "истори",
-       "hide": "Къайлдаккха",
+       "hide": "Ð\9aÑ\8aайладаккÑ\85а",
        "show": "Хьахьокха",
        "minoreditletter": "зI",
        "newpageletter": "К",
        "rc-change-size-new": "Хувцам баьнначул тӀехьагIа бола боарам: $1 {{PLURAL:$1|байт}}",
        "rc-enhanced-expand": "Ма дарра чулоацамаш хьахьокха (JavaScriptаца)",
        "rc-enhanced-hide": "Ма дарра чулоацамаш къайладаккха",
-       "recentchangeslinked": "Ð\93аÑ\80галон Ñ\85Ñ\83вÑ\86амаш",
+       "recentchangeslinked": "Ð\92IаÑ\88агIдÑ\83взаденна Ð½Ð¸Ð¹Ñ\81даÑ\80аш",
        "recentchangeslinked-feed": "Гаргалон хувцамаш",
        "recentchangeslinked-toolbox": "Укханца вIашагIдувзаденна хувцамаш",
        "recentchangeslinked-title": "$1ца вIашидувзаденна хувцамаш",
        "recentchangeslinked-summary": "Ер, Iинк яь йола оагIув (е укх цатегачу чуйоагIараш), дукха ха йоацаш хьийца оагIувнашкий дагарле я.\n[[Special:Watchlist|Шун теркама дагарленашках]] чуйоагIа оагIувнаш '''белгалаяь я'''.",
-       "recentchangeslinked-page": "ОагIува цIи",
-       "recentchangeslinked-to": "Ð\9eагIÑ\83внаÑ\88 Ñ\82Iа Ñ\85Ñ\83вÑ\86амаÑ\88 Ñ\85Ñ\8cаÑ\85Ñ\8cокÑ\85а, Ñ\85Ñ\8cаÑ\85Ñ\8cекÑ\85а Ð¹Ð¾Ð»Ð° Ð¾Ð°Ð³IÑ\83в Ñ\82Iа IинкаÑ\88 ÐµÑ\88 Ð¹Ð¾Ð»а.",
+       "recentchangeslinked-page": "ОагIон цIи",
+       "recentchangeslinked-to": "Ð\92еÑ\88Ñ\82а, Ð±ÐµÐ»Ð³Ð°Ð»Ñ\8fÑ\8cккÑ\85а Ð¾Ð°Ð³Iон Ñ\82IаÑ\85Ñ\8cожавеÑ\88 Ð´Ð¾Ð»Ð° Ð¾Ð°Ð³IонаÑ\88Ñ\82а Ð´Ð°Ñ\8c Ñ\85Ñ\83вÑ\86амаÑ\88 Ñ\85Ñ\8cаÑ\85Ñ\8cокÑ\85а.",
        "upload": "Файл чуяккха",
        "uploadbtn": "Паьл чуяьккха",
        "uploadlogpage": "Чуяьккхамий тептар",
-       "filedesc": "Ð\9bоаÑ\86а Ð»Ð¾Ð°Ñ\86ам",
+       "filedesc": "Ð\9bоаÑ\86а Ð¹Ð¾Ð°Ð·Ð¾Ð½Ñ\86а Ñ\81Ñ\83Ñ\80Ñ\82 Ð¾Ñ\82Ñ\82адаÑ\80",
        "fileuploadsummary": "Лоаца лоацам:",
        "license": "ЦIийяздар",
-       "license-header": "ЦIийÑ\8fздаÑ\80",
+       "license-header": "Ð\9bиÑ\86ензиÑ\80ование",
        "imgfile": "файл",
        "listfiles": "Паьлий дагарче",
        "listfiles_date": "Денха",
        "filehist-comment": "Белгалдаккхар",
        "imagelinks": "Файлах пайда эцар",
        "linkstoimage": "{{PLURAL:$1|1=ТIехьайоагIача $1 оагIуво тIахьожаву|ТIехьайоагIача $1 оагIувнаша тIахьожаву}} укх файла тIа:",
-       "nolinkstoimage": "Ð\99ола Ð¿Ð°Ñ\8cла Ñ\82Iа  Iинк Ñ\8e Ð¾Ð°Ð³IÑ\83внаÑ\88 Ð´Ð°Ñ\86",
+       "nolinkstoimage": "УкÑ\85 Ñ\84айла Ñ\82IаÑ\85Ñ\8cожавеÑ\88 Ð¹Ð¾Ð»Ð° Ð¾Ð°Ð³IонаÑ\88 Ñ\8fÑ\86.",
        "sharedupload": "Ер паьла $1чера я, кхыча хьахьоадайтамча хьахайраде йийшайолаш я.",
        "sharedupload-desc-here": "Ер файл $1 чура я, иштта кхыйола проекташ чу пайда эца аьттув болаш я.\nЦун [$2 йоазонца сурт оттадара оагIон] информаци кIалхахь хьайоалаяй.",
        "uploadnewversion-linktext": "Укх паьлий керда бIаса чуяьккха",
        "listgrouprights-members": "(тоабий дагарче)",
        "emailuser": "Дакъалаьцархочоа д-хоамни:",
        "watchlist": "Теркама дагарче",
-       "mywatchlist": "ТеÑ\80кама Ð´Ð°Ð³Ð°Ñ\80ле",
+       "mywatchlist": "Ð\97ем Ð±Ð°Ñ\80а Ñ\81пиÑ\81ок",
        "watchlistfor2": "$1 $2 царна",
        "addedwatchtext": "\"[[:$1]]\" оагIув, шун [[Special:Watchlist|теркама дагаршкахь]] чуяккха я. \nТехьара мел йола укх оагIувни хувцамаш цу дагаршкахь хоам беш хургья. Вешта [[Special:RecentChanges|керда хувцама дагаршкаехь]] сома къоалмаца хьакъоастлуш хургья.",
        "removedwatchtext": "\"[[:$1]]\" оагIув, шун [[Special:Watchlist|теркама дарагчера]] дIаяккха хиннай.",
-       "watch": "Зе",
+       "watch": "Зем бе",
        "watchthispage": "Укх оагIува теркам бе",
        "unwatch": "Лора ма де",
        "watchlist-details": "Шун теркама дагарченгахь йола  $1 {{PLURAL:$1|1=оагIув|оагIувнаш}}, дувцама оагIувнаш ца лоархIаш.",
        "deleteotherreason": "Кхыдола бахьан/тIатохар:",
        "deletereasonotherlist": "Кхыдола бахьан",
        "rollbacklink": "юхаяккха",
-       "protectlogpage": "Ð\9bоÑ\80адаÑ\80а тептар",
+       "protectlogpage": "Ð\93Iон тептар",
        "protectedarticle": "\"[[$1]]\" оагIув лорам деж я",
        "modifiedarticleprotection": "\"[[$1]]\" оагIувни лорама лагIа хувцаяьннай",
        "protectcomment": "Бахьан:",
        "sp-contributions-toponly": "ТIехьара доржамаш лоархаш дола хувцамаш мара ма хьокха",
        "sp-contributions-submit": "Хьалáха",
        "whatlinkshere": "Хьожаяргаш укхаза",
-       "whatlinkshere-title": "\"$1\" тIа Iинкаш еш йола оагIувнаш",
+       "whatlinkshere-title": "\"$1\" тIахьожавеш йола оагIонаш",
        "whatlinkshere-page": "ОагIув",
-       "linkshere": "ТIехьайоагIа оагIувнаш тIахьожаву «'''[[:$1]]'''»:",
+       "linkshere": "ТIехьайоагIа оагIонаш тIахьожаву «'''[[:$1]]'''»:",
        "nolinkshere": "'''[[:$1]]''' оагIув тIа, кхыдола оагIувашкара Iинкаш йоацаш я",
-       "isredirect": "ТIаÑ\85Ñ\8cожадаÑ\80ан Ð¾Ð°Ð³IÑ\83в",
+       "isredirect": "оагIÑ\83в-дIа-Ñ\81аÑ\85Ñ\8cожадаÑ\80",
        "istemplate": "юкъейоалаяр",
-       "isimage": "Файлан хьожаярг",
+       "isimage": "Файлови Ñ\82Iахьожаярг",
        "whatlinkshere-prev": "{{PLURAL:$1|1=хьалхайоагIа|хьалхайоагIараш}} $1",
        "whatlinkshere-next": "{{PLURAL:$1|1=тIехьайоагIар|тIехьайоагIараш}} $1",
-       "whatlinkshere-links": "← хьожаяргаш",
-       "whatlinkshere-hideredirs": "$1 дIа-хьа чуяьккхамаш",
-       "whatlinkshere-hidetrans": "$1 чуяьккхамаш",
-       "whatlinkshere-hidelinks": "$1 Iинкаш",
+       "whatlinkshere-links": "â\86\90 Ñ\82IаÑ\85Ñ\8cожаÑ\8fÑ\80гаÑ\88",
+       "whatlinkshere-hideredirs": "ДIакъайладаккха дIа-сахьожадар",
+       "whatlinkshere-hidetrans": "ДIакъайладаккха юкъедахараш",
+       "whatlinkshere-hidelinks": "ДIакъайлаяккха тIахьожаяргаш",
        "whatlinkshere-hideimages": "$1 суртIинкаш",
-       "whatlinkshere-filters": "ЦIенÑ\8aеÑ\80аÑ\88",
+       "whatlinkshere-filters": "ФилÑ\8cÑ\82Ñ\80Ñ\8b",
        "blockip": "Укх {{GENDER:$1|доакъошхочоа}} ч|ега бола",
        "ipboptions": "2 сахьат:2 hours,1 ди:1 day,3 ди:3 days,1 кIира:1 week,2 кIира:2 weeks,1 бутт:1 month,3 бутт:3 months,6 бутт:6 months,1 шу:1 year,сиха ца луш:infinite",
        "ipblocklist": "ЧIега бела дакъалаьцархой",
        "movelogpage": "ЦIи хувцара тептар",
        "movereason": "Бахьан",
        "revertmove": "юхаяьккха",
-       "export": "ОагIувий эхфортам",
+       "export": "ОагIонай экспорт",
        "allmessagesname": "ЦIи",
        "allmessagesdefault": "Сатийна улла яздам",
        "allmessages-filter-all": "Дерригаш",
        "thumbnail-more": "Доккха де",
        "thumbnail_error": "ЗIамигасуртанчий кхеллама гIалат: $1",
        "import-upload-filename": "ПаьлацIи:",
-       "tooltip-pt-userpage": "{{GENDER:|Хьа}} доакъашхочунна оагIув",
+       "tooltip-pt-userpage": "{{GENDER:|Хьа}} доакъашхочун оагIув",
        "tooltip-pt-mytalk": "{{GENDER:|Хьа}} дувца оттадара оагIув",
-       "tooltip-pt-preferences": "{{GENDER:|Ð¥Ñ\8cа Ð¾Ñ\82Ñ\82амаш}}",
-       "tooltip-pt-watchlist": "ОоагIувна дагарле, шо бIаргалокхаш йола",
+       "tooltip-pt-preferences": "{{GENDER:|Ð¥Ñ\8cа Ð³IиÑ\80Ñ\81аш}}",
+       "tooltip-pt-watchlist": "Iа зем бу оагIонаш",
        "tooltip-pt-mycontris": "{{GENDER:|хьа}} хувцамаш",
        "tooltip-pt-login": "Укхаза хьай цIи аьле чувала/яла йиша я, амма из параз дац",
        "tooltip-pt-logout": "Аравала/яла",
        "tooltip-ca-protect": "Eр оагIув лорае",
        "tooltip-ca-delete": "Ер оагIув дIаяькха",
        "tooltip-ca-move": "Укх оагIон цIи хувца",
-       "tooltip-ca-watch": "Ер оагIув хьай теркам беча каьхата тIа тIаяьккха",
+       "tooltip-ca-watch": "Ер оагIув Iайха зувш йолча оагIонашта юкъеяккха",
        "tooltip-ca-unwatch": "Ер оагIув теркам беча каьхата тIара дIаяькха",
        "tooltip-search": "Хьалáха {{grammar:prepositional|{{SITENAME}}}} чу",
        "tooltip-search-go": "Изза мо цӀи йолаш оагӀув тӀa дехьавала",
        "tooltip-t-recentchangeslinked": "Укх оагIуво тIахьожавеш йолча оагIонай тIеххьара хувцамаш",
        "tooltip-feed-rss": "Укх оагIувна RSSчу гойтар",
        "tooltip-feed-atom": "Укх оаг|увна Atomчу гойтар",
-       "tooltip-t-contributions": "{{GENDER:$1|Укх доакъашхочо хийца}} йола оагIувнаш",
+       "tooltip-t-contributions": "{{GENDER:$1|Укх доакъашхочо хийца}} йола оагIонаш",
        "tooltip-t-emailuser": "Укх дакъалаьцархочоа зIы яхьийта",
        "tooltip-t-upload": "Файлаш чуяккха",
        "tooltip-t-specialpages": "ГIулакха оагIувнаш",
        "tooltip-t-print": "Укх оагIон зарба тохара верси",
        "tooltip-t-permalink": "ОагIон укх версин тIахьожавеш йола даим латташ йола хьожаярг",
        "tooltip-ca-nstab-main": "ОагIон чурадар",
-       "tooltip-ca-nstab-user": "Ð\94акÑ\8aалаÑ\8cÑ\86аÑ\80Ñ\85оÑ\87Ñ\83нна Ñ\88ий оагIув",
+       "tooltip-ca-nstab-user": "Ð\94оакÑ\8aаÑ\88Ñ\85оÑ\87Ñ\83н Ñ\88е Ð´Ð¾Ð°Ð»Ð°Ñ\85Ñ\8c Ð¹Ð¾Ð»Ð° оагIув",
        "tooltip-ca-nstab-special": "Ер гIулакха оагIув я, из хувца бокъо яц",
        "tooltip-ca-nstab-project": "Проектан оагIув",
        "tooltip-ca-nstab-image": "Файлан оагӀув",
-       "tooltip-ca-nstab-template": "Ð\9bон оагIув",
+       "tooltip-ca-nstab-template": "Ð\9bеÑ\80а оагIув",
        "tooltip-ca-nstab-help": "ГӀон оагIув",
        "tooltip-ca-nstab-category": "Категорий оагӀув",
        "tooltip-minoredit": "Ер хувцар башха доаца санна белгалде",
-       "tooltip-save": "Ð¥Ñ\8cай Ñ\85Ñ\83вÑ\86амаÑ\88 Ð»Ð¾Ñ\80адеÑ\88 Ð´IаÑ\8fзаде",
-       "tooltip-preview": "Дехар да, оагӀув лораешь дIаязаелехь из мишта я тахка хьалххе хьажарах пайда эцаш!",
+       "tooltip-save": "Хьай хувцамаш лорадеш дIаязде",
+       "tooltip-preview": "Дехар да, оагӀув лораешь дIаязъелехь из мишта я тахка хьалххе хьажарах пайда эцаш!",
        "tooltip-diff": "ДIадолалу текстаца даь хувцамаш хьахьокха",
        "tooltip-compareselectedversions": "Укх оагIувни шин доржамаш тIа юкъера хувцамаш зе.",
        "tooltip-watch": "Ер оагIув теркам беча каьхата тIа яькха",
        "tooltip-rollback": "Цкъа пIелг тоIабе дIадаккха тIехьара редакторас даь хувцамаш",
-       "tooltip-undo": "Даь хувцар дIадаьккха, хьалххе хьажар хьахьокха, дIадаккхара бахьан Iочуязаде аьттув болаш.",
-       "tooltip-summary": "Ð\9bоаÑ\86а Ð¹Ð¾Ð°Ð·Ð¾Ð½Ñ\86а Ñ\81Ñ\83Ñ\80Ñ\82 Ð¾Ñ\82Ñ\82адаÑ\80 IоÑ\87Ñ\83Ñ\8fзаде",
+       "tooltip-undo": "Даь хувцар дIадаьккха, хьалххе бIаргтохар хьахьокха, дIадаккхара бахьан Iочуязде аьттув болаш.",
+       "tooltip-summary": "Лоаца йоазонца сурт оттадар Iочуязде",
        "pageinfo-hidden-categories": "{{PLURAL:$1|1=Къайла категори|Къайла категореш}} ($1)",
        "pageinfo-toolboxlink": "ОагIонах бола хоам",
        "previousdiff": "← Хьалхара нийсдар",
        "metadata-fields": "Укх списке дагaрадаь суртай метахоамай йистош, хьахьекха хургда суртан оагIон тIа, метахоамай таблица хьоарчая йолаш. Юхедиса йистош къайла хургда.\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-imagewidth": "Шерал",
        "exif-imagelength": "Лакхал",
-       "exif-orientation": "Суртан белгало",
+       "exif-orientation": "Сурта белгало",
        "exif-imagedescription": "Сурта цIи",
        "exif-model": "Камера модель",
        "exif-software": "Программни Iалашдар",
        "exif-artist": "Яздархо",
        "exif-exifversion": "Верси Exif",
-       "exif-colorspace": "Ð\91аÑ\81аÑ\80а Ð°Ñ\80е",
+       "exif-colorspace": "Ð\91еÑ\81ай Ð¼Ð¾Ñ\82Ñ\82",
        "exif-pixelxdimension": "Сурта шорал",
        "exif-pixelydimension": "Сурта лакхал",
        "exif-datetimedigitized": "Оцифровк яь таьрахь а, ха а",
index ff60316..3da7522 100644 (file)
        "noname": "Vu ne donis valida uzantonomo.",
        "loginsuccesstitle": "Eniro sucesoza",
        "loginsuccess": "'''Vu eniris a {{SITENAME}} kom \"$1\".'''",
-       "nosuchuser": "Ne existas uzanto \"$1\".\nUzanto-nomi esas mayu/minuskulo-distingenda.\nKontrolez vua espelado, o [[Special:UserLogin/signup|krear nova konto]].",
+       "nosuchuser": "Ne existas uzanto \"$1\".\nUzanto-nomi esas mayu/minuskulo-distingenda.\nKontrolez vua espelado, o [[Special:CreateAccount|krear nova konto]].",
        "nosuchusershort": "Esas nula uzanto \"$1\".\nKontrolez la espelado.",
        "nouserspecified": "Vu mustas specigar uzantonomo.",
        "wrongpassword": "La skribita pasovorto esis nekorekta. Voluntez probar itere.",
index 325f6ad..8908443 100644 (file)
        "noname": "Þú hefur ekki tilgreint gilt notandanafn.",
        "loginsuccesstitle": "Innskráning tókst",
        "loginsuccess": "'''Þú ert nú innskráð(ur) á {{SITENAME}} sem „$1“.'''",
-       "nosuchuser": "Það er enginn notandi með þetta nafn: \"$1\".\nGerður er greinarmunur á há- og lágstöfum.\nAthugaðu hvort um innsláttavillu er að ræða eða [[Special:UserLogin/signup|búðu til nýtt notandanafn]].",
+       "nosuchuser": "Það er enginn notandi með þetta nafn: \"$1\".\nGerður er greinarmunur á há- og lágstöfum.\nAthugaðu hvort um innsláttavillu er að ræða eða [[Special:CreateAccount|búðu til nýtt notandanafn]].",
        "nosuchusershort": "Það er enginn notandi með nafnið „$1“. Athugaðu hvort nafnið sé ritað rétt.",
        "nouserspecified": "Þú verður að taka fram notandanafn.",
        "login-userblocked": "Þessi notandi hefur verið settur í bann.  Innskráning ekki leyfð.",
        "accmailtext": "Lykilorðið fyrir [[User talk:$1|$1]] hefur verið sent á $2. Hægt er að breyta því á síðunni ''[[Special:ChangePassword|breyta lykilorði]]'' þegar notandinn hefur skráð sig inn.",
        "newarticle": "(Ný)",
        "newarticletext": "Þú hefur fylgt tengli á síðu sem ekki er til ennþá.\nÞú getur búið til síðu með þessu nafni með því að skrifa í formið fyrir neðan\n(meiri upplýsingar í [$1 hjálpinni]).\nEf þú hefur óvart villst hingað geturðu notað '''til baka'''-hnappinn í vafranum þínum.",
-       "anontalkpagetext": "----''Þetta er spjallsíða fyrir óþekktan notanda sem hefur ekki búið til aðgang ennþá, eða notar hann ekki.\nÞar af leiðandi þurfum við að nota vistfang til að bera kennsli á hann/hana.\nNokkrir notendur geta deilt sama vistfangi.\nEf þú ert óþekktur notandi og finnst að óviðkomandi athugasemdum hafa verið beint að þér, gjörðu svo vel og [[Special:UserLogin/signup|búðu til aðgang]] eða [[Special:UserLogin|skráðu þig inn]] til þess að koma í veg fyrir þennan rugling við aðra óþekkta notendur í framtíðinni.''",
+       "anontalkpagetext": "----''Þetta er spjallsíða fyrir óþekktan notanda sem hefur ekki búið til aðgang ennþá, eða notar hann ekki.\nÞar af leiðandi þurfum við að nota vistfang til að bera kennsli á hann/hana.\nNokkrir notendur geta deilt sama vistfangi.\nEf þú ert óþekktur notandi og finnst að óviðkomandi athugasemdum hafa verið beint að þér, gjörðu svo vel og [[Special:CreateAccount|búðu til aðgang]] eða [[Special:UserLogin|skráðu þig inn]] til þess að koma í veg fyrir þennan rugling við aðra óþekkta notendur í framtíðinni.''",
        "noarticletext": "Enginn texti er á þessari síðu enn sem komið er.\nÞú getur [[Special:Search/{{PAGENAME}}|leitað að þessum titli]], í öðrum síðum,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leitað í tengdum skrám], eða [{{fullurl:{{FULLPAGENAME}}|action=edit}} búið hana til]</span>.",
        "noarticletext-nopermission": "Það er enginn texti á þessari síðu eins og er.\nÞú getur [[Special:Search/{{PAGENAME}}|leitað að þessum titli]] í öðrum síðum, eða <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leitað í tengdum skrám]</span>, en þú hefur ekki réttindi til þess að stofna þessa síðu.",
        "missing-revision": "Útgáfa #$1 síðunnar „{{FULLPAGENAME}}\" er ekki til.\n\nÞetta gerist oftast þegar úreld breytingaskrá tengir á síðu sem hefur verið eytt. Frekari upplýsingar eru í [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} eyðingarskránni].",
        "upload-form-label-own-work": "Það er mitt eigið verk",
        "upload-form-label-infoform-categories": "Flokkar",
        "upload-form-label-infoform-date": "Dagsetning",
-       "upload-form-label-own-work-message-local": "Ég skil að ég sé að hlaða inn skrá samkvæmt notkunarskilmálum og leyfisskilmálum {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Ef þú getur ekki hlaðið inn þessari skrá samkvæmt reglum {{SITENAME}}, lokaðu þá þessum glugga og reyndu aðra aðferð.",
-       "upload-form-label-not-own-work-local-local": "Þú gætir einnig prófað að nota [[Special:Upload|sjálfgefnu innhleðslusíðuna]].",
-       "upload-form-label-own-work-message-default": "Ég skil að ég sé að hlaða inn skrá á sameiginlegt vefsvæði. Ég staðfesti að ég sé að gera það samkvæmt notkunarskilmálum og leyfisskilmálum þess.",
-       "upload-form-label-not-own-work-message-default": "Ef þú getur ekki hlaðið inn þessari skrá samkvæmt reglum sameiginlega vefsvæðisins, lokaðu þá þessum glugga og reyndu aðra aðferð.",
-       "upload-form-label-not-own-work-local-default": "Þú gætir einnig prófað að nota [[Special:Upload|innhleðslusíðuna á {{SITENAME}}]], ef það má hlaða þessari skrá inn samkvæmt reglum þeirra.",
-       "upload-form-label-own-work-message-shared": "Ég staðfesti að ég eigi höfundarréttinn að þessari skrá og samþykki óafturkræft að gefa þessa skrá til Wikimedia Commons undir  [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] leyfi. Ég samþykki [https://wikimediafoundation.org/wiki/Terms_of_Use notendaskilmálana].",
-       "upload-form-label-not-own-work-message-shared": "Ef þú átt ekki höfundarréttinn að þessari skrá, eða þú vilt gefa það út undir öðru leyfi, prófaðu  [https://commons.wikimedia.org/wiki/Special:UploadWizard Innsendingaálfinn á Commons].",
-       "upload-form-label-not-own-work-local-shared": "Þú gætir einnig prófað að nota [[Special:Upload|innhleðslusíðuna á {{SITENAME}}]], ef það má hlaða þessari skrá inn samkvæmt reglum þeirra.",
+       "upload-form-label-own-work-message-generic-local": "Ég skil að ég sé að hlaða inn skrá samkvæmt notkunarskilmálum og leyfisskilmálum {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Ef þú getur ekki hlaðið inn þessari skrá samkvæmt reglum {{SITENAME}}, lokaðu þá þessum glugga og reyndu aðra aðferð.",
+       "upload-form-label-not-own-work-local-generic-local": "Þú gætir einnig prófað að nota [[Special:Upload|sjálfgefnu innhleðslusíðuna]].",
+       "upload-form-label-own-work-message-generic-foreign": "Ég skil að ég sé að hlaða inn skrá á sameiginlegt vefsvæði. Ég staðfesti að ég sé að gera það samkvæmt notkunarskilmálum og leyfisskilmálum þess.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Ef þú getur ekki hlaðið inn þessari skrá samkvæmt reglum sameiginlega vefsvæðisins, lokaðu þá þessum glugga og reyndu aðra aðferð.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Þú gætir einnig prófað að nota [[Special:Upload|innhleðslusíðuna á {{SITENAME}}]], ef það má hlaða þessari skrá inn samkvæmt reglum þeirra.",
        "backend-fail-stream": "Gat ekki streymt skránni „$1“.",
        "backend-fail-backup": "Öryggisafritun skrárinnar $1 mistókst.",
        "backend-fail-notexists": "Skráin $1 er ekki til.",
index fe2cd10..7c5e869 100644 (file)
        "noname": "Il nome utente indicato non è valido.",
        "loginsuccesstitle": "Accesso effettuato",
        "loginsuccess": "'''Sei stato connesso al server di {{SITENAME}} con il nome utente di \"$1\".'''",
-       "nosuchuser": "Non è registrato alcun utente di nome \"$1\".\nI nomi utente sono sensibili alle maiuscole.\nVerificare il nome inserito o [[Special:UserLogin/signup|creare una nuova utenza]].",
+       "nosuchuser": "Non è registrato alcun utente di nome \"$1\".\nI nomi utente sono sensibili alle maiuscole.\nVerificare il nome inserito o [[Special:CreateAccount|creare una nuova utenza]].",
        "nosuchusershort": "Non è registrato alcun utente di nome \"$1\". Verificare il nome inserito.",
        "nouserspecified": "È necessario specificare un nome utente.",
        "login-userblocked": "Questa utenza è bloccata. Non è possibile effettuare il login.",
        "accmailtext": "Una password generata casualmente per [[User talk:$1|$1]] è stata inviata a $2. Questa password può essere modificata nella pagina per ''[[Special:ChangePassword|cambiare la password]]'' subito dopo l'accesso.",
        "newarticle": "(Nuovo)",
        "newarticletext": "Il collegamento appena seguito corrisponde ad una pagina non ancora esistente.\nSe vuoi creare la pagina ora, basta cominciare a scrivere il testo nella casella qui sotto (vedi la [$1 pagina di aiuto] per maggiori informazioni).\nSe il collegamento è stato aperto per errore, è sufficiente fare clic sul pulsante <strong>Indietro</strong> del proprio browser.",
-       "anontalkpagetext": "----\n''Questa è la pagina di discussione di un utente anonimo, che non ha ancora creato un'utenza o comunque non la sta usando. Per identificarlo è quindi necessario usare il numero del suo indirizzo IP. Gli indirizzi IP possono però essere condivisi da più utenti. Se sei un utente anonimo e ritieni che i commenti presenti in questa pagina non si riferiscano a te, [[Special:UserLogin/signup|crea una nuova utenza]] o [[Special:UserLogin|entra con quella che già hai]] per evitare di essere confuso con altri utenti anonimi in futuro.''",
+       "anontalkpagetext": "----\n''Questa è la pagina di discussione di un utente anonimo, che non ha ancora creato un'utenza o comunque non la sta usando. Per identificarlo è quindi necessario usare il numero del suo indirizzo IP. Gli indirizzi IP possono però essere condivisi da più utenti. Se sei un utente anonimo e ritieni che i commenti presenti in questa pagina non si riferiscano a te, [[Special:CreateAccount|crea una nuova utenza]] o [[Special:UserLogin|entra con quella che già hai]] per evitare di essere confuso con altri utenti anonimi in futuro.''",
        "noarticletext": "In questo momento la pagina richiesta è vuota.\nPuoi [[Special:Search/{{PAGENAME}}|cercare questo titolo]] nelle altre pagine del sito, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercare nei registri correlati] oppure [{{fullurl:{{FULLPAGENAME}}|action=edit}} creare questa pagina]</span>.",
        "noarticletext-nopermission": "In questo momento la pagina richiesta è vuota. È possibile [[Special:Search/{{PAGENAME}}|cercare questo titolo]] nelle altre pagine del sito o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercare nei registri correlati]</span>, ma non hai i permessi per creare questa pagina.",
        "missing-revision": "La versione #$1 della pagina \"{{FULLPAGENAME}}\" non esiste.\n\nQuesto si verifica solitamente seguendo un collegamento a una pagina cancellata, in una cronologia non aggiornata.\nI dettagli possono essere trovati nel [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro delle cancellazioni].",
        "right-override-export-depth": "Esporta le pagine includendo le pagine collegate fino ad una profondità di 5",
        "right-sendemail": "Invia email ad altri utenti",
        "right-passwordreset": "Vede i messaggi di reimpostazione della password",
-       "right-managechangetags": "Crea ed elimina dal database i [[Special:Tags|tag]]",
+       "right-managechangetags": "Crea e attiva/disattiva le [[Special:Tags|etichette]]",
        "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",
+       "right-deletechangetags": "Cancella le [[Special:Tags|etichette]] dal database",
        "grant-generic": "Pacchetto diritti \"$1\"",
        "grant-group-page-interaction": "Interagisce con le pagine",
        "grant-group-file-interaction": "Interagisce con i file multimediali",
        "action-viewmyprivateinfo": "vedere i propri dati personali",
        "action-editmyprivateinfo": "modificare i propri dati personali",
        "action-editcontentmodel": "modificare il modello di contenuto di una pagina",
-       "action-managechangetags": "crea ed elimina i tag dal database",
+       "action-managechangetags": "creare e attivare/disattivare le etichette",
        "action-applychangetags": "applicare delle etichette alle tue modifiche",
        "action-changetags": "aggiungere o rimuovere specifiche etichette su singole versioni o voci di registro",
+       "action-deletechangetags": "cancellare le etichette dal database",
        "nchanges": "$1 {{PLURAL:$1|modifica|modifiche}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|dall'ultima visita}}",
        "enhancedrc-history": "cronologia",
        "upload-form-label-own-work": "Questo è un mio lavoro",
        "upload-form-label-infoform-categories": "Categorie",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Confermo che sto caricando questo file seguendo le condizioni di servizio e le politiche sulle license di {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Se non si è in grado di caricare il file in base alle politiche di {{SITENAME}}, chiudi questa finestra e prova un altro metodo.",
-       "upload-form-label-not-own-work-local-local": "Puoi anche provare la [[Special:Upload|pagina di caricamento predefinita]].",
-       "upload-form-label-own-work-message-default": "Ho capito che sto caricando questo file in un archivio condiviso. Confermo che lo sto facendo secondo le condizioni di servizio e le politiche sulle license di lì.",
-       "upload-form-label-not-own-work-message-default": "Se non si è in grado di caricare il file in base alle politiche dell'archivio condiviso, chiudi questa finestra e prova un altro metodo.",
-       "upload-form-label-not-own-work-local-default": "Puoi anche provare ad usare la [[Special:Upload|pagina di caricamento su {{SITENAME}}]], se questo file può essere caricato in base alle sue politiche.",
-       "upload-form-label-own-work-message-shared": "Attesto che possiedo i diritti d'autore su questo file, e accetto irrevocabilmente il rilascio di questo file su Wikimedia Commons in base alla licenza [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribuzione-Condividi allo stesso modo 4.0], e accetto le [https://wikimediafoundation.org/wiki/Terms_of_Use Condizioni d'uso].",
-       "upload-form-label-not-own-work-message-shared": "Se non possiedi i diritti d'autore su questo file, o se lo vuoi rilasciare con una licenza diversa, usa il [https://commons.wikimedia.org/wiki/Special:UploadWizard caricamento guidato di Commons].",
-       "upload-form-label-not-own-work-local-shared": "Puoi anche provare ad usare la [[Special:Upload|pagina di caricamento su {{SITENAME}}]], se il sito consente il caricamento di questo file in base alle sue politiche.",
+       "upload-form-label-own-work-message-generic-local": "Confermo che sto caricando questo file seguendo le condizioni di servizio e le politiche sulle license di {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Se non si è in grado di caricare il file in base alle politiche di {{SITENAME}}, chiudi questa finestra e prova un altro metodo.",
+       "upload-form-label-not-own-work-local-generic-local": "Puoi anche provare la [[Special:Upload|pagina di caricamento predefinita]].",
+       "upload-form-label-own-work-message-generic-foreign": "Ho capito che sto caricando questo file in un archivio condiviso. Confermo che lo sto facendo secondo le condizioni di servizio e le politiche sulle license di lì.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Se non si è in grado di caricare il file in base alle politiche dell'archivio condiviso, chiudi questa finestra e prova un altro metodo.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Puoi anche provare ad usare la [[Special:Upload|pagina di caricamento su {{SITENAME}}]], se questo file può essere caricato in base alle sue politiche.",
        "backend-fail-stream": "Impossibile trasmettere il file $1.",
        "backend-fail-backup": "Impossibile eseguire il backup del file $1 .",
        "backend-fail-notexists": "Il file $1 non esiste.",
        "whatlinkshere-prev": "{{PLURAL:$1|precedente|precedenti $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|successivo|successivi $1}}",
        "whatlinkshere-links": "← collegamenti",
-       "whatlinkshere-hideredirs": "$1 redirect",
-       "whatlinkshere-hidetrans": "$1 inclusioni",
-       "whatlinkshere-hidelinks": "$1 collegamenti",
-       "whatlinkshere-hideimages": "$1 link da file",
+       "whatlinkshere-hideredirs": "Nascondi redirect",
+       "whatlinkshere-hidetrans": "Nascondi inclusioni",
+       "whatlinkshere-hidelinks": "Nascondi collegamenti",
+       "whatlinkshere-hideimages": "Nascondi collegamenti da file",
        "whatlinkshere-filters": "Filtri",
        "whatlinkshere-submit": "Vai",
        "autoblockid": "Autoblocco #$1",
        "autoredircomment": "Redirect alla pagina [[$1]]",
        "autosumm-new": "Creata pagina con \"$1\"",
        "autosumm-newblank": "Creata pagina vuota",
-       "size-bytes": "$1 byte",
+       "size-bytes": "$1 {{PLURAL:$1|byte}}",
        "lag-warn-normal": "Le modifiche apportate {{PLURAL:$1|nell'ultimo secondo|negli ultimi $1 secondi}} potrebbero non apparire in questa lista.",
        "lag-warn-high": "A causa di un eccessivo ritardo nell'aggiornamento del server di database, le modifiche apportate {{PLURAL:$1|nell'ultimo secondo|negli ultimi $1 secondi}} potrebbero non apparire in questa lista.",
        "watchlistedit-normal-title": "Modifica osservati speciali",
        "tags-activate": "attiva",
        "tags-deactivate": "disattiva",
        "tags-hitcount": "$1 {{PLURAL:$1|modifica|modifiche}}",
-       "tags-manage-no-permission": "Non hai il permesso di gestire il cambiamento tag.",
+       "tags-manage-no-permission": "Non si dispone dei permessi necessari per gestire le etichette di modifica.",
        "tags-manage-blocked": "Non puoi gestire le etichette alle modifiche mentre sei bloccato.",
        "tags-create-heading": "Crea un nuovo tag",
        "tags-create-explanation": "Per impostazione predefinita, i tag appena creati saranno disponibili per l'utilizzo di utenti e bot.",
        "tags-delete-not-found": "Il tag \"$1\" non esiste.",
        "tags-delete-too-many-uses": "Il tag \"$1\" è applicato a più di $2 {{PLURAL:$2|revisione|revisioni}}, il che significa che non può essere eliminato.",
        "tags-delete-warnings-after-delete": "L'etichetta \"$1\" è stata cancellata, ma fai attenzione {{PLURAL:$2|al seguente avviso|ai seguenti avvisi}}:",
+       "tags-delete-no-permission": "Non si dispone dei permessi necessari per cancellare le etichette di modifica.",
        "tags-activate-title": "Attiva tag",
        "tags-activate-question": "Stai per attivare il tag \"$1\".",
        "tags-activate-reason": "Motivo:",
        "tags-apply-blocked": "Non puoi applicare le etichette alle modifiche mentre sei bloccato.",
        "tags-apply-not-allowed-one": "L'etichetta \"$1\" non può essere applicata manualmente.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|La seguente etichetta non può essere applicata|Le seguenti etichette non possono essere applicate}}  manualmente: $1",
-       "tags-update-no-permission": "Non hai il permesso di aggiungere o rimuovere modifiche di tag dalle singole revisioni o voci di registro.",
+       "tags-update-no-permission": "Non si dispone dei permessi necessari per aggiungere o rimuovere le etichette di modifica dalle singole versioni o voci di registro.",
        "tags-update-blocked": "Non puoi aggiungere o rimuovere le etichette alle modifiche mentre sei bloccato.",
        "tags-update-add-not-allowed-one": "Il tag \"$1\" non può essere aggiunto manualmente.",
        "tags-update-add-not-allowed-multi": "{{PLURAL:$2|Il seguente tag non può essere aggiunto|I seguenti tag non possono essere aggiunti}} manualmente: $1",
        "api-error-nomodule": "Errore interno: non è stato impostato il modulo di caricamento.",
        "api-error-ok-but-empty": "Errore interno: nessuna risposta dal server.",
        "api-error-overwrite": "Sovrascrivere un file esistente non è consentito.",
+       "api-error-ratelimited": "Stai cercando di caricare più file in meno tempo di quanto questo wiki permette.\nRiprova tra pochi minuti.",
        "api-error-stashfailed": "Errore interno: il server non è riuscito a memorizzare il documento temporaneo.",
        "api-error-publishfailed": "Errore interno: il server non è riuscito a pubblicare il documento temporaneo.",
        "api-error-stasherror": "Si è verificato un errore durante il caricamento del file in stash.",
index 1cdd9dc..4f85d2c 100644 (file)
@@ -69,7 +69,8 @@
                        "Sujiniku",
                        "Azeha",
                        "Kana Higashikawa",
-                       "Shield-9"
+                       "Shield-9",
+                       "Waiesu"
                ]
        },
        "tog-underline": "リンクの下線:",
        "noname": "有効な利用者名が指定されていません。",
        "loginsuccesstitle": "ログイン済み",
        "loginsuccess": "<strong>{{SITENAME}}に「$1」としてログインしました。</strong>",
-       "nosuchuser": "「$1」という名前の利用者は見当たりません。\n利用者名では大文字と小文字を区別します。\n綴りが正しいことを確認するか、[[Special:UserLogin/signup|新たにアカウントを作成]]してください。",
+       "nosuchuser": "「$1」という名前の利用者は見当たりません。\n利用者名では大文字と小文字を区別します。\n綴りが正しいことを確認するか、[[Special:CreateAccount|新たにアカウントを作成]]してください。",
        "nosuchusershort": "「$1」という名前の利用者は存在しません。\n綴りを確認してください。",
        "nouserspecified": "利用者名を指定してください。",
        "login-userblocked": "この利用者はブロックされています。ログインは拒否されます。",
        "accmailtext": "[[User talk:$1|$1]]のために無作為に生成したパスワードを、$2に送信しました。パスワードは、ログインした際に<em>[[Special:ChangePassword|パスワード変更]]</em>ページで変更できます。",
        "newarticle": "(新)",
        "newarticletext": "まだ存在しないページへのリンクをたどりました。\nこのページを新規作成するには、ページの内容を以下のボックスに記入してください (詳しくは[$1 ヘルプページ]を参照してください)。\n誤ってこのページにたどり着いた場合には、ブラウザーの<strong>戻る</strong>ボタンで前のページに戻ってください。",
-       "anontalkpagetext": "----\n<em>このページはアカウントをまだ作成していないか使用していない匿名利用者のための議論ページです。</em>\n\n匿名利用者を識別するために、利用者名の代わりにIPアドレスが使用されています。IP アドレスは複数の利用者で共有されている場合があります。もし、あなたが匿名利用者であり、自分に関係のないコメントが寄せられていると考えられる場合は、[[Special:UserLogin/signup|アカウントを作成する]]か[[Special:UserLogin|ログインして]]他の匿名利用者と間違えられないようにしてください。",
+       "anontalkpagetext": "----\n<em>このページはアカウントをまだ作成していないか使用していない匿名利用者のための議論ページです。</em>\n\n匿名利用者を識別するために、利用者名の代わりにIPアドレスが使用されています。IP アドレスは複数の利用者で共有されている場合があります。もし、あなたが匿名利用者であり、自分に関係のないコメントが寄せられていると考えられる場合は、[[Special:CreateAccount|アカウントを作成する]]か[[Special:UserLogin|ログインして]]他の匿名利用者と間違えられないようにしてください。",
        "noarticletext": "現在このページには内容がありません。\n他のページ内で[[Special:Search/{{PAGENAME}}|このページ名を検索]]、\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 関連する記録を検索]、\nまたは[{{fullurl:{{FULLPAGENAME}}|action=edit}} このページを作成]</span>できます。",
        "noarticletext-nopermission": "現在このページには内容がありません。\n他のページ内で[[Special:Search/{{PAGENAME}}|このページ名を検索]]、または<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 関連する記録を検索]</span>できますが、あなたにはこのページを作成する権限がありません。",
        "missing-revision": "「{{FULLPAGENAME}}」というページの版番号 $1 の版は存在しません。\n\n通常、削除されたページの版への古い差分表示や固定リンクをたどった際に、このようなことが起きます。 \n詳細は[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 削除記録]を参照してください。",
        "content-model-css": "CSS",
        "content-json-empty-object": "空のオブジェクト",
        "content-json-empty-array": "空の配列",
-       "duplicate-args-warning": "<strong>警告:</strong> [[:$1]]は「$3」パラメータの値が複数存在する[[:$2]]を呼び出しています。提供されている最後の値のみが使用されます。",
+       "duplicate-args-warning": "<strong>警告:</strong> [[:$1]]は複数の「$3」パラメータを伴って[[:$2]]を呼び出しています。提供されている最後の値のみが使用されます。",
        "duplicate-args-category": "テンプレート呼び出しで引数が重複しているページ",
        "duplicate-args-category-desc": "引数が重複したテンプレート呼び出しを含むページ。例: <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code>、<code><nowiki>{{foo|bar|1=baz}}</nowiki></code>",
        "expensive-parserfunction-warning": "<strong>警告:</strong> このページでは、高負荷なパーサー関数の呼び出し回数が多過ぎます。\n\n{{PLURAL:$2|呼び出しを $2 回}}未満にしてください ({{PLURAL:$1|現在は $1 回}})。",
        "upload-form-label-own-work": "これはあなた自身による作業です",
        "upload-form-label-infoform-categories": "カテゴリ",
        "upload-form-label-infoform-date": "日付",
-       "upload-form-label-own-work-message-local": "私は {{SITENAME}} 上での以下の利用規約とライセンス方針で、このファイルをアップロードしていることを確認します。",
-       "upload-form-label-not-own-work-message-local": "もし、あなたは {{SITENAME}} の方針の下で、このファイルをアップロードすることができない場合には、このダイアログを閉じて、別の方法をお試しください。",
-       "upload-form-label-not-own-work-local-local": "また、[[Special:Upload|デフォルトのアップロードページ]]を試してみてください。",
-       "upload-form-label-own-work-message-default": "私は共有リポジトリにこのファイルをアップロードしていることを理解しています。私は、そこにサービスやライセンス方針を以下のようにやっていることを、確認します。",
-       "upload-form-label-not-own-work-message-default": "もし、あなたは共有リポジトリの方針の下で、このファイルをアップロードすることができない場合には、このダイアログを閉じて、別の方法をお試しください。",
-       "upload-form-label-not-own-work-local-default": "このファイルはその方針の下でそこにアップロードすることができれば、また、 [[Special:Upload|the upload page on {{SITENAME}}]]を使用してみてください",
-       "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 Terms of Use] に同意します。",
-       "upload-form-label-not-own-work-message-shared": "このファイルの著作権を所有していない場合、または別のライセンスの下でそれをリリースしたい場合には、 [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard] を使用することを検討してください。",
-       "upload-form-label-not-own-work-local-shared": "もしサイトが、それらの方針の下にて、このファイルのアップロードを許可している場合は、[[Special:Upload|{{SITENAME}}上でのアップロードページ]]の利用も検討できます。",
+       "upload-form-label-own-work-message-generic-local": "私は {{SITENAME}} 上での以下の利用規約とライセンス方針で、このファイルをアップロードしていることを確認します。",
+       "upload-form-label-not-own-work-message-generic-local": "もし、あなたは {{SITENAME}} の方針の下で、このファイルをアップロードすることができない場合には、このダイアログを閉じて、別の方法をお試しください。",
+       "upload-form-label-not-own-work-local-generic-local": "また、[[Special:Upload|デフォルトのアップロードページ]]を試してみてください。",
+       "upload-form-label-own-work-message-generic-foreign": "私は共有リポジトリにこのファイルをアップロードしていることを理解しています。私は、そこにサービスやライセンス方針を以下のようにやっていることを、確認します。",
+       "upload-form-label-not-own-work-message-generic-foreign": "もし、あなたは共有リポジトリの方針の下で、このファイルをアップロードすることができない場合には、このダイアログを閉じて、別の方法をお試しください。",
+       "upload-form-label-not-own-work-local-generic-foreign": "このファイルはその方針の下でそこにアップロードすることができれば、また、 [[Special:Upload|the upload page on {{SITENAME}}]]を使用してみてください",
        "backend-fail-stream": "ファイル $1 をストリームできませんでした。",
        "backend-fail-backup": "ファイル $1 をバックアップできませんでした。",
        "backend-fail-notexists": "ファイル $1 は存在しません。",
        "whatlinkshere-prev": "前の$1件",
        "whatlinkshere-next": "次の$1件",
        "whatlinkshere-links": "← リンク",
-       "whatlinkshere-hideredirs": "転送ページを$1",
-       "whatlinkshere-hidetrans": "参照読み込みを$1",
-       "whatlinkshere-hidelinks": "リンクを$1",
-       "whatlinkshere-hideimages": "ファイルへのリンクを$1",
+       "whatlinkshere-hideredirs": "転送ページを非表示",
+       "whatlinkshere-hidetrans": "参照読み込みを非表示",
+       "whatlinkshere-hidelinks": "リンクを非表示",
+       "whatlinkshere-hideimages": "ファイルへのリンクを非表示",
        "whatlinkshere-filters": "絞り込み",
        "whatlinkshere-submit": "実行",
        "autoblockid": "自動ブロック #$1",
        "tags-delete-not-found": "タグ「$1」は存在しません。",
        "tags-delete-too-many-uses": "タグ「$1」は少なくとも$2版に付与されており、削除できません。",
        "tags-delete-warnings-after-delete": "タグ「$1」の削除しましたが、以下の{{PLURAL:$2|警告}}が発生しました:",
+       "tags-delete-no-permission": "変更タグを削除する権限がありません。",
        "tags-activate-title": "タグの有効化",
        "tags-activate-question": "タグ「$1」を有効化しようとしています。",
        "tags-activate-reason": "理由:",
index 34bd816..34241c6 100644 (file)
        "noname": "Yu no spesifai no valid yuuza niem.",
        "loginsuccesstitle": "Lagiin soksesful",
        "loginsuccess": "'''Yu nou lag iin tu {{SITENAME}} az \"$1\".'''",
-       "nosuchuser": "No yuuza no de bai di niem \"$1\".\nYuuza niem kies sensitiv.\nChek yu spelin, ar [[Special:UserLogin/signup|kriet a nyuu akount]].",
+       "nosuchuser": "No yuuza no de bai di niem \"$1\".\nYuuza niem kies sensitiv.\nChek yu spelin, ar [[Special:CreateAccount|kriet a nyuu akount]].",
        "nosuchusershort": "No yuuza no de bai di niem \"$1\".\nChek yu spelin.",
        "nouserspecified": "Yu afi spesifai a yuuzaniem.",
        "login-userblocked": "Dis yuuza blak. No lagiin no lou.",
        "accmailtext": "A random jinariet paaswod fi [[User talk:$1|$1]] sen tu $2.\n\nDi paaswod fi dis nyuu akount kiahn chienj a di ''[[Special:ChangePassword|chienj paaswod]]'' piej afta yu lag iin.",
        "newarticle": "(Nyuu)",
        "newarticletext": "Yu fala lingk tu piej we no egzis yet.\nFi kriet di piej, taat taip ina di bax biluo (si di [$1 elp piej] fi muo infamieshan).\nEf yu de ya by mistiek, klik yu brouza '''bak''' botn.",
-       "anontalkpagetext": "----''Dis a di diskoshan piej fi ananimos yuuza uu no kriet no akount yet, ar uu no yuuzi.\nWi dierfuor afi yuuz di nyuumerikal IP ajres fi aidentifai im/ar.\nSoch a IP ajres kiahn shier bai sebral yuuza.\nEf yu a ananimos yuuza ahn fiil se irelivant kament dairek tu yu, begyu [[Special:UserLogin/signup|kriet a akount]] ar [[Special:UserLogin|lag iin]] fi avaid fyuucha kanfyuujan wid ada ananimos yuuza.''",
+       "anontalkpagetext": "----''Dis a di diskoshan piej fi ananimos yuuza uu no kriet no akount yet, ar uu no yuuzi.\nWi dierfuor afi yuuz di nyuumerikal IP ajres fi aidentifai im/ar.\nSoch a IP ajres kiahn shier bai sebral yuuza.\nEf yu a ananimos yuuza ahn fiil se irelivant kament dairek tu yu, begyu [[Special:CreateAccount|kriet a akount]] ar [[Special:UserLogin|lag iin]] fi avaid fyuucha kanfyuujan wid ada ananimos yuuza.''",
        "noarticletext": "Korentli no tex no de ina dis piej.\nYu kiahn [[Special:Search/{{PAGENAME}}|saach fi dis piej taikl]] ina ada piej,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} saach di rilietid lagdem],\nar [{{fullurl:{{FULLPAGENAME}}|action=edit}} edit dis piej]</span>.",
        "noarticletext-nopermission": "Korantli no tex no de ina dis piej.\nYu kiah [[Special:Search/{{PAGENAME}}|saach fi dis piej taikl]] ina ada piej, ar <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} saach di rilietid lagdem]</span>, bot yu not ab pomishan fi kriet dis piej.‎",
        "userpage-userdoesnotexist": "Yuuza akount \"<nowiki>$1</nowiki>\" no rejista.\nBegyu chek ef yu waahn fi kriet/edit dis piej.",
index a20bb2f..874055f 100644 (file)
        "disclaimerpage": "Project:Sélakan umum",
        "edithelp": "Pitulung besut",
        "helppage-top-gethelp": "Pitulung",
-       "mainpage": "Kaca Pokok",
-       "mainpage-description": "Kaca pokok",
+       "mainpage": "Tepas",
+       "mainpage-description": "Tepas",
        "policy-url": "Project:Kabijakan",
        "portal": "Gapura paguyuban",
        "portal-url": "Project:Garupa paguyuban",
        "youhavenewmessagesmanyusers": "Sampéyang nduwé $1 saka akèh panganggo ($2).",
        "newmessageslinkplural": "{{PLURAL:$1|layang anyar|999=layang anyar}}",
        "newmessagesdifflinkplural": "{{PLURAL:$1|owahan|999=owahan}} pungkasan",
-       "youhavenewmessagesmulti": "Panjenengan olèh pesen-pesen anyar $1",
+       "youhavenewmessagesmulti": "Sampéyan éntuk nawala anyar ing $1",
        "editsection": "besut",
        "editold": "besut",
        "viewsourceold": "deleng sumber",
        "nstab-template": "Cithakan",
        "nstab-help": "Kaca pitulung",
        "nstab-category": "Kategori",
-       "mainpage-nstab": "Kaca pokok",
+       "mainpage-nstab": "Tepas",
        "nosuchaction": "Ora ana lelakon mangkono",
        "nosuchactiontext": "Pratingkah sing dirinci déning URL ora sah.\nPanjenengan manawa salah ketik nalika ngisi URL, utawa salah ngisi pranala.\nIki manawa uga nuduhaké anané kesalahan ing piranti alus sing dipigunakaké déning {{SITENAME}}.",
        "nosuchspecialpage": "Ora ana kaca mirunggan mangkono",
        "noname": "Asma panganggo sing panjenengan pilih ora sah.",
        "loginsuccesstitle": "Kasil mlebu",
        "loginsuccess": "'''Panjenengan saiki mlebu ing {{SITENAME}} kanthi asma \"$1\".'''",
-       "nosuchuser": "Ora ana panganggo mawa asma \"$1\".\nJeneng panganggo iku mbédakaké kapitalisasi.\nCoba dipriksa manèh pasang aksarané, utawa [[Special:UserLogin/signup|gawé akun anyar]].",
+       "nosuchuser": "Ora ana panganggo mawa asma \"$1\".\nJeneng panganggo iku mbédakaké kapitalisasi.\nCoba dipriksa manèh pasang aksarané, utawa [[Special:CreateAccount|gawé akun anyar]].",
        "nosuchusershort": "Ora ana panganggo mawa asma \"$1\". Coba dipriksa manèh pasang aksarané (éjaané).",
        "nouserspecified": "Panjenengan kudu milih asma panganggo.",
        "login-userblocked": "Panganggo iki pinalangan. Ora kena mbelu.",
        "accmailtext": "Sawijining tembung sandi sembarang kanggo [[User talk:$1|$1]] wis dikirim menyang $2.\n\nTembung sandi kanggo panganggo anyar iki isa diganti ing kaca ''[[Special:ChangePassword|ganti tembung sandi]]'' sawisé mlebu log.",
        "newarticle": "(Anyar)",
        "newarticletext": "Katonané panjenengan ngetutaké pranala artikel sing durung ana.\nManawa kersa manulis artikel iki, manggaa. (Mangga mirsani [$1 Pitulung] kanggo informasi sabanjuré).\nYèn ora sengaja tekan kéné, bisa ngeklik pencètan '''back''' waé ing panjlajah wèb panjenengan.",
-       "anontalkpagetext": "---- ''Iki yaiku kaca dhiskusi sawijining panganggo anonim sing durung kagungan akun utawa ora nganggo akuné, dadi kita keeksa kudu nganggo alamat IP-né kanggo nepangi. Alamat IP kaya mengkéné iki bisa dienggo déning panganggo sing séjé-séjé. Yèn panjenengan pancèn panganggo anonim lan olèh komentar-komentar miring, mangga [[Special:UserLogin/signup|nggawé akun]] utawa [[Special:UserLogin|log mlebu]] supaya ora rancu karo panganggo anonim liyané ing mangsa ngarep.''",
+       "anontalkpagetext": "---- ''Iki yaiku kaca dhiskusi sawijining panganggo anonim sing durung kagungan akun utawa ora nganggo akuné, dadi kita keeksa kudu nganggo alamat IP-né kanggo nepangi. Alamat IP kaya mengkéné iki bisa dienggo déning panganggo sing séjé-séjé. Yèn panjenengan pancèn panganggo anonim lan olèh komentar-komentar miring, mangga [[Special:CreateAccount|nggawé akun]] utawa [[Special:UserLogin|log mlebu]] supaya ora rancu karo panganggo anonim liyané ing mangsa ngarep.''",
        "noarticletext": "Kala saiki kaca iki durung ana tulisané.\nSampéyan bisa [[Special:Search/{{PAGENAME}}|nggolèki sesirahing kaca iki]] sajeroning kaca liya,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} nggolèki log sing magepokan],\nutawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} nggawé kaca iki]</span>.",
        "noarticletext-nopermission": "Saiki ora ana tèks ing kaca iki. \nSampéyan bisa [[Special:Search/{{PAGENAME}}|nggolèki judhul kaca iki]] nèng kaca liya, \nutawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|kaca={{urlencode:{{FULLPAGENAME}}}}}} nggolèki log sing kaitan]</span>, nanging Sampéyan ora nduwèni idin nggawé kaca iki.",
        "missing-revision": "Benahan #$1 saka kaca ajeneng \"{{FULLPAGENAME}}\" ora ana.\n\nIki biasané kasebabaké pranala riwayat sing kedaluwarsa saka kaca kuwi wis dibusak.\nRinciané bisa ditemokaké nèng [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} log pambusakan].",
        "recentchanges-label-minor": "Iki besutan cilik",
        "recentchanges-label-bot": "Besutan iki diayahi bot",
        "recentchanges-label-unpatrolled": "Besutan iki durung kapatroli",
-       "recentchanges-label-plusminus": "Ukuraning kaca diowah kanthi cacahing bèt samangkéné",
+       "recentchanges-label-plusminus": "Ukuran owahing kaca ing bèt",
        "recentchanges-legend-heading": "<strong>Legendha:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (uga deleng [[Special:NewPages|pratélaning kaca-kaca anyar]])",
        "recentchanges-legend-plusminus": "(''±123'')",
        "doubleredirects": "Pangalihan dobel",
        "doubleredirectstext": "Kaca iki ngandhut daftar kaca sing ngalih ing kaca pangalihan liyané.\nSaben baris ngandhut pranala menyang pangalihan kapisan lan kapindho, sarta tujuan saka pangalihan kapindho, sing biasané kaca tujuan sing \"sajatiné\", yakuwi pangalihan kapisan kuduné dialihaké menyang kaca tujuan iku.\nJeneng sing wis <del>dicorèk</del> tegesé wis rampung didandani.",
        "double-redirect-fixed-move": "[[$1]] wis kapindhahaké, saiki dadi kaca peralihan menyang [[$2]]",
-       "double-redirect-fixed-maintenance": "Mbenakaké rong pangalihan saka [[$1]] nèng [[$2]].",
+       "double-redirect-fixed-maintenance": "Otomatis ndandani lih-lihan dhobel saka [[$1]] nyang [[$2]] nalika ana opèn-opènan.",
        "double-redirect-fixer": "Révisi pangalihan",
        "brokenredirects": "Pangalihan rusak",
        "brokenredirectstext": "Pengalihan ing ngisor iki tumuju menyang kaca sing ora ana:",
        "wantedtemplates": "Cithakan sing diperlokaké",
        "mostlinked": "Kaca sing kerep dhéwé dituju",
        "mostlinkedcategories": "Kategori sing kerep dhéwé dienggo",
-       "mostlinkedtemplates": "Cithakan sing kerep dhéwé dienggo",
+       "mostlinkedtemplates": "Kaca paling akèh transklusi",
        "mostcategories": "Kaca sing kategoriné akèh dhéwé",
        "mostimages": "Berkas sing kerep dhéwé dienggo",
        "mostinterwikis": "Halaman dengan interwiki terbanyak",
        "nopagetext": "Kaca sing panjenengan tuju ora ditemokaké.",
        "pager-newer-n": "{{PLURAL:$1|1 luwih anyar|$1 luwih anyar}}",
        "pager-older-n": "{{PLURAL:$1|1 sing luwih lawas|$1 sing luwih lawas}}",
-       "suppress": "Pangawas (''oversight'')",
+       "suppress": "Dhelikaké",
        "querypage-disabled": "Kaca kusus iki dipatèni kanggo alesan kinerja.",
        "apisandbox": "Kothak wedhi API",
        "apisandbox-api-disabled": "API dipatèni nèng situs iki.",
        "whatlinkshere-prev": "{{PLURAL:$1|sadurungé|$1 sadurungé}}",
        "whatlinkshere-next": "{{PLURAL:$1|sabanjuré|$1 sabanjuré}}",
        "whatlinkshere-links": "← pranala",
-       "whatlinkshere-hideredirs": "$1 lih-lihan",
-       "whatlinkshere-hidetrans": "$1 transklusi",
-       "whatlinkshere-hidelinks": "pranala-pranala $1",
-       "whatlinkshere-hideimages": "$1 pranala berkas",
+       "whatlinkshere-hideredirs": "Dhelikaké lih-lihan",
+       "whatlinkshere-hidetrans": "Dhelikaké transklusi",
+       "whatlinkshere-hidelinks": "Dhelikaké pranala",
+       "whatlinkshere-hideimages": "Dhelikaké pranala barkas",
        "whatlinkshere-filters": "Panyaringan",
        "autoblockid": "Blokir otomatis #$1",
        "block": "Blokir panganggo",
        "unblock": "Uculaké blokirané panganggo",
-       "blockip": "Blokir panganggo",
+       "blockip": "Palang {{GENDER:$1|panganggo}}",
        "blockip-legend": "Blokir panganggo",
        "blockiptext": "Enggonen formulir ing ngisor iki kanggo mblokir sawijining alamat IP utawa panganggo supaya ora bisa nyunting kaca.\nPrekara iki perlu dilakoni kanggo menggak vandalisme, lan miturut [[{{MediaWiki:Policy-url}}|kawicaksanan {{SITENAME}}]].\nLebokna alesan panjenengan ing ngisor iki (contoné njupuk conto kaca sing wis tau dirusak).",
        "ipaddressorusername": "Alamat IP utawa jeneng panganggo",
        "movenotallowedfile": "Panjenengan ora duwé hak kanggo mindhahaké berkas.",
        "cant-move-user-page": "Panjenengan ora nduwèni hak aksès kanggo mindhahaké kaca panganggo (kapisah saka anak-kaca).",
        "cant-move-to-user-page": "Panjenengan ora nduwèni hak aksès kanggo mindhahaké kaca menyang sawijining kaca panganggoa (kajaba menyang anak-kaca panganggo).",
-       "newtitle": "Menyang irah-irahan utawa judhul anyar:",
+       "newtitle": "Sesirah anyar:",
        "move-watch": "Awasna kaca iki",
        "movepagebtn": "Ngalih kaca",
        "pagemovedsub": "Bisa kasil dipindhahaké",
        "movenosubpage": "Kaca iki ora duwé anak-kaca.",
        "movereason": "Alesan:",
        "revertmove": "balèkaké",
-       "delete_and_move_text": "== Perlu mbusak ==\n\nArtikel sing dituju, \"[[:$1]]\", wis ana isiné.\nApa panjenengan kersa mbusak iku supaya kacané bisa dialihaké?",
+       "delete_and_move_text": "Kaca jujugan \"[[:$1]]\" wis ana.\nApa sampéyan kersa mbusak iku supaya kacané bisa dilih?",
        "delete_and_move_confirm": "Ya, busak kaca iku.",
        "delete_and_move_reason": "Dibusak kanggo jaga-jaga ananing pamindhahan saka \"[[$1]]\"",
        "selfmove": "Pangalihan kaca ora bisa dilakoni amerga irah-irahan utawa judhul sumber lan tujuané padha.",
        "move-leave-redirect": "Gawé pangalihan menyang irah-irahan anyar",
        "protectedpagemovewarning": "'''Pènget:''' Kaca iki wis dikunci dadi mung panganggo sing nduwé hak aksès pangurus baé sing bisa mindhahaké.\nCathetan entri pungkasan disadiakaké ing ngisor kanggo referensi:",
        "semiprotectedpagemovewarning": "'''Cathetan:''' Kaca iki wis direksa saéngga mung panganggo kadhaptar sing bisa mindhahaké.\nEntri cathetan pungkasan disadiakake ing ngisor kanggo referensi:",
-       "move-over-sharedrepo": "== Berkas wis ana ==\n[[:$1]] ana ing panyimpenan bebarengan. Mindhahaké berkas mawa judul iki bakal nibani berkas bebarengan.",
+       "move-over-sharedrepo": "[[:$1]] ana ing panyimpenan barengan. Ngalih barkas mawa sesirah iki bakal ngamblegi barkas barengan iku.",
        "file-exists-sharedrepo": "Jeneng berkas kapilih wis ana kanggo nèng panyimpenan bebarengan.\nMangga pilih jeneng liya.",
        "export": "Ekspor kaca",
        "exporttext": "Panjenengan bisa ngèkspor tèks lan sajarah panyuntingan sawijining kaca tartamtu utawa sawijining sèt kaca awujud XML tartamtu. Banjur iki bisa diimpor ing wiki liyané nganggo MediaWiki nganggo fasilitas [[Special:Import|impor kaca]].\n\nKanggo ngèkspor kaca-kaca artikel, lebokna irah-irahan utawa judhul sajroning kothak tèks ing ngisor iki, irah-irahan utawa judhul siji per baris, lan pilihen apa panjenengan péngin ngèkspor jangkep karo vèrsi sadurungé, utawa namung vèrsi saiki mawa cathetan panyuntingan pungkasan.\n\nYèn panjenengan namun péngin ngimpor vèrsi pungkasan, panjenengan uga bisa nganggo pranala kusus, contoné [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] kanggo ngèkspor artikel \"[[{{MediaWiki:Mainpage}}]]\".",
        "thumbnail_gd-library": "Konfigurasi pustaka GD ora pepak: fungsi $1 ilang",
        "thumbnail_image-missing": "Berkas katonané ilang: $1",
        "import": "Impor kaca",
-       "importinterwiki": "Impor transwiki",
+       "importinterwiki": "Impor saka wiki liya",
        "import-interwiki-text": "Pilih sawijining wiki lan irah-irahan kaca sing arep diimpor.\nTanggal révisi lan jeneng panyunting bakal dilestarèkaké.\nKabèh aktivitas impor transwiki bakal dilog ing [[Special:Log/import|log impor]].",
        "import-interwiki-history": "Tuladen kabèh vèrsi lawas saka kaca iki",
        "import-interwiki-templates": "Katutna kabèh cithakan",
        "tooltip-pt-anonuserpage": "Kaca panganggo IP panjenengan",
        "tooltip-pt-mytalk": "Kaca gegunemaning {{GENDER:|sampéyan}}",
        "tooltip-pt-anontalk": "Rerembugan bab besutan-besutan saka alamat IP iki",
-       "tooltip-pt-preferences": "Pilihaning {{GENDER:|sampéyan}}",
+       "tooltip-pt-preferences": "Pilih-pilihaning {{GENDER:|sampéyan}}",
        "tooltip-pt-watchlist": "Pratélaning kaca sing sampéyan awasi owah-owahané",
        "tooltip-pt-mycontris": "Pratélaning sumbanganing {{GENDER:|sampéyan}}",
        "tooltip-pt-login": "Sampéyan prayogané mlebu masiya ora kudu",
        "tooltip-search": "Golèk nyang {{SITENAME}}",
        "tooltip-search-go": "Jujug kaca asesirah persis mangkéné yèn ana",
        "tooltip-search-fulltext": "Golèk kaca isi tulisan kaya mangkéné",
-       "tooltip-p-logo": "Menyang kaca pokok",
-       "tooltip-n-mainpage": "Menyang kaca pokok",
-       "tooltip-n-mainpage-description": "Menyang kaca pokok",
+       "tooltip-p-logo": "Menyang tepas",
+       "tooltip-n-mainpage": "Menyang tepas",
+       "tooltip-n-mainpage-description": "Menyang tepas",
        "tooltip-n-portal": "Bab proyèk, apa sing bisa sampéyan garap, ana ing endi saprelu golèk apa-apa",
        "tooltip-n-currentevents": "Temokaké katerangan latar wuri saka kadadéan saiki",
        "tooltip-n-recentchanges": "Pratélaning owah-owahan pungkasan sajeroning wiki.",
        "exif-sublocationdest": "Dhaèrahé kutha katampilaké",
        "exif-objectname": "Judhul cendhèk",
        "exif-specialinstructions": "Prèntah kusus",
-       "exif-headline": "Warta utama",
+       "exif-headline": "Tajuk",
        "exif-credit": "Krédit/Panyadhiya",
        "exif-source": "Sumber",
        "exif-editstatus": "Status kapanyuntingan gambar",
        "duplicate-defaultsort": "Pènget: Kunci pilih asal (''Default sort key'') \"$2\" nggantèkaké kunci pilih asal sadurungé \"$1\".",
        "version": "Versi",
        "version-extensions": "Èkstènsi sing wis diinstalasi",
-       "version-skins": "Kulit",
+       "version-skins": "Kulit sing disetèl",
        "version-specialpages": "Kaca astaméwa (kaca kusus)",
        "version-parserhooks": "Canthèlan parser",
        "version-variables": "Variabel",
        "version-entrypoints": "URL tithik lebon",
        "version-entrypoints-header-entrypoint": "Tithik lebon",
        "version-entrypoints-header-url": "URL",
-       "redirect": "Dialihake dening gambar, panganggo, kaca, utawa ID revisi",
+       "redirect": "Lih-lihan miturut barkas, panganggo, kaca, owahan, utawa cathetan",
        "redirect-summary": "Kaca astamiwa iki dialihake menyang gambar (jeneng gambar diwenehi), kaca (ID revisi utama ID kaca diwenehi), utawa kaca panganggo (ID panganggo diwenehi). Cara nganggo: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], or [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "Nuju",
        "redirect-lookup": "Golek:",
        "api-error-badaccess-groups": "Sampéyan ora dililakaké ngunggah berkas nèng wiki iki.",
        "api-error-badtoken": "Kasalahan njero: Token èlèk.",
        "api-error-copyuploaddisabled": "Ngunggah saka URL dipatèni nèng sasana iki.",
-       "api-error-duplicate": "Ana {{PLURAL:$1|berkas liya|pirang-pirang berkas liya}} sing wis ana nèng situsé saha isiné padha.",
+       "api-error-duplicate": "Wis ana {{PLURAL:$1|barkas liya|barkas-barkas liya}} mawa isi sing padha sajeroning sana jaringan iki.",
        "api-error-duplicate-archive": "Ana {{PLURAL:$1|berkas liya|pirang-pirang berkas liya}} sing wis ana nèng situsé saha isiné padha, nanging {{PLURAL:$1|kuwi|kuwi kabèh}} wis dibusak.",
        "api-error-empty-file": "Berkas sing Sampéyan kirim kosong.",
        "api-error-emptypage": "Nggawé kaca kosong anyar ora dilikaké.",
        "special-characters-group-lao": "Lao",
        "special-characters-group-khmer": "Khmer",
        "api-error-blacklisted": "Mangga pilih judhul liya sing njelasaké",
-       "randomrootpage": "Kaca root waton"
+       "randomrootpage": "Kaca dhasaran waton"
 }
index d6f3dfb..b9885e0 100644 (file)
        "noname": "თქვენს მიერ მითითებული მომხმარებლის სახელი ქმედითი არ არის.",
        "loginsuccesstitle": "სისტემაში შესვლა განხორციელდა",
        "loginsuccess": "'''ამჟამად შესული ხართ {{SITENAME}}-ში როგორც „$1“.'''",
-       "nosuchuser": "მომხმარებელი სახელად $1 არ არსებობს.\nმომხმარებელთა სახელები გრძნობადია ასოების რეგისტრამდე..\nშეამოწმეთ სახელის დაწერა ან[[Special:UserLogin/signup|შექმენით ახალი ანგარიში]].",
+       "nosuchuser": "მომხმარებელი სახელად $1 არ არსებობს.\nმომხმარებელთა სახელები გრძნობადია ასოების რეგისტრამდე..\nშეამოწმეთ სახელის დაწერა ან[[Special:CreateAccount|შექმენით ახალი ანგარიში]].",
        "nosuchusershort": "მომხმარებელი სახელით „$1“ არ არსებობს. შეამოწმეთ მართლწერა.",
        "nouserspecified": "საჭიროა მომხმარებლის სახელის მითითება.",
        "login-userblocked": "ეს მომხმარებელი დაბლოკილია. სისტემაში შესვლა არაა ნებადართული.",
        "accmailtext": "შემთხვევითი მეთოდით შექმნილი პაროლი მომხმარებლისათვის [[User talk:$1|$1]] გაგზავნილია მისამართზე $2.\n\nავტორიზაციის გავლის შემდეგ შესაძლებელი იქნება ამ ანგარიშის  ''[[Special:ChangePassword|პაროლის შეცვლა]]'' ანგარიშში შესვლის გვერდზე.",
        "newarticle": "(ახალი)",
        "newarticletext": "ბმულის მეშვეობით თქვენ მოხვდით გვერდზე, რომელიც ჯერ არ არსებობს.\nგვერდის შესაქმნელად შეიყვანეთ ინფორმაცია ქვემო ფანჯარაში\n(იხ. [$1 დახმარების გვერდი] დამატებითი ინფორმაციისთვის).\nთუ ამ გვერდზე შეცდომით მოხვდით, დაბრუნდით უკან თქვენი ბრაუზერის მეშვეობით.",
-       "anontalkpagetext": "----''ეს არის ანონიმური მომხმარებლის განხილვის გვერდი, რომელსაც ანგარიში ჯერ არ შეუქმნია ან არ იყენებს მას.\n\nშესაბამისად, ჩვენ მისი ციფრული IP მისამართი უნდა გამოვიყენოთ მისი იდენტიფიცირებისთვის.\n\nამგვარი მისამართი შეიძლება რამდენიმე მომხმარებელმა გამოიყენოს.\n\nთუ თქვენ ანონიმური მომხმარებელი ხართ და თვლით, რომ სხვისთვის გამიზნული მითითება მიიღეთ, გთხოვთ [[Special:UserLogin/signup|შექმენით ანგარიში ან დარეგისტრირდით]] მომავალში გაუგებრობის თავიდან ასაცილებლად.''",
+       "anontalkpagetext": "----''ეს არის ანონიმური მომხმარებლის განხილვის გვერდი, რომელსაც ანგარიში ჯერ არ შეუქმნია ან არ იყენებს მას.\n\nშესაბამისად, ჩვენ მისი ციფრული IP მისამართი უნდა გამოვიყენოთ მისი იდენტიფიცირებისთვის.\n\nამგვარი მისამართი შეიძლება რამდენიმე მომხმარებელმა გამოიყენოს.\n\nთუ თქვენ ანონიმური მომხმარებელი ხართ და თვლით, რომ სხვისთვის გამიზნული მითითება მიიღეთ, გთხოვთ [[Special:CreateAccount|შექმენით ანგარიში ან დარეგისტრირდით]] მომავალში გაუგებრობის თავიდან ასაცილებლად.''",
        "noarticletext": "ამჟამად ამ გვერდზე ტექსტი არ არსებობს.\nთქვენ შეგიძლიათ [[Special:Search/{{PAGENAME}}|მოძებნოთ ამ გვერდის სათაური]] სხვა გვერდებზე,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} მოძებნოთ შესაბამისი ჟურნალები],\nან [{{fullurl:{{FULLPAGENAME}}|action=edit}} დაიწყოთ ამ გვერდის რედაქტირება]</span>.",
        "noarticletext-nopermission": "ამ დროისთვის ეს გვერდი ცარიელია.\nთქვენ შეგიძლიათ [[Special:Search/{{PAGENAME}}|მოძებნოთ ეს სათაური]] სხვა გვერდებზე,\nან <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} მოძებნოთ ჟურნალების შესაბამისი ჩანაწერები].</span> თქვენ არ გაქვთ ამ გვერდის შექმნის ნებართვა.",
        "missing-revision": "ვერსია $1 გვერდისათვის „{{FULLPAGENAME}}“ არ არსებობს.\n\nეს ჩვეულებრივ ხდება მაშინ, თუ მოძველებული ბმულით გადადიხართ გვერდზე, რომელიც წაიშალა.\nდეტალური ინფორმაცია შესაძლებელია იყოს [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} წაშლების ჟურნალში].",
        "upload-form-label-own-work": "ეს ჩემი პირადი ნამუშევარია",
        "upload-form-label-infoform-categories": "კატეგორიები",
        "upload-form-label-infoform-date": "თარიღი",
-       "upload-form-label-own-work-message-local": "ვადასტურებ, რომ ვტვირთავვ ამ ფაილს მომსახურების პირობებისა და ლიცენსიის პოლიტიკის შესაბამისად {{SITENAME}}-ზე.",
-       "upload-form-label-not-own-work-message-local": "თუ ვერ ტვირთავთ ამ ფაილს {{SITENAME}}-ის წესების დაცვით, გთხოვთ დახურეთ ეს ფანჯარა და სცადეთ სხვა მეთოდი.",
-       "upload-form-label-not-own-work-local-local": "შეგიძლიათ სცადოთ [[Special:Upload|მთავარი ატვირთვის გვერდი]].",
-       "upload-form-label-own-work-message-default": "ვიცი, რომ ამ ფაილს ვტვირთავ საზიარო ბაზაში. ვადასტურებ, რომ ამას ვაკეთებ მომსახურების პირობებისა და ლიცენზიის პოლიტიკის შესაბამისად.",
-       "upload-form-label-not-own-work-message-default": "თუ ვერ ტვირთავთ ამ ფაილს {{SITENAME}}-ის წესების დაცვით, გთხოვთ დახურეთ ეს ფანჯარა და სცადეთ სხვა მეთოდი.",
-       "upload-form-label-not-own-work-local-default": "შეგიძლიათ ასევე სცადოთ [[Special:Upload|ატვირთვის გვერდი {{SITENAME}}-ზე]], თუ ამ ფაილის ატვირთვა დაშვებულია მათი პოლიტიკის მიხედვით.",
-       "upload-form-label-own-work-message-shared": "მე ვადასტურებ, რომ ამ ფაილზე საავტორო უფლებების მფლობელი ვარ და ვთანხმდები ამ ფაილის შეუქცევადად განთავსებაზე ვიკისაწყობში [https://creativecommons.org/licenses/by-sa/4.0/deed.ka Creative Commons Attribution-ShareAlike 4.0] ლიცენზიით, აგრეთვე ვეთანხმები [https://wikimediafoundation.org/wiki/Terms_of_Use გამოყენების წესებს].",
-       "upload-form-label-not-own-work-message-shared": "თუ თქვენ არ ხართ საავტორო უფლებებს ამ ფაილზე, ან გსურთ გაუშვათ განსხვავებული ლიცენზიით, გთხოვთ გამოიყენეთ [https://commons.wikimedia.org/wiki/Special:UploadWizard ვიკისაწყობის ატვირთვის ფუნქცია].",
-       "upload-form-label-not-own-work-local-shared": "შეგიძლიათ ასევე სცადოთ [[Special:Upload|ატვირთვის გვერდი {{SITENAME}}-ზე]], თუ ამ ფაილის ატვირთვა დაშვებულია მათი პოლიტიკის შესაბამისად.",
+       "upload-form-label-own-work-message-generic-local": "ვადასტურებ, რომ ვტვირთავვ ამ ფაილს მომსახურების პირობებისა და ლიცენსიის პოლიტიკის შესაბამისად {{SITENAME}}-ზე.",
+       "upload-form-label-not-own-work-message-generic-local": "თუ ვერ ტვირთავთ ამ ფაილს {{SITENAME}}-ის წესების დაცვით, გთხოვთ დახურეთ ეს ფანჯარა და სცადეთ სხვა მეთოდი.",
+       "upload-form-label-not-own-work-local-generic-local": "შეგიძლიათ სცადოთ [[Special:Upload|მთავარი ატვირთვის გვერდი]].",
+       "upload-form-label-own-work-message-generic-foreign": "ვიცი, რომ ამ ფაილს ვტვირთავ საზიარო ბაზაში. ვადასტურებ, რომ ამას ვაკეთებ მომსახურების პირობებისა და ლიცენზიის პოლიტიკის შესაბამისად.",
+       "upload-form-label-not-own-work-message-generic-foreign": "თუ ვერ ტვირთავთ ამ ფაილს {{SITENAME}}-ის წესების დაცვით, გთხოვთ დახურეთ ეს ფანჯარა და სცადეთ სხვა მეთოდი.",
+       "upload-form-label-not-own-work-local-generic-foreign": "შეგიძლიათ ასევე სცადოთ [[Special:Upload|ატვირთვის გვერდი {{SITENAME}}-ზე]], თუ ამ ფაილის ატვირთვა დაშვებულია მათი პოლიტიკის მიხედვით.",
        "backend-fail-stream": "ფაილი $1 ტრანსლირება ვერ მოხერხდა.",
        "backend-fail-backup": "ფაილი $1 სარეზერვო ასლის გაკეთება ვერ მოხერხდა.",
        "backend-fail-notexists": "ფაილი $1 არ არსებობს.",
index e37711b..226f578 100644 (file)
        "noname": "Siz kiritken paydalanıwshı atı qate.",
        "loginsuccesstitle": "Kiriw tabıslı a'melge asırıldı",
        "loginsuccess": "'''{{SITENAME}} saytına \"$1\" paydalanıwshı atı menen kirdin'iz.'''",
-       "nosuchuser": "\"$1\" atlı paydalanıwshı joq.\nTuwrı jazılg'anlıg'ın tekserin' yamasa [[Special:UserLogin/signup|taza akkaunt jaratın']].",
+       "nosuchuser": "\"$1\" atlı paydalanıwshı joq.\nTuwrı jazılg'anlıg'ın tekserin' yamasa [[Special:CreateAccount|taza akkaunt jaratın']].",
        "nosuchusershort": "\"$1\" atlı paydalanıwshı joq. Tuwrı jazılg'anlıg'ın tekserin'.",
        "nouserspecified": "Siz paydalanıwshı atın ko'rsetpedin'iz.",
        "wrongpassword": "Qate parol kiritlgen. Qaytadan kiritin'.",
        "categories": "Kategoriyalar",
        "categoriespagetext": "To'mendegi {{PLURAL:$1|kategoriya|kategoriyalar}} o'z ishine betler yamasa medialardı alg'an.\nBul jerde [[Special:UnusedCategories|paydalanılmag'an kategoriyalar]] ko'rsetilmegen.\nJa'nede [[Special:WantedCategories|kerekli kategoriyalardı]] qarap ko'rin'.",
        "categoriesfrom": "Kategoriyalardı to'mendegilerden baslap ko'rset:",
-       "special-categories-sort-count": "sanı boyınsha ta'rtiplew",
-       "special-categories-sort-abc": "a'lipbe boyınsha ta'rtiplew",
        "deletedcontributions": "Paydalanıwshının' o'shiriw u'lesi",
        "linksearch": "Sırtqı siltewler",
        "linksearch-pat": "İzlew shablonı:",
index 1be5057..3daac22 100644 (file)
        "noname": "Ur tefkiḍ ara isem n wemseqdac ṣaḥiḥ.",
        "loginsuccesstitle": "Tkecmeḍ !",
        "loginsuccess": "'''Tkecmeḍ ar {{SITENAME}} s yisem n wemseqdac \"$1\".'''",
-       "nosuchuser": "Aseqdac « $1 » ulac-it d-agi.\nSsenqed tira n yisem-nni, naɣ [[Special:UserLogin/signup|snulfu-d amiḍan amaynut]].",
+       "nosuchuser": "Aseqdac « $1 » ulac-it d-agi.\nSsenqed tira n yisem-nni, naɣ [[Special:CreateAccount|snulfu-d amiḍan amaynut]].",
        "nosuchusershort": "Ulac isem n wemseqdac s yisem \"$1\". Ssenqed tira n yisem-nni.",
        "nouserspecified": "Yessefk ad tefkeḍ isem n wemseqdac.",
        "login-userblocked": "Aseqdac agi i sewḥel. Tuqqna t-ugwi.",
        "accmailtext": "Awal n uɛaddi id yuran s ugacur i [[User talk:$1|$1]] yetweceggaɛ i $2.\nYezmer ad yetbeddel ɣef usebtar [[Special:ChangePassword|Abeddel n awal uɛddi]] sakin tuqqna.",
        "newarticle": "(Amaynut)",
        "newarticletext": "Tḍefreḍ azday ɣer usebter mazal ur yettwaxleq ara.\nAkken ad txelqeḍ asebter-nni, aru deg tenkult i tella deg ukessar\n(ẓer [$1 asebter n tallalt] akken ad tessneḍ kter).\nMa tɣelṭeḍ, wekki kan ɣef tqeffalt \"Back/Précédent\" n browser/explorateur inek.",
-       "anontalkpagetext": "---- ''Wagi d asebter n umyennan n useqdac adrig, mazal ur d-yesnufa ara amiḍan. I taɣẓint agi, ilaq an seqdec tansa IP ines iwakken at-id n sulu. Yiwet tansa IP tezmer at tettuseqdac sɣur aṭṭas n iseqdacen. Lukan ula d kečč aqla-k amseqdac adrig dɣa ur tebɣiḍ ara ad tettwabcreḍ izen am wigini, ihi [[Special:UserLogin/signup|snulfud amiḍan]] naɣ [[Special:UserLogin|qqened]] iwakken sya d asawen ur t-illint ara uguren n usulu.''",
+       "anontalkpagetext": "---- ''Wagi d asebter n umyennan n useqdac adrig, mazal ur d-yesnufa ara amiḍan. I taɣẓint agi, ilaq an seqdec tansa IP ines iwakken at-id n sulu. Yiwet tansa IP tezmer at tettuseqdac sɣur aṭṭas n iseqdacen. Lukan ula d kečč aqla-k amseqdac adrig dɣa ur tebɣiḍ ara ad tettwabcreḍ izen am wigini, ihi [[Special:CreateAccount|snulfud amiḍan]] naɣ [[Special:UserLogin|qqened]] iwakken sya d asawen ur t-illint ara uguren n usulu.''",
        "noarticletext": "Ulac aḍris deg usebter-agi, tzemreḍ ad [[Special:Search/{{PAGENAME}}|tnadiḍ ɣef wezwel n usebter-agi]] deg isebtar wiyaḍ neɣ [{{fullurl:{{FULLPAGENAME}}|action=edit}} tettbeddileḍ asebter-agi].",
        "noarticletext-nopermission": "Imira ulac aḍris deg usebter agi.\nTzemreḍ [[Special:Search/{{PAGENAME}}|ad nadiḍ ɣef azwel agi]] deg isebtaren nniḍen,\nnaɣ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|asebter={{FULLPAGENAMEE}}}} ad nadiḍ deg iɣmisen iqqenen]</span>.",
        "missing-revision": "Tacaggart #$1 n usebter s isem « {{FULLPAGENAME}} » ulac-itt.\n\nAcku azday n umezruy, ɣef wayen tsennedeḍ, d-aqbur. Asebter yemḥa.\nTzemreḍ ad affeḍ tilɣa deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n isebtar yemḥan].",
index 8fce9dc..2a9aad5 100644 (file)
        "noname": "ЦӀыхухэтыцӀэ хъунур иптхакъым.",
        "loginsuccesstitle": "ЗыкъегъэцӀыхуныр фӀыуэ зэфӀэкӀа.",
        "loginsuccess": "'''Иджы цӀэ \"$1\" зепхьу улэжьэфыну.'''",
-       "nosuchuser": "\"$1\" цӀэ зезыхьэ цӀыхухэт щыӀэкъым.\nЦӀыхухэтыцӀэхэр гурышхъу щытхэ хьэрыф регистырымкӀэ.\nЦӀэр тэрэзу птхамэ еплъ иэ [[Special:UserLogin/signup|аккаунт щӀэуэ щӀы]].",
+       "nosuchuser": "\"$1\" цӀэ зезыхьэ цӀыхухэт щыӀэкъым.\nЦӀыхухэтыцӀэхэр гурышхъу щытхэ хьэрыф регистырымкӀэ.\nЦӀэр тэрэзу птхамэ еплъ иэ [[Special:CreateAccount|аккаунт щӀэуэ щӀы]].",
        "nosuchusershort": "ЦӀыхухэт \"$1\" щыӀэкъым.\nЦӀэр тэрэзу итхамэ еплъ.",
        "nouserspecified": "ЦӀыхухэтыцӀэр иптхэн хуэй.",
        "login-userblocked": "Мы цӀыхухэтыр теубыдауэ щытщ. Ихьэныр хуадэкъым.",
        "accmailtext": "ЦӀыхухэт [[User talk:$1|$1-м]] щхьэкӀэ дэмыгъэ зэхэдзыгъэншу парол зэхэгъувар $2 адресым егъэхьа.\nСистемэм регистрациэ быщӀа яуж ''[[Special:ChangePassword|паролыр зэпхъуэкӀыфыну]].''",
        "newarticle": "(ЩIэуэ)",
        "newarticletext": "НапэкӀуэцӀ иджыри щымыӀэм утехуащ техьэпӀэмкэ.\nАр быщӀын щхьэкӀэ, и щӀагъым щӀэт игъувапӀэм тхылъ итхэ (еплъ [$1 щӀэупщӀэгъуэхэм я напэкӀуэцӀ]).\nГъуэщэгъуэкӀэ мыбым утехуамэ, уи браузерым гъэзэжыгъуэ иӀэм текъузи зэфэкащ.",
-       "anontalkpagetext": "----''Мы напэкӀуэцӀ тепсэлъыхьыгъуэр аноним цӀыхухэтым ей, аккаунт зымыщӀам иэ къэзмыгъэсэбэпым.\nАбым щхьэкӀэ IP-адресымкӀэ идентификациэ иратыр.\nАноним цӀыхухэту ущытмэ, тхыгъэ къыпхуэкӀуауэ уигугъэмэ, [[Special:UserLogin/signup|аккаунт щӀы]] иэ [[Special:UserLogin|системэм зыкъегъэцӀыху]], япэкӀэ хэукъуэгъуэ пэмыкӀ цӀыхухэт анэнимхэм ям ухэмыхуэн щхьэкӀэ.''",
+       "anontalkpagetext": "----''Мы напэкӀуэцӀ тепсэлъыхьыгъуэр аноним цӀыхухэтым ей, аккаунт зымыщӀам иэ къэзмыгъэсэбэпым.\nАбым щхьэкӀэ IP-адресымкӀэ идентификациэ иратыр.\nАноним цӀыхухэту ущытмэ, тхыгъэ къыпхуэкӀуауэ уигугъэмэ, [[Special:CreateAccount|аккаунт щӀы]] иэ [[Special:UserLogin|системэм зыкъегъэцӀыху]], япэкӀэ хэукъуэгъуэ пэмыкӀ цӀыхухэт анэнимхэм ям ухэмыхуэн щхьэкӀэ.''",
        "noarticletext": "Иджыпсту мы напэкӀуэцӀыр нэщӀ.\nУзхуэныкъуэм [[Special:Search/{{PAGENAME}}|игугъ бгъуэтыфыну]] нэгъуэщӀ напэкӀуэцӀхэм, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} тхылъхэм абым теухуа тхыгъэхэм], иэ '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} апхуэдэцӀэ зиӀэ напэкӀуцӀ быщӀыфынущ]'''</span>.",
        "noarticletext-nopermission": "Джыпсту мы напэкӀуэцӀыр нэщӀу щытщ.\nУзхуэныкъуэм [[Special:Search/{{PAGENAME}}|и гугъ бгъуэтыфыну]] нэгъуэщӀ напэкӀуэцӀхэм, \nиэ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} абым теухуа тхъыгъэхэр].</span>",
        "userpage-userdoesnotexist": "Аккаунт «<nowiki>$1</nowiki>» щыӀэкъым. У арэзыуэ ущыт мы напэкӀуэцӀыр быщӀыным иэ зэпхъуэкӀыным.",
index 8d9a6ad..db9d962 100644 (file)
        "noname": "تو تان صحیح اسم صارفو داخل نو آرو",
        "loginsuccesstitle": "داخلہ کامیاب",
        "loginsuccess": "'''ھانیسے تو {{SITENAME}} بنام \"$1\" داخل بیتی آسوس'''",
-       "nosuchuser": "\"$1\" نامو سورا کیہ صارف موجود نیکی.\nبرائے مہربانی! ہجان درست اندراجان تصدیقو کورے.\nاگر تو کہ مناسب جوشیتاو تھے [[Special:UserLogin/signup|نوغ کھاتہ دی ساوزیکو بوس]].",
+       "nosuchuser": "\"$1\" نامو سورا کیہ صارف موجود نیکی.\nبرائے مہربانی! ہجان درست اندراجان تصدیقو کورے.\nاگر تو کہ مناسب جوشیتاو تھے [[Special:CreateAccount|نوغ کھاتہ دی ساوزیکو بوس]].",
        "nouserspecified": "تہ ای اسمِ صارفو مخصوص کوریلک",
        "login-userblocked": "ھیہ صارفو سورا پاوبندی شیر. داخلِ نوشتہ بیکو اجازت نیکی",
        "wrongpassword": "تو غلط کلمۂ شناخت درج کوری آسوس دوبارہ کو شش کورے",
index 9eb5e96..5c72e48 100644 (file)
        "noname": "Ebe namê do vêrdoği ra cınêkota.",
        "loginsuccesstitle": "Cıkotene biye ra",
        "loginsuccess": "'''Sıta {{SITENAME}} de ebe namê karberi \"$1\" kota cı.'''",
-       "nosuchuser": "Ebe namê \"$1\"i jü karber çino.\nNustena namunê karberu de herfa pil u qıze rê diqet kerê.\nNustena ho qonrol kerê, ya ki [[Special:UserLogin/signup|jü hesabo newe rakerê]].",
+       "nosuchuser": "Ebe namê \"$1\"i jü karber çino.\nNustena namunê karberu de herfa pil u qıze rê diqet kerê.\nNustena ho qonrol kerê, ya ki [[Special:CreateAccount|jü hesabo newe rakerê]].",
        "nosuchusershort": "Karberê do ebe namê \"$1\" çino.\nNustena cı qontrol ke.",
        "nouserspecified": "Gunê namê jü karberi bıdekernê.",
        "login-userblocked": "No karber engel biyo. Cıkotene rê mısade cı nêdino.",
index f88289d..dd471d5 100644 (file)
@@ -14,7 +14,8 @@
                        "Macofe",
                        "Batyrbek.kz",
                        "Matma Rex",
-                       "Nemo bis"
+                       "Nemo bis",
+                       "Mormegil"
                ]
        },
        "tog-underline": "Сілтеменің астын сызу:",
        "noname": "Сізде жарамды қатысушы аты анықталмаған.",
        "loginsuccesstitle": "Кірдіңіз.",
        "loginsuccess": "<strong>Сіз енді {{SITENAME}} жобасына «$1» ретінде кірдіңіз.</strong>",
-       "nosuchuser": "Мұнда «$1» деп аталған қатысушы тіркелмеген.\nҚатысушы аттары кіші әріптерден тұру керек.\nЕмлеңізді тексеріңіз немесе [[Special:UserLogin/signup|жаңа тіркелгі жасаңыз]].",
+       "nosuchuser": "Мұнда «$1» деп аталған қатысушы тіркелмеген.\nҚатысушы аттары кіші әріптерден тұру керек.\nЕмлеңізді тексеріңіз немесе [[Special:CreateAccount|жаңа тіркелгі жасаңыз]].",
        "nosuchusershort": "Мұнда «$1» деп аталған қатысушы тіркелмеген.\nЕмлеңізді тексеріңіз.",
        "nouserspecified": "Қатысушы атын көрсетуіңіз керек.",
        "login-userblocked": "Бұл қатысушы бұғатталған. Жүйеге кiру рұқсат етiлмеген.",
        "accmailtext": "$2 дегенге [[User talk:$1|$1]] үшін құпия сөзі жөнелтілді. Оны <em>[[Special:ChangePassword|құпия сөзді өзгерту]]</em> бетінде жүйеге кірген кезде өзгеруге болады.",
        "newarticle": "(Жаңа)",
        "newarticletext": "Сілтеме бойынша әлі басталмаған бетке келіпсіз.\nБетті бастау үшін төменгі терезеде мәтінді теріңіз (көбірек ақпарат үшін [$1 анықтама бетін] қараңыз).\nЕгер жаңылғаннан осында келген болсаңыз браузеріңіздің <strong>артқа</strong> деген батырмасын басыңыз.",
-       "anontalkpagetext": "----\n<em>Бұл тіркелгісіз анонимді (немесе тіркелгісін қолданбаған) қатысушының талқылау беті.</em> \nСондықтан біз оны сандық IP мекенжайымен қолдануға тиістіміз.\nОсындай IP мекенжайды бірнеше пайдаланушы ортақтаса алады.\nЕгер сіз анонимді қатысушы болсаңыз және сізге қатыссыз хабарлама жіберілгенін сезсеңіз басқа анонимді қатысушылармен алдағы уақыттарда шатастырмау үшін [[Special:UserLogin/signup|тіркеліңіз]] не [[Special:UserLogin|кіріңіз]].",
+       "anontalkpagetext": "----\n<em>Бұл тіркелгісіз анонимді (немесе тіркелгісін қолданбаған) қатысушының талқылау беті.</em> \nСондықтан біз оны сандық IP мекенжайымен қолдануға тиістіміз.\nОсындай IP мекенжайды бірнеше пайдаланушы ортақтаса алады.\nЕгер сіз анонимді қатысушы болсаңыз және сізге қатыссыз хабарлама жіберілгенін сезсеңіз басқа анонимді қатысушылармен алдағы уақыттарда шатастырмау үшін [[Special:CreateAccount|тіркеліңіз]] не [[Special:UserLogin|кіріңіз]].",
        "noarticletext": "Қазіргі уақытта бұл бетте еш мәтін жоқ.\n* Басқа беттерден [[Special:Search/{{PAGENAME}}|бұл бет атауын іздеу]],\n* <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} Журналдардан бұл бетке қатысты сәйкес жазбаларды табу]</span>,\n* <span class=\"plainlinks\">'''[{{fullurl:{{FULLPAGENAME}}|action=edit}} Бұл бетті жаңадан бастау]'''</span>.",
        "noarticletext-nopermission": "Қазіргі уақытта бұл бетте мәтін жоқ.\nСіз бұл бет атауын басқа беттерден [[Special:Search/{{PAGENAME}}|іздей аласыз]], немесе <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} қатысты журналдардан іздей аласыз]</span>. Ал бұл бетті жаңадан бастауға сізде рұқсат жоқ.",
        "missing-revision": "#$1 нұсқалы «{{FULLPAGENAME}}» деп аталатын бет жоқ",
        "upload-form-label-own-work": "Бұл менің өз туындым",
        "upload-form-label-infoform-categories": "Санаттар",
        "upload-form-label-infoform-date": "Ай-күні",
-       "upload-form-label-own-work-message-shared": "Мен осы файлдың авторы екенінімді растаймын және бұл файлды Wikimedia Commons ортаққорына [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] лицензиясы аясында қайтымсыз жариялауға келісемін, сонымен бірге [https://wikimediafoundation.org/wiki/Terms_of_Use Қолдану шарттарына] да келісемін.",
-       "upload-form-label-not-own-work-message-shared": "Егер сіз осы файлдың авторы өзіңіз болмасаңыз немесе оны сіз басқа лицензия аясында жариялағыңыз келсе [https://commons.wikimedia.org/wiki/Special:UploadWizard Ортаққор Жүктеу шеберін] қолданыңыз.",
-       "upload-form-label-not-own-work-local-shared": "Егер сайт бұл файлды жүктеуге өзінің ережелері аясында рұқсат беретін болса, сіз сондай-ақ [[Special:Upload|{{SITENAME}} жобасындағы жүктеу бетін]] қолданып көргіңіз келетін шығар.",
        "backend-fail-stream": "«$1» файлы ақпады.",
        "backend-fail-backup": "«$1» файлының сақтық есесі жасалмады.",
        "backend-fail-notexists": "$1 файлы бар емес.",
        "delete-toobig": "Бұл бетте үлкен түзету тарихы бар, $1 {{PLURAL:$1|түзетуден|түзетуден}} астам.\nБұндай беттердің жоюы {{SITENAME}} торабын әлдеқалай үзіп тастауына бөгет салу үшін тиымдалған.",
        "delete-warning-toobig": "Бұл бетте үлкен өңдеу тарихы бар, $1 {{PLURAL:$1|түзетуден|түзетуден}} астам.\nБұның жоюы {{SITENAME}} торабындағы дерекқор әрекеттерді үзіп тастауын мүмкін;\nбұны абайлап өткізіңіз.",
        "deleteprotected": "Сіз бұл бетті жоя алмайсыз, себебі ол қорғалған.",
-       "deleting-backlinks-warning": "</strong>Ескерту:</strong>  Сіз жоймақшы болған бетке [[Special:WhatLinksHere/{{FULLPAGENAME}}|басқа беттерден]] сілтенген немесе [[Special:WhatLinksHere/{{FULLPAGENAME}}|басқа беттерге]] кірістірілген.",
+       "deleting-backlinks-warning": "<strong>Ескерту:</strong>  Сіз жоймақшы болған бетке [[Special:WhatLinksHere/{{FULLPAGENAME}}|басқа беттерден]] сілтенген немесе [[Special:WhatLinksHere/{{FULLPAGENAME}}|басқа беттерге]] кірістірілген.",
        "rollback": "Өңдемелерді шегіндіру",
        "rollbacklink": "шегіндіру",
        "rollbacklinkcount": "$1 {{PLURAL:$1|өңдемені|өңдемені}} шегіндіру",
index 6612f1a..f9a100f 100644 (file)
        "noname": "អ្នកមិនបានផ្ដល់អត្តនាមត្រឹមត្រូវទេ។",
        "loginsuccesstitle": "កត់ឈ្មោះចូលបានសម្រេច",
        "loginsuccess": "'''ពេលនេះអ្នកបានកត់ឈ្មោះចូល{{SITENAME}}ដោយប្រើឈ្មោះ \"$1\"ហើយ។'''",
-       "nosuchuser": "មិនមានអ្នកប្រើដែលមានឈ្មោះ \"$1\" ទេ។\n\nសូម​ពិនិត្យ​ក្រែង​លោ​មានកំហុស​អក្ខរាវិរុទ្ធឬ [[Special:UserLogin/signup|បង្កើត​គណនី​ថ្មី]]។",
+       "nosuchuser": "មិនមានអ្នកប្រើដែលមានឈ្មោះ \"$1\" ទេ។\n\nសូម​ពិនិត្យ​ក្រែង​លោ​មានកំហុស​អក្ខរាវិរុទ្ធឬ [[Special:CreateAccount|បង្កើត​គណនី​ថ្មី]]។",
        "nosuchusershort": "គ្មានអ្នកប្រើដែលមានឈ្មោះ $1\" ទេ។\n\nសូម​ពិនិត្យ​​អក្ខរាវិរុទ្ធ​របស់អ្នក ។",
        "nouserspecified": "អ្នកត្រូវតែ​ផ្ដល់អត្តនាម។",
        "login-userblocked": "អ្នកប្រើប្រាស់នេះស្ថិតក្រោមការហាមឃាត់។ មិនអនុញ្ញាតអោយកត់ឈ្មោះចូលទេ។",
        "accmailtext": "ពាក្យសម្ងាត់​ដែល​បាន​បង្កើត​ដោយ​ចៃដន្យ​សម្រាប់ [[User talk:$1|$1]] ត្រូវបានផ្ញើទៅ $2 ហើយ​។ អ្នកអាច​​ប្ដូរ​វាបាននៅ​​ទំព័រ ''[[Special:ChangePassword|ប្ដូរ​ពាក្យសម្ងាត់]]'' បន្ទាប់ពីកត់ឈ្មោះចូលហើយ​។",
        "newarticle": "(ថ្មី)",
        "newarticletext": "អ្នកបានតាម​តំណភ្ជាប់​ទៅ​ទំព័រដែលមិនទាន់មាននៅឡើយ។\nដើម្បីបង្កើតទំព័រនេះ សូមចាប់ផ្ដើមវាយ​ក្នុងប្រអប់ខាងក្រោម (សូមមើល [$1 ទំព័រ​ជំនួយ] សម្រាប់​ព័ត៌មានបន្ថែម)។\nបើ​អ្នក​មក​ទីនេះដោយអចេតនា សូមចុចប៊ូតុង '''ត្រឡប់ក្រោយ''' របស់ឧបករណ៍រាវរករបស់អ្នក។",
-       "anontalkpagetext": "----''ទំព័រពិភាក្សានេះគឺសម្រាប់តែអ្នកប្រើប្រាស់អនាមិកដែលមិនទាន់បានបង្កើតគណនីតែប៉ុណ្ណោះ។ ដូច្នេះអាសយដ្ឋានលេខIPរបស់កុំព្យូទ័ររបស់លោកអ្នក​នឹងត្រូវបានបង្ហាញ ដើមី្បសម្គាល់លោកអ្នក។\n\nអាសយដ្ឋានIPទាំងនោះអាចនឹងត្រូវប្រើដោយមនុស្សច្រើននាក់។\n\nប្រសិនបើអ្នកជាអ្នកប្រើប្រាស់អនាមិក​ហើយ​ប្រសិនបើអ្នកឃើញមានការបញ្ចេញយោបល់មកអ្នកពីអ្វី​ដែល​មិន​ទាក់ទងទៅនឹងអ្វីដែល​អ្នកបាន​ធ្វើ​ សូម[[Special:UserLogin/signup|បង្កើតគណនី]] ឬ [[Special:UserLogin|កត់ឈ្មោះចូល]] ដើម្បីចៀសវាង​ការភ័ន្តច្រឡំ​ណាមួយជាយថាហេតុជាមួយនិងអ្នកប្រើប្រាស់អនាមិកដទៃទៀត។''",
+       "anontalkpagetext": "----''ទំព័រពិភាក្សានេះគឺសម្រាប់តែអ្នកប្រើប្រាស់អនាមិកដែលមិនទាន់បានបង្កើតគណនីតែប៉ុណ្ណោះ។ ដូច្នេះអាសយដ្ឋានលេខIPរបស់កុំព្យូទ័ររបស់លោកអ្នក​នឹងត្រូវបានបង្ហាញ ដើមី្បសម្គាល់លោកអ្នក។\n\nអាសយដ្ឋានIPទាំងនោះអាចនឹងត្រូវប្រើដោយមនុស្សច្រើននាក់។\n\nប្រសិនបើអ្នកជាអ្នកប្រើប្រាស់អនាមិក​ហើយ​ប្រសិនបើអ្នកឃើញមានការបញ្ចេញយោបល់មកអ្នកពីអ្វី​ដែល​មិន​ទាក់ទងទៅនឹងអ្វីដែល​អ្នកបាន​ធ្វើ​ សូម[[Special:CreateAccount|បង្កើតគណនី]] ឬ [[Special:UserLogin|កត់ឈ្មោះចូល]] ដើម្បីចៀសវាង​ការភ័ន្តច្រឡំ​ណាមួយជាយថាហេតុជាមួយនិងអ្នកប្រើប្រាស់អនាមិកដទៃទៀត។''",
        "noarticletext": "បច្ចុប្បន្នគ្មានអត្ថបទក្នុងទំព័រនេះទេ។\n\nអ្នកអាច [[Special:Search/{{PAGENAME}}|ស្វែងរក​ចំណងជើង​នៃទំព័រនេះ]]ក្នុងទំព័រដទៃទៀត​​ ឬ [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ស្វែង​រក​កំណត់​ហេតុ​ដែល​ពាក់ព័ន្ធ] ឬ [{{fullurl:{{FULLPAGENAME}}|action=edit}} កែប្រែ​ទំព័រនេះ]។",
        "noarticletext-nopermission": "បច្ចុប្បន្ន គ្មានអត្ថបទណាមួយក្នុងទំព័រនេះទេ។\n\nអ្នកអាច [[Special:Search/{{PAGENAME}}|ស្វែងរក​ចំណងជើង​នៃទំព័រនេះ]] ក្នុងទំព័រ​ផ្សេងៗ ឬ<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ស្វែង​រក​កំណត់​ហេតុ​ដែល​ពាក់ព័ន្ធ]</span>។ ប៉ុន្តែអ្នកគ្មានសិទ្ធិក្នុងការបង្កើតទំព័រនេះទេ។",
        "userpage-userdoesnotexist": "គណនីអ្នកប្រើឈ្មោះ\"<nowiki>$1</nowiki>\" មិនទាន់បានចុះបញ្ជី។\n\nចូរគិតម្ដងទៀតថាអ្នកចង់ បង្កើត / កែប្រែ ទំព័រនេះឬទេ។",
index 50f3aa5..e8993dd 100644 (file)
        "noname": "ನೀವು ಸರಿಯಾದ ಬಳಕೆದಾರ ಹೆಸರನ್ನು ಸೂಚಿಸಿಲ್ಲ.",
        "loginsuccesstitle": "ಲಾಗಿನ್ ಯಶಸ್ವಿ",
        "loginsuccess": "ನೀವು ಈಗ \"$1\" ಆಗಿ ವಿಕಿಪೀಡಿಯಕ್ಕೆ ಲಾಗಿನ್ ಆಗಿದ್ದೀರಿ.",
-       "nosuchuser": "\"$1\" ಹೆಸರಿನ ಯಾವ ಸದಸ್ಯರೂ ಇಲ್ಲ.\nಸದಸ್ಯನಾಮದಲ್ಲಿ ಲಘು ಮತ್ತು ದೀರ್ಘ ಅಕ್ಷರಗಳಲ್ಲಿ ವ್ಯತ್ಯಾಸವಿದೆ.\nಕಾಗುಣಿತವನ್ನು ಪರೀಕ್ಷಿಸಿ, ಅಥವಾ [[Special:UserLogin/signup|ಹೊಸ ಸದಸ್ಯತ್ವ ಖಾತೆಯನ್ನು ಸೃಷ್ಟಿಸಿ]].",
+       "nosuchuser": "\"$1\" ಹೆಸರಿನ ಯಾವ ಸದಸ್ಯರೂ ಇಲ್ಲ.\nಸದಸ್ಯನಾಮದಲ್ಲಿ ಲಘು ಮತ್ತು ದೀರ್ಘ ಅಕ್ಷರಗಳಲ್ಲಿ ವ್ಯತ್ಯಾಸವಿದೆ.\nಕಾಗುಣಿತವನ್ನು ಪರೀಕ್ಷಿಸಿ, ಅಥವಾ [[Special:CreateAccount|ಹೊಸ ಸದಸ್ಯತ್ವ ಖಾತೆಯನ್ನು ಸೃಷ್ಟಿಸಿ]].",
        "nosuchusershort": "\"$1\" ಹೆಸರಿನ ಸದಸ್ಯರು ಯಾರೂ ಇಲ್ಲ.\nಹೆಸರಲ್ಲಿ ಕಾಗುಣಿತ ತಪ್ಪಿದೆಯೆ ಎಂದು ಪರೀಕ್ಷಿಸಿ.",
        "nouserspecified": "ನೀವು ಒಂದು ಸದಸ್ಯತ್ವದ ಹೆಸರನ್ನು ಸೂಚಿಸಬೇಕು.",
        "login-userblocked": "ಈ ಬಳಕೆದಾರರನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ. ಲಾಗಿನ್ ಆಗಲು ಸಾದ್ಯವಿಲ್ಲ.",
        "accmailtext": "[[User talk:$1|$1]] ಅವರ ಹೊಸ ಪ್ರವೇಶಪದ $2 ಗೆ ಕಳುಹಿಸಲಾಗಿದೆ.\n\nಈ ಖಾತೆಯ ಪ್ರವೇಶಪದವನ್ನು ಲಾಗಿನ್ ಆದ ನಂತರ ''[[Special:ChangePassword|ಪ್ರವೇಶಪದ ಬದಲಾವಣೆ]]'' ಪುಟದಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು.",
        "newarticle": "(ಹೊಸತು)",
        "newarticletext": "ಇನ್ನೂ ಅಸ್ಥಿತ್ವದಲ್ಲಿ ಇರದ ಪುಟದ ಲಿಂಕ್ ಅನ್ನು ನೀವು ಒತ್ತಿರುವಿರಿ.\nಈ ಪುಟವನ್ನು ಸೃಷ್ಟಿಸಲು ಕೆಳಗಿನ ಚೌಕದಲ್ಲಿ ಬರೆಯಲು ಆರಂಭಿಸಿರಿ.\n(ಹೆಚ್ಚು ಮಾಹಿತಿಗೆ [$1 ಸಹಾಯ ಪುಟ] ನೋಡಿ).\nಈ ಪುಟಕ್ಕೆ ನೀವು ತಪ್ಪಾಗಿ ಬಂದಿದ್ದಲ್ಲಿ ನಿಮ್ಮ ಬ್ರೌಸರ್‍ನ '''back''' ಬಟನ್ ಅನ್ನು ಒತ್ತಿ.",
-       "anontalkpagetext": "----''ಇದು ಖಾತೆಯೊಂದನ್ನು ಹೊಂದಿರದ ಅನಾಮಧೇಯ ಬಳಕೆದಾರರೊಬ್ಬರ ಚರ್ಚೆ ಪುಟ.\nಖಾತೆಯಿಲ್ಲದಿರುವುದರಿಂದ ಅವರನ್ನು ಗುರುತಿಸಲು ಅವರ IP ವಿಳಾಸವನ್ನು ಉಪಯೋಗಿಸುತ್ತಿದ್ದೇವೆ.\nಈ ರೀತಿಯ IP ವಿಳಾಸವು ಅನೇಕ ಬಳಕೆದಾರರಿಂದ ಉಪಯೋಗದಲ್ಲಿರಬಹುದು.\nನೀವು ಅನಾಮಧೇಯ ಬಳಕೆದಾರರಾಗಿದ್ದಲ್ಲಿ, ಹಾಗು ನಿಮಗೆ ಸಂಬಂಧವಿಲ್ಲದಂತ ಸಂದೇಶಗಳು ಬರುತ್ತಿವೆ ಎಂದು ಅನಿಸಿದರೆ, ಮುಂದೆ ಬೇರೆ ಅನಾಮಧೇಯ ಬಳಕೆದಾರರೊಂದಿಗೆ ತಪ್ಪಾಗಿ ಗುರುತಿಸಬಾರದೆಂದಿದ್ದರೆ ದಯವಿಟ್ಟು [[Special:UserLogin/signup|ಸದಸ್ಯರಾಗಿ]] ಅಥವ [[Special:UserLogin|ಲಾಗ್ ಇನ್ ಆಗಿ]].''",
+       "anontalkpagetext": "----''ಇದು ಖಾತೆಯೊಂದನ್ನು ಹೊಂದಿರದ ಅನಾಮಧೇಯ ಬಳಕೆದಾರರೊಬ್ಬರ ಚರ್ಚೆ ಪುಟ.\nಖಾತೆಯಿಲ್ಲದಿರುವುದರಿಂದ ಅವರನ್ನು ಗುರುತಿಸಲು ಅವರ IP ವಿಳಾಸವನ್ನು ಉಪಯೋಗಿಸುತ್ತಿದ್ದೇವೆ.\nಈ ರೀತಿಯ IP ವಿಳಾಸವು ಅನೇಕ ಬಳಕೆದಾರರಿಂದ ಉಪಯೋಗದಲ್ಲಿರಬಹುದು.\nನೀವು ಅನಾಮಧೇಯ ಬಳಕೆದಾರರಾಗಿದ್ದಲ್ಲಿ, ಹಾಗು ನಿಮಗೆ ಸಂಬಂಧವಿಲ್ಲದಂತ ಸಂದೇಶಗಳು ಬರುತ್ತಿವೆ ಎಂದು ಅನಿಸಿದರೆ, ಮುಂದೆ ಬೇರೆ ಅನಾಮಧೇಯ ಬಳಕೆದಾರರೊಂದಿಗೆ ತಪ್ಪಾಗಿ ಗುರುತಿಸಬಾರದೆಂದಿದ್ದರೆ ದಯವಿಟ್ಟು [[Special:CreateAccount|ಸದಸ್ಯರಾಗಿ]] ಅಥವ [[Special:UserLogin|ಲಾಗ್ ಇನ್ ಆಗಿ]].''",
        "noarticletext": "ಈ ಪುಟದಲ್ಲಿ ಸದ್ಯಕ್ಕೆ ಏನೂ ಇಲ್ಲ.\nನೀವು ಇತರ ಪುಟಗಳಲ್ಲಿ [[Special:Search/{{PAGENAME}}|ಈ ಹೆಸರನ್ನು ಹುಡುಕಬಹುದು]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ಸಂಬಂಧಿತ ದಾಖಲೆಗಳನ್ನು ಹುಡುಕಬಹುದು],\nಅಥವ [{{fullurl:{{FULLPAGENAME}}|action=edit}} ಈ ಪುಟವನ್ನು ಸಂಪಾದಿಸಬಹುದು]</span>.",
        "noarticletext-nopermission": "ಈ ಪುಟದಲ್ಲಿ ಸದ್ಯಕ್ಕೆ ಯಾವ ಪಠ್ಯವೂ ಇಲ್ಲ.\nನೀವು ಇತರ ಪುಟಗಳಲ್ಲಿ [[ವಿಶೇಷ:Search/{{PAGENAME}}|ಈ ಶೀರ್ಷಿಕೆಗಾಗಿ ಹುಡುಕಬಹುದು]],\nಅಥವಾ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ಸಂಬಂಧಿಸಿದ ದಾಖಲಾತಿ ಹುಡುಕಬಹುದು]</span>, ಆದರೆ ನಿಮಗೆ ಈ ಪುಟವನ್ನು ಸಂಪಾದಿಸಲು ಅನುಮತಿಯಿಲ್ಲ.",
        "userpage-userdoesnotexist": "ಬಳಕೆದಾರ ಖಾತೆ \"<nowiki>$1</nowiki>\" ದಾಖಲಾಗಿಲ್ಲ. ನೀವು ಇದೇ ಪುಟವನ್ನು ಸೃಷ್ಟಿ/ಸಂಪಾದನೆ ಮಾಡಬೇಕೆಂದಿರುವಿರಿ ಎಂದು ಖಾತ್ರಿ ಮಾಡಿಕೊಳ್ಳಿ.",
index e9a314c..dbee372 100644 (file)
        "noname": "사용자 계정 이름이 올바르지 않습니다.",
        "loginsuccesstitle": "로그인함",
        "loginsuccess": "<strong>{{SITENAME}}에 \"$1\" 계정으로 로그인했습니다.</strong>",
-       "nosuchuser": "이름이 \"$1\"인 사용자는 없습니다.\n사용자 계정 이름은 대소문자를 구별합니다.\n철자가 맞는지 확인해주세요. [[Special:UserLogin/signup|새 계정을 만들 수도 있습니다]].",
+       "nosuchuser": "이름이 \"$1\"인 사용자는 없습니다.\n사용자 계정 이름은 대소문자를 구별합니다.\n철자가 맞는지 확인해주세요. [[Special:CreateAccount|새 계정을 만들 수도 있습니다]].",
        "nosuchusershort": "이름이 \"$1\"인 사용자는 없습니다.\n철자가 맞는지 확인하세요.",
        "nouserspecified": "사용자 계정 이름을 입력하지 않았습니다.",
        "login-userblocked": "이 사용자는 차단되었습니다. 로그인할 수 없습니다.",
        "passwordreset-emailsentemail": "당신의 계정과 연결된 이메일 주소가 있다면, 비밀번호 재설정 메일이 전해질 것입니다.",
        "passwordreset-emailsentusername": "이 사용자 이름과 연결된 이메일 주소가 있다면 비밀번호 초기화 이메일이 전송됩니다.",
        "passwordreset-emailsent-capture": "비밀번호 재설정 이메일이 발송되었으며, 아래에 나타나 있습니다.",
-       "passwordreset-emailerror-capture": "ë¹\84ë°\80ë²\88í\98¸ ì\9e¬ì\84¤ì \95 ì\9d´ë©\94ì\9d¼ì\9d´ ì\83\9dì\84±ë\90\98ì\96´ ì\95\84ë\9e\98ì\97\90 ë³´ì\97¬ì ¸ 있지만, {{GENDER:$2|사용자}}에게 발송하는 데에는 실패했습니다: $1",
+       "passwordreset-emailerror-capture": "ë¹\84ë°\80ë²\88í\98¸ ì\9e¬ì\84¤ì \95 ì\9d´ë©\94ì\9d¼ì\9d´ ì\83\9dì\84±ë\90\98ì\96´ ì\95\84ë\9e\98ì\97\90 ë\82\98í\83\80ë\82\98 있지만, {{GENDER:$2|사용자}}에게 발송하는 데에는 실패했습니다: $1",
        "changeemail": "이메일 주소를 바꾸거나 제거하기",
        "changeemail-header": "이메일 주소를 바꾸려면 이 양식을 채우세요. 계정에서 이메일 연동을 취소하고 싶다면 양식을 제출할 때 새 이메일 주소를 공란으로 두세요.",
        "changeemail-passwordrequired": "변경을 적용하려면 비밀번호를 입력해야 합니다.",
        "accmailtext": "[[User talk:$1|$1]] 사용자의 비밀번호를 임의로 만들어 $2(으)로 보냈습니다. 로그인하고 나서 <em>[[Special:ChangePassword|비밀번호를 바꿀]]</em> 수 있습니다.",
        "newarticle": "(새 문서)",
        "newarticletext": "아직 없는 문서의 링크를 따라왔습니다.\n새 문서를 만들려면 아래 상자에 내용을 입력하면 됩니다. (자세한 내용은 [$1 도움말 문서]를 참조하세요)\n만약 잘못 찾아왔다면, 브라우저의 '''뒤로''' 버튼을 눌러 주세요.",
-       "anontalkpagetext": "----\n여기는 계정을 만들지 않았거나 사용하고 있지 않은 익명 사용자를 위한 토론 문서입니다.\n익명 사용자를 구별하기 위해서는 숫자로 된 IP 주소를 사용해야만 합니다.\nIP 주소는 여러 사용자가 공유할 수 있습니다.\n자신과 관계없는 의견이 자신에게 남겨져 있어 불쾌하다고 생각하는 익명 사용자는 [[Special:UserLogin/signup|계정을 만들고]] [[Special:UserLogin|로그인해서]] 나중에 다른 익명 사용자에게 줄 혼란을 줄일 수 있습니다.",
+       "anontalkpagetext": "----\n여기는 계정을 만들지 않았거나 사용하고 있지 않은 익명 사용자를 위한 토론 문서입니다.\n익명 사용자를 구별하기 위해서는 숫자로 된 IP 주소를 사용해야만 합니다.\nIP 주소는 여러 사용자가 공유할 수 있습니다.\n자신과 관계없는 의견이 자신에게 남겨져 있어 불쾌하다고 생각하는 익명 사용자는 [[Special:CreateAccount|계정을 만들고]] [[Special:UserLogin|로그인해서]] 나중에 다른 익명 사용자에게 줄 혼란을 줄일 수 있습니다.",
        "noarticletext": "이 문서가 현재 존재하지 않습니다.\n이 문서와 제목이 비슷한 문서가 있는지 [[Special:Search/{{PAGENAME}}|검색하거나]],\n이 문서에 관련된 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 기록]을 확인하거나,\n문서를 직접 [{{fullurl:{{FULLPAGENAME}}|action=edit}} 생성]</span>할 수 있습니다.",
        "noarticletext-nopermission": "이 문서가 현재 존재하지 않습니다.\n이 문서와 제목이 비슷한 문서가 있는지 [[Special:Search/{{PAGENAME}}|검색하거나]], 이 문서에 관련된 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 기록]을 확인할 수 있습니다.</span> 그러나 이 문서를 만들 수 있는 권한은 없습니다.",
        "missing-revision": "\"{{FULLPAGENAME}}\"이라는 문서의 #$1판이 존재하지 않습니다.\n\n이 문제는 주로 삭제된 문서를 가리키는 오래된 문서 역사 링크로 인해 발생합니다.\n자세한 내용은 [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 삭제 기록]에서 확인할 수 있습니다.",
        "right-managechangetags": "데이터베이스에서 [[Special:Tags|태그]]를 만들거나 지우기",
        "right-applychangetags": "자신이 편집할 때 [[Special:Tags|태그]]를 적용하기",
        "right-changetags": "문서의 특정 판과 특정 기록 항목에 임의의 [[Special:Tags|태그]]를 추가하거나 제거하기",
+       "right-deletechangetags": "데이터베이스에서 [[Special:Tags|태그]]를 지우기",
        "grant-generic": "\"$1\" 권한 번들",
        "grant-group-page-interaction": "문서로 상호 작용",
        "grant-group-file-interaction": "미디어로 상호 작용",
        "action-viewmyprivateinfo": "자신의 개인정보 보기",
        "action-editmyprivateinfo": "자신의 개인정보 편집",
        "action-editcontentmodel": "문서의 콘텐츠 모델을 편집",
-       "action-managechangetags": "ë\8d°ì\9d´í\84°ë² ì\9d´ì\8a¤ì\97\90ì\84\9c í\83\9c그를 ë§\8cë\93¤ê±°ë\82\98 ì§\80ì\9a¸",
+       "action-managechangetags": "ë\8d°ì\9d´í\84°ë² ì\9d´ì\8a¤ì\97\90ì\84\9c í\83\9c그를 ë§\8cë\93¤ê±°ë\82\98 ì§\80ì\9a°ê¸°",
        "action-applychangetags": "당신이 편집할 때 태그를 적용하기",
        "action-changetags": "문서의 특정 판과 특정 기록 항목에 임의의 태그를 추가하거나 제거하기",
+       "action-deletechangetags": "데이터베이스에서 태그를 지우기",
        "nchanges": "$1개 {{PLURAL:$1|바뀜}}",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|마지막 방문 이후}} $1개",
        "enhancedrc-history": "역사",
        "upload-form-label-own-work": "자작입니다",
        "upload-form-label-infoform-categories": "분류",
        "upload-form-label-infoform-date": "날짜",
-       "upload-form-label-own-work-message-local": "사용자는 이 파일을 업로드할 때 {{SITENAME}}의 서비스 조항과 라이선스 정책에 동의하는 것으로 간주합니다.",
-       "upload-form-label-not-own-work-message-local": "이 파일을 {{SITENAME}}의 정책에 따라 업로드할 수 없으면 이 대화 상자를 닫고 다른 방식을 사용하십시오.",
-       "upload-form-label-not-own-work-local-local": "[[Special:Upload|기본 파일 올리기 문서]]를 이용할 수도 있습니다.",
-       "upload-form-label-own-work-message-default": "이 파일을 공유 저장소에 업로드하면 사용자는 서비스 조항과 라이선스 정책에 동의하는 것으로 간주합니다.",
-       "upload-form-label-not-own-work-message-default": "이 파일을 공유 저장소의 정책에 따라 업로드할 수 없으면 이 대화 상자를 닫고 다른 방식을 사용하십시오.",
-       "upload-form-label-not-own-work-local-default": "이 파일이 정책에 따라 업로드가 가능할 경우 [[Special:Upload|{{SITENAME}}의 기본 파일 올리기 문서]]를 이용할 수도 있습니다.",
-       "upload-form-label-own-work-message-shared": "나는 이 파일에 대한 저작권을 소유하고 있음을 입증하고, 영구히 위키미디어 공용에 이 파일을 [https://creativecommons.org/licenses/by-sa/4.0/ 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0]에 따라 배포하는 데 동의하며, [https://wikimediafoundation.org/wiki/Terms_of_Use 이용 약관]에 동의합니다.",
-       "upload-form-label-not-own-work-message-shared": "이 파일의 저작권을 소유하지 않거나 다른 라이선스로 배포하고 싶다면 [https://commons.wikimedia.org/wiki/Special:UploadWizard 공용 파일 올리기 마법사]를 사용하는 것을 고려해보세요.",
-       "upload-form-label-not-own-work-local-shared": "사이트가 정책에 따라 이 파일의 업로드를 허용할 경우 [[Special:Upload|{{SITENAME}}의 기본 파일 올리기 문서]]를 이용할 수도 있습니다.",
+       "upload-form-label-own-work-message-generic-local": "사용자는 이 파일을 업로드할 때 {{SITENAME}}의 서비스 조항과 라이선스 정책에 동의하는 것으로 간주합니다.",
+       "upload-form-label-not-own-work-message-generic-local": "이 파일을 {{SITENAME}}의 정책에 따라 업로드할 수 없으면 이 대화 상자를 닫고 다른 방식을 사용하십시오.",
+       "upload-form-label-not-own-work-local-generic-local": "[[Special:Upload|기본 파일 올리기 문서]]를 이용할 수도 있습니다.",
+       "upload-form-label-own-work-message-generic-foreign": "이 파일을 공유 저장소에 업로드하면 사용자는 서비스 조항과 라이선스 정책에 동의하는 것으로 간주합니다.",
+       "upload-form-label-not-own-work-message-generic-foreign": "이 파일을 공유 저장소의 정책에 따라 업로드할 수 없으면 이 대화 상자를 닫고 다른 방식을 사용하십시오.",
+       "upload-form-label-not-own-work-local-generic-foreign": "이 파일이 정책에 따라 업로드가 가능할 경우 [[Special:Upload|{{SITENAME}}의 기본 파일 올리기 문서]]를 이용할 수도 있습니다.",
        "backend-fail-stream": "\"$1\" 파일을 스트림할 수 없습니다.",
        "backend-fail-backup": "\"$1\" 파일을 백업할 수 없습니다.",
        "backend-fail-notexists": "$1 파일이 존재하지 않습니다.",
        "apisandbox-results": "결과",
        "apisandbox-sending-request": "API 요청을 보내는 중...",
        "apisandbox-loading-results": "API 결과를 받는 중...",
-       "apisandbox-results-error": "API ì§\88ì\9d\98 ì\9a\94ì²­ì\9d\84 ë¡\9cë\94©í\95\98ë\8a\94 ë\8f\84ì¤\91 ì\97\90ë\9f¬ 발생: $1.",
+       "apisandbox-results-error": "API ì§\88ì\9d\98 ì\9d\91ë\8bµì\9d\84 ë¶\88ë\9f¬ì\98¤ë\8a\94 ë\8f\84ì¤\91 ì\98¤ë¥\98 발생: $1.",
        "apisandbox-request-url-label": "요청 URL:",
        "apisandbox-request-time": "요청 처리 시간: {{PLURAL:$1|$1 ms}}",
        "apisandbox-results-fixtoken": "토큰 수정 후 다시 제출",
        "unwatchthispage": "주시 해제하기",
        "notanarticle": "문서가 아님",
        "notvisiblerev": "이 판은 삭제되었습니다.",
-       "watchlist-details": "별도의 토론 문서를 세지 않고, 주시문서 목록에 {{PLURAL:$1|문서 $1개}}가 있습니다.",
+       "watchlist-details": "토론 문서의 수는 제외하고, 주시문서 목록에 {{PLURAL:$1|문서 $1개}}가 있습니다.",
        "wlheader-enotif": "이메일 알림 기능이 활성화되었습니다.",
        "wlheader-showupdated": "마지막으로 방문한 이후에 바뀐 문서는 '''굵은 글씨'''로 보입니다.",
        "wlnote": "$3 $4 기준으로, 아래에 최근 {{PLURAL:$2|한 시간|<strong>$2</strong>시간}} 동안 {{PLURAL:$1|마지막 바뀜이|마지막 바뀜 <strong>$1</strong>개가}} 있습니다.",
        "changecontentmodel-success-text": "[[:$1]]의 콘텐츠 종류가 변경되었습니다.",
        "changecontentmodel-cannot-convert": "[[:$1]]의 콘텐츠 모델이 $2의 모델로 전환될 수 없습니다.",
        "changecontentmodel-nodirectediting": "$1 콘텐츠 모델은 직접 편집을 지원하지 않습니다",
+       "changecontentmodel-emptymodels-title": "이용 가능한 콘텐츠 모델이 없음",
+       "changecontentmodel-emptymodels-text": "[[:$1]]의 콘텐츠가 임의의 종류로 전환될 수 없습니다.",
        "log-name-contentmodel": "콘텐츠 모델 변경 기록",
        "log-description-contentmodel": "페이지의 콘텐츠 모델과 관련된 행위",
        "logentry-contentmodel-new": "$1님이 비 기본값 \"$5\" 콘텐츠 모델을 사용해  $3 문서를 {{GENDER:$2|만들었습니다}}",
        "whatlinkshere-prev": "{{PLURAL:$1|이전|이전 $1개}}",
        "whatlinkshere-next": "{{PLURAL:$1|다음|다음 $1개}}",
        "whatlinkshere-links": "← 가리키는 문서 목록",
-       "whatlinkshere-hideredirs": "넘겨주기를 $1",
-       "whatlinkshere-hidetrans": "끼워넣기를 $1",
-       "whatlinkshere-hidelinks": "링크를 $1",
-       "whatlinkshere-hideimages": "파일 링크를 $1",
+       "whatlinkshere-hideredirs": "넘겨주기를 숨기기",
+       "whatlinkshere-hidetrans": "끼워넣기를 숨기기",
+       "whatlinkshere-hidelinks": "링크를 숨기기",
+       "whatlinkshere-hideimages": "파일 링크를 숨기기",
        "whatlinkshere-filters": "필터",
        "whatlinkshere-submit": "계속",
        "autoblockid": "자동 차단 #$1",
        "lockdbsuccesstext": "데이터베이스가 잠겼습니다.<br />\n관리가 끝나면 잊지 말고 [[Special:UnlockDB|잠금을 풀어]] 주세요.",
        "unlockdbsuccesstext": "데이터베이스 잠금 상태가 해제되었습니다.",
        "lockfilenotwritable": "데이터베이스 잠금 파일에 쓰기 권한이 없습니다.\n데이터베이스를 잠그거나 잠금 해제하려면, 웹 서버에서 이 파일의 쓰기 권한을 설정해야 합니다.",
+       "databaselocked": "데이터베이스가 이미 잠겨 있습니다.",
        "databasenotlocked": "데이터베이스가 잠겨 있지 않습니다.",
        "lockedbyandtime": "({{GENDER:$1|$1}} 사용자가 $2 $3에 잠금)",
        "move-page": "$1 이동",
        "timezone-local": "로컬",
        "duplicate-defaultsort": "<strong>경고:</strong> 기본 정렬 키 \"$2\"가 이전의 기본 정렬 키 \"$1\"를 덮어쓰고 있습니다.",
        "duplicate-displaytitle": "<strong>경고:</strong> \"$2\" 제목 표시는 기존의 표시되는 제목 \"$1\"을 덮어씁니다.",
+       "restricted-displaytitle": "<strong>경고:</strong> 표시하려는 제목 \"$1\"은(는) 문서의 실제 제목과 동일하지 않으므로 무시되었습니다.",
        "invalid-indicator-name": "<strong>오류:</strong> 문서 상태 표시기의 <code>name</code> 특성은 비어 있지 않아야 합니다.",
        "version": "버전",
        "version-extensions": "설치된 확장 기능",
        "tags-delete-not-found": "\"$1\" 태그가 존재하지 않습니다.",
        "tags-delete-too-many-uses": "\"$1\" 태그가 $2개 이상의 판에 적용되어 있으므로 삭제할 수 없습니다.",
        "tags-delete-warnings-after-delete": "\"$1\" 태그가 삭제되었으나 다음과 같은 $2개의 경고 태그가 발생하였습니다:",
+       "tags-delete-no-permission": "변경 태그를 삭제할 권한이 없습니다.",
        "tags-activate-title": "태그 활성화",
        "tags-activate-question": "\"$1\" 태그를 활성화하려고 합니다.",
        "tags-activate-reason": "이유:",
        "logentry-protect-protect-cascade": "$1님이 $3 문서를 {{GENDER:$2|보호했습니다}} $4 [연쇄적]",
        "logentry-protect-modify": "$1님이 $3 문서의 보호 수준을 {{GENDER:$2|바꾸었습니다}} $4",
        "logentry-protect-modify-cascade": "$1님이 $3 문서의 보호 수준을 {{GENDER:$2|바꾸었습니다}} $4 [연쇄적]",
-       "logentry-rights-rights": "$1님이 {{GENDER:$6|$3}}을(를) 위해 $3 사용자의 권한을 $4에서 $5(으)로 {{GENDER:$2|바꾸었습니다}}",
+       "logentry-rights-rights": "$1님이 $3 사용자의 권한을 $4에서 $5(으)로 {{GENDER:$2|바꾸었습니다}}",
        "logentry-rights-rights-legacy": "$1님이 $3 사용자의 권한을 {{GENDER:$2|바꾸었습니다}}",
        "logentry-rights-autopromote": "$1님이 권한을 자동적으로 $4에서 $5으로 {{GENDER:$2|바꾸었습니다}}",
        "logentry-upload-upload": "$1님이 $3 파일을 {{GENDER:$2|올렸습니다}}",
index 432a3ab..3a4caa7 100644 (file)
        "noname": "Терс атны джазгъансыз.",
        "loginsuccesstitle": "Авторизация тыйыншлы ётдю",
        "loginsuccess": "'''Энди {{SITENAME}} сайтха «$1» ат бла кирдигиз.'''",
-       "nosuchuser": "$1 аты бла къошулуучу джокъду.\nКъошулуучу атла харифни регистрин (уллу-гитчеликлерин) айырады.\nАтны тюз джазылгъанына къарагъыз неда [[Special:UserLogin/signup|джангы тергеу джазыу (аккаунт) къурагъаз]].",
+       "nosuchuser": "$1 аты бла къошулуучу джокъду.\nКъошулуучу атла харифни регистрин (уллу-гитчеликлерин) айырады.\nАтны тюз джазылгъанына къарагъыз неда [[Special:CreateAccount|джангы тергеу джазыу (аккаунт) къурагъаз]].",
        "nosuchusershort": "$1 аты бла къшулуучу джокъду. Атны тюз джазылгъанына къарагъыз.",
        "nouserspecified": "Сиз къошулуучу атыгъызны джазаргъа керексиз.",
        "login-userblocked": "Бу къошулуучу блокга салыннганды. Кирирге мадары джокъду.",
        "accmailtext": "[[User talk:$1|$1]] къошулуучугъа къуралгъан пароль $2 адресине джиберилгенди.\n\nРегистрацияны тындыргъындан сора,  ''[[Special:ChangePassword|паролну тюрлендирирге]]'' боллукъсуз.",
        "newarticle": "(Джангы)",
        "newarticletext": "Сиз джибериу бла алкъын къуралмагъан бетге кёчгенсиз.\nАны къурар ючюн тюбюндеги терезеде статьяны текстин басмалагъыз (толуракъ [$1 ангылатыу бетде] къарагъыз).\nДжангылыб кирген эсегиз а уа бери, браузеригизни '''артха''' тиегин басыгъызда къоюгъуз.",
-       "anontalkpagetext": "----''Бу сюзюу бет, тергеу джазыу (аккаунт) къурамагъан неда аны бла хайырланмагъан аноним къошулуучунукъуду.\nАны ючюн идентификация этерге IP-адрес хайырланады.\nТалай башха къошулуучуланы да болургъа боллукъ быллай адреслери.\nСиз аноним къошулуучу эсегиз эмда сизге джиберилмеген билдириуле алама деб суна эсегиз, ангылашылмаула болмаз ючюн [[Special:UserLogin|тергеу джазыу (аккаунт) къурагъыз]] неда [[Special:UserLogin/signup|системагъа киригиз]].''",
+       "anontalkpagetext": "----''Бу сюзюу бет, тергеу джазыу (аккаунт) къурамагъан неда аны бла хайырланмагъан аноним къошулуучунукъуду.\nАны ючюн идентификация этерге IP-адрес хайырланады.\nТалай башха къошулуучуланы да болургъа боллукъ быллай адреслери.\nСиз аноним къошулуучу эсегиз эмда сизге джиберилмеген билдириуле алама деб суна эсегиз, ангылашылмаула болмаз ючюн [[Special:UserLogin|тергеу джазыу (аккаунт) къурагъыз]] неда [[Special:CreateAccount|системагъа киригиз]].''",
        "noarticletext": "Бусагъатда бу бетде текст джокъду.\nСиз [[Special:Search/{{PAGENAME}}|бу атны башха статьялада]] излерге , <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} журналлагъа къараргъа], неда '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} быллай атлы джангы бет къураргъа боллукъсуз]'''</span>.",
        "noarticletext-nopermission": "Бусагъатда бу бетде текст джокъду.\nСиз [[Special:Search/{{PAGENAME}}|бу атны таныгъан]] башха статьяланы, неда <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} журналлада джазылгъанланы]</span> табаргъа боллукъсуз, алай а бу бетни къураргъа эркинлигигиз джокъду.",
        "userpage-userdoesnotexist": "«<nowiki>$1</nowiki>» тергеу джазыу (аккаунт) джокъду. Къураргъа/тюрлендирирге излеймисиз бу бетни?",
index 7e4cc1a..0a3f3a3 100644 (file)
        "noname": "Dat jeiht nit als ene Metmaacher Name. Jetz muss De et noch ens versöke.",
        "loginsuccesstitle": "Enjelogg",
        "loginsuccess": "'''Do bes jetz enjelogg {{GRAMMAR:en|{{SITENAME}}}}, un Dinge Name als ene Metmaacher es „$1“.'''",
-       "nosuchuser": "Dä Metmaacher Name „$1“ wor verkihrt.\nJroß- un Kleinboochshtabe maache ene Ungerscheid!\n<br />\nJetz muss De et noch ens versöke.\nUdder donn_[[Special:UserLogin/signup|ene neue Metmaacher aanmelde]].",
+       "nosuchuser": "Dä Metmaacher Name „$1“ wor verkihrt.\nJroß- un Kleinboochshtabe maache ene Ungerscheid!\n<br />\nJetz muss De et noch ens versöke.\nUdder donn_[[Special:CreateAccount|ene neue Metmaacher aanmelde]].",
        "nosuchusershort": "Dä Metmaacher Name „$1“ wor verkihrt. Jetz muss De et noch ens versöke.",
        "nouserspecified": "Dat jeiht nit als ene Metmaacher Name",
        "login-userblocked": "Heh dä Kääl es jesperrt. Enlogge verbodde.",
        "accmailtext": "En automattesch un zofällesch neu ußjewörfelt Passwood för dä\nMetmaacher „[[User talk:$1|$1]]“ es noh „$2“ jescheck woode.\n\nDat Passwoot för dä neue Zojang kanns De op dä {{int:Specialpage}} zom\n„[[Special:ChangePassword|{{int:resetpass}}]]“ ändere,\nwann De wider enjelogg bes.",
        "newarticle": "(Neu)",
        "newarticletext": "Ene Link op en Sigg, wo noch nix drop steiht, weil et se noch jar nit jitt, hät Dich noh heh jebraht.\nÖm di Sigg aanzelähje, schriev heh unge en dat Feld eren, un dun dat dann avspeichere.\nLuur op de [$1 Sigge met Hölp] noh, wann De mih doh drövver weßße wells.\nWann De jar nit heh hen kumme wollts, dann jangk zeröck op di Sigg, wo De herjekumme bes, Dinge Brauser hät ene Knopp doför.",
-       "anontalkpagetext": "----\n<i>Dat heh es de Klaaf Sigg för ene namenlose Metmaacher. Dä hät sich noch keine Metmaacher Name jejovve un\nenjerich, ov deit keine bruche. Dröm bruche mer sing IP Adress öm It oder In en uns Lisste fasszehalde.\nSu en IP Adress kann vun janz vill Metmaacher jebruch wääde, un eine Metmaacher kann janz flöck\nzwesche de ungerscheidlichste IP Adresse wähßele, womöchlich ohne dat hä et merk. Wann Do jetz ene namenlose\nMetmaacher bes, un fings, dat heh Saache an Dich jeschrevve wääde, wo Do jar nix met am Hot häs, dann bes Do\nwahrscheinlich och nit jemeint. Denk villeich ens drüvver noh, datte Dich [[Special:UserLogin/signup|anmelde]] deis,\ndomet De dann donoh nit mieh met esu en Ömständ ze dun häs, wie de andere namenlose Metmaacher heh. Wann de aanjemelldt bes un deis [[Special:UserLogin|enlogge]], dann kam_mer Desch och fun alle andere Metmaacher ongerschejde.</i>",
+       "anontalkpagetext": "----\n<i>Dat heh es de Klaaf Sigg för ene namenlose Metmaacher. Dä hät sich noch keine Metmaacher Name jejovve un\nenjerich, ov deit keine bruche. Dröm bruche mer sing IP Adress öm It oder In en uns Lisste fasszehalde.\nSu en IP Adress kann vun janz vill Metmaacher jebruch wääde, un eine Metmaacher kann janz flöck\nzwesche de ungerscheidlichste IP Adresse wähßele, womöchlich ohne dat hä et merk. Wann Do jetz ene namenlose\nMetmaacher bes, un fings, dat heh Saache an Dich jeschrevve wääde, wo Do jar nix met am Hot häs, dann bes Do\nwahrscheinlich och nit jemeint. Denk villeich ens drüvver noh, datte Dich [[Special:CreateAccount|anmelde]] deis,\ndomet De dann donoh nit mieh met esu en Ömständ ze dun häs, wie de andere namenlose Metmaacher heh. Wann de aanjemelldt bes un deis [[Special:UserLogin|enlogge]], dann kam_mer Desch och fun alle andere Metmaacher ongerschejde.</i>",
        "noarticletext": "<span class=\"plainlinks\">Em Momang es keine Täx op heh dä Sigg. Jangk en de Täxte vun ander Sigge [[Special:Search/{{PAGENAME}}|noh däm Titel söhke]], udder [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} donn en de Logböscher donoh loore], udder [{{FULLURL:{{FULLPAGENAME}}|action=edit}} fang di Sigg aan] ze schrieve, udder jangk zeröck woh De heer kohms. Do hät Dinge Brauser ene Knopp för.</span>",
        "noarticletext-nopermission": "Op dä Sigg es em Momang nix drop.\nDo kanns noh däm Tittel vun heh dä Sigg [[Special:Search/{{PAGENAME}}|em Tex op ander Sigge söhke]],\nudder en dä zopaß <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} Logbööscher nohloore]</span>.",
        "missing-revision": "En Version $1 vun dä Sigg „{{FULLPAGENAME}}“ jidd_et nit.\n\nEsu jät kütt för jewöhnlesch, wam_mer enem övverhollte Lengk ob en Sigg follesch, di zweschedren fottjeschmeße woode es.\nMih doh drövver fengk mer em [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Logbooch vum Sigge Fottschmiiße].",
        "upload-form-label-own-work": "Dat es ming eije Wärk",
        "upload-form-label-infoform-categories": "Saachjroppe",
        "upload-form-label-infoform-date": "Dattum",
-       "upload-form-label-not-own-work-message-local": "Wann De di Dattei nit en de jemeinsamme Sammlong vun Datteule huh lahde kanns un derbei och de Rähjelle {{GRAMMAR:vun|{{ucfirst:{{SITENAME}}}}}} ennhalde, dann maach heh nit wigger, un probehr ene anndere Wähsch.",
-       "upload-form-label-not-own-work-local-local": "Do künnts edd och ens met dä [[Special:Upload|Schtandatt-Sigg zom Huhlahde]] versöhke welle.",
-       "upload-form-label-own-work-message-default": "Esch verschtonn, dadd esch en en jemeinsamme Sammlong huh aam lahde ben un dadd sesch dat met dä Bedengonge un de Lezänzbedengonge heh verdräht.",
-       "upload-form-label-not-own-work-message-default": "Wann De di Dattei nit en de jemeinsamme Sammlong vun Datteije huh lahde kanns un derbei de Rähjelle ennhalde, dann maach heh nit wigger, un probehr ene anndere Wähsch.",
-       "upload-form-label-not-own-work-local-default": "Wann dat noh dä Rähjelle doh jeiht, kanns De och probehre, [[Special:Upload|di Dattei {{GRAMMAR:em|{{ucfirst:{{SITENAME}}}}}} huhzelahde]]",
+       "upload-form-label-not-own-work-message-generic-local": "Wann De di Dattei nit en de jemeinsamme Sammlong vun Datteule huh lahde kanns un derbei och de Rähjelle {{GRAMMAR:vun|{{ucfirst:{{SITENAME}}}}}} ennhalde, dann maach heh nit wigger, un probehr ene anndere Wähsch.",
+       "upload-form-label-not-own-work-local-generic-local": "Do künnts edd och ens met dä [[Special:Upload|Schtandatt-Sigg zom Huhlahde]] versöhke welle.",
+       "upload-form-label-own-work-message-generic-foreign": "Esch verschtonn, dadd esch en en jemeinsamme Sammlong huh aam lahde ben un dadd sesch dat met dä Bedengonge un de Lezänzbedengonge heh verdräht.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Wann De di Dattei nit en de jemeinsamme Sammlong vun Datteije huh lahde kanns un derbei de Rähjelle ennhalde, dann maach heh nit wigger, un probehr ene anndere Wähsch.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Wann dat noh dä Rähjelle doh jeiht, kanns De och probehre, [[Special:Upload|di Dattei {{GRAMMAR:em|{{ucfirst:{{SITENAME}}}}}} huhzelahde]]",
        "backend-fail-stream": "Mer kunnte di Dattei $1 nit övverdraare.",
        "backend-fail-backup": "Mer kunnte kein Sescherongskopih vun dä Dattei $1 maache.",
        "backend-fail-notexists": "En Dattei $1 jidd et nit.",
        "whatlinkshere-prev": "de vörijje {{PLURAL:$1||$1|noll}} zeije",
        "whatlinkshere-next": "de nächste {{PLURAL:$1||$1|noll}} zeije",
        "whatlinkshere-links": "← Links",
-       "whatlinkshere-hideredirs": "de Ömleijdonge $1",
-       "whatlinkshere-hidetrans": "de Oproofe $1",
-       "whatlinkshere-hidelinks": "de nommahle Lengks $1",
-       "whatlinkshere-hideimages": "$1 de Lengks op Datteihje",
+       "whatlinkshere-hideredirs": "De Ömleijdonge verschteijsche",
+       "whatlinkshere-hidetrans": "De Oproofe verschteijsche",
+       "whatlinkshere-hidelinks": "De nommahle Lengks verschteijsche",
+       "whatlinkshere-hideimages": "De Lengks op Datteihje verscheijsche",
        "whatlinkshere-filters": "Ußsööke",
        "whatlinkshere-submit": "Lohß jonn!",
        "autoblockid": "Automattesche Sperr Nommer $1",
index 8277a44..f03072b 100644 (file)
        "actionthrottled": "Hejmara guherandinên hatine hesibandin",
        "actionthrottledtext": "Te ev tişt di demeke gelekî kin de kir. Ji kerema xwe çend xulekan bisekine û carekî din biceribîne.",
        "protectedpagetext": "Ev rûpel ji bo guhertin û karên din ne kirin hatiye parastin.",
-       "viewsourcetext": "Tu dikarî li çavkaniya vê rûpelê binêrî û wê kopî bikî:",
+       "viewsourcetext": "Tu dikarî li çavkaniya vê rûpelê binêrî û wê kopî bikî.",
        "viewyourtext": "Hûn çavkaniyê <strong>guhertinê xwe</strong> yê di vê rûpelê de dikarin bibînin û kopî bikin:",
        "protectedinterface": "Di vê rûpelê de nivîsandin ji bo navrû(interface)yî zimanan yê vê nivîsbariyê ye û ew tê parastin ku vandalîzm li vê derê çênebe.\nBo lêzêdekirin an jî guherandina wergerên bo hemû wîkiyan ji kerema xwe re mehelîkirina Mediawîkiyê [//translatewiki.net/ translatewiki.net]'ê bi kar bîne.",
        "editinginterface": "'''Hişyarî:''' Tu rûpelekê a ku di Wîkîpediya de ji bo sîstemê girîng e,  diguherînî. Guherandinên di vê rûpelê de wê ji aliyê hemû bikarhêneran ve werin dîtin. Ji bo wergerê ji kerema xwe di [//translatewiki.net/wiki/Main_Page?setlang=ku-latn translatewiki.net] de bixebite, projeya MediaWiki.",
        "newpassword": "Şîfreya nû",
        "retypenew": "Şîfreya nû careke din binîvîse",
        "resetpass_submit": "Şîfreyê pêkbîne û têkeve",
-       "changepassword-success": "Guhertine şîfreya te serkeftî bû!",
+       "changepassword-success": "Şîfreya te hate guhertandin!",
        "botpasswords-label-appid": "Navê bot:",
        "botpasswords-label-create": "Çêke",
        "botpasswords-label-update": "Rojane bike",
        "accmailtext": "Şîfreyekê ketober ê ji bo [[User talk:$1|$1]] hatiye çêkirin ji navnîşana $2 re hat şandin. Şîfreya ji bo vê hesabê nû, piştî ko te têket ji beşa <em>[[Special:ChangePassword|şîfreyê biguherîne]]</em> dikare were guhertin.",
        "newarticle": "(Nû)",
        "newarticletext": "Ev rûpel hîn tune. Eger tu bixwazî vê rûpelê çêkî, dest bi nivîsandinê bike û piştre qeyd bike. '''Wêrek be''', biceribîne!<br />\nJi bo alîkariyê binêre: [$1 Alîkarî].<br />\nHeke tu bi şaşîtî hatî, bizîvire rûpela berê.",
-       "anontalkpagetext": "----''Ev rûpela gotûbêjê ye ji bo bikarhênerên nediyarkirî ku hîn hesabekî xwe çênekirine an jî bikarnaînin. Ji ber vê yekê divê em wan bi navnîşana IP ya hejmarî nîşan bikin. Navnîşaneke IP dikare ji aliyê gelek kesan ve were bikaranîn. Heger tu bikarhênerekî nediyarkirî bî û bawerdikî ku nirxandinên bê peywend di der barê te de hatine kirin ji kerema xwe re [[Special:UserLogin/signup|hesabekî xwe veke an jî têkeve]] da ku tu xwe ji tevlîheviyên bi bikarhênerên din re biparêzî.''",
+       "anontalkpagetext": "----''Ev rûpela gotûbêjê ye ji bo bikarhênerên nediyarkirî ku hîn hesabekî xwe çênekirine an jî bikarnaînin. Ji ber vê yekê divê em wan bi navnîşana IP ya hejmarî nîşan bikin. Navnîşaneke IP dikare ji aliyê gelek kesan ve were bikaranîn. Heger tu bikarhênerekî nediyarkirî bî û bawerdikî ku nirxandinên bê peywend di der barê te de hatine kirin ji kerema xwe re [[Special:CreateAccount|hesabekî xwe veke an jî têkeve]] da ku tu xwe ji tevlîheviyên bi bikarhênerên din re biparêzî.''",
        "noarticletext": "Ev rûpel niha vala ye, tu dikarî [[Special:Search/{{PAGENAME}}|Di nav gotarên din de li \"{{PAGENAME}}\" bigerî]] an jî [{{fullurl:{{FULLPAGENAME}}|action=edit}} vê rûpelê biguherînî].",
        "noarticletext-nopermission": "Ev rûpel niha vala ye. \nTu dikarî [[Special:Search/{{PAGENAME}}|Di nav gotarên din de li \"{{PAGENAME}}\" bigere]] \nan <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} vê rûpelê biguherînî]</span>, lê ji bo çêkirina vê rûpelê desthilatdariya te tine ye.",
        "userpage-userdoesnotexist": "Hesabê bikarhêneran \"<nowiki>$1</nowiki>\" nehatiye qeydkirin. Heke tu bixwazî vê rûpelê çêkî/biguherînî ji kerema xwe lê binêre.",
        "right-sendemail": "Ji bikarhênerên di re ename bişîne",
        "grant-editpage": "Rûpelên ku hene biguherîne",
        "grant-editprotected": "Rûpelên parastî bigûherîne",
+       "grant-basic": "Mafên bingehîn",
        "newuserlogpage": "Çêkirina hesabê nû",
        "newuserlogpagetext": "Ev têketina hesabên bikarhêneriyê ye ên ku nû hatine afirandin.",
        "rightslog": "Guhertina mafê bikarhêneriyê",
        "listusers-noresult": "Bikarhêner nehate dîtin.",
        "listusers-blocked": "(hate astengkirin)",
        "activeusers": "Lîsteya bikarhênerên çalak",
+       "activeusers-from": "Li bikarhênerên bi vê dest pê dikin bigere:",
        "activeusers-hidebots": "Bot'an veşêre",
        "activeusers-hidesysops": "Rêveberan veşêre",
        "activeusers-noresult": "Tu bikarhêner nehate dîtin.",
        "whatlinkshere-prev": "{{PLURAL:$1|yê|$1 yên}} berê",
        "whatlinkshere-next": "{{PLURAL:$1|yê|$1 yên}} din",
        "whatlinkshere-links": "← girêdan",
-       "whatlinkshere-hideredirs": "Beralîkirinan $1",
+       "whatlinkshere-hideredirs": "Beralîkirinan veşêre",
        "whatlinkshere-hidetrans": "Naverokan $1",
-       "whatlinkshere-hidelinks": "Girêdanan $1",
-       "whatlinkshere-hideimages": "Girêdanên wêneyan $1",
+       "whatlinkshere-hidelinks": "Girêdanan veşêre",
+       "whatlinkshere-hideimages": "Girêdanên wêneyan veşêre",
        "whatlinkshere-filters": "Parzûn",
        "block": "Bikarhêner asteng bike",
        "unblock": "Astengkirinê rake",
        "tooltip-feed-rss": "RSS feed'ên ji bo rûpelê",
        "tooltip-feed-atom": "Atom feed'ên ji bo vê rûpelê",
        "tooltip-t-contributions": "Lîsteyekî beşdariyên {{GENDER:$1|vê bikarhênerê}} bibîne",
-       "tooltip-t-emailuser": "Jê re name bişîne",
+       "tooltip-t-emailuser": "Jê {{GENDER:$1|vî bikarhênerî}}re peyamê bişîne",
        "tooltip-t-info": "Bêhtir agahî di derbarê vê rûpelê de",
        "tooltip-t-upload": "Dosyeyan bar bike",
        "tooltip-t-specialpages": "Lîsteya hemû rûpelên taybetî",
index d67a8b7..7896792 100644 (file)
        "noname": "Сиз колдонуучунун анык атын көрсөткөн жоксуз.",
        "loginsuccesstitle": "Сиз ийгиликтүү кирдиңиз",
        "loginsuccess": "'''Сиз азыр {{SITENAME}} сайтына \"$1\" болуп кирдиңиз.'''",
-       "nosuchuser": "\"$1\" аттуу колдонуучу катталган эмес.\nКолдонуучун аты регистирди айырмалайт.\nКатасын текшериңиз же [[Special:UserLogin/signup|жаңы эсеп түзүү]]",
+       "nosuchuser": "\"$1\" аттуу колдонуучу катталган эмес.\nКолдонуучун аты регистирди айырмалайт.\nКатасын текшериңиз же [[Special:CreateAccount|жаңы эсеп түзүү]]",
        "nosuchusershort": "\"$1\" аттуу колдонуучу жок.\nЖазылышын текшериңиз.",
        "nouserspecified": "Сиз колдонуучу атын көрсөтүшүңүз керек.",
        "login-userblocked": "Бул колдонуучу бөгөттөлгөн. Системага кирүүгө уруксат жок.",
index f02128e..db42da9 100644 (file)
        "noname": "Nomen usoris ratum non designavisti.",
        "loginsuccesstitle": "Nomen feliciter datum est",
        "loginsuccess": "'''Apud {{grammar:accusative|{{SITENAME}}}} agnosceris nomine \"$1\".'''",
-       "nosuchuser": "Nomen \"$1\" ignoratur.\nIn nominibus interest litterarum aut maiuscularum aut minuscularum!\nQuarum cura rationem habeas aut [[Special:UserLogin/signup|novum nomen tibi da]].",
+       "nosuchuser": "Nomen \"$1\" ignoratur.\nIn nominibus interest litterarum aut maiuscularum aut minuscularum!\nQuarum cura rationem habeas aut [[Special:CreateAccount|novum nomen tibi da]].",
        "nosuchusershort": "Usor \"$1\" non est.\nConfirma orthographiam.",
        "nouserspecified": "Nomen usoris indicare debes.",
        "wrongpassword": "Tessera quam scripsisti non constat. Conare denuo.",
        "accmailtext": "Tessera nova usori [[User talk:$1|$1]] per inscriptionem $2 missa est. Nomine dato suademus ipsam tesseram ''[[Special:ChangePassword|mutare]]''.",
        "newarticle": "(Nova)",
        "newarticletext": "Per nexum progressus es ad paginam quae nondum exsistit.\nNovam paginam si vis creare, in capsam infra praebitam scribe.\n(Vide [$1 paginam auxilii] si plura cognoscere vis.)\nSi hic es propter errorem, solum '''Retrorsum''' in navigatro tuo preme.",
-       "anontalkpagetext": "----\n<em>Haec est pagina disputationis usoris anonymi vel potius loci IP cuiusdam.</em>\nMemento locos IP interdum mutari et ab usoribus vel pluribus adhiberi.\nSi ipse sis usor ignotus et ex improviso invenias querulas aliquas, nomen tibi [[Special:UserLogin/signup|impone]] vel [[Special:UserLogin|nomen tuum da]], ut decetero confusionem effugias!",
+       "anontalkpagetext": "----\n<em>Haec est pagina disputationis usoris anonymi vel potius loci IP cuiusdam.</em>\nMemento locos IP interdum mutari et ab usoribus vel pluribus adhiberi.\nSi ipse sis usor ignotus et ex improviso invenias querulas aliquas, nomen tibi [[Special:CreateAccount|impone]] vel [[Special:UserLogin|nomen tuum da]], ut decetero confusionem effugias!",
        "noarticletext": "Hac in pagina non sunt litterae.\nLicet [[Special:Search/{{PAGENAME}}|hanc rem in aliis paginis quaerere]] vel\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} acta huius paginae inspicere]\nvel [{{fullurl:{{FULLPAGENAME}}|action=edit}} hanc paginam creare]</span>.",
        "noarticletext-nopermission": "Hac in pagina non sunt litterae.\nPotes [[Special:Search/{{PAGENAME}}|hanc rem in aliis paginis quaerere]] aut <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} acta huius paginae videre], sed tibi non licet hanc paginam creare.",
        "userpage-userdoesnotexist": "Usor \"<nowiki>$1</nowiki>\" non est. Visne re vera hanc paginam creare vel recensere?",
index 374650e..f6c6e64 100644 (file)
@@ -34,6 +34,7 @@
        "tog-watchdefault": "Säiten a Fichieren déi ech änneren op meng Iwwerwaachungslëscht derbäisetzen",
        "tog-watchmoves": "Säiten a Fichieren déi ech réckelen automatesch op meng Iwwerwaachungslëscht derbäisetzen",
        "tog-watchdeletion": "Säiten a Fichieren déi ech läschen op meng Iwwerwaachungslëscht derbäisetzen",
+       "tog-watchuploads": "Nei Fichieren déi ech eroplueden op meng Iwwerwaachungslëscht setzen",
        "tog-watchrollback": "Säiten déi ech zréckgesat hunn op meng Iwwerwaachungslëscht derbäisetzen",
        "tog-minordefault": "All Ännerungen automatesch als 'Kleng Ännerungen' markéieren.",
        "tog-previewontop": "Déi ''nach-net gespäichert Versioun'' iwwer der Ännerungsfënster weisen",
        "noname": "Dir hutt kee gëltege Benotzernumm uginn.",
        "loginsuccesstitle": "Ageloggt",
        "loginsuccess": "'''Dir sidd elo als \"$1\" op {{SITENAME}} ugemellt.'''",
-       "nosuchuser": "Et gëtt kee Benotzernumm mam Numm \"$1\".\nBeim Benotzernumm gëtt tëscht groussen a klenge Buschtawen ënnerscheet (casesensitive).\nKuckt w.e.g. op d'Schreifweis richteg ass, oder [[Special:UserLogin/signup|maacht en neie Benotzerkont op]].",
+       "nosuchuser": "Et gëtt kee Benotzernumm mam Numm \"$1\".\nBeim Benotzernumm gëtt tëscht groussen a klenge Buschtawen ënnerscheet (casesensitive).\nKuckt w.e.g. op d'Schreifweis richteg ass, oder [[Special:CreateAccount|maacht en neie Benotzerkont op]].",
        "nosuchusershort": "De Benotzernumm \"$1\" gëtt et net.\nKuckt w.e.g. op d'Schreifweis richteg ass.",
        "nouserspecified": "Gitt w.e.g. e Benotzernumm un.",
        "login-userblocked": "Dëse Benotzer ass gespaart. Aloggen ass net erlaabt.",
        "accmailtext": "En zoufälleg generéiert Passwuert fir [[User talk:$1|$1]] gouf op $2 geschéckt.\n\nEt kann op der ''[[Special:ChangePassword|Passwuert ännere]]'' Säit beim Alogge geännert ginn.",
        "newarticle": "(Nei)",
        "newarticletext": "Dir hutt op e Link vun enger Säit geklickt, déi et nach net gëtt. Fir déi Säit unzeleeën, gitt w.e.g. Ären Text an déi Këscht hei drënner an (kuckt d'[$1 Hëllef Säit] fir méi Informatiounen). Wann Dir duerch een Iertum heihi komm sidd, da klickt einfach op de Knäppchen '''Zréck''' vun Ärem Browser.",
-       "anontalkpagetext": "---- ''Dëst ass d'Diskussiounssäit fir en anonyme Benotzer deen nach kee Kont opgemaach huet oder en net benotzt. Dowéinst musse mir d'IP Adress benotzen, fir de Benotzer z'identifizéieren.\nSou eng IP Adress ka vun e puer Benotzer gedeelt ginn.\nWann Dir en anonyme Benotzer sidd an Dir irrelevant Bemierkunge krut, [[Special:UserLogin/signup|maacht w.e.g. e Kont op]] oder [[Special:UserLogin|loggt Iech an]], fir weider Verwiesselunge mat aneren anonyme Benotzer ze verhënneren.''",
+       "anontalkpagetext": "---- ''Dëst ass d'Diskussiounssäit fir en anonyme Benotzer deen nach kee Kont opgemaach huet oder en net benotzt. Dowéinst musse mir d'IP Adress benotzen, fir de Benotzer z'identifizéieren.\nSou eng IP Adress ka vun e puer Benotzer gedeelt ginn.\nWann Dir en anonyme Benotzer sidd an Dir irrelevant Bemierkunge krut, [[Special:CreateAccount|maacht w.e.g. e Kont op]] oder [[Special:UserLogin|loggt Iech an]], fir weider Verwiesselunge mat aneren anonyme Benotzer ze verhënneren.''",
        "noarticletext": "Dës Säit huet elo keen Text.\nDir kënnt op anere Säiten no [[Special:Search/{{PAGENAME}}|dësem Säitentitel sichen]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} an den entspriechende Logbicher nokucken] oder [{{fullurl:{{FULLPAGENAME}}|action=edit}} dës Säit uleeën]</span>.",
        "noarticletext-nopermission": "Elo ass keen Text op dëser Säit.\nDir kënnt op anere Säiten [[Special:Search/{{PAGENAME}}|no dësem Säitentitel sichen]], oder <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} an de Logbicher sichen]</span>, mä Dir hutt net déi néideg Rechter fir dës Säit unzeleeën.",
        "missing-revision": "D'Versioun #$1 vun der Säit mam Numm \"{{FULLPAGENAME}}\" gëtt et net.\n\nDat geschitt normalerweis wann Dir op e vereelste Link vun enger Versioun vun enger Säit klickt déi geläscht ginn ass.\nDetailer fannt Dir am [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Logbuch vum Läschen].",
        "whatlinkshere-prev": "{{PLURAL:$1|vireg|vireg $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|nächsten|nächst $1}}",
        "whatlinkshere-links": "← Linken",
-       "whatlinkshere-hideredirs": "Viruleedunge $1",
-       "whatlinkshere-hidetrans": "Agebonne Schabloune $1",
-       "whatlinkshere-hidelinks": "Linken $1",
-       "whatlinkshere-hideimages": "Linken op Fichiere $1",
+       "whatlinkshere-hideredirs": "Viruleedunge verstoppen",
+       "whatlinkshere-hidetrans": "Agebonne Schabloune verstoppen",
+       "whatlinkshere-hidelinks": "Linke verstoppen",
+       "whatlinkshere-hideimages": "Linken op Fichiere verstoppen",
        "whatlinkshere-filters": "Filteren",
        "whatlinkshere-submit": "Lass",
        "autoblockid": "Automatesch Spär #$1",
index 11c7397..2dc0222 100644 (file)
        "noname": "Towandise linnya ly'obwa memba eriyinz'okukkirizibwa.",
        "loginsuccesstitle": "Oyingidde",
        "loginsuccess": "'''Kati oyingidde mu {{SITENAME}} nga okozesa erinnya \"$1\".'''",
-       "nosuchuser": "Tewali memba akozesa lya \"$1\".<br />\nEry'obwa memba bw'okyusa obunene bw'ennukuta z'oliwandisa obeera owandise eddala.<br />\nKebera bw'oliwandise, oba [[Special:UserLogin/signup|kolawo akawunti empya]].",
+       "nosuchuser": "Tewali memba akozesa lya \"$1\".<br />\nEry'obwa memba bw'okyusa obunene bw'ennukuta z'oliwandisa obeera owandise eddala.<br />\nKebera bw'oliwandise, oba [[Special:CreateAccount|kolawo akawunti empya]].",
        "nosuchusershort": "Tewali memba akozesa lya \"$1\".<br />\nKebera bw'oliwandise.",
        "nouserspecified": "Kyetaagisa owandikewo erinnya ly'obwa memba.",
        "login-userblocked": "Memba ono agaanidwa. Takkirizibwa kuyingira.",
        "accmailtext": "Ekigambo ekikuumi ekya akawunti empya [[User talk:$1|$1]] kisindikiddwa ku $2.\n\nOkukyusa ekigambo kino memba, ng'ayingidde mu wiki, alage ku lupapula ''[[Special:ChangePassword|change password]]''.",
        "newarticle": "(Lupapula lupya)",
        "newarticletext": "Enyunzi gy'ogenzeko egguka ku lupapula olutannakolebwawo.<br />\nOba gwe oyagala okulukolawo, wandika mu kabokisi akaddako wano.\n(okuyiga ebisingawo, genda ku [$1 lupapula olw'obuyambi]).<br />\nBw'obanga tewagenderedde kutuuka wano, nyigabunyizi eppeesa ery'omu kalambula-neti yo\nerya '''ddayo'''.",
-       "anontalkpagetext": "----''Olupapula olw'emboozi luno lukoledwawo at'eyanjula atakolera mu akawunti ya ku wiki eno.<br />\nKino kitwetaagisizza okumwawulira ku ndagiriro ya IP kwe yayimidde.<br />\nAbantu bangi basobola okukozesa endagiriro eya IP y'emu.<br />\nBw'obanga naawe okola ng'at'eyanjudde nga owulira nti ebimu ku birowozo ebikwolekezedwa wano<br />\ntebiggirawo oba tebikwatagana naawe, [[Special:UserLogin/signup|funa akawunti]] oba [[Special:UserLogin|yingira]] baleme okukutabula n'abalala abakola ng'abat'eyanjudde.",
+       "anontalkpagetext": "----''Olupapula olw'emboozi luno lukoledwawo at'eyanjula atakolera mu akawunti ya ku wiki eno.<br />\nKino kitwetaagisizza okumwawulira ku ndagiriro ya IP kwe yayimidde.<br />\nAbantu bangi basobola okukozesa endagiriro eya IP y'emu.<br />\nBw'obanga naawe okola ng'at'eyanjudde nga owulira nti ebimu ku birowozo ebikwolekezedwa wano<br />\ntebiggirawo oba tebikwatagana naawe, [[Special:CreateAccount|funa akawunti]] oba [[Special:UserLogin|yingira]] baleme okukutabula n'abalala abakola ng'abat'eyanjudde.",
        "noarticletext": "Wano tewali kyawandikidwawo.<br />\nKy'obadde onoonya osobola [[Special:Search/{{PAGENAME}}| okukinoonyeza]] ku mpapula ndala oba oyinza [{{fullurl:{{FULLPAGENAME}}|action=edit}} gwe okukiwandikako] wano.",
        "noarticletext-nopermission": "Wano tewali kyawandikidwawo.<br />\nKy'obadde onoonya osobola [[Special:Search/{{PAGENAME}}| okukinoonyeza]] ku mpapula ndala oba\noyinza <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} okukinoonyeza mu nkalala z'ebifuddeyo ebyekuusana nakyo]</span>.",
        "userpage-userdoesnotexist": "Akawunti y'obwamemba \"<nowiki>$1</nowiki>\" teri mu nkalala za wiki eno.<br />\nSooka okakase oba ddala oyagala okukolawo olupapula olupya luno.",
index 62e24b2..1875dee 100644 (file)
        "noname": "De mos 'n gebroekersnaam opgaeve.",
        "loginsuccesstitle": "Aanmèlde geluk.",
        "loginsuccess": "Doe bis noe es \"$1\" aangemeld bie {{SITENAME}}.",
-       "nosuchuser": "D'r besjteit geine gebroeker mit de naam \"$1\".\nDie seen huidlettegevullig\nControleer dien spelling, of gebroek ongersjtaond formuleer om 'n [[Special:UserLogin/signup|nuuj]] gebroekersprofiel aan te make.",
+       "nosuchuser": "D'r besjteit geine gebroeker mit de naam \"$1\".\nDie seen huidlettegevullig\nControleer dien spelling, of gebroek ongersjtaond formuleer om 'n [[Special:CreateAccount|nuuj]] gebroekersprofiel aan te make.",
        "nosuchusershort": "De gebroeker \"$1\" besjteit neet. Konterleer de sjriefwieze.",
        "nouserspecified": "Doe deens 'ne gebroekersnaam op te gaeve.",
        "login-userblocked": "Deze gebroeker steit geblokkeerd. Aanmèlje is neet toegestange.",
        "accmailtext": "'n Willekäörig wachwaord veur [[User talk:$1|$1]] is nao $2 gesjtuurd.\n\n't Wachwaord veur deze nuje gebroeker kan gewiezig waere via de pagina ''[[Special:ChangePassword|Wachwaord wiezige]]'' nao 't aanmelje.",
        "newarticle": "(Nuuj)",
        "newarticletext": "De höbs 'ne link gevolg nao 'n pagina die nog neet besjteit.\nType in de box hiejónger óm de pagina te beginne (zuug de [$1 helppagina] veur mie infermasie).\nEs te hie per óngelök terech bis gekómme, klik dan op de '''trök'''-knóp van diene browser.",
-       "anontalkpagetext": "----''Dit is de euverlèkpagina veur 'ne anonieme gebroeker dae nog gein account haet aangemaak of dae 't neet gebroek.\nDaoveur gebroeke v'r 't IP-adres óm de gebroeker te identificere.\nDet adres kan waere gedeild door mierdere gebroekers.\nEs te 'ne anonieme gebroeker bis en de höbs 't geveul dat 'r ónrelevante commentare aan dich gerich zeen, kèns te 't bèste [[Special:UserLogin/signup|'n account crëere]] of [[Special:UserLogin|inlogge]] óm toekomstige verwarring mit anger anoniem gebroekers te veurkomme.''",
+       "anontalkpagetext": "----''Dit is de euverlèkpagina veur 'ne anonieme gebroeker dae nog gein account haet aangemaak of dae 't neet gebroek.\nDaoveur gebroeke v'r 't IP-adres óm de gebroeker te identificere.\nDet adres kan waere gedeild door mierdere gebroekers.\nEs te 'ne anonieme gebroeker bis en de höbs 't geveul dat 'r ónrelevante commentare aan dich gerich zeen, kèns te 't bèste [[Special:CreateAccount|'n account crëere]] of [[Special:UserLogin|inlogge]] óm toekomstige verwarring mit anger anoniem gebroekers te veurkomme.''",
        "noarticletext": "Dees pagina bevat gein teks.\nDe kèns [[Special:Search/{{PAGENAME}}|nao deze term zeuke]] in anger pagina's, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logbeuk doorzeuke] of [{{fullurl:{{FULLPAGENAME}}|action=edit}} dees pagina bewirke]</span>.",
        "noarticletext-nopermission": "Dees pagina bevat gein teks.\nDe kans [[Special:Search/{{PAGENAME}}|nao dees term zeuke]] in anger pagina's of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logbeuk doorzeuke]</span>.",
        "userpage-userdoesnotexist": "Doe bewirks 'n gebroekerspagina van 'ne gebroeker dae neet besjteit (gebroeker \"<nowiki>$1</nowiki>\"). Controlere ofs doe dees pagina waal wils aanmake/bewirke.",
index 9ede18f..cd2aa8c 100644 (file)
        "noname": "O nomme d'ûtente o l'è sballiòu.",
        "loginsuccesstitle": "Accesso effettuòu",
        "loginsuccess": "'''O collegamento a-o server de {{SITENAME}} co-o nomme d'ûtente \"$1\" o l'è attivo.'''",
-       "nosuchuser": "No gh'è nisciun utente de nomme \"$1\".\nI nommi utente son senscibbili a-e maiuscole.\nVerifica o nomme inserîo ò [[Special:UserLogin/signup|crea una neuva utensa]].",
+       "nosuchuser": "No gh'è nisciun utente de nomme \"$1\".\nI nommi utente son senscibbili a-e maiuscole.\nVerifica o nomme inserîo ò [[Special:CreateAccount|crea una neuva utensa]].",
        "nosuchusershort": "No gh'è nisciûn ûtente con quello nomme \"$1\". Verificâ o nomme inserîo.",
        "nouserspecified": "Ti g'hæ da specificâ un nomme utente.",
        "login-userblocked": "St'utente o l'è bloccou. Accesso negou.",
        "accmailtext": "Una poula segretta generâ abrettio pe [[User talk:$1|$1]] a l'è stæta mandâ a $2.\n\nSta poula segretta a peu ese cangiâ inta paggina pe ''[[Special:ChangePassword|cangiâ a poula segretta]]'' subbito doppo l'accesso.",
        "newarticle": "(Nêuvo)",
        "newarticletext": "Sto colegaménto o corisponde a 'na pàgina ch'a no l'existe ancon.\n\nSe se vêu creâ a pàgina òua, se pêu comensâ a scrive into spàçio chì sotta.\n(amia e [$1 paggine d'agiûtto] pe ciû informaçioìn).\n\nSe t'ê intròu chì pe sballio,  sciacca '''Inderê''' into navegatô.",
-       "anontalkpagetext": "----\n''Sta chì a l'è a paggina de discuscion de un utente anonnimo, ch'o no l'ha ancon creou un'utensa o comunque o no a doeuvia oua. Pe identificâlo l'è quindi necessaio doeuviâ o nummero do so adresso IP. I adresci IP poeuan però ese condivixi da ciù utenti. Se t'ê un utente anonimo e ti ritegni che i commenti inte sta pagina no se riferiscian a ti, [[Special:UserLogin/signup|crea una noeuva utensa]] o donque [[Special:UserLogin|intra con quella che ti g'hæ za]] pe evitâ de chì avanti de ese confuzo con di atri utenti anonnimi .''",
+       "anontalkpagetext": "----\n''Sta chì a l'è a paggina de discuscion de un utente anonnimo, ch'o no l'ha ancon creou un'utensa o comunque o no a doeuvia oua. Pe identificâlo l'è quindi necessaio doeuviâ o nummero do so adresso IP. I adresci IP poeuan però ese condivixi da ciù utenti. Se t'ê un utente anonimo e ti ritegni che i commenti inte sta pagina no se riferiscian a ti, [[Special:CreateAccount|crea una noeuva utensa]] o donque [[Special:UserLogin|intra con quella che ti g'hæ za]] pe evitâ de chì avanti de ese confuzo con di atri utenti anonnimi .''",
        "noarticletext": "Po-u momento a pagina çercâ a l'è vêua. L'è poscibbile [[Special:Search/{{PAGENAME}}|çercâ 'sto tittolo]] inte âtre pagine do scîto opû [{{fullurl:{{FULLPAGENAME}}|action=edit}} cangiâ a pagina òua].",
        "noarticletext-nopermission": "Òua a pàgina çercâ a l'è vêua. L'è poscìbile [[Special:Search/{{PAGENAME}}|çercâ sto tìtolo]] inte di âtre pàgine do scîto o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} çercâ inti registri corelæ]</span>, ma no ti gh'hæ i outorizzaçioin pe creâ sta paggina.",
        "missing-revision": "La verscion #$1 da paggina \"{{FULLPAGENAME}}\" a no l'esiste.\n\nQuesto succede solitamente se inta stoia ti sciacchi un vegio ingancio a una paggina scassâ.\n\nI dettaggi peuan ese attrovæ into [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de scançellaçioin].",
        "allpagesbadtitle": "O tittolo dæto a-a paggina o non va ben, òpû o conten di prefissi inter-lengua o inter-wiki. O porriæ ascì contegnî un o ciù caratteri che inti tittoli no se peuan deuviâ.",
        "allpages-bad-ns": "\"$1\" o no ghe in {{SITENAME}}.",
        "categories": "Categorîe",
-       "special-categories-sort-count": "ordenâ pe nûmmero",
-       "special-categories-sort-abc": "ordenâ arfabeticamente",
        "linksearch-line": "$1 colegòu a-a pagina $2",
        "listusers-submit": "Fanni vedde",
        "listusers-noresult": "Utente non trovöo.",
index b331307..dbc94bc 100644 (file)
        "noname": ".هؤمة نام کاربةری معتبری دیاری نکردئة",
        "loginsuccesstitle": "إنۆم سیستم هەتن انجۆم گرت",
        "loginsuccess": "هؤمة ایسة هةتیإ نؤم سیستم {{SITENAME}} وۀ نام\"$1\".'",
-       "nosuchuser": "کاربةری وۀ نام «$1» ئة ائرة نیة.\nنام کاربةری وة کةڵنگی و گؤجةری حروف حساسة .\nاملای نام را بررسی کنید، یا [[Special:UserLogin/signup|یک حساب کاربری تازه بسازید]].",
+       "nosuchuser": "کاربةری وۀ نام «$1» ئة ائرة نیة.\nنام کاربةری وة کةڵنگی و گؤجةری حروف حساسة .\nاملای نام را بررسی کنید، یا [[Special:CreateAccount|یک حساب کاربری تازه بسازید]].",
        "nosuchusershort": "هؤیچ کاربةری وة نام ''$1'' ئة ائرة نیة.\nاملایتان را وارسی کنید.",
        "nouserspecified": ".باید یإ گِلة  نام کاربةری دیاری کئین",
        "login-userblocked": ".ئی کاربرە بەسیائە. إنؤم هەتِن سیستم ڕاووآ(مجاز)نیە",
        "accmailtext": "یک گذرواژهٔ تصادفی برای [[User talk:$1|$1]] به $2 فرستاده شد. می‌توان آن را از صفحهٔ ''[[Special:ChangePassword|تغییر گذرواژه]]'' که هنگام ثبت ورود نمایش می‌یابد تغییر داد.",
        "newarticle": "(تازه)",
        "newarticletext": "شما پیوندی را دنبال کرده‌اید و به صفحه‌ای رسیده‌اید که هنوز وجود ندارد.\nبرای ایجاد صفحه، در مستطیل زیر شروع به نوشتن کنید (برای اطلاعات بیشتر به [$1 صفحهٔ راهنما] مراجعه کنید).\nاگر به اشتباه اینجا آمده‌اید، دکمهٔ «بازگشت» مرورگرتان را بزنید.",
-       "anontalkpagetext": "----''این صفحهٔ بحث برای کاربر گمنامی است که هنوز حسابی درست نکرده است یا از آن استفاده نمی‌کند.\nبنا بر این برای شناسایی‌اش مجبوریم از نشانی آی‌پی عددی استفاده کنیم.\nچنین نشانی‌های آی‌پی ممکن است توسط چندین کاربر به شکل مشترک استفاده شود.\nاگر شما کاربر گمنامی هستید و تصور می‌کنید اظهار نظرات نامربوط به شما صورت گرفته است، لطفاً برای پیشگیری از اشتباه گرفته شدن با کاربران گمنام دیگر در آینده [[Special:UserLogin/signup|حسابی ایجاد کنید]] یا [[Special:UserLogin|به سامانه وارد شوید]].''",
+       "anontalkpagetext": "----''این صفحهٔ بحث برای کاربر گمنامی است که هنوز حسابی درست نکرده است یا از آن استفاده نمی‌کند.\nبنا بر این برای شناسایی‌اش مجبوریم از نشانی آی‌پی عددی استفاده کنیم.\nچنین نشانی‌های آی‌پی ممکن است توسط چندین کاربر به شکل مشترک استفاده شود.\nاگر شما کاربر گمنامی هستید و تصور می‌کنید اظهار نظرات نامربوط به شما صورت گرفته است، لطفاً برای پیشگیری از اشتباه گرفته شدن با کاربران گمنام دیگر در آینده [[Special:CreateAccount|حسابی ایجاد کنید]] یا [[Special:UserLogin|به سامانه وارد شوید]].''",
        "noarticletext": "این صفحه هم‌اکنون دارای هیچ متنی نیست.\nشما می‌توانید در صفحه‌های دیگر [[Special:Search/{{PAGENAME}}|عنوان این صفحه را جستجو کنید]]،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} سیاهه‌های مرتبط را جستجو کنید]،\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} این صفحه را ویرایش کنید]</span>.",
        "noarticletext-nopermission": "این صفحه هم‌اکنون متنی ندارد.\nشما می‌توانید در دیگر صفحات [[Special:Search/{{PAGENAME}}|این عنوان را جستجو کنید]]،\nیا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} سیاهه‌های مرتبط را بگردید]</span> ولی شما اجازه ایجاد این صفحه را ندارید.",
        "missing-revision": "ویرایش #$1 از صفحهٔ «{{FULLPAGENAME}}» موجود نیست.\n\nمعمولاً در اثر پیوند به تاریخچهٔ به‌روز نشدهٔ صفحهٔ حذف شده است.\nمی‌توانید جزئیات بیشتر را در [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} سیاههٔ حذف] بیابید.",
        "upload-form-label-own-work": "یة کار ووژمة",
        "upload-form-label-infoform-categories": "ڕزگەل",
        "upload-form-label-infoform-date": "تاریخ",
-       "upload-form-label-own-work-message-local": "تائید می کنم که این پرونده را تحت مجوزها و سیاست‌های {{SITENAME}} بارگذاری می‌کنم.",
-       "upload-form-label-not-own-work-message-local": "اگر امکان بارگذاری پرونده تحت سیاست‌های {{SITENAME}} را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
-       "upload-form-label-not-own-work-local-local": "ممکن بخواهید از [[Special:Upload|پنجرهٔ بارگذاری پیش‌فرض]] استفاده کنید.",
-       "upload-form-label-own-work-message-default": "متوجهم که این پرونده را بر روی مخزن مشترک بارگذاری می‌کنم و تائید می‌کنم که تحت سیاست‌ها و مجوزهای آنجا عمل می‌کنم.",
-       "upload-form-label-not-own-work-message-default": "اگر امکان بارگذاری پرونده تحت سیاست‌ها و مجوزهای مخزن اشتراک‌گذاری را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
-       "upload-form-label-not-own-work-local-default": "در صورتی که نمی‌شود پرونده را تحت سیاست‌ها بارگذاری کنید ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
-       "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 سیاست نحوهٔ استفاده] هستم.",
-       "upload-form-label-not-own-work-message-shared": "اگر مالک حق تکثیر این پرونده نیستید یا قصد بارگذاری تحت مجوز دیگری دارید، از [https://commons.wikimedia.org/wiki/Special:UploadWizard جادوگر بارگذاری ویکی‌انبار] استفاده کنید.",
-       "upload-form-label-not-own-work-local-shared": "در صورتی که سایت امکان بارگذاری پرونده را تحت سیاست‌ها بارگذاری می‌دهد ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
+       "upload-form-label-own-work-message-generic-local": "تائید می کنم که این پرونده را تحت مجوزها و سیاست‌های {{SITENAME}} بارگذاری می‌کنم.",
+       "upload-form-label-not-own-work-message-generic-local": "اگر امکان بارگذاری پرونده تحت سیاست‌های {{SITENAME}} را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
+       "upload-form-label-not-own-work-local-generic-local": "ممکن بخواهید از [[Special:Upload|پنجرهٔ بارگذاری پیش‌فرض]] استفاده کنید.",
+       "upload-form-label-own-work-message-generic-foreign": "متوجهم که این پرونده را بر روی مخزن مشترک بارگذاری می‌کنم و تائید می‌کنم که تحت سیاست‌ها و مجوزهای آنجا عمل می‌کنم.",
+       "upload-form-label-not-own-work-message-generic-foreign": "اگر امکان بارگذاری پرونده تحت سیاست‌ها و مجوزهای مخزن اشتراک‌گذاری را ندارید، لطفاً این پنجره را ببندید و از روش‌های دیگر استفاده کنید.",
+       "upload-form-label-not-own-work-local-generic-foreign": "در صورتی که نمی‌شود پرونده را تحت سیاست‌ها بارگذاری کنید ممکن است بخواهید از [[Special:Upload|پنجرهٔ بارگذاری در {{SITENAME}}]] استفاده کنید.",
        "backend-fail-stream": "نمی‌توان پروندهٔ $1 را ارسال کرد.",
        "backend-fail-backup": "نمی‌توان نسخهٔ پشتیبان برای پروندهٔ $1 ایجاد کرد",
        "backend-fail-notexists": "پروندهٔ $1 وجود ندارد.",
index 554ddad..9aa2d1a 100644 (file)
        "noname": "Te g'hét mìa specificàt en nòm de ütènt bù.",
        "loginsuccesstitle": "Login efetuàt.",
        "loginsuccess": "<strong>Adès te sét cunitìt al server de {{SITENAME}} col nòm ütènt de \"$1\".</strong>",
-       "nosuchuser": "A gh'è nissün druvat cun 'l nom ''$1''. <br />\nI suranomm i henn sensibil a i leter majùscul.<br />\nCuntrola 'l nom che t'hee metüü denter o [[Special:UserLogin/signup|crea un cünt növ]].",
+       "nosuchuser": "A gh'è nissün druvat cun 'l nom ''$1''. <br />\nI suranomm i henn sensibil a i leter majùscul.<br />\nCuntrola 'l nom che t'hee metüü denter o [[Special:CreateAccount|crea un cünt növ]].",
        "nosuchusershort": "Ghe n'è mia d'ütent cun el nom de \"$1\". Ch'el cuntrola se l'ha scrivüü giüst.",
        "nouserspecified": "Te gh'heet da specificà un nom del druvatt.",
        "login-userblocked": "Chèsta ütènsa l'è blocàda. La conesiù l'è mìa cunsentìda.",
        "accmailtext": "La password per [[User talk:$1|$1]] l'è stada mandada a $2. Chèsta password la pöl véser cambiàda per [[Special:ChangePassword|cambià la password]] apéna dòpo che te g'harét fat el log-in.",
        "newarticle": "(Nöf)",
        "newarticletext": "Te seet andaa adree a un ligam a una pagina che la esista gnamò.\nPer creà la pagina, a l'è assee che te tachet a scriv in del box desota (varda la [$1 pagina de vüt] per savèn püssee).\nSe te seet chì per erur, schiscia \"indree\" in sül tò browser.",
-       "anontalkpagetext": "''Questa chí a l'é la pagina da ciciarada d'un druvadur che l'ha nonanmò registraa un cünt, o che 'l le dröva mia.\nPer 'sta reson chí, el pò vess identificaa dumà cunt el sò indirizz nümereg de IP.\n'Stu indirizz IP el pö vess druvaa da püssee d'un druvadur. Se te seet un druvadur anònim e te someja che un quaj messagg al ga nagòt à vidé con de ti, prœuva a [[Special:UserLogin/signup|creà 'n ütènsa nöa]] o [[Special:UserLogin|regìstret con chèla che te g'hét zà]] inscì de minga vess scunfundüü anmò con quaj alter ütent anomim.''",
+       "anontalkpagetext": "''Questa chí a l'é la pagina da ciciarada d'un druvadur che l'ha nonanmò registraa un cünt, o che 'l le dröva mia.\nPer 'sta reson chí, el pò vess identificaa dumà cunt el sò indirizz nümereg de IP.\n'Stu indirizz IP el pö vess druvaa da püssee d'un druvadur. Se te seet un druvadur anònim e te someja che un quaj messagg al ga nagòt à vidé con de ti, prœuva a [[Special:CreateAccount|creà 'n ütènsa nöa]] o [[Special:UserLogin|regìstret con chèla che te g'hét zà]] inscì de minga vess scunfundüü anmò con quaj alter ütent anomim.''",
        "noarticletext": "Per 'l mument quela pagina chì l'è vöja. Te pòdet [[Special:Search/{{PAGENAME}}|cercà quel articul chì]] int i alter paginn, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercà int i register imparentaa], o sedenò [{{fullurl:{{FULLPAGENAME}}|action=edit}} mudifichè 'sta pagina chì adess-adess]</span>.",
        "noarticletext-nopermission": "Per 'l mument quela pagina chì l'è vöja. Te pòdet [[Special:Search/{{PAGENAME}}|cercà quel articul chì]] int i alter paginn, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercà int i register imparentaa], ma te gh'ét nò i permiss per creà-la</span>.",
        "userpage-userdoesnotexist": "L'ütènsa \"$1\" l'è mìa registràda. Arda bé semài che te ölet creàla o mudificàa adilbù.",
        "allpagesprefix": "Varda i pagin ch'i scumenza per:",
        "allpages-hide-redirects": "Scond i bot",
        "categories": "Categurij",
-       "special-categories-sort-count": "mèt en ùrden per nömer",
-       "special-categories-sort-abc": "mèt en ùrden alfabétich",
        "deletedcontributions": "Mudìfiche del ütènt scancelàde",
        "deletedcontributions-title": "Cuntribüziun scancelaa",
        "sp-deletedcontributions-contribs": "mudìfiche",
index 7caf232..915a875 100644 (file)
        "noname": "Jūs nesate nurodęs teisingo naudotojo vardo.",
        "loginsuccesstitle": "Sėkmingai prisijungėte",
        "loginsuccess": "'''Dabar jūs prisijungęs prie {{SITENAME}} kaip „$1“.'''",
-       "nosuchuser": "Nėra jokio naudotojo, turinčio vardą „$1“.\nNaudotojų varduose skiriamos didžiosios ir mažosios raidės.\nPatikrinkite rašybą, arba [[Special:UserLogin/signup|sukurkite naują paskyrą]].",
+       "nosuchuser": "Nėra jokio naudotojo, turinčio vardą „$1“.\nNaudotojų varduose skiriamos didžiosios ir mažosios raidės.\nPatikrinkite rašybą, arba [[Special:CreateAccount|sukurkite naują paskyrą]].",
        "nosuchusershort": "Nėra jokio naudotojo, pavadinto „$1“. Patikrinkite rašybą.",
        "nouserspecified": "Jums reikia nurodyti naudotojo vardą.",
        "login-userblocked": "Šis naudotojas yra užblokuotas. Prisijungti neleidžiama.",
        "accmailtext": "Atsitiktinai sukurtas naudotojo [[User talk:$1|$1]] slaptažodis nusiųstas į $2.\n\nŠios naujos paskyros slaptažodis gali būti pakeistas <em>[[Special:ChangePassword|keisti slaptažodį]]</em> puslapyje beprisijungiant.",
        "newarticle": "(Naujas)",
        "newarticletext": "Jūs patekote į dar neegzistuojantį puslapį.\nNorėdami sukurti puslapį, pradėkite rašyti žemiau esančiame įvedimo lauke\n(plačiau [$1 pagalbos puslapyje]).\nJei patekote čia per klaidą, paprasčiausiai spustelkite  naršyklės mygtuką '''atgal'''.",
-       "anontalkpagetext": "----''Tai yra anoniminio naudotojo, nesusikūrusio arba nenaudojančio paskyros, aptarimų puslapis.\nDėl to naudojamas IP adresas jo identifikavimui.\nŠis IP adresas gali būti dalinamas keliems naudotojams.\nJeigu Jūs esate anoniminis naudotojas ir atrodo, kad komentarai nėra skirti Jums, [[Special:UserLogin/signup|sukurkite paskyrą]] arba [[Special:UserLogin|prisijunkite]], ir nebūsite tapatinamas su kitais anoniminiais naudotojais.''",
+       "anontalkpagetext": "----''Tai yra anoniminio naudotojo, nesusikūrusio arba nenaudojančio paskyros, aptarimų puslapis.\nDėl to naudojamas IP adresas jo identifikavimui.\nŠis IP adresas gali būti dalinamas keliems naudotojams.\nJeigu Jūs esate anoniminis naudotojas ir atrodo, kad komentarai nėra skirti Jums, [[Special:CreateAccount|sukurkite paskyrą]] arba [[Special:UserLogin|prisijunkite]], ir nebūsite tapatinamas su kitais anoniminiais naudotojais.''",
        "noarticletext": "Šiuo metu šiame puslapyje nėra jokio teksto.\nJūs galite [[Special:Search/{{PAGENAME}}|ieškoti šio puslapio pavadinimo]] kituose puslapiuose,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ieškoti susijusių įrašų] arba [{{fullurl:{{FULLPAGENAME}}|action=edit}} sukurti šį puslapį]</span>.",
        "noarticletext-nopermission": "Šiuo metu šiame puslapyje nėra jokio teksto.\nJūs galite [[Special:Search/{{PAGENAME}}|ieškoti šio puslapio pavadinimo]] kituose puslapiuose,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ieškoti susijusių įrašų]</span>, bet jūs neturite teisės sukurti šį puslapį.",
        "missing-revision": "Puslapio peržiūra #$1 pavadinto „{{FULLPAGENAME}}“ neegzistuoja.\n\nTai paprastai atsitinka kai pasenusi nuoroda veda į puslapį, kuris buvo ištrintas.\nInformaciją galima rasti [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].",
        "upload-form-label-own-work": "Tai yra mano darbas",
        "upload-form-label-infoform-categories": "Kategorijos",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Patvirtinu, kad įkeliu šį failą su šiomis naudojimosi sąlygomis ir licencijavimo politika į {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Jeigu Jūs negalite įkelti šio failo su {{SITENAME}}  politika, prašome uždaryti dialogą ir pabandyti kitą metodą.",
-       "upload-form-label-not-own-work-local-local": "Jūs taip pat galite norėti išbandyti [[Special:Upload|numatytąjį įkėlimo puslapį]].",
-       "upload-form-label-own-work-message-default": "Aš suprantu, kad įkeliu šį failą į dalinimosi repozitoriją. Aš patvirtinu, kad tai darau laikydamasis jų paslaugų teikimo sąlygų ir licencijavimo politikos.",
-       "upload-form-label-not-own-work-message-default": "Jeigu negalite įkelti šio failo su dalinimosi repozitorijos politika, prašome uždaryti šį dialogą ir bandyti kitą metodą.",
-       "upload-form-label-not-own-work-local-default": "Jūs taip pat galite norėti išbandyti [[Special:Upload|{{SITENAME}} įkėlimo puslapį]], jeigu šis failas gali būti įkeltas su jų politika.",
-       "upload-form-label-own-work-message-shared": "Aš patvirtinu, kad man priklauso šio failo autorinės teisės ir sutinku neatšaukiamai išleisti šį failą į Wikimedia Commons su [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] licencija, ir aš sutinku su [https://wikimediafoundation.org/wiki/Terms_of_Use paslaugų teikimo sąlygomis].",
-       "upload-form-label-not-own-work-message-shared": "Jeigu Jums nepriklauso šio failo autorinės teisės arba Jūs norite išleisti jį su kitokia licencija, apsvarstykite naudojimą [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons įkėlimo vedlį].",
-       "upload-form-label-not-own-work-local-shared": "Jūs taip pat galite norėti išbandyti [[Special:Upload|{{SITENAME}} įkėlimo puslapį]], jeigu šis puslapis leidžia failų įkėlimą pagal jų politiką.",
+       "upload-form-label-own-work-message-generic-local": "Patvirtinu, kad įkeliu šį failą su šiomis naudojimosi sąlygomis ir licencijavimo politika į {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Jeigu Jūs negalite įkelti šio failo su {{SITENAME}}  politika, prašome uždaryti dialogą ir pabandyti kitą metodą.",
+       "upload-form-label-not-own-work-local-generic-local": "Jūs taip pat galite norėti išbandyti [[Special:Upload|numatytąjį įkėlimo puslapį]].",
+       "upload-form-label-own-work-message-generic-foreign": "Aš suprantu, kad įkeliu šį failą į dalinimosi repozitoriją. Aš patvirtinu, kad tai darau laikydamasis jų paslaugų teikimo sąlygų ir licencijavimo politikos.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Jeigu negalite įkelti šio failo su dalinimosi repozitorijos politika, prašome uždaryti šį dialogą ir bandyti kitą metodą.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Jūs taip pat galite norėti išbandyti [[Special:Upload|{{SITENAME}} įkėlimo puslapį]], jeigu šis failas gali būti įkeltas su jų politika.",
        "backend-fail-stream": "Negali būti apdorotas failas $1.",
        "backend-fail-backup": "Negali būti išsaugotas failas $1.",
        "backend-fail-notexists": "Failas $1 neegzistuoja.",
        "whatlinkshere-links": "← nuorodos",
        "whatlinkshere-hideredirs": "$1 nukreipimus",
        "whatlinkshere-hidetrans": "$1 įtraukimus",
-       "whatlinkshere-hidelinks": "$1 nuorodas",
-       "whatlinkshere-hideimages": "$1 failų nuorodos",
+       "whatlinkshere-hidelinks": "Paslėpti nuorodas",
+       "whatlinkshere-hideimages": "Paslėpti failo nuorodas",
        "whatlinkshere-filters": "Filtrai",
        "whatlinkshere-submit": "Eiti",
        "autoblockid": "Automatinis blokavimas # $1",
index 740b149..a1532c4 100644 (file)
        "noname": "Hmangtu hming dik a ziak lo.",
        "loginsuccesstitle": "Hlawhtling takin i lût tâ e.",
        "loginsuccess": "'''{{SITENAME}}-ah \"$1\" hming puin a i lût ta.'''",
-       "nosuchuser": "Hmingtuhming \"$1\" a awm lo.\nHmangtuhming hi hawrawppui leh të thliar hran a ngai a ni (entirna: Thara leh thara an inang lo).\nI thilziah enfiah rawh, a nih loh pawhin [[Special:UserLogin/signup|siangchan thar siam rawh]].",
+       "nosuchuser": "Hmingtuhming \"$1\" a awm lo.\nHmangtuhming hi hawrawppui leh të thliar hran a ngai a ni (entirna: Thara leh thara an inang lo).\nI thilziah enfiah rawh, a nih loh pawhin [[Special:CreateAccount|siangchan thar siam rawh]].",
        "nosuchusershort": "Hmangtu hming \"$1\" a awm lo.\nI thilziah enfiah rawh.",
        "nouserspecified": "Hmangtuhming i ziah a ngai.",
        "login-userblocked": "Hë hmangtu hi danbeh a ni. Luh phalsak a ni lo.",
index 6c0b718..3eb98b1 100644 (file)
        "site-atom-feed": "$1 Atom padeve",
        "page-rss-feed": "\"$1\" RSS barotne",
        "page-atom-feed": "\"$1\" Atom barotne",
-       "red-link-title": "$1 (lapa neeksistē)",
+       "red-link-title": "$1 (lapa nepastāv)",
        "sort-descending": "Kārtot dilstošā secībā",
        "sort-ascending": "Kārtot augošā secībā",
        "nstab-main": "Raksts",
        "noname": "Tu neesi norādījis derīgu lietotāja vārdu.",
        "loginsuccesstitle": "Ieiešana veiksmīga",
        "loginsuccess": "Tu esi ienācis {{grammar:lokatīvs|{{SITENAME}}}} kā \"$1\".",
-       "nosuchuser": "Šeit nav lietotāja ar vārdu \"$1\". Lietotājvārdi ir reģistrjutīgi (lielie un mazie burti nav viens un tas pats) Pārbaudi, vai pareizi uzrakstīts, vai arī [[Special:UserLogin/signup|izveido jaunu kontu]].",
+       "nosuchuser": "Šeit nav lietotāja ar vārdu \"$1\". Lietotājvārdi ir reģistrjutīgi (lielie un mazie burti nav viens un tas pats) Pārbaudi, vai pareizi uzrakstīts, vai arī [[Special:CreateAccount|izveido jaunu kontu]].",
        "nosuchusershort": "Šeit nav lietotāja ar vārdu \"$1\". Pārbaudi, vai nav drukas kļūda.",
        "nouserspecified": "Tev jānorāda lietotājvārds.",
        "login-userblocked": "Šis dalībnieks ir bloķēts. Pieslēgšanās nav atļauta.",
        "accmailtext": "Nejauši ģenerēta parole lietotājam [[User talk:$1|$1]] tika nosūtīta uz $2.\n\nŠī konta paroli pēc ielogošanās varēs nomainīt ''[[Special:ChangePassword|šeit]]''.",
        "newarticle": "(Jauns raksts)",
        "newarticletext": "Šajā projektā vēl nav lapas ar šādu nosaukumu.\nLai izveidotu lapu, sāc rakstīt teksta logā apakšā (par teksta formatēšanu un sīkākai informācija skatīt [$1 palīdzības lapu]).\nJa tu šeit nonāci kļūdas pēc, vienkārši uzspied <strong>back</strong> pogu pārlūkprogrammā.",
-       "anontalkpagetext": "----''Šī ir diskusiju lapa anonīmam dalībniekam, kurš vēl nav kļuvis par reģistrētu dalībnieku vai arī neizmanto savu dalībnieka vārdu. Tādēļ mums ir jāizmanto skaitliskā IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt vairākiem dalībniekiem.\nJa tu esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:UserLogin/signup|kļūsti par dalībnieku]] vai arī [[Special:UserLogin|izmanto jau izveidotu dalībnieka vārdu]], lai izvairītos no turpmākām neskaidrībām un tu netiktu sajaukts ar citiem anonīmiem dalībniekiem.''",
+       "anontalkpagetext": "----''Šī ir diskusiju lapa anonīmam dalībniekam, kurš vēl nav kļuvis par reģistrētu dalībnieku vai arī neizmanto savu dalībnieka vārdu. Tādēļ mums ir jāizmanto skaitliskā IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt vairākiem dalībniekiem.\nJa tu esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:CreateAccount|kļūsti par dalībnieku]] vai arī [[Special:UserLogin|izmanto jau izveidotu dalībnieka vārdu]], lai izvairītos no turpmākām neskaidrībām un tu netiktu sajaukts ar citiem anonīmiem dalībniekiem.''",
        "noarticletext": "Šajā lapā šobrīd nav nekāda teksta, tu vari [[Special:Search/{{PAGENAME}}|meklēt citās lapās pēc šīs lapas nosaukuma]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītos reģistru ierakstos] vai arī [{{fullurl:{{FULLPAGENAME}}|action=edit}} sākt rediģēt šo lapu]</span>.",
        "noarticletext-nopermission": "Šajā lapā pašlaik nav nekāda teksta.\nTu vari [[Special:Search/{{PAGENAME}}|meklēt šīs lapas nosaukumu]] citās lapās,\nvai <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītus reģistru ierakstus]</span>, bet jums nav atļauja izveidot si lapu.",
        "userpage-userdoesnotexist": "Lietotājs \"<nowiki>$1</nowiki>\" nav reģistrēts.\nLūdzu, pārliecinies vai vēlies izveidot/izmainīt šo lapu.",
        "special-characters-group-sinhala": "Singāļu",
        "special-characters-group-gujarati": "Gudžarati",
        "mw-widgets-dateinput-no-date": "Nav izvēlēts datums",
+       "mw-widgets-titleinput-description-new-page": "lapa vēl nepastāv",
        "api-error-blacklisted": "Lūdzu, izvēlieties citu, aprakstošu nosaukumu!"
 }
index 1272972..eee6234 100644 (file)
        "noname": "缺簿名,或不格也。",
        "loginsuccesstitle": "登簿成矣",
        "loginsuccess": "'''$1'''登{{SITENAME}}矣",
-       "nosuchuser": "查無此人。惠請更名,查大小寫或[[Special:UserLogin/signup|立此簿]]。",
+       "nosuchuser": "查無此人。惠請更名,查大小寫或[[Special:CreateAccount|立此簿]]。",
        "nosuchusershort": "查無\"$1\",惠核之。",
        "nouserspecified": "簿名必須",
        "login-userblocked": "此簿見錮矣。是之登未見許可。",
        "accmailtext": "\"$1\"之新符節至$2矣\n\n此符節可於登簿後改",
        "newarticle": "撰",
        "newarticletext": "此頁尚缺。欲補,撰於下,有惑見[$1 助]。\n誤入者,返前即可。",
-       "anontalkpagetext": "----''此匿論也,為未簿或不簿者設,IP俱錄以辨人焉。然IP不獨,恐生亂象,不喜惠[[Special:UserLogin/signup|增]][[Special:UserLogin|登簿]]遠之。",
+       "anontalkpagetext": "----''此匿論也,為未簿或不簿者設,IP俱錄以辨人焉。然IP不獨,恐生亂象,不喜惠[[Special:CreateAccount|增]][[Special:UserLogin|登簿]]遠之。",
        "noarticletext": "查無此文。[[Special:Search/{{PAGENAME}}|尋題]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 尋誌],\n或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 纂頁]</span>。",
        "noarticletext-nopermission": "查無此文。[[Special:Search/{{PAGENAME}}|尋題]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 尋誌],\n或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 纂頁]</span>。",
        "userpage-userdoesnotexist": "「<nowiki>$1</nowiki>」之簿未增也。請建纂本頁前查之。",
index ddae430..c968a69 100644 (file)
        "noname": "अहाँ वैध प्रयोक्तानाम नै देने छी।",
        "loginsuccesstitle": "सम्प्रवेश भएल",
        "loginsuccess": "'''अहाँ सम्प्रवेश केलहुँ {{SITENAME}} \"$1\".'''क रूपमे।",
-       "nosuchuser": "\"$1\" नामसँ कोनो प्रयोक्ता नै अछि।\nप्रयोक्तानाम ब्रह्मक्षर-लघ्वक्षर भेद युक्त अछि।\nअपन ह्रिजै जाँची, वा [[Special:UserLogin/signup|नव खाता बनाबी]] ।",
+       "nosuchuser": "\"$1\" नामसँ कोनो प्रयोक्ता नै अछि।\nप्रयोक्तानाम ब्रह्मक्षर-लघ्वक्षर भेद युक्त अछि।\nअपन ह्रिजै जाँची, वा [[Special:CreateAccount|नव खाता बनाबी]] ।",
        "nosuchusershort": "\"$1\" नाम्ना कोनो प्रयोक्ता नै अछि।\nअपन हिजए सुधारी।",
        "nouserspecified": "अहाँक एकटा प्रयोक्तानाम देबऽ पडत।",
        "login-userblocked": "ई प्रयोक्ता प्रतिबन्धित अछि। सम्प्रवेशक अधिकार नै अछि।",
        "accmailtext": "एकटा बिना क्रमबला निर्माण पद्धतिसँ कूटशब्दक निर्माण [[User talk:$1|$1]] लेल $2 केँ पठाएल गेल अछि।\n\nऐ खाताक कूटशब्द बदलल जा सकैए ''[[Special:ChangePassword|कूटशब्द बदलू]]'' पन्नापर सम्प्रवेश केलाक बाद।",
        "newarticle": "(नव)",
        "newarticletext": "अहाँ एहेन पन्नाक लिंकक अनुसरण कऽ आएल छी जे पन्ना अखन बनले नै अछि।\nपन्ना बनेबाक लेल नीचाँक बक्शामे टाइप केनाइ शुरू करू (देखू [$1  सहायता पन्ना] विषेष जानकारी लेल)।",
-       "anontalkpagetext": "----'' ई एकटा अनाम प्रयोक्ताक लेल वार्ता पन्ना छी जे अखन धरि अपन खाता नै खोलने छथि, वा जे एकर प्रयोग नै करै छथि।\nहमरा सभकेँ तइ लेल अंकीय अनिकेतक प्रयोग हुनका देखार करबा लेल करऽ पड़ि रहल अछि।\nऐ तरहक अनिकेत अनेक प्रयोक्ता द्वारा साझी कएल जा सकैत अछि।\nजँ अहाँ अनाम प्रयोक्ता छी आ बुझै छी जे बिना मतलबक टिप्पणी अहाँ दिस देल जा रहल अछि, तँ कृपा कऽ [[Special:UserLogin/signup|एकटा खाता खोलू]] वा [[Special:UserLogin|सम्प्रवेश]] जइसँ भविष्यमे आन अनाम प्रयोक्तासँ अहाँकेँ दिक्कत नै हो।''",
+       "anontalkpagetext": "----'' ई एकटा अनाम प्रयोक्ताक लेल वार्ता पन्ना छी जे अखन धरि अपन खाता नै खोलने छथि, वा जे एकर प्रयोग नै करै छथि।\nहमरा सभकेँ तइ लेल अंकीय अनिकेतक प्रयोग हुनका देखार करबा लेल करऽ पड़ि रहल अछि।\nऐ तरहक अनिकेत अनेक प्रयोक्ता द्वारा साझी कएल जा सकैत अछि।\nजँ अहाँ अनाम प्रयोक्ता छी आ बुझै छी जे बिना मतलबक टिप्पणी अहाँ दिस देल जा रहल अछि, तँ कृपा कऽ [[Special:CreateAccount|एकटा खाता खोलू]] वा [[Special:UserLogin|सम्प्रवेश]] जइसँ भविष्यमे आन अनाम प्रयोक्तासँ अहाँकेँ दिक्कत नै हो।''",
        "noarticletext": "अखन ई पन्नापर कोनो पाठ नै अछि।\nअहाँ [[Special:Search/{{PAGENAME}}|ई पन्नाक शीर्षक ताकी]] आन पन्नापर,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} सम्बन्धी वृत्तलेख ताकी],\nआकि [{{fullurl:{{FULLPAGENAME}}|action=edit}} ई पन्नाक निर्माण करी]</span>।",
        "noarticletext-nopermission": "अखन ऐ पन्नापर कोनो पाठ नै अछि।\nअहाँ [[Special:Search/{{PAGENAME}}|ऐ पन्ना शीर्षक लेल ताकू]]",
        "missing-revision": "\"{{FULLPAGENAME}}\" पन्ना के अवतरण #$1 मौजूद नई अछि ।\n\nओन त ई एक हट्याल गेल पन्ना के पुराना कडी पे क्लिक करबाक कारण से होएत अछि।\nअधिक जानकारी के लेल आहाँ [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटाबै के लॉग] देख सकै अछि।",
index f6f6ef7..b2eed98 100644 (file)
        "noname": "Jeneng panganggo sing Rika lebokna ora sah.",
        "loginsuccesstitle": "Sukses mlebu log",
        "loginsuccess": "'''Rika sekiye mlebu log nang {{SITENAME}} nganggo jeneng \"$1\".'''",
-       "nosuchuser": "Ora ana panganggo sing jenenge \"$1\".\nJeneng panganggo kuwe mbedakna kapitalisasi.\nPriksa maning ejaane Rika, utawa [[Special:UserLogin/signup|gawe akun anyar]]",
+       "nosuchuser": "Ora ana panganggo sing jenenge \"$1\".\nJeneng panganggo kuwe mbedakna kapitalisasi.\nPriksa maning ejaane Rika, utawa [[Special:CreateAccount|gawe akun anyar]]",
        "nosuchusershort": "Ora ana panganggo sing jenenge \"$1\".\nJajal dipriksa maning ejaane Rika.",
        "nouserspecified": "Rika kudu nglebokna jeneng panganggo.",
        "login-userblocked": "Panganggo kiye diblok. Ora olih mlebu log.",
index bd32f5a..0031621 100644 (file)
        "noname": "Тон изеть пута кемокстаф тиить лемоц.",
        "loginsuccesstitle": "Сувамась ётась лац",
        "loginsuccess": "'''Тон сувать {{SITENAME}}-с кода \"$1\".'''",
-       "nosuchuser": "Тиись \"$1\" лемса аш.\nВанк, улема, тон сёрмадыть лемть аф лац.\nИлякс тондейть эряви [[Special:UserLogin/signup|сёрматфтомс одукс]].",
+       "nosuchuser": "Тиись \"$1\" лемса аш.\nВанк, улема, тон сёрмадыть лемть аф лац.\nИлякс тондейть эряви [[Special:CreateAccount|сёрматфтомс одукс]].",
        "nosuchusershort": "Тиись \"$1\" лемса аш. Ванк, улема, тон сёрмадыть лемть аф лац.",
        "nouserspecified": "Тиить лемсь эряви.",
        "login-userblocked": "Тиись перяф. Сувама кардаф.",
        "accmailtext": "Апак арьсек тиф [[User talk:$1|$1]]нь сувама валоц кучфоль $2с.\n\nТя од сёрматфтомать сувама валть ули кода полафтомс ''[[Special:ChangePassword|сувама валонь полафтома]]'' лопаса сувамда меле.",
        "newarticle": "(Од)",
        "newarticletext": "Тон сать лопас кона нинге изь тие.\nСёрматк паксяв алу тя лопать тиеманкса\n(ванк [$1 лезкс лопа] лездама информациенкса).\nСаньдярять тяза эльбядезь, люпштак пунять '''меки''' тонь интернет полатксонь вальмасонза.",
-       "anontalkpagetext": "----''Тя корхтама лопать тизе лемфтома тиись кона нинге аф сёрматфтф эли кона эсь сёрматфтомас аф кунци. Тяса минь нолдамс сонь лувомтяшкснень IP адресонц тиить содафтоманкса.\nТя IP адресть вельде ули кода сашендомс иля тиихненди.\nУляндяряйхть иля тиинь мяльполатксонза конатнень аф видекста лувозь тоннекс, тондейть пароль сай пингста [[Special:UserLogin/signup|сёрматфтомс/сувамс]] иля лемфтома тиихнень марс аф шовореманкса.''",
+       "anontalkpagetext": "----''Тя корхтама лопать тизе лемфтома тиись кона нинге аф сёрматфтф эли кона эсь сёрматфтомас аф кунци. Тяса минь нолдамс сонь лувомтяшкснень IP адресонц тиить содафтоманкса.\nТя IP адресть вельде ули кода сашендомс иля тиихненди.\nУляндяряйхть иля тиинь мяльполатксонза конатнень аф видекста лувозь тоннекс, тондейть пароль сай пингста [[Special:CreateAccount|сёрматфтомс/сувамс]] иля лемфтома тиихнень марс аф шовореманкса.''",
        "noarticletext": "Тяни аш текст тя лопаса.\nТондейть ули кода [[Special:Search/{{PAGENAME}}|вешендемс тя лопать коняксонц]] иля лопава,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} вешендемс малады лувомава],\nэли [{{fullurl:{{FULLPAGENAME}}|action=edit}} петнемс тя лопать]</span>.",
        "noarticletext-nopermission": "Тяни аш текст тя лопаса.\nТондейть ули кода [[Special:Search/{{PAGENAME}}|вешендемс тя лопать коняксонц]] иля лопава,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} вешендемс малады лувомава]</span>, аньцек тонь аш мярьговомаце тя лопать ушедомс.",
        "userpage-userdoesnotexist": "Сёрматфтомась «<nowiki>$1</nowiki>» лемса аш. Арьсек лацкаста, афкукс тонь улендяряй мяльце тиемс эли полафтомс тя лопать.",
index f3c2f52..b1d70c2 100644 (file)
        "accmailtext": "Nalefa tany amin'i $2 ny tenimiafina kisendra ho an'ny kaonty [[User talk:$1|$1]]! Azo ovaina eo amin'i ''[[Special:ChangePassword|Manova tenimiafina]]'' izany amin'ny alalan'ny fidirana.",
        "newarticle": "(Vaovao)",
        "newarticletext": "Mbola tsy misy ity takelaka ity koa azonao atao ny mamorona azy eto ambany. Jereo ny [$1 Fanoroana] raha misy fanazavana ilainao.\n\nRaha toa moa ka tsy nieritreritra ny hamorona ity takelaka ity ianao dia miverena etsy amin'ny fandraisana.",
-       "anontalkpagetext": "----<i>Ity pejy ity dia pejin-dresak'olona tsy nanokatra na tsy nampiasa ny kaontiny.\nNoho izany dia ilainay ny mampiasa ny adiresy IP-ny hanondroana azy. Mety zarazarain'olona maro ny adiresy IP iray. Raha mpikambana tsy nisoratra anarana ianao, ka raha mahita resaka ts ho anao, azonao atao ny [[Special:UserLogin/signup|manokatra kaonty]], na [[Special:UserLogin|miditra]] mba tsy ho voafangarao amin'ny mpikambana hafa tsy nisoratra anarana.</i>",
+       "anontalkpagetext": "----<i>Ity pejy ity dia pejin-dresak'olona tsy nanokatra na tsy nampiasa ny kaontiny.\nNoho izany dia ilainay ny mampiasa ny adiresy IP-ny hanondroana azy. Mety zarazarain'olona maro ny adiresy IP iray. Raha mpikambana tsy nisoratra anarana ianao, ka raha mahita resaka ts ho anao, azonao atao ny [[Special:CreateAccount|manokatra kaonty]], na [[Special:UserLogin|miditra]] mba tsy ho voafangarao amin'ny mpikambana hafa tsy nisoratra anarana.</i>",
        "noarticletext": "'''Tsy mbola nisy namorona io lahatsoratra io.\nAzonao atao ny [[Special:Search/{{PAGENAME}}|Tadiavo ny momba ny {{PAGENAME}}]].'''\n* '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} Na forony eto ny lahatsoratra momba ny {{PAGENAME}}]'''.",
        "noarticletext-nopermission": "Mbola tsy misy lahatsoratra ao amin'io pejy io.\n\nAzonao atao ny [[Special:Search/{{PAGENAME}}|mikaroka ity lohateny ity]] eny amin'ny pejy hafa na <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mitady ao amin'ny laogy misy fifandraisana]</span>, fa tsy azonao atao ny mamorona ity pejy ity.",
        "missing-revision": "Tsy misy ny santiôna #$1 ny pejy \"{{FULLPAGENAME}}\".\n\nMitranga izany rehefa manaraka rohin-tantara tola mankany amina pejy voafafa. Ahitana fampahalalana fanampiny ny  [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} laogim-pamafana].",
index 5df7821..c1006b7 100644 (file)
        "userlogin-resetpassword-link": "Шолыпмутым монденат?",
        "createaccountmail": "Кӱчык жаплан чокым ыштыме шолыпмутым мылам e-mail дене колташ",
        "createacct-benefit-heading": "{{SITENAME}} тендан гаяк еҥ-влак дене ыштен шындалтын.",
-       "nosuchuser": "\"$1\" лӱман пайдаланыше уке.\nПайдаланышын лӱмыштӧ йӱкпале-влакын кугытшо тӱрыс лийшаш.\nЛӱмым чын возымым терге але [[Special:UserLogin/signup|регистрацийым эрте]].",
+       "nosuchuser": "\"$1\" лӱман пайдаланыше уке.\nПайдаланышын лӱмыштӧ йӱкпале-влакын кугытшо тӱрыс лийшаш.\nЛӱмым чын возымым терге але [[Special:CreateAccount|регистрацийым эрте]].",
        "nosuchusershort": "\"$1\" лӱман пайдаланыше уке.\nЛӱмым чын возымым терге.",
        "nouserspecified": "Тылат пайдаланышын лӱмжым пуртыман.",
        "wrongpassword": "Тый йоҥылыш шолыпмутым пуртенат.\nЭше ик гана ыштен ончо.",
index 8d4af20..38a19d1 100644 (file)
        "noname": "Namo pangguno nan Sanak masuakan indak sah.",
        "loginsuccesstitle": "Bahasil masuak log",
        "loginsuccess": "'''Sanak kini lah masuak log di {{SITENAME}} sabagai \"$1\".'''",
-       "nosuchuser": "Indak ado pangguno jo namo \"$1\".\nNamo pangguno mambedoan kapitalisasi.\nPariso baliak ejaan Sanak, atau [[Special:UserLogin/signup|buek akun baru]].",
+       "nosuchuser": "Indak ado pangguno jo namo \"$1\".\nNamo pangguno mambedoan kapitalisasi.\nPariso baliak ejaan Sanak, atau [[Special:CreateAccount|buek akun baru]].",
        "nosuchusershort": "Indak ado pangguno jo namo \"$1\".\nCubo pariso baliak ejaan Sanak.",
        "nouserspecified": "Sanak harus mamasuakan namo pangguno.",
        "login-userblocked": "Pangguno ko kanai sakek. Indak diizinan untuak masuak log.",
        "accmailtext": "Sabuah kato sandi acak untuak [[User talk:$1|$1]] alah dibuek dan dikiriman ka $2.\n\nKato sandi untuak akun baharu iko dapek diubah di laman ''[[Special:ChangePassword|pangubahan kato sandi]]'' satalah masuak log.",
        "newarticle": "(Baru)",
        "newarticletext": "Laman nan Sanak cari alun ado.\nUntuak mambuek laman tu, mulailah jo manulih dalam kotak di bawah (caliak [$1 laman bantuan] untuak informasi labiah lanjuik).\nJikok Sanak indak sangajo sampai ka laman ko, klik tombol '''back''' pado paramban web Sanak.",
-       "anontalkpagetext": "----''Iko adolah laman rundiang saurang pangguno anonim nan alun mambuek akun atau indak manggunoannyo.\nJadi, kami tapaso mamakai alamat IP nan takaik untuak mangenalinyo.\nJikok Sanak adolah pangguno anonim dan maraso mandapek komentar nan indak lamak nan ditujuan langsung kapado Sanak, cubolah [[Special:UserLogin/signup|mambuek akun]] atau [[Special:UserLogin|masuak log]] guno manghindari karancuan jo pangguno anonim lainnyo.''",
+       "anontalkpagetext": "----''Iko adolah laman rundiang saurang pangguno anonim nan alun mambuek akun atau indak manggunoannyo.\nJadi, kami tapaso mamakai alamat IP nan takaik untuak mangenalinyo.\nJikok Sanak adolah pangguno anonim dan maraso mandapek komentar nan indak lamak nan ditujuan langsung kapado Sanak, cubolah [[Special:CreateAccount|mambuek akun]] atau [[Special:UserLogin|masuak log]] guno manghindari karancuan jo pangguno anonim lainnyo.''",
        "noarticletext": "Kini ko indak ado teks di laman ko.\nSanak dapek [[Special:Search/{{PAGENAME}}|malakuan pancarian untuak judul laman ko]] di laman-laman lain, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mancari log takaik], atau [{{fullurl:{{FULLPAGENAME}}|action=edit}} manyuntiang laman iko]</span>.",
        "noarticletext-nopermission": "Kini ko indak ado teks dalam laman ko.\nSanak dapek [[Special:Search/{{PAGENAME}}|malakukan pancarian untuak judul laman ko]] di laman lain, atau <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mancahari log takaik] </span>, tapi Sanak indak punyo izin untuak mambuek laman ko.",
        "missing-revision": "Revisi $1 di laman nan banamo \"{{FULLPAGENAME}}\" ko indak ado.\n\nHal iko biasonyo disababkan dek pautan sijarah nan alah kadaluarsa ka laman nan alah diapuih.\nRinciannyo dapek dicaliak di [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} log pangapuihan].",
index 4093966..accb759 100644 (file)
        "noname": "Внесовте погрешно корисничко име.",
        "loginsuccesstitle": "Најавени сте",
        "loginsuccess": "Сега сте најавени на {{SITENAME}} како „$1“.",
-       "nosuchuser": "Нема корисник со името „$1“.\nКорисничките имиња разликуваат мали и големи букви.\nПроверете да не сте направиле грешка во пишувањето, или [[Special:UserLogin/signup|создајте нова корисничка сметка]].",
+       "nosuchuser": "Нема корисник со името „$1“.\nКорисничките имиња разликуваат мали и големи букви.\nПроверете да не сте направиле грешка во пишувањето, или [[Special:CreateAccount|создајте нова корисничка сметка]].",
        "nosuchusershort": "Нема корисник со името „$1“.\nПроверете дали правилно сте напишале.",
        "nouserspecified": "Мора да наведете корисничко име.",
        "login-userblocked": "Овој корисник е блокиран. Најавувањето не е дозволено.",
        "accmailtext": "На $2 е спратена е случајно создадена лозинка за [[User talk:$1|$1]] е испратена. Истата може да се смени на страницата ''[[Special:ChangePassword|Менување на лозинка]]'' откако ќе се најавите.",
        "newarticle": "(нова)",
        "newarticletext": "Проследивте врска до страница која не постои.\nЗа да ја создадете страницата, напишете текст во полето подолу ([$1 помош]). Ако сте овде по грешка, само систнете на копчето '''назад''' во вашиот прелистувач.",
-       "anontalkpagetext": "----''Ова е страница за разговор со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:UserLogin/signup|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.''",
+       "anontalkpagetext": "----''Ова е страница за разговор со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:CreateAccount|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.''",
        "noarticletext": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии,\nда ги <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате дневниците],\nили да [{{fullurl:{{FULLPAGENAME}}|action=edit}} ја создадете]</span>.",
        "noarticletext-nopermission": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии или пак да <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате поврзаните дневници]</span>, но немате дозвола да ја создадете страницата.",
        "missing-revision": "Не ја пронајдов преработката бр. $1 на страницата со наслов „{{FULLPAGENAME}}“.\n\nОва обично се должи на застарена врска за разлики што води кон избришана страница.\nПовеќе подробности ќе најдете во [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневникот на бришења].",
        "upload-form-label-own-work": "Ова е мое дело",
        "upload-form-label-infoform-categories": "Категории",
        "upload-form-label-infoform-date": "Датум",
-       "upload-form-label-own-work-message-local": "Потврдувам дека податотекава ја подигам во согласност со уловите на користење и правилата за лиценцирање на {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Ако не сте во можност да ја подигнете податотекава согласно правилата на {{SITENAME}}. Затворете го дијалогов и обидете се на друг начин.",
-       "upload-form-label-not-own-work-local-local": "Можете да ја пробате и [[Special:Upload|стандардната страница за подигање]].",
-       "upload-form-label-own-work-message-default": "Разбирам дека ја подигам податотекава на заедничко складиште. Потврдувам дека со тоа ги почитувам тамошните услови на користење и лиценцните правила.",
-       "upload-form-label-not-own-work-message-default": "Ако не сте во можност да ја подигнете податотекава во склад со правилата на заедничкото складиште, би ве замолиле да го затворите дијалогов и да пробате на друг начин.",
-       "upload-form-label-not-own-work-local-default": "Можете да се обидете и на [[Special:Upload|страницата за подигање на {{SITENAME}}]], доколку податотеката може да се подигне под тамошните правила.",
-       "upload-form-label-own-work-message-shared": "Сведочам дека јас сум имател на авторските права на оваа податотека, дека се согласувам дека неотповикливо ја објавувам на Ризницата под лиценцата [https://creativecommons.org/licenses/by-sa/4.0/deed.mk Криејтив комонс Наведи извор-Сподели под исти услови 4.0] и дека се согласувам да се придржувам до [https://wikimediafoundation.org/wiki/Terms_of_Use/mk Условите на употреба].",
-       "upload-form-label-not-own-work-message-shared": "Доколку вие не сте имател на авторските права на податотекава, или пак сакате да ја објавите под поинаква лиценца, веројатно ќе треба да се послужите со [https://commons.wikimedia.org/wiki/Special:UploadWizard?uselang=mk Помошникот за подигање].",
-       "upload-form-label-not-own-work-local-shared": "Можете да се обидете и на [[Special:Upload|страницата за подигање на {{SITENAME}}]], доколку податотеката може да се подигне под тамошните правила.",
+       "upload-form-label-own-work-message-generic-local": "Потврдувам дека податотекава ја подигам во согласност со уловите на користење и правилата за лиценцирање на {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Ако не сте во можност да ја подигнете податотекава согласно правилата на {{SITENAME}}. Затворете го дијалогов и обидете се на друг начин.",
+       "upload-form-label-not-own-work-local-generic-local": "Можете да ја пробате и [[Special:Upload|стандардната страница за подигање]].",
+       "upload-form-label-own-work-message-generic-foreign": "Разбирам дека ја подигам податотекава на заедничко складиште. Потврдувам дека со тоа ги почитувам тамошните услови на користење и лиценцните правила.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Ако не сте во можност да ја подигнете податотекава во склад со правилата на заедничкото складиште, би ве замолиле да го затворите дијалогов и да пробате на друг начин.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Можете да се обидете и на [[Special:Upload|страницата за подигање на {{SITENAME}}]], доколку податотеката може да се подигне под тамошните правила.",
        "backend-fail-stream": "Не можев да ја емитувам податотеката $1.",
        "backend-fail-backup": "Не можев да направам резерва на податотеката $1.",
        "backend-fail-notexists": "Податотеката $1 не постои.",
        "whatlinkshere-prev": "{{PLURAL:$1|претходна|претходни $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|следна|следни $1}}",
        "whatlinkshere-links": "← врски",
-       "whatlinkshere-hideredirs": "$1 пренасочувања",
-       "whatlinkshere-hidetrans": "$1 превметнувања",
-       "whatlinkshere-hidelinks": "$1 врски",
+       "whatlinkshere-hideredirs": "Скриј пренасочувања",
+       "whatlinkshere-hidetrans": "Скриј превметнувања",
+       "whatlinkshere-hidelinks": "Скриј врски",
        "whatlinkshere-hideimages": "$1 врски кон податотека",
        "whatlinkshere-filters": "Филтри",
        "whatlinkshere-submit": "Дај",
index ac0d9d2..0ec3c1b 100644 (file)
        "noname": "താങ്കൾ സാധുവായ ഉപയോക്തൃനാമം സൂചിപ്പിച്ചിട്ടില്ല.",
        "loginsuccesstitle": "സ്വാഗതം! \nതാങ്കൾ വിജയകരമായി പ്രവേശിച്ചിരിക്കുന്നു.",
        "loginsuccess": "'''{{SITENAME}} സംരംഭത്തിൽ \"$1\" എന്ന പേരിൽ താങ്കൾ ലോഗിൻ ചെയ്തിരിക്കുന്നു.'''",
-       "nosuchuser": "ഇതുവരെ \"$1\" എന്ന പേരിൽ ആരും അംഗത്വമെടുത്തിട്ടില്ല.\nദയവായി അക്ഷരപ്പിശകുകൾ പരിശോധിക്കുക, അല്ലെങ്കിൽ\nപുതിയ [[Special:UserLogin/signup|അംഗത്വമെടുക്കുക]].",
+       "nosuchuser": "ഇതുവരെ \"$1\" എന്ന പേരിൽ ആരും അംഗത്വമെടുത്തിട്ടില്ല.\nദയവായി അക്ഷരപ്പിശകുകൾ പരിശോധിക്കുക, അല്ലെങ്കിൽ\nപുതിയ [[Special:CreateAccount|അംഗത്വമെടുക്കുക]].",
        "nosuchusershort": "\"$1\" എന്ന പേരിൽ ഒരു ഉപയോക്താവ് ഇല്ല. അക്ഷരങ്ങൾ ഒന്നു കൂടി പരിശോധിക്കുക.",
        "nouserspecified": "ഉപയോക്തൃനാമം നിർബന്ധമായും ചേർക്കണം.",
        "login-userblocked": "ഈ ഉപയോക്താവ് തടയപ്പെട്ടിരിക്കുന്നു. പ്രവേശനം അനുവദിക്കുന്നില്ല.",
        "accmailtext": "[[User talk:$1|$1]] എന്ന ഉപയോക്താവിനുള്ള ക്രമരഹിതമായി നിർമ്മിച്ച രഹസ്യവാക്ക് $2 എന്ന വിലാസത്തിലേക്ക് അയച്ചിട്ടുണ്ട്. പ്രവേശിച്ചതിനു ശേഷം ഇത് ''[[Special:ChangePassword|രഹസ്യവാക്ക് മാറ്റുക]]'' എന്ന താളിൽ മാറ്റാവുന്നതാണ്.",
        "newarticle": "(പുതിയത്)",
        "newarticletext": "ഇതുവരെ നിലവിലില്ലാത്ത ഒരു താൾ സൃഷ്ടിക്കാനുള്ള ശ്രമത്തിലാണ് താങ്കൾ. അതിനായി താഴെ ആവശ്യമുള്ള വിവരങ്ങൾ എഴുതിച്ചേർത്ത് സേവ് ചെയ്യുക (കൂടുതൽ വിവരങ്ങൾക്ക് [$1 സഹായം താൾ] കാണുക). താങ്കളിവിടെ അബദ്ധത്തിൽ വന്നതാണെങ്കിൽ ബ്രൗസറിന്റെ ബാക്ക് ബട്ടൺ ഞെക്കിയാൽ തിരിച്ചുപോകാം.",
-       "anontalkpagetext": "----\n{| class=\"messagebox standard-talk\" style=\"border: 1px solid #B3B300; background-color:#FFFFBF; text-align: left;\"\n|\n''ഇതുവരെ അംഗത്വം എടുക്കാതിരിക്കുകയോ, നിലവിലുള്ള അംഗത്വം ഉപയോഗിക്കാതിരിക്കുകയോ ചെയ്യുന്ന '''ഒരു അജ്ഞാത ഉപയോക്താവിന്റെ സം‌വാദം താളാണിത്'''.\nഅതിനാൽ അദ്ദേഹത്തെ തിരിച്ചറിയുവാൻ അക്കരൂപത്തിലുള്ള ഐ.പി. വിലാസം ഉപയോഗിക്കേണ്ടതുണ്ട്. ഇത്തരം ഒരു ഐ.പി. വിലാസം പല ഉപയോക്താക്കൾ പങ്കുവെക്കുന്നുണ്ടാവാം.\nതാങ്കൾ ഈ സന്ദേശം ലഭിച്ച ഒരു അജ്ഞാത ഉപയോക്താവാണെങ്കിൽ, ഭാവിയിൽ ഇതര ഉപയോക്താക്കളുമായി ഉണ്ടായേക്കാവുന്ന ആശയക്കുഴപ്പം ഒഴിവാക്കാൻ ദയവായി [[Special:UserLogin/signup|ഒരു അംഗത്വമെടുക്കുക]] അല്ലെങ്കിൽ  [[Special:UserLogin|പ്രവേശിക്കുക]].\n|}",
+       "anontalkpagetext": "----\n{| class=\"messagebox standard-talk\" style=\"border: 1px solid #B3B300; background-color:#FFFFBF; text-align: left;\"\n|\n''ഇതുവരെ അംഗത്വം എടുക്കാതിരിക്കുകയോ, നിലവിലുള്ള അംഗത്വം ഉപയോഗിക്കാതിരിക്കുകയോ ചെയ്യുന്ന '''ഒരു അജ്ഞാത ഉപയോക്താവിന്റെ സം‌വാദം താളാണിത്'''.\nഅതിനാൽ അദ്ദേഹത്തെ തിരിച്ചറിയുവാൻ അക്കരൂപത്തിലുള്ള ഐ.പി. വിലാസം ഉപയോഗിക്കേണ്ടതുണ്ട്. ഇത്തരം ഒരു ഐ.പി. വിലാസം പല ഉപയോക്താക്കൾ പങ്കുവെക്കുന്നുണ്ടാവാം.\nതാങ്കൾ ഈ സന്ദേശം ലഭിച്ച ഒരു അജ്ഞാത ഉപയോക്താവാണെങ്കിൽ, ഭാവിയിൽ ഇതര ഉപയോക്താക്കളുമായി ഉണ്ടായേക്കാവുന്ന ആശയക്കുഴപ്പം ഒഴിവാക്കാൻ ദയവായി [[Special:CreateAccount|ഒരു അംഗത്വമെടുക്കുക]] അല്ലെങ്കിൽ  [[Special:UserLogin|പ്രവേശിക്കുക]].\n|}",
        "noarticletext": "ഈ താളിൽ ഇതുവരെ ഉള്ളടക്കം ആയിട്ടില്ല.\nതാങ്കൾക്ക് മറ്റുതാളുകളിൽ [[Special:Search/{{PAGENAME}}|ഇതേക്കുറിച്ച് അന്വേഷിക്കുകയോ]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ബന്ധപ്പെട്ട രേഖകൾ പരിശോധിക്കുകയോ], [{{fullurl:{{FULLPAGENAME}}|action=edit}} ഈ താൾ തിരുത്തുകയോ ചെയ്യാവുന്നതാണ്]</span>.",
        "noarticletext-nopermission": "ഇപ്പോൾ ഈ താളിൽ എഴുത്തുകളൊന്നും ഇല്ല.\nതാങ്കൾക്ക് മറ്റു താളുകളിൽ [[Special:Search/{{PAGENAME}}|ഈ താളിന്റെ തലക്കെട്ടിനായി തിരയാവുന്നതാണ്‌]], അല്ലെങ്കിൽ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ബന്ധപ്പെട്ട രേഖകൾ പരിശോധിക്കാവുന്നതാണ്‌]</span>. പക്ഷേ ഈ താൾ സൃഷ്ടിക്കാൻ താങ്കൾക്ക് അനുവാദമില്ല.",
        "missing-revision": "\"{{FULLPAGENAME}}\" എന്ന താളിന്റെ #$1 എന്ന നാൾപ്പതിപ്പ് നിലവിലില്ല.\n\nമായ്ക്കപ്പെട്ട താളിന്റെ കാലഹരണപ്പെട്ട നാൾവഴി കണ്ണി ഉപയോഗിച്ചാലാണ് സാധാരണ ഇങ്ങനെ സംഭവിക്കുക.\nകൂടുതൽ വിവരങ്ങൾ [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} മായ്ക്കൽ രേഖയിൽ] കാണാവുന്നതാണ്.",
        "upload-form-label-own-work": "ഇതെന്റെ സ്വന്തം സൃഷ്ടി ആണ്",
        "upload-form-label-infoform-categories": "വർഗ്ഗങ്ങൾ",
        "upload-form-label-infoform-date": "തീയതി",
-       "upload-form-label-own-work-message-local": "{{SITENAME}} സംരംഭത്തിലെ സേവന നിബന്ധനകൾക്കും ഉപയോഗാനുമതി നയങ്ങൾക്കും അനുസരിച്ചാണ് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യുന്നതെന്ന് ഞാൻ സ്ഥിരീകരിക്കുന്നു.",
-       "upload-form-label-not-own-work-message-local": "{{SITENAME}} സംരംഭത്തിലെ നയങ്ങളനുസരിച്ച് താങ്കൾക്ക് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യാൻ കഴിയില്ലെങ്കിൽ, ദയവായി ഇത് അടച്ച് മറ്റൊരു മാർഗ്ഗം ശ്രമിക്കുക.",
-       "upload-form-label-not-own-work-local-local": "താങ്കൾക്ക് [[Special:Upload|സ്വതേ ഉള്ള അപ്‌ലോഡ് താളും]] പരിശോധിക്കാവുന്നതാണ്.",
-       "upload-form-label-own-work-message-default": "ഈ പ്രമാണം പങ്ക് വെയ്ക്കപ്പെട്ടിരിക്കുന്ന ഒരു ശേഖരത്തിലോട്ടാണ് അപ്‌ലോഡ് ചെയ്യുന്നതെന്ന് ഞാൻ മനസ്സിലാക്കുന്നു. അവിടുത്തെ ഉപയോഗ നിബന്ധനകൾക്കും അനുമതി നയങ്ങൾക്കും അനുസൃതമായാണ് ഇത് ചെയ്യുന്നതെന്ന് ഞാൻ സ്ഥിരീകരിക്കുന്നു.",
-       "upload-form-label-not-own-work-message-default": "പങ്ക് വെയ്ക്കപ്പെട്ടിരിക്കുന്ന ശേഖരത്തിന്റെ നയങ്ങളനുസരിച്ച് താങ്കൾക്ക് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യാൻ കഴിയില്ലെങ്കിൽ, ദയവായി ഇത് അടക്കുകയും മറ്റൊരു മാർഗ്ഗം ശ്രമിക്കുകയും ചെയ്യുക.",
-       "upload-form-label-not-own-work-local-default": "ഈ പ്രമാണം അവരുടെ നയങ്ങളുമായി ചേർന്നുപോകുമെങ്കിൽ താങ്കൾക്ക് [[Special:Upload|{{SITENAME}} സംരംഭത്തിലെ അപ്‌ലോഡ് താൾ]] പരീക്ഷിച്ചു നോക്കാവുന്നതാണ്.",
-       "upload-form-label-own-work-message-shared": "ഈ പ്രമാണത്തിന്റെ പകർപ്പവകാശം എനിക്ക് സ്വന്തമാണെന്നും, ഈ പ്രമാണം വിക്കിമീഡിയ കോമൺസിൽ പിന്നീട് മാറ്റാനാവത്തവിധം [https://creativecommons.org/licenses/by-sa/4.0/ ക്രിയേറ്റീവ് കോമൺസ് ആട്രിബ്യൂഷൻ-ഷെയർഎലൈക് 4.0] ഉപയോഗാനുമതിയിൽ പ്രസിദ്ധീകരിക്കാമെന്നും [https://wikimediafoundation.org/wiki/Terms_of_Use/ml ഉപയോഗനിബന്ധനകൾ] അംഗീകരിക്കുന്നുവെന്നും സാക്ഷ്യപ്പെടുത്തുന്നു.",
-       "upload-form-label-not-own-work-message-shared": "ഈ പ്രമാണത്തിന്റെ പകർപ്പവകാശം താങ്കളുടെ സ്വന്തമല്ലെങ്കിൽ അഥവാ മറ്റൊരു ഉപയോഗാനുമതിയിലാണ് പ്രമാണം പ്രസിദ്ധീകരിക്കാൻ ഉദ്ദേശിക്കുന്നതെങ്കിൽ [https://commons.wikimedia.org/wiki/Special:UploadWizard?uselang=ml കോമൺസിലെ അപ്‌ലോഡ് സഹായി] ഉപയോഗിക്കുന്നത് പരിഗണിക്കുക.",
-       "upload-form-label-not-own-work-local-shared": "ഈ പ്രമാണം അവരുടെ നയങ്ങൾക്കനുസൃതമായി അപ്‌ലോഡ് ചെയ്യാൻ സൈറ്റ് അനുവദിക്കുമെങ്കിൽ [[Special:Upload|{{SITENAME}} സംരംഭത്തിലെ അപ്‌ലോഡ് താൾ]] പരീക്ഷിച്ചു നോക്കാവുന്നതാണ്.",
+       "upload-form-label-own-work-message-generic-local": "{{SITENAME}} സംരംഭത്തിലെ സേവന നിബന്ധനകൾക്കും ഉപയോഗാനുമതി നയങ്ങൾക്കും അനുസരിച്ചാണ് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യുന്നതെന്ന് ഞാൻ സ്ഥിരീകരിക്കുന്നു.",
+       "upload-form-label-not-own-work-message-generic-local": "{{SITENAME}} സംരംഭത്തിലെ നയങ്ങളനുസരിച്ച് താങ്കൾക്ക് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യാൻ കഴിയില്ലെങ്കിൽ, ദയവായി ഇത് അടച്ച് മറ്റൊരു മാർഗ്ഗം ശ്രമിക്കുക.",
+       "upload-form-label-not-own-work-local-generic-local": "താങ്കൾക്ക് [[Special:Upload|സ്വതേ ഉള്ള അപ്‌ലോഡ് താളും]] പരിശോധിക്കാവുന്നതാണ്.",
+       "upload-form-label-own-work-message-generic-foreign": "ഈ പ്രമാണം പങ്ക് വെയ്ക്കപ്പെട്ടിരിക്കുന്ന ഒരു ശേഖരത്തിലോട്ടാണ് അപ്‌ലോഡ് ചെയ്യുന്നതെന്ന് ഞാൻ മനസ്സിലാക്കുന്നു. അവിടുത്തെ ഉപയോഗ നിബന്ധനകൾക്കും അനുമതി നയങ്ങൾക്കും അനുസൃതമായാണ് ഇത് ചെയ്യുന്നതെന്ന് ഞാൻ സ്ഥിരീകരിക്കുന്നു.",
+       "upload-form-label-not-own-work-message-generic-foreign": "പങ്ക് വെയ്ക്കപ്പെട്ടിരിക്കുന്ന ശേഖരത്തിന്റെ നയങ്ങളനുസരിച്ച് താങ്കൾക്ക് ഈ പ്രമാണം അപ്‌ലോഡ് ചെയ്യാൻ കഴിയില്ലെങ്കിൽ, ദയവായി ഇത് അടക്കുകയും മറ്റൊരു മാർഗ്ഗം ശ്രമിക്കുകയും ചെയ്യുക.",
+       "upload-form-label-not-own-work-local-generic-foreign": "ഈ പ്രമാണം അവരുടെ നയങ്ങളുമായി ചേർന്നുപോകുമെങ്കിൽ താങ്കൾക്ക് [[Special:Upload|{{SITENAME}} സംരംഭത്തിലെ അപ്‌ലോഡ് താൾ]] പരീക്ഷിച്ചു നോക്കാവുന്നതാണ്.",
        "backend-fail-stream": "$1 എന്ന പ്രമാണം സ്ട്രീം ചെയ്യാൻ കഴിഞ്ഞില്ല.",
        "backend-fail-backup": "$1 എന്ന പ്രമാണത്തിന്റെ ബാക്ക്അപ് എടുക്കാൻ കഴിഞ്ഞില്ല.",
        "backend-fail-notexists": "$1 എന്ന പ്രമാണം നിലവിലില്ല.",
index 5b990cc..4c28ba3 100644 (file)
        "noname": "Та хүчинтэй хэрэглэгчийн нэрийг өгөөгүй байна.",
        "loginsuccesstitle": "Амжилттай нэвтэрлээ",
        "loginsuccess": "'''Та {{SITENAME}} руу \"$1\" нэрээр нэвтэрлээ.'''",
-       "nosuchuser": "\"$1\" нэртэй хэрэглэгч олдсонгүй.\nХэрэглэгчийн нэрийн үсгүүд том жижгээр бичсэн байдлаар өөр байна.\nТа зөв бичсэн эсэхээ шалгах, эсвэл [[Special:UserLogin/signup| шинээр бүртгүүлнэ үү]].",
+       "nosuchuser": "\"$1\" нэртэй хэрэглэгч олдсонгүй.\nХэрэглэгчийн нэрийн үсгүүд том жижгээр бичсэн байдлаар өөр байна.\nТа зөв бичсэн эсэхээ шалгах, эсвэл [[Special:CreateAccount| шинээр бүртгүүлнэ үү]].",
        "nosuchusershort": "\"$1\" гэсэн нэртэй хэрэглэгч байхгүй байна.\nҮсгийн алдаагаа шалгана уу.",
        "nouserspecified": "Та хэрэглэгчийн нэрээ зааж өгөх хэрэгтэй.",
        "login-userblocked": "Энэхүү хэрэглэгчийг түгжсэн байна. Нэвтрэх боломжгүй.",
        "accmailtext": "[[User talk:$1|$1]]-н санамсаргүй байдлаар үүсгэгдсэн нууц үг $2 руу илгээгдлээ.\n\nНэвтэрч орсны дараа энэ шинэ бүртгэлийн нууц үгийг ''[[Special:ChangePassword|энэ хуудсаар]]'' солих боломжтой.",
        "newarticle": "(Шинэ)",
        "newarticletext": "Таны орохыг хүссэн хуудас байхгүй байна. Энэ хуудсыг шинээр үүсгэхийн тулд доорх талбарт мэдээллээ оруулна уу (дэлгэрэ\nнгүй мэдээллийг [$1 тусламжийн хуудсаас] авч болно).\nХэрэв та энд санамсаргүйгээр орж ирсэн бол броузерийнхаа '''back''' товчийг дар.",
-       "anontalkpagetext": "----''Энэ нь хараахан бүртгүүлээгүй байгаа буюу хэрэглэгчийн нэр хэрэглэхгүй байгаа хэрэглэгчийн хэлэлцүүлгийн хуудас болно.\nБид ийм хэрэглэгчдийг тогтоохын тулд IP хаягийг нь ашигладаг.\nНэг IP хаягийг хэд хэдэн хэрэглэгч хамтран ашиглаж байж болно.\nХэрвээ таны IP хаягт ирсэн санал хүсэлтүүд танд хандаагүй бөгөөд та цаашид ийм байдал үүсэхээс сэргийлье гэвэл [[Special:UserLogin/signup|бүртгүүлэх]] буюу [[Special:UserLogin|нэвтрэхийг]] хүсье.''",
+       "anontalkpagetext": "----''Энэ нь хараахан бүртгүүлээгүй байгаа буюу хэрэглэгчийн нэр хэрэглэхгүй байгаа хэрэглэгчийн хэлэлцүүлгийн хуудас болно.\nБид ийм хэрэглэгчдийг тогтоохын тулд IP хаягийг нь ашигладаг.\nНэг IP хаягийг хэд хэдэн хэрэглэгч хамтран ашиглаж байж болно.\nХэрвээ таны IP хаягт ирсэн санал хүсэлтүүд танд хандаагүй бөгөөд та цаашид ийм байдал үүсэхээс сэргийлье гэвэл [[Special:CreateAccount|бүртгүүлэх]] буюу [[Special:UserLogin|нэвтрэхийг]] хүсье.''",
        "noarticletext": "Одоогийн байдлаар энэ хуудсанд текст алга.\nТа нэг бол энэ хуудасны нэрээр бусад хуудсуудад [[Special:Search/{{PAGENAME}}|хайлт хийх]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} холбоотой логуудад хайлт хийх],\nэсвэл [{{fullurl:{{FULLPAGENAME}}|action=edit}} энэ хуудсыг засварлаж болно]</span>.",
        "noarticletext-nopermission": "Яг одоогоор уг хуудсанд текст алга.\nТа бусад хуудсан уг хуудасны [[Special:Search/{{PAGENAME}}| гарчигаар хайлт хийх]], эсвэл <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} холбоотой логоор хайлт]</span> хийж болно, гэхдээ танд уг хуудсыг үүсгэх зөвшөөрөл алга.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" хэрэглэгчийн бүртгэл бүртгэгдээгүй байна. Та энэ хуудсыг үүсгэх/засварлах тухайгаа дахин тунгааж бодно уу.",
index ed1ae3c..b2af24b 100644 (file)
        "noname": "आपण वैध सदस्यनाम नमूद केले नाही.",
        "loginsuccesstitle": "आपल्या सनोंद-प्रवेशाची नोंदणी यशस्वीरीत्या पूर्ण झाली",
        "loginsuccess": "'''तुम्ही {{SITENAME}} वर \"$1\" नावाने सनोंद प्रवेशित आहात.'''",
-       "nosuchuser": "\"$1\" या नावाचा कोणताही सदस्य नाही.तुमचे शुद्धलेखन तपासा, किंवा [[Special:UserLogin/signup|नवीन खाते]] तयार करा.",
+       "nosuchuser": "\"$1\" या नावाचा कोणताही सदस्य नाही.तुमचे शुद्धलेखन तपासा, किंवा [[Special:CreateAccount|नवीन खाते]] तयार करा.",
        "nosuchusershort": "\"$1\" या नावाचा सदस्य नाही. लिहीताना आपली चूक तर नाही ना झाली?",
        "nouserspecified": "तुम्हाला सदस्यनाव नमूद करावे लागेल.",
        "login-userblocked": "हा सदस्य ’प्रतिबंधित’ आहे. त्यास सनोंद-प्रवेशाची परवानगी नाही.",
        "accmailtext": "[[User talk:$1|$1]] यांसाठी अनियतक्रमाने निर्मित केलेला परवलीचा शब्द $2 यांना पाठवण्यात आला आहे.\n\nया नवीन खात्यासाठीचा परवलीचा शब्द,सनोंद-प्रवेश घेतल्यावर [[Special:ChangePassword|परवलीचा शब्द बदला]] येथे बदलता येईल.",
        "newarticle": "(नवीन लेख)",
        "newarticletext": "आपण सध्या अस्तित्त्वात नसलेल्या पानाच्या दुव्याचा मागोवा घेत आला आहात.\nहे पान नव्याने तयार करण्यासाठी खालील पेटीत टंकन करणे सुरु करा(अधिक माहितीसाठी [$1 साहाय्य पान] बघा).\n\nजर आपण येथे चुकून आला असाल तर ब्राउझरच्या  <strong>परत</strong>(बॅक) कळीवर टिचकी द्या.",
-       "anontalkpagetext": "---- ''हे चर्चापान अशा अज्ञात सदस्यासाठी आहे, ज्यांनी खाते तयार केलेले नाही किंवा त्याचा वापर करत नाहीत. त्यांच्या ओळखीसाठी आम्ही आंतरजाल अंकपत्ता वापरतो आहोत. असा अंकपत्ता बऱ्याच लोकांचा एकच असू शकतो. जर आपण अज्ञात सदस्य असाल आणि आपल्याला काही अप्रासंगिक संदेश मिळाला असेल तर कृपया [[Special:UserLogin| खाते तयार करा]] किंवा [[Special:UserLogin/signup|सनोंद-प्रवेश करा]] ज्यामुळे, पुढे असे गैरसमज होणार नाहीत.''",
+       "anontalkpagetext": "---- ''हे चर्चापान अशा अज्ञात सदस्यासाठी आहे, ज्यांनी खाते तयार केलेले नाही किंवा त्याचा वापर करत नाहीत. त्यांच्या ओळखीसाठी आम्ही आंतरजाल अंकपत्ता वापरतो आहोत. असा अंकपत्ता बऱ्याच लोकांचा एकच असू शकतो. जर आपण अज्ञात सदस्य असाल आणि आपल्याला काही अप्रासंगिक संदेश मिळाला असेल तर कृपया [[Special:UserLogin| खाते तयार करा]] किंवा [[Special:CreateAccount|सनोंद-प्रवेश करा]] ज्यामुळे, पुढे असे गैरसमज होणार नाहीत.''",
        "noarticletext": "या लेखात सध्या काहीही मजकूर नाही.\nतुम्ही विकिपीडियावरील इतर लेखांमध्ये या [[Special:Search/{{PAGENAME}}| मथळ्याचा शोध घेऊ शकता]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} इतर नोंदी शोधा],\nकिंवा हा लेख [{{fullurl:{{FULLPAGENAME}}|action=edit}}संपादू शकता]</span>.",
        "noarticletext-nopermission": "सध्या या लेखात  काहीही मजकूर नाही.\nतुम्ही विकिपीडियावरील इतर लेखांमध्ये [[Special:Search/{{PAGENAME}}| या मथळ्याचा शोध घेऊ शकता]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAME}}}}आपण या लेखाच्या इतर नोंदी शोधा]</span>,परंतु, आपणास हा लेख लिहीण्याची परवानगी देण्यात येउ शकत नाही.",
        "missing-revision": "\"{{FULLPAGENAME}}\" या लेखाचे #$1 हे संस्करण अस्तित्वात नाही.वगळल्या गेलेल्या लेखपानाच्या जुन्या इतिहास-दुव्याचे अनुसरण केल्यामुळे असे होते.याबाबत विस्तृत माहिती  [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} वगळलेल्या नोंदी]येथे बघता येईल.",
index 221b3bb..59249b9 100644 (file)
        "noname": "Nama pengguna tidak sah.",
        "loginsuccesstitle": "Berjaya log masuk",
        "loginsuccess": "'''Anda telah log masuk ke dalam {{SITENAME}} sebagai \"$1\".'''",
-       "nosuchuser": "Pengguna \"$1\" tidak wujud. Nama pengguna adalah peka huruf besar. Sila semak ejaan anda, atau anda boleh [[Special:UserLogin/signup|membuka akaun baru]].",
+       "nosuchuser": "Pengguna \"$1\" tidak wujud. Nama pengguna adalah peka huruf besar. Sila semak ejaan anda, atau anda boleh [[Special:CreateAccount|membuka akaun baru]].",
        "nosuchusershort": "Pengguna \"$1\" tidak wujud. Sila semak ejaan anda.",
        "nouserspecified": "Sila nyatakan nama pengguna.",
        "login-userblocked": "Pengguna ini disekat. Log masuk tidak dibenarkan.",
        "accmailtext": "Kata laluan janaan rawak untuk [[User talk:$1|$1]] telah dikirim kepada $2. Anda boleh menukarnya di halaman ''[[Special:ChangePassword|tukar kata laluan]]'' sebaik sahaja log masuk.",
        "newarticle": "(Baru)",
        "newarticletext": "Anda telah mengikuti pautan ke laman yang belum wujud.\nUntuk mencipta laman ini, sila taip dalam kotak di bawah\n(lihat [$1 laman bantuan] untuk maklumat lanjut).\nJika anda tiba di sini secara tak sengaja, hanya klik butang '''back''' pada pelayar anda.",
-       "anontalkpagetext": "----''Ini ialah laman perbincangan bagi pengguna tanpa nama yang belum membuka akaun atau tidak log masuk.\nOleh itu kami terpaksa menggunakan alamat IP untuk mengenal pasti pengguna tersebut. Alamat IP ini boleh dikongsi oleh ramai pengguna.\nSekiranya anda adalah seorang pengguna tanpa nama dan berasa bahawa komen yang tidak kena mengena telah ditujukan kepada anda, sila [[Special:UserLogin/signup|buka akaun baru]] atau [[Special:UserLogin|log masuk]] untuk mengelakkan sebarang kekeliruan dengan pengguna tanpa nama yang lain.''",
+       "anontalkpagetext": "----''Ini ialah laman perbincangan bagi pengguna tanpa nama yang belum membuka akaun atau tidak log masuk.\nOleh itu kami terpaksa menggunakan alamat IP untuk mengenal pasti pengguna tersebut. Alamat IP ini boleh dikongsi oleh ramai pengguna.\nSekiranya anda adalah seorang pengguna tanpa nama dan berasa bahawa komen yang tidak kena mengena telah ditujukan kepada anda, sila [[Special:CreateAccount|buka akaun baru]] atau [[Special:UserLogin|log masuk]] untuk mengelakkan sebarang kekeliruan dengan pengguna tanpa nama yang lain.''",
        "noarticletext": "Laman ini buat masa sekarang tidak berteks. Anda boleh [[Special:Search/{{PAGENAME}}|cari tajuk bagi laman ini]] dalam laman-laman lain, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cari log-log yang berkaitan], atau [{{fullurl:{{FULLPAGENAME}}|action=edit}} sunting laman ini]</span>.",
        "noarticletext-nopermission": "Tiada teks dalam laman ini ketika ini.\nAnda boleh [[Special:Search/{{PAGENAME}}|mencari tajuk laman ini]] dalam laman lain,\natau <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mencari log yang berkaitan]</span>.",
        "missing-revision": "Semakan #$1 pada halaman \"{{FULLPAGENAME}}\" tidak wujud.\n\nHal ini biasanya disebabkan oleh pautan sejarah yang lapuk ke halaman yang sudah dihapuskan.\nButirannya boleh didapati di [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} log penghapusan].",
        "upload-form-label-own-work": "Ini ialah karya saya sendiri",
        "upload-form-label-infoform-categories": "Kategori",
        "upload-form-label-infoform-date": "Tarikh",
-       "upload-form-label-own-work-message-local": "Saya mengesahkan bahawa saya memuat naik fail ini dengan mengikut terma perkhidmatan dan dasar perlesenan di {{SITENAME}}.",
-       "upload-form-label-not-own-work-local-local": "Anda mungkin juga mahu mencuba [[Special:Upload|laman muat naik yang asal]].",
+       "upload-form-label-own-work-message-generic-local": "Saya mengesahkan bahawa saya memuat naik fail ini dengan mengikut terma perkhidmatan dan dasar perlesenan di {{SITENAME}}.",
+       "upload-form-label-not-own-work-local-generic-local": "Anda mungkin juga mahu mencuba [[Special:Upload|laman muat naik yang asal]].",
        "backend-fail-stream": "Fail $1 tidak dapat distrimkan.",
        "backend-fail-backup": "Fail $1 tidak dapat disandarkan.",
        "backend-fail-notexists": "Fail $1 tidak wujud.",
index 2993d36..f4e8742 100644 (file)
        "noname": "L-isem tal-utent li tajt mhuwiex validu.",
        "loginsuccesstitle": "Dħalt b'suċċess",
        "loginsuccess": "'''Irnexxielek taqbad mas-server ta' {{SITENAME}} bl-isem tal-utent \"$1\".'''",
-       "nosuchuser": "M'hemm l-ebda utent bl-isem ta' \"$1\".<br />\nL-ismijiet tal-utenti huma sensittivi fuq kif jinkitbu.<br />\nJekk jogħġbok kun żġur li ktibtu sew, jew minflok [[Special:UserLogin/signup|oħloq kont ġdid]].",
+       "nosuchuser": "M'hemm l-ebda utent bl-isem ta' \"$1\".<br />\nL-ismijiet tal-utenti huma sensittivi fuq kif jinkitbu.<br />\nJekk jogħġbok kun żġur li ktibtu sew, jew minflok [[Special:CreateAccount|oħloq kont ġdid]].",
        "nosuchusershort": "M'hemm l-ebda utent bl-isem \"$1\".\nAgħmel żġur li ktibta sew.",
        "nouserspecified": "Trid tispeċifika isem tal-utent.",
        "login-userblocked": "Dan l-utent huwa imblukkat. Mhuwiex possibbli li jsir il-login.",
        "accmailtext": "Intbagħtet lil $2 password iġġenerata każwalment għal [[User talk:$1|$1]] .\nTista' tinbidel fuq il-paġna għat-<em>[[Special:ChangePassword|tibdil tal-password]]</em> wara d-dħul fil-kont.",
        "newarticle": "(Ġdid)",
        "newarticletext": "Inti segwejt link għal paġna li għadha ma ġietx maħluqa.\nSabiex toħloq il-paġna, ikteb fil-kaxxa li tinsab hawn taħt (ara [$1 paġna tal-għajnuna] għal aktar informazzjoni).\nJekk wasalt hawn biż-żball, agħfas il-buttuna '''lura''' (''back'') fuq il-browser tiegħek.",
-       "anontalkpagetext": "----''Din hija l-paġna ta' diskussjoni ta' utent anonimu li għadu ma ħoloqx kont, jew inkella li ma jużahx.\nGħaldaqstant biex nidentifikawh ikollna nużaw l-indirizz tal-IP tiegħu/tagħha.\nL-istess indirizz tal-IP jista' jkun użat minn bosta utenti differenti.\nJekk int utent anonimu u tħoss li qiegħed tirċievi kummenti irrelevanti jew li ma jagħmlux sens, jekk jogħġbok [[Special:UserLogin|idħol fil-kont tiegħek]] jew [[Special:UserLogin/signup|oħloq wieħed]] sabiex tevita li fil-futur tiġi konfuż ma' utenti anonimi oħra.''",
+       "anontalkpagetext": "----''Din hija l-paġna ta' diskussjoni ta' utent anonimu li għadu ma ħoloqx kont, jew inkella li ma jużahx.\nGħaldaqstant biex nidentifikawh ikollna nużaw l-indirizz tal-IP tiegħu/tagħha.\nL-istess indirizz tal-IP jista' jkun użat minn bosta utenti differenti.\nJekk int utent anonimu u tħoss li qiegħed tirċievi kummenti irrelevanti jew li ma jagħmlux sens, jekk jogħġbok [[Special:UserLogin|idħol fil-kont tiegħek]] jew [[Special:CreateAccount|oħloq wieħed]] sabiex tevita li fil-futur tiġi konfuż ma' utenti anonimi oħra.''",
        "noarticletext": "Bħalissa m'hemm l-ebda test f'din il-paġna.\nTista' [[Special:Search/{{PAGENAME}}|tfittex it-titlu ta' din il-paġna]] f'paġni oħra, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tfittex ir-reġistri relatati], jew [{{fullurl:{{FULLPAGENAME}}|action=edit}} toħloq dil-paġna]</span>.",
        "noarticletext-nopermission": "Bħalissa m'hemm l-ebda test f'din il-paġna. Inti tista' [[Special:Search/{{PAGENAME}}|tfittex dan it-titlu tal-paġna]] f'paġni oħra, jew <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tfittex ir-reġistri relatati]</span>, imma m'għandikx permess toħloq dil-paġna.",
        "missing-revision": "Ir-reviżjoni #$1 tal-paġna bl-isem \"{{FULLPAGENAME}}\" ma teżistix.\n\nDan ħafna drabi jiġri minħabba li tkun segwejt ħolqa lejn paġna mħassra, f'kronoloġija li mhix aġġornata.\nId-detallji tista' ssibhom fir-[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} reġistru tat-tħassir].",
index c02ff03..56e90a0 100644 (file)
        "loginerror": "Erro de outenticaçon",
        "loginsuccesstitle": "Antreste cumo debe de ser",
        "loginsuccess": "'''Stás agora lhigado a {{SITENAME}} cumo \"$1\"'''.",
-       "nosuchuser": "Num eisiste nanhun outelizador cul nome \"$1\".\nLs nomes de outelizador son defrentes an lhetra grande ó pequeinha.\nBei cumo screbiste, ó [[Special:UserLogin/signup|cria ua nuoba cuonta]].",
+       "nosuchuser": "Num eisiste nanhun outelizador cul nome \"$1\".\nLs nomes de outelizador son defrentes an lhetra grande ó pequeinha.\nBei cumo screbiste, ó [[Special:CreateAccount|cria ua nuoba cuonta]].",
        "nosuchusershort": "Nun eisiste nanhun outelizador cul nome \"$1\".\nBei se l screbiste bien.",
        "nouserspecified": "Tenes que dezir un nome de outelizador.",
        "wrongpassword": "La palabra chabe ye ambálida.\nPor fabor, spurmenta outra beç.",
        "move-page-legend": "Mover página",
        "movepagetext": "Outelizando este formulário tu puodes renomear ua páigina, arrastrando to l stórico para l nuobo títalo. L títalo anterior será transformado nun ancaminamiento para l nuobo.\nYe possible amanhar de forma outomática ancaminamientos que lhigen un títalo oureginal.\nCauso scuolhas para que esso nun seia feito, bei se nun hai ancaminamientos [[Special:DoubleRedirects|dues bezes]] ó [[Special:BrokenRedirects|scachados]].\nYe de la tue respunsablidade tener la certeza de que las lhigaçones cuntinan a apuntar pa adonde dében.\n\nArrepara que la páigina '''nun''' será arrastrada se yá eisistir ua páigina cul nuobo títalo, a nun ser que steia bazio ó seia un ancaminamiento i nun tenga stórico de eidiçones. Esto quier dezir que puodes renomear outra beç ua páigina pa l nome que tenie antes de l anganho i que nun puodes subrescrebir ua páigina.\n\n<b>CUIDADO!</b>\nEsto puode ser ua altaraçon drástica i einesperada pa ua páigina popular; por fabor, ten la certeza de que antendes las cunsequéncias desto antes de cuntinar.",
        "movepagetalktext": "La páigina de \"çcusson\" associada, se eistir, será outomaticamente arrastrada, '''a nun ser que:'''\n*Ua páigina de çcusson cun contenido yá eisista subre l nuobo títalo, ou\n*Tu marques la caixa ambaixo.\n\nNestes causos, tu terás que arrastrar ou ajuntar la páigina a la mano, se assi quejires.",
-       "movearticle": "Arrastrar páigina",
        "newtitle": "Pa nuobo títalo:",
        "move-watch": "Begiar esta páigina",
        "movepagebtn": "Arrastrar páigina",
index 9f40bf4..97f653d 100644 (file)
        "noname": "Зярс эзить максо кемекстазь теицянь лем.",
        "loginsuccesstitle": "Совавить",
        "loginsuccess": "'''Тон совить {{SITENAME}}-с кода \"$1\".'''",
-       "nosuchuser": "$1 лем марто теиця арась.\nТеиця лемтне явозь тень корясь вишка эли покш тештинесэ сёрмадозь. Ваннык видестэ - а видестэ сёрмадык, эли [[Special:UserLogin/signup|тейть-шкак од совамо тарка]].",
+       "nosuchuser": "$1 лем марто теиця арась.\nТеиця лемтне явозь тень корясь вишка эли покш тештинесэ сёрмадозь. Ваннык видестэ - а видестэ сёрмадык, эли [[Special:CreateAccount|тейть-шкак од совамо тарка]].",
        "nosuchusershort": "\"$1\" лемсэ теиця арась. Варштака, кизды, аволь истя сёрмадозь.",
        "nouserspecified": "Теицянь лем эряви.",
        "login-userblocked": "Те теицясь аравтозь саймас. Совамонзо а мерить.",
index ddbb9a1..8a350a6 100644 (file)
        "noname": "Ahmo ōtiquihto cualli tlatequitiltilīlli tōcāitl.",
        "loginsuccesstitle": "Cualli calaquiliztli",
        "loginsuccess": "'''Ōticalac {{SITENAME}} quemeh \"$1\".'''",
-       "nosuchuser": "Ayāc tlatequitiltilīlli motōcāitia \"$1\".\nIn tlatequitiltilīltōcāitl quimati in huēyimachiyōtlahtōliztli.\nXiquitta in yēquihcuilōlli, ahnozo [[Special:UserLogin/signup|xicchīhua yancuīc cuenta]].",
+       "nosuchuser": "Ayāc tlatequitiltilīlli motōcāitia \"$1\".\nIn tlatequitiltilīltōcāitl quimati in huēyimachiyōtlahtōliztli.\nXiquitta in yēquihcuilōlli, ahnozo [[Special:CreateAccount|xicchīhua yancuīc cuenta]].",
        "nosuchusershort": "Ayāc tlatequitiltilīlli motōcāitia \"$1\". Xiquitta in tlein ōtitlahcuiloh melāhuacā cah.\nXiquitta moyēquihcuilōl.",
        "nouserspecified": "Mohuīquilia tiquihtoa cualli tlatequitiltilīltōcāitl.",
        "wrongpassword": "Ahcualli motlahtōlichtacāyo.\nTimitztlātlauhtia xicchīhua occeppa.",
        "categories": "Tlaìxmatkàtlàlilòmë",
        "categoriespagetext": "{{PLURAL:$1|Inìn tlaìxmatkàtlàlilòtl kimpia|Inîkë tlaìxmatkàtlàlilòmë kimpiâkë}} tlaìxtlapaltìn noso medios.\nÂmò monèxtiâkë nikàn in [[Special:UnusedCategories|tlaìxmatkàtlàlilòmë tlèn âmò mokìntekitìltia]].\nNò mà mỏta in tlèn [[Special:WantedCategories|ìpan kineki tlaìxmatkàtlàlilòtl]].",
        "categoriesfrom": "Mà monèxtìkàn tlaìxmatkàtlàlilòmë tlèn pèwâkë ìka:",
-       "special-categories-sort-count": "tlapōhualcopa",
-       "special-categories-sort-abc": "tlahtōlcopa",
        "linksearch": "Calān tzonhuiliztli tlatemoliztli",
        "linksearch-ns": "Tōcātzin:",
        "linksearch-ok": "Tictēmōz",
index 6b6edce..1e5f271 100644 (file)
        "noname": "Lí phah--ê iōng-chiá-miâ boē-sái.",
        "loginsuccesstitle": "Teng-ji̍p sêng-kong",
        "loginsuccess": "Lí hiān-chhú-sî í-keng teng-ji̍p {{SITENAME}} chò \"$1\".",
-       "nosuchuser": "Chia bô iōng-chiá hō-chò \"$1\".\nIiōng-chiá hō-chò ū hun toā-jī sè-jī.\nChhiáⁿ kiám-cha lí ê phèng-im, a̍h-sī  [[Special:UserLogin/signup|khui sin iōng-chiá ê kháu-chō.]]",
+       "nosuchuser": "Chia bô iōng-chiá hō-chò \"$1\".\nIiōng-chiá hō-chò ū hun toā-jī sè-jī.\nChhiáⁿ kiám-cha lí ê phèng-im, a̍h-sī  [[Special:CreateAccount|khui sin iōng-chiá ê kháu-chō.]]",
        "nosuchusershort": "Bô \"$1\" chit ê iōng-chiá miâ.\nTùi khoàⁿ-māi,  lí phah--ê.",
        "nouserspecified": "Lí ài chí-tēng chi̍t ê iōng-chiá miâ.",
        "login-userblocked": "這個用者已經予人封鎖,袂使登入。",
index 6d43901..cbf66c4 100644 (file)
        "noname": "Nun avite specificato nu nomme valido d'utente.",
        "loginsuccesstitle": "Acciesso affettuato",
        "loginsuccess": "'''Si stato cunnesso ô server 'e {{SITENAME}} cu 'o nomme utente 'e \"$1\".'''",
-       "nosuchuser": "Nun è riggistrato nisciuno utente c' 'o nomme \"$1\".\n'E nomme utente songo sensibbele a 'e maiuscole.\nCuntrolla 'o nomme nzertàto, o [[Special:UserLogin/signup|crìa n'utenza nova]].",
+       "nosuchuser": "Nun è riggistrato nisciuno utente c' 'o nomme \"$1\".\n'E nomme utente songo sensibbele a 'e maiuscole.\nCuntrolla 'o nomme nzertàto, o [[Special:CreateAccount|crìa n'utenza nova]].",
        "nosuchusershort": "Nun ce stanno utente cu o nòmme \"$1\". Cuntrolla si scrivìste buòno.",
        "nouserspecified": "Tiene 'a dìcere nu nomme pricìso.",
        "login-userblocked": "Chist'utente è bloccato. Nun se può effettuà 'o login.",
        "accmailtext": "'Na password gennerata casualmente ppe [[User talk:$1|$1]] è stata mannata a $2. Chista password può essere càgnata dint'â paggena ppe ''[[Special:ChangePassword|càgna 'a password]]'' subbeto doppo l'acciesso.",
        "newarticle": "(Nuovo)",
        "newarticletext": "Site ghiuto/a addò nu link 'e na paggena ca nun esiste ancora.\nP' 'a crià sta paggena, accummenciate a scrivere dint'a cascia cà abbascio (vedite 'a [$1 paggena d'aiuto] pe liegge cchiù nfurmazziune).\nSi site venuto/a ccà pe' sbaglio, vedite 'e sprémmere 'o buttòne '''Arreto''' d' 'o navigatóre.",
-       "anontalkpagetext": "----\n''Chest'è 'a paggena 'e discussione 'e n'utente anonimo, ca nun ave criàt' 'ancora n'utenza o ca nun sta ausanno. Pe' l'identificà avite 'e truvà 'o nummero d' 'o ndirizzo IP d' 'o sujo. L'indirizze IP se ponno spartì però sempe ausanno cunte differente. Si site n'utente anonimo e penzate ca 'e cummente ccà dint'a sta paggena nun parlano 'e vuje, allora [[Special:UserLogin/signup|criate n'utenza nnova]] o [[Special:UserLogin|trasite cu chella ca tenite già]] pe' nun sta' mmescato mmiez'a l'ati utente anonime n futuro.''",
+       "anontalkpagetext": "----\n''Chest'è 'a paggena 'e discussione 'e n'utente anonimo, ca nun ave criàt' 'ancora n'utenza o ca nun sta ausanno. Pe' l'identificà avite 'e truvà 'o nummero d' 'o ndirizzo IP d' 'o sujo. L'indirizze IP se ponno spartì però sempe ausanno cunte differente. Si site n'utente anonimo e penzate ca 'e cummente ccà dint'a sta paggena nun parlano 'e vuje, allora [[Special:CreateAccount|criate n'utenza nnova]] o [[Special:UserLogin|trasite cu chella ca tenite già]] pe' nun sta' mmescato mmiez'a l'ati utente anonime n futuro.''",
        "noarticletext": "Mo' mo' 'a paggena richiesta è abbacante. Se pò [[Special:Search/{{PAGENAME}}|ascià stu titolo]] dint'a l'ati paggene d' 'o sito, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ascià dint'e riggistre azzeccate] o pure [{{fullurl:{{FULLPAGENAME}}|action=edit}} crià 'a paggena mo']</span>.",
        "noarticletext-nopermission": "Mo' mo' 'a paggena richiesta è abbacante. Se pò [[Special:Search/{{PAGENAME}}|ascià stu titolo]] dint'a l'ati paggene d' 'o sito, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ascià dint'e riggistre azzeccate]</span>, però nun tenite 'o permesso 'a crià sta paggena.",
        "missing-revision": "'A verziona #$1 d' 'a paggena \"{{FULLPAGENAME}}\" nun esiste.\n\nChest'è causato quanno se và dint'a nu link a na paggena ch'è stata scancellata.\n'E dettaglie se ponno truvà dint'a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 'o riggistro 'e scancellamiente].",
        "upload-form-label-own-work": "Chest'è fatica mia",
        "upload-form-label-infoform-categories": "Categurìe",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Io cunfermo ca songh'io ca carrecanno stu file sto secutanno 'e tiermene 'e servizio e pulitiche 'e licienza dint'a {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Si nun site capace 'e carrecà stu file pe' bbìa d' 'e pulitiche 'e {{SITENAME}}, pe' piacere nchiurete sta casciulella e tentate n'ata maniera.",
-       "upload-form-label-not-own-work-local-local": "Forse vulite pure tentà [[Special:Upload|'a paggena 'e carreche predefinita]].",
-       "upload-form-label-own-work-message-default": "Capisco ca sto a carrecà stu file a nu repositorio spartuto. Cunfermo ca facenno chesto sto secutanno 'e tèrmene 'e servizio e licienze llanno.",
-       "upload-form-label-not-own-work-message-default": "Si nun site capace 'e carrecà stu file pe' bbìa d' 'e pulitiche d' 'o repusitorio spartuto, pe' piacere nchiurete sta casciulella e tentate n'ata maniera.",
-       "upload-form-label-not-own-work-local-default": "Putite pure tentà 'ausà [[Special:Upload|'a paggena 'e carreche 'e {{SITENAME}}]], si stu file nun se putesse carrecà llanno pe' bbìa d' 'e pulitiche.",
-       "upload-form-label-own-work-message-shared": "Faccio attestato ca songo 'o detentore d' 'o copyright 'e stu file, e so' d'accordo 'e lassà irrevocabbelmente stu file a Wikimedia Commons sott'a licienza [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribuziona-SparteEguale 4.0], e so' d'accordo cu sti [https://wikimediafoundation.org/wiki/Terms_of_Use Termene d'Uso].",
-       "upload-form-label-not-own-work-message-shared": "Si nun tenite 'o copyright 'e stu file, o pure 'o vulite lassà libbero cu n'ata licienza, cunziderate 'ausà 'o [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard].",
-       "upload-form-label-not-own-work-local-shared": "Putite pure tentà 'e ausà [[Special:Upload|'a paggena 'e carreche 'e {{SITENAME}}]], si stu sito ve premmettesse 'e carrecà llanno pe' bbìa d' 'e pulitiche.",
+       "upload-form-label-own-work-message-generic-local": "Io cunfermo ca songh'io ca carrecanno stu file sto secutanno 'e tiermene 'e servizio e pulitiche 'e licienza dint'a {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Si nun site capace 'e carrecà stu file pe' bbìa d' 'e pulitiche 'e {{SITENAME}}, pe' piacere nchiurete sta casciulella e tentate n'ata maniera.",
+       "upload-form-label-not-own-work-local-generic-local": "Forse vulite pure tentà [[Special:Upload|'a paggena 'e carreche predefinita]].",
+       "upload-form-label-own-work-message-generic-foreign": "Capisco ca sto a carrecà stu file a nu repositorio spartuto. Cunfermo ca facenno chesto sto secutanno 'e tèrmene 'e servizio e licienze llanno.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Si nun site capace 'e carrecà stu file pe' bbìa d' 'e pulitiche d' 'o repusitorio spartuto, pe' piacere nchiurete sta casciulella e tentate n'ata maniera.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Putite pure tentà 'ausà [[Special:Upload|'a paggena 'e carreche 'e {{SITENAME}}]], si stu file nun se putesse carrecà llanno pe' bbìa d' 'e pulitiche.",
        "backend-fail-stream": "Nun se può mannà 'o file \"$1\".",
        "backend-fail-backup": "Nun se può ffà 'o backup d' 'o file \"$1\".",
        "backend-fail-notexists": "'O file $1 nun esiste.",
index 575ba2d..d01ff9a 100644 (file)
        "noname": "Du har ikke oppgitt et gyldig brukernavn.",
        "loginsuccesstitle": "Du er nå logget inn",
        "loginsuccess": "Du er nå logget inn på {{SITENAME}} som «$1».",
-       "nosuchuser": "Det eksisterer ingen bruker ved navn «$1».\nMerk at det skilles mellom store og små bokstaver.\nSjekk stavemåten eller [[Special:UserLogin/signup|opprett en ny konto]].",
+       "nosuchuser": "Det eksisterer ingen bruker ved navn «$1».\nMerk at det skilles mellom store og små bokstaver.\nSjekk stavemåten eller [[Special:CreateAccount|opprett en ny konto]].",
        "nosuchusershort": "Det finnes ingen bruker ved navn «$1». Kontroller stavemåten.",
        "nouserspecified": "Du må oppgi et brukernavn.",
        "login-userblocked": "Brukeren er blokkert. Innlogging er ikke tillatt.",
        "accmailtext": "Et tilfeldig passord for [[User talk:$1|$1]] har blitt sendt til $2. Det kan endres på [[Special:ChangePassword|passordendringssiden]] under innlogging.",
        "newarticle": "(Ny)",
        "newarticletext": "Du har fulgt en lenke til en side som ikke finnes ennå.\nFor å opprette siden, begynn å skrive i boksen under (se [$1 hjelpesiden] for mer informasjon).\nOm du havnet her ved en feil, trykk '''tilbake''' i nettleseren.",
-       "anontalkpagetext": "----\n''Dette er en diskusjonsside for en uregistrert bruker som ikke har opprettet konto eller ikke er logget inn.\nVi er derfor nødt til å bruke den numeriske IP-adressen til å identifisere ham eller henne.\nEn IP-adresse kan være delt mellom flere brukere.\nHvis du er en uregistrert bruker og synes at du har fått irrelevante kommentarer på en slik side, [[Special:UserLogin/signup|opprett en konto]] eller [[Special:UserLogin|logg inn]] så vi unngår fremtidige forvekslinger med andre uregistrerte brukere.''",
+       "anontalkpagetext": "----\n''Dette er en diskusjonsside for en uregistrert bruker som ikke har opprettet konto eller ikke er logget inn.\nVi er derfor nødt til å bruke den numeriske IP-adressen til å identifisere ham eller henne.\nEn IP-adresse kan være delt mellom flere brukere.\nHvis du er en uregistrert bruker og synes at du har fått irrelevante kommentarer på en slik side, [[Special:CreateAccount|opprett en konto]] eller [[Special:UserLogin|logg inn]] så vi unngår fremtidige forvekslinger med andre uregistrerte brukere.''",
        "noarticletext": "Det er for tiden ingen tekst på denne siden.\nDu kan [[Special:Search/{{PAGENAME}}|søke etter denne sidetittelen]] på andre sider,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søke i relaterte logger],\neller [{{fullurl:{{FULLPAGENAME}}|action=edit}} opprette siden]</span>.",
        "noarticletext-nopermission": "Det er for tiden ingen tekst på denne siden.\nDu kan [[Special:Search/{{PAGENAME}}|søke etter sidens tittel]] blant andre sider, eller <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søke i relevante logger]</span>, men du har ikke tillatelse til å opprette denne siden.",
        "missing-revision": "Revisjonen #$1 av siden med navnet \"{{FULLPAGENAME}}\" eksisterer ikke.\n\nDette skyldes som regel at en gammel historikklenke er fulgt til en side som er slettet.\nDetaljer kan finnes i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} sletteloggen].",
        "upload-form-label-own-work": "Dette er mitt eget verk",
        "upload-form-label-infoform-categories": "Kategorier",
        "upload-form-label-infoform-date": "Dato",
-       "upload-form-label-own-work-message-local": "Jeg bekrefter at jeg ved å laste opp denne filen følger bruksvilkårene og lisensieringspolitikken på {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Hvis filen ikke lar seg laste opp under {{SITENAME}}s politikk må du lukke denne dialogboksen og prøve en annen metode.",
-       "upload-form-label-not-own-work-local-local": "Du kan eventuelt forsøke [[Special:Upload|den ordinære opplastingssiden]].",
-       "upload-form-label-own-work-message-default": "Jeg forstår at jeg laster opp denne filen til et delt arkiv. Jeg bekrefter at dette gjøres i tråd med bruksvilkårene og lisensieringspolitikken der.",
-       "upload-form-label-not-own-work-message-default": "Hvis filen ikke lar seg laste opp under arkivets politikk må du lukke denne dialogboksen og prøve en annen metode.",
-       "upload-form-label-not-own-work-local-default": "Du kan eventuelt forsøke [[Special:Upload|den ordinære opplastingssiden]] på {{SITENAME}} hvis filen kan lastes opp under politikken som gjelder der.",
-       "upload-form-label-own-work-message-shared": "Jeg bekrefter at jeg har opphavsretten til denne filen, samtykker til å ugjenkallelig slippe filen til Wikimedia Commons under lisensen [https://creativecommons.org/licenses/by-sa/4.0/deed.no Creative Commons Navngivelse-DelPåSammeVilkår 4.0], og samtykker til [https://wikimediafoundation.org/wiki/Terms_of_Use bruksvilkårene].",
-       "upload-form-label-not-own-work-message-shared": "Hvis du ikke sitter på opphavsretten til filen, eller ønsker å slippe den under en annen lisens, prøv [https://commons.wikimedia.org/wiki/Special:UploadWizard Opplastingsveiviseren på Commons].",
-       "upload-form-label-not-own-work-local-shared": "Du kan eventuelt forsøke [[Special:Upload|den ordinære opplastingssiden på {{SITENAME}}]] hvis filen kan lastes opp under politikken som gjelder der.",
+       "upload-form-label-own-work-message-generic-local": "Jeg bekrefter at jeg ved å laste opp denne filen følger bruksvilkårene og lisensieringspolitikken på {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Hvis filen ikke lar seg laste opp under {{SITENAME}}s politikk må du lukke denne dialogboksen og prøve en annen metode.",
+       "upload-form-label-not-own-work-local-generic-local": "Du kan eventuelt forsøke [[Special:Upload|den ordinære opplastingssiden]].",
+       "upload-form-label-own-work-message-generic-foreign": "Jeg forstår at jeg laster opp denne filen til et delt arkiv. Jeg bekrefter at dette gjøres i tråd med bruksvilkårene og lisensieringspolitikken der.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Hvis filen ikke lar seg laste opp under arkivets politikk må du lukke denne dialogboksen og prøve en annen metode.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Du kan eventuelt forsøke [[Special:Upload|den ordinære opplastingssiden]] på {{SITENAME}} hvis filen kan lastes opp under politikken som gjelder der.",
        "backend-fail-stream": "Kunne ikke strømme filen $1.",
        "backend-fail-backup": "Kunne ikke sikkerhetskopiere filen $1.",
        "backend-fail-notexists": "Filen $1 finnes ikke.",
index bee6f50..49d1068 100644 (file)
        "noname": "Je mutten n gebrukersnaam opgeven.",
        "loginsuccesstitle": "An-emeld",
        "loginsuccess": "Je bin noen an-emeld bie {{SITENAME}} as \"$1\".",
-       "nosuchuser": "Der is gien gebruker mit de naam \"$1\".\nGebrukersnamen bin heufdlettergeveulig.\nKiek de schriefwieze effen nao of [[Special:UserLogin/signup|maak n nieje gebruker an]].",
+       "nosuchuser": "Der is gien gebruker mit de naam \"$1\".\nGebrukersnamen bin heufdlettergeveulig.\nKiek de schriefwieze effen nao of [[Special:CreateAccount|maak n nieje gebruker an]].",
        "nosuchusershort": "Der is gien gebruker mit de naam \"$1\". Kiek de spelling nao.",
        "nouserspecified": "Vul n naam in",
        "login-userblocked": "Disse gebruker is eblokkeerd.\nJe kunnen niet anmelden.",
        "accmailtext": "Der is n willekeurig wachtwoord veur [[User talk:$1|$1]] verstuurd naor $2. t Kan ewiezigd wörden op de zied ''[[Special:ChangePassword|wachtwoord wiezigen]]'' naoda'j an-emeld bin.",
        "newarticle": "(Niej)",
        "newarticletext": "Disse zied besteet nog niet.\nIn t veld hieronder ku'j wat schrieven um disse zied an te maken (meer informasie vie'j op de [$1 hulpzied]).\nA'j hier per ongelok terechtekeumen bin gebruuk dan de knoppe '''veurige''' um weerumme te gaon.",
-       "anontalkpagetext": "---- ''Disse overlegzied heurt bie n anonieme gebruker die nog gien gebrukersnaam hef, of t niet gebruukt. We gebruken daorumme t IP-adres um hum of heur te herkennen, mer t kan oek ween dat meerdere personen t zelfde IP-adres gebruken, en da'j hiermee berichten ontvangen die niet veur joe bedoeld bin. A'j dit veurkoemen willen, dan ku'j t best [[Special:UserLogin/signup|n gebrukersnaam anmaken]] of [[Special:UserLogin|anmelden]].''",
+       "anontalkpagetext": "---- ''Disse overlegzied heurt bie n anonieme gebruker die nog gien gebrukersnaam hef, of t niet gebruukt. We gebruken daorumme t IP-adres um hum of heur te herkennen, mer t kan oek ween dat meerdere personen t zelfde IP-adres gebruken, en da'j hiermee berichten ontvangen die niet veur joe bedoeld bin. A'j dit veurkoemen willen, dan ku'j t best [[Special:CreateAccount|n gebrukersnaam anmaken]] of [[Special:UserLogin|anmelden]].''",
        "noarticletext": "Der steet noen gien tekste op disse zied.\nJe kunnen [[Special:Search/{{PAGENAME}}|de titel opzeuken]] in aandere ziejen,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zeuken in de logboeken],\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse zied anmaken]</span>.",
        "noarticletext-nopermission": "Op disse zied steet gien tekste.\nJe kunnen [[Special:Search/{{PAGENAME}}|zeuken naor disse term]] in aandere ziejen of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken deurzeuken]</span>, mer je hebben gien rechten um disse zied an te maken.",
        "missing-revision": "De versie #$1 van de zied \"{{FULLPAGENAME}} besteet niet.\n\nDit kömp meestentieds deur t volgen van n verouwerde verwiezing naor n zied die vortedaon is.\nWaorschienlik ku'j der meer gegevens over vienen in t [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} vortdologboek].",
        "sectioneditnotsupported-text": "Je kunnen op disse zied gien seksies bewarken.",
        "permissionserrors": "Gien toestemming",
        "permissionserrorstext": "Je maggen of kunnen dit niet doon. De {{PLURAL:$1|reden|redens}} daorveur {{PLURAL:$1|is|bin}}:",
-       "permissionserrorstext-withaction": "Je hebben gien rech um $2, mit de volgende {{PLURAL:$1|reden|redens}}:",
+       "permissionserrorstext-withaction": "Je hebben gien recht um $2, mit de volgende {{PLURAL:$1|reden|redens}}:",
        "recreate-moveddeleted-warn": "'''Waorschuwing: je maken n zied an die eerder al vortedaon is.'''\n\nBedenk eerst of t neudig is um disse zied veerder te bewarken.\nVeur de dudelikheid steet hieronder  t vortdologboek en t herneumlogboek veur disse zied:",
        "moveddeleted-notice": "Disse zied is vortedaon.\nHieronder steet de informasie uut t vortdologboek en t herneumlogboek.",
        "log-fulllog": "t Hele logboek bekieken",
        "sorbsreason": "Joew IP-adres is op-eneumen as open proxyserver in de zwarte lieste van DNS die'w veur {{SITENAME}} gebruken.",
        "sorbs_create_account_reason": "Joew IP-adres is op-eneumen as open proxyserver in de zwarte lieste van DNS, die'w veur {{SITENAME}} gebruken.\nJe kunnen gien gebrukerszied anmaken.",
        "xffblockreason": "n IP-adres dat jie gebruken is eblokkeerd. Dit steet in de kop 'X-Forwarded-For'. De oorspronkelike reden veur de blokkerings is: $1",
-       "cant-see-hidden-user": "De gebruker die'j proberen te blokkeren is al eblokkeerd en verbörgen.\nUmda'j gien rech hebben um gebrukers te verbargen, ku'j de blokkering van de gebruker niet bekieken of bewarken.",
+       "cant-see-hidden-user": "De gebruker die'j proberen te blokkeren is al eblokkeerd en verbörgen.\nUmda'j gien recht hebben um gebrukers te verbargen, ku'j de blokkering van de gebruker niet bekieken of bewarken.",
        "ipbblocked": "Je kunnen gien aandere gebrukers (de)blokkeren, umda'j zelf eblokkeerd bin",
        "ipbnounblockself": "Je maggen je eigen niet deblokkeren",
        "lockdb": "Databanke blokkeren",
index 37e4a51..aa9052c 100644 (file)
        "oct": "Okt",
        "nov": "Nov.",
        "dec": "Dez",
+       "january-date": "$1. Januar",
+       "february-date": "$1. Februar",
+       "march-date": "$1. März",
+       "april-date": "$1. April",
+       "may-date": "$1. Mai",
+       "june-date": "$1. Juni",
+       "july-date": "$1. Juli",
+       "august-date": "$1. August",
+       "september-date": "$1. September",
+       "october-date": "$1. Oktober",
+       "november-date": "$1. November",
+       "december-date": "$1. Dezember",
        "pagecategories": "{{PLURAL:$1|Kategorie|Kategorien}}",
        "category_header": "Sieden in de Kategorie „$1“",
        "subcategories": "Ünnerkategorien",
        "hidetoc": "Nich wiesen",
        "collapsible-collapse": "Versteken",
        "collapsible-expand": "Wiesen",
+       "confirmable-yes": "Jo",
+       "confirmable-no": "Nee",
        "thisisdeleted": "Ankieken oder weerholen vun $1?",
        "viewdeleted": "$1 ankieken?",
        "restorelink": "{{PLURAL:$1|ene löschte Version|$1 löschte Versionen}}",
        "virus-scanfailed": "Scan hett nich klappt (Code $1)",
        "virus-unknownscanner": "Unbekannten Virenscanner:",
        "logouttext": "'''Du büst nu afmellt.'''\n\nDu kannst {{SITENAME}} nu anonym wiederbruken oder di ünner dissen oder en annern Brukernaam wedder <span class='plainlinks'>[$1 anmellen]</span>.\nDenk dor an, dat welk Sieden ünner Ümstänn noch jümmer so wiest warrn köönt, as wenn du anmellt weerst. Dat ännert sik, wenn du den Cache vun dien Browser leddig maakst.",
+       "welcomeuser": "Willkamen, $1!",
        "yourname": "Dien Brukernaam",
        "userlogin-yourname": "Brukernaam",
        "yourpassword": "Dien Passwoort",
        "noname": "Du muttst en Brukernaam angeven.",
        "loginsuccesstitle": "Anmellen hett Spood",
        "loginsuccess": "Du büst nu as „$1“ bi {{SITENAME}} anmellt.",
-       "nosuchuser": "Den Brukernaam „$1“ gifft dat nich.\nBrukernaams maakt en Ünnerscheed twischen groot un lütt schrevene Bookstaven.\nKiek de Schrievwies na oder [[Special:UserLogin/signup|mell di as ne’en Bruker an]].",
+       "nosuchuser": "Den Brukernaam „$1“ gifft dat nich.\nBrukernaams maakt en Ünnerscheed twischen groot un lütt schrevene Bookstaven.\nKiek de Schrievwies na oder [[Special:CreateAccount|mell di as ne’en Bruker an]].",
        "nosuchusershort": "De Brukernaam „$1“ existeert nich. Prööv de Schrievwies.",
        "nouserspecified": "Du musst en Brukernaam angeven",
        "login-userblocked": "Disse Bruker is sperrt. Anmellen geiht nich.",
        "accmailtext": "En tofällig Passwoord för [[User talk:$1|$1]] is $2 tostüürt worrn.\n\nDat Passwoord för dit Brukerkonto kann na dat Anmellen ünner ''[[Special:ChangePassword|Passwoord ännern]]'' ännert warrn.",
        "newarticle": "(Nee)",
        "newarticletext": "Du büst op en Sied kamen, de dat noch nich gifft.\nWenn du disse Sied opstellen wullt, schriev dien Text in dat Finster ünnen  (kiek op de [$1 Hülpsied] för mehr Infos).\nWenn du de Sied gornich ännern wullst, denn klick op den '''Trügg'''-Knoop in dien Webkieker.",
-       "anontalkpagetext": "---- ''Dit is de Diskuschoonssiet vun en nich anmellt Bruker, de noch keen Brukerkonto anleggt hett oder dat jüst nich bruukt.\nWi mööt hier de numerische IP-Adress verwennen, üm den Bruker to identifizeern.\nSo en Adress kann vun verscheden Brukern bruukt warrn.\nWenn du en anonymen Bruker büst un meenst, dat disse Kommentaren nich an di richt sünd, denn [[Special:UserLogin/signup|legg di en Brukerkonto an]] oder [[Special:UserLogin|mell di an]], dat dat Problem nich mehr dor is.''",
+       "anontalkpagetext": "---- ''Dit is de Diskuschoonssiet vun en nich anmellt Bruker, de noch keen Brukerkonto anleggt hett oder dat jüst nich bruukt.\nWi mööt hier de numerische IP-Adress verwennen, üm den Bruker to identifizeern.\nSo en Adress kann vun verscheden Brukern bruukt warrn.\nWenn du en anonymen Bruker büst un meenst, dat disse Kommentaren nich an di richt sünd, denn [[Special:CreateAccount|legg di en Brukerkonto an]] oder [[Special:UserLogin|mell di an]], dat dat Problem nich mehr dor is.''",
        "noarticletext": "Dor is opstunns keen Text op disse Sied. Du kannst [[Special:Search/{{PAGENAME}}|na dissen Utdruck in annere Sieden söken]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} in de Logböker söken],\noder [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse Sied ännern]</span>.",
        "noarticletext-nopermission": "Disse Sied hett opstunns keen Text.\nDu kannst in annere Sieden [[Special:Search/{{PAGENAME}}|na dissen Titel söken]]\noder <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} in de Logböker söken]</span>, man du hest nich dat Recht, de Sied optostellen.",
        "userpage-userdoesnotexist": "Dat Brukerkonto „<nowiki>$1</nowiki>“ gifft dat noch nich. Överlegg, wat du disse Siet würklich nee opstellen/ännern wullt.",
        "expand_templates_ok": "Los",
        "expand_templates_remove_comments": "Kommentaren rutnehmen",
        "expand_templates_generate_xml": "XML-Parser-Boom wiesen",
-       "expand_templates_preview": "Vörschau"
+       "expand_templates_preview": "Vörschau",
+       "pagelang-language": "Spraak"
 }
index 1c581a8..abc600d 100644 (file)
        "noname": "तपाईंले सही प्रयोगकर्ता नाम दिनु भएन।",
        "loginsuccesstitle": "प्रवेश सफल",
        "loginsuccess": "'''तपाईंले {{SITENAME}}मा  \"$1\" को रुपमा प्रवेश गर्नुभएकोछ।'''",
-       "nosuchuser": "\"$1\" को नामबाट कुनै पनि प्रयोगकर्ता भेटिएनन् ।\nप्रयोगकर्ता नाम वर्णसंवेदनशील हुन्छन् ।\nहिज्जे जाँच्नुहोस् , या [[Special:UserLogin/signup|नयाँ खाता बनाउनुहोस्]].",
+       "nosuchuser": "\"$1\" को नामबाट कुनै पनि प्रयोगकर्ता भेटिएनन् ।\nप्रयोगकर्ता नाम वर्णसंवेदनशील हुन्छन् ।\nहिज्जे जाँच्नुहोस् , या [[Special:CreateAccount|नयाँ खाता बनाउनुहोस्]].",
        "nosuchusershort": " \"$1\" नामको कुनै पनि प्रयोगकर्ता भेटिएन।\n तपाईँको हिज्जे जाँच्नुहोस् ।",
        "nouserspecified": "तपाँईले प्रयोगकर्ताको नाम जनाउनुपर्छ।",
        "login-userblocked": "यस प्रयोगकर्तालाई रोक लगाइएको छ। प्रवेश गर्ने अनुमति छैन।",
        "accmailtext": "जथाभावीरूपमा सृजना गरिएको प्रवेशशब्द प्रयोगकर्ता [[User talk:$1|$1]] को  $2 मा पठाइएको छ।\n\nयो नयाँ खाताको प्रवेशशब्द  ''[[Special:ChangePassword|change password]]'' मा प्रवेश गरेर परिवर्तन गर्न सकिन्छ ।",
        "newarticle": "(नयाँ)",
        "newarticletext": "तपाईँले अहिले सम्म नभएको पृष्ठको लिंङ्क पहिल्याउनु भएको छ।\nयो पृष्ठ निर्माण गर्न तलको कोष्ठमा टाइप गर्नुहोस्  ।(थप जानकारीको लागि [$1 help page] हेर्नुहोस् )।\nयहाँ त्यत्तिकै आइपुग्नु भएको हो भने , ब्राउजरको  '''back''' बटन थिच्नुहोस ।",
-       "anontalkpagetext": "----''यो वार्तालाप पृष्ठ अज्ञात प्रयोगकर्ताको हो जसले अहिलेसम्म खाता बनाएकै छैन, अथवा जसले यस पृष्ठको उपयोग गर्दैन।\nयस कारण हामीले उसलाई उसको आइ पी (IP) ठेगानाले चिन्न सक्छौं। \nयस्तो आइ पी (IP) ठेगाना धेरै प्रयोगकर्ताहरूको साझा हुनसक्छ।\nयदि तपाईं अज्ञात प्रयोगकर्ता हुनुहुन्छ र तपाईंमाथि अचाहिँदो टिप्पणी भएको अनुभव गर्नुहुन्छ भने भविष्यमा अन्य अज्ञात प्रयोगकर्तासितको भ्रमबाट बाँच्न कृपया [[Special:UserLogin/signup|खाता खोल्नुहोस्]] अथवा [[Special:UserLogin|प्रवेश गर्नुहोस्]] ''",
+       "anontalkpagetext": "----''यो वार्तालाप पृष्ठ अज्ञात प्रयोगकर्ताको हो जसले अहिलेसम्म खाता बनाएकै छैन, अथवा जसले यस पृष्ठको उपयोग गर्दैन।\nयस कारण हामीले उसलाई उसको आइ पी (IP) ठेगानाले चिन्न सक्छौं। \nयस्तो आइ पी (IP) ठेगाना धेरै प्रयोगकर्ताहरूको साझा हुनसक्छ।\nयदि तपाईं अज्ञात प्रयोगकर्ता हुनुहुन्छ र तपाईंमाथि अचाहिँदो टिप्पणी भएको अनुभव गर्नुहुन्छ भने भविष्यमा अन्य अज्ञात प्रयोगकर्तासितको भ्रमबाट बाँच्न कृपया [[Special:CreateAccount|खाता खोल्नुहोस्]] अथवा [[Special:UserLogin|प्रवेश गर्नुहोस्]] ''",
        "noarticletext": "यस लेखमा अहिले केहि पनि पाठ छैन ।\nतपाईंले अन्य पृष्ठमा [[Special:Search/{{PAGENAME}}|यस पृष्ठको शीर्षकको लागि खोज]] गर्न सक्नुहुन्छ ।\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} पृष्ठ सम्बन्धित ढड्डामा खोज],\nवा [{{fullurl:{{FULLPAGENAME}}|action=edit}}  यसै पृष्ठलाई सम्पादन गर्ने]</span>.",
        "noarticletext-nopermission": "यस लेखमा अहिले कुनै पनि पाठ छैन ।\nतपाईंले अन्य पृष्ठमा [[Special:Search/{{PAGENAME}}|यस पृष्ठको शीर्षकको लागि खोज]] गर्न सक्नुहुन्छ,\nअथवा <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|पृष्ठ={{FULLPAGENAMEE}}}} सम्बन्धित लगहरू खोज्न सक्नुहुनेछ ]</span> तर तपाईंलाई नयाँ पृष्ठ बनाउने अधिकार छैन।",
        "missing-revision": "\"{{FULLPAGENAME}}\" पृष्ठको अवतरण #$1 रहेको छैन।\n\nसामान्य रूपमा यसो एउटा हटाइएको पृष्ठको पुरानो लिङ्कमा क्लिक गर्दा हुन्छ।\nअधिक जानकारीको लागि तपाईं [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} हटाएको लग] हेर्न सक्नुहुन्छ।",
index 53573c8..172abe7 100644 (file)
        "noname": "U hebt geen geldige gebruikersnaam opgegeven.",
        "loginsuccesstitle": "Aangemeld",
        "loginsuccess": "<strong>U bent nu aangemeld bij {{SITENAME}} als \"$1\".</strong>",
-       "nosuchuser": "De gebruiker \"$1\" bestaat niet.\nGebruikersnamen zijn hoofdlettergevoelig.\nControleer de schrijfwijze of [[Special:UserLogin/signup|maak een nieuw gebruiker aan]].",
+       "nosuchuser": "De gebruiker \"$1\" bestaat niet.\nGebruikersnamen zijn hoofdlettergevoelig.\nControleer de schrijfwijze of [[Special:CreateAccount|maak een nieuw gebruiker aan]].",
        "nosuchusershort": "De gebruiker \"$1\" bestaat niet.\nControleer de schrijfwijze.",
        "nouserspecified": "Geef een gebruikersnaam op.",
        "login-userblocked": "Deze gebruiker is geblokkeerd.\nAanmelden is niet mogelijk.",
        "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.",
-       "anontalkpagetext": "----\n<em>Deze overlegpagina hoort bij een anonieme gebruiker die geen gebruikersnaam heeft of deze niet gebruikt.</em>\nDaarom wordt het IP-adres ter identificatie gebruikt.\nHet is mogelijk dat meerdere personen hetzelfde IP-adres gebruiken.\nMogelijk ontvangt u hier berichten die niet voor u bedoeld zijn.\nAls u dat wilt voorkomen, [[Special:UserLogin/signup|registreer u]] of [[Special:UserLogin|meld u aan]] om verwarring met andere anonieme gebruikers te voorkomen.",
+       "anontalkpagetext": "----\n<em>Deze overlegpagina hoort bij een anonieme gebruiker die geen gebruikersnaam heeft of deze niet gebruikt.</em>\nDaarom wordt het IP-adres ter identificatie gebruikt.\nHet is mogelijk dat meerdere personen hetzelfde IP-adres gebruiken.\nMogelijk ontvangt u hier berichten die niet voor u bedoeld zijn.\nAls u dat wilt voorkomen, [[Special:CreateAccount|registreer u]] of [[Special:UserLogin|meld u aan]] om verwarring met andere anonieme gebruikers te voorkomen.",
        "noarticletext": "Deze pagina bevat geen tekst.\nU kunt [[Special:Search/{{PAGENAME}}|naar deze term zoeken]] in andere pagina's, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken doorzoeken] of [{{fullurl:{{FULLPAGENAME}}|action=edit}} deze pagina aanmaken]</span>.",
        "noarticletext-nopermission": "Deze pagina bevat geen tekst.\nU kunt [[Special:Search/{{PAGENAME}}|naar deze term zoeken]] in andere pagina's of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken doorzoeken]</span>, maar u mag de pagina niet aanmaken.",
        "missing-revision": "De versie #$1 van de pagina \"{{FULLPAGENAME}}\" bestaat niet.\n\nDit wordt meestal veroorzaakt door het volgen van een verouderde koppeling naar een pagina die is verwijderd.\nMeer gegevens zijn mogelijk te vinden in het [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} verwijderingslogboek].",
        "upload-form-label-own-work": "Dit is mijn eigen werk",
        "upload-form-label-infoform-categories": "Categorieën",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Ik bevestig dat ik dit bestand upload onder de voorwaarden en het licentiebeleid van {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Als u niet in staat bent dit bestand te uploaden onder het beleid van {{SITENAME}}, sluit dit venster dan alstublieft en kies een andere methode.",
-       "upload-form-label-not-own-work-local-local": "U kunt ook de [[Special:Upload|standaard uploadpagina]] gebruiken.",
-       "upload-form-label-own-work-message-default": "Ik begrijp dat ik dit bestand upload naar een gedeelde repository. Ik bevestig dat ik voldoe aan de voorwaarden en het licentiebeleid daar.",
-       "upload-form-label-not-own-work-message-default": "Als u niet in staat bent dit bestand te uploaden onder het beleid van de gedeelde repository, sluit dit venster dan alstublieft en kies een andere methode.",
-       "upload-form-label-not-own-work-local-default": "U kunt ook proberen de [[Special:Upload|uploadpagina van {{SITENAME}}]] te gebruiken als dit bestand geüpload kan worden onder hun beleid.",
-       "upload-form-label-own-work-message-shared": "Ik verklaar dat ik de auteursrechten bezit op dit bestand en ik ga onherroepbaar akkoord met het vrijgeven van dit bestand aan Wikimedia Commons onder de licentie [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Naamsvermelding-GelijkDelen 4.0] en ik ga akkoord met de [https://wikimediafoundation.org/wiki/Terms_of_Use Gebruiksvoorwaarden].",
-       "upload-form-label-not-own-work-message-shared": "Als u geen eigenaar bent van de auteursrechten van dit bestand, of als u het onder een andere licentie wilt vrijgeven, overweeg dan gebruik te maken van de [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard].",
-       "upload-form-label-not-own-work-local-shared": "U kunt ook de [[Special:Upload|uploadpagina op {{SITENAME}}]] gebruiken, als de site het uploaden van dit bestand onder hun beleid toestaat.",
+       "upload-form-label-own-work-message-generic-local": "Ik bevestig dat ik dit bestand upload onder de voorwaarden en het licentiebeleid van {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Als u niet in staat bent dit bestand te uploaden onder het beleid van {{SITENAME}}, sluit dit venster dan alstublieft en kies een andere methode.",
+       "upload-form-label-not-own-work-local-generic-local": "U kunt ook de [[Special:Upload|standaard uploadpagina]] gebruiken.",
+       "upload-form-label-own-work-message-generic-foreign": "Ik begrijp dat ik dit bestand upload naar een gedeelde repository. Ik bevestig dat ik voldoe aan de voorwaarden en het licentiebeleid daar.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Als u niet in staat bent dit bestand te uploaden onder het beleid van de gedeelde repository, sluit dit venster dan alstublieft en kies een andere methode.",
+       "upload-form-label-not-own-work-local-generic-foreign": "U kunt ook proberen de [[Special:Upload|uploadpagina van {{SITENAME}}]] te gebruiken als dit bestand geüpload kan worden onder hun beleid.",
        "backend-fail-stream": "Het was niet mogelijk het bestand \"$1\" te streamen.",
        "backend-fail-backup": "Het was niet mogelijk een reservekopie van het bestand $1 te maken.",
        "backend-fail-notexists": "Het bestand $1 bestaat niet.",
        "whatlinkshere-prev": "{{PLURAL:$1|vorige|vorige $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|volgende|volgende $1}}",
        "whatlinkshere-links": "← koppelingen",
-       "whatlinkshere-hideredirs": "doorverwijzingen $1",
-       "whatlinkshere-hidetrans": "Transclusies $1",
-       "whatlinkshere-hidelinks": "koppelingen $1",
+       "whatlinkshere-hideredirs": "Verberg doorverwijzingen",
+       "whatlinkshere-hidetrans": "Verberg transclusies",
+       "whatlinkshere-hidelinks": "Verberg links",
        "whatlinkshere-hideimages": "Bestandskoppelingen $1",
        "whatlinkshere-filters": "Filters",
        "whatlinkshere-submit": "OK",
        "mw-widgets-titleinput-description-redirect": "doorverwijzing naar $1",
        "api-error-blacklisted": "Kies een andere, beschrijvende naam.",
        "sessionmanager-tie": "Het is niet mogelijk om meerdere authenticatietypen voor verzoeken te combineren: $1.",
-       "sessionprovider-generic": "$1 sessies",
-       "sessionprovider-mediawiki-session-cookiesessionprovider": "sessies gebaseerd op cookies",
+       "sessionprovider-generic": "$1-sessies",
+       "sessionprovider-mediawiki-session-cookiesessionprovider": "op cookies gebaseerde sessies",
        "sessionprovider-nocookies": "Cookies kunnen uitgeschakeld zijn. Zorg ervoor dat u cookies hebt ingeschakeld en probeer het opnieuw.",
        "randomrootpage": "Willekeurige hoofdpagina",
        "log-action-filter-block": "Soort blokkade:",
index 2d415db..c5c1570 100644 (file)
        "noname": "Du har ikkje oppgjeve gyldig brukarnamn.",
        "loginsuccesstitle": "Du er no innlogga",
        "loginsuccess": "Du er no innlogga som «$1».",
-       "nosuchuser": "Det finst ikkje nokon brukar med brukarnamnet «$1».\nBrukarnamn skil mellom stor og liten bokstav. Sjekk at du har skrive brukarnamet rett eller [[Special:UserLogin/signup|opprett ein ny konto]].",
+       "nosuchuser": "Det finst ikkje nokon brukar med brukarnamnet «$1».\nBrukarnamn skil mellom stor og liten bokstav. Sjekk at du har skrive brukarnamet rett eller [[Special:CreateAccount|opprett ein ny konto]].",
        "nosuchusershort": "Det finst ikkje nokon brukar med brukarnamnet «$1». Sjekk at du har skrive rett.",
        "nouserspecified": "Du må oppgje eit brukarnamn.",
        "login-userblocked": "Denne brukaren er blokkert. Innlogging er ikkje tillate.",
        "accmailtext": "Eit tilfeldig laga passord for [[User talk:$1|$1]] er sendt til $2.\n\nPassordet for den nye kontoen kan verta endra på ''[[Special:ChangePassword|endra passord]]''-sida etter innlogging.",
        "newarticle": "(Ny)",
        "newarticletext": "Du har følgt ei lenkje til ei side som ikkje finst enno.\nFor å opprette sida, kan du skrive i boksen under (sjå [$1 hjelpesida] for meir informasjon).\nHamna du her ved ein feil, klikk på '''attende'''-knappen i nettlesaren din.",
-       "anontalkpagetext": "----''Dette er ei diskusjonsside for ein anonym brukar som ikkje har oppretta konto eller ikkje har logga inn.\nVi er difor nøydde til å bruke den numeriske IP-adressa til å identifisere brukaren. Same IP-adresse kan vere knytt til fleire brukarar. Om du er ein anonym brukar og meiner at du har fått irrelevante kommentarar på ei slik side, [[Special:UserLogin/signup|opprett ein brukarkonto]] eller [[Special:UserLogin|logg inn]] slik at vi unngår framtidige forvekslingar med andre anonyme brukarar.''",
+       "anontalkpagetext": "----''Dette er ei diskusjonsside for ein anonym brukar som ikkje har oppretta konto eller ikkje har logga inn.\nVi er difor nøydde til å bruke den numeriske IP-adressa til å identifisere brukaren. Same IP-adresse kan vere knytt til fleire brukarar. Om du er ein anonym brukar og meiner at du har fått irrelevante kommentarar på ei slik side, [[Special:CreateAccount|opprett ein brukarkonto]] eller [[Special:UserLogin|logg inn]] slik at vi unngår framtidige forvekslingar med andre anonyme brukarar.''",
        "noarticletext": "Det er nett no ikkje noko tekst på denne sida.\nDu kan [[Special:Search/{{PAGENAME}}|søkja etter sidetittelen]] i andre sider, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søkja i dei relaterte loggane]\neller [{{fullurl:{{FULLPAGENAME}}|action=edit}} endra denne sida]</span>.",
        "noarticletext-nopermission": "Der er nett no ikkje noko tekst på denne sida.\nDu kan [[Special:Search/{{PAGENAME}}|søkja etter sidetittelen]] i andre sider\neller <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} søkja i dei relaterte loggane]</span>, men du har ikkje løyve til å oppretta denne sida.",
        "missing-revision": "Versjonen #$1 av sida med namnet «{{FULLPAGENAME}}» finst ikkje.\n\nDette skriv seg som oftast frå at ei forelda historikklenkje vart fylgd til ei side som er sletta.\nDetaljar kan ein finna i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} sletteloggen].",
index 36a22e4..79c3ea9 100644 (file)
        "noname": "Gawa fana ka leina la mošomiši la go loka.",
        "loginsuccesstitle": "O tsene ka katlego",
        "loginsuccess": "'''Bjale o tsene go {{SITENAME}} bjalo ka \"$1\".'''",
-       "nosuchuser": "Ga gona mošomiši wa leina la \"$1\".\nMaina a huduetša ke ditlhaka.\nLebele mopeleto wa gago goba [[Special:UserLogin/signup|o tlhome mošomiši yo mophsa]].",
+       "nosuchuser": "Ga gona mošomiši wa leina la \"$1\".\nMaina a huduetša ke ditlhaka.\nLebele mopeleto wa gago goba [[Special:CreateAccount|o tlhome mošomiši yo mophsa]].",
        "nosuchusershort": "Ga gona mošomiši wa leina la \"$1\". Hlokomela mopeleto wa gago.",
        "nouserspecified": "O swanela ke go fana ka leina la mošomiši.",
        "wrongpassword": "O loketše ditlhaka-tša-siphiri tšeo e sego tšona. Ka kgopelo, leka gape.",
index e16a822..d67458b 100644 (file)
        "noname": "Avètz pas picat de nom d'utilizaire valid.",
        "loginsuccesstitle": "Identificacion capitada.",
        "loginsuccess": "Sètz actualament connectat(ada) sus {{SITENAME}} en tant que « $1 ».",
-       "nosuchuser": "L'utilizaire « $1 » existís pas.\nLo nom d'utilizaire es sensible a la cassa.\nVerificatz qu'avètz plan ortografiat lo nom, o [[Special:UserLogin/signup|creatz-vos un compte novèl]].",
+       "nosuchuser": "L'utilizaire « $1 » existís pas.\nLo nom d'utilizaire es sensible a la cassa.\nVerificatz qu'avètz plan ortografiat lo nom, o [[Special:CreateAccount|creatz-vos un compte novèl]].",
        "nosuchusershort": "I a pas de contributor amb lo nom « $1 ». Verificatz l’ortografia.",
        "nouserspecified": "Vos cal especificar vòstre nom d'utilizaire.",
        "login-userblocked": "Aqueste utilizaire es blocat. Connexion pas autorizada.",
        "accmailtext": "Un senhal generat aleatòriament per [[User talk:$1|$1]] es estat mandat a $2.\nLo senhal per aqueste compte novèl pòt èsser cambiat sus la pagina ''[[Special:ChangePassword|Cambiament de senhal]]'' aprèp s'èsser connectat.",
        "newarticle": "(Novèl)",
        "newarticletext": "Avètz seguit un ligam cap a una pagina qu’existís pas encara o qu'es estada [{{fullurl:Special:Log|type=delete&page={{FULLPAGENAMEE}}}} escafada].\nPer crear aquesta pagina, picatz vòstre tèxte dins la bóstia çaijós (podètz consultar [$1 la pagina d’ajuda] per mai d’entresenhas).\nSe sètz arribat(ada) aicí per error, clicatz sul boton '''retorn''' de vòstre navigador.",
-       "anontalkpagetext": "---- ''Sètz sus la pagina de discussion d'un utilizaire anonim qu'a pas encara creat un compte o que n'utiliza pas.\nPer aquesta rason, devèm utilizar son adreça IP per l'identificar. Una adreça d'aqueste tipe pòt èsser partejada entre mantun utilizaire. Se sètz un utilizaire anonim e se constatatz que de comentaris que vos concernisson pas vos son estats adreçats, podètz [[Special:UserLogin/signup|crear un compte]] o [[Special:UserLogin|vos connectar]] per evitar tota confusion venenta amb d’autres contributors anonims.''",
+       "anontalkpagetext": "---- ''Sètz sus la pagina de discussion d'un utilizaire anonim qu'a pas encara creat un compte o que n'utiliza pas.\nPer aquesta rason, devèm utilizar son adreça IP per l'identificar. Una adreça d'aqueste tipe pòt èsser partejada entre mantun utilizaire. Se sètz un utilizaire anonim e se constatatz que de comentaris que vos concernisson pas vos son estats adreçats, podètz [[Special:CreateAccount|crear un compte]] o [[Special:UserLogin|vos connectar]] per evitar tota confusion venenta amb d’autres contributors anonims.''",
        "noarticletext": "Pel moment, i a pas cap de tèxte sus aquesta pagina ;\npodètz [[Special:Search/{{PAGENAME}}|aviar una recèrca sul títol d'aqueste títol de pagina]] dins las autras pagina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} recercar dins las operacions ligadas]\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear aquesta pagina]</span>.",
        "noarticletext-nopermission": "Actualament i a pas cap de tèxte dins aquesta pagina.\nPodètz [[Special:Search/{{PAGENAME}}|far una recèrca sul títol de la pagina]] dins las autras paginas,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} recercar dins los jornals associats]</span>.",
        "missing-revision": "La revision n° $1 de la pagina intitulada « {{FULLPAGENAME}} » existís pas.\n\nAquò se produsís en general en seguent un ligam istoric obsolèt cap a una pagina qu'es estada suprimida.\nPodètz trobar mai de detalhs dins lo [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} jornal de las supressions].",
index d711ecd..3d5bc9c 100644 (file)
        "noname": "ଆପଣ ଗୋଟିଏ ବୈଧ ଇଉଜର ନାମ ଦେଇନାହାନ୍ତି ।",
        "loginsuccesstitle": "ଠିକଭାବେ ଲଗ-ଇନ ହେଲା",
        "loginsuccess": "'''ଆପଣ {{SITENAME}}ରେ \"$1\" ନାମରେ ଲଗ-ଇନ କରିଛନ୍ତି ।'''",
-       "nosuchuser": "\"$1\" ନାମରେ କେହି ଜଣେ ବି ସଭ୍ୟ ନାହାନ୍ତି ।\nଇଉଜର ନାମ ଇଂରାଜୀ ଛୋଟ ଓ ବଡ଼ ଅକ୍ଷର ପ୍ରତି ସମ୍ବେଦନଶୀଳ ।\nଆପଣ ନିଜର ବନାନ ପରଖି ନିଅନ୍ତୁ, ଅଥବା [[Special:UserLogin/signup|ନୂଆ ଖାତାଟିଏ ତିଆରି କରନ୍ତୁ]] ।",
+       "nosuchuser": "\"$1\" ନାମରେ କେହି ଜଣେ ବି ସଭ୍ୟ ନାହାନ୍ତି ।\nଇଉଜର ନାମ ଇଂରାଜୀ ଛୋଟ ଓ ବଡ଼ ଅକ୍ଷର ପ୍ରତି ସମ୍ବେଦନଶୀଳ ।\nଆପଣ ନିଜର ବନାନ ପରଖି ନିଅନ୍ତୁ, ଅଥବା [[Special:CreateAccount|ନୂଆ ଖାତାଟିଏ ତିଆରି କରନ୍ତୁ]] ।",
        "nosuchusershort": "\"$1\" ନାମରେ କେହି ଜଣେ ବି ସଭ୍ୟ ନାହାନ୍ତି ।\nଆପଣ ବନାନ ପରଖି ନିଅନ୍ତୁ ।",
        "nouserspecified": "ଆପଣଙ୍କୁ ଇଉଜର ନାମଟିଏ ଦେବାକୁ ପଡ଼ିବ ।",
        "login-userblocked": "ଏହି ସଭ୍ୟଙ୍କୁ ଅଟକାଯାଇଛି । ଲଗ ଇନ କରିବାକୁ ଅନୁମତି ନାହିଁ ।",
        "accmailtext": "[[User talk:$1|$1]] ପାଇଁ $2କୁ ଏକ ଆପେ ଆପେ ତିଆରି ପାସୱାର୍ଡ଼ ପଠାଗଲା । ଏହା ଲଗ ଇନ କଲା ପରେ<em>[[Special:ChangePassword|ପାସୱାର୍ଡ଼ ବଦଳ]]</em> ପୃଷ୍ଠାରେ ବଦଳାଯାଇପାରିବ ।",
        "newarticle": "(ନୁଆ)",
        "newarticletext": "ଆପଣ ଖୋଲିଥିବା ଲିଙ୍କଟିରେ ଏଯାଏଁ କିଛିବି ପୃଷ୍ଠା ନାହିଁ ।\nଏହି ପୃଷ୍ଠାଟିକୁ ତିଆରି କରିବା ପାଇଁ ତଳ ବାକ୍ସରେ ଟାଇପ କରନ୍ତୁ (ଅଧିକ ଜାଣିବା ପାଇଁ [$1 ସାହାଯ୍ୟ ପୃଷ୍ଠା] ଦେଖନ୍ତୁ) ।\nଯଦି ଆପଣ ଏଠାକୁ ଭୁଲରେ ଆସିଯାଇଥାନ୍ତି ତେବେ ଆପଣଙ୍କ ବ୍ରାଉଜରର '''Back''' ପତିଟି ଦବାନ୍ତୁ ।",
-       "anontalkpagetext": "----''ଏହା ଏକ ଖାତା ଖୋଲିନଥିବା ବା ଖାତା ବ୍ୟବହାର କରିନଥିବା ଜଣେ ବେନାମି ସଭ୍ୟଙ୍କର ଆଲୋଚନା ପୃଷ୍ଠା ।''\nତେଣୁ ଆମ୍ଭେ ସଂଖ୍ୟା ଦେଇ ସୂଚୀତ IP ଠିକଣା ଦେଇ ତାଙ୍କୁ ଜାଣିବା ।\nଏହି ପ୍ରକାରର ଗୋଟିଏ IP ଠିକଣା ବହୁ ସଭ୍ୟଙ୍କ ଦେଇ ବ୍ୟବହାର କରାଯାଇପାରେ । \nଯଦି ଆପଣ ଜଣେ ଅଜଣା ସଭ୍ୟ ଓ ଭାବୁଛନ୍ତି ଇଆଡୁ ସିଆଡୁ ମତାମତ ସବୁ ଆପଣଙ୍କ ପାଇଁ ଦିଆଯାଇଛି ତେବେ ଦୟାକରି [[Special:UserLogin/signup|ନୂଆ ଖାତାଟିଏ ଖୋଲନ୍ତୁ]] କିମ୍ବା [[Special:UserLogin|ଆଗରୁ ଥିବା ଖାତାରେ ଲଗ ଇନ କରନ୍ତୁ]] ଯାହା ବେନାମି ସଭ୍ୟଙ୍କୁ ନେଇ ଉପୁଜିଥିବା ଦ୍ଵନ୍ଦର ସମାଧାନ କରିବ ।",
+       "anontalkpagetext": "----''ଏହା ଏକ ଖାତା ଖୋଲିନଥିବା ବା ଖାତା ବ୍ୟବହାର କରିନଥିବା ଜଣେ ବେନାମି ସଭ୍ୟଙ୍କର ଆଲୋଚନା ପୃଷ୍ଠା ।''\nତେଣୁ ଆମ୍ଭେ ସଂଖ୍ୟା ଦେଇ ସୂଚୀତ IP ଠିକଣା ଦେଇ ତାଙ୍କୁ ଜାଣିବା ।\nଏହି ପ୍ରକାରର ଗୋଟିଏ IP ଠିକଣା ବହୁ ସଭ୍ୟଙ୍କ ଦେଇ ବ୍ୟବହାର କରାଯାଇପାରେ । \nଯଦି ଆପଣ ଜଣେ ଅଜଣା ସଭ୍ୟ ଓ ଭାବୁଛନ୍ତି ଇଆଡୁ ସିଆଡୁ ମତାମତ ସବୁ ଆପଣଙ୍କ ପାଇଁ ଦିଆଯାଇଛି ତେବେ ଦୟାକରି [[Special:CreateAccount|ନୂଆ ଖାତାଟିଏ ଖୋଲନ୍ତୁ]] କିମ୍ବା [[Special:UserLogin|ଆଗରୁ ଥିବା ଖାତାରେ ଲଗ ଇନ କରନ୍ତୁ]] ଯାହା ବେନାମି ସଭ୍ୟଙ୍କୁ ନେଇ ଉପୁଜିଥିବା ଦ୍ଵନ୍ଦର ସମାଧାନ କରିବ ।",
        "noarticletext": "ଏହି ପୃଷ୍ଠାଟିରେ କିଛି ବି ଲେଖା ନାହିଁ ।\nଆପଣ [[Special:Search/{{PAGENAME}}|ଏହି ଲେଖାଟିର ନାଆଁ]] ବାକି ପୃଷ୍ଠାମାନଙ୍କରେ ଖୋଜି ପାରନ୍ତି,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}ରେ ଯୋଡ଼ାଯାଇଥିବା ବାକି ପୃଷ୍ଠାସବୁକୁ ଖୋଜି ପାରନ୍ତି],\nକିମ୍ବା [{{fullurl:{{FULLPAGENAME}}|action=edit}} ଏହି ପୃଷ୍ଠାଟିକୁ ବଦଳାଇ ପାରନ୍ତି]</span> ।",
        "noarticletext-nopermission": "ଏବେ ଏହି ପୃଷ୍ଠାଟିରେ କିଛି ବି ଲେଖା ନାହିଁ ।\nଆପଣ [[Special:Search/{{PAGENAME}}|ଏହି ଲେଖାଟିର ନାଆଁ]] ବାକି ପୃଷ୍ଠାମାନଙ୍କରେ ଖୋଜି ପାରନ୍ତି, କିମ୍ବା\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}ରେ ଯୋଡ଼ାଯାଇଥିବା ବାକି ପୃଷ୍ଠାସବୁକୁ ଖୋଜି ପାରନ୍ତି]\n</span>, କିନ୍ତୁ ଏହି ପୃଷ୍ଠାଟିକୁ ଆପଣ ତିଆରି କରିପାରିବେ ନାହିଁ ।",
        "missing-revision": "\"{{FULLPAGENAME}}\" ନାମରେ ଥିବା ପୃଷ୍ଠାଟିର #$1 ପୁନରାବୃତ୍ତି ନାହିଁ ।\n\nପୁରୁଣା ହୋଇଯାଇଥିବା ଇତିହାସ ଲିଙ୍କ ଯାହା ଏକ ଲିଭାଯାଇଥିବା ପୃଷ୍ଠାକୁ ଦିଆଯାଇଥିବାରୁ ଏହା ସାଧାରଣତଃ ହୋଇଥାଏ ।\nଅଧିକ ବିବରଣୀ [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log]ରେ ମିଳିପାରିବ ।",
        "preferences": "ପସନ୍ଦ",
        "mypreferences": "ପସନ୍ଦ",
        "prefs-edits": "ସମ୍ପାଦନା ସଂଖ୍ୟା:",
-       "prefsnologintext2": "ନିà¬\9cର à¬ªà¬¸à¬¨à­\8dଦ à¬¬à¬¦à¬²ାଇବା ପାଇଁ ଲଗ ଇନ କରନ୍ତୁ ।",
+       "prefsnologintext2": "ନିà¬\9cର à¬ªà¬¸à¬¨à­\8dଦ à¬¬à¬¦à¬³ାଇବା ପାଇଁ ଲଗ ଇନ କରନ୍ତୁ ।",
        "prefs-skin": "ବହିରାବରଣ",
        "skin-preview": "ସାଇତା ଆଗରୁ ଦେଖଣା",
        "datedefault": "କୌଣସି ପସନ୍ଦ ନାହିଁ",
index 460665e..76d9abf 100644 (file)
        "noname": "Раст фæсномыг нæ ныффыстай.",
        "loginsuccesstitle": "Бахызтæ",
        "loginsuccess": "'''Ныр ды дæ хыст {{grammar:genitive|{{SITENAME}}}} куыд \"$1\".'''",
-       "nosuchuser": "Нæй ахæм архайæг \"$1\" номимæ.\nАрхайджыты нæмттæ хатынц дамгъæты регистр.\nСбæрæг æй кæн, раст ныффыстай ном, æви [[Special:UserLogin/signup|бакæн ног аккаунт]].",
+       "nosuchuser": "Нæй ахæм архайæг \"$1\" номимæ.\nАрхайджыты нæмттæ хатынц дамгъæты регистр.\nСбæрæг æй кæн, раст ныффыстай ном, æви [[Special:CreateAccount|бакæн ног аккаунт]].",
        "nosuchusershort": "Нæй архайæг \"$1\" фæсномыгимæ.\nФен, фæсномыг раст ныффыстай, æви нæ.",
        "nouserspecified": "Ды хъуамæ зæгъай дæ фæсномыг.",
        "login-userblocked": "Ацы архайæг хъодыгонд у. Нæй гæнæн бахизын.",
        "accmailtext": "[[User talk:$1|{{grammar:dative|$1}}]] халæй ист пароль æрвыст æрцыд ацы адрисмæ: $2. Ацы ног аккаунты пароль гæнæн ис фæивын <em>[[Special:ChangePassword|пароль ивæн фарсыл]]</em> бахизыны фæстæ.",
        "newarticle": "(Ног)",
        "newarticletext": "Ды ныххæцыдтæ ахæм æрвитæныл, кæй фарс нырмæ нæй.\nФарс бакæнынæн байдай фыссын дæлдæр цы къæртт ис, уым (кæс [$1 æххуысы фарс] фылдæр базонынæн).",
-       "anontalkpagetext": "----''Ай у æнæном архайæджы ныхасы фарс. Ацы архайæг нырмæ нæ срегистраци кодта, кæнæ та йæ аккаунтæй нæ архайы.\nУый тыххæй мах пайда кæнæм йæ IP адрисæй, цæмæй-иу æй бæрæг кæнæм.\nАхæм IP адристæй гæнæн ис архайой цалдæр архайæджы.\nКæд ды æнæном архайæг дæ æмæ дæм цыдæр зæгъæлы фыстæджытæ цæуы, уæд, дæ хорзæхæй, [[Special:UserLogin/signup|бакæн аккаунт]] кæнæ [[Special:UserLogin|бахиз системæмæ]], цæмæй дæ мауал хæццæ кæной æндæр æнæном архайджытимæ.''",
+       "anontalkpagetext": "----''Ай у æнæном архайæджы ныхасы фарс. Ацы архайæг нырмæ нæ срегистраци кодта, кæнæ та йæ аккаунтæй нæ архайы.\nУый тыххæй мах пайда кæнæм йæ IP адрисæй, цæмæй-иу æй бæрæг кæнæм.\nАхæм IP адристæй гæнæн ис архайой цалдæр архайæджы.\nКæд ды æнæном архайæг дæ æмæ дæм цыдæр зæгъæлы фыстæджытæ цæуы, уæд, дæ хорзæхæй, [[Special:CreateAccount|бакæн аккаунт]] кæнæ [[Special:UserLogin|бахиз системæмæ]], цæмæй дæ мауал хæццæ кæной æндæр æнæном архайджытимæ.''",
        "noarticletext": "Ацы фарсы нырмæ текст нæй.\nДæ бон у [[Special:Search/{{PAGENAME}}|бацагурын ацы фарсы ном]] æндæр фæрсты,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} агурын йæ кой логты],\nкæнæ [{{fullurl:{{FULLPAGENAME}}|action=edit}} скæнын ацы фарс]</span>.",
        "noarticletext-nopermission": "Ацы фарсы нырмæ текст нæй.\nДæ бон у [[Special:Search/{{PAGENAME}}|бацагурын ацы фарсы ном]] æндæр фæрсты, кæнæ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} агурын йæ кой логты]</span>, фæлæ дын йæ саразыны бар нæй.",
        "missing-revision": "\"{{grammar:genitive|{{FULLPAGENAME}}}}\" фарсæн $1-æм фæлтæр нæй.\n\nАй арæх æрцæуы, исчи хафт фарсы зæронд историйы æрвитæны фæдыл куы ацæуы.\nФылдæр гæнæн ис базонын [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} хафыны логы].",
        "cachedspecial-refresh-now": "Фæстаг фенын.",
        "categories": "Категоритæ",
        "categoriespagetext": "Ацы {{PLURAL:$1|категорийы|категориты}} ис фæрстæ кæнæ файлтæ.\n[[Special:UnusedCategories|Нæ пайдагонд категоритæ]] æвдыст не сты.\nНоджы кæс [[Special:WantedCategories|хъæугæ категоритæ]].",
-       "special-categories-sort-count": "нымæцмæ гæсгæ равæр",
-       "special-categories-sort-abc": "алфавитмæ гæсгæ равæр",
        "sp-deletedcontributions-contribs": "бавæрд",
        "linksearch": "Æддаг æрвитæнтæ агурын",
        "linksearch-ns": "Номдон:",
index 65bcad1..1085b31 100644 (file)
        "noname": "ਤੁਸੀਂ ਇੱਕ ਸਹੀ ਯੂਜ਼ਰ-ਨਾਂ ਨਹੀਂ ਦਿੱਤਾ।",
        "loginsuccesstitle": "ਲਾਗਇਨ ਸਫ਼ਲ",
        "loginsuccess": "'''ਤੁਸੀਂ {{SITENAME}} ਉੱਤੇ \"$1\" ਵਜੋਂ ਲਾਗਇਨ ਹੋ ਚੁੱਕੇ ਹੋ।'''",
-       "nosuchuser": "!\"$1\" ਨਾਂ ਨਾਲ਼ ਕੋਈ ਵਰਤੋਂਕਾਰ ਨਹੀਂ ਹੈ। ਵੱਡੇ ਅਤੇ ਛੋਟੇ ਅੱਖਰ ਵਰਤਣ ਨਾਲ ਫ਼ਰਕ ਪੈਂਦਾ ਹੈ।\nਆਪਣੇ ਸਪੈਲਿੰਗ ਨੂੰ ਧਿਆਨ ਨਾਲ ਚੈੱਕ ਕਰੋ ਜਾਂ [[Special:UserLogin/signup|ਨਵਾਂ ਖਾਤਾ ਬਣਾਓ]]",
+       "nosuchuser": "!\"$1\" ਨਾਂ ਨਾਲ਼ ਕੋਈ ਵਰਤੋਂਕਾਰ ਨਹੀਂ ਹੈ। ਵੱਡੇ ਅਤੇ ਛੋਟੇ ਅੱਖਰ ਵਰਤਣ ਨਾਲ ਫ਼ਰਕ ਪੈਂਦਾ ਹੈ।\nਆਪਣੇ ਸਪੈਲਿੰਗ ਨੂੰ ਧਿਆਨ ਨਾਲ ਚੈੱਕ ਕਰੋ ਜਾਂ [[Special:CreateAccount|ਨਵਾਂ ਖਾਤਾ ਬਣਾਓ]]",
        "nosuchusershort": "\"$1\" ਨਾਂ ਨਾਲ ਕੋਈ ਵੀ ਵਰਤੋਂਕਾਰ ਨਹੀਂ ਹੈ। ਆਪਣੇ ਸਪੈਲਿੰਗ ਧਿਆਨ ਨਾਲ ਚੈੱਕ ਕਰੋ।",
        "nouserspecified": "ਤੁਹਾਨੂੰ ਇੱਕ ਯੂਜ਼ਰ-ਨਾਂ ਦੇਣਾ ਪਵੇਗਾ।",
        "login-userblocked": "ਇਹ ਯੂਜ਼ਰ-ਨਾਂ ਪਾਬੰਦੀਸ਼ੁਦਾ ਹੈ। ਲਾਗਇਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।",
        "accmailtext": "[[User talk:$1|$1]] ਲਈ ਰਲ਼ਵੇਂ ਤੌਰ ’ਤੇ ਬਣਿਆ ਪਾਸਵਰਡ $2 ਨੂੰ ਭੇਜਿਆ ਜਾ ਚੁੱਕਾ ਹੈ।\nਇਸ ਨਵੇਂ ਖਾਤੇ ਲਈ ਲਾਗਇਨ ਕਰਨ ਤੋਂ ਬਾਅਦ ''[[Special:ChangePassword|ਪਾਸਵਰਡ ਬਦਲੋ]]'' ’ਤੇ ਜਾ ਕੇ ਪਾਸਵਰਡ ਬਦਲਿਆ ਜਾ ਸਕਦਾ ਹੈ।",
        "newarticle": "(ਨਵਾਂ)",
        "newarticletext": "ਤੁਸੀਂ ਕਿਸੇ ਅਜਿਹੇ ਸਫ਼ੇ ਦੇ ਕੜੀ ’ਤੇ ਹੋ ਜੋ ਹਾਲੇ ਬਣਾਇਆ ਨਹੀਂ ਗਿਆ।\nਸਫ਼ਾ ਬਣਾਉਣ ਲਈ ਹੇਠ ਦਿੱਤੇ ਖਾਨੇ ਵਿਚ ਲਿਖਣਾ ਸ਼ੁਰੂ ਕਰੋ। (ਹੋਰ ਮਦਦ ਲਈ [$1 ਮਦਦ ਸਫ਼ਾ] ਵੇਖੋ।)\nਜੇ ਤੁਸੀਂ ਗ਼ਲਤੀ ਨਾਲ ਇੱਥੇ ਆਏ ਹੋ ਤਾਂ ਆਪਣੇ ਬ੍ਰਾਊਜ਼ਰ ਦੇ '''ਪਿੱਛੇ''' ਬਟਨ ’ਤੇ ਕਲਿੱਕ ਕਰੋ।",
-       "anontalkpagetext": "----''ਇਹ ਇਕ ਗੁਮਨਾਮ ਮੈਂਬਰ ਲਈ ਇਕ ਚਰਚਾ ਸਫ਼ਾ ਹੈ ਜਿਸਨੇ ਹਾਲੇ ਖਾਤਾ ਨਹੀ ਬਣਾਇਆ ਜਾਂ ਉਸਨੂੰ ਵਰਤ ਨਹੀਂ ਰਿਹਾ।\nਇਸ ਵਾਸਤੇ ਸਾਡੇ ਕੋਲ ਉਸਨੂੰ ਪਛਾਨਣ ਲਈ IP ਪਤਾ ਹੈ।\nਇਕ IP ਪਤਾ ਕਈ ਵਰਤਣ ਵਾਲ਼ਿਆਂ ਦੁਆਰਾ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ।\nਜੇ ਤੁਸੀਂ ਇੱਕ ਗੁਮਨਾਮ ਮੈਂਬਰ ਹੋ ਅਤੇ ਸਮਝਦੇ ਹੋ ਕਿ ਇਹ ਟਿੱਪਣੀਆਂ ਤੁਹਾਡੇ ਲਈ ਹਨ ਤਾਂ ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਹੋਰਾਂ ਗੁਮਨਾਮ ਮੈਂਬਰਾਂ ਨਾਲ਼ ਪੈਦਾ ਹੋਣ ਵਾਲ਼ੀ ਉਲਝਣ ਤੋਂ ਬਚਣ ਲਈ [[Special:UserLogin/signup|ਖਾਤਾ ਬਣਾਓ]] ਜਾਂ [[Special:UserLogin|ਲਾਗਇਨ ਕਰੋ]]।''",
+       "anontalkpagetext": "----''ਇਹ ਇਕ ਗੁਮਨਾਮ ਮੈਂਬਰ ਲਈ ਇਕ ਚਰਚਾ ਸਫ਼ਾ ਹੈ ਜਿਸਨੇ ਹਾਲੇ ਖਾਤਾ ਨਹੀ ਬਣਾਇਆ ਜਾਂ ਉਸਨੂੰ ਵਰਤ ਨਹੀਂ ਰਿਹਾ।\nਇਸ ਵਾਸਤੇ ਸਾਡੇ ਕੋਲ ਉਸਨੂੰ ਪਛਾਨਣ ਲਈ IP ਪਤਾ ਹੈ।\nਇਕ IP ਪਤਾ ਕਈ ਵਰਤਣ ਵਾਲ਼ਿਆਂ ਦੁਆਰਾ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ।\nਜੇ ਤੁਸੀਂ ਇੱਕ ਗੁਮਨਾਮ ਮੈਂਬਰ ਹੋ ਅਤੇ ਸਮਝਦੇ ਹੋ ਕਿ ਇਹ ਟਿੱਪਣੀਆਂ ਤੁਹਾਡੇ ਲਈ ਹਨ ਤਾਂ ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਹੋਰਾਂ ਗੁਮਨਾਮ ਮੈਂਬਰਾਂ ਨਾਲ਼ ਪੈਦਾ ਹੋਣ ਵਾਲ਼ੀ ਉਲਝਣ ਤੋਂ ਬਚਣ ਲਈ [[Special:CreateAccount|ਖਾਤਾ ਬਣਾਓ]] ਜਾਂ [[Special:UserLogin|ਲਾਗਇਨ ਕਰੋ]]।''",
        "noarticletext": "ਫ਼ਿਲਹਾਲ ਇਸ ਸਫ਼ੇ ’ਤੇ ਕੋਈ ਲਿਖਤ ਨਹੀਂ ਹੈ। ਤੁਸੀਂ ਦੂਜੇ ਸਫ਼ਿਆਂ ’ਤੇ [[Special:Search/{{PAGENAME}}|ਇਸ ਸਿਰਲੇਖ ਦੀ ਖੋਜ]] ਕਰ ਸਕਦੇ ਹੋ, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ਸਬੰਧਤ ਚਿੱਠੇ ਲੱਭ] ਸਕਦੇ ਹੋ ਜਾਂ ਇਸ [{{fullurl:{{FULLPAGENAME}}|action=edit}} ਸਫ਼ੇ ਵਿਚ ਲਿਖ] ਸਕਦੇ ਹੋ</span>।",
        "noarticletext-nopermission": "ਫ਼ਿਲਹਾਲ ਇਸ ਪੰਨੇ ’ਤੇ ਕੋਈ ਲਿਖਤ ਨਹੀਂ ਹੈ। ਤੁਸੀਂ ਦੂਸਰੇ ਪੰਨਿਆਂ ’ਤੇ [[Special:Search/{{PAGENAME}}|ਇਸ ਸਿਰਲੇਖ ਦੀ ਖੋਜ]] ਕਰ ਸਕਦੇ ਹੋ, ਸਬੰਧਤ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ਚਿੱਠੇ] ਖੋਜ ਸਕਦੇ ਹੋ ਜਾਂ [{{fullurl:{{FULLPAGENAME}}|action=edit}} ਇਸ ਪੰਨੇ ਵਿੱਚ ਲਿਖ] ਸਕਦੇ ਹੋ।</span>",
        "userpage-userdoesnotexist": "ਵਰਤੋਂਕਾਰ ਖਾਤਾ \"$1\" ਰਜਿਸਟਰ ਨਹੀਂ ਹੈ।\nਜੇ ਤੁਸੀਂ ਇਸਨੂੰ ਬਣਾਉਣਾ/ਸੋਧਣਾ ਚਾਹੁੰਦੇ ਹੋ ਤਾਂ ਮਿਰਬਾਨੀ ਕਰਕੇ ਜਾਂਚ ਕਰ ਲਓ।",
index dc4f956..b00ef21 100644 (file)
        "noname": "To nie jest poprawna nazwa użytkownika.",
        "loginsuccesstitle": "Zalogowano",
        "loginsuccess": "'''{{GENDER:|Zalogowałeś się|Zalogowałaś się|Zalogowano}} do {{GRAMMAR:D.lp|{{SITENAME}}}} jako „$1”.'''",
-       "nosuchuser": "Brak użytkownika o nazwie „$1”.\nW nazwie użytkownika ma znaczenie wielkość znaków.\nSprawdź pisownię lub [[Special:UserLogin/signup|utwórz nowe konto]].",
+       "nosuchuser": "Brak użytkownika o nazwie „$1”.\nW nazwie użytkownika ma znaczenie wielkość znaków.\nSprawdź pisownię lub [[Special:CreateAccount|utwórz nowe konto]].",
        "nosuchusershort": "Brak użytkownika o nazwie „$1”.\nSprawdź poprawność pisowni.",
        "nouserspecified": "Musisz podać nazwę użytkownika.",
        "login-userblocked": "Ten użytkownik jest zablokowany. Zalogowanie się jest niemożliwe.",
        "accmailtext": "Losowo wygenerowane hasło dla [[User talk:$1|$1]] zostało wysłane do $2.\n\nHasło dla tego nowego konta po zalogowaniu można zmienić na stronie ''[[Special:ChangePassword|zmiana hasła]]''.",
        "newarticle": "(Nowy)",
        "newarticletext": "Brak strony o tym tytule.\nJeśli chcesz ją utworzyć, wpisz treść strony w poniższym polu (więcej informacji odnajdziesz [$1 na stronie pomocy]).\nJeśli utworzenie nowej strony nie było Twoim zamiarem, wciśnij ''Wstecz'' w swojej przeglądarce.",
-       "anontalkpagetext": "---- ''To jest strona dyskusji anonimowego użytkownika – takiego, który nie ma jeszcze swojego konta lub nie chce go w tej chwili używać.\nBy go identyfikować, używamy adresów IP.\nJednak adres IP może być współdzielony przez wielu użytkowników.\nJeśli jesteś anonimowym użytkownikiem i uważasz, że zamieszczone tu komentarze nie są skierowane do Ciebie, [[Special:UserLogin/signup|utwórz konto]] lub [[Special:UserLogin|zaloguj się]] – dzięki temu unikniesz w przyszłości podobnych nieporozumień.''",
+       "anontalkpagetext": "---- ''To jest strona dyskusji anonimowego użytkownika – takiego, który nie ma jeszcze swojego konta lub nie chce go w tej chwili używać.\nBy go identyfikować, używamy adresów IP.\nJednak adres IP może być współdzielony przez wielu użytkowników.\nJeśli jesteś anonimowym użytkownikiem i uważasz, że zamieszczone tu komentarze nie są skierowane do Ciebie, [[Special:CreateAccount|utwórz konto]] lub [[Special:UserLogin|zaloguj się]] – dzięki temu unikniesz w przyszłości podobnych nieporozumień.''",
        "noarticletext": "Obecnie ta strona nie ma zawartości.\nMożesz [[Special:Search/{{PAGENAME}}|wyszukać ten tytuł na innych stronach]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przeszukać rejestr] \nlub [{{fullurl:{{FULLPAGENAME}}|action=edit}} utworzyć tę stronę]</span>.",
        "noarticletext-nopermission": "Ta strona nie posiada jeszcze zawartości.\nMożesz [[Special:Search/{{PAGENAME}}|wyszukać ten tytuł]] w treści innych stron\nlub <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przeszukać powiązane rejestry]</span>, ale nie masz uprawnień do utworzenia tej strony",
        "missing-revision": "Wersja #$1 strony \"{{FULLPAGENAME}}\" nie istnieje.\n\nZazwyczaj jest to spowodowane przestarzałym linkiem do usuniętej strony. Powód usunięcia znajduje się w [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rejestrze].",
        "right-override-export-depth": "Eksport stron wraz z linkowanymi do głębokości 5 linków",
        "right-sendemail": "Wysyłanie e‐maili do innych użytkowników",
        "right-passwordreset": "Sprawdzanie treści e‐maila o resetowaniu hasła",
-       "right-managechangetags": "Tworzenie i usuwanie [[Special:Tags|znaczników]] z bazy danych",
+       "right-managechangetags": "Tworzenie i (dez)aktywowanie [[Special:Tags|znaczników]]",
        "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",
+       "right-deletechangetags": "Usuwanie [[Special:Tags|znaczników]] z bazy danych",
        "grant-group-page-interaction": "Interakcja ze stronami",
        "grant-group-file-interaction": "Interakcja z plikami multimedialnymi",
        "grant-group-watchlist-interaction": "Interakcja z listą obserwowanych",
        "action-viewmyprivateinfo": "zobaczenia swoich prywatnych danych",
        "action-editmyprivateinfo": "edycji swoich prywatnych danych",
        "action-editcontentmodel": "edycji modelu zawartości strony",
-       "action-managechangetags": "utwórz lub usuń znaczniki z bazy danych",
+       "action-managechangetags": "tworzenia i de(aktywowania) znaczników",
        "action-applychangetags": "wprowadzania znaczników wraz z własnymi zmianami",
        "action-changetags": "dodawania i usuwania dowolnych znaczników z poszczególnych wersji i wpisów w rejestrze",
+       "action-deletechangetags": "usuwania znaczników z bazy danych",
        "nchanges": "$1 {{PLURAL:$1|zmiana|zmiany|zmian}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od ostatniej wizyty}}",
        "enhancedrc-history": "historia",
        "upload-form-label-own-work": "To moja własna praca",
        "upload-form-label-infoform-categories": "Kategorie",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Potwierdzam, że wysyłam ten plik zgodnie z warunkami i zasadami licencjonowania obowiązującymi na {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Jeśli nie możesz wysłać tego pliku zgodnie z zasadami obowiązującymi na {{SITENAME}}, zamknij ten komunikat i spróbuj innej metody.",
-       "upload-form-label-not-own-work-local-local": "Możesz skorzystać też z [[Special:Upload|domyślnej strony przesyłania plików]].",
-       "upload-form-label-own-work-message-default": "Rozumiem, że przesyłam ten plik do współdzielonego repozytorium. Potwierdzam, że robię to zgodnie z warunkami i zasadami licencjonowania tam obowiązującymi.",
-       "upload-form-label-not-own-work-message-default": "Jeśli nie jesteś w stanie przesłać tego pliku zgodnie z zasadami współdzielonego repozytorium, zamknij to okno i spróbuj innej metody.",
-       "upload-form-label-not-own-work-local-default": "Możesz spróbować użyć [[Special:Upload|strony przesyłania plików {{GRAMMAR:D.lp|{{SITENAME}}}}]], jeżeli zasady tej strony dopuszczają publikację tego pliku.",
-       "upload-form-label-own-work-message-shared": "Oświadczam, że mam prawa autorskie do tego pliku, nieodwołalnie publikuję go na Wikimedia Commons na licencji [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0] i zgadzam się na [https://wikimediafoundation.org/wiki/Terms_of_Use/pl warunki użytkowania].",
-       "upload-form-label-not-own-work-message-shared": "Jeżeli nie masz praw autorskich do tego pliku albo chcesz go opublikować na innej licencji, rozważ użycie [https://commons.wikimedia.org/wiki/Special:UploadWizard kreatora przesyłania plików].",
-       "upload-form-label-not-own-work-local-shared": "Możesz spróbować użyć [[Special:Upload|strony przesyłania plików {{GRAMMAR:D.lp|{{SITENAME}}}}]], jeżeli zasady tej strony dopuszczają publikację tego pliku.",
+       "upload-form-label-own-work-message-generic-local": "Potwierdzam, że wysyłam ten plik zgodnie z warunkami i zasadami licencjonowania obowiązującymi na {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Jeśli nie możesz wysłać tego pliku zgodnie z zasadami obowiązującymi na {{SITENAME}}, zamknij ten komunikat i spróbuj innej metody.",
+       "upload-form-label-not-own-work-local-generic-local": "Możesz skorzystać też z [[Special:Upload|domyślnej strony przesyłania plików]].",
+       "upload-form-label-own-work-message-generic-foreign": "Rozumiem, że przesyłam ten plik do współdzielonego repozytorium. Potwierdzam, że robię to zgodnie z warunkami i zasadami licencjonowania tam obowiązującymi.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Jeśli nie jesteś w stanie przesłać tego pliku zgodnie z zasadami współdzielonego repozytorium, zamknij to okno i spróbuj innej metody.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Możesz spróbować użyć [[Special:Upload|strony przesyłania plików {{GRAMMAR:D.lp|{{SITENAME}}}}]], jeżeli zasady tej strony dopuszczają publikację tego pliku.",
        "backend-fail-stream": "Nie można odczytać pliku $1.",
        "backend-fail-backup": "Nie można utworzyć kopii zapasowej pliku  $1 .",
        "backend-fail-notexists": "Plik  $1  nie istnieje.",
        "whatlinkshere-prev": "{{PLURAL:$1|poprzednie|poprzednie $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|następne|następne $1}}",
        "whatlinkshere-links": "← linkujące",
-       "whatlinkshere-hideredirs": "$1 przekierowania",
-       "whatlinkshere-hidetrans": "$1 dołączenia",
-       "whatlinkshere-hidelinks": "$1 linki",
-       "whatlinkshere-hideimages": "$1 linki z plików",
+       "whatlinkshere-hideredirs": "Ukryj przekierowania",
+       "whatlinkshere-hidetrans": "Ukryj dołączenia",
+       "whatlinkshere-hidelinks": "Ukryj linki",
+       "whatlinkshere-hideimages": "Ukryj linki z plików",
        "whatlinkshere-filters": "Filtry",
        "whatlinkshere-submit": "Dalej",
        "autoblockid": "automatyczna blokada nr $1",
index f127930..68b47a3 100644 (file)
        "noname": "A l'ha nen ëspessificà në stranòm vàlid.",
        "loginsuccesstitle": "Compliment! A l'é pen-a rintrà ant ël sistema.",
        "loginsuccess": "'''Adess a l'é colegà a {{SITENAME}} con lë stranòm «$1».'''",
-       "nosuchuser": "A-i é pa gnun utent con lë stranòm «$1».\nJë stranòm ëd j'utent a son sensìbij a le majùscole.\nCh'a contròla ël nòm che a l'ha batù, o [[Special:UserLogin/signup|ch'a crea un neuv cont]].",
+       "nosuchuser": "A-i é pa gnun utent con lë stranòm «$1».\nJë stranòm ëd j'utent a son sensìbij a le majùscole.\nCh'a contròla ël nòm che a l'ha batù, o [[Special:CreateAccount|ch'a crea un neuv cont]].",
        "nosuchusershort": "A-i é pa gnun utent che as ciama «$1». Për piasì, che a contròla se a l'ha scrit tut giust.",
        "nouserspecified": "A venta che a specìfica në stranòm d'utent",
        "login-userblocked": "St'utent-sì a l'é blocà. A peul pa intré ant ël sistema.",
        "accmailtext": "Na ciav generà a l'ancàpit për [[User talk:$1|$1]] a l'é stàita mandà a $2.\nA peul esse modificà an sla pàgina ''[[Special:ChangePassword|modìfica dla ciav]]'' apress esse rintrà ant ël sistema.",
        "newarticle": "(Neuv)",
        "newarticletext": "A l'é andaje dapress a na liura a na pàgina che a esist ancor nen.\nPër creé la pàgina, ch'a ancamin-a a scrive ant lë spassi sì-sota (vëdde la [$1 pàgina d'agiut] për savèjne ëd pì).\nS'a l'é rivà sì për eror, ch'a sgnaca ël boton '''andaré''' ëd sò navigador.",
-       "anontalkpagetext": "----''Costa a l'é la pàgina ëd ciaciarade për n'utent anònim che a l'é ancó pa dorbusse un cont, ò pura che a lo deuvra nen. Alora i l'oma da dovré ël nùmer d'adrëssa IP për deje n'identificassion a chiel o chila.\nN'adrëssa IP përparèj a peul esse partagià da vàire utent.\nSe chiel a l'é n'utent anònim e a l'ha l'impression d'arsèive dij coment sensa sust, për piasì [[Special:UserLogin/signup|ch'a crea un cont]] o [[Special:UserLogin|ch'a rintra ant ël sistema]] për evité dë fé confusion con d'àutri utent anònim.''",
+       "anontalkpagetext": "----''Costa a l'é la pàgina ëd ciaciarade për n'utent anònim che a l'é ancó pa dorbusse un cont, ò pura che a lo deuvra nen. Alora i l'oma da dovré ël nùmer d'adrëssa IP për deje n'identificassion a chiel o chila.\nN'adrëssa IP përparèj a peul esse partagià da vàire utent.\nSe chiel a l'é n'utent anònim e a l'ha l'impression d'arsèive dij coment sensa sust, për piasì [[Special:CreateAccount|ch'a crea un cont]] o [[Special:UserLogin|ch'a rintra ant ël sistema]] për evité dë fé confusion con d'àutri utent anònim.''",
        "noarticletext": "Al moment costa pàgina a l'é veuida.\nA peul [[Special:Search/{{PAGENAME}}|sërché cost tìtol]] andrinta a d'àutre pàgine, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sërché ant ij registr colegà],\no purament [{{fullurl:{{FULLPAGENAME}}|action=edit}} modìfiché sta pàgina]</span>.",
        "noarticletext-nopermission": "Al moment a-i é gnun test ansima a costa pàgina.\nA peul [[Special:Search/{{PAGENAME}}|sërché ës tìtol ëd pàgina]] an d'àutre pàgine,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sërché ant j'argistr colegà]</span>, ma a l'ha pa ël përmess ëd creé costa pàgina.",
        "missing-revision": "La revision nùmer $1 dla pàgina antitolà «{{FULLPAGENAME}}» a esist pa.\n\nSòn a l'é normalment causà da l'andèje dapress a na vej liura stòrica a na pàgina ch'a l'é stàita scancelà. Ij detaj a peulo esse trovà ant ël [registr ëd jë scancelament ëd {{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}}].",
        "whatlinkshere-prev": "{{PLURAL:$1|d'un andré|andré ëd $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|d'un anans|anans ëd $1}}",
        "whatlinkshere-links": "← anliure",
-       "whatlinkshere-hideredirs": "$1 le ridiression",
-       "whatlinkshere-hidetrans": "$1 anclusion",
-       "whatlinkshere-hidelinks": "$1 anliura",
+       "whatlinkshere-hideredirs": "Stërmé ridiression",
+       "whatlinkshere-hidetrans": "Stërmé transclusion",
+       "whatlinkshere-hidelinks": "Stërmé anliure",
        "whatlinkshere-hideimages": "$1 j'archivi lijà",
        "whatlinkshere-filters": "Filtr",
        "autoblockid": "Blocagi automàtich #$1",
        "javascripttest": "Preuva ëd JavaScript",
        "javascripttest-pagetext-unknownaction": "Assion nen conossùa «$1».",
        "javascripttest-qunit-intro": "Vëdde [$1 la documentassion dle preuve] dzora a mediawiki.org.",
-       "tooltip-pt-userpage": "Soa pàgina utent",
+       "tooltip-pt-userpage": "{{GENDER:|Soa}} pàgina utent",
        "tooltip-pt-anonuserpage": "La pàgina utent për l'IP con ël qual chiel a contribuiss",
        "tooltip-pt-mytalk": "Soa pàgina ëd discussion e ciaciarade",
        "tooltip-pt-anontalk": "La pàgina ëd ciaciarade an sle contribussion da costa adrëssa IP",
        "tooltip-pt-preferences": "Coma che i veuj mia {{SITENAME}}.",
        "tooltip-pt-watchlist": "Lista dle pàgine che chiel as ten sot euj.",
-       "tooltip-pt-mycontris": "Lista ëd soe contribussion",
+       "tooltip-pt-mycontris": "Lista ëd {{GENDER:|soe}} contribussion",
        "tooltip-pt-login": "Un a l'é nen obligà a rintré ant al sistema, ma se a lo fa a l'é mej",
        "tooltip-pt-logout": "Seurte da",
        "tooltip-pt-createaccount": "I-j consejoma ëd creé un cont e ëd rintré ant ël sistema; però a l'é nen obligatòri",
index 3ef60e7..21de22e 100644 (file)
        "noname": "تسی کوئی پکا ورتن آلا ناں نئیں رکھ رۓ۔",
        "loginsuccesstitle": "تسی لاگن ہوگۓ او",
        "loginsuccess": "'''ہن تسی {{SITENAME}} تے \"$1\" دے ناں توں لاگ ان او'''",
-       "nosuchuser": "اس $1 ناں نال کوئی ورتن آلا نہیں۔\nاپنی لکھائی درست کرو یا نیا [[Special:UserLogin/signup|کھاتہ بناؤ]]۔",
+       "nosuchuser": "اس $1 ناں نال کوئی ورتن آلا نہیں۔\nاپنی لکھائی درست کرو یا نیا [[Special:CreateAccount|کھاتہ بناؤ]]۔",
        "nosuchusershort": "اس \"$1\" ناں دا کوئی ورتن آلا نہيں اے۔\n\nاپنی الف، بے چیک کرو۔",
        "nouserspecified": "توانوں اپنا ورتن آلا ناں دسنا ہوۓ گا۔",
        "login-userblocked": "اے ورتن آلے روکیا ہویا اے۔ اے لاگ ان نئیں کرسکدا۔",
        "accmailtext": "اک کنجی [[User talk:$1|$1]] $2 نوں پیج دتی گئی اے۔\nایس نویں کھاتے دی کنجی بدلی جاسکدی  اے ''[[Special:ChangePassword|change password]]'' صفے تے لاگ ان ہون تے۔",
        "newarticle": "(نواں)",
        "newarticletext": "تسی ایسے صفحے دے جوڑ توں ایتھے پہنچے او جیڑا ھلے تک نہیں بنیا۔<br />\nاس صفحہ بنانے آسطے تھلے دتے گۓ ڈبے وچ لکھنا شروع کر دیو(زیادہ رہنمائی آستے اے ویکھو [$1 <br />مدد دا صفحہ])۔\nاگر تسی ایتھے غلطی نال پہنچے او تے اپنے کھوجی توں \"بیک\" دا بٹن دبا دیو۔",
-       "anontalkpagetext": "----'' ایہ اک گمنام ورتن والے دا گل بات دا صفہ اے جینے ہلے کھاتہ نئیں کھولیا یا او اینون ورتدا نئیں۔\nسانوں فیر نمبراں والا آئی پی پتہ ورتنا پوے گا اونوں لئی. ایہو جیا آئی پی پتہ گئی ورتن والے ورت سکدے نیں۔ \nاگر تسیں اک گمنام ورتن والے او تے اے مسوس کردے او جے پیڑی گل بات تواڈی بارے ہوئی اے، مہربانی کرکے [[Special:UserLogin/signup|create an account]] یا [[Special:UserLogin|log in]] اگے کسے مسلے توں بچن گمنام ورتن والیاں کولوں",
+       "anontalkpagetext": "----'' ایہ اک گمنام ورتن والے دا گل بات دا صفہ اے جینے ہلے کھاتہ نئیں کھولیا یا او اینون ورتدا نئیں۔\nسانوں فیر نمبراں والا آئی پی پتہ ورتنا پوے گا اونوں لئی. ایہو جیا آئی پی پتہ گئی ورتن والے ورت سکدے نیں۔ \nاگر تسیں اک گمنام ورتن والے او تے اے مسوس کردے او جے پیڑی گل بات تواڈی بارے ہوئی اے، مہربانی کرکے [[Special:CreateAccount|create an account]] یا [[Special:UserLogin|log in]] اگے کسے مسلے توں بچن گمنام ورتن والیاں کولوں",
        "noarticletext": "اس ویلے اس صفے تے کج نہیں لکھیا ہویا تسیں [[Special:Search/{{PAGENAME}}|اس صفے دے ناں نوں دوجے صفیاں تے کھوج سکدے او]] یا فیر [{{fullurl:{{FULLPAGENAME}}|action=edit}} اس صفے نوں لکھ سکدے او۔]",
        "noarticletext-nopermission": "ایس ویلے ایس صفے تے کوئی لکھت نئیں۔ \nتسیں [[Special:Search/{{PAGENAME}}|search for this page title]] دوسریاں صفیاں تے،\nیا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>۔",
        "userpage-userdoesnotexist": "ورتن کھاتہ \"$1\" رجسٹر نئیں ہویا۔\nمہربانی کرکے دسو جے تسیں ای ھفا بنانا/بدلنا چاندے او۔",
index bf6af7a..16f214a 100644 (file)
        "noname": "'Κ έβαλατε καλόν όνεμαν χρήστε.",
        "loginsuccesstitle": "Έντον τ' εσέβεμαν",
        "loginsuccess": "'''Εσήβετεν σο {{SITENAME}} με τ'όνεμαν \"$1\".'''",
-       "nosuchuser": "Αδαπές 'κ εχ' χρήστεν με τ' όνομαν \"$1\".\nΤερέστεν τα γράμματα τη ονοματί, τερέστεν τα τρανογράμματα και τα μικρογράμματα να είναι τογρία.\nΤερέστεν την ορθογραφίαν ή [[Special:UserLogin/signup|ποισέστεν καινούρεον λογαρίαν]].",
+       "nosuchuser": "Αδαπές 'κ εχ' χρήστεν με τ' όνομαν \"$1\".\nΤερέστεν τα γράμματα τη ονοματί, τερέστεν τα τρανογράμματα και τα μικρογράμματα να είναι τογρία.\nΤερέστεν την ορθογραφίαν ή [[Special:CreateAccount|ποισέστεν καινούρεον λογαρίαν]].",
        "nosuchusershort": "Αδαπές 'κ εχ' χρήστεν με τ' όνομαν \"$1\".\nΤ'όνομαν γραφέστεν ατο τογρία.",
        "nouserspecified": "Πρέπ' να ψιλίζετε έναν όνεμαν.",
        "login-userblocked": "Το χρήστεν έδεξαν ατον. Να εμπαίν 'κ ίνεται.",
        "deleteotherreason": "Άλλον/αλλομίαν λόγον:",
        "deletereasonotherlist": "Άλλον λόγον",
        "rollback": "Φέρον ξαν σην υστερναίαν",
-       "rollback_short": "Επαναφοράν",
        "rollbacklink": "φέρον ξαν σην υστερναίαν",
        "protectlogpage": "Αρχείον ασπαλιγματίων",
        "protectedarticle": "ασπαλιζμένον \"[[$1]]\"",
        "move-page-legend": "Ετεροχλάεμαν σελίδας",
        "movepagetext": "Εάν εφτάτε το ψαλαφίον αφκά θα δείτε άλλον όνομαν σ' έναν σελίδαν και θα παίρτεν τ' ιστορικόνατς εκαικά. Το παλαιόν η σελίδαν θα μεταβάλκεται σε σύνδεσμον σην καινούραιαν.\n\nΕπορείτε να μεταβάλκετε τα συνδέσμαι που δεκνίζνε σο παλαιόν τη σελίδαν αυτόματα. Εάν 'κ φτάτε αέτς,\nευρέστεν [[Special:DoubleRedirects|διπλά]] για [[Special:BrokenRedirects|τσακωμένα συνδέσμ]].\nΈχετ' ευθύνην τα παλαιά τα συνδέσμαι να δεκνίζνε σο σωστόν τη σελίδαν.\n\nΗ σελίδαν ''''κ θ' αλλάζ'''' τη θέσηνατς όντες έχ' άλλον σελίδαν με το νέον τ' όνεμαν. Εξαίρεσην εν τ' εύκαιρα τα σελίδας και τα συνδέσμαι, ντο 'κ έχνε ιστορικόν.\nΕπορείτε δηλαδή να παίρετε τη σελίδαν σ' όνομαν ντ' είχεν προτεσνά. Άμα 'κ επορείτε με το ετεροχλάεμαν να σβήετε άλλον σελίδαν.\n\n'''ΩΡΙΑ!'''\nΑβούτεν η ενέργειαν επορεί να φέρει τρανά διαφοράς σ' έναν σελίδαν που δεβάζνε πολλοί.\nΝουνίστενατο καλά πριχού να εφτάτε τ' άλλαγμαν τ' ονοματί.",
        "movepagetalktext": "Η σελίδαν καλατσεματί αυτόματα θα πηγαίν' εντάμαν, '''εξόν:'''\n*Έχ' άλλον σελίδαν καλατσεματί ντο 'κ εν εύκαιρον άμα έχ' το ίδιον τ' όνεμαν\n*θα ευκαιρώνετε το χουτίν αφκά.\n\nΕάν θέλετε να εφτάτε τα ένωμαν, να εφτάτε ατό με copy και paste.",
-       "movearticle": "Ετεροχλάεμαν σελίδας:",
        "newtitle": "Νέον τίτλον:",
        "move-watch": "Ωρίαγμαν τη σελίδας",
        "movepagebtn": "Ετεροχλάεμαν σελίδας",
        "movelogpage": "Αρχείον ετεροχλαεματί",
        "movereason": "Λόγον:",
        "revertmove": "κλώσιμον",
-       "delete_and_move": "Σβήσον και ετεροχλάεψον",
        "export": "Εξαγωγήν σελίδιων",
        "export-addcattext": "Βαλέστεν σελίδας ασήν κατηγορίαν:",
        "export-addcat": "Βαλέστεν",
        "importstart": "Έμπαζμαν σελιδίων...",
        "import-noarticle": "'Κ εχ' σελίδαν για έμπαζμαν!",
        "importlogpage": "Αρχείον εμπαζματίων",
-       "import-logentry-interwiki": "εγέντον εισαγωγήν transwiki σην σελίδαν $1",
        "tooltip-pt-userpage": "Τ' εσόν η σελίδαν",
        "tooltip-pt-mytalk": "Τ' εσόν το καλάτσεμαν",
        "tooltip-pt-preferences": "Τ' εμά τα προτιμήσεις",
        "htmlform-selectorother-other": "Άλλον",
        "rightsnone": "(τιδέν)",
        "revdelete-summary": "σύνοψην",
-       "searchsuggest-search": "Αράεμαν"
+       "searchsuggest-search": "Αράεμαν",
+       "special-characters-group-ipa": "ΔΦΑ",
+       "special-characters-group-telugu": "Τελούγκου"
 }
index 698cbc1..c1e7cb9 100644 (file)
        "noname": "Sta ni ast tikrōmiska tērpautajas pabilisnā.",
        "loginsuccesstitle": "Enēisenis izpalla",
        "loginsuccess": "'''Assei teinū engūbun {{SITENAME}} kāigi \"$1\".'''",
-       "nosuchuser": "Ni ast tērpautajs sen pabilīsnan \"$1\".\nZentlin debban en tērpautajas pabilīsnai ast zentlawingi.\nIzbandais peisāsnan anga [[Special:UserLogin/signup|teīkeis nāunan rekkenan]].",
+       "nosuchuser": "Ni ast tērpautajs sen pabilīsnan \"$1\".\nZentlin debban en tērpautajas pabilīsnai ast zentlawingi.\nIzbandais peisāsnan anga [[Special:CreateAccount|teīkeis nāunan rekkenan]].",
        "nosuchusershort": "Ni ast tērpautajs sen pabilīsnan \"$1\".\nIzbandais peisāsnan.",
        "nouserspecified": "Tu turri enpeisātun tērpautajas pabilīsnan.",
        "wrongpassword": "Nitikrōmiskas kliptaswīrds. Bandais dabber rēizan.",
        "accmailtitle": "Kliptaswīrds tengīntan.",
        "newarticle": "(Nāuns)",
        "newarticletext": "Tu assei autengīntan pra autengīnsenin en dabber niekzistīntin pāusan.\nKāi teīklai šin pāusan, pagaūneis enpeisātun en zemmaišasmu lāukan (wīdais [$1 help page] per tūls infōrmaciōnis).\nIk tū ni kwaitīwuns(si) teīktun nāunan pāusan, gnetteis \"Etwārtai\" knuppan en twajjai lasātlin.",
-       "anontalkpagetext": "----''Sta ast anōnimas tērpautajas diskusiōnis pāusan - stawīdan, kawīds ni turri dabber swajjan rekkenan anga ni tērpaui din.\nKāi identificīlai tennan, tērpawimai IP adressins.\nAdder IP adressi mazzi būtwei dallautan pra tūlin tērpautajans.\nIk tu assei anōnims tērpautajs be tu mīri, kāi kumentārai stwi ni ast wartīntan prei tin, [[Special:UserLogin/signup|teīkeis rekkenan]] anga [[Special:UserLogin|enēis]] kāi aulānktun perejīngins kurtīsenins sen kittans anōnimans tērpautajs.''",
+       "anontalkpagetext": "----''Sta ast anōnimas tērpautajas diskusiōnis pāusan - stawīdan, kawīds ni turri dabber swajjan rekkenan anga ni tērpaui din.\nKāi identificīlai tennan, tērpawimai IP adressins.\nAdder IP adressi mazzi būtwei dallautan pra tūlin tērpautajans.\nIk tu assei anōnims tērpautajs be tu mīri, kāi kumentārai stwi ni ast wartīntan prei tin, [[Special:CreateAccount|teīkeis rekkenan]] anga [[Special:UserLogin|enēis]] kāi aulānktun perejīngins kurtīsenins sen kittans anōnimans tērpautajs.''",
        "noarticletext": "Tēnti šin pāusan ni turri ēn sen tekstan.\nTu mazzi [[Special:Search/{{PAGENAME}}|laukītun šisse pāusas tītelin]] en kitēimans pāusans,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pralaukītun registerin],\nanga [{{fullurl:{{FULLPAGENAME}}|action=edit}} redigītun šin pāusan]</span>.",
        "userpage-userdoesnotexist": "Tērpautajs \"<nowiki>$1</nowiki>\" ni ast registrītan.\nIzbāndais, anga tū perarwi kwaitīwuns teīktun/redigītun šin pāusan.",
        "userpage-userdoesnotexist-view": "Tērpautajas rekkens \"$1\" ni ast registrītan.",
index 76b66bf..cc3f168 100644 (file)
        "noname": "تاسې تر اوسه پورې کوم کره کارن نوم نه دی ځانگړی کړی.",
        "loginsuccesstitle": "غونډال کې ورننوتلۍ",
        "loginsuccess": "'''تاسې اوس {{SITENAME}} کې د \"$1\" په نوم ننوتي ياست.'''",
-       "nosuchuser": "د \"$1\" په نوم هېڅ کارن نشته.\nد کارنانو نومونه د غټو او واړو تورو سره حساس دي.\nخپل حجا وڅارۍ، او يا هم [[Special:UserLogin/signup|يو نوی گڼون جوړ کړی]].",
+       "nosuchuser": "د \"$1\" په نوم هېڅ کارن نشته.\nد کارنانو نومونه د غټو او واړو تورو سره حساس دي.\nخپل حجا وڅارۍ، او يا هم [[Special:CreateAccount|يو نوی گڼون جوړ کړی]].",
        "nosuchusershort": "د \"$1\" په نوم هېڅ کوم گڼون نشته. لطفاً خپل د نوم ليکلې بڼې ته ځير شی چې پکې تېروتنه نه وي.",
        "nouserspecified": "تاسې ځان ته کوم کارن نوم نه دی ځانگړی کړی.",
        "login-userblocked": "په دې کارن بنديز لگېدلی. غونډال کې ننوتلو ته پرې نه ښودلی شو.",
        "accmailtitle": "پټنوم ولېږل شو.",
        "newarticle": "(نوی)",
        "newarticletext": "تاسې د يوې داسې تړنې څارنه کړې چې لا تر اوسه پورې نه شته.\nکه همدا مخ ليکل غواړۍ، نو په لانديني چوکاټ کې خپل متن وټاپئ (د لا نورو مالوماتو لپاره د [$1 لارښود مخ] وگورئ).\nکه چېرته تاسې دلته په تېروتنه راغلي ياست، نو يواځې د خپل د کتنمل '''مخ پر شا''' تڼۍ مو وټوکئ.",
-       "anontalkpagetext": "----''دا د يوه ورکنومي کارن چې کارن-نوم نه لري او يا خپل کارن-نوم نه کاروي، د سکالو يوه پاڼه ده. نو د يوه کس د پېژندلو پخاطر موږ د هماغه کارن د انټرنېټ شمېره يا IP پته دلته ثبتوؤ. داسې يوه IP پته د ډېرو کارنانو لخوا هم کارېدلی شي. که تاسې يو ورکنومی کارن ياست او تاسې ته دا څرگندېږي چې تاسې ته نااړونده پېغامونه او تبصرې اشاره شوي، نو د نورو بې نومو کارنانو او ستاسې ترمېنځ د ټکنتوب د مخ نيونې لپاره لطفاً [[Special:UserLogin/signup|يو گڼون جوړ کړۍ]] او يا هم [[Special:UserLogin|غونډال ته ورننوځۍ]].''",
+       "anontalkpagetext": "----''دا د يوه ورکنومي کارن چې کارن-نوم نه لري او يا خپل کارن-نوم نه کاروي، د سکالو يوه پاڼه ده. نو د يوه کس د پېژندلو پخاطر موږ د هماغه کارن د انټرنېټ شمېره يا IP پته دلته ثبتوؤ. داسې يوه IP پته د ډېرو کارنانو لخوا هم کارېدلی شي. که تاسې يو ورکنومی کارن ياست او تاسې ته دا څرگندېږي چې تاسې ته نااړونده پېغامونه او تبصرې اشاره شوي، نو د نورو بې نومو کارنانو او ستاسې ترمېنځ د ټکنتوب د مخ نيونې لپاره لطفاً [[Special:CreateAccount|يو گڼون جوړ کړۍ]] او يا هم [[Special:UserLogin|غونډال ته ورننوځۍ]].''",
        "noarticletext": "دم مهال په دې مخ کې څه نشته.\nتاسې کولای شی چې په نورو مخونو کې [[Special:Search/{{PAGENAME}}|د دې مخ د سرليک پلټنه]]،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د اړوندو يادښتونو پلټنه] ،\nاو يا [{{fullurl:{{FULLPAGENAME}}|action=edit}} همدا مخ جوړ کړئ]</span>.",
        "noarticletext-nopermission": "دم مهال په دې مخ کې متن نشته.\nتاسې کولای شی چې [[Special:Search/{{PAGENAME}}|همدا سرليک په نورو مخونو کې وپلټۍ]], يا هم <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} اړونده يادښتونه وپلټۍ]</span>، خو تاسې د دې مخ د جوړولو اجازه نه لرۍ.",
        "userpage-userdoesnotexist": "د \"<nowiki>$1</nowiki>\" گڼون نه دی ثبت شوی.\nلطفاً ځان ډاډه کړئ چې آيا تاسې په رښتيا همدا مخ جوړول/سمول غواړئ.",
index b55ded6..de3a243 100644 (file)
        "noname": "Você não colocou um nome de usuário válido.",
        "loginsuccesstitle": "Autenticado",
        "loginsuccess": "'''Agora você está {{GENDER:autenticado|autenticada}} ao wiki {{SITENAME}} como \"$1\"'''.",
-       "nosuchuser": "Não existe nenhum usuário com o nome \"$1\".\nOs nomes de usuário são sensíveis a letras maiúsculas.\nVerifique o que foi digitado ou [[Special:UserLogin/signup|crie uma nova conta]].",
+       "nosuchuser": "Não existe nenhum usuário com o nome \"$1\".\nOs nomes de usuário são sensíveis a letras maiúsculas.\nVerifique o que foi digitado ou [[Special:CreateAccount|crie uma nova conta]].",
        "nosuchusershort": "Não existe um usuário com o nome \"$1\". Verifique o nome que introduziu.",
        "nouserspecified": "Você precisa especificar um nome de usuário.",
        "login-userblocked": "Este usuário está bloqueado. Entrada proibida.",
        "accmailtext": "Uma senha gerada aleatoriamente para [[User talk:$1|$1]] foi enviada para $2.\n\nEla pode ser alterada na página ''[[Special:ChangePassword|de troca de senha]]'', após o início de sessão.",
        "newarticle": "(Nova)",
        "newarticletext": "Você seguiu um link para uma página que ainda não existe.\nPara criá-la, comece escrevendo na caixa abaixo (veja [$1 a página de ajuda] para mais informações).\nSe você chegou aqui por engano, clique no botão '''voltar''' do seu navegador.",
-       "anontalkpagetext": "---- ''Esta é a página de discussão para um usuário anônimo que ainda não criou uma conta ou que não a usa, de forma que temos de utilizar o endereço de IP para identificá-lo(a). Tal endereço de IP pode ser compartilhado por vários usuários. Se você é um usuário anônimo e acha que comentários irrelevantes foram direcionados a você, por gentileza, [[Special:UserLogin/signup|crie uma conta]] ou [[Special:UserLogin|autentique-se]], a fim de evitar futuras confusões com outros usuários anônimos.''",
+       "anontalkpagetext": "---- ''Esta é a página de discussão para um usuário anônimo que ainda não criou uma conta ou que não a usa, de forma que temos de utilizar o endereço de IP para identificá-lo(a). Tal endereço de IP pode ser compartilhado por vários usuários. Se você é um usuário anônimo e acha que comentários irrelevantes foram direcionados a você, por gentileza, [[Special:CreateAccount|crie uma conta]] ou [[Special:UserLogin|autentique-se]], a fim de evitar futuras confusões com outros usuários anônimos.''",
        "noarticletext": "Não há conteúdo nesta página no momento.\nVocê pode [[Special:Search/{{PAGENAME}}|pesquisar pelo título desta página]] em outras páginas, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar por registros relacionados],\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} criar esta página]</span>.",
        "noarticletext-nopermission": "No momento, não há conteúdo nesta página\nVocê pode [[Special:Search/{{PAGENAME}}|pesquisar pelo título desta página]] em outras páginas,\nou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar por registros relacionados] </span>. Note que, no entanto, você não tem permissão para criar esta página.",
        "missing-revision": "A revisão #$1 da página denominada \"{{FULLPAGENAME}}\" não existe.\n\nIsto é geralmente causado por seguir um link de histórico desatualizado para uma página que foi eliminada.\nOs detalhes podem ser encontrados no [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de eliminação].",
        "upload-form-label-own-work": "Isto é o meu próprio trabalho",
        "upload-form-label-infoform-categories": "Categorias",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-not-own-work-local-local": "Você pode também querer tentar [[Special:Upload|the default upload page]]",
+       "upload-form-label-not-own-work-local-generic-local": "Você pode também querer tentar [[Special:Upload|the default upload page]]",
        "backend-fail-stream": "Não foi possível transmitir o arquivo  $1.",
        "backend-fail-backup": "Não foi possível fazer backup do arquivo  $1 .",
        "backend-fail-notexists": "O arquivo $1 não existe.",
index 4168a65..4881cf8 100644 (file)
        "noname": "Não especificou um nome de utilizador válido.",
        "loginsuccesstitle": "Autenticação bem sucedida",
        "loginsuccess": "'''Encontra-se agora ligado à {{SITENAME}} como \"$1\"'''.",
-       "nosuchuser": "Não existe nenhum utilizador com o nome \"$1\".\nOs nomes de utilizador são sensíveis à capitalização.\nVerifique a ortografia, ou [[Special:UserLogin/signup|crie uma nova conta]].",
+       "nosuchuser": "Não existe nenhum utilizador com o nome \"$1\".\nOs nomes de utilizador são sensíveis à capitalização.\nVerifique a ortografia, ou [[Special:CreateAccount|crie uma nova conta]].",
        "nosuchusershort": "Não existe um utilizador com o nome \"$1\". Verifique o nome que introduziu.",
        "nouserspecified": "Precisa de especificar um nome de utilizador.",
        "login-userblocked": "Este utilizador está bloqueado. Não é permitido o acesso.",
        "accmailtext": "Uma palavra-passe gerada aleatoriamente para [[User talk:$1|$1]] foi enviada para $2.\n\nEla pode ser alterada na página [[Special:ChangePassword|de alteração da palavra-passe]] após iniciar sessão.",
        "newarticle": "(Nova)",
        "newarticletext": "Seguiu uma ligação para uma página que ainda não existe.\nPara criá-la, escreva o seu conteúdo na caixa abaixo (consulte a [$1 página de ajuda] para mais detalhes).\nSe chegou aqui por engano, clique o botão '''voltar''' (ou ''back'') do seu navegador.",
-       "anontalkpagetext": "----''Esta é a página de discussão de um utilizador anónimo que ainda não criou uma conta ou não a utiliza, pelo que temos de utilizar o endereço IP para identificá-lo(a).\nUm endereço IP pode ser partilhado por vários utilizadores.\nSe é um utilizador anónimo e sente que lhe foram direcionados comentários irrelevantes, por favor [[Special:UserLogin/signup|crie uma conta]] ou [[Special:UserLogin|inicie sessão]] para evitar futuras confusões com outros utilizadores anónimos.''",
+       "anontalkpagetext": "----''Esta é a página de discussão de um utilizador anónimo que ainda não criou uma conta ou não a utiliza, pelo que temos de utilizar o endereço IP para identificá-lo(a).\nUm endereço IP pode ser partilhado por vários utilizadores.\nSe é um utilizador anónimo e sente que lhe foram direcionados comentários irrelevantes, por favor [[Special:CreateAccount|crie uma conta]] ou [[Special:UserLogin|inicie sessão]] para evitar futuras confusões com outros utilizadores anónimos.''",
        "noarticletext": "Ainda não existe texto nesta página.\nPode [[Special:Search/{{PAGENAME}}|pesquisar o título desta página]] noutras páginas,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} procurar registos relacionados]\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} editar esta página]</span>.",
        "noarticletext-nopermission": "Ainda não existe texto nesta página.\nPode [[Special:Search/{{PAGENAME}}|pesquisar o título desta página]] noutras páginas, ou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} procurar nos registos relacionados]</span>, mas não tem permissão para criar esta página.",
        "missing-revision": "A revisão #$1 da página denominada \"{{FULLPAGENAME}}\" não existe.\n\nIsto é geralmente causado por seguir uma ligação de histórico desatualizada para uma página que foi eliminada.\nOs detalhes podem ser encontrados no [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registo de eliminação].",
        "upload-form-label-own-work": "Este é minha obra própria",
        "upload-form-label-infoform-categories": "Categorias",
        "upload-form-label-infoform-date": "Data",
-       "upload-form-label-own-work-message-local": "Confirmo que estou a carregar este ficheiro segundo as condições de serviço e política de licenças de {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Se não é capaz de carregar este ficheiro sob as políticas de {{SITENAME}}, por favor feche esta janela e tente outro método.",
-       "upload-form-label-not-own-work-local-local": "Poderá querer experimentar [[Special:Upload|a página padrão de carregamento]].",
-       "upload-form-label-own-work-message-default": "Entendo que estou a carregar este ficheiro em um repositório partilhado. Confirmo que estou a fazê-lo cumprindo com os termos de serviço e com as políticas de licenciamento.",
-       "upload-form-label-not-own-work-message-default": "Se não é capaz de carregar este ficheiro sob as políticas do repositório partilhado, por favor feche esta janela e tente outro método.",
-       "upload-form-label-not-own-work-local-default": "Pode querer tentar utilizar [[Special:Upload|a página de carregamento em {{SITENAME}}]], se este ficheiro puder ser carregado de acordo com suas as políticas.",
-       "upload-form-label-own-work-message-shared": "Confirmo que sou o proprietário dos direitos de autor deste ficheiro, e aceito partilhar irrevogavelmente este ficheiro para o Wikimedia Commons nos termos da licença [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Atribuição-CompartilhaIgual 4.0], e concordo com os [https://wikimediafoundation.org/wiki/Terms_of_Use Termos de Utilização].",
-       "upload-form-label-not-own-work-message-shared": "Se não é o proprietário dos direitos de autor deste ficheiro, ou caso deseje partilhá-lo sob uma licença diferente, considere utilizar o [https://commons.wikimedia.org/wiki/Special:UploadWizard Assistente de Envio de Ficheiros do Commons].",
-       "upload-form-label-not-own-work-local-shared": "Pode querer tentar utilizar [[Special:Upload|a página de carregamento em {{SITENAME}}]], se o sítio aceitar o carregamento deste ficheiro de acordo com as suas políticas.",
+       "upload-form-label-own-work-message-generic-local": "Confirmo que estou a carregar este ficheiro segundo as condições de serviço e política de licenças de {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Se não é capaz de carregar este ficheiro sob as políticas de {{SITENAME}}, por favor feche esta janela e tente outro método.",
+       "upload-form-label-not-own-work-local-generic-local": "Poderá querer experimentar [[Special:Upload|a página padrão de carregamento]].",
+       "upload-form-label-own-work-message-generic-foreign": "Entendo que estou a carregar este ficheiro em um repositório partilhado. Confirmo que estou a fazê-lo cumprindo com os termos de serviço e com as políticas de licenciamento.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Se não é capaz de carregar este ficheiro sob as políticas do repositório partilhado, por favor feche esta janela e tente outro método.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Pode querer tentar utilizar [[Special:Upload|a página de carregamento em {{SITENAME}}]], se este ficheiro puder ser carregado de acordo com suas as políticas.",
        "backend-fail-stream": "Não foi possível transmitir o ficheiro \"$1\".",
        "backend-fail-backup": "Não foi possível fazer cópia de segurança do ficheiro \"$1\".",
        "backend-fail-notexists": "O ficheiro $1 não existe.",
index 2faea55..7a6a253 100644 (file)
                        "Robin van der Vliet",
                        "Conquistador",
                        "Frigory",
-                       "Psychoslave"
+                       "Psychoslave",
+                       "Guycn2"
                ]
        },
        "sidebar": "{{notranslate}}",
        "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.",
        "login": "{{Doc-special|UserLogin|unlisted=1}}\n{{Identical|Log in}}",
+       "login-security": "Used as the title of the login page when the user is already logged in but sent to reauthenticate before getting access to a feature with elevated security.",
        "nav-login-createaccount": "Shown to anonymous users in the upper right corner of the page. When you can't create an account, the message {{msg-mw|login}} is shown.\n{{Identical|Log in / create account}}",
        "loginprompt": "{{ignored}}",
        "userlogin": "Since 1.22 no longer used in core, but may still be used by extensions. DEPRECATED\n\n{{Identical|Log in / create account}}",
        "helplogin-url": "{{doc-important|Do not translate the namespace name <code>Help</code>.}}\nUsed as name of the page that provides information about logging into the wiki.\n\nUsed as a link target in the message {{msg-mw|Userlogin-helplink}}.",
        "userlogin-helplink2": "Label for a link to login help.\n\nSee example: [[Special:UserLogin]]\n\nSee also:\n* {{msg-mw|Helplogin-url}}",
        "userlogin-loggedin": "Used as warning on [[Special:UserLogin]] when the current user is already logged in.\n\nFollowed by the Login form.\n\nSee example: [[Special:UserLogin]].\n\nParameters:\n* $1 - user name (used for display and for gender support)\nSee also:\n* {{msg-mw|Mobile-frontend-userlogin-loggedin-register}}",
+       "userlogin-reauth": "Used as an explanatory message on [[Special:UserLogin]] when the user is redirected there to log in again when trying to use a security-sensitive page.\n\nParameters:\n* $1 - user name (used for display and for gender support)",
        "userlogin-createanother": "Used as label for the button on [[Special:UserLogin]] shown when the current user is already logged in.\n{{Identical|Create another account}}",
        "createacct-emailrequired": "Label in create account form for email field when it is required.\n\nSee also:\n* {{msg-mw|Createacct-emailoptional}}\n{{Identical|E-mail address}}",
        "createacct-emailoptional": "Label in vertical-layout create account form for email field when it is optional.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n\nSee also:\n* {{msg-mw|Createacct-emailrequired}}",
        "createacct-email-ph": "Placeholder in vertical-layout create account form for email field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
        "createacct-another-email-ph": "Placeholder in create account form for email field when one user creates an account for another.",
-       "createaccountmail": "The label for the checkbox for creating a new account and sending the new password to the specified email address directly, as used on [[Special:UserLogin/signup]] when one user creates an account for another (if creating accounts by email is allowed).\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
+       "createaccountmail": "The label for the checkbox for creating a new account and sending the new password to the specified email address directly, as used on [[Special:CreateAccount]] when one user creates an account for another (if creating accounts by email is allowed).\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
+       "createaccountmail-help": "Account creation API help message for the <code>mailpassword</code> parameter.",
        "createacct-realname": "In vertical-layout create account form, label for field to enter optional real name.",
        "createaccountreason": "Since 1.22 no longer used in core, but may be used by some extensions. DEPRECATED\n\n{{Identical|Reason}}",
        "createacct-reason": "In create account form, label for field to enter reason to create an account when already logged-in.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Reason}}",
        "createacct-reason-ph": "Placeholder in vertical-layout create account form for reason field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
+       "createacct-reason-help": "Account creation API help message for the <code>reason</code> parameter.",
        "createacct-imgcaptcha-help": "{{Optional}} Optional help text in vertical-layout create account form for image CAPTCHA input field when repositioned by JavaScript.\n\nBlank by default.",
        "createacct-submit": "Submit button on vertical-layout create account form.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
-       "createacct-another-submit": "Submit button of  [[Special:UserLogin/signup]] ([[Special:CreateAccount]]) when accessed by a registered user.\n\nThe original means \"create an account in addition to the one you already have\"; sometimes, but not always, it means you are going to \"Create the account on behalf of somebody else\" or \"Create account for another\".\n{{Identical|Create another account}}",
+       "createacct-another-submit": "Submit button of  [[Special:CreateAccount]] when accessed by a registered user.\n\nThe original means \"create an account in addition to the one you already have\"; sometimes, but not always, it means you are going to \"Create the account on behalf of somebody else\" or \"Create account for another\".\n{{Identical|Create another account}}",
+       "createacct-continue-submit": "Submit button on create account form in second and later steps of a multistep account creation.",
+       "createacct-another-continue-submit": "Submit button on create account form in second and later steps of a multistep account creation, when done by a registered user.",
        "createacct-benefit-heading": "In vertical-layout create account form, the heading for the section describing the benefits of creating an account. See example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n\nIf in your language you need to know the gender of the name for the wiki (which is the subject of the English sentence), please adapt the sentence as much as you need for your translation to fit.",
        "createacct-benefit-icon1": "In vertical-layout create account form, the CSS style for the div next to the first benefit. If you replace this you will need probably need to adjust CSS.\n\nUsed as a CSS class name.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup&useNew=1}} Special:UserLogin?type=signup&useNew=1]",
        "createacct-benefit-head1": "In vertical-layout create account form, the text in the heading for the first benefit. Do not edit the magic word; if you replace it you will probably need to adjust CSS.\n\nFollowed by the message {{msg-mw|Createacct-benefit-body1}}.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup&useNew=1}} Special:UserLogin?type=signup&useNew=1]",
        "nocookieslogin": "This message is displayed when someone tried to login, but the browser doesn't accept cookies.",
        "nocookiesfornew": "This message is displayed when the user tried to create a new account, but it failed the cross-site request forgery (CSRF) check. It could be blocking an attack, but most likely, the browser isn't  accepting cookies.",
        "nocookiesforlogin": "{{optional}}\nThis message is displayed when someone tried to login and the CSRF failed (most likely, the browser doesn't accept cookies).\n\nDefault:\n* {{msg-mw|Nocookieslogin}}",
+       "createacct-loginerror": "This message is displayed after a successful registration when there is a server-side error with logging the user in. This is not expected to happen.",
        "noname": "Error message.",
        "loginsuccesstitle": "The title of the page saying that you are logged in. The content of the page is the message {{msg-mw|Loginsuccess}}.\n{{Identical|Log in}}",
        "loginsuccess": "The content of the page saying that you are logged in. The title of the page is {{msg-mw|Loginsuccesstitle}}.\n\nParameters:\n* $1 - the name of the logged in user\n{{Gender}}",
        "createacct-another-realname-tip": "{{doc-singularthey}}\nUsed on the account creation form when creating another user's account. Similar to {{msg-mw|prefs-help-realname}}.\n{{Identical|Real name attribution}}",
        "pt-login": "Shown to anonymous users in the upper right corner of the page when they can't create an account (otherwise the message {{msg-mw|nav-login-createaccount}} is shown there).\n{{Identical|Log in}}",
        "pt-login-button": "Shown as the caption of the button at [[Special:UserLogin]].\n{{Identical|Log in}}",
+       "pt-login-continue-button": "Shown as the caption of the button at [[Special:UserLogin]] in second and later steps of a multipage login.",
        "pt-createaccount": "Used on the top of the page for logged out users, where it appears next to {{msg-mw|login}}, so consider making them similar.\n{{Identical|Create account}}",
        "pt-userlogout": "{{Doc-actionlink}}\n{{Identical|Log out}}",
        "pear-mail-error": "{{notranslate}}\nParameters:\n* $1 - error message which is returned by PEAR mailer.",
        "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_forbidden-reason": "Like {{msg-mw|resetpass_forbidden}} but the auth provider gave a reason.\n\nParameters:\n* $1 - reason given by auth provider",
        "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}}",
        "resetpass-submit-cancel": "Used on [[Special:ResetPass]].\n{{Identical|Cancel}}",
        "passwordreset-emailsentusername": "Used in [[Special:PasswordReset]].\n\nSee also:\n* {{msg-mw|Passwordreset-emailsent-capture}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
        "passwordreset-emailsent-capture": "Used in [[Special:PasswordReset]].\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
        "passwordreset-emailerror-capture": "Error message displayed in [[Special:PasswordReset]] when sending an email fails. Parameters:\n* $1 - error message\n* $2 - username, used for GENDER\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailsent-capture}}",
+       "passwordreset-emailsent-capture2": "Used in [[Special:PasswordReset]].\n\nParameters:\n* $1 - number of accounts notified\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
+       "passwordreset-emailerror-capture2": "Error message displayed in [[Special:PasswordReset]] when sending an email fails. Parameters:\n* $1 - error message\n* $2 - username, used for GENDER\n* $3 - number of accounts notified\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailsent-capture}}",
+       "passwordreset-nocaller": "Shown when a password reset was requested but the caller was not provided. This is an internal error.",
+       "passwordreset-nosuchcaller": "Shown when a password reset was requested but the username of the caller could not be resolved to a user. This is an internal error.\n\nParameters:\n* $1 - username of the caller",
+       "passwordreset-ignored": "Shown when password reset was unsuccessful due to configuration problems.",
+       "passwordreset-invalideamil": "Returned when the email address is syntatically invalid.",
+       "passwordreset-nodata": "Returned when no data was provided.",
        "changeemail": "Title of [[Special:ChangeEmail|special page]]. This page also allows removing the user's email address.",
        "changeemail-summary": "{{ignored}}",
        "changeemail-header": "Text of [[Special:ChangeEmail]].",
        "image_tip": "This is the text that appears when you hover the mouse over the sixth (middle) button on the edit toolbar.\n\n{{Identical|Embedded file}}",
        "media_sample": "{{optional}}\n{{Identical|Example}}",
        "media_tip": "This is the text that appears when you hover the mouse over the fifth button from the right in the edit toolbar.\n{{Identical|File link}}",
+       "sig-text": "{{notranslate}} This is the text that appears when you click on the signature button (second button from the right) on the edit toolbar. $1 will be replaced with four tildes (which cannot be included directly in the message for technical reasons).",
        "sig_tip": "This is the text that appears when you hover the mouse over the second key from the right on the edit toolbar.\n{{Identical|Signature with timestamp}}",
        "hr_tip": "This is the text that appears when you hover the mouse over the first button on the right on the edit toolbar.",
        "summary": "The Summary text beside the edit summary field\n\nSee also:\n* {{msg-mw|Subject}}\nSee also:\n* {{msg-mw|Accesskey-summary}}\n* {{msg-mw|Tooltip-summary}}\n{{Identical|Summary}}",
        "showpreview": "The text of the button to preview the page you are editing. See also {{msg-mw|showdiff}} and {{msg-mw|savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showpreview}}\n* {{msg-mw|Accesskey-preview}}\n* {{msg-mw|Tooltip-preview}}\n{{Identical|Show preview}}",
        "showdiff": "Button below the edit page. See also {{msg-mw|Showpreview}} and {{msg-mw|Savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showdiff}}\n* {{msg-mw|Accesskey-diff}}\n* {{msg-mw|Tooltip-diff}}\n{{Identical|Show change}}",
        "blankarticle": "Notice displayed once after the user tries to save an empty page.",
-       "anoneditwarning": "Shown when editing a page anonymously.\n\nParameters:\n* $1 – A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 – A link to sign up, <nowiki>{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
+       "anoneditwarning": "Shown when editing a page anonymously.\n\nParameters:\n* $1 – A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 – A link to sign up, <nowiki>{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
        "anonpreviewwarning": "See also:\n* {{msg-mw|Anoneditwarning}}",
        "missingsummary": "The text \"edit summary\" is in {{msg-mw|Summary}}.\n\nSee also:\n* {{msg-mw|Missingcommentheader}}\n* {{msg-mw|Savearticle}}",
        "selfredirect": "Notice displayed once after the user tries to create a redirect to the same article.",
        "yourtext": "Used in Diff Preview page. The diff is between {{msg-mw|currentrev}} and {{msg-mw|yourtext}}.\n\nAlso used in Edit Conflict page; the diff between {{msg-mw|yourtext}} and {{msg-mw|storedversion}}.",
        "storedversion": "This is used in an edit conflict as the label for the top revision that has been stored, as opposed to your version {{msg-mw|yourtext}} that has not been stored which is shown at the bottom of the page.",
        "nonunicodebrowser": "Used as warning when editing page.",
-       "editingold": "Used as warning when editing page.",
+       "editingold": "Used as warning when editing an old revision of a page.",
        "yourdiff": "Used as h2 header for the diff of the current version of a page with the user's version in case there is an edit conflict or a spam filter hit.",
        "copyrightwarning": "Copyright warning displayed under the edit box in editor. Parameters:\n* $1 - link\n* $2 - license name",
        "copyrightwarning2": "Copyright warning displayed under the edit box in editor\n*$1 - license name",
        "undo-summary-username-hidden": "Edit summary for an undo action where the username of the old revision is hidden.\n\nParameters:\n* $1 - the revision ID being undone\nSee also:\n* {{msg-mw|Undo-summary}}",
        "cantcreateaccounttitle": "Used as title of the error message {{msg-mw|Cantcreateaccount-text}}.",
        "cantcreateaccount-text": "Used as error message, with the title {{msg-mw|Cantcreateaccounttitle}}.\n* $1 - target IP address\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\nSee also:\n* {{msg-mw|Cantcreateaccount-range-text}}",
-       "cantcreateaccount-range-text": "Used as more detailed version of the {{msg-mw|Cantcreateaccount-text}} error message, with the title {{msg-mw|Cantcreateaccounttitle}}.\n* $1 - target IP address range\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\n* $4 - current user's IP address",
+       "cantcreateaccount-range-text": "Used instead of the {{msg-mw|Cantcreateaccount-text}} when the block is a range block.\n* $1 - target IP address range\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\n* $4 - current user's IP address",
        "createaccount-hook-aborted": "Placeholder message to return with API errors on account create; passes through the message from a hook {{notranslate}}",
        "viewpagelogs": "Link displayed in history of pages",
        "nohistory": "Message shown when there are no history to list. See [{{canonicalurl:x|action=history}} example history].\n----\nAlso used as title of error message when the feed is empty. See [{{canonicalurl:x|action=history&feed=atom}} example feed].\n\nSee the error message:\n* {{msg-mw|history-feed-empty}}",
        "right-managechangetags": "{{doc-right|managechangetags}}",
        "right-applychangetags": "{{doc-right|applychangetags}}",
        "right-changetags": "{{doc-right|changetags}}",
+       "right-deletechangetags": "{{doc-right|deletechangetags}}",
        "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}}",
        "action-managechangetags": "{{doc-action|managechangetags}}",
        "action-applychangetags": "{{doc-action|applychangetags}}",
        "action-changetags": "{{doc-action|changetags}}",
+       "action-deletechangetags": "{{doc-action|deletechangetags}}",
        "nchanges": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to a diff of the changes.\n\nParameters:\n* $1 - the number of changes on that day (2 or more)\nThree messages are shown side-by-side: ({{msg-mw|Nchanges}} | {{msg-mw|Enhancedrc-since-last-visit}} | {{msg-mw|Enhancedrc-history}}).",
        "enhancedrc-since-last-visit": "Appears on enhanced watchlist and recent changes when page has more than one change on given date and at least one that the user hasn't seen yet, linking to a diff of the unviewed changes.\n\nParameters:\n* $1 - the number of unviewed changes (1 or more)\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).",
        "enhancedrc-history": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to its history.\n\nThis is the same as {{msg-mw|hist}}, but not abbreviated.\n\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).\n{{Identical|History}}",
        "upload-form-label-own-work": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Label for own work confirmation checkbox",
        "upload-form-label-infoform-categories": "Label for category selector input\n{{Identical|Category}}",
        "upload-form-label-infoform-date": "Label for date input\n{{Identical|Date}}",
-       "upload-form-label-own-work-message-local": "Message shown by local when a user affirms that their file upload to the local wiki follows the terms of service and licensing policies of the local wiki.",
-       "upload-form-label-not-own-work-message-local": "Message shown by local when a user cannot upload a file to the local wiki.",
-       "upload-form-label-not-own-work-local-local": "Suggests uploading a file via Special:Upload instead of using whatever method they're currently using.",
-       "upload-form-label-own-work-message-default": "Message shown by default when a user affirms that they are allowed to upload a file to a remote wiki.",
-       "upload-form-label-not-own-work-message-default": "Message shown by default when a user cannot upload a file to a remote wiki.",
-       "upload-form-label-not-own-work-local-default": "Suggests uploading a file locally instead of to a remote wiki.",
-       "upload-form-label-own-work-message-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Legal message, confirming that the user is allowed to upload the file.",
-       "upload-form-label-not-own-work-message-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Explains alternatives when the copyright isn't owned by the uploader.",
-       "upload-form-label-not-own-work-local-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Message suggesting the user might want to upload a file locally instead of to Wikimedia Commons.",
+       "upload-form-label-own-work-message-generic-local": "Message shown by local when a user affirms that their file upload to the local wiki follows the terms of service and licensing policies of the local wiki.",
+       "upload-form-label-not-own-work-message-generic-local": "Message shown by local when a user cannot upload a file to the local wiki.",
+       "upload-form-label-not-own-work-local-generic-local": "Suggests uploading a file via Special:Upload instead of using whatever method they're currently using.",
+       "upload-form-label-own-work-message-generic-foreign": "Message shown by default when a user affirms that they are allowed to upload a file to a remote wiki.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Message shown by default when a user cannot upload a file to a remote wiki.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Suggests uploading a file locally instead of to a remote wiki.",
        "backend-fail-stream": "Parameters:\n* $1 - a filename",
        "backend-fail-backup": "Parameters:\n* $1 - a filename",
        "backend-fail-notexists": "Parameters:\n* $1 - a filename",
        "whatlinkshere-prev": "This is part of the navigation message on the top and bottom of Whatlinkshere pages, where it is used as the first argument of {{msg-mw|Viewprevnext}}.\n\nParameters:\n* $1 - the number of items shown per page. It is not used when $1 is zero; not sure what happens when $1 is one.\nSpecial pages use {{msg-mw|Prevn}} instead (still as an argument to {{msg-mw|Viewprevnext}}).\n\nSee also:\n* {{msg-mw|Whatlinkshere-next}}\n{{Identical|Previous}}",
        "whatlinkshere-next": "This is part of the navigation message on the top and bottom of Whatlinkshere pages, where it is used as the second argument of {{msg-mw|Viewprevnext}}.\n\nParameters:\n* $1 - the number of items shown per page. It is not used when $1 is zero; not sure what happens when $1 is one.\nSpecial pages use {{msg-mw|Nextn}} instead (still as an argument to {{msg-mw|Viewprevnext}}).\n\nSee also:\n* {{msg-mw|Whatlinkshere-prev}}\n{{Identical|Next}}",
        "whatlinkshere-links": "Used on [[Special:WhatLinksHere]]. It is a link to the WhatLinksHere page of that page.\n\nExample line:\n* [[Main Page]] ([[Special:WhatLinksHere/Main Page|{{int:whatlinkshere-links}}]])\n{{Identical|Link}}",
-       "whatlinkshere-hideredirs": "Filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}\n{{Identical|Redirect}}",
-       "whatlinkshere-hidetrans": "First filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}\n{{Identical|Transclusion}}",
-       "whatlinkshere-hidelinks": "Filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}\n{{Identical|Link}}",
+       "whatlinkshere-hideredirs": "Filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}",
+       "whatlinkshere-hidetrans": "First filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}",
+       "whatlinkshere-hidelinks": "Filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 is the {{msg-mw|hide}} or {{msg-mw|show}}",
        "whatlinkshere-hideimages": "Filter option in [[Special:WhatLinksHere]]. Parameters:\n* $1 - the {{msg-mw|Hide}} or {{msg-mw|Show}}\n\nSee also:\n*{{msg-mw|Isimage}}\n*{{msg-mw|Media tip}}",
        "whatlinkshere-filters": "{{Identical|Filter}}",
        "whatlinkshere-submit": "Label for submit button in [[Special:WhatLinksHere]]\n{{Identical|Go}}",
        "timezone-local": "Label to indicate that a time is in the user's local timezone.\n{{Identical|Local}}",
        "duplicate-defaultsort": "See definition of [[w:Sorting|sort key]] on Wikipedia. Parameters:\n* $1 - old default sort key\n* $2 - new default sort key",
        "duplicate-displaytitle": "Warning shown when a page has its display title set multiple times. Parameters:\n* $1 - old display title\n* $2 - new display title",
+       "restricted-displaytitle": "Warning shown a display title is ignored because it is not equivalent to its actual title. Parameters:\n* $1 - the ignored display title",
        "invalid-indicator-name": "Warning shown when the [https://www.mediawiki.org/wiki/Help:Page_status_indicators &lt;indicator name=\"''unique-identifier''\">''content''&lt;/indicator>] parser tag is used incorrectly.",
        "version": "{{doc-special|Version}}\n{{Identical|Version}}",
        "version-summary": "{{doc-specialpagesummary|version}}",
        "tags-delete-not-found": "Error message on [[Special:Tags]]",
        "tags-delete-too-many-uses": "Error message on [[Special:Tags]]",
        "tags-delete-warnings-after-delete": "Warning shown after deleting a tag.\n\nParameters:\n* $1 - the code name of the tag that was deleted\n* $2 - the number of warnings",
+       "tags-delete-no-permission": "Error message on [[Special:Tags]]",
        "tags-activate-title": "The title of a page used to activate a tag. For more information on tags see [[mw:Manual:Tags|MediaWiki]].",
        "tags-activate-question": "An explanation to tell users what they are about to do.\n\nParameters:\n* $1 - the code name of the tag that is about to be activated",
        "tags-activate-reason": "{{Identical|Reason}}",
        "log-action-filter-suppress-block": "{{doc-log-action-filter-action|suppress|block}}",
        "log-action-filter-suppress-reblock": "{{doc-log-action-filter-action|suppress|reblock}}",
        "log-action-filter-upload-upload": "{{doc-log-action-filter-action|upload|upload}}",
-       "log-action-filter-upload-overwrite": "{{doc-log-action-filter-action|upload|overwrite}}"
+       "log-action-filter-upload-overwrite": "{{doc-log-action-filter-action|upload|overwrite}}",
+       "authmanager-authn-not-in-progress": "Error message when AuthManager session data is lost during authentication, or the user hits the \"continue\" endpoint without an active authentication attempt.",
+       "authmanager-authn-no-primary": "Error message when no AuthenticationProvider handles the AuthenticationRequests for login. This might mean the user needs to fill out all the form fields.",
+       "authmanager-authn-no-local-user": "Error message when authentication somehow succeeds without a username being known. This probably should never happen.",
+       "authmanager-authn-no-local-user-link": "Error message when federated authentication (e.g. \"login with Google\") succeeds, but no account is associated.",
+       "authmanager-authn-autocreate-failed": "Error message when auto-creation fails during login. Parameters:\n* $1 - Error message from the account creation attempt, as wikitext.",
+       "authmanager-change-not-supported": "Error message when all PrimaryAuthenticationProviders ignore the change request.",
+       "authmanager-create-disabled": "Message displayed when account creation is disabled.",
+       "authmanager-create-from-login": "Message displayed when moving from login to account creation and additional data must be collected from the user.",
+       "authmanager-create-not-in-progress": "Error message when AuthManager session data is lost during account creation, or the user hits the \"continue\" endpoint without an active account creation attempt.",
+       "authmanager-create-no-primary": "Error message when no AuthenticationProvider handles the AuthenticationRequests for account creation. This might mean the user needs to fill out all the form fields.",
+       "authmanager-link-no-primary": "Error message when no AuthenticationProvider handles the AuthenticationRequests for account linking. This might mean the user needs to fill out all the form fields.",
+       "authmanager-link-not-in-progress": "Error message when AuthManager session data is lost during account linking, or the user hits the \"continue\" endpoint without an active account link attempt.",
+       "authmanager-authplugin-setpass-failed-title": "Title of error page from AuthManager if AuthPlugin returns false from its setPassword() method.",
+       "authmanager-authplugin-setpass-failed-message": "Text of error page from AuthManager if AuthPlugin returns false from its setPassword() method.",
+       "authmanager-authplugin-create-fail": "Error message from AuthManager if the AuthPlugin returns false from its addUser() method.",
+       "authmanager-authplugin-setpass-denied": "Error message from AuthManager if the AuthPlugin returns false from its allowPasswordChange() method.",
+       "authmanager-authplugin-setpass-bad-domain": "Error message from AuthManager if the AuthPlugin rejects the passed domain.",
+       "authmanager-autocreate-noperm": "Error message when auto-creation fails due to lack of permission.",
+       "authmanager-autocreate-exception": "Error message when auto-creation fails because we tried recently and an exception was thrown, so we're not going to try again yet.",
+       "authmanager-userdoesnotexist": "Error message when a user account does not exist. Parameters:\n* $1 - User name.",
+       "authmanager-userlogin-remembermypassword-help": "Description of the field with label {{msg-mw|userlogin-remembermypassword}}.",
+       "authmanager-username-help": "Description of the field with label {{msg-mw|userlogin-yourname}}.",
+       "authmanager-password-help": "Description of the field with label {{msg-mw|userlogin-yourpassword}}.",
+       "authmanager-domain-help": "Description of the field with label {{msg-mw|yourdomainname}}.",
+       "authmanager-retype-help": "Description of the field with label {{msg-mw|createacct-yourpasswordagain}}.",
+       "authmanager-email-label": "Label for the email field.",
+       "authmanager-email-help": "Description of the field with label {{msg-mw|authmanager-email-label}}.",
+       "authmanager-realname-label": "Label for the realname field.",
+       "authmanager-realname-help": "Description of the field with label {{msg-mw|authmanager-realname-label}}.",
+       "authmanager-provider-password": "Description for PasswordAuthenticationRequest. Will be used as $1 in messages such as {{msg-mw|authprovider-confirmlink-option}}.",
+       "authmanager-provider-password-domain": "Description for PasswordDomainAuthenticationRequest. Will be used as $1 in messages such as {{msg-mw|authprovider-confirmlink-option}}.",
+       "authmanager-account-password-domain": "Format to display username and domain for PasswordDomainAuthenticationRequest. Will be used as $2 in messages such as {{msg-mw|authprovider-confirmlink-option}}. Parameters:\n* $1 - Username\n* $2 - Domain",
+       "authmanager-provider-temporarypassword": "Description for TemporaryPasswordAuthenticationRequest. Will be used as $1 in messages such as {{msg-mw|authprovider-confirmlink-option}}.",
+       "authprovider-confirmlink-message": "Message from ConfirmLinkSecondaryAuthenticationProvider to indicate that credentials may be linked.",
+       "authprovider-confirmlink-option": "Used to format linkable credentials in ConfirmLinkSecondaryAuthenticationProvider. Parameters:\n* $1 - Credential provider (e.g. the name of the third-party authentication service).\n* $2 - Credential account (e.g. the email address).",
+       "authprovider-confirmlink-request-label": "Form field label for the list of linkable credentials",
+       "authprovider-confirmlink-request-help": "Form field help text",
+       "authprovider-confirmlink-success-line": "Line to display that credentials were linked successfully. Parameters:\n* $1 - Linked credentials, formatted with {{msg-mw|authprovider-confirmlink-option}}\n\nSee also:\n* {{msg-mw|authprovider-confirmlink-failed}}\n* {{msg-mw|authprovider-confirmlink-failed-line}}",
+       "authprovider-confirmlink-failed-line": "Line to display that credentials were not linked successfully. Parameters:\n* $1 - Credentials that failed, formatted with {{msg-mw|authprovider-confirmlink-option}}\n* $2 - Failure message text.\n\nSee also:\n* {{msg-mw|authprovider-confirmlink-failed}}\n* {{msg-mw|authprovider-confirmlink-success-line}}",
+       "authprovider-confirmlink-failed": "Used to prefix the list of individual link statuses when some did not succeed. Parameters:\n* $1 - Failure message, or a wikitext bulleted list of failure messages.\n\nSee also:\n* {{msg-mw|authprovider-confirmlink-success-line}}\n* {{msg-mw|authprovider-confirmlink-failed-line}}",
+       "authprovider-confirmlink-ok-help": "Description of the \"ok\" field when ConfirmLinkSecondaryAuthenticationProvider needs to display link failure messages to the user.",
+       "authprovider-resetpass-skip-label": "Label for the \"Skip\" button when it's possible to skip resetting a password in ResetPasswordSecondaryAuthenticationProvider.",
+       "authprovider-resetpass-skip-help": "Description of the option to skip resetting a password in ResetPasswordSecondaryAuthenticationProvider.",
+       "authform-nosession-login": "Error message shown when the login was successful, but the session could not be reestablished on the next request. $1 is an explanation which depends on what session handler is being used, such as {{msg-mw|sessionprovider-nocookies}}.",
+       "authform-nosession-signup": "Error message shown when the account creation was successful, but the session could not be reestablished on the next request. $1 is an explanation which depends on what session handler is being used, such as {{msg-mw|sessionprovider-nocookies}}.",
+       "authform-newtoken": "Error message shown on the auth form when the session has no CSRF token. This can be caused by session expiry but it is more likely that the client does not support sessions for some reason (e.g. a browser with all cookies diabled). $1 is an explanation (in the form of full sentences) given by the session provider of why sessions might not work (usually this will be {{msg-mw|sessionprovider-nocookies}}).",
+       "authform-notoken": "Error message shown on the auth form when the submitted data has no CSRF token.",
+       "authform-wrongtoken": "Error message shown on the auth form when the submitted CSRF token value is invalid.",
+       "specialpage-securitylevel-not-allowed-title": "Error page title shown when the user visits a special page but the authentication security check fails.",
+       "specialpage-securitylevel-not-allowed": "Error message shown when the user visits a special page but the authentication security check fails.",
+       "authpage-cannot-login": "Error message shown on authentication-related special pages when login cannot start. This is not supposed to happen unless the site is misconfigured.",
+       "authpage-cannot-login-continue": "Error message shown on authentication-related special pages when login cannot continue. This most likely means a session timeout.",
+       "authpage-cannot-create": "Error message shown on authentication-related special pages when account creation cannot start. This is not supposed to happen unless the site is misconfigured.",
+       "authpage-cannot-create-continue": "Error message shown on authentication-related special pages when account creation cannot continue. This most likely means a session timeout.",
+       "authpage-cannot-link": "Error message shown on authentication-related special pages when account linking cannot start. This is not supposed to happen unless the site is misconfigured.",
+       "authpage-cannot-link-continue": "Error message shown on authentication-related special pages when account linking cannot continue. This most likely means a session timeout.",
+       "cannotauth-not-allowed-title": "Title of the error page shown when the user tries t use an authentication-related page they should not have access to.",
+       "cannotauth-not-allowed": "Text of the error page shown when the user tries t use an authentication-related page they should not have access to.",
+       "changecredentials": "Title of the special page [[Special:ChangeCredentials]] which allows changing authentication credentials (such as the password).",
+       "changecredentials-submit": "Used on [[Special:ChangeCredentials]].",
+       "changecredentials-submit-cancel": "Used on [[Special:ChangeCredentials]].\n{{Identical|Cancel}}",
+       "changecredentials-invalidsubpage": "Error message shown when using [[Special:ChangeCredentials]] with an invalid type.\n\nParameters:\n* $1 - subpage name.",
+       "changecredentials-success": "Success message after using [[Special:ChangeCredentials]].",
+       "removecredentials": "Title of the special page [[Special:RemoveCredentials]] which allows removing authentication credentials (such as a two-factor token).",
+       "removecredentials-submit": "Used on [[Special:RemoveCredentials]].",
+       "removecredentials-submit-cancel": "Used on [[Special:RemoveCredentials]].\n{{Identical|Cancel}}",
+       "removecredentials-invalidsubpage": "Error message shown when using [[Special:RemoveCredentials]] with an invalid type.\n\nParameters:\n* $1 - subpage name.",
+       "removecredentials-success": "Success message after using [[Special:RemoveCredentials]].",
+       "credentialsform-provider": "Shown on [[Special:ChangeCredentials]]/[[Special:RemoveCredentials]] as the label for the authentication type (e.g. \"password\", \"English Wikipedia via OAuth\")",
+       "credentialsform-account": "Shown on [[Special:ChangeCredentials]]/[[Special:RemoveCredentials]] as the label for the account name",
+       "cannotlink-no-provider-title": "Error page title shown when the user visits [[Special:LinkAccounts]] but there is no external account provider that could be linked.",
+       "cannotlink-no-provider": "Error message shown when the user visits [[Special:LinkAccounts]] but there is no external account provider that could be linked.",
+       "linkaccounts": "Title of the special page [[Special:LinkAccounts]] which allows the user to connect the local user accounts with external ones such as Google or Facebook.",
+       "linkaccounts-success-text": "Text shown on top of the form after a successful action.",
+       "linkaccounts-submit": "Text of the main submit button on [[Special:LinkAccounts]] (when there is one)",
+       "unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.",
+       "unlinkaccounts-success": "Account unlinking form success message"
 }
index 65d39e5..a77384d 100644 (file)
        "noname": "Manam niwarqankichu ruraqpa allin sutinta.",
        "loginsuccesstitle": "Llamk'apuy tiyayqa qallarisqañam",
        "loginsuccess": "Llamk'apuy tiyayniykiqa qallarisqam {{SITENAME}}-pi \"$1\" sutiyuq kaspa.",
-       "nosuchuser": "Nisqayki \"$1\" sutiyuq ruraqqa manam kanchu.\nRuraqpa sutinqa uchuypas hatunpas sanampakunayuqmi kayta atin.\nAllin qillqasqaykita llanchiriy, ichataq urapi kaq hunt'ana p'anqata llamk'achiy [[Special:UserLogin/signup|musuq rakiqunata kicharinaykipaq]].",
+       "nosuchuser": "Nisqayki \"$1\" sutiyuq ruraqqa manam kanchu.\nRuraqpa sutinqa uchuypas hatunpas sanampakunayuqmi kayta atin.\nAllin qillqasqaykita llanchiriy, ichataq urapi kaq hunt'ana p'anqata llamk'achiy [[Special:CreateAccount|musuq rakiqunata kicharinaykipaq]].",
        "nosuchusershort": "Nisqayki \"$1\" sutiyuq ruraqqa manam kanchu.\nAllin qillqasqaykita llanchiriy.",
        "nouserspecified": "Ruraqpa sutiykitam qunayki.",
        "login-userblocked": "Kay ruraqqa hark'asqam. Manam yaykuyta atinchu.",
        "accmailtext": "Kikinmanta kamarisqa [[User talk:$1|$1]]-paq yaykuna rimaqa $2-manmi kachasqaña.\n\nYaykurqaspaqa ''[[Special:ChangePassword|yaykuna rima hukchana]]'' p'anqapi kay yaykuna rimata hukchaytam atinki.",
        "newarticle": "(Musuq)",
        "newarticletext": "Manaraq kachkaq p'anqatam llamk'apuchkanki. Musuq p'anqata kamariyta munaspaykiqa, qillqarillay. Astawan ñawiriyta munaspaykiqa, [$1 yanapana p'anqata] qhaway. Mana munaspaykitaq, ñawpaq p'anqaman ripuy.",
-       "anontalkpagetext": "---- ''Kayqa huk sutinnaq icha mana sutinta llamk'achiq ruraqpa rimanakuyninmi. IP huchhantam hallch'asunchik payta sutinchanapaq. Achka ruraqkunam huklla IP huchhanta llamk'achiyta atin. Sutinnaq ruraq kaspaykiqa, mana qampa rurasqaykimanta willamusqakunata rikuspaykiqa, ama hina kaspa [[Special:UserLogin/signup|rakiqunaykita kamariy]] icha [[Special:UserLogin|yaykuy]] huk sutinnaq ruraqkunawan ama pantasqa kanaykipaq.''",
+       "anontalkpagetext": "---- ''Kayqa huk sutinnaq icha mana sutinta llamk'achiq ruraqpa rimanakuyninmi. IP huchhantam hallch'asunchik payta sutinchanapaq. Achka ruraqkunam huklla IP huchhanta llamk'achiyta atin. Sutinnaq ruraq kaspaykiqa, mana qampa rurasqaykimanta willamusqakunata rikuspaykiqa, ama hina kaspa [[Special:CreateAccount|rakiqunaykita kamariy]] icha [[Special:UserLogin|yaykuy]] huk sutinnaq ruraqkunawan ama pantasqa kanaykipaq.''",
        "noarticletext": "Kunanqa kay p'anqa ch'usaqmi kachkan.\nKaytam rurayta atinkiman: kay p'anqap sutinta [[Special:Search/{{PAGENAME}}|huk p'anqakunapi maskay]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hallch'ankunapi maskay]\nicha [{{fullurl:{{FULLPAGENAME}}|action=edit}} kay p'anqata llamk'apuy]</span>.",
        "noarticletext-nopermission": "Kunanqa kay p'anqa ch'usaqmi kachkan.\nKaytam rurayta atinkiman: kay p'anqap sutinta [[Special:Search/{{PAGENAME}}|huk p'anqakunapi maskay]]\nicha payman kapuq <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hallch'akunapi maskay]</span>,\nichataq kay p'anqata kamariyta manam saqillasunkichu.",
        "missing-revision": "\"{{FULLPAGENAME}}\" nisqa p'anqapaq #$1 musuqchasqaqa manam kanchu.\n\nKayqa tukurqanman qullusqa p'anchaman t'inkimuq mawk'ayasqa wiñay kawsay t'inkiraykuchá.\nImaymanata [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} qulluy hallch'apim] tariykiman.",
index 73f33af..eb3ee7c 100644 (file)
        "noname": "Ti n'has betg inditgà in num d'utilisader valid.",
        "loginsuccesstitle": "T'annunzià cun success",
        "loginsuccess": "'''Ti es t'annunzia tar {{SITENAME}} sco \"$1\".'''",
-       "nosuchuser": "I n'exista nagin utilisader cun il num \"$1\".\nFa stim dad utilisar correctamain maiusclas e minusclas.\nCurregia il num u [[Special:UserLogin/signup|creescha in nov conto]].",
+       "nosuchuser": "I n'exista nagin utilisader cun il num \"$1\".\nFa stim dad utilisar correctamain maiusclas e minusclas.\nCurregia il num u [[Special:CreateAccount|creescha in nov conto]].",
        "nosuchusershort": "I na dat nagin utilisader cun il num \"$1\".\nCurregia ti'endataziun.",
        "nouserspecified": "Inditgescha per plaschair in num d'utilisader.",
        "login-userblocked": "Quest utilisader è bloccà. Betg pussaivel da t'annunziar.",
        "accmailtext": "In pled-clav casual per [[User talk:$1|$1]] è vegnì tramess a $2. El po vegnir midà sin la pagina ''[[Special:ChangePassword|midar pled-clav]]'' suenter che ti t'es annunzià.",
        "newarticle": "(Nov)",
        "newarticletext": "Ti has cliccà ina colliaziun ad ina pagina che n'exista anc betg. Per crear ina pagina, entschaiva a tippar en la stgaffa sutvart (guarda [$1 la pagina d'agid] per t'infurmar).",
-       "anontalkpagetext": "----''Quai è la pagina da discussiun per in utilisader anomim che n'ha anc betg creà in conto d'utilisader u che n'al utilisescha betg.\nPerquai avain nus d'utilisar l'adressa dad IP per l'identifitgar.\nIna tala adressa dad IP po vegnir utilisada da differents utilisaders.\nSche ti es in utilisaders anonim e pensas che commentaris che na pertutgan betg tai vegnan adressads a tai, lura [[Special:UserLogin/signup|creescha in conto]] u [[Special:UserLogin|t'annunzia]] per evitar en futur che ti vegns sbaglià cun auters utilisaders.''",
+       "anontalkpagetext": "----''Quai è la pagina da discussiun per in utilisader anomim che n'ha anc betg creà in conto d'utilisader u che n'al utilisescha betg.\nPerquai avain nus d'utilisar l'adressa dad IP per l'identifitgar.\nIna tala adressa dad IP po vegnir utilisada da differents utilisaders.\nSche ti es in utilisaders anonim e pensas che commentaris che na pertutgan betg tai vegnan adressads a tai, lura [[Special:CreateAccount|creescha in conto]] u [[Special:UserLogin|t'annunzia]] per evitar en futur che ti vegns sbaglià cun auters utilisaders.''",
        "noarticletext": "Quest artitgel na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar il term]] sin in'autra pagina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols],\nu [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear questa pagina]</span>.",
        "noarticletext-nopermission": "Questa pagina na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar quest titel]] en autras paginas u <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols correspundents]</span>, ma ti n'has betg ils dretgs da crear questa pagina.",
        "missing-revision": "La versiun #$1 da la pagina cun il num \"{{FULLPAGENAME}}\" n'exista betg.\n\nQuai capita savnes sche ti cliccas sin ina colliaziun antiquada en la cronologia per ina pagina ch'è vegnida stizzada.\nDetagls pon vegnri chattads en il [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protocol da stizzar].",
index fb7e4fc..cecb1be 100644 (file)
        "noname": "Numele de utilizator pe care l-ați introdus nu este valid.",
        "loginsuccesstitle": "Autentificat(ă)",
        "loginsuccess": "'''Ați fost autentificat la {{SITENAME}} ca „$1”.'''",
-       "nosuchuser": "Nu există nici un utilizator cu numele „$1”.\nNumele de utilizatori sunt sensibile la majuscule.\nVerifică dacă ai scris corect sau [[Special:UserLogin/signup|creează un nou cont de utilizator]].",
+       "nosuchuser": "Nu există nici un utilizator cu numele „$1”.\nNumele de utilizatori sunt sensibile la majuscule.\nVerifică dacă ai scris corect sau [[Special:CreateAccount|creează un nou cont de utilizator]].",
        "nosuchusershort": "Nu există niciun utilizator cu numele „$1”.\nVerificați ortografierea.",
        "nouserspecified": "Trebuie să specificați un nume de utilizator.",
        "login-userblocked": "Acest utilizator este blocat. Autentificarea nu este permisă.",
        "accmailtext": "O parolă generată aleator pentru [[User talk:$1|$1]] a fost trimisă la $2. Parola poate fi schimbată după autentificare din pagina ''[[Special:ChangePassword|schimbare parolă]]''.",
        "newarticle": "(Nou)",
        "newarticletext": "Ați încercat să ajungeți la o pagină care nu există. Pentru a o crea, începeți să scrieți în caseta de mai jos (vedeți [$1 pagina de ajutor] pentru mai multe informații). Dacă ați ajuns aici din greșeală, întoarceți-vă folosind controalele navigatorului dumneavoastră.",
-       "anontalkpagetext": "---- ''Aceasta este pagina de discuții pentru un utilizator care nu și-a creat un cont încă, sau care nu s-a autentificat.\nDe aceea trebuie să folosim adresă IP pentru a identifica această persoană.\nO adresă IP poate fi folosită în comun de mai mulți utilizatori.\nDacă sunteți un astfel de utilizator și credeți că vă sunt adresate mesaje irelevante, vă rugăm să [[Special:UserLogin/signup|vă creați un cont]] sau să [[Special:UserLogin|vă autentificați]] pentru a evita confuzii cu alți utilizatori anonimi în viitor.''",
+       "anontalkpagetext": "---- ''Aceasta este pagina de discuții pentru un utilizator care nu și-a creat un cont încă, sau care nu s-a autentificat.\nDe aceea trebuie să folosim adresă IP pentru a identifica această persoană.\nO adresă IP poate fi folosită în comun de mai mulți utilizatori.\nDacă sunteți un astfel de utilizator și credeți că vă sunt adresate mesaje irelevante, vă rugăm să [[Special:CreateAccount|vă creați un cont]] sau să [[Special:UserLogin|vă autentificați]] pentru a evita confuzii cu alți utilizatori anonimi în viitor.''",
        "noarticletext": "Actualmente, această pagină este lipsită de conținut.\nPuteți [[Special:Search/{{PAGENAME}}|căuta titlul acestei pagini]] în alte pagini, puteți <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} căuta înregistrări în jurnalele relevante]\nsau puteți [{{fullurl:{{FULLPAGENAME}}|action=edit}} crea această pagină]</span>.",
        "noarticletext-nopermission": "Actualmente, această pagină este lipsită de conținut.\nPuteți [[Special:Search/{{PAGENAME}}|căuta acest titlu]] în alte pagini sau puteți <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} căuta înregistrări în jurnale]</span>; nu aveți însă permisiunea de a crea această pagină.",
        "missing-revision": "Versiunea nr. $1 a paginii „{{FULLPAGENAME}}” nu există.\n\nAcest lucru se întâmplă de obicei atunci când se accesează o legătură expirată către istoricul unei pagini șterse.\nDetalii se pot găsi în [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} jurnalul ștergerilor].",
        "upload-form-label-own-work": "Aceasta este propria mea operă",
        "upload-form-label-infoform-categories": "Categorii",
        "upload-form-label-infoform-date": "Dată",
-       "upload-form-label-own-work-message-local": "Confirm că încarc acest fișier în concordanță cu termenii serviciului și politicile de licențiere de la {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Dacă nu puteți încărca acest fișier în conformitate cu politicile de la {{SITENAME}}, închideți această casetă de dialog și încercați altă metodă.",
-       "upload-form-label-not-own-work-local-local": "Poate doriți să încercați [[Special:Upload|pagina de încărcare implicită]].",
-       "upload-form-label-own-work-message-default": "Înțeleg că încarc acest fișier într-un depozit partajat. Confirm că fac acest lucru conform termenilor serviciului și politicilor de licențiere de acolo.",
-       "upload-form-label-not-own-work-message-default": "Dacă nu puteți încărca acest fișier în conformitate cu politicile depozitului partajat, închideți această casetă de dialog și încercați altă metodă.",
-       "upload-form-label-not-own-work-local-default": "Poate doriți să încercați [[Special:Upload|pagina de încărcare de la {{SITENAME}}]], în cazul în care acest fișier poate fi încărcat acolo în conformitate cu politicele lor.",
-       "upload-form-label-own-work-message-shared": "Declar că dețin drepturile de autor asupra acestui fișier, accept să public irevocabil acest fișier la Wikimedia Commons sub licența [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Atribuire și distribuire în condiții identice 4.0] și sunt de acord cu [https://wikimediafoundation.org/wiki/Terms_of_Use Termenii de utilizare].",
-       "upload-form-label-not-own-work-message-shared": "Dacă nu dețineți drepturile de autor asupra acestui fișier sau doriți să-l publicați sub o altă licență, puteți utiliza [https://commons.wikimedia.org/wiki/Special:UploadWizard Expertul de încărcare de la Commons].",
-       "upload-form-label-not-own-work-local-shared": "Poate doriți să încercați [[Special:Upload|pagina de încărcare de la {{SITENAME}}]], în cazul în care acest site permite încărcarea acestui fișier în conformitate cu politicele lor.",
+       "upload-form-label-own-work-message-generic-local": "Confirm că încarc acest fișier în concordanță cu termenii serviciului și politicile de licențiere de la {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Dacă nu puteți încărca acest fișier în conformitate cu politicile de la {{SITENAME}}, închideți această casetă de dialog și încercați altă metodă.",
+       "upload-form-label-not-own-work-local-generic-local": "Poate doriți să încercați [[Special:Upload|pagina de încărcare implicită]].",
+       "upload-form-label-own-work-message-generic-foreign": "Înțeleg că încarc acest fișier într-un depozit partajat. Confirm că fac acest lucru conform termenilor serviciului și politicilor de licențiere de acolo.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Dacă nu puteți încărca acest fișier în conformitate cu politicile depozitului partajat, închideți această casetă de dialog și încercați altă metodă.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Poate doriți să încercați [[Special:Upload|pagina de încărcare de la {{SITENAME}}]], în cazul în care acest fișier poate fi încărcat acolo în conformitate cu politicele lor.",
        "backend-fail-stream": "Imposibil de citit fișierul $1.",
        "backend-fail-backup": "Imposibil de efectuat o copie de rezervă a fișierului $1.",
        "backend-fail-notexists": "Fișierul $1 nu există.",
index eb0bed1..dd73eab 100644 (file)
        "noname": "Non gìè specifichete 'nu nome utende valide.",
        "loginsuccesstitle": "Tutte a poste, è trasute!",
        "loginsuccess": "'''Mò tu si colleghete jndr'à {{SITENAME}} cumme \"$1\".'''",
-       "nosuchuser": "Non g'esiste n'utende cu 'u nome \"$1\".\nFà attenzione ca le nome de l'utinde so senzibbele a le lettere granne e piccenne.\nVide bbuene a cumme l'è scritte, o [[Special:UserLogin/signup|ccreje n'utende nuève]].",
+       "nosuchuser": "Non g'esiste n'utende cu 'u nome \"$1\".\nFà attenzione ca le nome de l'utinde so senzibbele a le lettere granne e piccenne.\nVide bbuene a cumme l'è scritte, o [[Special:CreateAccount|ccreje n'utende nuève]].",
        "nosuchusershort": "Non ge ste nisciune utende cu 'u nome \"$1\".\nCondrolle accume l'è scritte.",
        "nouserspecified": "A scrivere pe forze 'u nome de l'utende.",
        "login-userblocked": "Stu utende jè bloccate. Non ge puè trasè.",
        "accmailtext": "'A passuord ccrejate a uecchije pe [[User talk:$1|$1]] ha state mannate sus a $2.\n\n'A passuord pe stu cunde utende pò essere cangiate sus a pàgene ''[[Special:ChangePassword|cange passuord]]'' 'na vote ca è trasute.",
        "newarticle": "(Nuève)",
        "newarticletext": "Tu ste segue 'nu collegamende a pàgene ca angore non g'esiste.\nPe ccrejà 'a pàgene, accuminze a scrivere jndr'à 'u scatole de sotte (vide 'a [$1 pàggene d'ajute] pe avè cchiù 'mbormaziune).\nCe tu te iacche aqquà e manghe tu 'u se purcè, allore cazze 'u buttone '''back''' d'u brauser.",
-       "anontalkpagetext": "----''Queste jè 'na pàgene de 'ngazzaminde pe 'n'utende anonime, ca non ge vò ccu ccreje angore 'nu cunde utende, o de ce non g'u use.\nNuje auseme 'n'indirizze IP (ca jè numereche) pe identificarle.\nE' normale ca essende 'n'indirizze IP pò essere ausete pure da otre utinde ca 'u pigghiene.\nCe tu non ge si 'n'utende anonime e pinze ca le commende ca so revolte a te sonde studecarije, pe piacere [[Special:UserLogin/signup|ccreje 'nu cunde utende]] o [[Special:UserLogin|tràse]] pe no fà confusione jndr'à 'u future cu otre utinde anoneme.''",
+       "anontalkpagetext": "----''Queste jè 'na pàgene de 'ngazzaminde pe 'n'utende anonime, ca non ge vò ccu ccreje angore 'nu cunde utende, o de ce non g'u use.\nNuje auseme 'n'indirizze IP (ca jè numereche) pe identificarle.\nE' normale ca essende 'n'indirizze IP pò essere ausete pure da otre utinde ca 'u pigghiene.\nCe tu non ge si 'n'utende anonime e pinze ca le commende ca so revolte a te sonde studecarije, pe piacere [[Special:CreateAccount|ccreje 'nu cunde utende]] o [[Special:UserLogin|tràse]] pe no fà confusione jndr'à 'u future cu otre utinde anoneme.''",
        "noarticletext": "Non ge stè scritte ninde jndr'à sta pàgene.\nTu puè [[Special:Search/{{PAGENAME}}|cercà pe quiste titele]] jndr'à otre pàggene, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}] oppure [{{fullurl:{{FULLPAGENAME}}|action=edit}} cange sta pàgene]</span>.",
        "noarticletext-nopermission": "Pe mò non ge stè teste jndr'à sta pàgene.\nTu puè [[Special:Search/{{PAGENAME}}|cercà pe stu titole]] jndr'à otre pàggene,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cirche jndr'à l'archivije cullegate]</span>, ma non ge tìne le permesse pe ccrejà sta pàgene.",
        "missing-revision": "'A revisione #$1 d'a pàgene chiamate \"{{FULLPAGENAME}}\" non g'esiste.\n\nQuiste succede normalmende purcé 'u cunde jè collegate a 'na pàgene ca ha state scangellate.\nLe dettaglie le puè acchià jndr'à l'[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} archivije de le scangellaziune].",
index bfd77ff..2e0be53 100644 (file)
        "noname": "Вы не указали допустимого имени участника.",
        "loginsuccesstitle": "Вход произведён",
        "loginsuccess": "Теперь вы работаете под именем $1.",
-       "nosuchuser": "Участника с именем «$1» не существует.\nИмена участников чувствительны к регистру букв.\nПроверьте правильность написания имени или [[Special:UserLogin/signup|создайте новую учётную запись]].",
+       "nosuchuser": "Участника с именем «$1» не существует.\nИмена участников чувствительны к регистру букв.\nПроверьте правильность написания имени или [[Special:CreateAccount|создайте новую учётную запись]].",
        "nosuchusershort": "Не существует участника с именем «$1». Проверьте написание имени.",
        "nouserspecified": "Вы должны указать имя участника.",
        "login-userblocked": "Этот участник заблокирован. Вход в систему не разрешён.",
        "accmailtext": "Сгенерированный случайным образом пароль для [[User talk:$1|$1]] выслан на адрес $2.\n\nПосле авторизации можно будет сменить пароль для этой учётной записи на ''[[Special:ChangePassword|специальной странице смены пароля]]''.",
        "newarticle": "(Новая)",
        "newarticletext": "Вы перешли по ссылке на страницу, которой пока не существует.\nЧтобы её создать, наберите текст в окне, расположенном ниже (подробнее см. [$1 справочную страницу]).\nЕсли вы оказались здесь по ошибке, просто нажмите кнопку '''назад''' своего браузера.",
-       "anontalkpagetext": "----''Эта страница обсуждения принадлежит анонимному участнику, который ещё не создал учётной записи, или не использует её.\nПоэтому для идентификации используется цифровой IP-адрес.\nЭтот же адрес может соответствовать нескольким другим участникам.\nЕсли вы анонимный участник и полагаете, что получили сообщения, адресованные не вам, пожалуйста, [[Special:UserLogin/signup|создайте учётную запись]] или [[Special:UserLogin|представьтесь системе]], чтобы впредь избежать возможной путаницы с другими анонимными участниками.''",
+       "anontalkpagetext": "----''Эта страница обсуждения принадлежит анонимному участнику, который ещё не создал учётной записи, или не использует её.\nПоэтому для идентификации используется цифровой IP-адрес.\nЭтот же адрес может соответствовать нескольким другим участникам.\nЕсли вы анонимный участник и полагаете, что получили сообщения, адресованные не вам, пожалуйста, [[Special:CreateAccount|создайте учётную запись]] или [[Special:UserLogin|представьтесь системе]], чтобы впредь избежать возможной путаницы с другими анонимными участниками.''",
        "noarticletext": "В настоящий момент текст на данной странице отсутствует.\nВы можете [[Special:Search/{{PAGENAME}}|найти упоминание данного названия]] на других страницах,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} найти соответствующие записи журналов]\nили '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} создать страницу с таким названием]'''</span>.",
        "noarticletext-nopermission": "В настоящее время на этой странице нет текста.\nВы можете [[Special:Search/{{PAGENAME}}|найти упоминание данного названия]] на других страницах,\nили <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} найти соответствующие записи журналов].</span> У вас нет разрешения создать данную страницу.",
        "missing-revision": "Версия $1 страницы «{{FULLPAGENAME}}» не существует.\n\nЭто обычно бывает, если последовать по устаревшей ссылке на страницу, которая была удалена.\nПодробности могут быть в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале удалений].",
        "right-override-export-depth": "экспортирование страниц, включая связанные страницы с глубиной до 5",
        "right-sendemail": "отправка электронной почты другим участникам",
        "right-passwordreset": "просмотр электронных писем с изменением пароля",
-       "right-managechangetags": "создание и удаление [[Special:Tags|меток]] из базы данных",
+       "right-managechangetags": "Создание и (де)активация [[Special:Tags|меток]]",
        "right-applychangetags": "применение [[Special:Tags|меток]] вместе со своими правками",
        "right-changetags": "добавление и удаление произвольных [[Special:Tags|меток]] на отдельных правках и записях в журнале",
+       "right-deletechangetags": "Удаление [[Special:Tags|меток]] из базы данных",
        "grant-generic": "Набор прав «$1»",
        "grant-group-page-interaction": "Взаимодействие со страницами",
        "grant-group-file-interaction": "Взаимодействие с медиафайлами",
        "action-viewmyprivateinfo": "просмотр вашей частной информации",
        "action-editmyprivateinfo": "редактирование вашей частной информации",
        "action-editcontentmodel": "редактирование контентной модели страницы",
-       "action-managechangetags": "создание и удаление меток из базы данных",
+       "action-managechangetags": "создание и (де)активацию меток",
        "action-applychangetags": " применять теги наряду с Вашими изменениями",
        "action-changetags": "Добавлять и удалять произвольные теги на отдельных изменениях и записях в журнале",
+       "action-deletechangetags": "удаление меток из базы данных",
        "nchanges": "$1 {{PLURAL:$1|изменение|изменения|изменений}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|с последнего посещения}}",
        "enhancedrc-history": "история",
        "upload-form-label-own-work": "Это моя собственная работа",
        "upload-form-label-infoform-categories": "Категории",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Я подтверждаю, что загружаю этот файл в соответствиями с правилами и лицензионной политикой сайта {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Если Вы не можете загрузить этот файл в соответствиями с правилами сайта {{SITENAME}}, пожалуйста, закройте это диалоговое окно и попробуйте другой метод.",
-       "upload-form-label-not-own-work-local-local": "Возможно, вы также захотите попробовать [[Special:Upload|страницу загрузки по умолчанию]].",
-       "upload-form-label-own-work-message-default": "Я понимаю, что загружаю этот файл в общий репозиторий. Я подтверждаю, что я делаю это в соответствии с пользовательским соглашением и лицензионной политикой.",
-       "upload-form-label-not-own-work-message-default": "Если Вы не можете загрузить этот файл в соответствиями с правилами общего хранилища, пожалуйста, закройте это диалоговое окно и попробуйте другой метод.",
-       "upload-form-label-not-own-work-local-default": "В том случае, если этот файл может быть загружен в соответствии с правилами сайта {{SITENAME}}, вы также можете попробовать использовать его [[Special:Upload|страницу загрузки]].",
-       "upload-form-label-own-work-message-shared": "Я подтверждаю, что являюсь владельцем авторских прав на этот файл, и соглашаюсь на безотзывной основе разместить этот файл на Викискладе под лицензией [https://creativecommons.org/licenses/by-sa/4.0/deed.ru Creative Commons Attribution-ShareAlike 4.0], а также соглашаюсь с [https://wikimediafoundation.org/wiki/Условия_использования Условиями использования].",
-       "upload-form-label-not-own-work-message-shared": "Если вы не являетесь владельцем авторских прав на этот файл, или вы хотите выпустить его под другой лицензией, рассмотрите возможность использования [https://commons.wikimedia.org/wiki/Special:UploadWizard Мастера загрузки на Викисладе].",
-       "upload-form-label-not-own-work-local-shared": "В том случае, если этот файл может быть загружен в соответствии с правилами сайта {{SITENAME}}, вы также можете попробовать использовать его [[Special:Upload|страницу загрузки]].",
+       "upload-form-label-own-work-message-generic-local": "Я подтверждаю, что загружаю этот файл в соответствиями с правилами и лицензионной политикой сайта {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Если Вы не можете загрузить этот файл в соответствиями с правилами сайта {{SITENAME}}, пожалуйста, закройте это диалоговое окно и попробуйте другой метод.",
+       "upload-form-label-not-own-work-local-generic-local": "Возможно, вы также захотите попробовать [[Special:Upload|страницу загрузки по умолчанию]].",
+       "upload-form-label-own-work-message-generic-foreign": "Я понимаю, что загружаю этот файл в общий репозиторий. Я подтверждаю, что я делаю это в соответствии с пользовательским соглашением и лицензионной политикой.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Если Вы не можете загрузить этот файл в соответствиями с правилами общего хранилища, пожалуйста, закройте это диалоговое окно и попробуйте другой метод.",
+       "upload-form-label-not-own-work-local-generic-foreign": "В том случае, если этот файл может быть загружен в соответствии с правилами сайта {{SITENAME}}, вы также можете попробовать использовать его [[Special:Upload|страницу загрузки]].",
        "backend-fail-stream": "Не удалось транслировать файл $1.",
        "backend-fail-backup": "Невозможно сделать резервную копию файла $1.",
        "backend-fail-notexists": "Файл $1 не существует.",
        "whatlinkshere-prev": "{{PLURAL:$1|1=предыдущая|предыдущие}} $1",
        "whatlinkshere-next": "{{PLURAL:$1|1=следующая|следующие}} $1",
        "whatlinkshere-links": "← ссылки",
-       "whatlinkshere-hideredirs": "$1 перенаправления",
-       "whatlinkshere-hidetrans": "$1 включения",
-       "whatlinkshere-hidelinks": "$1 ссылки",
-       "whatlinkshere-hideimages": "$1 файловые ссылки",
+       "whatlinkshere-hideredirs": "Скрыть перенаправления",
+       "whatlinkshere-hidetrans": "Скрыть включения",
+       "whatlinkshere-hidelinks": "Скрыть ссылки",
+       "whatlinkshere-hideimages": "Скрыть файловые ссылки",
        "whatlinkshere-filters": "Фильтры",
        "whatlinkshere-submit": "Выполнить",
        "autoblockid": "Автоблокировка #$1",
        "timezone-local": "Местное",
        "duplicate-defaultsort": "Внимание. Ключ сортировки по умолчанию «$2» переопределяет прежний ключ сортировки по умолчанию «$1».",
        "duplicate-displaytitle": "<strong>Внимание:</strong> Отображаемое название «$2» переопределяет ранее заданное отображаемое название «$1».",
+       "restricted-displaytitle": "<strong>Внимание:</strong> Отображаемое название «$1» было проигнорировано, поскольку она не соответствует актуальному названию страницы.",
        "invalid-indicator-name": "<strong>Ошибка:</strong> Атрибут <code>name</code> индикаторов состояния страницы не должен быть пустым.",
        "version": "Версия",
        "version-extensions": "Установленные расширения",
        "tags-delete-not-found": "Метка «$1» не существует.",
        "tags-delete-too-many-uses": "Метка «$1» применяется в более чем $2 {{PLURAL:$2|версии|версиям}}, что означает, что она не может быть удалена.",
        "tags-delete-warnings-after-delete": "Метка «$1» была удалена, но {{PLURAL:$2|было обнаружено следующее предупреждение|были обнаружены следующие предупреждения}}:",
+       "tags-delete-no-permission": "У вас нет прав на удаление изменений меток.",
        "tags-activate-title": "Активировать метку",
        "tags-activate-question": "Вы собираетесь активировать метку «$1».",
        "tags-activate-reason": "Причина:",
index 60b2992..b7f8c44 100644 (file)
        "noname": "Мусите увести мено свого конта.",
        "loginsuccesstitle": "Успішне приголошіня",
        "loginsuccess": "'''Теперь працуєте {{grammar:locative|{{SITENAME}}}} під меном $1.'''",
-       "nosuchuser": "Не екзістує хоснователь з меном «$1». У хосновательскых мен ся розлишують малы/великы писмена. Сконтролюйте запис, або собі [[Special:UserLogin/signup|зареґіструйте нове конто]].",
+       "nosuchuser": "Не екзістує хоснователь з меном «$1». У хосновательскых мен ся розлишують малы/великы писмена. Сконтролюйте запис, або собі [[Special:CreateAccount|зареґіструйте нове конто]].",
        "nosuchusershort": "Хоснователь з меном $1 не екзістує.\nПеревірте правилность написаня мена.",
        "nouserspecified": "Мусите задати мено хоснователя.",
        "login-userblocked": "Тот хоснователь є заблокованый. Приголошіня не є дозволене.",
        "accmailtext": "Трафунково выґенероване гесло про хоснователя [[User talk:$1|$1]] было послане на $2.\nОно може быти змінене на  [[Special:ChangePassword|сторінцї про зміну гесла]].",
        "newarticle": "(Нова)",
        "newarticletext": "Перешли сте на сторінку, котра іщі не екзістує.\nНову сторінку створите так, же зачнете писати в окнї ниже (вид. [$1 сторінка помочі], про вецей інформації).\nКідь сте ту помылково, просто кликните в переглядачу на '''назад'''",
-       "anontalkpagetext": "----''Тото є діскузна сторінка анонімного хоснователя, котрый іщі не має конто або го не хоснує. Про&nbsp;ёго ідентіфікацію прото мусиме хосновати IP адресу. Таку IP адресу може хосновати декілько хоснователїв. Покы сьте анонімный хоснователь і&nbsp;і уважуєте, же вам суть адресованы ірелевантны коментарї, просиме, [[Special:UserLogin/signup|створьте собі конто]] або [[Special:UserLogin|ся приголосте]], уникнете тым будучій замінї з&nbsp;іншыма анонімныма хоснователями.''",
+       "anontalkpagetext": "----''Тото є діскузна сторінка анонімного хоснователя, котрый іщі не має конто або го не хоснує. Про&nbsp;ёго ідентіфікацію прото мусиме хосновати IP адресу. Таку IP адресу може хосновати декілько хоснователїв. Покы сьте анонімный хоснователь і&nbsp;і уважуєте, же вам суть адресованы ірелевантны коментарї, просиме, [[Special:CreateAccount|створьте собі конто]] або [[Special:UserLogin|ся приголосте]], уникнете тым будучій замінї з&nbsp;іншыма анонімныма хоснователями.''",
        "noarticletext": "Теперь на тїй сторінцї не є текст.\nМожете [[Special:Search/{{PAGENAME}}|глядати тоту назву]] в іншых сторінках,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} глядати в лоґах]\nабо [{{fullurl:{{FULLPAGENAME}}|action=edit}} вытворити сторінку з таков назвов]</span>.",
        "noarticletext-nopermission": "Теперь на тій сторінцї тексту не є.\nМожете [[Special:Search/{{PAGENAME}}|глядати тоту назву]] в іншых сторінках, або <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} глядати в лоґах]</span>, но вы не мате права створити тоту сторінку.",
        "missing-revision": "Ревізія #$1 сторінкы з назвов „{{FULLPAGENAME}}“ не є.\n\nГевсе звычайно запрічінене так, же наслїдовали сьте застарїлый історічный одказ на сторінку, котра была уж змазана.\nДетайлы можуть быти найджены в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} книзї змазаных сторінок].",
index c9c9069..8b5d51c 100644 (file)
        "noname": "भवता/भवत्या योग्यं सदस्यनाम न प्रदत्तम् ।",
        "loginsuccesstitle": "स्वागतं ! प्रवेशः सिद्धः ।",
        "loginsuccess": "भवता/भवत्या {{SITENAME}} इत्यत्र \"$1\"-योजकत्वेन प्रवेशः प्राप्तः ।",
-       "nosuchuser": "'''$1''' नाम्नः न कोऽपि योजकः विद्यते ।\n\nप्रयोक्तृनामानि पक्षानुगुणं (case sensitive) भवन्ति ।\n\nयत् टङ्कितं, तत् पश्यतु अथवा [[Special:UserLogin/signup|नूतनसदस्यता प्राप्यताम्]] ।",
+       "nosuchuser": "'''$1''' नाम्नः न कोऽपि योजकः विद्यते ।\n\nप्रयोक्तृनामानि पक्षानुगुणं (case sensitive) भवन्ति ।\n\nयत् टङ्कितं, तत् पश्यतु अथवा [[Special:CreateAccount|नूतनसदस्यता प्राप्यताम्]] ।",
        "nosuchusershort": "'''$1''' नाम्नः न कोऽपि सदस्यः विद्यते ।\n\nयत् टङ्कितं, तत् पश्यतु ।",
        "nouserspecified": "भवता/भवत्या एकं योग्यं सदस्यनाम अवश्यमेव दातव्यम् ।",
        "login-userblocked": "एषः सदस्यः प्रतिबन्धितः । प्रवेष्टुम् अनुमतिः नास्ति ।",
        "accmailtext": "[[User talk:$1|$1]] कृते अशृङ्खलितरीत्या (randomly) उत्पादितः कूटशब्दः $2 वि-पत्रसङ्केतं प्रति प्रेषितः अस्ति । <em>[[Special:ChangePassword|कूटशब्दः परिवर्त्यताम्]]</em> अत्र तत् परिर्तयितुं शक्यते ।",
        "newarticle": "(नूतनम्)",
        "newarticletext": "भवान्/भवती अनिर्मिते पृष्ठे अस्ति । \nपृष्ठं स्रष्टुम् अधः प्रदत्तायां पेटिकायां टङ्कनं प्रारभताम् (साहाय्यार्थं [$1 अत्र]) नुदतु ।\nभवान्/भवती यदि क्षतिकारणात् एतत् पृष्ठं प्रति आगच्छत्, तर्हि अस्य गवेषकस्य (browser) Back नुदतु ।",
-       "anontalkpagetext": "----\n<em>एतत् सम्भाषणपृष्ठम् अनामकसदस्येभ्यः अस्ति । एतत् तेभ्यः अनामकसदस्येभ्यः रचितमस्ति, यैः सदस्यता न प्राप्ता अस्ति तथा च अस्य पृष्ठस्य उपयोगं न कुर्वन्तः सन्ति ।</em>\nतेषां व्यक्तिगतसूचनां प्राप्तुमेव वयं तस्य/तस्याः अन्तर्जालसंविदः उपयोगं कुर्मः । केचन सदस्याः स्वस्य अन्तर्जालसंविदम् अन्यान् सदस्यान् कथयन्ति । \nयद्यपि अनामकसदस्यः अहं नास्मि, तथापि अयोग्यसूचनाः मम पार्श्वे आगच्छन्त्यः सन्ति इति यदि भवान्/भवती शङ्कते, तर्हि एतत् [[Special:UserLogin/signup|create an account]] एतत् [[Special:UserLogin|log in]] वा कृत्वा भविष्यस्य अनामकसदस्यानां सन्देशेभ्यः स्वस्य रक्षणं करोतु ।",
+       "anontalkpagetext": "----\n<em>एतत् सम्भाषणपृष्ठम् अनामकसदस्येभ्यः अस्ति । एतत् तेभ्यः अनामकसदस्येभ्यः रचितमस्ति, यैः सदस्यता न प्राप्ता अस्ति तथा च अस्य पृष्ठस्य उपयोगं न कुर्वन्तः सन्ति ।</em>\nतेषां व्यक्तिगतसूचनां प्राप्तुमेव वयं तस्य/तस्याः अन्तर्जालसंविदः उपयोगं कुर्मः । केचन सदस्याः स्वस्य अन्तर्जालसंविदम् अन्यान् सदस्यान् कथयन्ति । \nयद्यपि अनामकसदस्यः अहं नास्मि, तथापि अयोग्यसूचनाः मम पार्श्वे आगच्छन्त्यः सन्ति इति यदि भवान्/भवती शङ्कते, तर्हि एतत् [[Special:CreateAccount|create an account]] एतत् [[Special:UserLogin|log in]] वा कृत्वा भविष्यस्य अनामकसदस्यानां सन्देशेभ्यः स्वस्य रक्षणं करोतु ।",
        "noarticletext": "अस्मिन् पृष्ठे अधुना किमपि न विद्यते । [[Special:Search/{{PAGENAME}}|एषः शब्दः]] येषु पृष्ठेषु अन्तर्भवति, तानि पृष्ठानि अन्वेष्टुं शक्यन्ते । \n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  सम्बद्धेषु पृष्ठेषु अन्वेषणं]\n[{{fullurl:{{FULLPAGENAME}}|action=edit}} अस्य पृष्ठस्य निर्माणं] वा  शक्यम्</span>.",
        "noarticletext-nopermission": "अस्मिन् पृष्ठे अधुना किमपि न विद्यते । [[Special:Search/{{PAGENAME}}|एषः शब्दः]] येषु पृष्ठेषु अन्तर्भवति, तानि पृष्ठानि अन्वेष्टुं शक्यन्ते । \n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  सम्बद्धेषु पृष्ठेषु अन्वेषणं]\n[{{fullurl:{{FULLPAGENAME}}|action=edit}} अस्य पृष्ठस्य सम्पादनं] वा  शक्यम्</span>.",
        "missing-revision": "\"{{FULLPAGENAME}}\" पृष्ठस्य संस्करणं #$1 नोपलभ्यम् ।\nयस्य पृष्ठस्य इतिहासे परिसन्धयः कालातीताः सन्ति, तेषु पृष्ठेषु एवं भवति ।\nअधिकसूचनाः अत्र प्राप्तुं शक्यते [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} अपाकृतानाम् आवलिः].",
index 5d6618f..deaa0d6 100644 (file)
        "noname": "Эн тиһилик билэр аатын киллэрбэтэххин.",
        "loginsuccesstitle": "Киирдиҥ",
        "loginsuccess": "'''Билигин бу аатынан үлэлиигин: \"$1\".'''",
-       "nosuchuser": "Маннык - \"$1\" - ааттаах кыттааччы суох.\nУлахан кыра буукубалар атыннаахтар.\nАатыҥ сөпкө суруллубутун көр эбэтэр [[Special:UserLogin/signup|саҥаттан бэлиэтэн]].",
+       "nosuchuser": "Маннык - \"$1\" - ааттаах кыттааччы суох.\nУлахан кыра буукубалар атыннаахтар.\nАатыҥ сөпкө суруллубутун көр эбэтэр [[Special:CreateAccount|саҥаттан бэлиэтэн]].",
        "nosuchusershort": "Маннык - \"$1\" - ааттаах кыттааччы суох. Аатыҥ сөпкө суруллубутун көр.",
        "nouserspecified": "Кыттааччы аатын киллэриэхтээххин.",
        "login-userblocked": "Бу кыттааччы бобуллубут. Тиһиккэ киирии көҥүллэммэт.",
        "accmailtext": "[[User talk:$1|$1]] кыттааччыга түбэспиччэ бэлиэлэртэн оҥоһуллубут киирии тыл бу аадырыска $2 ыытылынна.\nТиһиккэ бэлиэтэнэн баран киирии тылгын ''[[Special:ChangePassword|уларытыаххын]]'' сөп.",
        "newarticle": "(Саҥа ыстатыйа)",
        "newarticletext": "Эн суох сирэйгэ киирэ сатаатыҥ.\nМаннык ааттаах саҥа ыстатыйаны оҥорор буоллаххына, аллара баар түннүккэ суруй\n(сиһ. [$1 көмөнү] көрүөххүн сөп).\nӨскө манна сыыһа киирбит буоллаххына интэриниэтиҥ бырагыраамматын \"төнүн\" диэххин сөп.",
-       "anontalkpagetext": "----''Бу аатын эппэтэх кыттааччы ырытар сирэйэ.\nIP-аадырыһа эрэ көстөр.\nБиир IP-аадырыс хас да киһиэхэ бэриллиэн сөп. Өскө атын киһиэхэ суруллубут суругу алҕас туппут буоллаххына, бэйэҥ [[Special:UserLogin/signup|ааккын билиһиннэр]] эбэтэр [[Special:UserLogin|киир]], оччоҕо кэлин да булкуур тахсыа суоҕа.''",
+       "anontalkpagetext": "----''Бу аатын эппэтэх кыттааччы ырытар сирэйэ.\nIP-аадырыһа эрэ көстөр.\nБиир IP-аадырыс хас да киһиэхэ бэриллиэн сөп. Өскө атын киһиэхэ суруллубут суругу алҕас туппут буоллаххына, бэйэҥ [[Special:CreateAccount|ааккын билиһиннэр]] эбэтэр [[Special:UserLogin|киир]], оччоҕо кэлин да булкуур тахсыа суоҕа.''",
        "noarticletext": "Билигин бу сирэй кураанах.\nБу аат атын ыстатыйалга туттулларын [[Special:Search/{{PAGENAME}}|булуоххун сөп]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} сурунаалларга көрдүөххүн сөп],\nэбэтэр [{{fullurl:{{FULLPAGENAME}}|action=edit}} маннык ааттаах саҥа ыстатыйаны суруйуоххун] сөп</span>.",
        "noarticletext-nopermission": "Билигин бу сирэй кураанах.\nБу [[Special:Search/{{PAGENAME}}|ааты атын сирэйдэргэ көрдөөн көрүөххүн]] сөп,\nэбэтэр <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} сурунаалларга манна сыһыаннаах суруктары булуоххун сөп].</span> Бу сирэйи айар кыаҕыҥ суох.",
        "missing-revision": "«{{FULLPAGENAME}}» сирэй $1 барыла суох.\n\nМаннык үксүн хайыы-үйэ сотуллубут билэҕэ эргэрбит сигэнэн бардахха буолааччы.\nСиһилии баҕар [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} сотуу сурунаалыгар] баара буолуо.",
        "upload-form-label-own-work": "Бу бэйэм оҥоруум",
        "upload-form-label-infoform-categories": "Категорията",
        "upload-form-label-infoform-date": "Күнэ-дьыла",
-       "upload-form-label-own-work-message-local": "{{SITENAME}} быраабылатын уонна лиссиэнсийэлиир бэлиитикэтин тутуһан бу билэни киллэрэрбин бигэргэтэбин.",
-       "upload-form-label-not-own-work-message-local": "Бу билэҕин {{SITENAME}} быраабылатынан угар кыаҕыҥ суох буоллаҕына, маны сап уонна атын ньыманан туһанан көр.",
-       "upload-form-label-not-own-work-local-local": "Баҕар [[Special:Upload|киллэрии сүрүн ньыматын]] туһаныаххын баҕарыаҥ.",
-       "upload-form-label-own-work-message-default": "Уопсай репозиторийга угарбын өйдөөн туран угабын. Туһаныы сиэрин уонна лиссиэнсийэлиир бэлиитикэни кытта сөп түбэһэрин мэктиэлиибин.",
-       "upload-form-label-not-own-work-message-default": "Бу билэҕин уопсай репозиторий быраабылатынан угар кыаҕыҥ суох буоллаҕына, маны сап уонна атын ньыманы туһанан көр.",
-       "upload-form-label-not-own-work-local-default": "Өскө, {{SITENAME}} быраабылатынан угар сатанар буоллаҕына, кини [[Special:Upload|киллэрии тэрилин]] туһаныаххын сөп.",
-       "upload-form-label-own-work-message-shared": "Бу билэни бас билэрбин уонна Биики Ыскылаакка төнүннэрбэттии [https://creativecommons.org/licenses/by-sa/4.0/deed.ru Creative Commons Attribution-ShareAlike 4.0] лиссиэнсийэннэн угары бигэргэтэбин. Ону тэҥэ [https://wikimediafoundation.org/wiki/Условия_использования Туһаныы усулуобуйатын кытта] сөбүлэһэбин.",
-       "upload-form-label-not-own-work-message-shared": "Өскөтө билэни бас билбэт буоллаххына, биитэр атын лиссиэнсийэннэн угуоххун баҕарар буоллаххына, манна баар ньыманы туһаныаххын сөп: [https://commons.wikimedia.org/wiki/Special:UploadWizard Биики Ыскылаакка угуу маастара].",
-       "upload-form-label-not-own-work-local-shared": "Өскө, {{SITENAME}} быраабылатынан угар сатанар буоллаҕына, кини [[Special:Upload|киллэрии тэрилин]] туһаныаххын эмиэ сөп.",
+       "upload-form-label-own-work-message-generic-local": "{{SITENAME}} быраабылатын уонна лиссиэнсийэлиир бэлиитикэтин тутуһан бу билэни киллэрэрбин бигэргэтэбин.",
+       "upload-form-label-not-own-work-message-generic-local": "Бу билэҕин {{SITENAME}} быраабылатынан угар кыаҕыҥ суох буоллаҕына, маны сап уонна атын ньыманан туһанан көр.",
+       "upload-form-label-not-own-work-local-generic-local": "Баҕар [[Special:Upload|киллэрии сүрүн ньыматын]] туһаныаххын баҕарыаҥ.",
+       "upload-form-label-own-work-message-generic-foreign": "Уопсай репозиторийга угарбын өйдөөн туран угабын. Туһаныы сиэрин уонна лиссиэнсийэлиир бэлиитикэни кытта сөп түбэһэрин мэктиэлиибин.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Бу билэҕин уопсай репозиторий быраабылатынан угар кыаҕыҥ суох буоллаҕына, маны сап уонна атын ньыманы туһанан көр.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Өскө, {{SITENAME}} быраабылатынан угар сатанар буоллаҕына, кини [[Special:Upload|киллэрии тэрилин]] туһаныаххын сөп.",
        "backend-fail-stream": "$1 билэни ыытар табыллыбата.",
        "backend-fail-backup": "Бу билэ $1 резервнэй куопуйатын оҥорор табыллыбата.",
        "backend-fail-notexists": "Маннык $1 билэ суох эбит.",
index 332e9f6..5a1c117 100644 (file)
        "noname": "Su nùmene impitadore insertadu no est vàlidu.",
        "loginsuccesstitle": "Ti ses identificadu",
        "loginsuccess": "'''Immoe ses intradu in {{SITENAME}} cun su nùmene impitadore \"$1\".'''",
-       "nosuchuser": "Non b'at impitadore cun su nùmene \"$1\".\nIs nùmenes impitadore sunt sensìbiles a is lìteras mannas.\nVerìfica su nùmene insertadu o [[Special:UserLogin/signup|crea unu contu nou]].",
+       "nosuchuser": "Non b'at impitadore cun su nùmene \"$1\".\nIs nùmenes impitadore sunt sensìbiles a is lìteras mannas.\nVerìfica su nùmene insertadu o [[Special:CreateAccount|crea unu contu nou]].",
        "nosuchusershort": "Non b'est perunu impitadore chi tenet \"$1\" comente nùmene.\nCòmpida su nùmene insertadu.",
        "nouserspecified": "Depes ispetzificare unu nùmene impitadore.",
        "login-userblocked": "{{GENDER:$1|Custu impitadore est arreadu|Custa impidadora est arreada}}. Atzessu no adduidu.",
        "accmailtext": "Una password ingendrada a manera casuale pro [[User talk:$1|$1]] est istada imbiada a $2. Podet èssere cambiada in sa pàgina de <em>[[Special:ChangePassword|càmbiu password]]</em> a pustis de èssere intradu in su contu tuo.",
        "newarticle": "(Nou)",
        "newarticletext": "Custa pàgina no esistit galu.\nPro creare sa pàgina, iscrie in sa casella a suta (càstia sa [$1 pàgina de agiudu] pro àteras informatziones).\nSi ses intradu inoghe pro isbàlliu, carca su butone <strong>back/indietro</strong> in su navigadore tuo.",
-       "anontalkpagetext": "----\n<em>Custa est sa pàgina de cuntierra de unu impitadore anònimu ki no at creadu unu contu galu, o ki non dd'usat.</em>\nPro custu impreamus su nùmeru de indiritzos IP pro ddu identificare. Is indiritzos IP podent perou èsser cundivìdidos dae unos cantos impitadores. Si ses unu impitadore anònimu e ritenes ki custos cummentos non sunt diretos a tue, pro praxere [[Special:UserLogin/signup|crea unu contu]] o [[Special:UserLogin|identifica·ti (log in)]] pro evitare cunfusione cun àteros impitadore anònimos.''",
+       "anontalkpagetext": "----\n<em>Custa est sa pàgina de cuntierra de unu impitadore anònimu ki no at creadu unu contu galu, o ki non dd'usat.</em>\nPro custu impreamus su nùmeru de indiritzos IP pro ddu identificare. Is indiritzos IP podent perou èsser cundivìdidos dae unos cantos impitadores. Si ses unu impitadore anònimu e ritenes ki custos cummentos non sunt diretos a tue, pro praxere [[Special:CreateAccount|crea unu contu]] o [[Special:UserLogin|identifica·ti (log in)]] pro evitare cunfusione cun àteros impitadore anònimos.''",
        "noarticletext": "In custu momentu sa pàgina est bùida.\nPodes [[Special:Search/{{PAGENAME}}|chircare custu tìtulu]] in àteras pàginas, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chircare in is registros ligados] o puru [{{fullurl:{{FULLPAGENAME}}|action=edit}} modificare sa pàgina]</span>.",
        "noarticletext-nopermission": "In custu tempu sa pàgina rechesta est bùida.\nPodes [[Special:Search/{{PAGENAME}}|chircare custu tìtulu]] in is àteras pàginas, o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} chircare in is regìstros ligados]</span>, ma non tenes su permissu de creare custa pàgina.",
        "userpage-userdoesnotexist": "Su contu de s'impitadore \"<nowiki>$1</nowiki>\" no est stadu registradu.\nPro praxere abbàida si boles a sèriu creare/cambiare custa pàgina.",
index b50b5b1..ef529ee 100644 (file)
        "noname": "Nun spicificasti nu nomu utenti vàlidu.",
        "loginsuccesstitle": "Trasuta rinisciuta",
        "loginsuccess": "<strong>Ora trasisti nta {{SITENAME}} comu \"$1\".</strong>",
-       "nosuchuser": "Nun è riggistratu nuddu utenti di nomu \"$1\".\nLi nomi di l'utenti fannu diffirenza tra maiùsculi e minùsculi.\nCuntrolla chi scrivisti lu nomu bonu, o puru [[Special:UserLogin/signup|crea un cuntu novu]].",
+       "nosuchuser": "Nun è riggistratu nuddu utenti di nomu \"$1\".\nLi nomi di l'utenti fannu diffirenza tra maiùsculi e minùsculi.\nCuntrolla chi scrivisti lu nomu bonu, o puru [[Special:CreateAccount|crea un cuntu novu]].",
        "nosuchusershort": "Nun è riggistratu nuddu utenti di nomu \"$1\".\nCuntrolla chi scrivisti lu nomu bonu.",
        "nouserspecified": "Hai a spicificari un nomu utenti.",
        "login-userblocked": "St'utenti è bluccatu. Nun è pussìbbili di tràsiri.",
        "accmailtext": "Na password ginirata casualmenti pi [[User talk:$1|$1]] fu spiduta a $2. Si pò canciari dâ pàggina di <em>[[Special:ChangePassword|canciamentu dâ password]]</em> comu unu trasi.",
        "newarticle": "(Novu)",
        "newarticletext": "Siguisti na lijami a na pàggina c'ancora nun esisti.\nPi criari sta pàggina, accumenza a scrìviri ccassutta (talìa la [$1 pàggina d'aiutu] p'aviri maiuri nfurmazzioni).\nSi agghicasti ccà pi sbagghiu, carca lu buttuni <strong>n arreri</strong> dû tò browser.",
-       "anontalkpagetext": "----''Chista è la pàggina di discussioni di n’utenti anònimu, ca nun criau ancora n’accessu o ca nun l’usa.\nP’idintificàrilu è pirciò nicissariu usari lu nùmmiru di lu sò nnirizzu IP.\nLi nnirizzi IP ponnu pirò èssiri spartuti di cchiù utenti.\nSiddu sî n’utenti anònimu e riteni ca li cummenti prisenti nta sta pàggina nun si rifirìscinu a tia, [[Special:UserLogin/signup|crea n’accessu novu]] o [[Special:UserLogin|trasi]] cu chiddu ca già hai p’evitari d’èssiri cunfusu cu àutri utenti anònimi ‘n futuru.''",
+       "anontalkpagetext": "----''Chista è la pàggina di discussioni di n’utenti anònimu, ca nun criau ancora n’accessu o ca nun l’usa.\nP’idintificàrilu è pirciò nicissariu usari lu nùmmiru di lu sò nnirizzu IP.\nLi nnirizzi IP ponnu pirò èssiri spartuti di cchiù utenti.\nSiddu sî n’utenti anònimu e riteni ca li cummenti prisenti nta sta pàggina nun si rifirìscinu a tia, [[Special:CreateAccount|crea n’accessu novu]] o [[Special:UserLogin|trasi]] cu chiddu ca già hai p’evitari d’èssiri cunfusu cu àutri utenti anònimi ‘n futuru.''",
        "noarticletext": "Nta stu mumentu la pàggina addumannata è vacanti. È pussìbbili [[Special:Search/{{PAGENAME}}|circari stu tìtulu]] nta l'àutri pàggini dû situ oppuru <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|action=edit}} circari ntê riggistri culligati] oppuru [{{fullurl:{{FULLPAGENAME}}|action=edit}} canciari la pàggina ora]</span>.",
        "noarticletext-nopermission": "Nta stu mumentu la pàggina addumannata è vacanti. È pussìbbili [[Special:Search/{{PAGENAME}}|circari stu tìtulu]] nti àutri pàggini dû situ o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} circari ntê riggistri culligati]</span>, ma nun hai li pirmissi pi criari sta pàggina.",
        "missing-revision": "La virsioni #$1 dâ paggina ntitulata \"{{FULLPAGENAME}}\" nun esisti.\n\nStu fattu di sòlitu succedi quannu si segui nu lijami di crunuluggìa versu na pàggina chi fu cancillata.\nSi ponnu vìdiri li dittagghî ntô [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} riggistru dî cancillazzioni].",
index 4b69dfe..6f6ec3b 100644 (file)
        "noname": "Ye'v na speceefie'd ae valid uisername.",
        "loginsuccesstitle": "Login fine",
        "loginsuccess": "<strong>Ye're nou loggit in tae {{SITENAME}} aes \"$1\".</strong>",
-       "nosuchuser": "Thaur's nae sic uiser aes \"$1\".\nUiser names ar case-sensiteeve.\nCheck yer speelin, or [[Special:UserLogin/signup|mak ae new accoont]].",
+       "nosuchuser": "Thaur's nae sic uiser aes \"$1\".\nUiser names ar case-sensiteeve.\nCheck yer speelin, or [[Special:CreateAccount|mak ae new accoont]].",
        "nosuchusershort": "Thaur's nae sic uiser aes \"$1\". Check yer spellin.",
        "nouserspecified": "Ye hae tae merk up ae uisername.",
        "login-userblocked": "Uiser \"$1\" is blockit. Log-in naw permitit.",
        "accmailtext": "Ae randomly generated passwaird fer [[User talk:$1|$1]] haes been sent til $2. It can be chynged oan the <em>[[Special:ChangePassword|chynge passwaird]]</em> page upo loggin in.",
        "newarticle": "(New)",
        "newarticletext": "Ye'v follaed aen airtin til ae page that disna exeest yet. Tae cræft the page, stairt typin in the kist ablo (see the [$1 heelp page] fer mair info). Gif ye'r here bi mistak, jist clap yer brouser's <strong>back</strong> button.",
-       "anontalkpagetext": "----\n<em>This is the discussion page fer aen anonymoos uiser that's naw cræftit aen accoont yet, or that disna uise it.</em>\nWe maun therefore uise the numerical IP address tae identifie him/her.\nSic aen IP address can be shaired bi several uisers.\nGif ye'r aen anonymos uiser n feel that onreelavant comments hae been directed at ye, please [[Special:UserLogin/signup|cræft aen accoont]] or [[Special:UserLogin|log in]] tae avoid futur confusion wi ither anonymoos uisers.",
+       "anontalkpagetext": "----\n<em>This is the discussion page fer aen anonymoos uiser that's naw cræftit aen accoont yet, or that disna uise it.</em>\nWe maun therefore uise the numerical IP address tae identifie him/her.\nSic aen IP address can be shaired bi several uisers.\nGif ye'r aen anonymos uiser n feel that onreelavant comments hae been directed at ye, please [[Special:CreateAccount|cræft aen accoont]] or [[Special:UserLogin|log in]] tae avoid futur confusion wi ither anonymoos uisers.",
        "noarticletext": "Thaur's naw tex oan this page the nou. \nYe can [[Special:Search/{{PAGENAME}}|rake fer this page teitle]] in ither pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rake the related logs],\nor [{{fullurl:{{FULLPAGENAME}}|action=edit}} eedit this page].</span>",
        "noarticletext-nopermission": "Thaur's nae tex in this page the nou.\nYe can [[Special:Search/{{PAGENAME}}|rake fer this page title]] in ither pages, or <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rake the relatit logs]</span>, but ye dinna hae permeession tae cræft this page.",
        "missing-revision": "The reveesion #$1 o the page named \"{{FULLPAGENAME}}\" disna exeest.\n\nThis is uissuallie caused bi follaein aen ootdated histerie airtin til ae page that haes been delytit.\nDetails can be foond in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} delytion log].",
index 11f8af2..23a7daa 100644 (file)
        "noname": "توهان جو ڄاڻايل يُوزرنانءُ ناقابل ڪار آهي.",
        "loginsuccesstitle": "لاگ اِن ڪامياب",
        "loginsuccess": "'''هاڻي توهان {{SITENAME}} تي بطور \"$1\" لاگ اِن ٿيل آهيو.'''",
-       "nosuchuser": "\"$1\" نالي سان ڪو بہ يوزر نہ آهي.  \"$1\".\n ننڍن وڏن اکرن ۾ امتياز ڪرڻ لازمي آهي. \nهِجي چڪاسيو،يا [[Special:UserLogin/signup|نئون کاتو تخليق ڪريو]]",
+       "nosuchuser": "\"$1\" نالي سان ڪو بہ يوزر نہ آهي.  \"$1\".\n ننڍن وڏن اکرن ۾ امتياز ڪرڻ لازمي آهي. \nهِجي چڪاسيو،يا [[Special:CreateAccount|نئون کاتو تخليق ڪريو]]",
        "nosuchusershort": "\"$1\" نالي ڪو بہ يُوزر ناهي.\nهِجي جي پڪ ڪندا.",
        "nouserspecified": "توهان کي ڪو يوزرنانءُ ڄاڻائڻو پوندو.",
        "login-userblocked": "هيءُ يُوزر بندشيل آهي. لاگ اِن جي اجازت نہ ٿي ڏجي.",
index 2488e06..5570761 100644 (file)
        "noname": "War mana goykaw maa henna kayandi.",
        "loginsuccesstitle": "Huryanoo boori",
        "loginsuccess": "<strong>War huru {{SITENAME}} ra sohõ sanda \"$1\".</strong>",
-       "nosuchuser": "Goykaw kul šii kaŋ ti \"$1\".  \nGoykawmaaɲey ga kula nda harfu azzaati.\nWar hantumoo koroši, wala [[Special:UserLogin/signup|kontu taaga tee]].",
+       "nosuchuser": "Goykaw kul šii kaŋ ti \"$1\".  \nGoykawmaaɲey ga kula nda harfu azzaati.\nWar hantumoo koroši, wala [[Special:CreateAccount|kontu taaga tee]].",
        "nosuchusershort": "Goykaw kul šii kaŋ ti \"$1\".\nHantum-tenjiyan koroši.",
        "nouserspecified": "War ga hima ka goykawmaa kayandi.",
        "login-userblocked": "Goykaw gagayandi. Huruyan šii nda fondo.",
        "accmailtext": "Ɲaami-ra šennikufal kaŋ tee  [[User talk:$1|$1]] sanbandi $2 do. A ga hin ka barmay  <em>[[Special:ChangePassword|šennikufal barmay]] moɲoo ga </em> nda war ga huru.",
        "newarticle": "(Itaaga)",
        "newarticletext": "War hanga dobu kaŋ ka fatta moo foo kaŋ ši bara jina ga.\nKa moɲoo tee, soobay ka hantum ganda bataa ra ([$1 faaba moɲoo] guna ka bay ka tonton.\nNda war n' ka dere ka kaa ne, war ceecikaa <strong>banda</strong>butoŋoo naagu.",
-       "anontalkpagetext": "----\n<em>Kakaw moɲoo woo goo goykaw kaŋ maaɲoo ši bangay se, boro kaɲ mana kontu tee jina, wal'a ši a ka goy.</em>\nAdiši kal'ir ma goy nda hinna IP aderesu ka boraa alhaaloo tabatandi.\nIP aderesu dumoo woo ga hin ka žemnandi goykaw booboyaŋ game.\nNda war ti goykaw kaŋ maaɲoo ši bangay nda war ga tammahã kaŋ war ši kula nda šenney wey, [[Special:UserLogin/signup|kontu tee]] wala [[Special:UserLogin|huru]] ka ganji hiino war nda goykaw taney kaŋ šii nda maa ma birji cere ra. \\",
+       "anontalkpagetext": "----\n<em>Kakaw moɲoo woo goo goykaw kaŋ maaɲoo ši bangay se, boro kaɲ mana kontu tee jina, wal'a ši a ka goy.</em>\nAdiši kal'ir ma goy nda hinna IP aderesu ka boraa alhaaloo tabatandi.\nIP aderesu dumoo woo ga hin ka žemnandi goykaw booboyaŋ game.\nNda war ti goykaw kaŋ maaɲoo ši bangay nda war ga tammahã kaŋ war ši kula nda šenney wey, [[Special:CreateAccount|kontu tee]] wala [[Special:UserLogin|huru]] ka ganji hiino war nda goykaw taney kaŋ šii nda maa ma birji cere ra. \\",
        "noarticletext": "Hantum kul šii moɲoo woo ga sohõda.\nWar ga hin ka [Special:Search/{{PAGENAME}}|moɲoo woo maaɲoo ceeci]] moɲe jerey ra,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|moo={{FULLPAGENAMEE}}}} hantum himantey guna],\nwala [{{fullurl:{{FULLPAGENAME}}|teera=fasal}} moɲoo woo fasal]</span>.",
        "noarticletext-nopermission": "Hantum kul šii moɲoo woo ra sohõda.\nWar ga hin ka [[Special:Search/{{PAGENAME}}|moɲoo woo maaɲoo ceeci]] moɲey jerey ra wala <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|moo={{FULLPAGENAMEE}}}} cceci hantum himantey ra]</span>, amma war ši nda fondo ka moɲoo woo tee.",
        "missing-revision": "Filla #$1 moɲoo kaŋ maaɲoo ti \"{{FULLPAGENAME}}\" se ši bara.\n\nWoo ga doona ka tee nda boro hanga taariki dobu žeena banda kaŋ ga fatta moo tuusante ga.\nŠilbaywey ga hin ka duwandi [{{fullurl:{{#Special:Log}}/tuusu|moo={{FULLPAGENAMEE}}}} tuusuyan taariki] ra.",
index b994d26..61607db 100644 (file)
        "nocookieslogin": "{{SITENAME}} nauduo pakavukus (''cookies''), ka prėkergtom nauduotuojus. Tamsta esat ėšjongis anūs. Prašuom ijongtė pakavukus ė pamiegītė apent.",
        "loginsuccesstitle": "Gerā prėsėjongiet",
        "loginsuccess": "'''Dabā Tamsta esat prėsėjongis prī {{SITENAME}} kāp „$1“.'''",
-       "nosuchuser": "Nier anėjuokė nauduotuojė pavadėnta „$1“.\nPatikrinkat rašība, aba [[Special:UserLogin/signup|padėrbkat naujė paskīra]].",
+       "nosuchuser": "Nier anėjuokė nauduotuojė pavadėnta „$1“.\nPatikrinkat rašība, aba [[Special:CreateAccount|padėrbkat naujė paskīra]].",
        "nosuchusershort": "Nier juokė nauduotuojė, pavadėnta „$1“. Patėkrinkat rašība.",
        "nouserspecified": "Tamstā rēk nuruodītė nauduotuojė varda.",
        "login-userblocked": "Nauduotuos ožgints īr. Pakliūtė nie galama.",
        "accmailtext": "Bikāp padėrbts slaptažuodis, katros prėgol prī [[User talk:$1|$1]] bova siōsts pošto $2. Kāp prėsėjongsat, galat <em>[[Special:ChangePassword|anon parkeistė]]</em>.",
        "newarticle": "(Naus)",
        "newarticletext": "Tamsta pakliovat poslapin, katros dā nie padėrbts.\nJēgo nuorat anon padėrbtė, rašīkat laukė, katros ī apatiuo\n(veiziekat [$1 pagelbas poslapi]).\nJēgo pakliovat čė netīčiuom, paprastiausē paspauskat naršīklės mīgtoka '''atgal'''.",
-       "anontalkpagetext": "----''Tas īr anonimėnė nauduotuojė, katros nier sosėkūrės aba nenauduo paskīruos, aptarėmu poslapis.\nDielē tuo nauduojams IP adresos anuo atpažėnėmō.\nTas IP adresos gal būtė dalinams keletō nauduotuoju.\nJēgo Tamsta esat anonimėnis nauduotuos ėr veizėt, kū kuomentarā nier skėrtė Tamstā, [[Special:UserLogin/signup|sokorkėt paskīra]] aba [[Special:UserLogin|prisėjonkėt]], ė nebūsėt maišuoms so kėtās anonimėnēs nauduotuojās.''",
+       "anontalkpagetext": "----''Tas īr anonimėnė nauduotuojė, katros nier sosėkūrės aba nenauduo paskīruos, aptarėmu poslapis.\nDielē tuo nauduojams IP adresos anuo atpažėnėmō.\nTas IP adresos gal būtė dalinams keletō nauduotuoju.\nJēgo Tamsta esat anonimėnis nauduotuos ėr veizėt, kū kuomentarā nier skėrtė Tamstā, [[Special:CreateAccount|sokorkėt paskīra]] aba [[Special:UserLogin|prisėjonkėt]], ė nebūsėt maišuoms so kėtās anonimėnēs nauduotuojās.''",
        "noarticletext": "Nūnā tamė poslapie nie nė juokė teksta.\nTamsta galat [[Special:Search/{{PAGENAME}}|ėiškuotė ton poslapė pavadėnėma]] terp kėtū poslapiū,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ėiškuotė prėgolontiu īrašu],\naba [{{fullurl:{{FULLPAGENAME}}|action=edit}} keistė ton poslapi]</span>.",
        "noarticletext-nopermission": "Nūnā tamė poslapie nier anėjuokė teksta.\nTamsta galat [[Special:Search/{{PAGENAME}}|ėiškuotė šėtuo poslapė pavadėnėma]] kėtūs poslapiūs,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ėiškuotė prėgolontiūm ragėstru]</span>.",
        "userpage-userdoesnotexist": "Nauduotuojė paskīra „<nowiki>$1</nowiki>“ nier ožregėstrouta. Prašuom patikrėntė, a Tamsta nuorėt kortė/keistė ta poslapi.",
index 062454c..e8e1631 100644 (file)
        "noname": "Niste izabrali ispravno korisničko ime.",
        "loginsuccesstitle": "Prijavljivanje uspješno",
        "loginsuccess": "Trenutno ste prijavljeni na {{SITENAME}} kao \"$1\".",
-       "nosuchuser": "Ne postoji korisnik sa imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite vaše kucanje ili [[Special:UserLogin/signup|napravite novi korisnički račun]].",
+       "nosuchuser": "Ne postoji korisnik sa imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite vaše kucanje ili [[Special:CreateAccount|napravite novi korisnički račun]].",
        "nosuchusershort": "Ne postoji korisnik sa imenom \"$1\".\nProvjerite da li ste dobro ukucali.",
        "nouserspecified": "Morate izabrati korisničko ime.",
        "login-userblocked": "Ovaj korisnik je blokiran. Prijava nije dozvoljena.",
        "accmailtext": "Nasumično odabrana šifra za [[User talk:$1|$1]] je poslata na adresu $2.\n\nŠifra/lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|izmjene šifre]]'' nakon prijave.",
        "newarticle": "(Novi)",
        "newarticletext": "Preko linka ste došli na stranicu koja još uvijek ne postoji.\n* Ako želite stvoriti stranicu, počnite tipkati u okviru dolje (v. [$1 stranicu za pomoć] za više informacija).\n* Ukoliko ste došli greškom, pritisnike dugme '''Nazad''' ('''back''') na vašem pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio račun ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:UserLogin/signup|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
+       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio račun ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
        "noarticletext": "Na ovoj stranici trenutno nema teksta.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] u drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretraživati srodne registre],\nili [{{fullurl:{{FULLPAGENAME}}|action=edit}} urediti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Trenutno nema teksta na ovoj stranici.\nMožete [[Special:Search/{{PAGENAME}}|tražiti ovaj naslov stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane registre]</span>. alio nemate dozvolu za stvaranje ove stranice.",
        "missing-revision": "Ne mogu da pronađem izmenu br. $1 na stranici pod nazivom „{{FULLPAGENAME}}“.\n\nOvo se obično dešava kada pratite zastarjelu vezu do stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].",
        "upload-form-label-own-work": "Ovo je moje djelo",
        "upload-form-label-infoform-categories": "Kategorije",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Potvrđujem kako postavljam ovu datoteku u skladu sa uvjetima korištenja i politikom licenciranja na {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Ukoliko niste u stanju postaviti ovu datoteku pod politikom {{SITENAME}}, molimo zatvorite ovaj dijalog i pokušajte drugom metodom.",
-       "upload-form-label-not-own-work-local-local": "Također možete pokušati [[Special:Upload|na standarnoj stranici za postavljanje]].",
-       "upload-form-label-own-work-message-default": "Razumijem kako postavljam ovu datoteku na dijeljeno skladište. Potvrđujem kako to činim u skladu sa uvjetima korištenja i ovdašnjom politikom licenciranja.",
-       "upload-form-label-not-own-work-message-default": "Ukoliko niste u stanju postaviti ovu datoteku pod politikom dijeljene ostave, molimo zatvorite ovaj dijalog i pokušajte drugu metodu.",
-       "upload-form-label-not-own-work-local-default": "Možete također pokušati koristeći  [[Special:Upload|stranicu za postavljanje na  {{SITENAME}}]], ukoliko se ova datoteka može postaviti pod tamošnjom politikom.",
-       "upload-form-label-own-work-message-shared": "Potvrđujem da posjedujem autorska prava za ovu datoteku i slažem se da ću je neopozivo postaviti na Wikimedia Commons pod licencom [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0], te se slažem s [https://wikimediafoundation.org/wiki/Terms_of_Use Uvjetima korištenja].",
-       "upload-form-label-not-own-work-message-shared": "Ako ne posjedujete autorska prava za ovu datoteku ili je želite postaviti pod drugom licencom, imajte na umu da možete koristiti [https://commons.wikimedia.org/wiki/Special:UploadWizard čarobnjak za postavljanje datoteka na Commonsu].",
-       "upload-form-label-not-own-work-local-shared": "Također možete koristiti [[Special:Upload|stranicu za postavljanje datoteka na projektu {{SITENAME}}]] ako politika stranice dozvoljava postavljanje ove datoteke.",
+       "upload-form-label-own-work-message-generic-local": "Potvrđujem kako postavljam ovu datoteku u skladu sa uvjetima korištenja i politikom licenciranja na {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Ukoliko niste u stanju postaviti ovu datoteku pod politikom {{SITENAME}}, molimo zatvorite ovaj dijalog i pokušajte drugom metodom.",
+       "upload-form-label-not-own-work-local-generic-local": "Također možete pokušati [[Special:Upload|na standarnoj stranici za postavljanje]].",
+       "upload-form-label-own-work-message-generic-foreign": "Razumijem kako postavljam ovu datoteku na dijeljeno skladište. Potvrđujem kako to činim u skladu sa uvjetima korištenja i ovdašnjom politikom licenciranja.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Ukoliko niste u stanju postaviti ovu datoteku pod politikom dijeljene ostave, molimo zatvorite ovaj dijalog i pokušajte drugu metodu.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Možete također pokušati koristeći  [[Special:Upload|stranicu za postavljanje na  {{SITENAME}}]], ukoliko se ova datoteka može postaviti pod tamošnjom politikom.",
        "backend-fail-stream": "Ne mogu da emitujem datoteku $1.",
        "backend-fail-backup": "Ne mogu da napravim rezervu datoteke $1.",
        "backend-fail-notexists": "Datoteka $1 ne postoji.",
index b02efbb..71a8157 100644 (file)
        "createaccounterror": "$1 ur as tufit at kcmt",
        "loginsuccesstitle": "Tkcmt mzyan (tllit ɣil ɣ ifalan)",
        "loginsuccess": "Tllit ɣilad ɣ ifalan (tzdit d tuqqna) {{GENDER:$1|||}} ar {{SITENAME}} zund « $1 ».",
-       "nosuchuser": "Asqdac « $1 » ur illi.\nUssaɣ n isqdacn ḥiln hlli.\nẒṛ daɣ ist turit mzyan mayad, niɣd [[Special:UserLogin/signup|tmmurẓmt amiḍan amaynu]].",
+       "nosuchuser": "Asqdac « $1 » ur illi.\nUssaɣ n isqdacn ḥiln hlli.\nẒṛ daɣ ist turit mzyan mayad, niɣd [[Special:CreateAccount|tmmurẓmt amiḍan amaynu]].",
        "nosuchusershort": "Ur illa umsaws lli ilan assaɣ « $1 ». Ẓṛ ist turit mzyan mayad.",
        "nouserspecified": "Illa fllak ad tarat assaɣ nk.",
        "login-userblocked": "Asqdac ad ur as yufi ad ikcm. Tazdayt ɣ ifalan uras ttuyskar",
index 0edaa16..6844955 100644 (file)
@@ -31,7 +31,8 @@
                        "Macofe",
                        "Roonyh",
                        "Matma Rex",
-                       "SusithCM"
+                       "SusithCM",
+                       "Sandaru"
                ]
        },
        "tog-underline": "සබැඳි යටීර කිරීම:",
        "tog-newpageshidepatrolled": "විමසුමට ලක්කෙරුණු පිටු, අළුත් පිටු ලැයිස්තුව තුල නොපෙන්වන්න",
        "tog-hidecategorization": "පිටුවේ ප්‍රවර්ගීකරණය සගවන්න",
        "tog-extendwatchlist": "මෑත වෙනස්වීම් පමණක් නොව, අදාළ සියළු වෙනස්වීම් දක්වා පෙන්වන අයුරින් මුර-ලැයිස්තුව පුළුල් කරන්න",
-       "tog-usenewrc": "මෑත වෙනස්වීම් සහ මුර ලැයිස්තුව හී පිටුව අනුව සමූහ වෙනස්වීම් (ජාවාස්ක්‍රිප්ට් ඇවැසිය)",
+       "tog-usenewrc": "මෑත වෙනස්වීම් සහ මුර ලැයිස්තුව හී පිටුව අනුව සමූහ වෙනස්වීම්",
        "tog-numberheadings": "ශීර්ෂ-නාම ස්වයංක්‍රීයව අංකනය කරන්න",
        "tog-showtoolbar": "සංස්කරණ මෙවලම්තීරුව පෙන්වන්න",
        "tog-editondblclick": "ද්විත්ව-ක්ලික් කිරීම මගින් පිටු සංස්කරණය අරඹන්න",
-       "tog-editsectiononrightclick": "ඡේද ශීර්ෂ මත දකුණු-ක්ලික් කිරීමෙන් ඡේද සංස්කරණය සක්‍රීය කරන්න (ජාවාස්ක්‍රිප්ට්)",
+       "tog-editsectiononrightclick": "ඡේද ශීර්ෂ මත දකුණු-ක්ලික් කිරීමෙන් ඡේද සංස්කරණය සක්‍රීය කරන්න",
        "tog-watchcreations": "මම තනන පිටු හා මම උඩුගත කරන ගොනු මාගේ මුරලැයිස්තුවට එක් කරන්න",
        "tog-watchdefault": "මම සංස්කරණය කරන පිටු හා ගොනු මාගේ මුර ලැයිස්තුවට එක් කරන්න",
        "tog-watchmoves": "මම ගෙනයන පිටු හා ගොනු මාගේ මුර ලැයිස්තුවට එක් කරන්න",
        "noname": "වලංගු පරිශීලක-නාමයක් සඳහන් කිරීමට ඔබ අසමත් වී ඇත.",
        "loginsuccesstitle": "පිවිසුම සාර්ථකයි!",
        "loginsuccess": "'''දැන් ඔබ , \"$1\" ලෙස, {{SITENAME}} වෙත පිවිස සිටී.'''",
-       "nosuchuser": "\"$1\" යන නමැති පරිශීලකයෙකු නොමැත.\nපරිශීලක නාමයන්හි මහාප්‍රාණ ආදිය සැලකේ (case sensitive).\nඔබගේ අක්ෂර-වින්‍යාසය පිරික්සා බැලීම හෝ, [[Special:UserLogin/signup|නව ගිණුමක් තැනීම]] හෝ සිදුකරන්න.",
+       "nosuchuser": "\"$1\" යන නමැති පරිශීලකයෙකු නොමැත.\nපරිශීලක නාමයන්හි මහාප්‍රාණ ආදිය සැලකේ (case sensitive).\nඔබගේ අක්ෂර-වින්‍යාසය පිරික්සා බැලීම හෝ, [[Special:CreateAccount|නව ගිණුමක් තැනීම]] හෝ සිදුකරන්න.",
        "nosuchusershort": "\"$1\" නමින් පරිශීලකයෙකු නොමැත.\nඅක්‍ෂර-වින්‍යාසය පිරික්සා බලන්න.",
        "nouserspecified": "ඔබ විසින් පරිශීලක-නාමයක් සඳහන් කල යුතු වේ.",
        "login-userblocked": "මෙම පරිශීලකයා වාරණය කොට ඇත. පිවිසීමට ඉඩ දෙනු නොලැබේ.",
        "accmailtext": "[[User talk:$1|$1]] සඳහා අහඹු ලෙස ජනනය කරන ලද මුරපදයක් $2 වෙත යවා ඇත.\n\nමෙම නව ගිණුම සඳහා මුරපදය, ප්‍රවිෂ්ට වීමෙන් අනතුරුව, ''[[Special:ChangePassword|මුර පදය වෙනස් කරන්න]]''  පිටුව තුලදී වෙනස් කල හැක.",
        "newarticle": "(නව)",
        "newarticletext": "බැඳියක් ඔස්සේ පැමිණ ඔබ පිවිස ඇත්තේ දැනට නොපවතින පිටුවකටයි.\nමෙම ලිපිය තැනීමට අවශ්‍ය නම්, පහත ඇති කොටුව තුල අකුරු ලිවීම අරඹන්න (වැඩිදුර තොරතුරු සඳහා [$1 උදවු පිටුව] බලන්න).\nඔබ මෙහි පිවිස ඇත්තේ අත්වැරැද්දකින් නම්, ඔබගේ ගවේෂකයෙහි '''ආපසු''' බොත්තම ඔබන්න.",
-       "anontalkpagetext": "----''මෙම සංවාද පිටුව අයත් වන්නේ තවමත් ගිණුමක් තනා නැති හෝ එසේ කොට එනමුදු එය භාවිතා නොකරන හෝ නිර්නාමික පරිශීලකයෙකුටය.\nඑබැවින්, ඔහු/ඇය හැඳින්වීමට සංඛ්‍යාත්මක IP ලිපිනය භාවිතා කිරීමට අප හට සිදුවේ.\nපරිශීලකයන් කිහිප දෙනෙකු විසින් මෙවැනි IP ලිපිනයක් හවුලේ පරිහරණය කරනවා විය හැක.\nඔබ නිර්නාමික පරිශීලකයෙකු නම් හා ඔබ පිළිබඳ අනනුකූල පරිකථනයන් සිදුවෙන බවක් ඔබට හැ‍ඟේ නම්, අනෙකුත් නිර්නාමික පරිශීලකයන් හා සමග  මෙවැනි සංකූලතා ඇතිවීම වලක්වනු වස්,  කරුණාකර  [[Special:UserLogin/signup|ගිණුමක් තැනීමට]] හෝ [[Special:UserLogin|ප්‍රවිෂ්ට වීමට]]  කාරුණික වන්න.''",
+       "anontalkpagetext": "----''මෙම සංවාද පිටුව අයත් වන්නේ තවමත් ගිණුමක් තනා නැති හෝ එසේ කොට එනමුදු එය භාවිතා නොකරන හෝ නිර්නාමික පරිශීලකයෙකුටය.\nඑබැවින්, ඔහු/ඇය හැඳින්වීමට සංඛ්‍යාත්මක IP ලිපිනය භාවිතා කිරීමට අප හට සිදුවේ.\nපරිශීලකයන් කිහිප දෙනෙකු විසින් මෙවැනි IP ලිපිනයක් හවුලේ පරිහරණය කරනවා විය හැක.\nඔබ නිර්නාමික පරිශීලකයෙකු නම් හා ඔබ පිළිබඳ අනනුකූල පරිකථනයන් සිදුවෙන බවක් ඔබට හැ‍ඟේ නම්, අනෙකුත් නිර්නාමික පරිශීලකයන් හා සමග  මෙවැනි සංකූලතා ඇතිවීම වලක්වනු වස්,  කරුණාකර  [[Special:CreateAccount|ගිණුමක් තැනීමට]] හෝ [[Special:UserLogin|ප්‍රවිෂ්ට වීමට]]  කාරුණික වන්න.''",
        "noarticletext": "දැනට මෙම පිටුවෙහි කිසිදු පෙළක් නොමැත.\nඅනෙකුත් පිටුවල  [[Special:Search/{{PAGENAME}}|මෙම පිටු ශීර්ෂය සඳහා ගවේශනය කිරීම]] හෝ,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} අදාළ ලඝු-සටහන් සඳහා ගවේෂණය කිරීම],\nහෝ [{{fullurl:{{FULLPAGENAME}}|action=edit}} මෙම පිටුව සංස්කරණය කිරීම] හෝ ඔබට සිදු කල හැක</span>.",
        "noarticletext-nopermission": "දැනට මෙම පිටුවෙහි කිසිදු පෙළක් නොමැත.\nඅනෙකුත් පිටුවල [[Special:Search/{{PAGENAME}}|මෙම පිටු ශීර්ෂය සඳහා ගවේශනය කිරීම]] හෝ, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}අදාළ ලඝු-සටහන් සඳහා ගවේෂණය කිරීම]</span>, හෝ මෙම පිටුව සංස්කරණය කිරීම හෝ ඔබට කල හැක.",
        "missing-revision": "සංශෝධනය නම් පිටුවේ #$1 \"{{FULLPAGENAME}}\" නොපවතියි.\n\nමෙය සාමාන්යයෙන් මකා දැමූ පිටුවක ඉතිහාසය සබැඳියන් යල් පැනගිය පහත සඳහන් හේතු වේ [{{fullurl:{{#Special:Log}}/මකන්න|page={{FULLPAGENAMEE}}}} මැකීමේ ලොගය].",
index 1fcafaa..24dd68c 100644 (file)
        "noname": "Nezadali ste platné používateľské meno.",
        "loginsuccesstitle": "Prihlásenie úspešné",
        "loginsuccess": "'''Teraz ste prihlásený do {{GRAMMAR:genitív|{{SITENAME}}}} ako „$1“.'''",
-       "nosuchuser": "Používateľské meno „$1“ neexistuje.\nV používateľských menách sa rozlišuje veľkosť písmen.\nSkontrolujte preklepy alebo sa [[Special:UserLogin/signup|zaregistrujte ako nový používateľ]].",
+       "nosuchuser": "Používateľské meno „$1“ neexistuje.\nV používateľských menách sa rozlišuje veľkosť písmen.\nSkontrolujte preklepy alebo sa [[Special:CreateAccount|zaregistrujte ako nový používateľ]].",
        "nosuchusershort": "V súčasnosti neexistuje používateľ s menom „$1“. Skontrolujte preklepy.",
        "nouserspecified": "Musíte uviesť meno používateľa.",
        "login-userblocked": "Tento používateľ je zablokovaný. Nie je mu dovolené prihlásiť sa.",
        "accmailtext": "Náhodne vytvorené heslo pre používateľa [[User talk:$1|$1]] bolo poslané na $2. Je možné ho zmeniť na stránke ''[[Special:ChangePassword|zmena hesla]]'' po prihlásení.",
        "newarticle": "(Nový)",
        "newarticletext": "Sledovali ste odkaz na stránku, ktorá zatiaľ neexistuje.\nStránku vytvoríte tak, že začnete písať do poľa nižšie (viac informácií nájdete na stránkach [$1 nápovedy]).\nAk ste sa sem dostali nechtiac, kliknite na tlačidlo <strong>späť</strong> vo svojom prehliadači.",
-       "anontalkpagetext": "----''Toto je diskusná stránka anonymného používateľa, ktorý nemá vytvorené svoje konto alebo ho nepoužíva.\nPreto musíme na jeho identifikáciu použiť numerickú IP adresu. Je možné, že takúto IP adresu používajú viacerí používatelia.\nAk ste anonymný používateľ a máte pocit, že vám boli adresované irelevantné diskusné príspevky, [[Special:UserLogin/signup|vytvorte si konto]] alebo sa [[Special:UserLogin|prihláste]], aby sa zamedzilo budúcim zámenám s inými anonymnými používateľmi.''",
+       "anontalkpagetext": "----''Toto je diskusná stránka anonymného používateľa, ktorý nemá vytvorené svoje konto alebo ho nepoužíva.\nPreto musíme na jeho identifikáciu použiť numerickú IP adresu. Je možné, že takúto IP adresu používajú viacerí používatelia.\nAk ste anonymný používateľ a máte pocit, že vám boli adresované irelevantné diskusné príspevky, [[Special:CreateAccount|vytvorte si konto]] alebo sa [[Special:UserLogin|prihláste]], aby sa zamedzilo budúcim zámenám s inými anonymnými používateľmi.''",
        "noarticletext": "Na tejto stránke sa momentálne nenachádza žiadny text.\nMôžete [[Special:Search/{{PAGENAME}}|vyhľadávať názov tejto stránky]] v obsahu iných stránok,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} vyhľadávať v súvisiacich záznamoch] alebo [{{fullurl:{{FULLPAGENAME}}|action=edit}} vytvoriť túto stránku]</span>.",
        "noarticletext-nopermission": "Táto stránka momentálne neobsahuje žiadny text.\nMôžete [[Special:Search/{{PAGENAME}}|hľadať názov tejto stránky]] v texte iných stránok\nalebo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hľadať v súvisiacich záznamoch]</span>, ale nemáte oprávnenie túto stránku vytvoriť.",
        "missing-revision": "Revízia #$1 stránky s názvom „{{FULLPAGENAME}}“ neexistuje.\n\nPravdepodobne ste nasledovali zastaraný odkaz na historickú verziu stránky, ktorá bola medzičasom odstránená.\nPodrobnosti nájdete v [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} zázname zmazaní].",
index 728952f..ac9345e 100644 (file)
        "noname": "Niste vnesli veljavnega uporabniškega imena.",
        "loginsuccesstitle": "Sedaj ste prijavljeni",
        "loginsuccess": "'''Zdaj ste prijavljeni v {{GRAMMAR:tožilnik|{{SITENAME}}}} kot »$1«.'''",
-       "nosuchuser": "Uporabnik z imenom »$1« ne obstaja.\nUporabniška imena so občutljiva na velikost črk.\nPreverite črkovanje ali pa si [[Special:UserLogin/signup|ustvarite nov uporabniški račun]].",
+       "nosuchuser": "Uporabnik z imenom »$1« ne obstaja.\nUporabniška imena so občutljiva na velikost črk.\nPreverite črkovanje ali pa si [[Special:CreateAccount|ustvarite nov uporabniški račun]].",
        "nosuchusershort": "Uporabnik z imenom »$1« ne obstaja.\nPreverite črkovanje.",
        "nouserspecified": "Prosimo, vpišite uporabniško ime.",
        "login-userblocked": "Ta uporabnik je blokiran. Prijava ni dovoljena.",
        "accmailtext": "Naključno generirano geslo za [[User talk:$1|$1]] smo poslali na $2. Po prijavi ga lahko spremenite na strani za ''[[Special:ChangePassword|spremembo gesla]]''.",
        "newarticle": "(Nov)",
        "newarticletext": "Sledili ste povezavi na stran, ki še ne obstaja.\nDa bi stran ustvarili, vnesite v spodnji obrazec besedilo\n(za več informacij glej [$1 pomoč]).\nČe ste sem prišli po pomoti, v svojem brskalniku kliknite gumb ''Nazaj''.",
-       "anontalkpagetext": "---- ''To je pogovorna stran brezimnega uporabnika, ki si še ni ustvaril računa ali pa ga ne uporablja. Zaradi tega moramo uporabiti IP-naslov za njegovo/njeno ugotavljanje istovetnosti. Takšen IP-naslov si lahko deli več uporabnikov. Če ste brezimni uporabnik in menite, da so nepomembne pripombe namenjene vam, prosimo [[Special:UserLogin|ustvarite račun]] ali pa se [[Special:UserLogin/signup|vpišite]], da preprečite zmedo z drugimi nepodpisanimi uporabniki.''",
+       "anontalkpagetext": "---- ''To je pogovorna stran brezimnega uporabnika, ki si še ni ustvaril računa ali pa ga ne uporablja. Zaradi tega moramo uporabiti IP-naslov za njegovo/njeno ugotavljanje istovetnosti. Takšen IP-naslov si lahko deli več uporabnikov. Če ste brezimni uporabnik in menite, da so nepomembne pripombe namenjene vam, prosimo [[Special:UserLogin|ustvarite račun]] ali pa se [[Special:CreateAccount|vpišite]], da preprečite zmedo z drugimi nepodpisanimi uporabniki.''",
        "noarticletext": "Na tej strani ni trenutno nobenega besedila. Naslov strani lahko poskusite [[Special:Search/{{PAGENAME}}|poiskati]] na drugih straneh, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} v dnevniških zapisih] ali pa [{{fullurl:{{FULLPAGENAME}}|action=edit}} stran ustvarite]</span>.",
        "noarticletext-nopermission": "Na strani trenutno ni nobenega besedila.\nLahko poskusite [[Special:Search/{{PAGENAME}}|poiskati naslov strani]] na drugih straneh ali <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} v povezanih dnevniških zapisih]</span>, vendar za ustvarjanje strani nimate zadostnih dovoljenj.",
        "missing-revision": "Redakcija št. $1 strani »{{FULLPAGENAME}}« ne obstaja.\n\nPo navadi se to zgodi, ko sledite zastareli povezavi na zgodovino strani, ki jo je nekdo izbrisal.\nPodrobnosti lahko najdete v [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} dnevniku brisanja].",
        "right-override-export-depth": "Izvoz strani, vključno s povezaimi straneh do globine 5",
        "right-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
        "right-passwordreset": "Ogled e-pošt ponastavitve gesel",
-       "right-managechangetags": "Ustvarjanje in brisanje [[Special:Tags|oznak]] iz zbirke podatkov",
+       "right-managechangetags": "Ustvarjanje in (dez)aktivacijo [[Special:Tags|oznak]]",
        "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",
+       "right-deletechangetags": "Izbris [[Special:Tags|oznak]] iz zbirke podatkov",
        "grant-generic": "Snov pravic »$1«",
        "grant-group-page-interaction": "Interakcija s stranmi",
        "grant-group-file-interaction": "Interakcija s predstavnostjo",
        "action-viewmyprivateinfo": "ogled svojih zasebnih informacij",
        "action-editmyprivateinfo": "urejanje svojih zasebnih informacij",
        "action-editcontentmodel": "urejanje vsebinskega modela strani",
-       "action-managechangetags": "ustvarjanje in brisanje oznak iz zbirke podatkov",
+       "action-managechangetags": "ustvarjanje in (dez)aktivacijo oznak",
        "action-applychangetags": "uveljavitev oznak skupaj z vašimi spremembami",
        "action-changetags": "dodajanje in odstranjevanje poljubnih oznak na posameznih redakcijah in dnevniških vnosih",
+       "action-deletechangetags": "izbris oznak iz zbirke podatkov",
        "nchanges": "$1 {{PLURAL:$1|sprememba|spremembi|spremembe|sprememb|sprememb}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od zadnjega obiska}}",
        "enhancedrc-history": "zgodovina",
        "upload-form-label-own-work": "To je moje lastno delo",
        "upload-form-label-infoform-categories": "Kategorije",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Potrjujem, da datoteko nalagam v skladu s pogoji uporabe in pravili o licenciranju na {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Če datoteke ne morete naložiti pod pogoji {{SITENAME}}, zaprite to okno in poskusite drugo metodo.",
-       "upload-form-label-not-own-work-local-local": "Morda želite poskusiti [[Special:Upload|privzeto stran za nalaganje]].",
-       "upload-form-label-own-work-message-default": "Razumem, da datoteko nalagam v deljeno hrambo. Potrjujem, da to počnem v skladu s tukajšnjimi pogoji uporabe in pravili za licenciranje.",
-       "upload-form-label-not-own-work-message-default": "Če datoteke ne morete naložiti pod pogoji deljene hrambe, zaprite to okno in poskusite drugo metodo.",
-       "upload-form-label-not-own-work-local-default": "Morda želite datoteko poskusiti naložiti na [[Special:Upload|strani za nalaganje na {{SITENAME}}]], če jo lahko naložite pod njihovimi pravili.",
-       "upload-form-label-own-work-message-shared": "Izjavljam, da sem lastnik avtorskih pravic te datoteke, strinjam se z nepreklicno objavo datoteke v Wikimedijini Zbirki pod dovoljenjem [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Priznanje avtorstva-Deljenje pod enakimi pogoji 4.0] in strinjam se s [https://wikimediafoundation.org/wiki/Terms_of_Use Pogoji uporabe].",
-       "upload-form-label-not-own-work-message-shared": "Če niste lastnik avtorskih pravic datoteke ali jo želite objaviti pod drugačnim dovoljenje, uporabite [https://commons.wikimedia.org/wiki/Special:UploadWizard Čarovnik za nalaganje v Zbirko].",
-       "upload-form-label-not-own-work-local-shared": "Morda želite datoteko poskusiti naložiti na [[Special:Upload|strani za nalaganje na {{SITENAME}}]], če stran dovoljuje nalaganje datoteke pod njihovimi pravili.",
+       "upload-form-label-own-work-message-generic-local": "Potrjujem, da datoteko nalagam v skladu s pogoji uporabe in pravili o licenciranju na {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Če datoteke ne morete naložiti pod pogoji {{SITENAME}}, zaprite to okno in poskusite drugo metodo.",
+       "upload-form-label-not-own-work-local-generic-local": "Morda želite poskusiti [[Special:Upload|privzeto stran za nalaganje]].",
+       "upload-form-label-own-work-message-generic-foreign": "Razumem, da datoteko nalagam v deljeno hrambo. Potrjujem, da to počnem v skladu s tukajšnjimi pogoji uporabe in pravili za licenciranje.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Če datoteke ne morete naložiti pod pogoji deljene hrambe, zaprite to okno in poskusite drugo metodo.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Morda želite datoteko poskusiti naložiti na [[Special:Upload|strani za nalaganje na {{SITENAME}}]], če jo lahko naložite pod njihovimi pravili.",
        "backend-fail-stream": "Ne morem pretakati datoteke $1.",
        "backend-fail-backup": "Ne morem varnostno kopirati datoteke $1.",
        "backend-fail-notexists": "Datoteka $1 ne obstaja.",
        "whatlinkshere-prev": "{{PLURAL:$1|prejšnji|prejšnja $1|prejšnji $1|prejšnjih $1|prejšnjih $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|naslednji|naslednja $1|naslednji $1|naslednjih $1|naslednjih $1}}",
        "whatlinkshere-links": "← povezave",
-       "whatlinkshere-hideredirs": "$1 preusmeritve",
-       "whatlinkshere-hidetrans": "$1 vključitve",
-       "whatlinkshere-hidelinks": "$1 povezave",
-       "whatlinkshere-hideimages": "$1 povezave datotek",
+       "whatlinkshere-hideredirs": "Skrij preusmeritve",
+       "whatlinkshere-hidetrans": "Skrij vključitve",
+       "whatlinkshere-hidelinks": "Skrij povezave",
+       "whatlinkshere-hideimages": "Skrij povezave datotek",
        "whatlinkshere-filters": "Filtri",
        "whatlinkshere-submit": "Pojdi",
        "autoblockid": "Samodejna blokada št. $1",
        "timezone-local": "Krajevno",
        "duplicate-defaultsort": "'''Opozorilo:''' Privzeti ključ razvrščanja »$2« prepiše prejšnji privzeti ključ razvrščanja »$1«.",
        "duplicate-displaytitle": "<strong>Opozorilo:</strong> Prikazni naslov »$2« prepiše prejšnji prikazni naslov »$1«.",
+       "restricted-displaytitle": "<strong>Opozorilo:</strong> Prikazni naslov »$1« smo prezrli, saj ni enak dejanskemu naslovu strani.",
        "invalid-indicator-name": "<strong>Napaka:</strong> Atribut <code>name</code> indikatorjev stanja strani ne sme biti prazen.",
        "version": "Različica",
        "version-extensions": "Nameščene razširitve",
        "tags-delete-not-found": "Oznaka »$1« ne obstaja.",
        "tags-delete-too-many-uses": "Oznaka »$1« je uporabljena pri več kot $2 {{PLURAL:$2|redakciji|redakcijah}}, kar pomeni, da je ni mogoče izbrisati.",
        "tags-delete-warnings-after-delete": "Oznako »$1« smo izbrisali, vendar smo naleteli na {{PLURAL:$2|naslednjo težavo|naslednji težavi|naslednje težave}}:",
+       "tags-delete-no-permission": "Nimate dovoljenja za izbris oznak sprememb.",
        "tags-activate-title": "Aktiviraj oznako",
        "tags-activate-question": "Aktivirali boste oznako »$1«.",
        "tags-activate-reason": "Razlog:",
index 25e1648..693b6c5 100644 (file)
        "noname": "Du muußt enn giltiga Nutzernoama oangahn.",
        "loginsuccesstitle": "Oameldung erfolgreich",
        "loginsuccess": "Du biest jitz ols „$1“ bei {{SITENAME}} oagemeldet.",
-       "nosuchuser": "Dar Nutzernoame „$1“ existiert ne.\nIeberpriefe de Schreibweise (Gruß-/Kleenschreibung beachta) oder [[Special:UserLogin/signup|melde diech ols neuer Benutzer oa]].",
+       "nosuchuser": "Dar Nutzernoame „$1“ existiert ne.\nIeberpriefe de Schreibweise (Gruß-/Kleenschreibung beachta) oder [[Special:CreateAccount|melde diech ols neuer Benutzer oa]].",
        "nosuchusershort": "Dar Nutzernoame „$1“ existiert ne. Bitte ieberpriefe de Schreibweise.",
        "nouserspecified": "Bitte gieb enn Benutzernoamen oa.",
        "wrongpassword": "Doas Passwurt ies foalsch (oder fehlt). Bitte versuche is erneut.",
        "accmailtitle": "Passwurt wourde verschickt",
        "newarticle": "(Neu)",
        "newarticletext": "Du best an'm Link zu anner Seite gefulgt, de nee vorhanden ies.\nIm de Seite oazulega, trage denn Text ei de undastehende Box a (siehe de [$1 Hilfeseite] fier meh Informationen).\nBest du fälschlicherweise hier, klicke de '''Zerricke'''-Schaltfläche dennes Browsers.",
-       "anontalkpagetext": "----''Diese Seite dient dazu, a'm nee oagemeldeta Benutzer Noachrichta zu hinterlassa. 'S werd senne IP-Atresse zur Identifizierung verwendet. IP-Atressen kinna vu mehrera Nutzern gemeensam verwendet waan. Wenn du miet dann Kommentarn uff dieser Seite nischt oafanga koast, richta se siech vermutlich oa an'n friehera Inhaber denner IP-Atresse und du koast se ignorieren. Du koast dir au a [[Special:UserLogin/signup|Nutzerkonto erstalla]] oder dich [[Special:UserLogin|oamelda]], im kinftig Verwechslunga miet andern anonyma Nutzern zu vermeida.''",
+       "anontalkpagetext": "----''Diese Seite dient dazu, a'm nee oagemeldeta Benutzer Noachrichta zu hinterlassa. 'S werd senne IP-Atresse zur Identifizierung verwendet. IP-Atressen kinna vu mehrera Nutzern gemeensam verwendet waan. Wenn du miet dann Kommentarn uff dieser Seite nischt oafanga koast, richta se siech vermutlich oa an'n friehera Inhaber denner IP-Atresse und du koast se ignorieren. Du koast dir au a [[Special:CreateAccount|Nutzerkonto erstalla]] oder dich [[Special:UserLogin|oamelda]], im kinftig Verwechslunga miet andern anonyma Nutzern zu vermeida.''",
        "noarticletext": "Diese Seite enthält momentan noo kenn Text.\nDu koast diesen Tittel uffa andern Seita [[Special:Search/{{PAGENAME}}|sucha]],\n<span class=\"plainlinks\"> ei dan zugeheeriga [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} Logbichern sucha] oder diese Seite [{{fullurl:{{FULLPAGENAME}}|action=edit}} bearbta]</span>.",
        "noarticletext-nopermission": "Diese Seite enthält momentan noo kenn Text.\nDu koast diesen Tittel uff dann andern Seita [[Special:Search/{{PAGENAME}}|sucha]]\noder de zugehieriga <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbichern betrachta].</span>",
        "userpage-userdoesnotexist": "Doas Nutzerkonto „<nowiki>$1</nowiki>“ ies nee vurhanda. Bitte priefe, ob du diese Seite wirklich erstella/beoarbta wielst.",
index e9782f0..f14b99f 100644 (file)
        "noname": "Ma jiro magaca isticmaalaha aad qortay",
        "loginsuccesstitle": "Gudagalida waa la dhamaystiray",
        "loginsuccess": "'''Waxaa hadda gudaha ugu soo gashay {{SITENAME}} sida \"$1\".'''",
-       "nosuchuser": "Ma jiro isticmaalo leh magacaan \"$1\".\nMagacyada way kala waaweyn yihiin.\nSax hingaada, ama  [[Special:UserLogin/signup|samayso magac gudagale ah]].",
+       "nosuchuser": "Ma jiro isticmaalo leh magacaan \"$1\".\nMagacyada way kala waaweyn yihiin.\nSax hingaada, ama  [[Special:CreateAccount|samayso magac gudagale ah]].",
        "nosuchusershort": "Majiro isticmaale leh magacaan \"$1\". Sax qoraalkaaga.",
        "nouserspecified": "Waa in aad magac gudagale heshaa.",
        "login-userblocked": "Isticmaalahaan waa la mamnuucay. Lama ogolo in oo gudaha u galo",
        "accmailtext": "Ereysir loogu tala galay [[User talk:$1|$1]] waxaa loogu diray $2.\n\nAkoonkaan cusub eraysirkiisa waxaa ku badali kartaa  bogga ''[[Special:ChangePassword|bedel eraysirka]]'' si aad u soo gashid.",
        "newarticle": "(Cusub)",
        "newarticletext": "Waxaa soo raacday link kula soo xiriiriyay bog oo hadda wali jirin.\nHadii aad rabto in aad sameyso bogga, hoos ka bilaaw qoraalkaada (fiiri [$1 bogga caawinaada] wax war ah oo kale).\nhadii aad meeshaan ku soo qaldantay, riix batoonka barowsahaaga  '''gadaal uuga noqo''' .",
-       "anontalkpagetext": "----''Meeshaan waa bogga wadahadalka isticmaalayaasha la aqoon oo aanan weli sameysanin akoon, ama  wali isticmaalin. \nSidaas darteed, waa in aan isticmaalnaa lambar cinwaaneedka IP:ga si aan u ogaano asiga/ayada. Cinwaanka IP:ga waxaa suurto gal ah in ay qeybsadaan isticmaaleyaal badan.\nHadii aad tahay isticmaale aanan la'aqoonsanin oo aad dareemaysid in laguu gafay, fadlan  [[Special:UserLogin/signup|sameyso akoon]]  ama [[Special:UserLogin|gudaha gal]] si aad u dhowrsatid in hadhowdi laguugu qaldo isticmaalada kale oo aann la'aqoonsanin.''",
+       "anontalkpagetext": "----''Meeshaan waa bogga wadahadalka isticmaalayaasha la aqoon oo aanan weli sameysanin akoon, ama  wali isticmaalin. \nSidaas darteed, waa in aan isticmaalnaa lambar cinwaaneedka IP:ga si aan u ogaano asiga/ayada. Cinwaanka IP:ga waxaa suurto gal ah in ay qeybsadaan isticmaaleyaal badan.\nHadii aad tahay isticmaale aanan la'aqoonsanin oo aad dareemaysid in laguu gafay, fadlan  [[Special:CreateAccount|sameyso akoon]]  ama [[Special:UserLogin|gudaha gal]] si aad u dhowrsatid in hadhowdi laguugu qaldo isticmaalada kale oo aann la'aqoonsanin.''",
        "noarticletext": "Boggaan hadda wax qoraal ah kuma qorno.\nWaxaa  [[Special:Search/{{PAGENAME}}|magaca boggaan]] ka raadin kartaa bogyaasha kale,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} raadi kuwo la mid ah],\nama [{{fullurl:{{FULLPAGENAME}}|action=edit}} wax ka bedel boggaan]</span>.",
        "noarticletext-nopermission": "Hadda boggaan wax qoraal ah kuma qorno.\nWaxaa  [[Special:Search/{{PAGENAME}}|magaca boggaan ka raadin kartaa]] boggaga kale ama <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ka raadi gudagalayaasha kale]</span>.",
        "userpage-userdoesnotexist": "Isticmaalahan  \"<nowiki>$1</nowiki>\" ma diiwaangashno.\nFadlan fiiri hadii aad rabto in aad sameeyso/wax ka bedesho boggaan.",
        "allpagesprefix": "Soo saar boggaga leh horgalaha:",
        "allpagesbadtitle": "Cinwaanka bogga xaq ma'aha ama waa ereyo u gaar ah isdhaafka-luqadaha ama isdhaafka-wiki. Waxaa ku jirikara xuruufo aanan loo isticmaalikarin cinwaan ahaan.",
        "categories": "Qeybaha",
-       "special-categories-sort-count": "xisaabi marka aad tirisid",
        "linksearch-ns": "Xarun magaceedka:",
        "linksearch-ok": "Raadi",
        "linksearch-line": "$1 wuxuu ka socdaa $2",
        "watchlisttools-raw": "Badal liiska waardiyeenta ceeriinka ah",
        "version-poweredby-others": "kuwa kale",
        "redirect-file": "Magaca faylka",
-       "fileduplicatesearch-legend": "Raadi mid tusaale ah",
        "fileduplicatesearch-submit": "Raadi",
        "specialpages": "bogagga khaaska ah",
        "specialpages-note-top": "Furaha",
index d17130e..b804acd 100644 (file)
        "noname": "Nuk keni dhënë një emër përdoruesi të pranueshëm.",
        "loginsuccesstitle": "Identifikim i suksesshëm",
        "loginsuccess": "'''Ju tani jeni identifikuar tek {{SITENAME}} si \"$1\".'''",
-       "nosuchuser": "Nuk ka ndonjë përdorues me emrin \"$1\".\nKontrolloni shkrimin ose [[Special:UserLogin/signup|hapni një llogari të re]].",
+       "nosuchuser": "Nuk ka ndonjë përdorues me emrin \"$1\".\nKontrolloni shkrimin ose [[Special:CreateAccount|hapni një llogari të re]].",
        "nosuchusershort": "Nuk ka asnjë përdorues me emrin \"$1\".",
        "nouserspecified": "Ju duhet të jepni një nofkë",
        "login-userblocked": "Ky përdorues është bllokuar. Identifikimi nuk lejohet.",
        "accmailtext": "Një fjalëkalim i krijuar në mënyrë të rastësishme për [[User talk:$1|$1]] u dërgua në $2.\n\nFjalëkalimi për këtë llogari mund të ndryshohet në faqen ''[[Special:ChangePassword|ndrysho fjalëkalimin]]'' pasi të jeni identifikuar.",
        "newarticle": "(I ri)",
        "newarticletext": "Ti ke ndjekur nje lidhje drejt një faqeje që nuk ekziston.\nPër ta krijuar këtë faqe, fillo të shkruash në kutinë e mëposhtme (shih [$1 faqen e ndihmës] për më shumë informacion).\nNëse ti ke mbërritur këtu gabimisht, atëherë kliko butonin '''pas''' të shfletuesit tënd.",
-       "anontalkpagetext": "----'' Kjo është një faqe diskutimi për një përdorues anonim i cili nuk ka krijuar akoma një llogari, ose qe nuk e përdor atë. \n Prandaj, ne duhet të përdorim adresën IP numerike për identifikimin e tij. \nKjo adresë IP mund të përdoret nga disa përdorues.\n Në qoftë se jeni një përdorues anonim dhe mendoni se ndaj jush janë bërë komente të parëndësishme, ju lutem [[Special:UserLogin/signup|krijoni një llogari]] ose [[Special:UserLogin|identifikohuni]] për të shmangur konfuzionin në të ardhmen me përdorues të tjerë anonim .''",
+       "anontalkpagetext": "----'' Kjo është një faqe diskutimi për një përdorues anonim i cili nuk ka krijuar akoma një llogari, ose qe nuk e përdor atë. \n Prandaj, ne duhet të përdorim adresën IP numerike për identifikimin e tij. \nKjo adresë IP mund të përdoret nga disa përdorues.\n Në qoftë se jeni një përdorues anonim dhe mendoni se ndaj jush janë bërë komente të parëndësishme, ju lutem [[Special:CreateAccount|krijoni një llogari]] ose [[Special:UserLogin|identifikohuni]] për të shmangur konfuzionin në të ardhmen me përdorues të tjerë anonim .''",
        "noarticletext": "Momentalisht nuk ka tekst në këtë faqe.\nJu mund [[Special:Search/{{PAGENAME}}|ta kërkoni këtë titull]] në faqe tjera,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} të kërkoni ngjarjet e ngjashme në regjistër],\nose [{{fullurl:{{FULLPAGENAME}}|action=edit}} të redaktoni këtë faqe]</span>.",
        "noarticletext-nopermission": "Për momentin faqja e kërkuar është bosh.\nJu mund të [[Special:Search/{{PAGENAME}}|kërkoni këtë titiull]] në faqet e tjera, ose të <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} këtkoni regjistrat e ngjashëm]</span>, por ju nuk mundeni ta krijoni këtë faqe.",
        "missing-revision": "Inspektimi #$1 i faqes me emrin \"{{FULLPAGENAME}}\" nuk ekziston.\n\nKjo zakonisht shkaktuar duke ndjekur një lidhje të vjetër tek një faqe që është fshirë. Hollësitë mund të gjenden në [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} regjistrin e fshirjeve].",
index faba454..59e50e2 100644 (file)
        "noname": "Унели сте неисправно корисничко име.",
        "loginsuccesstitle": "Успешно пријављивање",
        "loginsuccess": "'''Пријављени сте као „$1“.'''",
-       "nosuchuser": "Не постоји корисник с именом „$1“.\nКорисничка имена су осетљива на мала и велика слова.\nПроверите да ли сте га добро унели или [[Special:UserLogin/signup|отворите нови налог]].",
+       "nosuchuser": "Не постоји корисник с именом „$1“.\nКорисничка имена су осетљива на мала и велика слова.\nПроверите да ли сте га добро унели или [[Special:CreateAccount|отворите нови налог]].",
        "nosuchusershort": "Корисник с именом „$1“ не постоји.\nПроверите да ли сте правилно написали.",
        "nouserspecified": "Морате навести корисничко име.",
        "login-userblocked": "{{GENDER:$1|Овај корисник је блокиран|Ова корисница је блокирана|Овај корисник је блокиран}}. Пријава није дозвољена.",
        "accmailtext": "Лозинка за {{GENDER:$1|корисника|корисницу}} [[User talk:$1|$1]] је послата на $2. Након пријаве, лозинка се може променити [[Special:ChangePassword|овде]].",
        "newarticle": "(нови)",
        "newarticletext": "Дошли сте на страницу која још не постоји.\nДа бисте је направили, почните да куцате у прозор испод овог текста (погледајте [$1 страницу за помоћ]).\nАко сте овде дошли грешком, вратите се на претходну страницу.",
-       "anontalkpagetext": "---- Ово је страница за разговор с анонимним корисником који још нема налог или га не користи.\nЗбог тога морамо да користимо бројчану ИП адресу како бисмо га препознали.\nТакву адресу може делити више корисника.\nАко сте анонимни корисник и мислите да су вам упућене примедбе, [[Special:UserLogin/signup|отворите налог]] или се [[Special:UserLogin|пријавите]] да бисте избегли будућу забуну с осталим анонимним корисницима.",
+       "anontalkpagetext": "---- Ово је страница за разговор с анонимним корисником који још нема налог или га не користи.\nЗбог тога морамо да користимо бројчану ИП адресу како бисмо га препознали.\nТакву адресу може делити више корисника.\nАко сте анонимни корисник и мислите да су вам упућене примедбе, [[Special:CreateAccount|отворите налог]] или се [[Special:UserLogin|пријавите]] да бисте избегли будућу забуну с осталим анонимним корисницима.",
        "noarticletext": "На овој страници тренутно нема садржаја.\nМожете [[Special:Search/{{PAGENAME}}|потражити овај наслов]] на другим страницама,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} претражити сродне извештаје] или [{{fullurl:{{FULLPAGENAME}}|action=edit}} направити ову страницу]</span>.",
        "noarticletext-nopermission": "На овој страници тренутно нема садржаја.\nМожете [[Special:Search/{{PAGENAME}}|потражити овај наслов]] на другим страницама или <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} претражити сродне дневнике]</span>, али немате дозволу да направите ову страницу.",
        "missing-revision": "Не могу да пронађем измену бр. $1 на страници под називом „{{FULLPAGENAME}}“.\n\nОво се обично дешава када пратите застарелу везу до странице која је обрисана.\nВише информација можете пронаћи у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневнику брисања].",
        "whatlinkshere-prev": "{{PLURAL:$1|претходни|претходних $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|следећи|следећих $1}}",
        "whatlinkshere-links": "← везе",
-       "whatlinkshere-hideredirs": "$1 преусмерења",
-       "whatlinkshere-hidetrans": "$1 укључивања",
-       "whatlinkshere-hidelinks": "$1 везе",
+       "whatlinkshere-hideredirs": "Сакриј преусмерења",
+       "whatlinkshere-hidetrans": "Сакриј укључивања",
+       "whatlinkshere-hidelinks": "Сакриј везе",
        "whatlinkshere-hideimages": "$1 везе до датотеке",
        "whatlinkshere-filters": "Филтери",
        "whatlinkshere-submit": "Иди",
        "size-megabytes": "$1 MB",
        "size-gigabytes": "$1 GB",
        "lag-warn-normal": "Измене новије од $1 {{PLURAL:$1|секунде|секунде|секунди}} неће бити приказане.",
-       "lag-warn-high": "Због преоптерећења базе података, измене новије од $1 {{PLURAL:$1|секунда|секунде}} неће бити приказане.",
+       "lag-warn-high": "Због преоптерећења базе података, измене новије од $1 {{PLURAL:$1|1=секунде|секунде|секунди}} неће бити приказане.",
        "watchlistedit-normal-title": "Уређивање списка надгледања",
        "watchlistedit-normal-legend": "Уклањање наслова са списка надгледања",
        "watchlistedit-normal-explain": "Наслови на вашем списку надгледања су приказани испод.\nДа бисте уклонили наслов, означите квадратић до њега и кликните на „{{int:Watchlistedit-normal-submit}}“.\nМожете и да [[Special:EditWatchlist/raw|уредите сиров списак]].",
index 3879905..e786f12 100644 (file)
        "noname": "Uneli ste neispravno korisničko ime.",
        "loginsuccesstitle": "Uspešno prijavljivanje",
        "loginsuccess": "'''Prijavljeni ste kao „$1“.'''",
-       "nosuchuser": "Ne postoji korisnik s imenom „$1“.\nKorisnička imena su osetljiva na mala i velika slova.\nProverite da li ste ga dobro uneli ili [[Special:UserLogin/signup|otvorite novi nalog]].",
+       "nosuchuser": "Ne postoji korisnik s imenom „$1“.\nKorisnička imena su osetljiva na mala i velika slova.\nProverite da li ste ga dobro uneli ili [[Special:CreateAccount|otvorite novi nalog]].",
        "nosuchusershort": "Korisnik s imenom „$1“ ne postoji.\nProverite da li ste pravilno napisali.",
        "nouserspecified": "Morate navesti korisničko ime.",
        "login-userblocked": "{{GENDER:$1|Ovaj korisnik je blokiran|Ova korisnica je blokirana|Ovaj korisnik je blokiran}}. Prijava nije dozvoljena.",
        "accmailtext": "Lozika za {{GENDER:$1|korisnika|korisnicu}} [[User talk:$1|$1]] je poslata na $2. Nakon prijave, lozinka se može promeniti [[Special:ChangePassword|ovde]].",
        "newarticle": "(novi)",
        "newarticletext": "Došli ste na stranicu koja još ne postoji.\nDa biste je napravili, počnite da kucate u prozor ispod ovog teksta (pogledajte [$1 stranicu za pomoć]).\nAko ste ovde došli greškom, vratite se na prethodnu stranicu.",
-       "anontalkpagetext": "---- Ovo je stranica za razgovor s anonimnim korisnikom koji još nema nalog ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo ga prepoznali.\nTakvu adresu može deliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene primedbe, [[Special:UserLogin/signup|otvorite nalog]] ili se [[Special:UserLogin|prijavite]] da biste izbegli buduću zabunu s ostalim anonimnim korisnicima.",
+       "anontalkpagetext": "---- Ovo je stranica za razgovor s anonimnim korisnikom koji još nema nalog ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo ga prepoznali.\nTakvu adresu može deliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene primedbe, [[Special:CreateAccount|otvorite nalog]] ili se [[Special:UserLogin|prijavite]] da biste izbegli buduću zabunu s ostalim anonimnim korisnicima.",
        "noarticletext": "Na ovoj stranici trenutno nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|potražiti ovaj naslov]] na drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti srodne izveštaje] ili [{{fullurl:{{FULLPAGENAME}}|action=edit}} napraviti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Na ovoj stranici trenutno nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|potražiti ovaj naslov]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti srodne dnevnike]</span>, ali nemate dozvolu da napravite ovu stranicu.",
        "missing-revision": "Ne mogu da pronađem izmenu br. $1 na stranici pod nazivom „{{FULLPAGENAME}}“.\n\nOvo se obično dešava kada pratite zastarelu vezu do stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} dnevniku brisanja].",
        "whatlinkshere-prev": "{{PLURAL:$1|prethodni|prethodnih $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|sledeći|sledećih $1}}",
        "whatlinkshere-links": "← veze",
-       "whatlinkshere-hideredirs": "$1 preusmerenja",
-       "whatlinkshere-hidetrans": "$1 uključivanja",
-       "whatlinkshere-hidelinks": "$1 veze",
+       "whatlinkshere-hideredirs": "Sakrij preusmerenja",
+       "whatlinkshere-hidetrans": "Sakrij uključivanja",
+       "whatlinkshere-hidelinks": "Sakrij veze",
        "whatlinkshere-hideimages": "$1 veze do datoteke",
        "whatlinkshere-filters": "Filteri",
        "whatlinkshere-submit": "Idi",
        "size-megabytes": "$1 MB",
        "size-gigabytes": "$1 GB",
        "lag-warn-normal": "Izmene novije od $1 {{PLURAL:$1|sekunde|sekunde|sekundi}} neće biti prikazane.",
-       "lag-warn-high": "Zbog preopterećenja baze podataka, izmene novije od $1 {{PLURAL:$1|sekunde|sekundi}} neće biti prikazane.",
+       "lag-warn-high": "Zbog preopterećenja baze podataka, izmene novije od $1 {{PLURAL:$1|1=sekunde|sekunde|sekundi}} neće biti prikazane.",
        "watchlistedit-normal-title": "Uređivanje spiska nadgledanja",
        "watchlistedit-normal-legend": "Uklanjanje naslova sa spiska nadgledanja",
        "watchlistedit-normal-explain": "Naslovi na vašem spisku nadgledanja su prikazani ispod.\nDa biste uklonili naslov, označite kvadratić do njega i kliknite na „{{int:Watchlistedit-normal-submit}}“.\nMožete i da [[Special:EditWatchlist/raw|uredite sirov spisak]].",
index 0df794d..3c3d811 100644 (file)
        "noname": "Du moast n Benutsernoome anreeke.",
        "loginsuccesstitle": "Anmäldenge mäd Ärfoulch",
        "loginsuccess": "'''Du bäst nu as \"$1\" bie {{SITENAME}} anmälded.'''",
-       "nosuchuser": "Die Benutsernoome \"$1\" bestoant nit.\nUurpröif ju Skrieuwwiese (Groot-/Littekskrieuwenge beoachtje) of [[Special:UserLogin/signup|mäld die as näien Benutser an]].",
+       "nosuchuser": "Die Benutsernoome \"$1\" bestoant nit.\nUurpröif ju Skrieuwwiese (Groot-/Littekskrieuwenge beoachtje) of [[Special:CreateAccount|mäld die as näien Benutser an]].",
        "nosuchusershort": "Die Benutsernooome \"$1\" bestoant nit. Uurpröif ju Skrieuwwiese.",
        "nouserspecified": "Reek jädden n Benutsernoome an.",
        "login-userblocked": "Dissen Benutser is speerd. Anmäldenge nit ferlööwed.",
        "accmailtext": "N toufällich generierd Paaswoud foar [[User talk:$1|$1]] wuud an $2 fersoand.\n\nDät Paaswoud foar dit näie Benutserkonto kon ap ju Spezioalsiede\n„[[Special:ChangePassword|Paaswoud annerje]]“ annerd wäide.",
        "newarticle": "(Näi)",
        "newarticletext": "Du hääst n Link foulged ätter ne Siede, ju dät noch nit rakt.\nUum ju Siede tou moakjen, dien Text fon dän näie Artikkel iendreege in ju unnerstoundene Box (sjuch ju [$1 Hälpesiede] foar moor Informatione).\nBäst du hier bie Fersjoon, klik ju '''Tourääch'''-Skaltfläche fon din Browser.",
-       "anontalkpagetext": "----''Dit is ju Diskussionssiede fon n uunbekoanden Benutser, die sik nit anmälded häd.\nWail naan Noome deer is, wäd ju nuumeriske IP-Adrässe tou Identifizierenge ferwoand.\nMan oafte wäd sunne Adrässe fon moorere Benutsere ferwoand.\nWan du n uunbekoanden Benutser bääst un du toankst dät du Kommentare krichst do nit foar die meend sunt, dan koast du ap bääste n [[Special:UserLogin/signup|Benutserkonto iengjuchte]] of die [[Special:UserLogin|anmäldje]], uum sukke Fertuusengen mäd uur anomyme Benutsere tou fermieden.''",
+       "anontalkpagetext": "----''Dit is ju Diskussionssiede fon n uunbekoanden Benutser, die sik nit anmälded häd.\nWail naan Noome deer is, wäd ju nuumeriske IP-Adrässe tou Identifizierenge ferwoand.\nMan oafte wäd sunne Adrässe fon moorere Benutsere ferwoand.\nWan du n uunbekoanden Benutser bääst un du toankst dät du Kommentare krichst do nit foar die meend sunt, dan koast du ap bääste n [[Special:CreateAccount|Benutserkonto iengjuchte]] of die [[Special:UserLogin|anmäldje]], uum sukke Fertuusengen mäd uur anomyme Benutsere tou fermieden.''",
        "noarticletext": "Deer is apstuuns naan Text ap disse Siede.\nDu koast dissen Tittel ap do uur Sieden [[Special:Search/{{PAGENAME}}|säike]],\n<span class=\"plainlinks\"> in do touheerige [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} Logbouke säike] of disse Siede [{{fullurl:{{FULLPAGENAME}}|action=edit}} beoarbaidje]</span>.",
        "noarticletext-nopermission": "Der is apstuuns noch naan Text ap disse Siede.\nDu koast dissen Tittel ap do uur Sieden [[Special:Search/{{PAGENAME}}|säike]]\nof <span class=\"plainlinks\">in do touheerige [{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbouke säike].</span>",
        "userpage-userdoesnotexist": "Dät Benutserkonto „<nowiki>$1</nowiki>“ is nit deer. Pröif, of du disse Siede wuddelk moakje/beoarbaidje wolt.",
index ce6ca9d..cf4c8c4 100644 (file)
        "noname": "Anjeun teu nuliskeun ngaran pamaké nu sah.",
        "loginsuccesstitle": "Asup log geus hasil",
        "loginsuccess": "Anjeun ayeuna geus asup log ka {{SITENAME}} salaku \"$1\".",
-       "nosuchuser": "Euweuh pamaké nu ngaranna \"$1\".\nNgaran pamaké ngabedakeun hurup kapital.\nPariksa éjahanana, atawa paké formulir di handap pikeun [[Special:UserLogin/signup|nyieun rekening anyar]].",
+       "nosuchuser": "Euweuh pamaké nu ngaranna \"$1\".\nNgaran pamaké ngabedakeun hurup kapital.\nPariksa éjahanana, atawa paké formulir di handap pikeun [[Special:CreateAccount|nyieun rekening anyar]].",
        "nosuchusershort": "Taya pamaké nu ngaranna \"$1\", pariksa éjahanana!",
        "nouserspecified": "Anjeun kudu ngeusian ngaran landihan.",
        "login-userblocked": "Ieu pamaké keur dipeungpeuk, teu diwenangkeun asup log.",
        "accmailtext": "Sandi acak pikeun [[User talk:$1|$1]] geus dikirim ka $2. Éta sandi bisa diganti dina kaca ''[[Special:ChangePassword|ganti sandi]]'' sanggeus asup log.",
        "newarticle": "(anyar)",
        "newarticletext": "Anjeun geus nuturkeun tutumbu ka kaca nu can aya.\nPikeun nyieun kaca, mimitian ku ngetik jeroeun kotak di handap\n(tempo [$1 kaca pitulung] pikeun leuwih écés).\nMun anjeun ka dieu teu ngahaja, klik baé tombol '''back''' na panyungsi anjeun.",
-       "anontalkpagetext": "----\n<em>Ieu mangrupa kaca sawala pikeun pamaké anonim anu can nyieun akun, atawa anu henteu maké.</em>\nKu kituna kapaksa make alamat IP pikeun nyirikeun anjeunna. Alamat IP ieu bisa dipaké ku sababaraha jalma. Lamun anjeun salasahiji pamaké anonim sarta ngarasa aya koméntar nu teu pakait geus ditujukeun ka anjeun, mangga [[Special:UserLogin/signup|nyieun akun]] atawa [[Special:UserLogin|asup log]] sangkan teu pacorok jeung pamaké anonim lianna.",
+       "anontalkpagetext": "----\n<em>Ieu mangrupa kaca sawala pikeun pamaké anonim anu can nyieun akun, atawa anu henteu maké.</em>\nKu kituna kapaksa make alamat IP pikeun nyirikeun anjeunna. Alamat IP ieu bisa dipaké ku sababaraha jalma. Lamun anjeun salasahiji pamaké anonim sarta ngarasa aya koméntar nu teu pakait geus ditujukeun ka anjeun, mangga [[Special:CreateAccount|nyieun akun]] atawa [[Special:UserLogin|asup log]] sangkan teu pacorok jeung pamaké anonim lianna.",
        "noarticletext": "Kiwari can aya téks dina ieu kaca.\nAnjeun bisa [[Special:Search/{{PAGENAME}}|nyusud judul ieu kaca]] dina kaca séjén,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} nyusud log nu tumali],\natawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} nyieun ieu kaca]</span>.",
        "noarticletext-nopermission": "Kiwari can aya téks dina ieu kaca.\nAnjeun bisa [[Special:Search/{{PAGENAME}}|nyusud judul ieu kaca]] dina kaca séjén,atawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} nyusud log nu tumali]</span>, tapi teu wenang pikeun nyieun ieu kaca.",
        "missing-revision": "Révisi #$1 kaca \"{{FULLPAGENAME}}\" teu aya.\n\nKajadian ieu biasana kusabab nuturkeun tutumbu jujutan kaca anu geus dihapus.\nWincikanana bisa ditempo di [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} log hapusan].",
index df323fe..50a8700 100644 (file)
        "noname": "Du har angett ett ogiltigt användarnamn.",
        "loginsuccesstitle": "Inloggad",
        "loginsuccess": "'''Du är nu inloggad på {{SITENAME}} som \"$1\".'''",
-       "nosuchuser": "Det finns ingen användare med namnet \"$1\".\nAnvändarnamn är skiftlägeskänsliga.\nKontrollera din stavning, eller [[Special:UserLogin/signup|skapa ett nytt konto]].",
+       "nosuchuser": "Det finns ingen användare med namnet \"$1\".\nAnvändarnamn är skiftlägeskänsliga.\nKontrollera din stavning, eller [[Special:CreateAccount|skapa ett nytt konto]].",
        "nosuchusershort": "Det finns ingen användare som heter \"$1\". Kontrollera att du stavat rätt.",
        "nouserspecified": "Du måste ange ett användarnamn.",
        "login-userblocked": "Denna användare är blockerad. Inloggning är inte tillåtet.",
        "accmailtext": "Ett slumpgenererat lösenord för [[User talk:$1|$1]] har skickats till $2. Det kan ändras på sidan ''[[Special:ChangePassword|ändra lösenord]]'' när du loggar in.",
        "newarticle": "(Ny)",
        "newarticletext": "Du har klickat på en länk till en sida som inte finns ännu. För att skapa sidan, börja att skriva i fältet nedan (du kan läsa mer på [$1 hjälpsidan]). Om du kom hit av misstag kan du bara trycka på <strong>tillbaka</strong>-knappen i din webbläsare.",
-       "anontalkpagetext": "----''Detta är diskussionssidan för en anonym användare som inte ännu skapat ett konto, eller som inte använder det.\nDärför måste vi använda den numeriska IP-adressen för att identifiera honom/henne.\nEn sådan IP-adress kan delas av flera användare.\nOm du är en anonym användare och känner att irrelevanta kommentarer har riktats mot dig, vänligen [[Special:UserLogin/signup|skapa ett konto]] eller [[Special:UserLogin|logga in]] för att undvika framtida förväxlingar med andra anonyma användare.''",
+       "anontalkpagetext": "----''Detta är diskussionssidan för en anonym användare som inte ännu skapat ett konto, eller som inte använder det.\nDärför måste vi använda den numeriska IP-adressen för att identifiera honom/henne.\nEn sådan IP-adress kan delas av flera användare.\nOm du är en anonym användare och känner att irrelevanta kommentarer har riktats mot dig, vänligen [[Special:CreateAccount|skapa ett konto]] eller [[Special:UserLogin|logga in]] för att undvika framtida förväxlingar med andra anonyma användare.''",
        "noarticletext": "Det finns just nu ingen text på denna sida.\nDu kan [[Special:Search/{{PAGENAME}}|söka efter denna sidtitel]] på andra sidor, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} söka i relaterade loggar], eller [{{fullurl:{{FULLPAGENAME}}|action=edit}} skapa denna sida]</span>.",
        "noarticletext-nopermission": "Det finns för tillfället ingen text på denna sida.\nDu kan [[Special:Search/{{PAGENAME}}|söka efter denna sidas titel]] på andra sidor,\neller <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} söka i relaterade loggar]</span> men du har inte behörighet att skapa sidan.",
        "missing-revision": "Revisionen #$1 av sidan med namnet \"{{FULLPAGENAME}}\" finns inte.\n\nDetta orsakas vanligen av efter en gammal historiklänk till en sida som har raderats.\nDetaljer kan hittas i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} raderingsloggen].",
        "right-override-export-depth": "Exportera sidor inklusive länkade sidor till ett djup på 5",
        "right-sendemail": "Skicka e-post till andra användare",
        "right-passwordreset": "Visa e-postmeddelanden med lösenordsåterställning",
-       "right-managechangetags": "Skapa och radera [[Special:Tags|märken]] från databasen",
+       "right-managechangetags": "Skapa och (in)aktivera [[Special:Tags|märken]]",
        "right-applychangetags": "Tillämpa [[Special:Tags|märken]] tillsammans med ens ändringar",
        "right-changetags": "Lägg till och ta bort godtyckliga [[Special:Tags|märken]] på individuella sidversioner och loggposter.",
+       "right-deletechangetags": "Radera [[Special:Tags|märken]] från databasen",
        "grant-generic": "Rättighetsgrupp \"$1\"",
        "grant-group-page-interaction": "Interagera med sidor",
        "grant-group-file-interaction": "Interagera med media",
        "action-viewmyprivateinfo": "visa din privata information",
        "action-editmyprivateinfo": "redigera din privata information",
        "action-editcontentmodel": "ändra innehållsmodellen för en sida",
-       "action-managechangetags": "skapa och radera märken från databasen",
+       "action-managechangetags": "skapa och (in)aktivera märken",
        "action-applychangetags": "tillämpa märken tillsammans med dina ändringar",
        "action-changetags": "lägg till och ta bort godtyckliga märken på individuella sidversioner och loggposter",
+       "action-deletechangetags": "radera märken från databasen",
        "nchanges": "$1 {{PLURAL:$1|ändring|ändringar}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|sedan senaste besöket}}",
        "enhancedrc-history": "historik",
        "upload-form-label-own-work": "Detta är mitt eget verk",
        "upload-form-label-infoform-categories": "Kategorier",
        "upload-form-label-infoform-date": "Datum",
-       "upload-form-label-own-work-message-local": "Jag bekräftar att jag laddar upp denna fil enligt {{SITENAME}}s användarvillkor och licenspolicys.",
-       "upload-form-label-not-own-work-message-local": "Stäng denna dialogruta och prova ett annat sätt om du inte kan ladda upp denna fil under {{SITENAME}}s policys.",
-       "upload-form-label-not-own-work-local-local": "Du kanske också skulle vilja prova [[Special:Upload|standarduppladdningssidan]].",
-       "upload-form-label-own-work-message-default": "Jag förstår att jag laddar upp denna fil till ett delat centralförvar. Jag bekräftar att jag gör det enligt de användarvillkor och licenspolicys som finns där.",
-       "upload-form-label-not-own-work-message-default": "Stäng denna dialogruta och prova en annan metod om du inte kan ladda upp denna fil under de policys som gäller för det delade centralförvaret.",
-       "upload-form-label-not-own-work-local-default": "Du kanske också skulle vilja prova att använda [[Special:Upload|uppladdningssidan på {{SITENAME}}]] om denna fil kan laddas upp där under deras policys.",
-       "upload-form-label-own-work-message-shared": "Jag intygar att jag äger upphovsrätten för denna fil och samtycker till att oåterkalleligen släppa filen på Wikimedia Commons under licensen \n[https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] och jag accepterar [https://wikimediafoundation.org/wiki/Terms_of_Use villkoren för användning].",
-       "upload-form-label-not-own-work-message-shared": "Om du inte äger upphovsrätten för denna fil eller om du önskar att släppa den under en annan licens bör du överväga att använda [https://commons.wikimedia.org/wiki/Special:UploadWizard uppladdningsguiden på Commons].",
-       "upload-form-label-not-own-work-local-shared": "Du kanske skulle vilja prova att använda [[Special:Upload|uppladdningssidan på {{SITENAME}}]] om webbplatsens policys tillåter att denna fil laddas upp.",
+       "upload-form-label-own-work-message-generic-local": "Jag bekräftar att jag laddar upp denna fil enligt {{SITENAME}}s användarvillkor och licenspolicys.",
+       "upload-form-label-not-own-work-message-generic-local": "Stäng denna dialogruta och prova ett annat sätt om du inte kan ladda upp denna fil under {{SITENAME}}s policys.",
+       "upload-form-label-not-own-work-local-generic-local": "Du kanske också skulle vilja prova [[Special:Upload|standarduppladdningssidan]].",
+       "upload-form-label-own-work-message-generic-foreign": "Jag förstår att jag laddar upp denna fil till ett delat centralförvar. Jag bekräftar att jag gör det enligt de användarvillkor och licenspolicys som finns där.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Stäng denna dialogruta och prova en annan metod om du inte kan ladda upp denna fil under de policys som gäller för det delade centralförvaret.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Du kanske också skulle vilja prova att använda [[Special:Upload|uppladdningssidan på {{SITENAME}}]] om denna fil kan laddas upp där under deras policys.",
        "backend-fail-stream": "Kunde inte strömma filen $1.",
        "backend-fail-backup": "Kunde inte säkerhetskopiera filen ''$1''.",
        "backend-fail-notexists": "Filen $1 finns inte.",
        "whatlinkshere-prev": "{{PLURAL:$1|förra|förra $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|nästa|nästa $1}}",
        "whatlinkshere-links": "← länkar",
-       "whatlinkshere-hideredirs": "$1 omdirigeringar",
-       "whatlinkshere-hidetrans": "$1 inkluderingar",
-       "whatlinkshere-hidelinks": "$1 länkar",
-       "whatlinkshere-hideimages": "$1 fillänkar",
+       "whatlinkshere-hideredirs": "Dölj omdirigeringar",
+       "whatlinkshere-hidetrans": "Dölj inkluderingar",
+       "whatlinkshere-hidelinks": "Dölj länkar",
+       "whatlinkshere-hideimages": "Dölj fillänkar",
        "whatlinkshere-filters": "Filter",
        "whatlinkshere-submit": "Gå",
        "autoblockid": "Autoblockera #$1",
        "autoredircomment": "Omdirigerar till [[$1]]",
        "autosumm-new": "Skapade sidan med '$1'",
        "autosumm-newblank": "Skapade tom sida",
-       "size-bytes": "$1 byte",
+       "size-bytes": "$1 {{PLURAL:$1|byte}}",
        "size-kilobytes": "$1 kbyte",
        "size-megabytes": "$1 Mbyte",
        "size-gigabytes": "$1 Gbyte",
+       "size-pixel": "$1 {{PLURAL:$1|bildpunkt|bildpunkter}}",
        "lag-warn-normal": "Ändringar under {{PLURAL:$1|den senaste sekunden|de $1 senaste sekunderna}} kanske inte visas i den här listan.",
        "lag-warn-high": "På grund av omfattande fördröjning i databasen visas kanske inte ändringar nyare än $1 {{PLURAL:$1|sekund|sekunder}} i den här listan.",
        "watchlistedit-normal-title": "Redigera bevakningslista",
        "timezone-local": "Lokal",
        "duplicate-defaultsort": "'''Varning:''' Standardsorteringsnyckeln \"$2\" tar över från den tidigare standardsorteringsnyckeln \"$1\".",
        "duplicate-displaytitle": "<strong>Varning:</strong> Visningstiteln \"$2\" skriver över den tidigare visningstiteln \"$1\".",
+       "restricted-displaytitle": "<strong>Varning:</strong> Visningstiteln \"$1\" ignorerades eftersom den inte motsvarar sidans riktiga titel.",
        "invalid-indicator-name": "<p>Fel:</strong> Sidstatus-indikatorernas <code>namn</code>-attributet får inte vara tomt.",
        "version": "Version",
        "version-extensions": "Installerade programtillägg",
        "tags-delete-not-found": "Märket \"$1\" finns inte.",
        "tags-delete-too-many-uses": "Märket \"$1\" appliceras på fler än $2 {{PLURAL:$2|version|versioner}}, vilket innebär att det inte kan raderas.",
        "tags-delete-warnings-after-delete": "Märket \"$1\" raderades, men följande {{PLURAL:$2|varning|varningar}} uppstod:",
+       "tags-delete-no-permission": "Du har inte behörighet att radera ändringsmärken.",
        "tags-activate-title": "Aktivera märke",
        "tags-activate-question": "Du är på väg att aktivera märket \"$1\".",
        "tags-activate-reason": "Anledning:",
        "log-action-filter-suppress-event": "Loggcensur",
        "log-action-filter-suppress-revision": "Sidversionscensur",
        "log-action-filter-suppress-delete": "Sidcensur",
+       "log-action-filter-suppress-block": "Användarcensur efter blockering",
+       "log-action-filter-suppress-reblock": "Användarcensur efter återblockering",
        "log-action-filter-upload-upload": "Ny uppladdning",
        "log-action-filter-upload-overwrite": "Återuppladdning"
 }
index c39fa4e..d47434e 100644 (file)
        "noname": "Hauja dhihilisha jina la mtumiaji.",
        "loginsuccesstitle": "Umefaulu kuingia",
        "loginsuccess": "'''Umeingia {{SITENAME}} kama \"$1\".'''",
-       "nosuchuser": "Hakuna mtumiaji mwenye jina \"$1\".\nKumbuka kwamba programu inatofautishana kati ya herufi kubwa na ndogo.\nLabda umeandika vibaya, au [[Special:UserLogin/signup|sajili akaunti mpya]].",
+       "nosuchuser": "Hakuna mtumiaji mwenye jina \"$1\".\nKumbuka kwamba programu inatofautishana kati ya herufi kubwa na ndogo.\nLabda umeandika vibaya, au [[Special:CreateAccount|sajili akaunti mpya]].",
        "nosuchusershort": "Hakuna mtumiaji mwenye jina \"$1\". Labda umeandika vibaya.",
        "nouserspecified": "Lazima uandike jina la mtumiaji.",
        "login-userblocked": "Mtumiaji huyu amezuiwa. Hawezi kuingia.",
        "accmailtext": "Neno la siri limetolewa na programu kwa ajili ya [[User talk:$1|$1]] na limetumwa kwa $2.\n\nUnaweza kubadilisha neno la siri hilo kwenye ukurasa wa ''[[Special:ChangePassword|kubadilisha neno la siri]]'' baada ya kuingia.",
        "newarticle": "(Mpya)",
        "newarticletext": "Ukurasa unaotaka haujaandikwa bado. Ukipenda unaweza kuuandika wewe mwenyewe kwa kutumia sanduku la hapa chini (tazama [$1 Mwongozo] kwa maelezo zaidi). Ukifika hapa kwa makosa, bofya kibonyezi '''back''' (nyuma) cha programu yako.",
-       "anontalkpagetext": "----''Huu ni ukurasa wa majadiliano wa mtumiaji ambaye hana jina na bado hajaumba akaunti bado, au hajawahi kutumia kabisa.\nKwa hiyo tunatumia namba za anwani ya IP yake kumtambulisha.\nAnwani ya IP kama hiyo inaweza kutumika na watumiaji kadhaa.\nLabda itakusumbua kwamba kuna maoni mengine yanawekwa hapa na unaamini kwamba haya maoni hayakulengi. Ikiwa hivyo, tafadhali [[Special:UserLogin/signup|fungua akaunti]] au  [[Special:UserLogin|ingia]] ili kuepuka kuchanganywa na watumiaji wengine ambao hawana jina.''",
+       "anontalkpagetext": "----''Huu ni ukurasa wa majadiliano wa mtumiaji ambaye hana jina na bado hajaumba akaunti bado, au hajawahi kutumia kabisa.\nKwa hiyo tunatumia namba za anwani ya IP yake kumtambulisha.\nAnwani ya IP kama hiyo inaweza kutumika na watumiaji kadhaa.\nLabda itakusumbua kwamba kuna maoni mengine yanawekwa hapa na unaamini kwamba haya maoni hayakulengi. Ikiwa hivyo, tafadhali [[Special:CreateAccount|fungua akaunti]] au  [[Special:UserLogin|ingia]] ili kuepuka kuchanganywa na watumiaji wengine ambao hawana jina.''",
        "noarticletext": "Ukurasa huu haujaandikwa bado. [[Special:Search/{{PAGENAME}}|tafutia jina hili]] katika kurasa nyingine, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tafuta kumbukumbu zinazohusika], au [{{fullurl:{{FULLPAGENAME}}|action=edit}} hariri ukurasa huu]</span>.",
        "noarticletext-nopermission": "Kwa sasa hakuna maandishi katika ukurasa huu.\nUnaweza [[Special:Search/{{PAGENAME}}|kutafuta jina la ukurasa huu]] katika kurasa nyingine,\nau <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tafuta kumbukumbu zinazohusika]</span>, lakini huruhusiwi kuanzisha ukurasa huu.",
        "userpage-userdoesnotexist": "Akaunti ya mtumiaji \"<nowiki>$1</nowiki>\" haijasajilishwa.\nUkitaka kuanzisha au kuhariri ukurasa huu tafadhali ucheki jina la akaunti.",
index 589adc0..23b272e 100644 (file)
        "noname": "To ńy je poprowne mjano użytkowńika.",
        "loginsuccesstitle": "Logowańy udane",
        "loginsuccess": "'''Terozki jeżeś zalogowany do {{SITENAME}} kej \"$1\".'''",
-       "nosuchuser": "Ńy ma sam użytkowńika uo mjańe \"$1\".\nSprowdź szrajbůng, abo [[Special:UserLogin/signup|utwůrz nowe kůnto]].",
+       "nosuchuser": "Ńy ma sam użytkowńika uo mjańe \"$1\".\nSprowdź szrajbůng, abo [[Special:CreateAccount|utwůrz nowe kůnto]].",
        "nosuchusershort": "Ńy mo sam użytkowńika uo mjańe \"$1\".",
        "nouserspecified": "Podej mjano użytkowńika.",
        "login-userblocked": "Tyn sprowjorz ma zawrzite sprowjyńa. Ńy możno sie zalogować.",
        "accmailtext": "Cufalńe hasło lo [[User talk:$1|$1]] uostoło posłane do $2. Hasło lo tygo nowygo kůnta po zalogowańu je mogebność pomjyńić na zajće ''[[Special:ChangePassword|pomjyńańe hasła]]''.",
        "newarticle": "(Nowy)",
        "newarticletext": "Ńy mo sam jeszcze artikla uo takijj titli. Eli chcesz go sprowjać, naszkryflej niżyj jego tekst (wjyncy informacyj nojdźesz [$1 na zajće půmocy]). Eli żeś chćoł zrobić cośik inksze, naćiś ino knefel \"Nazod\".",
-       "anontalkpagetext": "---- ''To je zajta godki lo anůnimowych używoczy  - takich, kerzi ńy majům jeszcze swojigo kůnta abo ńy chcům go terozki używać.\nBy jejich idyntyfikować, używomy numerůw IP.\nEli jeżeś anůnimowym używoczym a wydowo Ći śe, aże zamjyszczůne sam kůmyntorze ńy sům skjyrowane do Ćebje, [[Special:UserLogin/signup|utwůrz kůnto]] abo [[Special:UserLogin|zaloguj śe]] - beztůż uńikńesz potym podobnych ńyporozumjyń.''",
+       "anontalkpagetext": "---- ''To je zajta godki lo anůnimowych używoczy  - takich, kerzi ńy majům jeszcze swojigo kůnta abo ńy chcům go terozki używać.\nBy jejich idyntyfikować, używomy numerůw IP.\nEli jeżeś anůnimowym używoczym a wydowo Ći śe, aże zamjyszczůne sam kůmyntorze ńy sům skjyrowane do Ćebje, [[Special:CreateAccount|utwůrz kůnto]] abo [[Special:UserLogin|zaloguj śe]] - beztůż uńikńesz potym podobnych ńyporozumjyń.''",
        "noarticletext": "Ńy můmy zajta uo takij titli. Mogesz [{{fullurl:{{FULLPAGENAME}}|action=edit}} wćepać artikel {{FULLPAGENAME}}] abo [[Special:Search/{{PAGENAME}}|sznupać {{PAGENAME}} we inkszych]].",
        "noarticletext-nopermission": "Ta zajta terozki je pusto.\nMogesz [[Special:Search/{{PAGENAME}}|wysznupać ta titla]] we treśćach inkszych zajtůw, abo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przesznupać powjůnzane rejery]</span>, nale ńy mosz uprowńyń coby ta zajta wćepać",
        "userpage-userdoesnotexist": "Użytkowńik \"<nowiki>$1</nowiki>\" ńy je zarejesztrowany. Sprowdź eli na pewno chćołżeś stworzyć/pomjynić gynał ta zajta.",
index 14de43c..1308537 100644 (file)
        "noname": "நீங்கள் கொடுத்த பயனர் பெயர் செல்லுபடியற்றது.",
        "loginsuccesstitle": "புகுபதிகையில் உள்ளீர்கள்.",
        "loginsuccess": "நீங்கள் தற்பொழுது {{SITENAME}} தளத்தில் \"$1\" கணக்கினூடாக புகுபதிகை செய்துள்ளீர்கள்.",
-       "nosuchuser": "\"$1\" என்ற பெயரில் பயனர் எவருமில்லை.\n\nபயனர் பெயர், பெரிய எழுத்து,  சிறிய எழுத்து என்ற வித்தியாசத்திற்குட்பட்டது.\n\nஎழுத்துப் பிழைகளைச் சரி பார்க்கவும், அல்லது [[Special:UserLogin/signup|புதிய பயனர் கணக்கொன்றை உருவாக்கவும்]].",
+       "nosuchuser": "\"$1\" என்ற பெயரில் பயனர் எவருமில்லை.\n\nபயனர் பெயர், பெரிய எழுத்து,  சிறிய எழுத்து என்ற வித்தியாசத்திற்குட்பட்டது.\n\nஎழுத்துப் பிழைகளைச் சரி பார்க்கவும், அல்லது [[Special:CreateAccount|புதிய பயனர் கணக்கொன்றை உருவாக்கவும்]].",
        "nosuchusershort": "\"$1\" என்ற பெயரில் பயனர் யாரும் இல்லை. நீங்கள் உள்ளிட்ட பெயரைச் சரி பார்க்கவும்.",
        "nouserspecified": "நீங்கள் பயனர் பெயரொன்றைக் குறிப்பிட வேண்டும்.",
        "login-userblocked": "இப்பயனர் கணக்கு தடைசெய்யப்பட்டுள்ளது. புகுபதிகை அனுமதிக்கப்படவில்லை.",
        "accmailtext": "தானியக்கமாக [[User talk:$1|$1]]-க்கு ஒரு கடவுச்சொலை உறுவாக்கி $2-க்கு அனுப்பி வைக்கப்பட்டுள்ளது. இதனை புகுபதிகை செய்தவுடன் ''[[Special:ChangePassword|கடவுச்சொல்லை மாற்று]]'' பக்கத்தில் மாற்றிக்கொள்ளளாம்.",
        "newarticle": "(புதிது)",
        "newarticletext": "ஒரு இணைப்பினூடாக நீங்கள் வந்துள்ள இப்பக்கம் இன்னும் உருவாக்கப்படவில்லை. பக்கத்தை உருவாக்குவதற்குக் கீழேயுள்ள கட்டத்துள் தட்டச்சிடத் தொடங்குங்கள். (மேலதிக விபரங்களுக்கு [$1 உதவிப் பக்கத்தைப்] பார்க்கவும்). நீங்கள் தவறுதலாக இங்கே வந்திருந்தால், உங்கள் உலாவியின் பின் செல்வதற்கான பொத்தானைச் சொடுக்கவும்.",
-       "anontalkpagetext": "----''இது இன்னும் கணக்கொன்று ஏற்படுத்தாத அல்லது வழமையாக பயனர் கணக்கை பயன்படுத்தாத பயனர்களுக்குரிய கலந்துரையாடல் பக்கமாகும். அதனால் நாங்கள் இவரை அடையாளம் காண்பதற்கு எண்சார்ந்த ஐபி முகவரியைப் பயன்படுத்த வேண்டியதாய் இருக்கின்றது. இவ்வாறான ஐபி முகவரிகள் பல பயனர்களினால் பகிர்ந்துகொள்ளப்படலாம்.\nநீங்கள் ஒரு முகவரியற்ற பயனராயிருந்து, தொடர்பற்ற கருத்துக்கள் உங்களைக் குறித்துச் சொல்லப்பட்டிருப்பதாக நீங்கள் உணர்ந்தால், முகவரியற்ற ஏனைய பயனர்களுடனான குழப்பங்களை எதிர்காலத்தில் தவிர்ப்பதற்கு, தயவுசெய்து [[Special:UserLogin/signup|புதிய கணக்கொன்றை ஏற்படுத்துங்கள்]] அல்லது [[Special:UserLogin|புகுபதிகை]] செய்யுங்கள்.''",
+       "anontalkpagetext": "----''இது இன்னும் கணக்கொன்று ஏற்படுத்தாத அல்லது வழமையாக பயனர் கணக்கை பயன்படுத்தாத பயனர்களுக்குரிய கலந்துரையாடல் பக்கமாகும். அதனால் நாங்கள் இவரை அடையாளம் காண்பதற்கு எண்சார்ந்த ஐபி முகவரியைப் பயன்படுத்த வேண்டியதாய் இருக்கின்றது. இவ்வாறான ஐபி முகவரிகள் பல பயனர்களினால் பகிர்ந்துகொள்ளப்படலாம்.\nநீங்கள் ஒரு முகவரியற்ற பயனராயிருந்து, தொடர்பற்ற கருத்துக்கள் உங்களைக் குறித்துச் சொல்லப்பட்டிருப்பதாக நீங்கள் உணர்ந்தால், முகவரியற்ற ஏனைய பயனர்களுடனான குழப்பங்களை எதிர்காலத்தில் தவிர்ப்பதற்கு, தயவுசெய்து [[Special:CreateAccount|புதிய கணக்கொன்றை ஏற்படுத்துங்கள்]] அல்லது [[Special:UserLogin|புகுபதிகை]] செய்யுங்கள்.''",
        "noarticletext": "இப் பக்கத்தில் தற்பொழுது உள்ளடக்கம் எதுவுமில்லை. நீங்கள் இப்பக்க [[Special:Search/{{PAGENAME}}|தலைப்பை வேறு பக்கங்களில் தேடவோ]] அல்லது [{{fullurl:{{FULLPAGENAME}}|action=edit}} இப்பக்கத்தை தொகுக்கவோ] முடியும்.",
        "noarticletext-nopermission": "தற்பொழுது இப்பக்கத்தில் உரை எதுவும் இல்லை.\nநீங்கள் [[Special:Search/{{PAGENAME}}|பக்கத் தலைப்பை வைத்து]] அல்லது மற்ற பக்கங்களில்,\nஅல்லது <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} அல்லது தேடுதல் தொடர்பான பதிவுகளில் தேடலாம்.]</span>, ஆனால் இந்தப் பக்கத்தை உருவாக்க அனுமதியில்லை.",
        "missing-revision": "இந்த பரிசீலனை # $1  '' {{FULLPAGENAME}}' பெயருள்ள பக்கத்தின் இல்லை.!N வேடிக்கையானN!இது தான் வழக்கமாக ஏற்பட்டிருக்கலாம் நீக்கப்பட்டுள்ளது பக்கத்திற்கு outdated வரலாறு இணைப்பை தொடர்ந்து.\nவிவரங்கள் முடியும் கண்டறிய, [{{fullurl: {{# சிறப்பு: குறிப்பேடு}} / delete|page = {{FULLPAGENAMEE}}}} நீக்குதல் குறிப்பேடு].",
index fc8fc69..0a71f3f 100644 (file)
        "noname": "ಈರ್ ಸರಿಯಾಯಿನ ಬಳಕೆದಾರ ಪುದರ್ ಕೊರ್ತಿಜ್ಜರ್.",
        "loginsuccesstitle": "ಲಾಗ್ ಇನ್ ಯಶಸ್ವಿಯಾತ್ಂಡ್",
        "loginsuccess": "ಲಾಗ್ ಇನ್ ಯಶಸ್ವಿಯಾತ್‘ಂಡ್\". {{SITENAME}}  \"$1\".'''",
-       "nosuchuser": "!!\"$1\"ಪುದರ್‘ದ ವಾ ಸದಸ್ಯೆರ್‘ಲಾ ಇಜ್ಜೆರ್, ಅಕ್ಷರ ಸರಿಯಾದ ತೂಲೆ ಅಥವಾ  [[Special:UserLogin/signup|ಪೊಸ ಸದಸ್ಯತ್ವ  ಖಾತೆನ್ ಸೃಷ್ಟಿ ಮಲ್ಪುಲೆ]].",
+       "nosuchuser": "!!\"$1\"ಪುದರ್‘ದ ವಾ ಸದಸ್ಯೆರ್‘ಲಾ ಇಜ್ಜೆರ್, ಅಕ್ಷರ ಸರಿಯಾದ ತೂಲೆ ಅಥವಾ  [[Special:CreateAccount|ಪೊಸ ಸದಸ್ಯತ್ವ  ಖಾತೆನ್ ಸೃಷ್ಟಿ ಮಲ್ಪುಲೆ]].",
        "nosuchusershort": "!!\"$1\"ಪುದರ್‘ದ ವಾ ಸದಸ್ಯೆರ್‘ಲಾ ಇಜ್ಜೆರ್, ಅಕ್ಷರ ಸರಿಯಾದ ತೂಲೆ.",
        "nouserspecified": "ಈರ್ ಒಂಜಿ ಸದಸ್ಯತ್ವದ ಪುದರ್ ಸೂಚನೆ ಮಲ್ಪೊಡು.",
        "login-userblocked": "ಈ ಸದಸ್ಯರೆನ ಖಾತೆನ್ ತಡೆ ಪತ್ತ್‘ದುಂಡು. ಲಾಗ್ ಇನ್ ಮಲ್ಪರೆ ಆಪುಜ್ಜಿ.",
index de45686..ad0cd23 100644 (file)
        "noname": "మీరు సరైన వాడుకరి పేరు ఇవ్వలేదు.",
        "loginsuccesstitle": "ప్రవేశం విజయవంతమైంది",
        "loginsuccess": "<strong>మీరు ఇప్పుడు {{SITENAME}}లోనికి \"$1\"గా ప్రవేశించారు.</strong>",
-       "nosuchuser": "\"$1\" అనే పేరుతో వాడుకరులు లేరు.\nవాడుకరి పేర్లు కేస్ సెన్సిటివ్.\nఅక్షరక్రమం సరిచూసుకోండి, లేదా [[Special:UserLogin/signup|కొత్త ఖాతా సృష్టించుకోండి]].",
+       "nosuchuser": "\"$1\" అనే పేరుతో వాడుకరులు లేరు.\nవాడుకరి పేర్లు కేస్ సెన్సిటివ్.\nఅక్షరక్రమం సరిచూసుకోండి, లేదా [[Special:CreateAccount|కొత్త ఖాతా సృష్టించుకోండి]].",
        "nosuchusershort": "\"$1\" పేరుతో వాడుకరి ఎవరూ లేరు. పేరు సరి చూసుకోండి.",
        "nouserspecified": "వాడుకరి పేరును తప్పనిసరిగా ఇవ్వాలి.",
        "login-userblocked": "ఈ వాడుకరిని నిరోధించారు. ప్రవేశానికి అనుమతి లేదు.",
        "accmailtext": "[[User talk:$1|$1]] కొరకు ఒక యాదృచ్ఛిక సంకేతపదాన్ని $2కి పంపించాం. లాగినయ్యాక, ''[[Special:ChangePassword|సంకేతపదాన్ని మార్చుకోండి]]'' అనే పేజీలో ఈ సంకేతపదాన్ని మార్చుకోవచ్చు.",
        "newarticle": "(కొత్తది)",
        "newarticletext": "ఈ లింకుకు సంబంధించిన పేజీ లేనే లేదు.\nకింది పెట్టెలో మీ రచనను టైపు చేసి ఆ పేజీని సృష్టించండి (దీనిపై సమాచారం కొరకు [$1 సహాయం పేజీ] చూడండి). మీరిక్కడికి పొరపాటున వచ్చి ఉంటే, మీ బ్రౌజరు <strong>back</strong> మీట నొక్కండి.",
-       "anontalkpagetext": "----\n<em>ఇది ఒక అజ్ఞాత వాడుకరి చర్చా పేజీ. ఆ వాడుకరి ఇంకా తనకై ఖాతాను సృష్టించుకోలేదు, లేదా ఖాతా ఉన్నా దానిని ఉపయోగించడం లేదు.</em>\nఅంచేత, అతణ్ణి/ఆమెను గుర్తించడానికి ఐ.పీ. చిరునామాను వాడాల్సి వచ్చింది. \nఒకే ఐ.పీ. చిరునామాని చాలా మంది వాడుకరులు ఉపయోగించే అవకాశం ఉంది. \nమీరూ అజ్ఞాత వాడుకరి అయితే, మీకు సంబంధంలేని వ్యాఖ్యలు మిమ్మల్ని ఉద్దేశించినట్టుగా అనిపిస్తే, భవిష్యత్తులో ఇతర అజ్ఞాత వాడుకరులతో అయోమయం లేకుండా ఉండటానికి, [[Special:UserLogin/signup|ఖాతాను సృష్టించుకోండి]] లేదా [[Special:UserLogin|లాగినవండి]].''",
+       "anontalkpagetext": "----\n<em>ఇది ఒక అజ్ఞాత వాడుకరి చర్చా పేజీ. ఆ వాడుకరి ఇంకా తనకై ఖాతాను సృష్టించుకోలేదు, లేదా ఖాతా ఉన్నా దానిని ఉపయోగించడం లేదు.</em>\nఅంచేత, అతణ్ణి/ఆమెను గుర్తించడానికి ఐ.పీ. చిరునామాను వాడాల్సి వచ్చింది. \nఒకే ఐ.పీ. చిరునామాని చాలా మంది వాడుకరులు ఉపయోగించే అవకాశం ఉంది. \nమీరూ అజ్ఞాత వాడుకరి అయితే, మీకు సంబంధంలేని వ్యాఖ్యలు మిమ్మల్ని ఉద్దేశించినట్టుగా అనిపిస్తే, భవిష్యత్తులో ఇతర అజ్ఞాత వాడుకరులతో అయోమయం లేకుండా ఉండటానికి, [[Special:CreateAccount|ఖాతాను సృష్టించుకోండి]] లేదా [[Special:UserLogin|లాగినవండి]].''",
        "noarticletext": "ప్రస్తుతం ఈ పేజీలో పాఠ్యమేమీ లేదు.\nవేరే పేజీలలో [[Special:Search/{{PAGENAME}}|ఈ పేజీ శీర్షిక కోసం వెతకవచ్చు]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} సంబంధిత చిట్టాలు చూడవచ్చు],\nలేదా [{{fullurl:{{FULLPAGENAME}}|action=edit}} ఈ పేజీని మార్చవచ్చు]</span>.",
        "noarticletext-nopermission": "ప్రస్తుతం ఈ పేజీలో పాఠ్యమేమీ లేదు.\nమీరు ఇతర పేజీలలో [[Special:Search/{{PAGENAME}}|ఈ పేజీ శీర్షిక కోసం వెతకవచ్చు]], లేదా <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} సంబంధిత చిట్టాలలో వెతకవచ్చు]</span>, కానీ ఈ పేజీని సృష్టించడానికి మీకు అనుమతి లేదు.",
        "missing-revision": "\"{{FULLPAGENAME}}\" అనే పేజీ యొక్క కూర్పు #$1 ఉనికిలో లేదు. సాధారణంగా ఏదైనా తొలగించబడిన పేజీ యొక్క కాలం చెల్లిన చరితం లింకును నొక్కినపుడు ఇది జరుగుతుంది. వివరాలు [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} తొలగింపు లాగ్] లో దొరుకుతాయి.",
index 70b9b62..6450ba1 100644 (file)
        "noname": "Номи корбари дурустеро шумо пешниҳод накардед.",
        "loginsuccesstitle": "Вуруд бо муваффақият",
        "loginsuccess": "'''Шумо акнун ба Википедиа ҳамчун \"$1\". вуруд кардед'''",
-       "nosuchuser": "Корбаре бо номи \"$1\" вуҷуд надорад.\nАмали номро барраси кунед, ё [[Special:UserLogin/signup|ҳисоби ҷадидеро эҷод кунед]].",
+       "nosuchuser": "Корбаре бо номи \"$1\" вуҷуд надорад.\nАмали номро барраси кунед, ё [[Special:CreateAccount|ҳисоби ҷадидеро эҷод кунед]].",
        "nosuchusershort": "Ягон корбаре бо номи \"$1\" вуҷуд надорад. Тарзи навишти номро санҷед.",
        "nouserspecified": "Шумо бояд як номи корбарӣ мушаххас кунед.",
        "login-userblocked": "Ин корбар баста шудааст. Вуруд манъ аст.",
        "accmailtext": "Як гузарвожаи тасодуфан эҷодшуда барои [[User talk:$1|$1]] ба $2 фиристода шуд. Онро метавон дар саҳифаи <em>[[Special:ChangePassword|тағийри гузарвожа]]</em> пас аз вуруд тағйир дод.",
        "newarticle": "(Нав)",
        "newarticletext": "Шумо пайвандеро интихоб кардед, ки саҳифа дар он арзи вуҷуд надорад.\nБарои сохтани саҳифа, ба қуттии зерин нависед ([$1 саҳифаи роҳнаморо] барои маълумоти бештар нигаред).\nАгар аз сабаби хатогӣ ва ё иштибоҳ омадед, тугмаи '''Ба оқиб'''-ро дар браузери худ пахш кунед.",
-       "anontalkpagetext": "----\n<em>Ин саҳифаи баҳс барои як корбари гумном (аноним) аст, ки ҳанӯз ҳисоби ҷадид эҷод накардааст ва ё аз он истифода намекунад.</em> \nБинобар ин барои шиносоиаш маҷбурем аз нишонаи IP рақамии вай истифода баред.\nЧунин нишонаи IP шояд аз сӯи чандин корбарон ба шакли муштарак истифода шавад. \nАгар шумо корбари гумном ҳастед ва ҳис мекунед, ки изҳори назари номарбуте ба шумо сурат гирифтааст, лутфан [[Special:UserLogin/signup|ҳисоберо эҷод кунед]] ё [[Special:UserLogin|вуруд шавед]] барои пешгирӣ аз нофаҳмиҳои оянда бо корбарони гумном.",
+       "anontalkpagetext": "----\n<em>Ин саҳифаи баҳс барои як корбари гумном (аноним) аст, ки ҳанӯз ҳисоби ҷадид эҷод накардааст ва ё аз он истифода намекунад.</em> \nБинобар ин барои шиносоиаш маҷбурем аз нишонаи IP рақамии вай истифода баред.\nЧунин нишонаи IP шояд аз сӯи чандин корбарон ба шакли муштарак истифода шавад. \nАгар шумо корбари гумном ҳастед ва ҳис мекунед, ки изҳори назари номарбуте ба шумо сурат гирифтааст, лутфан [[Special:CreateAccount|ҳисоберо эҷод кунед]] ё [[Special:UserLogin|вуруд шавед]] барои пешгирӣ аз нофаҳмиҳои оянда бо корбарони гумном.",
        "noarticletext": "Дар ин саҳифа то кунун матне вуҷуд надорад.\nШумо метавонед дар дигар саҳифаҳо [[Special:Search/{{PAGENAME}}|унвони ин саҳифаро ҷустуҷӯ кунед]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} гузоришҳои алоқамандро ҷустуҷӯ намоед],\nё [{{fullurl:{{FULLPAGENAME}}|action=edit}} ин саҳифаро вироиш кунед]</span>.",
        "noarticletext-nopermission": "Дар ин саҳифа то кунун матне вуҷуд надорад. Шумо метавонед дар дигар саҳифаҳо [[Special:Search/{{PAGENAME}}|унвони ин саҳифаро ҷустуҷӯ кунед]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} гузоришҳои алоқамандро ҷустуҷӯ намоед], ё [{{fullurl:{{FULLPAGENAME}}|action=edit}} ин саҳифаро вироиш кунед]</span>.",
        "missing-revision": "Нусхаи #$1 саҳифа бо номи \"{{FULLPAGENAME}}\" вуҷуд надорад.\n\nОдатан ин амал дар ҳоли пайгирии пайванди барӯзнашудаи таърих ба саҳифаи ҳазвшуда рух медиҳад.\nИттилооти муфассалро [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дар гузориши ҳазв] дарёфт кардан мумкин аст.",
index 981c7ca..d5c41c3 100644 (file)
        "noname": "Nomi korbari durustero şumo peşnihod nakarded.",
        "loginsuccesstitle": "Vurud bo muvaffaqijat",
        "loginsuccess": "'''Şumo aknun ba Vikipedia hamcun \"$1\". vurud karded'''",
-       "nosuchuser": "Korbare bo nomi \"$1\" vuçud nadorad.\nAmali nomro barrasi kuned, jo [[Special:UserLogin/signup|hisobi çadidero eçod kuned]].",
+       "nosuchuser": "Korbare bo nomi \"$1\" vuçud nadorad.\nAmali nomro barrasi kuned, jo [[Special:CreateAccount|hisobi çadidero eçod kuned]].",
        "nosuchusershort": "Jagon korbare bo nomi \"$1\" vuçud nadorad. Tarzi navişti nomro sançed.",
        "nouserspecified": "Şumo bojad jak nomi korbarī muşaxxas kuned.",
        "login-userblocked": "In korbar basta şudaast. Vurud man' ast.",
index c81200a..2070932 100644 (file)
@@ -22,7 +22,8 @@
                        "วรากร อึ้งวิเชียร (Varakorn Ungvichian)",
                        "아라",
                        "Pphongpan355",
-                       "Macofe"
+                       "Macofe",
+                       "Pilarbini"
                ]
        },
        "tog-underline": "การขีดเส้นใต้ลิงก์:",
@@ -40,6 +41,7 @@
        "tog-watchdefault": "เพิ่มหน้าและไฟล์ที่ฉันแก้ไขเข้ารายการเฝ้าดู",
        "tog-watchmoves": "เพิ่มและไฟล์ที่ฉันย้ายเข้ารายการเฝ้าดู",
        "tog-watchdeletion": "เพิ่มหน้าและไฟล์ที่ฉันลบเข้ารายการเฝ้าดู",
+       "tog-watchuploads": "เพิ่มไฟล์ใหม่ที่ฉันอัพโหลดไปยังรายการเฝ้าดูของฉัน",
        "tog-watchrollback": "เพิ่มหน้าที่ฉันย้อนกลับฉุกเฉินเข้ารายการเฝ้าดู",
        "tog-minordefault": "กำหนดการแก้ไขทุกครั้งเป็นการแก้ไขเล็กน้อยโดยปริยาย",
        "tog-previewontop": "แสดงตัวอย่างก่อนกล่องแก้ไข",
        "noname": "คุณไม่ได้ใส่ชื่อผู้ใช้ที่ถูกต้อง",
        "loginsuccesstitle": "ล็อกอินสำเร็จ",
        "loginsuccess": "<strong>ขณะนี้คุณล็อกอินสู่ {{SITENAME}} ในชื่อ \"$1\"</strong>",
-       "nosuchuser": "ไม่มีผู้ใช้ชื่อ \"$1\"\nชื่อผู้ใช้นั้นไวต่ออักษรใหญ่เล็ก\nกรุณาตรวจการสะกดอีกครั้ง หรือ[[Special:UserLogin/signup|สร้างบัญชีใหม่]]",
+       "nosuchuser": "ไม่มีผู้ใช้ชื่อ \"$1\"\nชื่อผู้ใช้นั้นไวต่ออักษรใหญ่เล็ก\nกรุณาตรวจการสะกดอีกครั้ง หรือ[[Special:CreateAccount|สร้างบัญชีใหม่]]",
        "nosuchusershort": "ไม่มีผู้ใช้ชื่อ \"$1\" \nกรุณาตรวจสอบการสะกด",
        "nouserspecified": "คุณต้องระบุชื่อผู้ใช้",
        "login-userblocked": "ผู้ใช้นี้ถูกบล็อก ไม่อนุญาตให้ล็อกอิน",
        "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": "ตั้งรหัสผ่านใหม่",
        "resetpass_forbidden": "ไม่สามารถเปลี่ยนรหัสผ่านได้",
        "resetpass-no-info": "คุณต้องล็อกอินเพื่อเข้าถึงหน้านี้โดยตรง",
        "resetpass-submit-loggedin": "เปลี่ยนรหัสผ่าน",
        "minoredit": "เป็นการแก้ไขเล็กน้อย",
        "watchthis": "เฝ้าดูหน้านี้",
        "savearticle": "บันทึกหน้า",
+       "publishpage": "เผยแพร่หน้า",
        "preview": "ตัวอย่าง",
        "showpreview": "แสดงตัวอย่าง",
        "showdiff": "แสดงการเปลี่ยนแปลง",
        "accmailtext": "ส่งรหัสผ่านแบบสุ่มของ [[User talk:$1|$1]] ไป $2 แล้ว สามารถเปลี่ยนรหัสผ่านในหน้า<em>[[Special:ChangePassword|เปลี่ยนรหัสผ่าน]]</em> หลังล็อกอิน",
        "newarticle": "(ใหม่)",
        "newarticletext": "คุณตามลิงก์ไปยังหน้าที่ยังไม่มีในขณะนี้\nในการสร้างหน้า เริ่มพิมพ์ในกล่องด้านล่าง (ดูข้อมูลเพิ่มเติมใน[$1 หน้าคำอธิบาย])\nถ้าคุณเข้ามาหน้านี้โดยผิดพลาด ให้กดปุ่ม<strong>ถอยหลัง</strong> (back) ของเบราว์เซอร์",
-       "anontalkpagetext": "----\n<em>หน้านี้เป็นหน้าคุยกับผู้ใช้สำหรับผู้ใช้นิรนามซึ่งยังไม่ได้สร้างหรือใช้บัญชี</em>\nดังนั้นเราจึงระบุตัวตนโดยใช้เลขที่อยู่ไอพีแทน\nเลขที่อยู่ไอพีนี้อาจมีผู้ใช้ร่วมกันหลายคน\nถ้าคุณเป็นผู้ใช้นิรนาม และรู้สึกว่าคุณได้รับความเห็นที่ไม่เกี่ยวข้องส่งหาคุณ กรุณา[[Special:UserLogin/signup|สร้างบัญชี]]หรือ[[Special:UserLogin|ล็อกอิน]] เพื่อป้องกันการสับสนกับผู้ใช้นิรนามรายอื่นในอนาคต",
+       "anontalkpagetext": "----\n<em>หน้านี้เป็นหน้าคุยกับผู้ใช้สำหรับผู้ใช้นิรนามซึ่งยังไม่ได้สร้างหรือใช้บัญชี</em>\nดังนั้นเราจึงระบุตัวตนโดยใช้เลขที่อยู่ไอพีแทน\nเลขที่อยู่ไอพีนี้อาจมีผู้ใช้ร่วมกันหลายคน\nถ้าคุณเป็นผู้ใช้นิรนาม และรู้สึกว่าคุณได้รับความเห็นที่ไม่เกี่ยวข้องส่งหาคุณ กรุณา[[Special:CreateAccount|สร้างบัญชี]]หรือ[[Special:UserLogin|ล็อกอิน]] เพื่อป้องกันการสับสนกับผู้ใช้นิรนามรายอื่นในอนาคต",
        "noarticletext": "ปัจจุบันไม่มีข้อความในหน้านี้\nคุณสามารถ[[Special:Search/{{PAGENAME}}|ค้นหาชื่อเรื่องหน้านี้]]ในหน้าอื่น <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ค้นหาปูมที่เกี่ยวข้อง] หรือ[{{fullurl:{{FULLPAGENAME}}|action=edit}} แก้ไขหน้านี้]</span>",
        "noarticletext-nopermission": "ปัจจุบันไม่มีข้อความในหน้านี้\nคุณสามารถ[[Special:Search/{{PAGENAME}}|ค้นหาชื่อหน้านี้]]ในหน้าอื่น หรือ<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ค้นหาปูมที่เกี่ยวข้อง]</span> แต่คุณไม่มีสิทธิสร้างหน้านี้",
        "missing-revision": "ไม่มีรุ่นปรับปรุง #$1 ของหน้าชื่อ \"{{FULLPAGENAME}}\" \n\nปกติเกิดจากการตามการโยงประวัติเก่าไปยังหน้าที่ถูกลบแล้ว\nดูรายละเอียดได้ที่[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ปูมการลบ]",
        "yourdiff": "ความแตกต่าง",
        "copyrightwarning": "โปรดระลึกว่างานเขียนทั้งหมดใน {{SITENAME}} ถือว่าเผยแพร่ภายใต้ $2 (ดูรายละเอียดทาง $1)\nหากคุณไม่ต้องการให้งานของคุณถูกแก้ไขและกระจายได้ตามใจ ก็อย่าส่งเข้ามา<br />\nนอกจากนี้ คุณยังสัญญาเราว่าคุณเขียนงานด้วยตนเอง หรือคัดลอกจากสาธารณสมบัติหรือทรัพยากรเสรีที่คล้ายกัน\n<strong>อย่าส่งงานมีลิขสิทธิ์โดยไม่ได้รับอนุญาต!</strong>",
        "copyrightwarning2": "โปรดระลึกว่างานเขียนทั้งหมดใน {{SITENAME}} อาจถูกผู้เขียนอื่นแก้ไข เปลี่ยนแปลงหรือนำออก\nหากคุณไม่ต้องการให้งานของคุณถูกแก้ไข ก็อย่าส่งเข้ามา<br />\nนอกจากนี้ คุณยังสัญญาเราว่าคุณเขียนงานด้วยตนเอง หรือคัดลอกจากสาธารณสมบัติหรือทรัพยากรเสรีที่คล้ายกัน (ดูรายละเอียดที่ $1)\n<strong>อย่าส่งงานมีลิขสิทธิ์โดยไม่ได้รับอนุญาต!</strong>",
+       "editpage-cannot-use-custom-model": "รูปแบบเนื้อหาหน้านี้ไม่สามารถเปลี่ยนได้",
        "longpageerror": "<strong>ข้อผิดพลาด: ข้อความที่คุณส่งมีขนาด $1 กิโลไบต์\nซึ่งเกินสูงสุด $2 กิโลไบต์</strong>\nไม่สามารถบันทึกได้",
        "readonlywarning": "<strong>คำเตือน: ฐานข้อมูลถูกล็อกเพื่อบำรุงรักษา คุณจึงไม่สามารถบันทึกการแก้ไขของคุณได้ในขณะนี้</strong>\nคุณอาจต้องการคัดลอกและวางข้อความของคุณในไฟล์ข้อความ และบันทึกไว้ภายหลัง\n\nผู้ดูแลระบบที่ล็อกฐานข้อมูลให้คำอธิบายดังนี้: $1",
        "protectedpagewarning": "<strong>คำเตือน: หน้านี้ถูกล็อก เพื่อให้เฉพาะผู้ใช้ที่มีสิทธิผู้ดูแลระบบแก้ไขได้เท่านั้น</strong>\nรายการปูมล่าสุดจัดไว้ด้านล่างเพื่อการอ้างอิง:",
        "userrights": "การบริหารสิทธิผู้ใช้",
        "userrights-lookup-user": "บริหารกลุ่มผู้ใช้",
        "userrights-user-editname": "ใส่ชื่อผู้ใช้:",
-       "editusergroup": "แก้ไขกลุ่มผู้ใช้",
+       "editusergroup": "แก้ไขกลุ่ม{{GENDER:$1|ผู้ใช้}}",
        "editinguser": "กำลังเปลี่ยนสิทธิผู้ใช้ของผู้ใช้ <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "แก้ไขกลุ่มผู้ใช้",
-       "saveusergroups": "บันทึกกลุ่มผู้ใช้",
+       "saveusergroups": "บันทึกกลุ่ม{{GENDER:$1|ผู้ใช้}}",
        "userrights-groupsmember": "สมาชิกของ:",
        "userrights-groupsmember-auto": "สมาชิกโดยปริยายของ:",
        "userrights-groups-help": "คุณสามารถเปลี่ยนแปลงกลุ่มที่ผู้ใช้รายนี้อยู่:\n* กล่องที่มีเครื่องหมายถูก หมายความว่า ผู้ใช้อยู่ในกลุ่มนั้น\n* กล่องที่ไม่มีเครื่องหมายถูก หมายความว่า ผู้ใช้ไม่ได้อยู่ในกลุ่มนั้น\n* เครื่องหมาย * ชี้ว่าคุณไม่สามารถนำกลุ่มนั้นออกได้เมื่อคุณเพิ่มกลุ่มนั้นไปแล้ว หรือกลับกัน",
        "right-override-export-depth": "ส่งออกหน้า รวมหน้าที่เชื่อมโยงกับหน้านี้สูงสุด 5 ลำดับชั้น",
        "right-sendemail": "ส่งอีเมลหาผู้ใช้อื่น",
        "right-passwordreset": "ดูอีเมลตั้งรหัสผ่านใหม่",
+       "grant-group-email": "ส่งอีเมล",
+       "grant-createaccount": "สร้างบัญชี",
+       "grant-createeditmovepage": "สร้าง แก้ไข และย้ายหน้า",
+       "grant-editmywatchlist": "แก้ไขรายการเฝ้าดูของคุณ",
+       "grant-editpage": "แก้ไขหน้านี้",
+       "grant-uploadeditmovefile": "อัปโหลด แทนที่ และย้ายไฟล์",
+       "grant-uploadfile": "อัปโหลดไฟล์ใหม่",
+       "grant-viewmywatchlist": "ดูรายการเฝ้าดูของคุณ",
        "newuserlogpage": "ปูมการสร้างผู้ใช้",
        "newuserlogpagetext": "นี่คือปูมการสร้างผู้ใช้",
        "rightslog": "ปูมสิทธิผู้ใช้",
        "uploaderror": "การอัปโหลดผิดพลาด",
        "upload-recreate-warning": "<strong>คำเตือน: ไฟล์ชื่อนั้นถูกลบหรือเปลี่ยนชื่อแล้ว</strong>\n\nปูมการลบและปูมการย้ายของหน้านี้จัดไว้ด้านล่างเพื่อความสะดวก:",
        "uploadtext": "กรุณาใช้แบบด้านล่างในการอัปโหลดไฟล์\nสำหรับการดูหรือการค้นหาไฟล์ที่เคยอัปโหลดก่อนหน้านี้ ให้ไปที่[[Special:FileList|รายการไฟล์ที่ถูกอัปโหลด]] การอัปโหลดและการอัปโหลดซ้ำดูได้ที่[[Special:Log/upload|ปูมการอัปโหลด]] และการลบไฟล์ดูได้ที่[[Special:Log/delete|ปูมการลบ]]\n\nถ้าต้องการแทรกไฟล์ลงในหน้าหนึ่ง ๆ ให้ใช้คำสั่งหนึ่งในรูปแบบต่อไปนี้\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code>''' เพื่อใช้รูปขนาดเต็ม\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|ข้อความอธิบาย]]</nowiki></code>''' เพื่อใช้รูปย่อขนาดกว้าง 200 พิกเซลในกล่องที่จัดชิดซ้าย โดยมี \"ข้อความอธิบาย\" เป็นคำบรรยายใต้ภาพ\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>''' สำหรับการเชื่อมโยงไฟล์โดยตรง โดยไม่ปรากฏไฟล์นั้นออกมา",
-       "upload-permitted": "ชนิดไฟล์ที่อนุญาต: $1",
-       "upload-preferred": "ชนิดไฟล์ที่ควรใช้: $1",
-       "upload-prohibited": "ชนิดไฟล์ที่ไม่อนุญาต: $1",
+       "upload-permitted": "{{PLURAL:$2|type|ชนิด}}ไฟล์ที่อนุญาต: $1",
+       "upload-preferred": "{{PLURAL:$2|type|ชนิด}}ไฟล์ที่ควรใช้: $1",
+       "upload-prohibited": "{{PLURAL:$2|type|ชนิด}}:ไฟล์ที่ไม่อนุญาต: $1",
        "uploadlogpage": "ปูมการอัปโหลด",
        "uploadlogpagetext": "ด้านล่างเป็นรายการการอัปโหลดไฟล์ล่าสุด\nดูภาพรวมที่ [[Special:NewFiles|แกลอรีไฟล์ใหม่]]",
        "filename": "ชื่อไฟล์",
        "upload-form-label-infoform-description": "คำอธิบาย",
        "upload-form-label-usage-title": "การใช้",
        "upload-form-label-usage-filename": "ชื่อไฟล์",
+       "upload-form-label-infoform-categories": "หมวดหมู่",
+       "upload-form-label-infoform-date": "วันที่",
        "backend-fail-backup": "ไม่สามารถสำรองไฟล์ \"$1\"",
        "backend-fail-notexists": "ไม่มีไฟล์ $1",
        "backend-fail-delete": "ไม่สามารถลบไฟล์ \"$1\"",
        "suppress": "ผู้ดูแลประวัติ",
        "querypage-disabled": "หน้าพิเศษนี้ถูกปิดใช้งานด้วยเหตุผลด้านสมรรถภาพ",
        "apihelp-no-such-module": "ไม่พบมอดูล \"$1\"",
+       "apisandbox-unfullscreen": "แสดงหน้า",
+       "apisandbox-reset": "ล้าง",
+       "apisandbox-retry": "ลองใหม่",
+       "apisandbox-examples": "ตัวอย่าง",
+       "apisandbox-results": "ผลลัพธ์",
+       "apisandbox-request-url-label": "ขอ URL:",
        "booksources": "แหล่งหนังสือ",
        "booksources-search-legend": "ค้นหาแหล่งหนังสือ",
        "booksources-search": "ค้นหา",
        "listgrouprights-namespaceprotection-header": "การจำกัดเนมสเปซ",
        "listgrouprights-namespaceprotection-namespace": "เนมสเปซ",
        "listgrouprights-namespaceprotection-restrictedto": "สิทธิอนุญาตให้ผู้ใช้แก้ไข",
+       "listgrants-rights": "สิทธิ",
        "trackingcategories": "หมวดหมู่ค้นหาและติดตาม",
        "trackingcategories-summary": "หน้านี้แสดงรายการหมวดหมู่ค้นหาและติดตามซึ่งซอฟต์แวร์มีเดียวิกิจัดการอัตโนมัติ สามารถเปลี่ยนชื่อเหล่านี้ได้โดยการเปลี่ยนสารระบบที่เกี่ยวข้องในเนมสเปซ {{ns:8}}",
        "trackingcategories-msg": "หมวดหมู่ค้นหาและติดตาม",
        "rollback-success": "ย้อนการแก้ไขโดย $1; \nเปลี่ยนกลับไปรุ่นล่าสุดโดย $2",
        "sessionfailure-title": "ช่วงเวลาสื่อสารล้มเหลว",
        "sessionfailure": "ดูเหมือนมีปัญหากับช่วงเวลาสื่อสารล็อกอินของคุณ\nการกระทำนี้ถูกยกเลิกเป็นการป้องกันการลักลอบช่วงเวลาสื่อสารไว้ก่อน \nกลับไปหน้าที่แล้ว โหลดหน้าใหม่ แล้วลองอีกครั้ง",
+       "changecontentmodel-title-label": "ชื่อหน้า:",
+       "changecontentmodel-reason-label": "เหตุผล:",
+       "changecontentmodel-submit": "ความเปลี่ยนแปลง",
+       "logentry-contentmodel-change-revertlink": "ย้อน",
+       "logentry-contentmodel-change-revert": "ย้อน",
        "protectlogpage": "ปูมการล็อก",
        "protectlogtext": "ด้านล่างเป็นรายการการเปลี่ยนแปลงการล็อกหน้า\nดู[[Special:ProtectedPages|รายการหน้าที่ถูกล็อก]]สำหรับการล็อกหน้าที่มีผลอยู่ในปัจจุบัน",
        "protectedarticle": "ล็อก \"[[$1]]\"",
        "whatlinkshere-prev": "{{PLURAL:$1|ก่อนหน้า|ก่อนหน้า $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|ถัดไป|ถัดไป $1}}",
        "whatlinkshere-links": "← ลิงก์",
-       "whatlinkshere-hideredirs": "$1การเปลี่ยนทาง",
-       "whatlinkshere-hidetrans": "$1 ถูกรวมอยู่",
-       "whatlinkshere-hidelinks": "$1 ลิงก์",
-       "whatlinkshere-hideimages": "$1ลิงก์ไฟล์",
+       "whatlinkshere-hideredirs": "ซ่อนการเปลี่ยนทาง",
+       "whatlinkshere-hidetrans": "Hide transclusions",
+       "whatlinkshere-hidelinks": "ซ่อนลิงก์",
+       "whatlinkshere-hideimages": "ซ่อนลิงก์ไฟล์",
        "whatlinkshere-filters": "ตัวกรอง",
        "whatlinkshere-submit": "ไป",
        "autoblockid": "บล็อกอัตโนมัติ #$1",
        "lockdbsuccesstext": "ล็อกฐานข้อมูลเรียบร้อย\n<br />อย่าลืม[[Special:UnlockDB|ปลดล็อก]]หลังการบำรุงรักษาเสร็จสิ้น",
        "unlockdbsuccesstext": "ปลดล็อกฐานข้อมูลเรียบร้อย",
        "lockfilenotwritable": "ไม่สามารถล็อกฐานข้อมูลได้ เนื่องจากการเขียนลงฐานข้อมูล การล็อกและการปลดล็อกจำเป็นต้องทำที่เว็บเซิร์ฟเวอร์",
+       "databaselocked": "ฐานข้อมูลถูกล็อก",
        "databasenotlocked": "ฐานข้อมูลไม่ได้ล็อก",
        "lockedbyandtime": "(โดย {{GENDER:$1|$1}} เมื่อวันที่ $2 เวลา $3)",
        "move-page": "ย้าย $1",
        "thumbnail_gd-library": "การตั้งค่าไลบรารี GD ไม่สมบูรณ์: ไม่พบฟังก์ชัน $1",
        "thumbnail_image-missing": "ไฟล์ที่เหมือนหายไป: $1",
        "import": "หน้านำเข้า",
-       "importinterwiki": "à¸\99ำà¹\80à¸\82à¹\89าà¸\82à¹\89ามวิà¸\81ิ",
+       "importinterwiki": "à¸\99ำà¹\80à¸\82à¹\89าหà¸\99à¹\89าà¸\88าà¸\81วิà¸\81ิอืà¹\88à¸\99",
        "import-interwiki-text": "เลือกวิกิและชื่อหัวข้อที่ต้องการนำเข้า วันที่และชื่อผู้เขียนทั้งหมดจะถูกเก็บไว้ โดยการนำเข้าทุกส่วนจะถูกเก็บไว้ใน [[Special:Log/import|ปูมการนำเข้า]]",
        "import-interwiki-sourcewiki": "วิกิต้นทาง:",
        "import-interwiki-sourcepage": "หน้าต้นทาง:",
        "import-logentry-interwiki-detail": "นำเข้า $1 {{PLURAL:$1|รุ่นการแก้ไข|รุ่นการแก้ไข}}จาก $2",
        "javascripttest": "การทดสอบจาวาสคริปต์",
        "javascripttest-qunit-intro": "ดู[$1 เอกสารกำกับการทดสอบ]บน mediawiki.org",
-       "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": "ฟีดชนิดอาร์เอสเอส (RSS) ของหน้านี้",
        "tooltip-feed-atom": "ฟีดอะตอม (Atom) ของหน้านี้",
-       "tooltip-t-contributions": "รายการเรื่องที่ผู้ใช้นี้เขียน",
+       "tooltip-t-contributions": "รายการเรื่องที่{{GENDER:$1|ผู้ใช้นี้}}เขียน",
        "tooltip-t-emailuser": "ส่งอีเมลถึงผู้ใช้นี้",
        "tooltip-t-info": "สารสนเทศเพิ่มเติมเกี่ยวกับหน้านี้",
        "tooltip-t-upload": "อัปโหลดไฟล์",
index 590dbe2..7219a74 100644 (file)
        "noname": "Dogry bir ulanyjy adyny görkezmediňiz.",
        "loginsuccesstitle": "Hasaba girdiňiz",
        "loginsuccess": "'''{{SITENAME}} saýtynda \"$1\" ulanyjy ady bilen hasaba girdiňiz.'''",
-       "nosuchuser": "\"$1\" diýen at bilen ulanyjy ýok.\nUlanyjy atlary baş hem-de setir harplara duýgurdyr.\nÝazylyşyny barlaň ýa-da [[Special:UserLogin/signup|täze hasap açyň]].",
+       "nosuchuser": "\"$1\" diýen at bilen ulanyjy ýok.\nUlanyjy atlary baş hem-de setir harplara duýgurdyr.\nÝazylyşyny barlaň ýa-da [[Special:CreateAccount|täze hasap açyň]].",
        "nosuchusershort": "\"$1\" dýen at bilen ulanyjy ýok. Ýazylyşyny barlaň.",
        "nouserspecified": "Ulanyjy adyny görkezmegiňiz hökmanydyr.",
        "login-userblocked": "Bu ulanyjy blokirlenipdir. Sessiýa açmaga rugsat berilmeýär.",
        "accmailtext": "[[User talk:$1|$1]] üçin ugralla döredilen parol $2 adresine iberildi.\n\nBu paroly sessiýa açanyňyzdan soňra ''[[Special:ChangePassword|paroly üýtget]]'' sahypasynda üýtgedip bilersiňiz.",
        "newarticle": "(Täze)",
        "newarticletext": "Häzirlikçe ýazylmadyk bir sahypa goýlan çykgyda tykladyňyz. Bu sahypany döretmek üçin aşakdaky tekst gutusyndan peýdalanyň. Maglumat üçin [$1 ýardam sahypasyna] serediň. Bu ýere ýalňyşlyk bilen gelen bolsaňyz, programmanyň '''Yza''' düwmesine tyklaň.",
-       "anontalkpagetext": "----''Bu sahypa heniz ulanyjy hasaby edinmedik ýa-da hasabyny ulanmaýan bir anonim ulanyjynyň pikir alyşma sahypasydyr.\nŞonuň üçinem biz ony görkezmek üçin sanlaýyn IP adresini ulanmaly bolýarys.\nŞunuň ýaly IP adresinden ençeme ulanyjy peýdalanýan bolmagy ahmal.\nEger-de sizem anonim ulanyjy bolsaňyz we size siziň bilen dahyly ýok habarlaşyklar gelýän bolsa, onda mundan beýläk başga anonim ulanyjylar bilen garjaşmazlygyňyz üçin [[Special:UserLogin/signup|özüňize hasap ediniň]] ýa-da [[Special:UserLogin|sessiýa açyň]].''",
+       "anontalkpagetext": "----''Bu sahypa heniz ulanyjy hasaby edinmedik ýa-da hasabyny ulanmaýan bir anonim ulanyjynyň pikir alyşma sahypasydyr.\nŞonuň üçinem biz ony görkezmek üçin sanlaýyn IP adresini ulanmaly bolýarys.\nŞunuň ýaly IP adresinden ençeme ulanyjy peýdalanýan bolmagy ahmal.\nEger-de sizem anonim ulanyjy bolsaňyz we size siziň bilen dahyly ýok habarlaşyklar gelýän bolsa, onda mundan beýläk başga anonim ulanyjylar bilen garjaşmazlygyňyz üçin [[Special:CreateAccount|özüňize hasap ediniň]] ýa-da [[Special:UserLogin|sessiýa açyň]].''",
        "noarticletext": "Bu sahypa şu wagt boş dur.\nBu ady başga sahypalarda [[Special:Search/{{PAGENAME}}|gözläp bilersiňiz]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} degişli gündeliklerde gözleg geçirip bilersiňiz],\nýa-da bu sahypany [{{fullurl:{{FULLPAGENAME}}|action=edit}} üýtgedip bilersiňiz]</span>.",
        "noarticletext-nopermission": "Häzirki wagtda bu sahypada tekst ýok.\nBu sahypa adyny [[Special:Search/{{PAGENAME}}|başga sahypalarda gözläp]]\nýa-da <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} degişli gündeliklerde gözleg geçirip bilersiňiz]</span>, ýöne bu sahypany döretmäge rugsadyňyz ýok.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" ulanyjy hasaby hasaba alynmandyr.\nBu sahypany döretmek/redaktirlemek isleýän bolsaňyz, onda esewan boluň.",
index ca3e47b..670b022 100644 (file)
        "noname": "Hindi mo tinukoy ang isang tanggap na pangalan ng tagagamit.",
        "loginsuccesstitle": "Matagumpay ang paglagda",
        "loginsuccess": "'''Nakalagda ka na sa {{SITENAME}} bilang si \"$1\".'''",
-       "nosuchuser": "Walang tagagamit na may pangalang \"$1\".\nMaselan sa pagtitipa ang mga pangalan ng tagagamit.\nSuriin ang iyong pagbabaybay, o [[Special:UserLogin/signup|lumikha ng bagong account]].",
+       "nosuchuser": "Walang tagagamit na may pangalang \"$1\".\nMaselan sa pagtitipa ang mga pangalan ng tagagamit.\nSuriin ang iyong pagbabaybay, o [[Special:CreateAccount|lumikha ng bagong account]].",
        "nosuchusershort": "Walang tagagamit na may pangalang \"$1\".\nPakitingnan ang iyong pagbabaybay.",
        "nouserspecified": "Kailangang tukuyin mo ang isang pangalang pantagagamit.",
        "login-userblocked": "Hinarang ang tagagamit na ito.  Hindi pinahihintulutan ang paglalagda.",
        "accmailtext": "Ipinadala na sa $2 ang isang password na nilikha ng pagkakataon para kay [[User talk:$1|$1]].  Maaari itong baguhin sa pahinang ''[[Special:ChangePassword|palitan ang password]]'' kapag nag-login.",
        "newarticle": "(Bago)",
        "newarticletext": "Sinundan mo ang isang kawing para sa isang pahinang hindi pa umiiral.\nPara likhain ang pahina, magsimulang magmakinilya sa loob ng kahong nasa ibaba (tingnan ang [$1 pahina ng tulong] para sa mas maraming kabatiran).\nKung napunta ka rito dahil sa pagkakamali, pakipindot ang pinduntang '''balik''' ('''''back''''') ng iyong pantingin-tingin (''browser'').",
-       "anontalkpagetext": "Ito ang pahinang usapan para sa isang hindi nakikilalang tagagamit na hindi pa lumilikha ng account, o kaya hindi ito ginagamit.\nKaya't kinailangan naming gamitin ang may bilang na IP address para makilala siya.\nMaaaring pagsaluhan ng ilang mga tagagamit ang ganiyang  IP address.\nKung isa kang hindi nagpapakilalang tagagamit at nakadaramang may mga walang saysay na komentong patungkol sa iyo, [[Special:UserLogin/signup|pakilikha ng isang account]] o [[Special:UserLogin|lumagda]] para maiwasan ang kalituhan o mapagkamalan ka bilang ibang hindi nakikilalang mga tagagamit sa hinaharap.",
+       "anontalkpagetext": "Ito ang pahinang usapan para sa isang hindi nakikilalang tagagamit na hindi pa lumilikha ng account, o kaya hindi ito ginagamit.\nKaya't kinailangan naming gamitin ang may bilang na IP address para makilala siya.\nMaaaring pagsaluhan ng ilang mga tagagamit ang ganiyang  IP address.\nKung isa kang hindi nagpapakilalang tagagamit at nakadaramang may mga walang saysay na komentong patungkol sa iyo, [[Special:CreateAccount|pakilikha ng isang account]] o [[Special:UserLogin|lumagda]] para maiwasan ang kalituhan o mapagkamalan ka bilang ibang hindi nakikilalang mga tagagamit sa hinaharap.",
        "noarticletext": "Kasalukuyang walang teksto sa loob ng pahinang ito.\nMaaari mong [[Special:Search/{{PAGENAME}}|hanapin ang pamagat ng pahinang ito]] sa loob iba pang mga pahina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa kaugnay na mga talaan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} baguhin ang pahinang ito]</span>.",
        "noarticletext-nopermission": "Kasalukuyang walang teksto sa pahinang ito.\nMaaari mong [[Special:Search/{{PAGENAME}}|hanapin ang pamagat ng pahinang ito]] sa ibang mga pahina,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} maghanap sa kaugnay na mga talaan]</span>.",
        "missing-revision": "Hindi umiiral ang rebisyong #$1 ng pahinang napangalanang \"{{FULLPAGENAME}}\".\n\nKaraniwang itong dulot ng pagsunod sa isang wala na sa panahong kawing ng kasaysayan na papunta sa isang pahinang nabura na.\nMatatagpuan ang mga detalye sa loob ng [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} talaan ng pagbura].",
index 36f0429..8ee0000 100644 (file)
        "noname": "Naʻe ʻikai te ke ʻoatu hao hingoa ʻetita ʻoku totonu.",
        "loginsuccesstitle": "Kuo ola ʻa e kau-ki-ai",
        "loginsuccess": "'''ʻOku ke kau-ki-ai he taimí ni ki he {{SITENAME}} hangē \"$1\".'''",
-       "nosuchuser": "ʻOku ʻikai ʻi ai ha ʻetita mo hono hingoa \"$1\". Sivi hoʻo sipela pe [[Special:UserLogin/signup|fakatupu haʻo kau-ki-ai foʻou]].",
+       "nosuchuser": "ʻOku ʻikai ʻi ai ha ʻetita mo hono hingoa \"$1\". Sivi hoʻo sipela pe [[Special:CreateAccount|fakatupu haʻo kau-ki-ai foʻou]].",
        "nosuchusershort": "ʻOku ʻikai ʻi ai ha ʻetita mo hono hingoa \"$1\". Sivi hoʻo sipela.",
        "nouserspecified": "ʻOku pau te ke ʻoatu ha hingoa ʻo e ʻetita.",
        "wrongpassword": "ʻOku ʻikai totonu ʻa e leatapu, kātaki ʻe toki feinga.",
index 141e30d..16f13b9 100644 (file)
@@ -84,7 +84,8 @@
                        "HakanIST",
                        "Imabadplayer",
                        "İnternion",
-                       "Hbseren"
+                       "Hbseren",
+                       "Kumkumuk"
                ]
        },
        "tog-underline": "Bağlantıların altını çiz:",
        "noname": "Geçerli bir kullanıcı adı girmediniz.",
        "loginsuccesstitle": "Oturum açıldı",
        "loginsuccess": "'''{{SITENAME}} üzerinde \"$1\" kullanıcı adıyla oturum açtınız.'''",
-       "nosuchuser": "\"$1\" adında bir kullanıcı bulunmamaktadır.\nKullanıcı adları büyük-küçük harf duyarlıdır.\nYazılışı kontrol edin veya [[Special:UserLogin/signup|yeni bir hesap açın]].",
+       "nosuchuser": "\"$1\" adında bir kullanıcı bulunmamaktadır.\nKullanıcı adları büyük-küçük harf duyarlıdır.\nYazılışı kontrol edin veya [[Special:CreateAccount|yeni bir hesap açın]].",
        "nosuchusershort": "\"$1\" adında bir kullanıcı bulunmamaktadır. Yazılışı kontrol edin.",
        "nouserspecified": "Bir kullanıcı adı belirtmek zorundasınız.",
        "login-userblocked": "Bu kullanıcı engellenmiş. Giriş yapmaya izin verilmiyor.",
        "accmailtext": "[[User talk:$1|$1]] için rastgele oluşturulan parola $2 adresine gönderildi.\n\nBu yeni hesap için parola, giriş yapıldıktan sonra ''[[Special:ChangePassword|parolayı değiştir]]'' bölümünde değiştirilebilir.",
        "newarticle": "(Yeni)",
        "newarticletext": "Henüz varolmayan bir sayfaya konulmuş bir bağlantıya tıkladınız.\nSayfayı oluşturmak için aşağıdaki metin kutusunu kullanın. ([$1 yardım sayfasına] bakınız).\nBuraya yanlışlıkla geldiyseniz tarayıcınızın  <strong>geri </strong> tuşuna tıklayın.",
-       "anontalkpagetext": "----''Bu sayfa henüz bir kullanıcı hesabı oluşturmamış veya hesabını kullanmayan bir anonim kullanıcının mesaj sayfasıdır. Bu nedenle bu kişiyi belirtmek için rakamsal IP adresini kullanmak zorundayız. Bu gibi IP adresleri birçok kullanıcı tarafından paylaşılabilir. Eğer siz de bir anonim kullanıcıysanız ve size sizin ilginiz olmayan iletiler geliyorsa, lütfen diğer anonim kullanıcılarla olabilecek olan karmaşayı önlemek için [[Special:UserLogin/signup|bir hesap edinin]] veya [[Special:UserLogin|oturum açın]].''",
+       "anontalkpagetext": "----''Bu sayfa henüz bir kullanıcı hesabı oluşturmamış veya hesabını kullanmayan bir anonim kullanıcının mesaj sayfasıdır. Bu nedenle bu kişiyi belirtmek için rakamsal IP adresini kullanmak zorundayız. Bu gibi IP adresleri birçok kullanıcı tarafından paylaşılabilir. Eğer siz de bir anonim kullanıcıysanız ve size sizin ilginiz olmayan iletiler geliyorsa, lütfen diğer anonim kullanıcılarla olabilecek olan karmaşayı önlemek için [[Special:CreateAccount|bir hesap edinin]] veya [[Special:UserLogin|oturum açın]].''",
        "noarticletext": "Bu sayfa şu anda boştur.\nBu başlığı [[Special:Search/{{PAGENAME}}|diğer sayfalarda arayabilir]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ilgili kayıtları arayabilir],\nya da bu sayfayı [{{fullurl:{{FULLPAGENAME}}|action=edit}} oluşturabilirsiniz]</span>.",
        "noarticletext-nopermission": "Bu sayfa şu anda boştur. \nBu başlığı [[Special:Search/{{PAGENAME}}|diğer sayfalarda arayabilir]] ya da <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} ilgili kayıtları tarayabilirsiniz]</span>, fakat sayfayı oluşturma yetkiniz bulunmamaktadır.",
        "missing-revision": "\"{{FULLPAGENAME}}\" sayfasının #$1 sürümü yok.\n\nBu duruma genellikle silinmiş bir sayfaya eski tarihli bir bağlantının takip edilmesi neden olur.\n\nDaha fazla detaylı bilgi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} sayfasında bulunabilir].",
        "whatlinkshere-prev": "{{PLURAL:$1|önceki|önceki $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|sonraki|sonraki $1}}",
        "whatlinkshere-links": "← bağlantılar",
-       "whatlinkshere-hideredirs": "Yönlendirmeleri $1",
-       "whatlinkshere-hidetrans": "Dönüştürmeleri $1",
-       "whatlinkshere-hidelinks": "Bağlantıları $1",
+       "whatlinkshere-hideredirs": "Yönlendirmeleri gizle",
+       "whatlinkshere-hidetrans": "Dönüştürmeleri gizle",
+       "whatlinkshere-hidelinks": "Bağlantıları gizle",
        "whatlinkshere-hideimages": "Dosya bağlantılarını $1",
        "whatlinkshere-filters": "Süzgeçler",
        "autoblockid": "Otomatik engelleme #$1",
index 9080afa..cee7a0d 100644 (file)
@@ -43,6 +43,7 @@
        "tog-watchdefault": "Мин үзгәрткән битләр һәм файллар күзәтү исемлегемә өстәлсен",
        "tog-watchmoves": "Мин күчергән битләр һәм файллар күзәтү исемлегемә өстәлсен",
        "tog-watchdeletion": "Мин бетергән битләр һәм файлларны күзәтү исемлегемгә өстәлсен",
+       "tog-watchuploads": "Минем тарафтан йөкләнелгән файлларны күзәтү исемлегемә кертергә",
        "tog-minordefault": "Барлык үзгәртүләрне килешү буенча кече дип билгеләнсен",
        "tog-previewontop": "Үзгәртү тәрәзәсеннән өстәрәк битне алдан карау өлкәсен күрсәтелсен",
        "tog-previewonfirst": "Үзгәртү битенә күчкәндә башта алдан карау бите күрсәтелсен",
@@ -59,6 +60,7 @@
        "tog-watchlisthidebots": "Бот үзгәртүләре күзәтү исемлегеннән яшерелсен",
        "tog-watchlisthideminor": "Кече үзгәртүләр күзәтү исемлегеннән яшерелсен",
        "tog-watchlisthideliu": "Авторизацияне узган кулланучыларның үзгәртүләре күзәтү исемлегеннән яшерелсен",
+       "tog-watchlistreloadautomatically": "Фильтр алмашкан очракта күзәтү исемлеген автоматик рәвештә яңартырга (JavaScript кирәк)",
        "tog-watchlisthideanons": "Аноним кулланучыларның үзгәртүләре күзәтү исемлегеннән яшерелсен",
        "tog-watchlisthidepatrolled": "Тикшерелгән үзгәртүләр күзәтү исемлегеннән яшерелсен",
        "tog-watchlisthidecategorization": "Битләрне төркемләшүне ябу",
        "noname": "Сез кулланучы исемегезне күрсәтергә тиешсез.",
        "loginsuccesstitle": "Керү уңышлы үтте",
        "loginsuccess": "'''Сез {{SITENAME}} проектына $1 исеме белән кердегез.'''",
-       "nosuchuser": "$1 исемле кулланучы юк.\nКулланучы исеменең дөреслеге регистрга бәйле.\nЯзылышыгызны тикшерегез яки [[Special:UserLogin/signup|яңа хисап язмасы төзегез]].",
+       "nosuchuser": "$1 исемле кулланучы юк.\nКулланучы исеменең дөреслеге регистрга бәйле.\nЯзылышыгызны тикшерегез яки [[Special:CreateAccount|яңа хисап язмасы төзегез]].",
        "nosuchusershort": "$1 исемле кулланучы юк. Язылышыгызны тикшерегез.",
        "nouserspecified": "Сез теркәү исмегезне күрсәтергә тиешсез.",
        "login-userblocked": "Бу кулланучы тыелды. Керү тыелган.",
        "accmailtext": "[[User talk:$1|$1]] кулланучысы өчен төзелгән серсүз $2 адресына җибәрелде.\n\nАвторизация узгач, үз хисап язмагызда сез ''[[Special:ChangePassword|серсүзегезне үзгәртә аласыз]]''.",
        "newarticle": "(Яңа)",
        "newarticletext": "Сез әлегә язылмаган биткә кердегез.\nЯңа бит ясау өчен астагы тәрәзәдә мәкалә текстын җыегыз ([$1 ярдәм битен] карый аласыз).\nӘгәр сез бу биткә ялгышлык белән эләккән булсагыз, браузерыгызның '''артка''' төймәсенә басыгыз.",
-       "anontalkpagetext": "----''Бу бәхәс бите системада теркәлмәгән яисә үз исеме белән кермәгән кулланучыныкы.\nАны тану өчен IP адресы файдаланыла.\nӘгәр сез аноним кулланучы һәм сезгә юлланмаган хәбәрләр алдым дип саныйсыз икән (бер IP адресы күп кулланучы өчен булырга мөмкин), башка мондый аңлашылмаучанлыклар килеп чыкмасын өчен [[Special:UserLogin|системага керегез]] яисә [[Special:UserLogin/signup|теркәлегез]].''",
+       "anontalkpagetext": "----''Бу бәхәс бите системада теркәлмәгән яисә үз исеме белән кермәгән кулланучыныкы.\nАны тану өчен IP адресы файдаланыла.\nӘгәр сез аноним кулланучы һәм сезгә юлланмаган хәбәрләр алдым дип саныйсыз икән (бер IP адресы күп кулланучы өчен булырга мөмкин), башка мондый аңлашылмаучанлыклар килеп чыкмасын өчен [[Special:UserLogin|системага керегез]] яисә [[Special:CreateAccount|теркәлегез]].''",
        "noarticletext": "Хәзерге вакытта бу биттә текст юк.\nСез [[Special:Search/{{PAGENAME}}|бу исем кергән башка мәкаләләрне]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} көндәлекләрдәге язмаларны] таба\nяки '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} шушындый исемле яңа бит төзи]'''</span> аласыз.",
        "noarticletext-nopermission": "Хәзерге вакытта бу биттә текст юк.\nСез [[Special:Search/{{PAGENAME}}|бу исем кергән башка мәкаләләрне]] башка битләрдә,\nяисә <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} көндәлекләрдәге язмаларны] таба аласыз.</span> Сезнең бу битне ясарга хакыгыз юк.",
        "userpage-userdoesnotexist": "«<nowiki>$1</nowiki>» исемле хисап язмасы юк. Сез чынлап та бу битне ясарга яисә үзгәртергә телисезме?",
        "userpage-userdoesnotexist-view": "\"$1\" исемле хисап язмасы юк.",
        "blocked-notice-logextract": "Бу кулланучы хәзергә тыелды.\nТүбәндә тыю көндәлегенең соңгы язу бирелгән:",
-       "clearyourcache": "<strong>Искәрмә:</strong> Сез саклаган үзгәртүләр кулланышка керсен өчен браузерыгызның кешын чистартырга туры киләчәк. \n* <strong>Firefox/Safari:</strong> Shift төймшсенә баскан килеш җиһазлар тасмасында <em>Яңарту (Обновить)</em> язуына басыгыз, яисә <em>Ctrl-F5</em> яки  ''Ctrl-R</em> (Mac өчен <em>⌘-R</em>) төймәләренә басыгыз\n* <strong>Google Chrome:</strong>  <em>Ctrl-Shift-R</em> (Mac өчен <em>⌘-Shift-R</em> ) төймәләренә басыгыз\n* <strong>Internet Explorer:</strong> <em>Ctrl</em>  төймәсенә баскан килеш  <em>Яңарту (Обновить)</em> язуына, яисә <em>Ctrl-F5</em> басыгыз\n* <strong>Opera:</strong> Менюдан кеш чистартуны сайлагыз: <em>Кораллар (Инструменты) → Көйләнмәләр (Настройки)</em>",
+       "clearyourcache": "<strong>Искәрмә:</strong> Сез саклаган үзгәртүләр кулланышка керсен өчен браузерыгызның кешын чистартырга туры киләчәк. \n* <strong>Firefox/Safari:</strong> Shift төймшсенә баскан килеш җиһазлар тасмасында <em>Яңарту (Обновить)</em> язуына басыгыз, яисә <em>Ctrl-F5</em> яки  ''Ctrl-R</em> (Mac өчен <em>⌘-R</em>) төймәләренә басыгыз\n* <strong>Google Chrome:</strong>  <em>Ctrl-Shift-R</em> (Mac өчен <em>⌘-Shift-R</em> ) төймәләренә басыгыз\n* <strong>Internet Explorer:</strong> <em>Ctrl</em>  төймәсенә баскан килеш  <em>Яңарту (Обновить)</em> язуына, яисә <em>Ctrl-F5</em> басыгыз\n* <strong>Opera:</strong> <em>Menu → Көйләнмәләр</em> (<em>Opera → Көйләнмәләр</em> на Mac) бүлегенә күчегез,  аннан <em>Куркынычсызлык → Керүләр тарыхын чистарту → Рәсемнәр кэшлау</em>",
        "usercssyoucanpreview": "'''Ярдәм:''' \"{{int:showpreview}} төймәсенә басып, яңа CSS-файлны тикшереп була.",
        "userjsyoucanpreview": "'''Ярдәм:''' \"{{int:showpreview}}\" төймәсенә басып, яңа JS-файлны тикшереп була.",
        "usercsspreview": "'''Бу бары тик CSS-файлны алдан карау гына, ул әле сакланмаган!'''",
        "recentchangesdays-max": "(иң күбе $1 {{PLURAL:$1|көн}})",
        "recentchangescount": "Төп буларак кулланучы үзгәртүләр саны:",
        "prefs-help-recentchangescount": "Үз өченә үзгәртүләрне, битләрнең тарихын һәм язлу көндәлеген дә кертә.",
+       "prefs-help-watchlist-token2": "Бу сезнең кузәтү исемлеге өчен ясалган веб-агымының серле ачкычы.\nАны белгән һәркем сезнең күзәтү исемлегегезне карый ала, шуңа да башкаларга аны күрсәтмәгез. [[Special:ResetTokens|Ачкычны ташларга теләсәгез, әлеге юрамага басыгыз]].",
        "savedprefs": "Көйләнмәләрегез сакланды.",
        "timezonelegend": "Сәгать поясы:",
        "localtime": "Җирле вакыт",
        "whatlinkshere-prev": "{{PLURAL:$1|1=алдагы}} $1",
        "whatlinkshere-next": "{{PLURAL:$1|1=киләсе}} $1",
        "whatlinkshere-links": "← сылтамалар",
-       "whatlinkshere-hideredirs": "Юнәлтүләрне $1",
-       "whatlinkshere-hidetrans": "Кертүләрне $1",
-       "whatlinkshere-hidelinks": "Сылтамаларны $1",
-       "whatlinkshere-hideimages": "$1 файл сылтамалары",
+       "whatlinkshere-hideredirs": "Юнәлтүләрне яшер",
+       "whatlinkshere-hidetrans": "Кертүләрне яшер",
+       "whatlinkshere-hidelinks": "Сылтамаларны яшер",
+       "whatlinkshere-hideimages": "Файл сылтамаларын яшер",
        "whatlinkshere-filters": "Фильтрлар",
        "whatlinkshere-submit": "Башкару",
        "autoblockid": "Автотыю #$1",
index 4e7ea76..8007f58 100644 (file)
        "noname": "Sez qullanuçı isemegezne kürsätergä tieşsez.",
        "loginsuccesstitle": "Kerü uñışlı ütte",
        "loginsuccess": "'''Sez {{SITENAME}} proyektına $1 iseme belän kerdegez.'''",
-       "nosuchuser": "$1 isemle qullanuçı yuq.\nQullanuçı isemeneñ döreslege registrğa bäyle.\nYazılışığıznı tikşeregez yäki [[Special:UserLogin/signup|yaña xisap yazması tözegez]].",
+       "nosuchuser": "$1 isemle qullanuçı yuq.\nQullanuçı isemeneñ döreslege registrğa bäyle.\nYazılışığıznı tikşeregez yäki [[Special:CreateAccount|yaña xisap yazması tözegez]].",
        "nosuchusershort": "$1 isemle qullanuçı yuq. Yazılışığıznı tikşeregez.",
        "nouserspecified": "Sez terkäw ismegezne kürsätergä tieşsez.",
        "login-userblocked": "Bu qullanuçı tıyıldı. Kerü tıyılğan.",
        "accmailtext": "[[User talk:$1|$1]] qullanuçısı öçen tözelgän sersüz $2 adresına cibärelde.\n\nSaytqa kergäç sez ''[[Special:ChangePassword|sersüzegezne üzgärtä alasız]]''.",
        "newarticle": "(Yaña)",
        "newarticletext": "Sez älegä yazılmağan bitkä kerdegez.\nYaña bit yasaw öçen astağı täräzädä mäqälä tekstın cıyığız ([$1 yärdäm biten] qarıy alasız).\nÄgär sez bu bitkä yalğışlıq belän eläkkän bulsağız, brauzerığıznıñ '''artqa''' töymäsenä basığız.",
-       "anontalkpagetext": "----''Bu bäxäs bite sistemada terkälmägän yäisä üz iseme belän kermägän qullanuçınıqı.\nAnı tanu öçen IP adresı faydalanıla.\nÄgär sez anonim qullanuçı häm sezgä yullanmağan xäbärlär aldım dip sanıysız ikän (ber IP adresı küp qullanuçı öçen bulırğa mömkin), başqa mondıy añlaşılmawçanlıqlar kilep çıqmasın öçen [[Special:UserLogin|sistemağa keregez]] yäisä [[Special:UserLogin/signup|terkälegez]].''",
+       "anontalkpagetext": "----''Bu bäxäs bite sistemada terkälmägän yäisä üz iseme belän kermägän qullanuçınıqı.\nAnı tanu öçen IP adresı faydalanıla.\nÄgär sez anonim qullanuçı häm sezgä yullanmağan xäbärlär aldım dip sanıysız ikän (ber IP adresı küp qullanuçı öçen bulırğa mömkin), başqa mondıy añlaşılmawçanlıqlar kilep çıqmasın öçen [[Special:UserLogin|sistemağa keregez]] yäisä [[Special:CreateAccount|terkälegez]].''",
        "noarticletext": "Xäzerge waqıtta bu bittä tekst yuq.\nSez [[Special:Search/{{PAGENAME}}|bu isem kergän başqa mäqälälärne]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} köndäleklärdäge yazmalarnı] taba\nyäki '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} şuşındıy isemle yaña bit tözi]'''</span> alasız.",
        "noarticletext-nopermission": "Xäzerge waqıtta bu bittä tekst yuq.\n[[Special:Search/{{PAGENAME}}|Bu başlam qullanılğan başqa bitlärne]] yäki\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} köndäleklärdäge yazmalarnı] ezläp ala alasız</span>, läkin bu bitne tözü röxsätegez yuq.",
        "userpage-userdoesnotexist": "«<nowiki>$1</nowiki>» isemle xisap yazması yuq. Sez çınlap ta bu bitne yasarğa yäisä üzgärtergä telisezme?",
index 4ea5a42..3eeda7f 100644 (file)
        "noname": "سىز تېخى ئىناۋەتلىك ئىشلەتكۈچى نامىنى بەلگىلىمىدىڭىز.",
        "loginsuccesstitle": "تىزىمغا كىرىش مۇۋەپپەقىيەتلىك",
        "loginsuccess": "'''سىز {{SITENAME}} غا \"$1\" سالاھىيىتىدە كىردىڭىز.'''",
-       "nosuchuser": "\"$1\" ناملىق ئىشلەتكۈچىنى تاپالمىدى.\nئىشلەتكۈچى نامىنى تەكشۈرۈڭ.\nياكى [[Special:UserLogin/signup|يېڭى ھېسابات قۇرۇڭ]].",
+       "nosuchuser": "\"$1\" ناملىق ئىشلەتكۈچىنى تاپالمىدى.\nئىشلەتكۈچى نامىنى تەكشۈرۈڭ.\nياكى [[Special:CreateAccount|يېڭى ھېسابات قۇرۇڭ]].",
        "nosuchusershort": "\"$1\" ناملىق ئىشلەتكۈچى يوق.\nكىرگۈزگىنىڭىزنى تەكشۈرۈڭ.",
        "nouserspecified": "ئىشلەتكۈچى نامىدىن بىرنى بەلگىلەڭ.",
        "login-userblocked": "بۇ ئىشلەتكۈچى چەكلەنگەن. تىزىمغا كىرىشكە يول قويۇلمايدۇ.",
        "accmailtext": "[[User talk:$1|$1]] ئىختىيارىي قۇرۇلغان ئىم  $2 غا يوللاندى.\n\nيېڭى ھېساباتقا قۇرغان ئىمنى تىزىمغا كىرىپ''[[Special:ChangePassword|ئىم ئۆزگەرت]]'' بېتىدىن ئۆزگەرتەلەيسىز.",
        "newarticle": "(يېڭى)",
        "newarticletext": "سىز تېخى قۇرۇلمىغان بەتكە كىردىڭىز.\n بۇ بەتنى قۇرسىڭىز، تۆۋەندىكى تەھرىرلەش رامكىسىغا مەزمۇن كىرگۈزۈڭ(تەپسىلاتىنى  [$1 ياردەم بېتى]دىن كۆرۈڭ)",
-       "anontalkpagetext": "----''بۇ تېخى ھېسابات قۇرمىغان ئاتسىز ئىشلەتكۈچىنىڭ مۇنازىرە بېتى ياكى ئۇنى ئىشلەتمەڭ..\nبىز ئۇنىڭ بىلەن پەقەت IP ئادرېسى بىلەنلا ئالاقە قىلالايمىز..\nبۇ خىل IP ئادرېسنى بىر قانچە ئىشلەتكۈچى ئىشلىتىشى ئورتاق ئىشلىتىشى مۇمكىن.\nئەگەر سىز ئاتسىز ئىشلەتكۈچى بولسىڭىز ھەمدە بۇ بەتتىكى مۇنازىرە سىز بىلەن مۇناسىۋەتلىك بولسا،  [[Special:UserLogin/signup|ھېسابات قۇر]] ياكى [[Special:UserLogin|تىزىمغا كىر]]  ئارقىلىق كەلگۈسىدىكى باشقا ئاتسىز ئىشلەتكۈچى بىلەن ئارىلىشىپ كېتىشنىڭ ئالدىنى ئېلىڭ.''",
+       "anontalkpagetext": "----''بۇ تېخى ھېسابات قۇرمىغان ئاتسىز ئىشلەتكۈچىنىڭ مۇنازىرە بېتى ياكى ئۇنى ئىشلەتمەڭ..\nبىز ئۇنىڭ بىلەن پەقەت IP ئادرېسى بىلەنلا ئالاقە قىلالايمىز..\nبۇ خىل IP ئادرېسنى بىر قانچە ئىشلەتكۈچى ئىشلىتىشى ئورتاق ئىشلىتىشى مۇمكىن.\nئەگەر سىز ئاتسىز ئىشلەتكۈچى بولسىڭىز ھەمدە بۇ بەتتىكى مۇنازىرە سىز بىلەن مۇناسىۋەتلىك بولسا،  [[Special:CreateAccount|ھېسابات قۇر]] ياكى [[Special:UserLogin|تىزىمغا كىر]]  ئارقىلىق كەلگۈسىدىكى باشقا ئاتسىز ئىشلەتكۈچى بىلەن ئارىلىشىپ كېتىشنىڭ ئالدىنى ئېلىڭ.''",
        "noarticletext": "بۇ بەتتە ھازىرچە مەزمۇن يوق.\n سىز باشقا بەتتە [[Special:Search/{{PAGENAME}}|بۇ بەتنىڭ ماۋزۇسىنى ئىزدىيەلەيسىز]] ياكى\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} مۇناسىۋەتلىك خاتىرىسىنى ئىزدىيەلەيسىز،],\n[{{fullurl:{{FULLPAGENAME}}|action=edit}} بۇ بەتنى تەھرىرلىيەلەيسىز]</span>",
        "noarticletext-nopermission": "بۇ بەتتە ھازىرچە مەزمۇن يوق.\n سىز باشقا بەتتە [[Special:Search/{{PAGENAME}}|بۇ بەتنىڭ ماۋزۇسىنى ئىزدىيەلەيسىز]] ياكى <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}] مۇناسىۋەتلىك خاتىرىسىنى ئىزدىيەلەيسىز،</span>لىكىن سزنىڭ بەت قۇرۇش ھوقوقىڭز يوق.",
        "missing-revision": "\"{{FULLPAGENAME}}\" ئاتلىق بەتنىڭ تۈزىتىلگەن نەشرى #$1 مەۋجۇت ئەمەس.\n\nئادەتتە بۇ ئۆچۈرۈلگەن بىر بەتنىڭ ئۇلانمىسىغا كىرگەنلىك سەۋەبىدىن بولىدۇ.\nتەپسىلىي ئۇچۇرنى [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ئۆچۈرۈش خاتىرىسى] دىن تاپقىلى بولىدۇ.",
index 24f55bf..16ddb75 100644 (file)
        "noname": "Ви зазначили неправильне ім'я користувача.",
        "loginsuccesstitle": "Вхід виконано",
        "loginsuccess": "'''Тепер ви працюєте в {{grammar:locative|{{SITENAME}}}} під іменем $1.'''",
-       "nosuchuser": "Користувача з іменем «$1» не існує.\nВ іменах користувачів розрізняються великі й малі символи.\nПеревірте правильність написання або скористайтеся формою нижче, щоб [[Special:UserLogin/signup|створити новий обліковий запис]].",
+       "nosuchuser": "Користувача з іменем «$1» не існує.\nВ іменах користувачів розрізняються великі й малі символи.\nПеревірте правильність написання або скористайтеся формою нижче, щоб [[Special:CreateAccount|створити новий обліковий запис]].",
        "nosuchusershort": "Користувача з іменем «$1» не існує.\nПеревірте правильність написання імені.",
        "nouserspecified": "Ви повинні зазначити ім'я користувача.",
        "login-userblocked": "Цей користувач заблокований. Вхід в систему не дозволений.",
        "accmailtext": "Пароль для користувача [[User talk:$1|$1]], згенерований випадковим чином, надісланий на адресу $2.\nПісля реєстрації в системі ви зможете ''[[Special:ChangePassword|змінити пароль]]''.",
        "newarticle": "(Нова)",
        "newarticletext": "Ви перейшли на сторінку, яка поки що не існує.\n\nЩоб створити нову сторінку, наберіть текст у вікні нижче (див. [$1 довідкову статтю], щоб отримати більше інформації).\nЯкщо Ви опинились тут помилково, просто натисніть кнопку браузера '''назад'''.",
-       "anontalkpagetext": "----\n<em>Це сторінка обговорення анонімного користувача, який ще не зареєструвався або не скористався зареєстрованим обліковим записом.</em>\nТому ми вимушені використовувати IP-адресу для його ідентифікації.\nОдна IP-адреса може використовуватись кількома користувачами.\nЯкщо Ви — анонімний користувач і вважаєте, що отримали коментарі, адресовані не Вам, будь ласка [[Special:UserLogin/signup|зареєструйтесь]] або [[Special:UserLogin|увійдіть до системи]], щоб у майбутньому уникнути можливої плутанини з іншими анонімними користувачами.",
+       "anontalkpagetext": "----\n<em>Це сторінка обговорення анонімного користувача, який ще не зареєструвався або не скористався зареєстрованим обліковим записом.</em>\nТому ми вимушені використовувати IP-адресу для його ідентифікації.\nОдна IP-адреса може використовуватись кількома користувачами.\nЯкщо Ви — анонімний користувач і вважаєте, що отримали коментарі, адресовані не Вам, будь ласка [[Special:CreateAccount|зареєструйтесь]] або [[Special:UserLogin|увійдіть до системи]], щоб у майбутньому уникнути можливої плутанини з іншими анонімними користувачами.",
        "noarticletext": "Зараз на цій сторінці нема тексту.\nВи можете [[Special:Search/{{PAGENAME}}|пошукати цю назву]] на інших сторінках,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пошукати в журналах]\nабо [{{fullurl:{{FULLPAGENAME}}|action=edit}} створити сторінку з такою назвою]</span>.",
        "noarticletext-nopermission": "Зараз на цій сторінці немає тексту.\nВи можете [[Special:Search/{{PAGENAME}}|пошукати цю назву]] на інших сторінках,\nабо <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пошукати пов'язані записи в журналах]</span>, але ви не маєте дозволу на створення такої сторінки.",
        "missing-revision": "Версія #$1 сторінки «{{FULLPAGENAME}}» не існує.\n\nІмовірно, Ви перейшли за застарілим посиланням на вилучену сторінку.\nПодробиці можна дізнатися з [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журналу вилучень].",
        "upload-form-label-own-work": "Це моя власна робота",
        "upload-form-label-infoform-categories": "Категорії",
        "upload-form-label-infoform-date": "Дата",
-       "upload-form-label-own-work-message-local": "Я підтверджую, що вивантажую цей файл згідно з умовами користування та політики ліцензування {{GRAMMAR:locative|{{SITENAME}}}}.",
-       "upload-form-label-not-own-work-message-local": "Якщо Ви не можете завантажити цей файл згідно з правилами {{GRAMMAR:genitive|{{SITENAME}}}}, будь ласка, закрийте цей вікно та оберіть інший спосіб.",
-       "upload-form-label-not-own-work-local-local": "Ви також можете спробувати [[Special:Upload|сторінку завантаження за замовчуванням]].",
-       "upload-form-label-own-work-message-default": "Я розумію, що я завантажую цей файл до спільного сховища. Я підтверджую, що я роблю це у відповідності до Умов надання послуг та правил ліцензування.",
-       "upload-form-label-not-own-work-message-default": "Якщо ви не можете завантажити цей файл згідно з правилами спільного сховища, будь ласка, закрийте цей вікно та оберіть інший спосіб.",
-       "upload-form-label-not-own-work-local-default": "Ви також можете спробувати використати [[Special:Upload|сторінку завантаження на {{GRAMMAR:locative|{{SITENAME}}}}]], якщо цей файл може бути завантажений згідно з їх правилами.",
-       "upload-form-label-own-work-message-shared": "Я підтверджую, що я є власником авторських прав на цей файл та погоджуюся беззастережно поширити його у Вікісховищі на умовах ліцензії [https://creativecommons.org/licenses/by-sa/4.0/deed.uk Creative CommonsAttribution-ShareAlike 4.0], і я згоден з [https://wikimediafoundation.org/wiki/Terms_of_Use умовами використання].",
-       "upload-form-label-not-own-work-message-shared": "Якщо ви не є власником авторських прав на цей файл або ви хочете розповсюджувати його на умовах іншої ліцензії, рекомендуємо використати [https://commons.wikimedia.org/wiki/Special:UploadWizard Майстер завантажень на Вікісховищі].",
-       "upload-form-label-not-own-work-local-shared": "Ви також можете спробувати використати [[Special:Upload|сторінку завантаження на {{GRAMMAR:locative|{{SITENAME}}}}]], якщо цей файл може бути завантажений на цей сайт згідно з його правилами.",
+       "upload-form-label-own-work-message-generic-local": "Я підтверджую, що вивантажую цей файл згідно з умовами користування та політики ліцензування {{GRAMMAR:locative|{{SITENAME}}}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Якщо Ви не можете завантажити цей файл згідно з правилами {{GRAMMAR:genitive|{{SITENAME}}}}, будь ласка, закрийте цей вікно та оберіть інший спосіб.",
+       "upload-form-label-not-own-work-local-generic-local": "Ви також можете спробувати [[Special:Upload|сторінку завантаження за замовчуванням]].",
+       "upload-form-label-own-work-message-generic-foreign": "Я розумію, що я завантажую цей файл до спільного сховища. Я підтверджую, що я роблю це у відповідності до Умов надання послуг та правил ліцензування.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Якщо ви не можете завантажити цей файл згідно з правилами спільного сховища, будь ласка, закрийте цей вікно та оберіть інший спосіб.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Ви також можете спробувати використати [[Special:Upload|сторінку завантаження на {{GRAMMAR:locative|{{SITENAME}}}}]], якщо цей файл може бути завантажений згідно з їх правилами.",
        "backend-fail-stream": "Не вдалося транслювати файл $1.",
        "backend-fail-backup": "Не вдалося створити резервну копію файлу $1.",
        "backend-fail-notexists": "Файл $1 не існує.",
        "movesubpagetext": "Ця сторінка має $1 {{PLURAL:$1|підсторінку|підсторінки|підсторінок}}.",
        "movenosubpage": "Ця сторінка не має підсторінок.",
        "movereason": "Причина:",
-       "revertmove": "відкинути",
+       "revertmove": "скасувати перейменування",
        "delete_and_move_text": "Сторінка з назвою [[:$1|«$1»]] вже існує.\nБажаєте вилучити її для можливості перейменування?",
        "delete_and_move_confirm": "Так, вилучити для перейменування",
        "delete_and_move_reason": "Вилучена для можливості перейменування сторінки «[[$1]]»",
index f29edf3..73e3d6d 100644 (file)
        "noname": "آپ نے صحیح اسم صارف نہیں چنا.",
        "loginsuccesstitle": "داخلہ کامیاب",
        "loginsuccess": "'''اب آپ {{SITENAME}} میں بنام \"$1\" داخل ہوچکے ہیں۔'''",
-       "nosuchuser": "\"$1\" کے نام سے کوئی صارف موجود نہیں ہے.\nبرائے مہربانی! ہجوں کے درست اندراج کی تصدیق کرلیجئے.\nاگر آپ چاہیں تو [[Special:UserLogin/signup|نیا کھاتہ بھی بناسکتے ہیں]].",
+       "nosuchuser": "\"$1\" کے نام سے کوئی صارف موجود نہیں ہے.\nبرائے مہربانی! ہجوں کے درست اندراج کی تصدیق کرلیجئے.\nاگر آپ چاہیں تو [[Special:CreateAccount|نیا کھاتہ بھی بناسکتے ہیں]].",
        "nosuchusershort": "\"$1\" کے نام سے کوئی صارف موجود نہیں.\nاپنا ہجہ جانچئے.",
        "nouserspecified": "آپ کو ایک اسمِ صارف مخصوص کرنا ہے.",
        "login-userblocked": "اِس صارف پر پابندی ہے. داخلِ نوشتہ ہونے کی اجازت نہیں.",
        "accmailtext": "[[User talk:$1|$1]] کے لیے خودکار طریقے سے تخلیق کیا گیا پاسورڈ $2 کو بھیج دیا گیا ہے.\n\nلاگ ان ہونے کے بعد <em>[[Special:ChangePassword|اسے تبدیل]]</em> کیا جا سکتا ہے۔",
        "newarticle": "(نیا)",
        "newarticletext": "آپ نے ایک ایسے صفحے کے ربط کی پیروی کی ہے جو کہ ابھی موجود نہیں ہے.\nیہ صفحہ تخلیق کرنے کیلئے درج ذیل خانہ میں متن درج کیجئے (مزید معلومات کیلئے [$1 صفحۂ معاونت] ملاحظہ فرمائیے).\nاگر آپ یہاں غلطی سے پہنچے ہیں تو پچھلے صفحے پر واپس جانے کیلئے اپنے متصفح پر '''back''' کا بٹن ٹک کیجئے.",
-       "anontalkpagetext": "----''یہ صفحہ ایک ایسے صارف کا ہے جنہوں نے یا تو اب تک اپنا کھاتا نہیں بنایا یا پھر وہ اسے استعمال نہیں کر رہے/ رہی ہیں۔ لہٰذا ہمیں انکی شناخت کے لئے ایک عددی آئی پی پتہ استعمال کرنا پڑرہا ہے۔ اس قسم کا آئی پی پتہ ایک سے زائد صارفین کے لئے مشترک بھی ہوسکتا ہے۔ اگر آپکی موجودہ حیثیت ایک گمنام صارف کی ہے اور آپ محسوس کریں کہ اس صفحہ پر آپکی جانب منسوب یہ بیان غیرضروری ہے تو براہ کرم [[Special:UserLogin/signup|کھاتہ بنائیں]] یا [[Special:UserLogin|داخلِ نوشتہ]] ہوجائیے تاکہ مستقبل میں آپکو گمنام صارفین میں شمار کرنے سے پرہیز کیا جاسکے۔\"",
+       "anontalkpagetext": "----''یہ صفحہ ایک ایسے صارف کا ہے جنہوں نے یا تو اب تک اپنا کھاتا نہیں بنایا یا پھر وہ اسے استعمال نہیں کر رہے/ رہی ہیں۔ لہٰذا ہمیں انکی شناخت کے لئے ایک عددی آئی پی پتہ استعمال کرنا پڑرہا ہے۔ اس قسم کا آئی پی پتہ ایک سے زائد صارفین کے لئے مشترک بھی ہوسکتا ہے۔ اگر آپکی موجودہ حیثیت ایک گمنام صارف کی ہے اور آپ محسوس کریں کہ اس صفحہ پر آپکی جانب منسوب یہ بیان غیرضروری ہے تو براہ کرم [[Special:CreateAccount|کھاتہ بنائیں]] یا [[Special:UserLogin|داخلِ نوشتہ]] ہوجائیے تاکہ مستقبل میں آپکو گمنام صارفین میں شمار کرنے سے پرہیز کیا جاسکے۔\"",
        "noarticletext": "اِس صفحہ میں فی الحال کوئی متن موجود نہیں ہے.\nآپ دیگں صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کیلئے تلاش کرسکتے ہیں]]، <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات تلاش کرسکتے ہیں],\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} اِس صفحہ میں ترمیم کرسکتے ہیں]</span>",
        "noarticletext-nopermission": "اس صفحہ میں فی الحال کوئی متن موجود نہیں ہے.\nآپ دیگں صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کیلئے]] یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات تلاش کرسکتے ہیں]</span>",
        "userpage-userdoesnotexist-view": "صارف کھاتہ \"$1\" مندرج نہیں ہے۔",
        "allmessagesdefault": "طے شدہ متن",
        "allmessagescurrent": "موجودہ متن",
        "allmessagestext": "یہ میڈیاویکی: جاۓ نام میں دستیاب نظامی پیغامات کی فہرست ہے۔",
+       "allmessages-filter": "تلاش بلحاظ:",
        "allmessages-filter-all": "تمام",
        "allmessages-filter-modified": "تبدیل شدہ",
+       "allmessages-prefix": "تلاش بلحاظ سابقہ:",
        "allmessages-language": "زبان:",
        "allmessages-filter-submit": "ٹھیک",
        "allmessages-filter-translate": "ترجمہ",
        "logentry-delete-delete": "$1 {{GENDER:$2|حذف کیا گیا}} صفحہ $3",
        "logentry-move-move": "$1 نے صفحہ $3 کو بجانب $4 منتقل کیا",
        "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
+       "logentry-protect-modify": "$1 نے $3 کا درجۂ حفاظت {{GENDER:$2|تبدیل کیا}} $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|اپلوڈ}} $3",
        "rightsnone": "(کچھ نہیں)",
        "revdelete-summary": "خلاصۂ تدوین",
index f890815..608b867 100644 (file)
        "accmailtitle": "Maxfiy soʻz joʻnatildi",
        "newarticle": "(Yangi)",
        "newarticletext": "Bu sahifa hali mavjud emas.\nSahifani yaratish uchun quyida matn kiritishingiz mumkin (qoʻshimcha axborot uchun [$1 yordam sahifasini] koʻring).\nAgar bu sahifaga xatolik sabab kelib qolgan boʻlsangiz brauzeringizning '''orqaga''' tugmasini bosing.",
-       "anontalkpagetext": "----\n<em>Ushbu munozara sahifasi hisob yozuvi yaratmagan (yoki yaratishni xohlamaydigan) anonim foydalanuvchiga tegishli.</em>\n\nShu sababli, uni aniqlash uchun raqamli IP-manzildan foydalaniladi.\nUshbu IP-manzil bir nechta foydalanuvchilarga tegishli boʻlishi mumkin.\nAgar siz anonim foydalanuvchi boʻlsangiz va qoldirilgan xabarlar sizga yoʻnaltirilmagan deb hisoblasangiz, iltimos, boshqa anonim foydalanuvchilar bilan adashtirib yubormasliklari uchun [[Special:UserLogin/signup|hisob yozuvi yarating]] yoki [[Special:UserLogin|tizimga kiring]].",
+       "anontalkpagetext": "----\n<em>Ushbu munozara sahifasi hisob yozuvi yaratmagan (yoki yaratishni xohlamaydigan) anonim foydalanuvchiga tegishli.</em>\n\nShu sababli, uni aniqlash uchun raqamli IP-manzildan foydalaniladi.\nUshbu IP-manzil bir nechta foydalanuvchilarga tegishli boʻlishi mumkin.\nAgar siz anonim foydalanuvchi boʻlsangiz va qoldirilgan xabarlar sizga yoʻnaltirilmagan deb hisoblasangiz, iltimos, boshqa anonim foydalanuvchilar bilan adashtirib yubormasliklari uchun [[Special:CreateAccount|hisob yozuvi yarating]] yoki [[Special:UserLogin|tizimga kiring]].",
        "noarticletext": "Bu sahifada hozircha hech qanday matn yoʻq. Siz bu sarlavhani boshqa sahifalardan [[Special:Search/{{PAGENAME}}|qidirishingiz]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tegishli qaydlarga qarashingiz] yoki bu sahifani [{{fullurl:{{FULLPAGENAME}}|action=edit}} tahrirlashingiz]</span> mumkin.",
        "userpage-userdoesnotexist-view": "\"$1\" foydalanuvchi hisobi roʻyxatga olinmagan.",
        "clearyourcache": "'''Eslatma.''' Saqlaganingizdan so'ng o'zgarishlarni ko'rish uchun siz o'z brauzeringiz keshini tozalashingizga to'gri kelishi mumkin.\n* '''Firefox / Safari:''' ''Shift'' tugmasini bosgan holda, ''Yangilash'' unsurlar darchasini bosing, yoki ''Ctrl-F5'' yoki ''Ctrl-R'' (Macda ''⌘-R'') ni bosing\n* '''Google Chrome:''' ''Ctrl-Shift-R'' (Macda ''⌘-Shift-R'') ni bosing\n* '''Internet Explorer:''' ''Ctrl''ni bosgan holda, ''Yangilash''ni bosing, yoki ''Ctrl-F5''ni bosing\n* '''Opera:''' ''Asboblar → Moslamalar'' menyusidan keshni tozalashni tanlang",
        "categories": "Turkumlar",
        "categoriespagetext": "Quyidagi {{PLURAL:$1|turkumda|turkumlarda}} sahifa yoki media-fayllar mavjud.\n[[Special:UnusedCategories|Ishlatilmayotgan turkumlar]] bu yerda koʻrsatilmaydi.\nShuningdek qarang: [[Special:WantedCategories|talab qilinayotgan turkumlar]].",
        "categoriesfrom": "Quyidagidan boshlanuvchi turkumlarni koʻrsatish:",
-       "special-categories-sort-count": "miqdori bo‘yicha saralash",
-       "special-categories-sort-abc": "alifbo bo‘yicha saralash",
        "deletedcontributions": "Foydalanuvchining o‘chirilgan hissasi",
        "deletedcontributions-title": "O‘chirilgan foydalanuvchilar hissalari",
        "sp-deletedcontributions-contribs": "hissa",
index 5bcc052..bc47ef1 100644 (file)
        "noname": "El nome utente indicà no xè vałido.",
        "loginsuccesstitle": "Aceso efetuà",
        "loginsuccess": "'''Te si sta conesso al server de {{SITENAME}} con el nome utente de \"$1\".'''",
-       "nosuchuser": "Nol xè rejistrà alcun utente de nome \"$1\". I nomi utente i xè sensibiłi a łe majuscołe. Verifegare el nome inserio o [[Special:UserLogin/signup|creare on novo aceso]].",
+       "nosuchuser": "Nol xè rejistrà alcun utente de nome \"$1\". I nomi utente i xè sensibiłi a łe majuscołe. Verifegare el nome inserio o [[Special:CreateAccount|creare on novo aceso]].",
        "nosuchusershort": "No ghe xe nissun utente de nome \"$1\". Sito sicuro che te lo ghè scrito ben?",
        "nouserspecified": "Te ghè da métar un nome utente.",
        "login-userblocked": "Sta utensa xè blocà. No xè posibiłe efetuare el login.",
        "accmailtext": "Na password xenerà casualmente par [[User talk:$1|$1]] la xe stà mandà a $2.\n\nLa password par sta nova utensa la pode vegner canbià, dopo ver fato l'acesso, su la pàxena ''[[Special:ChangePassword|canbiar la password]]''.",
        "newarticle": "(Novo)",
        "newarticletext": "Te ghe sì 'ndà drio a un colegamento a na pagina che no esiste gnancora.\nSe te voli crear sta pagina, taca scrìvar el testo in te la casèla qua soto\n(varda le [$1 pagine de ajuto] par saverghene de pì).\nSe te sì rivà qua par sbajo, basta che te struchi '''Indrìo''' sul to browser.",
-       "anontalkpagetext": "----''Sta qua la xe la pagina de discussion de un utente anonimo che no'l se gà gnancora registrà o che no'l xe entrà col so nome utente.\nDe conseguenza xè necessario identificarlo tramite l'indirizo IP numerico.\nSto indirizo el pode èssar doparà da tanti utenti.\nSe te sì un utente anonimo e te ghè ricevù dei messagi che te secondo ti i gera par qualchedun altro, te podi [[Special:UserLogin/signup|registrarte]] o [[Special:UserLogin|entrar col to nome utente]] par evitar confusion con altri utenti anonimi in futuro.''",
+       "anontalkpagetext": "----''Sta qua la xe la pagina de discussion de un utente anonimo che no'l se gà gnancora registrà o che no'l xe entrà col so nome utente.\nDe conseguenza xè necessario identificarlo tramite l'indirizo IP numerico.\nSto indirizo el pode èssar doparà da tanti utenti.\nSe te sì un utente anonimo e te ghè ricevù dei messagi che te secondo ti i gera par qualchedun altro, te podi [[Special:CreateAccount|registrarte]] o [[Special:UserLogin|entrar col to nome utente]] par evitar confusion con altri utenti anonimi in futuro.''",
        "noarticletext": "In sto momento no ghe xe nissun testo su sta pagina.\nTe pol [[Special:Search/{{PAGENAME}}|sercar el titolo de sta pagina]] in altre pagine,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sercar in tei registri ligà a sta pagina] o se nò [{{fullurl:{{FULLPAGENAME}}|action=edit}} canbiar la pagina]</span>.",
        "noarticletext-nopermission": "In sto momento no ghe xe nissun testo su sta pagina.\nTe pol [[Special:Search/{{PAGENAME}}|sercar sto titolo de pagina]] in altre pagine,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sercar in tei registri ligà a sta pagina]</span>, ma no te ghè el parmesso de crear sta pagina.",
        "missing-revision": "Ła revixion #$1 de ła pàjina \"{{FULLPAGENAME}}\" nó ła existe.",
index 2b12101..3e57c4e 100644 (file)
        "noname": "Tö ei olgoi kirjutanuded lasktud kävutajan nimed.",
        "loginsuccesstitle": "Tulend sistemha om lopnus satusekahas.",
        "loginsuccess": "'''Tö radat nügüd' {{SITENAME}}-saital kut \"$1\".'''",
-       "nosuchuser": "Ei ole kävutajad \"$1\"-nimenke.\nKävutajan nimiden oigedkirjutamine rippub kirjamiden registraspäi.\nKodvgat teiden oigedkirjutamine, vai [[Special:UserLogin/signup|säkat uz' registracii]].",
+       "nosuchuser": "Ei ole kävutajad \"$1\"-nimenke.\nKävutajan nimiden oigedkirjutamine rippub kirjamiden registraspäi.\nKodvgat teiden oigedkirjutamine, vai [[Special:CreateAccount|säkat uz' registracii]].",
        "nosuchusershort": "Ei ole kävutajad \"$1\"-nimenke.\nKodvgat teiden oigedkirjutamine.",
        "nouserspecified": "Pidab kirjutada kävutajan nimi.",
        "login-userblocked": "Nece kävutai om blokiruidud. Tulend sistemha om kel'tud.",
index 2589d77..67005f6 100644 (file)
        "noname": "Chưa nhập tên.",
        "loginsuccesstitle": "Đã đăng nhập",
        "loginsuccess": "'''Bạn đã đăng nhập vào {{SITENAME}} với tên “$1”.'''",
-       "nosuchuser": "Không có thành viên nào có tên “$1”.\nTên người dùng có phân biệt chữ hoa chữ thường.\nHãy kiểm tra lại chính tả, hoặc [[Special:UserLogin/signup|mở tài khoản mới]].",
+       "nosuchuser": "Không có thành viên nào có tên “$1”.\nTên người dùng có phân biệt chữ hoa chữ thường.\nHãy kiểm tra lại chính tả, hoặc [[Special:CreateAccount|mở tài khoản mới]].",
        "nosuchusershort": "Không có thành viên nào có tên “$1”. Xin hãy kiểm tra lại chính tả.",
        "nouserspecified": "Bạn phải chỉ định một tên người dùng.",
        "login-userblocked": "Thành viên này đã bị cấm. Không cho phép đăng nhập.",
        "accmailtext": "Một mật khẩu được tạo ngẫu nhiên cho [[User talk:$1|$1]] đã được gửi đến $2. Có thể đổi mật khẩu tại trang ''[[Special:ChangePassword|đổi mật khẩu]]'' sau khi đã đăng nhập.",
        "newarticle": "(Mới)",
        "newarticletext": "Bạn đi đến đây từ một liên kết đến một trang chưa tồn tại. Để tạo trang, hãy bắt đầu gõ vào ô bên dưới (xem [$1 trang trợ giúp] để có thêm thông tin). Nếu bạn đến đây do nhầm lẫn, chỉ cần nhấn vào nút '''Lùi''' (hoặc Trở lại, Quay lại, Back) trong trình duyệt của bạn.",
-       "anontalkpagetext": "----''Đây là trang thảo luận của một người dùng vô danh chưa tạo tài khoản hoặc có tài khoản nhưng không đăng nhập.\nDo đó chúng ta phải dùng một dãy số gọi là địa chỉ IP để xác định anh/chị ta.\nMột địa chỉ IP như vậy có thể có nhiều người cùng dùng chung.\nNếu bạn là một thành viên vô danh và cảm thấy rằng có những lời bàn luận không thích hợp đang nhắm vào bạn, xin hãy [[Special:UserLogin/signup|tạo tài khoản]] hoặc [[Special:UserLogin|đăng nhập]] để tránh sự nhầm lẫn về sau với những thành viên vô danh khác.''",
+       "anontalkpagetext": "----''Đây là trang thảo luận của một người dùng vô danh chưa tạo tài khoản hoặc có tài khoản nhưng không đăng nhập.\nDo đó chúng ta phải dùng một dãy số gọi là địa chỉ IP để xác định anh/chị ta.\nMột địa chỉ IP như vậy có thể có nhiều người cùng dùng chung.\nNếu bạn là một thành viên vô danh và cảm thấy rằng có những lời bàn luận không thích hợp đang nhắm vào bạn, xin hãy [[Special:CreateAccount|tạo tài khoản]] hoặc [[Special:UserLogin|đăng nhập]] để tránh sự nhầm lẫn về sau với những thành viên vô danh khác.''",
        "noarticletext": "Trang này hiện chưa có nội dung.\nBạn có thể [[Special:Search/{{PAGENAME}}|tìm kiếm tựa trang này]] trong các trang khác, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tìm trong các nhật trình liên quan],\nhoặc [{{fullurl:{{FULLPAGENAME}}|action=edit}} tạo mới trang này]</span>.",
        "noarticletext-nopermission": "Trang này hiện đang trống.\nBạn có thể [[Special:Search/{{PAGENAME}}|tìm kiếm tựa trang này]] tại các trang khác, hoặc <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tìm kiếm các nhật trình liên quan]</span>, nhưng bạn không được phép tạo trang này.",
        "missing-revision": "Phiên bản #$1 của trang có tên “{{FULLPAGENAME}}” không tồn tại.\n\nLỗi này thường xuất hiện đối khi theo dõi liên kết lỗi thời đến phiên bản cũ của một trang đã bị xóa.\nXem chi tiết trong [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} nhật trình xóa].",
        "upload-form-label-own-work": "Đây là tác phẩm của chính tôi",
        "upload-form-label-infoform-categories": "Thể loại",
        "upload-form-label-infoform-date": "Ngày tháng",
-       "upload-form-label-own-work-message-local": "Tôi xác nhận rằng tôi tải lên tập tin này tuân theo các điều khoản sử dụng và quy định giấy phép của {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "Nếu bạn không được phép tải lên tập tin này tuân theo quy định của {{SITENAME}}, xin vui lòng đóng hộp thoại này và thử tải lên bằng một phương pháp khác.",
-       "upload-form-label-not-own-work-local-local": "Bạn cũng có thể muốn sử dụng [[Special:Upload|trang tải lên mặc định]].",
-       "upload-form-label-own-work-message-default": "Tôi hiểu rằng tôi đang tải tập tin này lên một kho dùng chung. Tôi xác nhận rằng tôi làm việc này tuân theo các điều khoản sử dụng và quy định về giấy phép tại đấy.",
-       "upload-form-label-not-own-work-message-default": "Nếu bạn không có thể tải tập tin này lên mà tuân theo quy định của kho dùng chung, xin vui lòng đóng hộp thoại này và thử một cách khác.",
-       "upload-form-label-not-own-work-local-default": "Bạn có thể muốn thử [[Special:Upload|trang tải lên tại {{SITENAME}}]] nếu tập tin này có thể được tải lên đấy theo các quy định của họ.",
-       "upload-form-label-own-work-message-shared": "Tôi khẳng định rằng tôi giữ quyền tác giả của tập tin này và đồng ý phát hành, một cách không thể hủy bỏ, tập tin này cho Wikimedia Commons theo giấy phép [https://creativecommons.org/licenses/by-sa/4.0/deed.vi Creative Commons Ghi công–Chia sẻ tương tự 4.0], và tôi chấp nhận các [https://wikimediafoundation.org/wiki/Terms_of_Use/vi?uselang=vi Điều khoản Sử dụng].",
-       "upload-form-label-not-own-work-message-shared": "Nếu bạn không giữ quyền tác giả của tập tin hoặc muốn phát hành nó theo một giấy phép khác, xin nghĩ đến việc sử dụng [https://commons.wikimedia.org/wiki/Special:UploadWizard?uselang=vi Trình thuật sĩ tải lên Commons].",
-       "upload-form-label-not-own-work-local-shared": "Bạn cũng có thể muốn thử [[Special:Upload|trang tải lên tại {{SITENAME}}]] nếu trang đó cho phép tải lên tập tin này theo các quy định của họ.",
+       "upload-form-label-own-work-message-generic-local": "Tôi xác nhận rằng tôi tải lên tập tin này tuân theo các điều khoản sử dụng và quy định giấy phép của {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "Nếu bạn không được phép tải lên tập tin này tuân theo quy định của {{SITENAME}}, xin vui lòng đóng hộp thoại này và thử tải lên bằng một phương pháp khác.",
+       "upload-form-label-not-own-work-local-generic-local": "Bạn cũng có thể muốn sử dụng [[Special:Upload|trang tải lên mặc định]].",
+       "upload-form-label-own-work-message-generic-foreign": "Tôi hiểu rằng tôi đang tải tập tin này lên một kho dùng chung. Tôi xác nhận rằng tôi làm việc này tuân theo các điều khoản sử dụng và quy định về giấy phép tại đấy.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Nếu bạn không có thể tải tập tin này lên mà tuân theo quy định của kho dùng chung, xin vui lòng đóng hộp thoại này và thử một cách khác.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Bạn có thể muốn thử [[Special:Upload|trang tải lên tại {{SITENAME}}]] nếu tập tin này có thể được tải lên đấy theo các quy định của họ.",
        "backend-fail-stream": "Không thể gửi luồng tập tin $1.",
        "backend-fail-backup": "Không thể sao lưu tập tin $1.",
        "backend-fail-notexists": "Tập tin $1 không tồn tại.",
        "whatlinkshere-prev": "{{PLURAL:$1|kết quả trước|$1 kết quả trước}}",
        "whatlinkshere-next": "{{PLURAL:$1|kết quả sau|$1 kết quả sau}}",
        "whatlinkshere-links": "← liên kết",
-       "whatlinkshere-hideredirs": "$1 trang đổi hướng",
+       "whatlinkshere-hideredirs": "Ẩn trang đổi hướng",
        "whatlinkshere-hidetrans": "$1 trang nhúng",
-       "whatlinkshere-hidelinks": "$1 liên kết",
-       "whatlinkshere-hideimages": "$1 liên kết tập tin",
+       "whatlinkshere-hidelinks": "Ẩn liên kết",
+       "whatlinkshere-hideimages": "Ẩn liên kết tập tin",
        "whatlinkshere-filters": "Bộ lọc",
        "whatlinkshere-submit": "Xem",
        "autoblockid": "Cấm tự động #$1",
index 998e2c5..c2aef82 100644 (file)
        "noname": "No egivol gebananemi lonöföl.",
        "loginsuccesstitle": "Enunädol oli benosekiko",
        "loginsuccess": "'''Binol anu in {{SITENAME}} as \"$1\".'''",
-       "nosuchuser": "No dabinon geban labü nem: \"$1\".\nGebananems distidons mayudis i minudis.\nKoräkolös tonatami nema at, u [[Special:UserLogin/signup|jafolös kali nulik]].",
+       "nosuchuser": "No dabinon geban labü nem: \"$1\".\nGebananems distidons mayudis i minudis.\nKoräkolös tonatami nema at, u [[Special:CreateAccount|jafolös kali nulik]].",
        "nosuchusershort": "No dabinon geban labü nem: \"$1\". Koräkolös tonatami nema at.",
        "nouserspecified": "Mutol välön gebananemi.",
        "wrongpassword": "Letavöd neveräton. Steifülolös dönu.",
index 3e1403c..b98a327 100644 (file)
        "noname": "Võlssi kirotõt pruukjanimi.",
        "loginsuccesstitle": "Sisseminek läts' kõrda",
        "loginsuccess": "Olõt nimega sisse lännüq. Suq pruukjanimi om \"$1\".",
-       "nosuchuser": "\"$1\" nimelist pruukjat olõ-i olõman.\nKaeq kiräpilt üle vai [[Special:UserLogin/signup|luuq vahtsõnõ pruukjanimi]].",
+       "nosuchuser": "\"$1\" nimelist pruukjat olõ-i olõman.\nKaeq kiräpilt üle vai [[Special:CreateAccount|luuq vahtsõnõ pruukjanimi]].",
        "nosuchusershort": "\"$1\" nimelist pruukjat olõ-i olõman. Kas kirotit iks nime õigõhe?",
        "nouserspecified": "Olõ-i kirotõt pruukjanimme.",
        "login-userblocked": "Seo pruukja om kinniq peet. Nimega sisseminemine olõ-õi lubat.",
        "accmailtext": "Pruukjalõ '$1' luud johuslinõ salasõna saadõti aadresi pääle $2.\n\nTuud salasõnna saa muutaq ''[[Special:ChangePassword|salasõba muutmisõ lehe pääl]]'' päält vahtsõ nimega sisseminemist.",
        "newarticle": "(Vahtsõnõ)",
        "newarticletext": "Taad lehekülge olõ-i viil luud.\nLeheküle luumisõs nakkaq kirotama alanolõvahe kasti (kaeq [$1 oppust]).\nKu sa johtuq siiäq kogõmaldaq, sis klõpsaq võrgokaeja '''Tagasi'''-nuppi.",
-       "anontalkpagetext": "---- ''Taa om arotusleht nimeldä pruukja kotsilõ, kiä olõ-i loonuq pruukjanimme vai pruugi-i tuud. Tuuperäst tulõ meil pruukja kimmästegemises pruukiq timä puutri võrgoaadrõssit. Taa aadrõs või ollaq mitmõ pruukja pääle ütine. Ku olõt nimeldä pruukja ja lövvät, et taa leheküle pääle kirotõt jutt käü suq kotsilõ, sis olõq hää, [[Special:UserLogin/signup|luuq konto]] vai [[Special:UserLogin|mineq nimega sisse]], et edespiten segähüisi ärq hoitaq.''",
+       "anontalkpagetext": "---- ''Taa om arotusleht nimeldä pruukja kotsilõ, kiä olõ-i loonuq pruukjanimme vai pruugi-i tuud. Tuuperäst tulõ meil pruukja kimmästegemises pruukiq timä puutri võrgoaadrõssit. Taa aadrõs või ollaq mitmõ pruukja pääle ütine. Ku olõt nimeldä pruukja ja lövvät, et taa leheküle pääle kirotõt jutt käü suq kotsilõ, sis olõq hää, [[Special:CreateAccount|luuq konto]] vai [[Special:UserLogin|mineq nimega sisse]], et edespiten segähüisi ärq hoitaq.''",
        "noarticletext": "Seo leht om parlaq tühi.\nVõit [[Special:Search/{{PAGENAME}}|otsiq soe lehe nimme]]  tõisi lehti päält vai\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uuriq muutmisnimekirjo] vai [{{fullurl:{{FULLPAGENAME}}|action=edit}} puuduolõva leheküle esiq luvvaq]</span>.",
        "noarticletext-nopermission": "Seo lehe pääl olõ-õi parlaq teksti.\nVõit [[Special:Search/{{PAGENAME}}|otsiq soe lehe nimme]] tõisi lehti päält vai\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uuriq muutmisnimekirjo]</span>, a sul olõ-õi õigust seod lehte alostaq.",
        "userpage-userdoesnotexist": "Pruukjanimme \"<nowiki>$1</nowiki>\" olõ-i kirjä pant. Kaeq perrä, kas olõt iks kimmäs, et tahat taad lehte toimõndaq.",
index 898a3df..eb0577b 100644 (file)
        "noname": "Vos n' avoz nén dné di no d' uzeu valide.",
        "loginsuccesstitle": "Vos estoz elodjî",
        "loginsuccess": "'''L' elodjaedje a stî comifåt, asteure vos estoz elodjî dins {{SITENAME}} dizo l' no d' uzeu «$1».'''",
-       "nosuchuser": "I g na nou uzeu dizo l' no «$1».\nNotez k' les nos d' uzeu fjhèt l' diferince etur pitites et grandès letes.\nVerifyîz çou k' vos avoz tapé, oudonbén [[Special:UserLogin/signup|ahivez on novea conte]].",
+       "nosuchuser": "I g na nou uzeu dizo l' no «$1».\nNotez k' les nos d' uzeu fjhèt l' diferince etur pitites et grandès letes.\nVerifyîz çou k' vos avoz tapé, oudonbén [[Special:CreateAccount|ahivez on novea conte]].",
        "nosuchusershort": "I g na nou uzeu dizo l' no «$1». Verifyîz çou k' vos avoz tapé.",
        "nouserspecified": "Vos dvoz dner on no d' elodjaedje.",
        "login-userblocked": "{{GENDER:$1|Cist uzeu est bloké|Ciste uzeuse est blokêye}}. L' elodjaedje n' est nén possibe.",
        "accmailtext": "On scret costrût a l' astcheyance po [[User talk:$1|$1]] a stî evoyî a $2.\n\nLi scret po ci novea conte ci pout esse candjî sol pådje di ''[[Special:ChangePassword|candjmint di scret]]'' après l' elodjaedje.",
        "newarticle": "(Novea)",
        "newarticletext": "Vos avoz clitchî so on loyén viè ene pådje ki n' egzistêye nén co.\nMins '''vos''' l' poloz askepyî! Po çoula, vos n' avoz k' a cmincî a taper vosse tecse dins l' boesse di tecse cial pa dzo (alez vey li [$1 pådje d' aidance] po pus d' infôrmåcion).\nSi vos estoz droci par accidint, clitchîz simplumint sol boton <strong>En erî</strong> di vosse betchteu waibe po rivni al pådje di dvant.",
-       "anontalkpagetext": "---- ''Çouchal, c' est li pådje di copene po èn uzeu anonime ki n' a nén (co) fwait on conte por lu s' elodjî, ou ki n' l' eploye nén.\nÇa fwait k' on doet eployî si adresse IP limerike po l' idintifyî.\nCome ene sifwaite adresse IP pout esse eployeye pa pus d' èn uzeu, i s' pout ki vos veyoz chal des rmarkes et des messaedjes ki n' sont nén por vos.\nLoukîz s' i vs plait po [[Special:UserLogin/signup|fé on novea conte]] ou [[Special:UserLogin|s' elodjî]] po n' pus aveur d' ecramiaedje avou des ôtes uzeus anonimes.''",
+       "anontalkpagetext": "---- ''Çouchal, c' est li pådje di copene po èn uzeu anonime ki n' a nén (co) fwait on conte por lu s' elodjî, ou ki n' l' eploye nén.\nÇa fwait k' on doet eployî si adresse IP limerike po l' idintifyî.\nCome ene sifwaite adresse IP pout esse eployeye pa pus d' èn uzeu, i s' pout ki vos veyoz chal des rmarkes et des messaedjes ki n' sont nén por vos.\nLoukîz s' i vs plait po [[Special:CreateAccount|fé on novea conte]] ou [[Special:UserLogin|s' elodjî]] po n' pus aveur d' ecramiaedje avou des ôtes uzeus anonimes.''",
        "noarticletext": "I gn a pol moumint nou tecse e cisse pådje chal.\nVos ploz [[Special:Search/{{PAGENAME}}|cweri après l' tite di cisse pådje ci]] dins des ôtès pådjes,\noudonbén <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cweri dins les djournås],\nou co [{{fullurl:{{FULLPAGENAME}}|action=edit}} ahiver l' pådje]</span>.",
        "noarticletext-nopermission": "I gn a pol moumint nou tecse e cisse pådje chal.\nVos ploz [[Special:Search/{{PAGENAME}}|cweri après l' tite di cisse pådje ci]] dins des ôtès pådjes,\noudonbén <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cweri dins les djournås]</span>, mins vos n' avoz nén l' livea d' permission pol poleur ahiver.",
        "blocked-notice-logextract": "{{GENDER:$1|Cist uzeu est bloké|Ciste uzeuse est blokêye}} pol moumint.\nLi dierinne intrêye e djournå des blocaedjes est dnêye chal pa dzo po infôrmåcion:",
        "categories-submit": "Mostrer",
        "categoriespagetext": "{{PLURAL:$1|Li categoreye shuvante est eployeye|Les categoreyes shuvantes sont-st eployeyes}} pa des pådjes ou des fitchîs.\n[[Special:UnusedCategories|Les categoreyes nén eployeyes]] èn sont nén håynêyes chal.\nLoukîz eto [[Special:WantedCategories|les categoreyes dimandêyes]].",
        "categoriesfrom": "Håyner les categoreyes a pårti di:",
-       "special-categories-sort-count": "relére pa nombe di cayets",
-       "special-categories-sort-abc": "relére alfabeticmint",
        "deletedcontributions": "Contribouwaedjes disfacés",
        "deletedcontributions-title": "Contribouwaedjes disfacés",
        "sp-deletedcontributions-contribs": "contribouwaedjes",
index d63feb5..f563ad9 100644 (file)
        "laggedslavemode": "Pahimatngon: It pakli bangin waray mga kabag-ohan nga bag-o.",
        "readonly": "Gintrankahan an database",
        "enterlockreason": "Pagbutang hin rason para han pagtrangka, upod hin banabana kon san-o kukuha-on an pagtrangka",
-       "readonlytext": "An database in nakatrangka yana ha bag-o nga mga entrada ngan iba nga mga modipikasyon, tungod siguro ha routine database maintenance, kahuman ini in mabalik ha normal.\n\nAn magdudumara nga nagtrangka hini in naghatag hini nga kasayoran: $1",
+       "readonlytext": "An database in nakatrangka yana ha bag-o nga mga entrada ngan iba nga mga modipikasyon, tungod siguro ha routine database maintenance, kahuman ini in mabalik ha normal.\n\nAn system administrator nga nagtrangka hini in naghatag hini nga kasayoran: $1",
        "missing-article": "Ini nga database in waray nakaagi han teksto han pakli nga dapat mabilngan, nga ginngaranan nga \"$1\" $2.\n\nIni in agsob hinungdan han pagsunod han kadaan nga kaibhan o sumpay han kaagi ngadto ha pakli nga ginpara.\n\nKun diri ini an kaso, bangin ka nakabiling hin bug ha software.\nAlayon la igsumat ini ha [[Special:ListUsers/sysop|administrator]], igsurat la an URL.",
        "missingarticle-rev": "(pagbag-o#: $1)",
        "missingarticle-diff": "(Kaibhan: $1, $2)",
        "protectedinterface": "Ini nga pakli in nahatag hin teksto hit interface para han software han hin nga wiki, ngan in pinasasaliporan para makalikay hit pag-abuso.\nPara makadugang o makaliwat hin mga paghubad para han tanan nga mga wiki, alayon paggamit han [//translatewiki.net/ translatewiki.net], an kanan MediaWiki proyekto hin lokalisasyon.",
        "editinginterface": "'''Pahimatngon:''' Imo ginliliwat an pakli nga gingagamit paghatag hin interface text para han software.\nAn mga pagbag-o hinin nga pakli in makakaapekto han itsura han user interface han iba nga mga gumaramit hini nga wiki.",
        "translateinterface": "Para han pagdugang o pagbag-o han mga paghubad han ngatanan nga mga wiki, alayon paggamit han [//translatewiki.net/ translatewiki.net], an MediaWiki lokalisasyon nga proyekto.",
-       "cascadeprotected": "Ini nga pakli in pinapasaliporan hin pagliwat tungod ini in nalalakip ha masunod nga {{PLURAL:$1|pakli, kun diin |mga pakli, kun diin}} pinapasaliporan hit \"cascading\" nga pagpili nga pinaandar:\n$2",
+       "cascadeprotected": "Ini nga pakli in pinapasaliporan tikang ha pagliwat tungod ini in naka-transclude ha masunod nga {{PLURAL:$1|pakli, kun diin |mga pakli, kun diin}} pinapasaliporan hit \"cascading\" nga pagpili nga pinaandar:\n$2",
        "namespaceprotected": "Diri ka gintutugutan pagliwat han mga pakli ha ngaran-lat'ang nga '''$1'''.",
        "customcssprotected": "Diri ka gintutugotan pagliwat hini nga CSS nga pakli, tungod nga nagsusulod ini hin kanan iba nga tawo personal nga karuyagon.",
        "customjsprotected": "Diri ka gintutugotan pagliwat hini nga JavaScript nga pakli, tungod nga nagsusulod ini hin kanan iba nga tawo personal nga karuyagon.",
        "mypreferencesprotected": "Waray nim pagtugot hit pagliwat hit imo mga karuyag.",
        "ns-specialprotected": "Diri maliliwat an mga ispisyal nga pakli.",
        "titleprotected": "Ini nga titulo pinasalipod ha paghimo ni [[User:$1|$1]].\nAn katadungan nga ginhatag amo in <em>$2</em>.",
-       "filereadonlyerror": "Diri maliliwat ini nga paypay \"$1\" tungod an ginsusudlan han paypay nga \"$2\" in aada la ha pagbasa-la nga kahimtang.\n\nAn magdudurmara nga nagtrangka hini in naghatag hini nga eksplenasyon: \"$3\".",
+       "filereadonlyerror": "Diri maliliwat ini nga paypay \"$1\" tungod an ginsusudlan han paypay nga \"$2\" in aada la ha pagbasa-la nga kahimtang.\n\nAn system administrator nga nagtrangka hini in naghatag hini nga eksplenasyon: \"$3\".",
        "invalidtitle-knownnamespace": "Titulo nga inbalido nga may pan-ngaran \"$2 ngan teksto nga \"$3\"",
        "invalidtitle-unknownnamespace": "Diri ginkakarawat nga titulo tungod mayda ini hin mga diri nakikilala nga ngaran-lat'ang ihap $1 ngan teksto \"$2\"",
        "exception-nologin": "Diri nakalog-in",
-       "exception-nologin-text": "Alayon [[Special:Userlogin|pagsakob]] basi makakadto hiní nga pakli o buruhatón.",
+       "exception-nologin-text": "Alayon paglog-in basi makakadto hiní nga pakli o buruhatón.",
        "exception-nologin-text-manual": "Alayon $1 basi makakadto hini nga pakli o buruhatón.",
        "virus-badscanner": "Maraot nga configuration: Waray kasabti nga virus scanner: ''$1''",
        "virus-scanfailed": "Pakyas an pag-scan (kodigo $1)",
        "noname": "Waray ka nakahatag hin maupay nga agnay-hit-gumaramit.",
        "loginsuccesstitle": "Nakalog-in",
        "loginsuccess": "'''Ikaw in nakalog-in ha {{SITENAME}} komo \"$1\".'''",
-       "nosuchuser": "Waray gumaramit an may-ada ngaran nga \"$1\".\nIt mga agnay-hi-gumaramit in case sensitive.\nPanginano-a it imo pagbaybay, o [[Special:UserLogin/signup|paghimo hin bag-o nga akawnt]].",
+       "nosuchuser": "Waray gumaramit an may-ada ngaran nga \"$1\".\nIt mga agnay-hi-gumaramit in case sensitive.\nPanginano-a it imo pagbaybay, o [[Special:CreateAccount|paghimo hin bag-o nga akawnt]].",
        "nosuchusershort": "Waray nagamit it may ngaran nga \"$1\".\nKitaa kun amo it im pagbaybay.",
        "nouserspecified": "Dapat nim magbutang hin agnay hit gumaramit.",
        "login-userblocked": "Ini nga gumaramit in ginpugngan.  Diri gintutugutan an pagsakob.",
        "noemail": "Waray e-mail nga adres nga ginrekord para han nágámit \"$1\".",
        "noemailcreate": "Kinahanglan nim maghatag hin may hinungdan nga e-mail address",
        "passwordsent": "Uska bag-o nga password in ginpadangat ha e-mail address nga nakarehistro kan \"$1\".\nAlayon paglog-in utro kahuman mo makarawat ini.",
-       "blocked-mailpassword": "An imo IP address in ginpugong ha pag-edit, ngan tungod hini in diri gintutugotan paggamit han password recovery function para malikyan an abuso.",
+       "blocked-mailpassword": "An imo IP address in ginpugngan pag-edit. Para pugngan an pag-abuso, ini in diri tinutogotan paggamit hin password recovery function tikang hinin nga IP address.",
        "eauthentsent": "Mayda e-mail hin pagkumpirma nga ginpadará hini nga ginhatag nga e-mail adres.\n\nSan-o magatagán pa hin ibá nga e-mail it akwant, kinahanglan nimo sundon an mga tugon nga nahabutáng han email basi makumpirma nga imo gud itón akawnt.",
        "throttled-mailpassword": "Usa nga tigaman-pagnakob reset email in ginpadangat na, ha sakob han urhi nga  {{PLURAL:$1|oras|$1 ka mga oras}}.\nBasi diri ini maabuso, uusa la nga tigaman-panakob in igpapadangat kada {{PLURAL:$1|oras|$1 ka mga oras}}.",
        "mailerror": "Sayop han pagpadangat hin surat: $1",
        "botpasswords-insert-failed": "Pakyas han pagdugang han ngaran han bot nga \"$1\". Naidugang na ini?",
        "botpasswords-update-failed": "Pakyas han pag-update han bot nga ngaran nga \"$1\". Ginpara na ini?",
        "botpasswords-created-title": "Nahimo an bot password",
-       "botpasswords-created-body": "An bot password nga \"$1\" in malinamposon nga nahimo.",
+       "botpasswords-created-body": "An bot password para han bot nga ngaran nga \"$1\" ni gumaramit \"$2\" in  nahimo.",
        "botpasswords-updated-title": "Gin-update an bot password",
-       "botpasswords-updated-body": "An bot password nga \"$1\" in malinamposon nga na-update.",
+       "botpasswords-updated-body": "An bot password para han ngaran han bot nga \"$1\" ni gumaramit \"$2\" in na-update.",
        "botpasswords-deleted-title": "Ginpara an bot password",
-       "botpasswords-deleted-body": "An bot password nga \"$1\" in ginpara.",
+       "botpasswords-deleted-body": "An bot password para han bot nga ngaran nga \"$1\" ni gumaramit \"$2\" in ginpara.",
        "botpasswords-newpassword": "An bag-o nga password para han pag log-in han <strong>$1</strong> in <strong>$2</strong>. <em>Alayon igrecord ini para han future reference.</em>",
        "botpasswords-no-provider": "BotPasswordsSessionProvider in waray dinhi.",
        "botpasswords-restriction-failed": "An mga restriction han bot password in nagpupugong han pag-login hinin.",
        "resetpass-no-info": "Kinahanglan mo paglog-in para direkta ka makasakob dinhi nga pakli.",
        "resetpass-submit-loggedin": "Igbal-iw an tigaman-pagsulod",
        "resetpass-submit-cancel": "Pasagdi",
-       "resetpass-wrong-oldpass": "Diri balido an temporaryo o yana nga tigaman-panakob.\nImo malinamposon nga ginsalyuan an imo tigaman-panakob o umaro ka na hin bag-o nga temporaryo nga tigman-panakob.",
+       "resetpass-wrong-oldpass": "Diri balido an temporaryo o yana nga password.\nImo na ginsalyuan an imo password o umaro ka na hin bag-o nga temporaryo nga password.",
        "resetpass-recycled": "Alayon pagreset han imo tigaman-pansakob hin lain tikang han imo yanâ nga tigaman-pansakob",
        "resetpass-temp-emailed": "Nagsakob ka pinaagi hin temporary nga gin-email nga kodigo.\nBasi matapos an imo pagsakob, kinahanglan ka maghimo hin bag-o nga tigaman-pansakob dinhi:",
        "resetpass-temp-password": "Temporaryo nga tigaman-pagsakob:",
        "passwordreset-emailtitle": "Mga detalye han akawnt ha {{SITENAME}}",
        "passwordreset-emailtext-ip": "Mayda gumaramit (bangin hi ikaw, tikang han IP adres nga $1) nga naghangyo hin reset han imo tigaman-pansulod han {{SITENAME}} ($4). An nasunod nga gumaramit {{PLURAL:$3|nga akawnt|nga mga akawnt}} nahanungod hini nga email nga adres: \n\n$2\n\n{{PLURAL:$3|Iní nga temporaryo nga tigaman-pansulod|Iní nga mga temporaryo nga tigaman-pansulod}} ma-waray bali hin {{PLURAL:$5|usa ka adlaw|$5 nga mga adlaw}}.\nAngay ka sumakob ngan pumílì hin bag-o nga tigaman-pansulod ha yanâ.  Kun mayda lain nga naghatag hini nga hangyo, o kun nahinumdoman mo an imo orihinal nga tigaman-pansulod, ngan nadírì ka na pagbalyo hiní, puyde mo pasagdan ini nga sumat ngan magpadayon hin paggamit han imo daan nga tigaman-pansulod.",
        "passwordreset-emailelement": "Agnay han gumaramit: \n$1\n\nTemporaryo nga tigaman han pagsakob: \n$2",
-       "passwordreset-emailsentemail": "Ginpadangat an password reset email.",
+       "passwordreset-emailsentemail": "Kun inin nga email address in may pagkahisumpay ha imo account, papadangaton ka hin usa nga password reset email.",
        "passwordreset-emailsent-capture": "Ginpadangat an password reset email, nga ginpakita ha ubos.",
        "passwordreset-emailerror-capture": "Ginhimo an password reset email, kun diin nakikita ha ubos, pero pakyas an pagpadara ha  {{GENDER:$2|gumaramit}}: $1",
        "changeemail": "Igliwat o igtanggal an e-mail address",
-       "changeemail-header": "Igliwan an e-mail address akawnt",
+       "changeemail-header": "Kompletoha ini nga form para masalyuan an imo email address. Kun karuyag nimo tanggalun an may pagkahisumpay han bisan ano nga email address tikang ha imo account, blankoha la an bag-o nga email address kun magsusumiti ka han form.",
        "changeemail-passwordrequired": "Kinahanglan nim igbutang an imo password para igkompirma inin nga pagbag-o.",
        "changeemail-no-info": "Kinahanglanon mo mag-log-in para ka direkta makasakob hini nga pakli.",
        "changeemail-oldemail": "Yana nga e-mail address:",
        "minoredit": "Gutiay ini nga pagliwat",
        "watchthis": "Bantayi ini nga pakli",
        "savearticle": "Igtipig an pakli",
+       "publishpage": "Igmantala an pakli",
        "preview": "Pahiuna nga pagawas",
        "showpreview": "Pakit-a an pahiuna nga pagawas",
        "showdiff": "Igpakita an mga ginliwat",
        "anonpreviewwarning": "''Diri ka naka-log in.  Mahisusurat an imo IP address ngada ha kanan pakli kaagi hit pagliwat kun igtipig nimo.''",
        "missingsummary": "<strong>Pahinumdom:</strong> Waray ka humatag hin halipotay nga masisiring hiton pagliwat. Kun pidliton mo an \"{{int:savearticle}}\" utro, an imo ginliwat in matitipig bisan waray hini.",
        "missingcommenttext": "Alayon pagbutang hin komento ha ilarom.",
-       "missingcommentheader": "'''Pahinumdom:''' Waray ka humatag hin subject/headline para hini nga komento.  Kun pinduton mo an \"{{int:savearticle}}\" utro, an imo pagliwat in matitipig bisan waray hini.",
+       "missingcommentheader": "<strong>Pahinumdom:</strong> Waray ka humatag hin subject para hinin nga komento.  Kun pinduton mo an \"{{int:savearticle}}\" utro, an imo pagliwat in matitipig bisan waray hini.",
        "summary-preview": "Pahiuna nga pagawas han dalikyat nga pulong:",
        "subject-preview": "Pahiuna nga pagawas hit himangrawan:",
        "blockedtitle": "Ginpugngan ini nga gumaramit",
        "accmailtext": "Uska hinimo nga random nga tigaman-panakob para kan [[User talk:$1|$1]] in ginpadangat ha $2. Puydi ini mabal-iwan ha ''[[Special:ChangePassword|liwani an tigaman-panakob]]'' nga pakli han paglog-in.",
        "newarticle": "(Bag-o)",
        "newarticletext": "Ginsunod mo an pakli nga waray pa kahihimo.  Para ighimo an pakli, tikanga pagmakinilya ha kahon nga aada ha ubos (kitaa an [$1 nabulig nga pakli] para han kadugangan nga pananabutan).  Kun sayop an imo pagkanhi, igpidlit an imo kanan panngaykay (''browser'') '''balik''' (''back'') nga piridlitan.",
-       "anontalkpagetext": "----\n''Ini in hiruhimangraw-nga-pakli para han waray magpakilala nga gumaramit, nga waray pa hinmimo hin akawnt.''\nMagamit la kami hin IP address para makilal-an hiya.\nSugad hini nga IP address, in puydi sinmaro hiton pipira nga mga gumaramit.\nKun ikaw in waray magpakilala nga gumaramit, ngan pag-abat mo in may mga diri naangay nga komento an ginpapadangat ha imo, alayon nala [[Special:UserLogin/signup|paghimo hin akawnt]] o [[Special:UserLogin|pag-log in]] para malikyan an sumurunod nga mga pagkalipat nga dapat para ha iba nga waray magpakilala nga mga gumaramit.",
+       "anontalkpagetext": "----\n''Ini in hiruhimangraw-nga-pakli para han waray magpakilala nga gumaramit, nga waray pa hinmimo hin akawnt.''\nMagamit la kami hin IP address para makilal-an hiya.\nSugad hini nga IP address, in puydi sinmaro hiton pipira nga mga gumaramit.\nKun ikaw in waray magpakilala nga gumaramit, ngan pag-abat mo in may mga diri naangay nga komento an ginpapadangat ha imo, alayon nala [[Special:CreateAccount|paghimo hin akawnt]] o [[Special:UserLogin|pag-log in]] para malikyan an sumurunod nga mga pagkalipat nga dapat para ha iba nga waray magpakilala nga mga gumaramit.",
        "noarticletext": "Waray yana teksto ha sulod hinin nga pakli.\nPuyde ka [[Special:Search/{{PAGENAME}}|mamiling hin titulo hinin nga pakli]] ha iba pa nga mga pakli,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pamilnga an may mga pagkahisumpay nga mga talaan],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} igliwat ini nga pakli]</span>.",
        "noarticletext-nopermission": "Waray yana nahasurat hini nga pakli\nPuyde hi ikaw [[Special:Search/{{PAGENAME}}|magbiling han ngaran hini nga pakli]] ha iba nga mga pakli,\no <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mamiling han mga nanginginlabot nga mga talaan]</span>, kundi diri ka gintutugotan hin paghímò hini nga pakli.",
        "missing-revision": "Waray na an rebisyon #$1 han pakli nga ginngaranan nga  \"{{FULLPAGENAME}}\".\n\nIni in agsob tungod han pagsunod hin daan nga sumpay hin kaagi ha pakli nga ginpara.\nAn mga detalye in mabibilngan ha [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].",
        "mergehistory-into": "Kakadtoan nga pakli:",
        "mergehistory-submit": "Igtampo an mga rebisyon",
        "mergehistory-empty": "Waray mga rebisyon in puydi matampo.",
+       "mergehistory-fail-bad-timestamp": "Diri puydi an timestamp.",
+       "mergehistory-fail-invalid-source": "Diri puydi an ginkuhaan nga pakli.",
+       "mergehistory-fail-invalid-dest": "Diri puydi an kakadtoan nga pakli.",
+       "mergehistory-fail-self-merge": "Pareho an tinikangan ngan kakadtoan nga mga pakli.",
        "mergehistory-no-source": "Waray pa an tinikangan nga pakli nga $1.",
        "mergehistory-no-destination": "Waray pa an kakadtuan nga pakli nga $1.",
+       "mergehistory-invalid-source": "An tinikangan nga pakli in dapat may-ada valid title.",
+       "mergehistory-invalid-destination": "An kakadtoan nga pakli in kinahanglan may-ada valid title.",
        "mergehistory-autocomment": "Gintampo an [[:$1]] tipakadto ha [[:$2]]",
        "mergehistory-comment": "Gintampo an [[:$1]] ngada ha [[:$2]]: $3",
        "mergehistory-same-destination": "An gintikangan ngan kakadtoan nga mga pakli in diri puydi magkaparo",
        "revertmerge": "Igbulag an gintampo",
        "history-title": "Kaagi han pagbag-o han ''$1''",
        "difference-title": "An pagkakaiba han mga rebisyon han \"$1\"",
+       "difference-title-multipage": "An pagkakaiba ha butnga han mga pakli \"$1\" ngan \"$2\"",
        "difference-multipage": "(Kaibhan ha butnga han mga pakli)",
        "lineno": "Bagis $1:",
        "compareselectedversions": "Igkumpara an mga pinili nga pagbabag-o",
        "prefs-watchlist-token": "Token hin talaan hin barantayon:",
        "prefs-misc": "Dirudilain",
        "prefs-resetpass": "Igliwan an tigaman-pagsulod",
-       "prefs-changeemail": "Igliwan an e-mail address",
+       "prefs-changeemail": "Igliwan o tatanggalon an e-mail address",
        "prefs-setemail": "Igbutang an email address",
        "prefs-email": "Mga pagpipilian han e-mail",
        "prefs-rendering": "Hitsura",
        "yournick": "Bag-o nga pirma:",
        "badsiglength": "Hilaba hin duro it im pirma.\nDapat diri malabaw ha $1 {{PLURAL:$1|agi|mga agi}} nga kahilaba.",
        "yourgender": "Ano an karuyag mo nga pangilal-an?",
-       "gender-unknown": "Karuyag ko diri la magyakan",
+       "gender-unknown": "Kun ikaw in mangangaranan, an software in magamit hin gender neutral words kun mahihimo",
        "gender-male": "Hiya in nag-aayad hin mga wiki nga pakli",
        "gender-female": "Hiya in nag-aayad hin mga wiki nga pakli",
        "email": "E-mail",
-       "prefs-help-realname": "Opsyonal an tinuod nga ngaran.\nKun pilion mo nga ihatag, ini in gagamiton ha paghatag hin atribusyon ha imo mga buhat.",
+       "prefs-help-realname": "Opsyonal an paggamit hin tinuod nga ngaran.\nKun ihatag, ini in puydi magamit paghatag hin pagkilala ha imo mga buhat.",
        "prefs-help-email": "Diri pinipirit it pagbutang hin E-mail address, pero kinahanglan ini para hin pag-utro hin tigaman-hit-pagsulod (''password''), ngan kun mangalimot ka hit imo tigaman-hit-pagsulod.",
        "prefs-help-email-others": "Puydi mo pilion nga it iba in makakontak ha imo gamit an e-mail pinaagi han sumpay ha imo gumaramit o hiruhimangraw nga pakli.\nAn imo e-mail address in diri makikit-an kun an iba nga mga gumaramit in makontak ha imo.",
        "prefs-help-email-required": "Kinahanglanon it e-mail address.",
        "userrights": "Pagdudumara hin mga katungod han gumaramit",
        "userrights-lookup-user": "Pagdumaraa han mga hugpo han gumaramit",
        "userrights-user-editname": "Igbutang an agnay han gumaramit:",
-       "editusergroup": "Igliwat han mga hugpo han gumaramit",
-       "editinguser": "Igliliwat an mga katungod han gumaramit han gumaramit '''[[User:$1|$1]]''' $2",
+       "editusergroup": "Igliwat an mga hugpo han {{GENDER:$1|gumaramit}}",
+       "editinguser": "Ginsasaliwanan an katungod-han-gumaramit ni {{GENDER:$1|gumaramit}} <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "Igliwat an mga hugpo hin gumaramit",
-       "saveusergroups": "Igtipig an mga hugpo han gumaramit",
+       "saveusergroups": "Igtipig an mga hugpo han {{GENDER:$1|gumaramit}}",
        "userrights-groupsmember": "Api han:",
        "userrights-groupsmember-auto": "Api nga daan han:",
        "userrights-reason": "Katadungan:",
        "userrights-notallowed": "Waray nim pagtugot hin pagdugang o pagtanggal hin mga katungod han gumaramit.",
        "userrights-changeable-col": "Mga hugpo nga puydi mo labtan",
        "userrights-unchangeable-col": "Mga hugpo nga diri mo puydi labtan",
-       "userrights-removed-self": "Malinamposon nim gintanggal an imo kalugaringon mga katungod. Tungod hito, diri ka na makaka-access hini nga pakli.",
+       "userrights-removed-self": "Imo gintanggal an imo kalugaringon mga katungod. Tungod hito, diri kana makaka-access hinin nga pakli.",
        "group": "Hugpo:",
        "group-user": "Mga gumaramit",
        "group-autoconfirmed": "Mga gumaramit nga lugaring nakokonpirma",
        "group-suppress": "Mga suppressor",
        "group-all": "(ngatanan)",
        "group-user-member": "{{HENERO:$1|gumaramit}}",
+       "group-autoconfirmed-member": "{{GENDER:$1|autoconfirmed user}}",
        "group-bot-member": "bot",
        "group-sysop-member": "magdudumara",
        "group-bureaucrat-member": "{{GENDER:$1|burokrata}}",
-       "group-suppress-member": "{{GENDER:$1|magmarangno}}",
+       "group-suppress-member": "{{GENDER:$1|suppressor}}",
        "grouppage-user": "{{ns:project}}:Mga gumaramit",
        "grouppage-autoconfirmed": "{{ns:project}}:Mga gumaramit nga naka-awtokompirmado",
        "grouppage-bot": "{{ns:project}}:Mga bot",
        "grouppage-sysop": "{{ns:project}}:Mga magdudumara",
        "grouppage-bureaucrat": "{{ns:project}}:Mga burokrata",
-       "grouppage-suppress": "{{ns:project}}:Nanginginano",
+       "grouppage-suppress": "{{ns:project}}:Suppress",
        "right-read": "Igbasa an mga pakli",
        "right-edit": "Igliwat an mga pakli",
        "right-createpage": "Paghimo hin mga pakli (nga diri an mga hiruhimangraw nga mga pakli)",
        "right-import": "Man-aangbit hin mga pakli tikang ha iba nga mga wiki",
        "right-importupload": "Man-aangbit hin mga pakli tikang ha uska paypay nga iginkarga-pasaka",
        "right-patrol": "Igmarka an kanan iba mga pagliwat komo ginpatrolya na",
+       "right-unwatchedpages": "Pakit-a an lista han mga gintanggal an pagbantay nga mga pakli",
        "right-mergehistory": "Igtampo an kaagi han mga pakli",
        "right-userrights": "Igliwat an ngatanan nga mga katungod han gumaramit",
        "right-userrights-interwiki": "Igliwat an mga katungod han gumaramit han mga gumaramit ha iba nga mga wiki",
        "right-siteadmin": "Igtrangka ngan igrangka an database",
        "right-sendemail": "Padad-i hin e-mail ngada ha iba nga mga gumaramit",
+       "right-passwordreset": "Pakit-a an mga password reset email",
+       "right-deletechangetags": "Igpara an [[Special:Tags|tags]] tikang han database",
+       "grant-generic": "mga katungod nga katitirok han \"$1\"",
+       "grant-group-page-interaction": "Pakig-interact han mga pakli",
+       "grant-group-file-interaction": "Pakig-interact hiton media",
+       "grant-group-watchlist-interaction": "Pakig-interact ha imo mga barantayan",
        "grant-group-email": "Padangat hin email",
        "grant-createaccount": "Pahimo hin mga account",
        "grant-createeditmovepage": "Paghimo, pagliwat, ngan pagbalhin hin mga pakli",
        "grant-delete": "Pagpara hin mga pakli, mga rebisyon, ngan mga iginsulod ha log",
+       "grant-editinterface": "Igliwat an MediaWiki namespace ngan kanan gumaramit CSS/JavaScript",
+       "grant-editmycssjs": "Igliwat an imo gumaramit nga CSS/JavaScript",
+       "grant-editmyoptions": "Igliwat an imo gumaramit nga mga karuyagon",
+       "grant-editmywatchlist": "Igliwat an imo barantayon",
+       "grant-editpage": "Igliwat an aada nga mga pakli",
+       "grant-editprotected": "Igliwat an mga pinansaliporan nga mga pakli",
+       "grant-highvolume": "High-volume nga pagliwat",
+       "grant-oversight": "Igtago an mga gumaramit ngan pag-suppress nga mga rebisyon",
+       "grant-patrol": "Igliwat an pagpatrolya ha mga pakli",
+       "grant-protect": "Igpanalipod ngan igtanggal an mga panalipod han mga pakli",
+       "grant-rollback": "Ig-rollback an mga pagliwat ngadto ha mga pakli",
        "grant-sendemail": "Igpadara hin email ngadto ha iba nga mga gumaramit",
        "grant-uploadeditmovefile": "Pagkarga, pagsaliwan, ngan pagbalhin hin mga file",
        "grant-uploadfile": "Pagkarga hin bag-o nga mga file",
        "action-createpage": "pahimo hin mga pakli",
        "action-createtalk": "Paghimo hin hiruhimangraw nga mga pakli",
        "action-createaccount": "Himoa ini nga akawnt hin gumaramit",
+       "action-autocreateaccount": "automatic nga hihimoon ini nga external user account",
+       "action-history": "kitaa an kaagi hinin nga pakli",
        "action-minoredit": "butanga hin tigaman hinin nga pagliwat komo gutiay",
        "action-move": "balhina ini nga pakli",
        "action-move-subpages": "igbalhin ini nga pakli, ngan iya mga bahin-pakli",
        "action-move-rootuserpages": "Igbalhin an gamot nga mga pakli han gumaramit",
+       "action-move-categorypages": "igbalhin an mga kaarangay nga pakli",
        "action-movefile": "igbalhin ini nga paypay",
        "action-upload": "igkarga-pasaka ini nga paypay",
        "action-reupload": "igsapaw ini nga aanhi nga paypay",
+       "action-reupload-shared": "ig-override ini nga file ha pinagsasaroan nga repositoryo",
        "action-upload_by_url": "igkaraga-pasaka ini nga paypay tikang ha uska URL",
+       "action-writeapi": "gamiti an write API",
        "action-delete": "paraa ini nga pakli",
        "action-deleterevision": "igpara ini nga pagbag-o",
        "action-deletedhistory": "kitaa an kanan hini nga pakli kaagi han mga ginpara",
        "action-browsearchive": "Pamiling hin mga ginpara nga mga pakli",
        "action-undelete": "Balika an ginpara hini nga pakli",
+       "action-suppressrevision": "ig-review ngan ig-restore inin nga nakatago nga rebisyon",
        "action-suppressionlog": "kitaa an kanan hini pribado nga talaan",
        "action-block": "Pugnga ini nga gumaramit ha pagliwat",
        "action-protect": "igsaliwan an katupngan han pananalipod para hini nga pakli",
+       "action-rollback": "dagmiti pag-rollback an mga pagliwat an kataposan nga gumaramit nga nagliwat hit usa ka partikular nga pakli",
        "action-import": "ig-angbit hin mga pakli tikang ha iba nga wiki",
        "action-importupload": "ig-angbit hin mga pakli pakli tikang ha uska ginkarga-pasaka nga paypay",
        "action-patrol": "markahi an kanan iba pagliwat komo nakapatrolya",
+       "action-autopatrol": "ig-marka nga ginpatrolyahan na an imo pagliwat",
+       "action-unwatchedpages": "kitaa an mga lista han gintanggal an pagbantay nga mga pakli",
        "action-mergehistory": "Igtampo an kaagi hini nga pakli",
        "action-userrights": "Igliwat an ngatanan nga mga katungod han gumaramit",
+       "action-userrights-interwiki": "igliwat an mga katungod han mga gumaramit ha iba nga mga wiki",
+       "action-siteadmin": "ig-lock o ig-unlock an database",
        "action-sendemail": "Padara hin mga e-mail",
        "action-editmywatchlist": "igliwat an imo watchlist",
        "action-viewmywatchlist": "kitaa an imo watchlist",
        "action-viewmyprivateinfo": "kitaa an imo pribado nga impormasyon",
        "action-editmyprivateinfo": "igliwat an imo pribado nga impormasyon",
        "action-editcontentmodel": "igliwat an content model han uska pakli",
-       "action-managechangetags": "himua ngan igpara na mga tag tikang ha database",
+       "action-managechangetags": "himoa ngan igpaandar/diri-paganahon an mga tag",
+       "action-applychangetags": "ig-apply an mga tag kaupod an imo mga pagliwat",
        "nchanges": "$1 {{PLURAL:$1|pagbag-o|mga pagbabag-o}}",
        "enhancedrc-history": "kasaysayan",
        "recentchanges": "Mga kabag-ohan",
        "recentchanges-legend-heading": "<strong>Leyenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (kitaa gihapon [[Special:NewPages|talaan han mga bag-o nga pakli]])",
        "recentchanges-submit": "Pakit-a",
-       "rcnotefrom": "An ha ubos in mga pagbabag-o tikang han <strong>$2</strong> (kutob ngadto ha <strong>$1</strong> nga ginpakita).",
+       "rcnotefrom": "Didi ha ubos amo {{PLURAL:$5|an pagbag-o|an mga pagbabag-o}} tikang<strong>$3, $4</strong> (tubtob <strong>$1</strong> nga ginpakita).",
        "rclistfrom": "Pakit-a an mga ginbag-ohan tikang han $3 $2",
        "rcshowhideminor": "$1 gudti nga mga pagliwat",
        "rcshowhideminor-show": "Pakit-a",
        "newpageletter": "B",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[$1 nagbabatay hin {{PLURAL:$1|gumaramit|mga gumaramit}}]",
-       "rc_categories_any": "Bisan ano nga",
+       "rc_categories_any": "Bisan ano nga pinili",
        "rc-change-size-new": "$1 {{PLURAL:$1|nga byte|nga mga byte}} kahuman han pagbag-o",
        "newsectionsummary": "/* $1 */ bag-o nga bahin",
        "rc-enhanced-expand": "Igpakita an detalye",
        "uploadnologintext": "Alayon $1 para han pag-upload han mga file.",
        "uploaderror": "Sayop hit pagkarga-pasaka",
        "upload-recreate-warning": "'''Pahimatngon:  An fayl nga may-ada hiton nga ngaran in ginpara o ginbalhin.'''\n\nAn taramdan han pagpara ngan pagbalhin para hini nga pakli in ginhahatag para han imo kamurayaw:",
-       "upload-permitted": "Gintutugotan nga mga klase han paypay: $1.",
-       "upload-preferred": "Karuyag nga mga tipo hin paypay: $1.",
-       "upload-prohibited": "Gindidire nga mga klase han paypay: $1.",
+       "upload-permitted": "Gintutugotan nga {{PLURAL:$2|klase|mga klase}} nga file: $1.",
+       "upload-preferred": "Mas karuyag nga {{PLURAL:$2|klase|mga klase}} hin file: $1.",
+       "upload-prohibited": "Gindidire nga {{PLURAL:$2|klase|mga klase}} hin file: $1.",
        "uploadlogpage": "Talaan han mga ginkarga-paigbaw",
        "filename": "Ngaran han fayl",
        "filedesc": "Dalikyat nga pulong",
        "filename-toolong": "Iton ngaran hin paypay in diri puyde na mas lapos pa ha 240 ka mga byte.",
        "badfilename": "An ngaran-han-paypay in ginliwat ngada ha \"$1\".",
        "empty-file": "An paypay nga imo ginsumite in waray sulod.",
+       "file-too-large": "An file nga imo ginhatag in sobra kadako.",
        "filename-tooshort": "An ngaran han fayl in halipot hin duro.",
        "filetype-banned": "Ini nga klase nga paypay in gindidire.",
+       "verification-error": "Ini nga pakli in waray nakapasar han file verification.",
+       "hookaborted": "An modipikasyon nga imo gintatalinguha nga himoon in gin-undang hin usa ka extension.",
        "illegal-filename": "An ngaran han fayl in diri gintutugutan.",
        "overwrite": "It pagsapaw han aada nga paypay in diri gintutugotan.",
        "unknown-error": "Nahitabo an waray kasasabti nga sayop.",
        "tmp-write-error": "Sayop ha pagsurat hin temporaryo nga paypay.",
        "large-file": "Ginrerekomenda nga it mga paypay in diri malapos hin $1;\nini nga paypay in $2.",
        "largefileserver": "Ini nga paypay in durudako kaysa ha ginpapakarawat han serbidor.",
+       "emptyfile": "An file nga imo gin-upload in baga waray sulod.\nIni in bangin tungod nagsayop pag-type han filename.\nAlayon kitaa kun imo karuyag gud nimo ig-upload inin nga file.",
        "windows-nonascii-filename": "Ini nga wiki in diri nakasuportado han mga ngaran-han-paypay nga may-ada pinaurog nga mga karakter.",
        "uploadwarning": "Pahimatngon han pagkarga paigbaw",
        "savefile": "Igtipig an paypay",
        "upload-proto-error": "Sayop nga protocol",
        "upload-file-error": "Sayop ha sulod",
        "upload-misc-error": "Waray kasasabti nga sayop hin pagkarga-paigbaw",
+       "upload-too-many-redirects": "An URL in nagsusulod hin damo hin duro nga mga redirect",
        "upload-http-error": "Mayda nahitabo nga sayop hin HTTP: $1",
        "upload-dialog-title": "Ig-upload an file",
        "upload-dialog-button-cancel": "Pasagda",
        "upload-dialog-button-upload": "Upload",
        "upload-form-label-infoform-title": "Mga detalye",
        "upload-form-label-infoform-name": "Ngaran",
+       "upload-form-label-infoform-description": "Deskripsyon",
        "upload-form-label-usage-title": "Paggamit",
        "upload-form-label-usage-filename": "Ngaran han file",
        "upload-form-label-own-work": "Buhat ko ini",
        "backend-fail-read": "Diri nababasahan han paypay nga \"$1\".",
        "backend-fail-create": "Diri nasusuratan an paypay nga \"$1\".",
        "backend-fail-maxsize": "Diri nasusuratan an paypay nga \"$1\" tungod nga mas dako ini kaysa hin {{PLURAL:\"$2|usa nga byte|$2 nga mga byte}}.",
-       "backend-fail-readonly": "An panluyo nga tiripigan nga \"$1\" in ha pagkayana in panbasa-la.  An rason nga ginhatag in: \"''$2''\"",
+       "backend-fail-readonly": "An storage backend nga \"$1\" in ha pagkayana read-only.  An rason nga ginhatag in: <em>$2</em>",
        "backend-fail-connect": "Diri nakakasumpay ha storage backend \"$1\".",
        "lockmanager-notlocked": "Waray ka rangka an \"$1\"; diri ini nakatrangka.",
        "lockmanager-fail-closelock": "Diri nakakasera han nakatrangka nga paypay para han \"$1\".",
        "lockmanager-fail-svr-acquire": "Diri nakakakarawat in mga trangka ha serbidor $1.",
        "lockmanager-fail-svr-release": "Diri nakakabul-iw in mga trangka ha serbidor $1.",
        "zip-wrong-format": "An espisipikado nga paypay in diri naka ZIP nga paypay.",
-       "uploadstash-errclear": "An paghawan han mga paypay in diri malinamposon.",
+       "uploadstash-errclear": "Pakyas an paghawan han mga file.",
        "uploadstash-refresh": "Igpalab-as utro an talaan hin mga paypay",
        "img-auth-accessdenied": "Diri gintutugutan makasulod",
        "img-auth-nofile": "Waray ngada an paypay nga \"$1\".",
+       "img-auth-streaming": "Nag ii-stream \"$1\".",
        "http-read-error": "HTTP maysayop ha pagbasa.",
        "http-timed-out": "Naubosan hin oras ha pagpaalayon ha HTTP.",
        "http-curl-error": "May sayop ha pagkuha hin URL: $1",
        "statistics-users": "Mga [[Special:ListUsers|gumaramit]] nga nakarehistro",
        "statistics-users-active": "Mga gumaramit nga nanggigios",
        "statistics-users-active-desc": "Mga gumaramit nga may-ada iginbuhat ha urhi nga {{PLURAL:$1|ka adlaw|$1 ka mga adlaw}}",
+       "pageswithprop-prop": "Ngaran han propyudad:",
        "pageswithprop-submit": "Kadto-a",
        "doubleredirects": "Mga doble nga redirekta",
        "double-redirect-fixer": "Mangangayad hin redirekta",
        "wantedtemplates": "Mga ginkikinahanglan nga batakan",
        "mostlinked": "Pinakadamo nga mga ginsumpayan nga pakli",
        "mostlinkedcategories": "Pinakadamo nga mga ginsumpayan nga kaarangay",
-       "mostlinkedtemplates": "Pinakadamo nga mga ginsumpayan nga batakan",
+       "mostlinkedtemplates": "Pinakana-transclude nga mga pakli",
        "mostcategories": "Mga paypay nga may-ada pinakadamo nga mga kaarangay",
        "mostimages": "Pinakadamo nga nahisumpayan nga mga paypay",
        "mostinterwikis": "Mga pakli nga may-ada pinakadamo nga mga interwiki",
        "longpages": "Haglaba nga mga pakli",
        "deadendpages": "Waray na kakadtoan nga mga pakli",
        "protectedpages": "Pinapasaliporan nga mga pakli",
+       "protectedpages-timestamp": "Timestamp",
+       "protectedpages-page": "Pakli",
+       "protectedpages-expiry": "Mahuhuman",
+       "protectedpages-performer": "Pinapasaliporan an gumaramit",
+       "protectedpages-params": "Mga parametro han pananalipod",
+       "protectedpages-reason": "Rason",
+       "protectedpages-submit": "Pagdisplay hin mga pakli",
+       "protectedpages-unknown-timestamp": "Waray kasabti",
+       "protectedpages-unknown-performer": "Waray magpasabot nga gumaramit",
        "protectedtitles": "Pinapasaliporan nga mga titulo",
+       "protectedtitles-submit": "Igpakita an mga titulo",
        "listusers": "Lista han mga gumaramit",
        "listusers-editsonly": "Igpakita la an mga gumaramit nga may-ada ginliwat",
        "listusers-creationsort": "Ginsusunodsunod pinaagi han paghimo nga petsa",
+       "listusers-desc": "Ig-ayos ha paubos nga orden",
        "usereditcount": "$1 {{PLURAL:$1|ka pagliwat|ka mga pagliwat}}",
        "usercreated": "{{GENDER:$3|Ginhimo}} han $1 ha $2",
        "newpages": "Bag-o nga mga pakli",
        "nopagetitle": "Waray sugad hito nga kakadtoan nga pakli",
        "pager-newer-n": "{{PLURAL:$1|burubag-o 1|burubag-o $1}}",
        "pager-older-n": "{{PLURAL:$1|durudaan 1|durudaan $1}}",
+       "suppress": "Ig-suppress",
+       "apihelp": "Pabulig hit API",
+       "apihelp-no-such-module": "Waray kahiagii an Module \"$1\"",
+       "apisandbox": "sandbox hit API",
+       "apisandbox-jsonly": "Kinahanglan hin JavaSript para ha paggamit han API sandbox.",
+       "apisandbox-fullscreen": "Igpahilawig an panel",
+       "apisandbox-fullscreen-tooltip": "Igpahilawig an sandbox panel para masudlan an browser window.",
        "apisandbox-unfullscreen": "Igpakita an pakli",
+       "apisandbox-unfullscreen-tooltip": "Igpaguti an sandbox panel, para an MediaWiki navigation link in mahikit-an.",
        "apisandbox-submit": "Paghimo hin request",
        "apisandbox-reset": "Hawana",
        "apisandbox-retry": "Utroha",
+       "apisandbox-loading": "Nagloload hin impormasyon para han API module nga \"$1\"...",
+       "apisandbox-load-error": "May sayop nga nahitabo samtang nagloload hin impormasyon para han API module nga \"$1\": $2",
+       "apisandbox-no-parameters": "Waray mga parametro ini nga API module.",
        "apisandbox-helpurls": "Mga sumpay hit pabulig",
        "apisandbox-examples": "Mga pananglitan",
        "apisandbox-dynamic-parameters": "Dugang nga mga parameter",
        "apisandbox-dynamic-parameters-add-label": "Dugngi hin parameter:",
        "apisandbox-dynamic-parameters-add-placeholder": "Ngaran hit parameter",
        "apisandbox-dynamic-error-exists": "May-ada na nakangaran nga \"$1\" nga parameter.",
+       "apisandbox-deprecated-parameters": "Naka-deprecate nga mga parametro",
        "apisandbox-results": "Mga resulta",
+       "apisandbox-request-url-label": "Ginpapaalayon nga URL:",
+       "apisandbox-request-time": "Oras nga naglabay han pagpaalayon : {{PLURAL:$1|$1 ms}}",
        "booksources": "Mga libro nga tinikangan",
        "booksources-search-legend": "Pamilnga an mga libro nga gintikangan",
        "booksources-search": "Bilnga",
        "specialloguserlabel": "Magburuhat:",
-       "speciallogtitlelabel": "iiguon (titulo o gumarami):",
+       "speciallogtitlelabel": "Hinihingyap nga (titulo o {{ns:user}}:ngaran-han-gumaramit para han gumaramit):",
        "log": "Mga talaan",
+       "logeventslist-submit": "Igpakita",
        "all-logs-page": "Ngatanan nga mga talaan panpubliko",
+       "checkbox-all": "Ngatanan",
+       "checkbox-none": "Waray",
+       "checkbox-invert": "Baliskara",
        "allpages": "Ngatanan nga mga pakli",
        "nextpage": "Sunod nga pakli ($1)",
        "prevpage": "Nahiuna nga pakli ($1)",
        "allpages-hide-redirects": "Igtago an mga redirekta",
        "cachedspecial-refresh-now": "Igkita an pinakaurhi.",
        "categories": "Mga kaarangay",
+       "categories-submit": "Igpakita",
        "categoriesfrom": "Igpakita in mga kaarangay nga natikang ha:",
        "deletedcontributions": "Mga ginpara nga mga ámot hin nágámit",
        "deletedcontributions-title": "Ginpara nga mga amot han nagamit",
        "listusers-noresult": "Waray gumaramit nga nahiagian.",
        "listusers-blocked": "(ginpugngan)",
        "activeusers": "Taramdan hin mga gumaramit nga nanggigios",
+       "activeusers-from": "Igpakita an mga gumaramit tikang ha:",
        "activeusers-hidebots": "Igtago an mga bot",
        "activeusers-hidesysops": "Igtago an mga magdudumara",
        "activeusers-noresult": "Waray gumaramit nga nahiagian.",
+       "activeusers-submit": "Igpakita an mga gumaramit nga nangigios",
        "listgrouprights": "Mga katungod han grupo hin gumaramit",
        "listgrouprights-summary": "An masunod nga uska talaan hin mga grupo hin gumaramit sumala hinin nga wiki, ngan an ira nahisusumpay nga paggamit nga katungod.  Bangin may-ada [[{{MediaWiki:Listgrouprights-helppage}}|dugang nga impormasyon]] mahiunong han indibiduwal nga mga katungod.",
        "listgrouprights-key": "Leyenda:\n* <span class=\"listgrouprights-granted\">Gintagan hin katungod</span>\n* <span class=\"listgrouprights-revoked\">Waray ginhatag an katungod</span>",
        "listgrouprights-removegroup-self": "Igtanggal an {{PLURAL:$2|grupo|mga grupo}} tikang ha kalugaringon nga akawnt: $1",
        "listgrouprights-addgroup-self-all": "Igdugang an ngatanan nga mga grupo ha kalugaringon nga akawnt",
        "listgrouprights-removegroup-self-all": "Igtanggal an ngatanan nga mga grupo tikang ha kalugaringon nga akawnt",
+       "listgrouprights-namespaceprotection-namespace": "Ngaran-lat'ang",
+       "listgrouprights-namespaceprotection-restrictedto": "(Mga) katungod han pagtugot ha gumaramit nga pagliwat",
+       "listgrants": "Mga iginhatag",
+       "listgrants-grant": "Iginhatag",
        "listgrants-rights": "Mga katungod",
        "trackingcategories": "Ginsusubay an mga kategorya",
+       "trackingcategories-msg": "Ginsusubay an kaarangay",
        "trackingcategories-name": "Ngaran han mensahe",
        "mailnologin": "Waray kakadtoan nga address",
        "mailnologintext": "Kinahanglan nimo nga [[Special:UserLogin|nakalog-in]] ngan may-ada balido nga email address ha imo[[Special:Preferences|mga preperensya]] para makapadangat hin email ngadto ha iba nga mga gumaramit.",
        "notanarticle": "Diri uska unod nga pakli",
        "notvisiblerev": "An urhi nga pagliwat han iba nga gumaramit in ginpara",
        "watchlist-details": "{{PLURAL:$1|$1 nga pakli|$1 nga mga pakli}} nga aada ha imo talaan nga binabantayan, diri bulag nga paglakip han mga hiruhimangraw-nga-pakli.",
-       "wlshowlast": "Igpakita an katapusan nga $1 nga mga oras $2 nga mga adlaw",
+       "wlshowlast": "Igpakita an katapusan nga $1 ka mga oras $2 ka mga adlaw",
        "watchlist-hide": "Tago-a",
        "watchlist-submit": "Pakit-a",
+       "wlshowtime": "Kaiha han oras ha pagdisplay:",
        "wlshowhideminor": "gudti nga mga pagliwat",
        "wlshowhidebots": "Mga bot",
        "wlshowhideliu": "Mga nakarehistro nga gumaramit",
        "wlshowhideanons": "Mga waray magpakilala nga gumaramit",
        "wlshowhidepatr": "Nakapatrolya na nga mga pagliwat",
        "wlshowhidemine": "ako mga pagliwat",
+       "wlshowhidecategorization": "Kategorisasyon han pakli",
        "watchlist-options": "Mga pirilian han talaan han binabantayan",
        "watching": "Ginbabantay...",
        "unwatching": "Diri na ginbabantay...",
        "deletepage": "Igpara an pakli",
        "confirm": "Kompirma",
        "excontent": "An sulod in: ''$1''",
-       "excontentauthor": "an sulod in: ''$1'' (ngan hi \"[[Special:Contributions/$2|$2]]\" la an nag-amot)",
+       "excontentauthor": "An nasusulod in: ''$1'', ngan hi \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|pakighimangraw]]) la an nag-amot",
        "exbeforeblank": "sulod san-o paghawan in: \"$1\"",
        "delete-confirm": "Igpara \"$1\"",
        "delete-legend": "Igpara",
+       "historyaction-submit": "Igpakita",
        "actioncomplete": "Malinampuson an ginbuhat",
        "actionfailed": "Napakyas an ginbuhat",
        "deletedtext": "Ginpara an \"$1\".\nKitaa an $2 para hin talaan han mga gibag-ohi nga mga ginpamara.",
        "deletereasonotherlist": "Lain nga katadungan",
        "deletereason-dropdown": "*Agsob nga rason hin pagpara\n** Spam\n** Bandalismo\n** Pagtalapas ha katungod hin pagtatag-iya (''copyright'')\n** Tugon han manunurat\n** Utod nga redirek",
        "delete-edit-reasonlist": "Igliwat an mga rason han pagpara",
+       "deleteprotected": "Diri nimo mapapara ini nga pakli tunod ini in ginpasaliporan.",
        "rollback": "Mga libot-pabalik nga pagliwat",
        "rollbacklink": "libot-pabalik",
        "rollbacklinkcount": "rollback $1 {{PLURAL:$1|ka pagliwat|ka mga pagliwat}}",
+       "rollbacklinkcount-morethan": "Igrollback hin labaw han $1 nga {{PLURAL:$1|pagliwat|mga pagliwat}}",
        "rollbackfailed": "Diri malinamposon an paglibot-pabalik",
+       "cantrollback": "Diri makakapabalik han pagliwat;\nan urhi nga nag-amot in amo la an awtor hinin nga pakli.",
+       "editcomment": "An halipotay nga masisiring hiunong han pagliwat in: <em>$1</em>.",
        "revertpage": "Ginpabalik an ginliwat ni [[Special:Contributions/$2|$2]] ([[User talk:$2|hiruhimangraw]]) ngadto ha urhi nga pagliwat ni [[User:$1|$1]]",
        "sessionfailure-title": "Pakyas an sesyon",
+       "changecontentmodel-title-label": "Titulo han pakli",
        "changecontentmodel-reason-label": "Rason:",
+       "changecontentmodel-submit": "Balyo-a",
        "protectlogpage": "Talaan han pinasaliporan",
        "protectedarticle": "pinasaliporan \"[[$1]]\"",
        "prot_1movedto2": "[[$1]] in ginbalhin ngadto ha [[$2]]",
        "restriction-move": "Balhina",
        "restriction-create": "Himo-a",
        "restriction-upload": "Igkarga-pasaka",
+       "restriction-level-sysop": "bug-os nga pinasaliporan",
+       "restriction-level-autoconfirmed": "tunga-tunga nga pinasaliporan",
        "restriction-level-all": "bisan ano nga katupngan",
        "undelete": "Igpakita an mga ginpara nga mga pakli",
+       "undeletepage": "Igpakita ngan igbalik-ha-pagkawara an mga pinara nga pakli",
+       "undeletepagetitle": "<strong>An masunod in nag-uupod hin mga pinara nga mga rebisyon han [[:$1|$1]]</strong>.",
+       "viewdeletedpage": "Igpakita an mga pinara nga pakli",
        "undeletelink": "igpakita/igbalik",
        "undeleteviewlink": "kitaa",
        "undeletecomment": "Katadungan:",
        "whatlinkshere-prev": "{{PLURAL:$1|nahiuna|nahiuna $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|masunod|masunod $1}}",
        "whatlinkshere-links": "← mga sumpay",
-       "whatlinkshere-hideredirs": "$1 nga mga redirek",
-       "whatlinkshere-hidetrans": "$1 nga mga transklusyon",
-       "whatlinkshere-hidelinks": "$1 an mga sumpay",
-       "whatlinkshere-hideimages": "$1 an mga sumpay han paypay",
+       "whatlinkshere-hideredirs": "Igtago an mga redirect",
+       "whatlinkshere-hidetrans": "Igtago an mga tranclusion",
+       "whatlinkshere-hidelinks": "Igtago an mga sumpay",
+       "whatlinkshere-hideimages": "Igtago an mga sumpay han file",
        "whatlinkshere-filters": "Mga panara",
        "whatlinkshere-submit": "Kadto-a",
        "block": "Pugngi an gumaramit",
-       "blockip": "Pugngi an gumaramit",
+       "blockip": "Pugngi an{{GENDER:$1|gumaramit}}",
        "blockip-legend": "Pugngi an gumaramit",
        "ipaddressorusername": "IP address o agnay-hit-gumaramit:",
        "ipbexpiry": "Matitima an dulot:",
        "movenotallowedfile": "Waray ka pagtugot para makabalhin hin mga paypay.",
        "cant-move-user-page": "Diri ka gintutugotan pagbalhin hin mga pakli nga gumaramit (labot la tikang ha mga bahin-pakli).",
        "cant-move-to-user-page": "Diri ka gintutugotan pagbalhin hin uska pakli pakada ha uska pakli hin gumaramit (labot la pakadto ha usa nga bahin-pakli han gumaramit).",
-       "newtitle": "Para ha bag-o nga titulo:",
+       "cant-move-category-page": "Waray ka pagtugot hin pagbalhin han mga kaarangay nga pakli.",
+       "cant-move-to-category-page": "Waray ka pagtugot pagbalhin hin pakli ngada ha kaarangay nga pakli.",
+       "newtitle": "Bag-o nga titulo:",
        "move-watch": "Kitaa an tinikangan nga pakli ngan kakadtoan nga pakli",
        "movepagebtn": "Igbalhin an pakli",
        "pagemovedsub": "Malinamposon an pagbalhin",
        "movepage-moved": "'''\"$1\" in ginbalhin ngadto ha \"$2\"'''",
        "movepage-moved-redirect": "Nahimo an uska redirect.",
+       "movepage-moved-noredirect": "An paghimo hin redirect in nai-suppress.",
+       "articleexists": "May-ada na hiton nga ngaran nga pakli, o an ngaran nga imo pinili in diri balido.\nAlayon pagpili hin iba nga ngaran.",
+       "cantmove-titleprotected": "Didi ka makakabalhin hin pakli ngadi nga lokasyon tungod an bag-o nga titulo in pinasaliporan tikang ha paghimo.",
+       "movetalk": "Igbalhin an may pagkahisumpay nga pakighimangraw-nga-pakli",
        "move-subpages": "Balhina an mga bahin-pakli (tubtob ngadto ha $1)",
        "move-talk-subpages": "Balhina an mga bahin-pakli han pakli han hiruhimangraw (tubtob ngadto ha $1)",
        "movepage-page-exists": "An pakli nga $1 in aada na ngan diri ini lugaring nga masasapawan pagsurat.",
        "movepage-page-unmoved": "An pakli nga $1 in diri mababalhin ngadto ha $2.",
        "movelogpage": "Talaan han pagbalhin",
        "movelogpagetext": "Ubos hini in uska talaan han ngatanan nga nabalhin nga pakli",
+       "movesubpage": "{{PLURAL:$1|Ubos-pakli|Mga ubos-pakli}}",
+       "movesubpagetext": "Ini nga pakli in may-ada $1 nga {{PLURAL:$1|ubos-pakli|mga ubos-pakli}} nga ginpapakita ha ubos.",
        "movenosubpage": "Ini nga pakli in waray mga bahin-pakli.",
        "movereason": "Rason:",
        "revertmove": "igbalik",
+       "delete_and_move_text": "An destinasyon nga pakli nga \"[[:$1]]\" in may-ada na.\nKaruyag mo nga igpara ini para maghatag hin paagi para hini nga pagbalhin?",
        "delete_and_move_confirm": "Oo, paraa an pakli",
        "delete_and_move_reason": "Ginpara para makahatag hin aragian para makabalhin tikang ha \"[[$1]]\"",
        "selfmove": "An tinikangan ngan kakadtoan nga mga titulo in parehas;\ndiri makakabalhin ha iya kalugaringon ngahaw.",
        "immobile-target-namespace-iw": "An sumpay interwiki in diri balido nga irig-on para han pagbalhin hin pakli.",
        "immobile-source-page": "Diri mababalhin ini nga pakli.",
        "immobile-target-page": "Diri makakabalhin ha ngada nga kakadtoon nga titulo.",
+       "imagenocrossnamespace": "Diri nakakabalhin hin file ngadto ha non-file namespace.",
+       "nonfile-cannot-move-to-file": "Diri nakakabalhin hin non-file ngadto ha file namespace.",
        "imageinvalidfilename": "An kakadtoon nga ngaran-han-paypay in diri balido",
        "move-leave-redirect": "Pagbilin hin redirect",
        "export": "Mga pakli hit pagexport",
        "importbadinterwiki": "Maraot nga sumpay hit interwiki",
        "importsuccess": "Natapos an pag-aangbit!",
        "import-noarticle": "Waray pakli nga maaangbit!",
-       "import-token-mismatch": "Nawara an datos sesyon.\nAlayon utroha.",
-       "import-error-edit": "An pakli nga \"$1\" in waray naangbit tungod nga diri ka gintutugotan pagliwat hini.",
-       "import-error-create": "An pakli nga \"$1\" in waray naangbit tungod nga diri ka gintutugotan paghimo hini.",
-       "import-error-interwiki": "An pakli nga \"$1\" in waray naangbit tungod nga an ngaran in nakareserba para han pagsusumpay ha gawas (interwiki).",
-       "import-error-special": "An pakli nga \"$1\" in waray naangbit tungod nga nahihilakip ini ha uska pinaurog nga ngaran-lat'ang nga diri natugot hin mga pakli.",
-       "import-error-invalid": "An pakli nga \"$1\" in waray naangbit tungod nga it iya ngaran in diri balido.",
+       "import-token-mismatch": "Nawara an datos sesyon.\n\n\nBangin ka naglog-out na. <strong>Alayon igberipika kun ikaw in nakalog-in pa ngan utro buhata</strong>. Kun diri pa iton nagana, alayon [[Special:UserLogout|pag-log out]] ngan paglog-in balik, ngan kitaa an imo browser kun natugot hin cookies tikang hinin nga site.",
+       "import-error-edit": "An pakli nga \"$1\" in waray napaangbit tungod nga diri ka gintutugotan pagliwat hini.",
+       "import-error-create": "An pakli nga \"$1\" in waray napaangbit tungod nga diri ka gintutugotan paghimo hini.",
+       "import-error-interwiki": "An pakli nga \"$1\" in waray napaangbit tungod nga an ngaran in nakareserba para han external linking (interwiki).",
+       "import-error-special": "An pakli nga \"$1\" in waray napaangbit tungod nga nahihilakip ini ha usa ka special namespace nga diri natugot hin mga pakli.",
+       "import-error-invalid": "An pakli nga \"$1\" in waray napaangbit tungod nga an ngaran kun diin inin ig-aangbit in diri balido para hinin nga wiki.",
        "import-options-wrong": "Sayop {{PLURAL:$2|nga pirilion|nga mga pirilion}}: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "An ginhatag nga gamot-pakli in uska diri balido nga titulo.",
        "import-rootpage-nosubpage": "Ngaran-lat'ang nga \"$1\" han gamot-pakli in diri natugot hin mga bahin-pakli.",
        "tooltip-feed-rss": "RSS nga pangarga para hini nga pakli",
        "tooltip-feed-atom": "Atom nga pangarga para hini nga pakli",
        "tooltip-t-contributions": "Kitaa an listahan hin mga amot {{GENDER:$1|hinin nga gumaramit}}",
-       "tooltip-t-emailuser": "Padad-i hin e-mail ini nga nágámit",
+       "tooltip-t-emailuser": "Padad-i hin e-mail ngadto {{GENDER:$1|hinin nga gumaramit}}",
        "tooltip-t-upload": "Pagkarga hin mga paypay",
        "tooltip-t-specialpages": "Talaan hin mga pinaurog nga pakli",
        "tooltip-t-print": "Maipapatik nga bersyon hini nga pakli",
        "tooltip-compareselectedversions": "Kitaa an mga kaibhan ha butnga han duha nga pinili nga mga pagliwat hini nga pakli",
        "tooltip-watch": "Dugnga ini nga pakli ngadto han imo talaan hin ginbibinantayan",
        "tooltip-watchlistedit-normal-submit": "Igtanggal an mga titulo",
+       "tooltip-watchlistedit-raw-submit": "Ig-update an barantayon",
        "tooltip-recreate": "Utroha paghimo an pakli bisan ini in ginpara na",
        "tooltip-upload": "Tikanga an pagkarga-pasaka",
        "tooltip-rollback": "An \"libot-pabalik\" in nabalik han (mga) pagliwat hini nga pakli ngadto han kataposan nga nag-amot hin usa ka pidlit",
        "tooltip-preferences-save": "Tipiga an mga karuyag",
        "tooltip-summary": "Pagbutang hin halipotay nga masisiring hiton pagliwat",
        "interlanguage-link-title": "$1 – $2",
+       "anonymous": "Waray magpakilala nga {{PLURAL:$1|gumaramit|mga gumaramit}} han {{SITENAME}}",
        "siteuser": "{{SITENAME}} gumaramit $1",
        "anonuser": "{{SITENAME}} waray nagpakilala nga gumaramit $1",
+       "lastmodifiedatby": "Ini nga pakli in urhi ginliwat $2, $1 ni $3.",
        "othercontribs": "Ginbasihan ha binuhat ni $1.",
        "others": "mga iba",
-       "siteusers": "{{SITENAME}} {{PLURAL:$2|gumaramit|mga gumaramit}} $1",
+       "siteusers": "{{SITENAME}} {{PLURAL:$2|{{GENDER:$1|gumaramit}}|mga gumaramit}} $1",
+       "anonusers": "{{SITENAME}} waray magpakilala nga {{PLURAL:$2|gumaramit|mga gumaramit}} $1",
+       "creditspage": "Pagkilala ha pakli",
+       "nocredits": "Waray pagkilala nga impormasyon para hinin nga pakli",
+       "spamprotectiontitle": "Filter para pag-iwas hit spam",
+       "spamprotectiontext": "An teksto nga karuyag nimo igtipig in ginpugong han spam filter.\nIni posible nahitabo tungod han sumpay ngadto ha usa nga blacklisted nga external site.",
+       "spamprotectionmatch": "An masunod nga teksto amo an nagpagana han amon spam filter: $1",
+       "spambot_username": "Paglimpyo han MediaWiki spam",
+       "spam_reverting": "Ginbabalik ha urhi nga rebisyon nga waray lakip nga sumpay ngadto ha $1",
+       "spam_blanking": "Ngatan nga mga rebisyon nga may-ada sumpay ngadto ha $1, ginhahawan",
+       "spam_deleting": "Ngatanan nga mga rebisyon nga naglalakip hin sumpay ngadto ha $1, ginpapara",
        "simpleantispam-label": "Anti-spam check.\n<strong>Ayaw</strong> pagbinutangi dinhi!",
        "pageinfo-title": "Impormasyon para \"$1\"",
        "pageinfo-not-current": "Pasaylo-a, imposible makahatag hin impormasyon hiunong han mga daan nga rebisyon.",
        "pageinfo-header-basic": "Panguna nga pananabotan",
        "pageinfo-header-edits": "Kaagi han pagliwat",
        "pageinfo-header-restrictions": "Panalipod han pakli",
+       "pageinfo-header-properties": "Mga propyudad han pakli",
        "pageinfo-display-title": "Iglatag an titulo",
+       "pageinfo-default-sort": "Default sort key",
        "pageinfo-length": "Kahilaba han pakli (ha mga byte)",
        "pageinfo-article-id": "ID han pakli",
+       "pageinfo-language": "An ginagamit nga pinulongan han pakli",
+       "pageinfo-content-model": "An gingagamit nga modelo han pakli",
        "pageinfo-robot-policy": "Pag-index hin mga robot",
        "pageinfo-robot-index": "Gintutugot",
        "pageinfo-robot-noindex": "Dírì gintutugot",
        "pageinfo-watchers": "Ihap han nangingita hin pakli",
+       "pageinfo-visiting-watchers": "An kadamo han mga nagbabantay han pakli nga nagduaw ha mga bag-o nga pagliwat",
+       "pageinfo-few-watchers": "Guruguti han $1 nga {{PLURAL:$1|nagbabantay|mga nagbabantay}}",
+       "pageinfo-few-visiting-watchers": "Bangin may-ada o waray nagbabantay nga gumaramit nga nagduduaw han mga bag-o nga pagliwat",
        "pageinfo-redirects-name": "Ihap hin mga redirek ngani nga pakli",
        "pageinfo-subpages-name": "Mga bahinpakli hin nga pakli",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|redirekta|mga redirekta}}; $3 {{PLURAL:$3|diri redirekta|mga diri redirekta}})",
        "pageinfo-lastuser": "Giurhii nga nagliwat",
        "pageinfo-lasttime": "Petsa han kataposan nga pagliwat",
        "pageinfo-edits": "Ngatanan nga ihap han mga pakli",
+       "pageinfo-authors": "An kadamo han mga awtor",
+       "pageinfo-recent-edits": "An kadamo han mga bag-o nga mga pakli (ha sulod han naglabay nga $1)",
+       "pageinfo-recent-authors": "An kadamo han awtor nga nagliwat pala",
+       "pageinfo-magic-words": "Magic {{PLURAL:$1|word|words}} ($1)",
+       "pageinfo-hidden-categories": "Nakatago nga {{PLURAL:$1|kaarangay|mga kaarangay}} ($1)",
+       "pageinfo-templates": "Naka-transclude nga {{PLURAL:$1|batakan|mga batakan}} ($1)",
+       "pageinfo-transclusions": "{{PLURAL:$1|Pakli|Mga pakli}} nga naka-transclude ha ($1)",
        "pageinfo-toolboxlink": "Impormasyon han pakli",
        "pageinfo-redirectsto": "Igredirect ngadto ha",
        "pageinfo-redirectsto-info": "info",
        "show-big-image-preview": "Kadako hin nga pahiuna nga pagawas: $1.",
        "show-big-image-other": "Iba {{PLURAL:$2|nga resolusyon|nga mga resolusyon}}: $1.",
        "show-big-image-size": "$1 × $2 nga mga pixel",
+       "file-info-gif-looped": "naka-loop",
+       "file-info-png-looped": "naka-loop",
+       "file-info-png-frames": "$1 {{PLURAL:$1|frame|mga frame}}",
+       "file-no-thumb-animation": "<strong>Pasabot: Tungod hin limitasyon pan-teknikal\n, an mga thumbnail hinin nga file in diri naka-animate.</strong>",
+       "file-no-thumb-animation-gif": "<strong>Pasabot: Tungod hin limitasyon pan-teknikal\n, an mga thumbnail nga may-ada hitaas nga resolusyon nga GIF images sugad hini in diri naka-animate.</strong>",
        "newimages": "Galeryia hin mga paypay nga bag-o",
+       "imagelisttext": "Ngadi ubos in lista hin  <strong>$1</strong> ka {{PLURAL:$1|file|mga file}} nga naka-sort $2.",
+       "newimages-summary": "Ini nga ispisyal nga pakli in nagpapakita hin kataposan nga gin-upload nga mga file.",
        "newimages-legend": "Panara",
        "newimages-label": "Ngaran han paypay (o uska bahin hini):",
+       "newimages-showbots": "Igpakita an upload han mga bot",
+       "newimages-hidepatrolled": "Igtago an nakapatrolya nga mga upload",
        "noimages": "Waray makikit-an.",
        "ilsubmit": "Bilnga",
        "bydate": "pinaagi han petsa",
+       "sp-newimages-showfrom": "Igpakita an mga bag-o nga file tikang $2, $1",
+       "seconds": "{{PLURAL:$1|$1 ka segundo|$1 ka mga segundo}}",
+       "minutes": "{{PLURAL:$1|$1 ka minuto|$1 ka mga minuto}}",
+       "hours": "{{PLURAL:$1|$1 ka oras|$1 ka mga oras}}",
+       "days": "{{PLURAL:$1|$1 ka adlaw|$1 ka mga adlaw}}",
+       "weeks": "{{PLURAL:$1|$1 ka semana|$1 ka mga semana}}",
+       "months": "{{PLURAL:$1|$1 ka bulan|$1 ka mga bulan}}",
+       "years": "{{PLURAL:$1|$1 ka tuig|$1 ka mga tuig}}",
        "ago": "$1 an nakalabay",
        "just-now": "yana pala",
+       "hours-ago": "{{PLURAL:$1|$1 ka oras|$1 ka mga oras}} nga naglabay",
+       "minutes-ago": "{{PLURAL:$1|$1 ka minuto|$1 ka mga minuto}} nga naglabay",
+       "seconds-ago": "{{PLURAL:$1|$1 ka segundo|$1 ka mga segundo}} nga naglabay",
        "monday-at": "Lunes ha $1",
        "tuesday-at": "Martes ha $1",
        "wednesday-at": "Miyerkules ha $1",
        "bad_image_list": "An kabutangan in masunod:\n\nAn nakatalala la nga mga butang (mga bagis nga nagtitikang hin *) in mahiuupod paglabot.\nAn syahan nga sumpay ha uska bagis in dapat may-ada sumpay ngadto ha maraot nga fayl.\nAn bisan ano nga masunod nga mga sumpay ha kapareho nga bagis in igtratrato nga eksepsyon, sugad hin, mga pakli kun diin an mga fayl in puydi mabubutang ha sulod han bagis.",
        "metadata": "Metadata",
        "metadata-help": "Iní nga paypay mayda dugang nga pagpasabot, nga bangin gindugáng tikang han digital nga camera o iskaner nga gin-gamit paghimo o pag-digitar hini.\nKon an paypay ginliwat tikang han orihinal nga kamutangan, mayda mga detalye nga bangin diri magpakita han ginliwat nga paypay",
+       "metadata-expand": "Igpakita an mga pinahilawig nga detalye",
+       "metadata-collapse": "Igtago an mga pinahilawig nga detalye",
        "metadata-fields": "An mga rumbay han hulagway han metadato nga nakatala dinhi nga mensahe in iglalakip ha padayag hin hulagway nga pakli kun an taramdan metadato in nakalukot.\nAn iba in daan nakatago.\n* make\n* modelo\n* pitsaorasorihinal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpsngalatitud\n* gpsngalongitud\n* gpsngaaltitud",
        "exif-imagewidth": "Kahaluag",
        "exif-imagelength": "Kahitaas",
+       "exif-bitspersample": "Mga bit kada komponente",
        "exif-orientation": "Orientation",
        "exif-ycbcrpositioning": "Pagpoposisyon han Y ngan C",
        "exif-xresolution": "Resolusyon horizontal",
        "exif-copyrighted": "Kahimtang han copyright",
        "exif-copyrightowner": "Tag-iya han copyright",
        "exif-usageterms": "Mga termino hit paggamit",
+       "exif-copyrighted-true": "Naka-copyright",
        "exif-copyrighted-false": "Status hin katungod-hin-panag-iya waray mahabutang",
        "exif-unknowndate": "Waray kasabti an petsa",
        "exif-orientation-1": "Normal",
+       "exif-orientation-3": "Igpalibot hin 180°",
+       "exif-orientation-4": "Igpalibot patukdaw",
+       "exif-orientation-5": "Igpalibot hin 90° CCW ngan igpalibot patukdaw",
+       "exif-orientation-6": "Igpalibot hin 90° CCW",
+       "exif-orientation-7": "Igpalibot hin 90° CW ngan igpalibot patukdaw",
+       "exif-orientation-8": "Igpalibot hin 90° CW",
        "exif-exposureprogram-1": "Mano-mano",
+       "exif-exposureprogram-2": "Normal nga progama",
        "exif-subjectdistance-value": "$1 ka mga metro",
        "exif-meteringmode-0": "Waray kasabti",
        "exif-meteringmode-255": "iba",
        "confirm-watch-top": "Dudugngon ini nga pakli ngadto han imo talaan hin ginbibinantayan?",
        "confirm-unwatch-button": "OK",
        "confirm-unwatch-top": "Tatanggalon ini nga pakli tikang han imo tala hin binabantayan?",
+       "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← naha-una nga pakli",
        "imgmultipagenext": "sunod nga pakli →",
        "imgmultigo": "Pakadto!",
        "imgmultigoto": "Pakadto ha pakli $1",
+       "img-lang-go": "Kadto",
        "ascending_abbrev": "pasaka",
        "descending_abbrev": "paubos",
        "table_pager_next": "Sunod nga pakli",
        "autosumm-replace": "Ginsaliwanan an sulod hin \"$1\"",
        "autoredircomment": "Ginredirecta an pakli ngada ha [[$1]]",
        "autosumm-new": "Nahimo an pakli nga may \"$1\"",
+       "autosumm-newblank": "Paghimo hin blangko nga pakli",
        "size-bytes": "$1 nga B",
        "size-kilobytes": "$1 nga KB",
        "size-megabytes": "$1 nga MB",
        "size-gigabytes": "$1 nga GB",
+       "lag-warn-normal": "An mga pagbabag-o nga burubag-o han $1 ka {{PLURAL:$1|segundo|mga segundo}} in diri maipapakita dinhi nga lista.",
        "watchlistedit-normal-title": "Igliwat an talaan han binabantayan",
        "watchlistedit-normal-legend": "Igtanggal an mga titulo tikang ha talaan hit binabantayan",
        "watchlistedit-normal-submit": "Igtanggal an mga titulo",
        "watchlistedit-raw-done": "Ginpayana an imo talaan han barantayon.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 nga titulo|$1 nga mga titulo}} in gindugang:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 nga titulo|$1 nga mga titulo}} in gintanggal:",
+       "watchlistedit-clear-titles": "Mga titulo:",
+       "watchlistedit-clear-submit": "Hawana an barantayon nga listahan (Permamente ini!)",
        "watchlisttools-view": "Kitaa an mga nanginginlabot nga mga pagbabag-o",
        "watchlisttools-edit": "Kitaa ngan igliwat an talaan han binabantayan",
        "watchlisttools-raw": "Igliwat an hilaw nga talaan han binabantayan",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|hiruhimangraw]])",
+       "timezone-local": "Lokal",
        "duplicate-defaultsort": "'''Pahimatngon:''' An daan-aada nga paglainlain nga piridlitan nga \"$2\" in igsasapaw an durudaan nga daan-aada nga paglainlain nga piridlitan nga \"$1\".",
        "version": "Bersyon",
-       "version-skins": "Mga panit",
+       "version-skins": "Mga panit nga naka-install",
        "version-specialpages": "Mga pinaurog nga pakli",
        "version-parserhooks": "Mga kawil parser",
        "version-variables": "Mga variable",
        "version-hook-name": "Ngaran han kawil",
        "version-version": "($1)",
        "version-license": "MediaWiki nga Lisensya",
+       "version-ext-license": "Lisensya",
+       "version-skin-colheader-name": "Panit",
+       "version-ext-colheader-version": "Bersyon",
+       "version-ext-colheader-license": "Lisensya",
+       "version-ext-colheader-description": "Deskripsyon",
+       "version-ext-colheader-credits": "Mga awtor",
+       "version-license-title": "Lisensya para han $1",
+       "version-license-not-found": "Waray detalye han impormasyon han lisensya nga mahihiagian para hinin nga ekstensyon.",
+       "version-credits-title": "Pagkilala para kan $1",
+       "version-credits-not-found": "Waray detalye nga mga pagkilala nga impormasyon an mahihiagian para hinin nga ekstensyon.",
        "version-poweredby-credits": "Ini nga wiki in pinapaandar han '''[https://www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
        "version-poweredby-others": "mga iba",
+       "version-poweredby-translators": "mga maghuhubad han translatewiki.net",
+       "version-credits-summary": "Gusto namon ngaranon an masunod nga mga tawo tungod han ira mga amot ha [[Special:Version|MediaWiki]].",
        "version-software-product": "Produkto",
        "version-software-version": "Bersyon",
        "version-entrypoints": "Surudlan nga mga URL",
        "version-entrypoints-header-entrypoint": "Surudlan",
        "version-entrypoints-header-url": "URL",
+       "version-libraries-version": "Bersyon",
+       "version-libraries-license": "Lisensya",
+       "version-libraries-description": "Deskripsyon",
+       "version-libraries-authors": "Mga awtor",
+       "redirect-submit": "Kadtoa",
+       "redirect-file": "Ngaran han file",
        "fileduplicatesearch": "Pamiling hin nadoble nga mga paypay",
        "fileduplicatesearch-filename": "Ngaran han paypay:",
        "fileduplicatesearch-submit": "Pamilnga",
        "fileduplicatesearch-noresults": "Waray nabilngan nga paypay nga an ngaran in \"$1\".",
        "specialpages": "Mga pinaurog nga pakli",
+       "specialpages-note-top": "Leyenda",
        "specialpages-group-maintenance": "Mga sumat han pagmintinar",
        "specialpages-group-other": "Mga iba nga pinaurog nga pakli",
        "specialpages-group-login": "Magpalista nga masakob / paghimo hin bag-o nga akawnt",
        "tag-filter": "[[Special:Tags|Tag]] panara:",
        "tag-filter-submit": "Panara",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ka tag|ka mga tag}}]]: $2)",
+       "tags-title": "Mga tag",
+       "tags-tag": "Ngaran han tag",
+       "tags-source-header": "Tinikangan",
+       "tags-active-header": "Nagios?",
+       "tags-actions-header": "Mga buhat",
+       "tags-active-yes": "Oo",
+       "tags-active-no": "Diri",
        "tags-edit": "igliwat",
+       "tags-delete": "paraa",
        "tags-hitcount": "$1 {{PLURAL:$1|nga pagbag-o|nga mga pagbag-o}}",
+       "tags-create-reason": "Rason:",
+       "tags-create-submit": "Himoa",
+       "tags-delete-title": "Igpara an tag",
+       "tags-delete-reason": "Rason:",
+       "tags-activate-reason": "Rason:",
+       "tags-deactivate-reason": "Rason:",
+       "tags-edit-new-tags": "Mga bag-o nga tag:",
+       "tags-edit-add": "Dugngi ini nga mga tag:",
+       "tags-edit-remove": "Tanggala ini nga mga tag:",
+       "tags-edit-remove-all-tags": "(tanggala an ngatanan nga mga tag)",
+       "tags-edit-chosen-placeholder": "Pilia an pipira nga mga tag",
+       "tags-edit-chosen-no-results": "Waray tag nga kaparehas",
+       "tags-edit-reason": "Rason:",
        "comparepages": "Igkumpara an mga pakli",
        "compare-page1": "Pakli 1",
        "compare-page2": "Pakli 2",
        "logentry-newusers-autocreate": "An gumaramit nga akawnt nga $1 in lugaring nga {{GENDER:$2|ginhimo}}",
        "logentry-upload-upload": "Hi $1 {{GENDER:$2|gin-upload}} an $3",
        "rightsnone": "(waray)",
+       "feedback-back": "Balik",
        "feedback-cancel": "Pasagdi",
        "feedback-close": "Human na.",
+       "feedback-error-title": "Sayop",
        "feedback-error2": "Sayop: Pakyas an pagliwat",
        "feedback-message": "Mensahe:",
        "feedback-subject": "Himangrawon:",
+       "feedback-thanks-title": "Salamat!",
        "searchsuggest-search": "Pamilnga",
        "searchsuggest-containing": "nagsusulod. . .",
        "api-error-badaccess-groups": "Diri ka gintutugotan pagkarga paigbaw ha dinhi nga wiki.",
        "duration-years": "$1 {{PLURAL:$1|tuig|mga tuig}}",
        "duration-decades": "$1 {{PLURAL:$1|dekada|mga dekada}}",
        "duration-centuries": "$1 {{PLURAL:$1|gatostuig|mga gatostuig}}",
-       "duration-millennia": "$1 {{PLURAL:$1|yukottuig|mga yukottuig}}"
+       "duration-millennia": "$1 {{PLURAL:$1|yukottuig|mga yukottuig}}",
+       "mediastatistics-header-unknown": "Waray kasabti",
+       "mediastatistics-header-audio": "Audio",
+       "mediastatistics-header-video": "Mga video",
+       "mediastatistics-header-office": "Buhatan",
+       "mediastatistics-header-total": "Ngatanan nga pakli"
 }
index cbfcdeb..3064aa8 100644 (file)
        "noname": "用户名无效。",
        "loginsuccesstitle": "登录哉",
        "loginsuccess": "<strong>侬现在以“$1”个身份登录到{{SITENAME}}。</strong>",
-       "nosuchuser": "寻弗着用户“$1”。用户名是大小写敏感外加区分繁简体个。请检查拼写,或者[[Special:UserLogin/signup|开只新账户]]。",
+       "nosuchuser": "寻弗着用户“$1”。用户名是大小写敏感外加区分繁简体个。请检查拼写,或者[[Special:CreateAccount|开只新账户]]。",
        "nosuchusershort": "无没叫“$1”个用户。请检查侬个输入。",
        "nouserspecified": "侬必须选个用户名。",
        "login-userblocked": "箇个用户拨封锁拉许。弗允许登录。",
        "accmailtext": "已经为[[User talk:$1|$1]]产生只随机密码,并且已经发送到$2。登录之后,侬可以垃拉<em>[[Special:ChangePassword|箇只页面]]</em>更改密码。",
        "newarticle": "(新)",
        "newarticletext": "倷跟著链接来着一个还弗勒里个页面。要创建该页面呢,就勒下底个框里向开始写([$1 帮助页面]浪有更加多个信息)。要是倷是弗用心到该𡍲个说话,请点击浏览器个<strong>返回</strong>揿钮。",
-       "anontalkpagetext": "---- ''箇是一个还弗曾建立账户个匿名用户个讨论页, 箇咾我伲只好用IP地址来搭渠联络。该IP地址可能由几名用户共享。如果侬是一名匿名用户并认为箇只页面高头个评语搭侬弗搭界,请 [[Special:UserLogin/signup|创建新账户]]或[[Special:UserLogin|登录]]来避免垃拉将来搭其他匿名用户混淆。''",
+       "anontalkpagetext": "---- ''箇是一个还弗曾建立账户个匿名用户个讨论页, 箇咾我伲只好用IP地址来搭渠联络。该IP地址可能由几名用户共享。如果侬是一名匿名用户并认为箇只页面高头个评语搭侬弗搭界,请 [[Special:CreateAccount|创建新账户]]或[[Special:UserLogin|登录]]来避免垃拉将来搭其他匿名用户混淆。''",
        "noarticletext": "箇只页面目前呒没文本。侬可以垃拉其他页面高头[[Special:Search/{{PAGENAME}}|寻该只标题]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 寻相关日志]或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 建立此页]</span>。",
        "noarticletext-nopermission": "箇只页面目前呒不文本。侬可以垃拉其他页面高头[[Special:Search/{{PAGENAME}}|寻箇页标题]],或者<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 寻相关日志]</span>,但必过侬呒不权限建立箇只页面。",
        "userpage-userdoesnotexist": "用户账户“<nowiki>$1</nowiki>”弗曾创建。请垃拉创建/编辑迭个页面前头先检查一记。",
index bae1ce5..7dae096 100644 (file)
        "noname": "Та зөвшәсн демнәчнә нер заасн уга.",
        "loginsuccesstitle": "Йовудта орлһн",
        "loginsuccess": "<strong>Та ода «$1» нертә {{SITENAME}} төсвд бәәнәт.</strong>",
-       "nosuchuser": "«$1» нертә демнәч уга.\nДемнәчнә нернд баһ болн том үзгүд әдл биш.\nЧик бичсн шүүтн аль [[Special:UserLogin/signup|бигчдлһн бүтәтн]].",
+       "nosuchuser": "«$1» нертә демнәч уга.\nДемнәчнә нернд баһ болн том үзгүд әдл биш.\nЧик бичсн шүүтн аль [[Special:CreateAccount|бигчдлһн бүтәтн]].",
        "nosuchusershort": "«$1» нертә демнәч уга.\nЧик бичсн шүүтн.",
        "nouserspecified": "Та демнәчнә нер заах йоста.",
        "login-userblocked": "Эн демнәч бүслсн, орҗ болшго.",
index 19b3bec..6ea0b36 100644 (file)
        "noname": "თქვენს მიერ მითითებული მომხმარებლის სახელი ქმედითი არ არის.",
        "loginsuccesstitle": "სისტემაში შესვლა განხორციელდა.",
        "loginsuccess": "'''ასე მიშულირ რეთ {{SITENAME}}-ს მუჭოთ \"$1\".'''",
-       "nosuchuser": "მომხმარებელი სახელად $1 არ არსებობს.\nმომხმარებელთა სახელები გრძნობადია ასოების რეგისტრამდე..\nშეამოწმეთ სახელის დაწერა ან[[Special:UserLogin/signup|შექმენით ახალი ანგარიში]].",
+       "nosuchuser": "მომხმარებელი სახელად $1 არ არსებობს.\nმომხმარებელთა სახელები გრძნობადია ასოების რეგისტრამდე..\nშეამოწმეთ სახელის დაწერა ან[[Special:CreateAccount|შექმენით ახალი ანგარიში]].",
        "nosuchusershort": "მომხმარებელი სახელით „$1“ არ არსებობს. შეამოწმეთ მართლწერა.",
        "nouserspecified": "საჭირო რე მახვარებუშ ჯოხოშ მიშაჭარუა.",
        "login-userblocked": "ეს მომხმარებელი დაბლოკილია. სისტემაში შესვლა არაა ნებადართული.",
        "accmailtext": "შემთხვევითი მეთოდით შექმნილი პაროლი მომხმარებლისათვის [[User talk:$1|$1]] გაგზავნილია მისამართზე $2.\n\nავტორიზაციის გავლის შემდეგ შესაძლებელი იქნება ამ ანგარიშის  <em>[[Special:ChangePassword|პაროლის შეცვლა]]</em> ანგარიშში შესვლის გვერდზე.",
        "newarticle": "(ახალ)",
        "newarticletext": "თქვა გეჸუნელჷ რეთ ხასჷლაშ რცხის, ნამუთ დიო ვა რე დორცხუაფილი.\nხასჷლაშ დარცხუაფალო გემშეჸონით ტექსტი თუდონ ოჭკორიეშა. (ქოძირით[$1 მოხვარაშ ხასჷლა] უმოს ინფორმაციაშო).\nთე ხასჷლას ჩილათირო მოხვადით–და, ქიგუნჭირით თქვან ბრაუზერიშ კონჭის '''უკახალე'''.\"",
-       "anontalkpagetext": "----\n<em>ეს არის ანონიმური მომხმარებლის განხილვის გვერდი, რომელსაც ანგარიში ჯერ არ შეუქმნია ან არ იყენებს მას.</em>\n\nშესაბამისად, ჩვენ მისი ციფრული IP მისამართი უნდა გამოვიყენოთ მისი იდენტიფიცირებისთვის.\n\nამგვარი მისამართი შეიძლება რამდენიმე მომხმარებელმა გამოიყენოს.\n\nთუ თქვენ ანონიმური მომხმარებელი ხართ და თვლით, რომ სხვისთვის გამიზნული მითითება მიიღეთ, გთხოვთ [[Special:UserLogin/signup|შექმენით ანგარიში ან დარეგისტრირდით]] მომავალში გაუგებრობის თავიდან ასაცილებლად.",
+       "anontalkpagetext": "----\n<em>ეს არის ანონიმური მომხმარებლის განხილვის გვერდი, რომელსაც ანგარიში ჯერ არ შეუქმნია ან არ იყენებს მას.</em>\n\nშესაბამისად, ჩვენ მისი ციფრული IP მისამართი უნდა გამოვიყენოთ მისი იდენტიფიცირებისთვის.\n\nამგვარი მისამართი შეიძლება რამდენიმე მომხმარებელმა გამოიყენოს.\n\nთუ თქვენ ანონიმური მომხმარებელი ხართ და თვლით, რომ სხვისთვის გამიზნული მითითება მიიღეთ, გთხოვთ [[Special:CreateAccount|შექმენით ანგარიში ან დარეგისტრირდით]] მომავალში გაუგებრობის თავიდან ასაცილებლად.",
        "noarticletext": "ასე თე ხასჷლას ტექსტი ვა რე. \nთქვა შეილებუნა [[Special:Search/{{PAGENAME}}|გორათ ათე ხასჷლაშ ჯოხო]] შხვა ხასჷლეფს,\n<span class=\\\"plainlinks\\\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} დოგორათ მეხუჯაფილ ჟურნალეფი],\nვარა [{{fullurl:{{FULLPAGENAME}}|action=edit}} დიჭყათ ათე ხასჷლაშ რედაქტირაფა]</span>.",
        "noarticletext-nopermission": "ათე ხასჷლას ასე ტექსტი ვა რე. თქვა შეილებუნა [[Special:Search/{{PAGENAME}}|დოგორათ თე ხასჷლაშ დუდჯოხო]] შხვა ხასჷლეფს,\nვარდა <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} დოგორათ გინორცხილ ჟურნალეფი]</span>.",
        "missing-revision": "ვერსია $1 გვერდისათვის „{{FULLPAGENAME}}“ არ არსებობს.\n\nეს ჩვეულებრივ ხდება მაშინ, თუ მოძველებული ბმულით გადადიხართ გვერდზე, რომელიც წაიშალა.\nდეტალური ინფორმაცია შესაძლებელია იყოს [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} წაშლების ჟურნალში].",
        "allpagessubmit": "გინულა",
        "categories": "კატეგორიეფი",
        "categoriesfrom": "გეგმარჩქინ ხასილეფ დოჭყაფილ:",
-       "special-categories-sort-count": "დაალაგეთ რაოდენობის მიხედვით",
-       "special-categories-sort-abc": "ანბანზე დალაგება",
        "deletedcontributions": "მომხმარებლის წაშლილი წვლილი",
        "deletedcontributions-title": "წაშლილი წვლილი",
        "sp-deletedcontributions-contribs": "წვლილი",
        "version-libraries-library": "ბიბლიოთეკა",
        "version-libraries-version": "ვერსია",
        "redirect": "გადამისამართება ფაილიდან, მომხმარებლიდან, გვერდიდან ან ვერსიის იდენტიფიკატორიდან",
-       "redirect-legend": "გადამისამართება ფაილზე ან გვერდზე",
        "redirect-summary": "ეს დამხმარე გვერდი ამისამართებს ფაილის (ფაილის სახელიდან) გვერდზე, (გვერდის ან ვერსიის იდენტიფიკატორიდან) ან მომხმარებლის გვერდზე (მომხმარებლის რაოდენობრივი იდენტიფიკატორიდან). გამოყენება: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]] ან [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "გინულა",
        "redirect-lookup": "ძიება:",
        "redirect-not-exists": "მნიშვნელობა ვერ მოიძებნა",
        "fileduplicatesearch": "ერთნაირი ფაილების ძიება",
        "fileduplicatesearch-summary": "ერთნაირი ფაილების ძიება ჰეშ-კოდის მიხედვით.",
-       "fileduplicatesearch-legend": "დუბლიკატების ძიება",
        "fileduplicatesearch-filename": "ფაილის სახელი:",
        "fileduplicatesearch-submit": "გორუა",
        "specialpages": "გჷშაკერძაფილი ხასჷლეფი",
index fe5623f..0e3b9fd 100644 (file)
        "noname": "איר האט נישט אַרײַנגעשטעלט קײַן גילטיקן באַניצער נאָמען.",
        "loginsuccesstitle": "אריינלאגירט",
        "loginsuccess": "'''איר זענט אַצינד אַרײַנלאָגירט אין {{SITENAME}} ווי \"$1\".'''",
-       "nosuchuser": "נישטא קיין באניצער מיטן נאמען  \"$1\".\n\nקוקט איבער אײַער אויסלייג, אדער [[Special:UserLogin/signup|שאַפֿט א נײַע קאנטע]].",
+       "nosuchuser": "נישטא קיין באניצער מיטן נאמען  \"$1\".\n\nקוקט איבער אײַער אויסלייג, אדער [[Special:CreateAccount|שאַפֿט א נײַע קאנטע]].",
        "nosuchusershort": "נישטאָ קיין באַניצער מיטן נאָמען \"$1\".\nביטע באַשטעטיקט דעם אויסלייג.",
        "nouserspecified": "איר ברויכט ספעציפֿיצירן א באַניצער-נאָמען.",
        "login-userblocked": "דער באַניצער איז בלאקירט. ארײַנלאגירן נישט ערלויבט.",
        "accmailtext": "א צופֿעליק פאַסווארט פֿאַר [[User talk:$1|$1]] איז געשיקט געוואָרן צו $2.\n\nמען קען עס טוישן אויפֿן [[Special:ChangePassword|טוישן פאַסווארט]] בלאַט נאָכן ארײַנלאגירן.",
        "newarticle": "(ניי)",
        "newarticletext": "איר זענט געקומען צו אַ בלאַט וואָס עקזיסטירט נאָך נישט!\nכדי שאַפֿן דעם בלאַט, קלאַפט אַרײַן טעקסט אין דעם קעסטל אונטן (זעט דעם [$1 הילף בלאַט] פֿאַר מער אינפֿארמאַציע).\nאויב איר זענט אַהערגעקומען בטעות, דרוקט דאָס '''Back''' קנעפל אין אײַער בלעטערער.",
-       "anontalkpagetext": "----'''דאָס איז א רעדן בלאַט פון א אַן אַנאנימען באַניצער וואָס האט נאך נישט געשאַפֿן קיין קאנטע, אדער באניצט זיך נישט דערמיט. דערוועגן, מוזן מיר זיך באניצן מיט זיין IP אדרעס כדי אים צו אידענטיפיצירן. עס קען זיין אז עטלעכע אנדערע ניצן אויך דעם  IP אדרעס. אויב זענט איר אן אנאנימער באַניצער וואס שפירט אז איר האט באקומען מעלדונגען וואס זענען נישט שייך צו אייך, ביטע [[Special:UserLogin/signup|שאַפֿט א קאנטע]] אדער [[Special:UserLogin|טוט זיך אריינלאגירן]] כדי צו פארמיידן דאס אין די עתיד זיך פארמישן מיט אנדערע אַנאנימע באַניצערס.'''",
+       "anontalkpagetext": "----'''דאָס איז א רעדן בלאַט פון א אַן אַנאנימען באַניצער וואָס האט נאך נישט געשאַפֿן קיין קאנטע, אדער באניצט זיך נישט דערמיט. דערוועגן, מוזן מיר זיך באניצן מיט זיין IP אדרעס כדי אים צו אידענטיפיצירן. עס קען זיין אז עטלעכע אנדערע ניצן אויך דעם  IP אדרעס. אויב זענט איר אן אנאנימער באַניצער וואס שפירט אז איר האט באקומען מעלדונגען וואס זענען נישט שייך צו אייך, ביטע [[Special:CreateAccount|שאַפֿט א קאנטע]] אדער [[Special:UserLogin|טוט זיך אריינלאגירן]] כדי צו פארמיידן דאס אין די עתיד זיך פארמישן מיט אנדערע אַנאנימע באַניצערס.'''",
        "noarticletext": "דערווייל איז נישט פאַרהאן קיין שום טעקסט אין דעם בלאט.\nאיר קענט [[Special:Search/{{PAGENAME}}|זוכן דעם בלאט טיטל]] אין אנדערע בלעטער,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} זוכן די רעלעוואנטע לאגביכער],\nאדער [{{fullurl:{{FULLPAGENAME}}|action=edit}} שאפֿן דעם בלאט]</span>.",
        "noarticletext-nopermission": "דערווײַל איז נישט פאַראַן קיין שום טעקסט אין דעם בלאַט.\nאיר קענט [[Special:Search/{{PAGENAME}}| זוכן דעם בלאט טיטל]] אין אנדערע בלעטער,\nאדער <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} נאָכזוכן די רעלעוואנטע לאגביכער]</span>., אבער איר זענט נישט ערלויבט צו שאפֿן דעם בלאט.",
        "missing-revision": "די רעוויזיע #$1 פונעם בלאט \"{{FULLPAGENAME}}\" עקזיסטירט נישט.\n\nדאס געשעט געוויינלעך פון פאלגן א פארעלטערטן היסטאריע לינק צו א בלאט וואס איז געווארן אויסגעמעקט.\nפרטים קען מען געפינען אינעם [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} אויסמעקונג לאגבוך].",
        "upload-form-label-own-work": "דאס איז מיין אייגענע ארבעט.",
        "upload-form-label-infoform-categories": "קאַטעגאריעס",
        "upload-form-label-infoform-date": "דאַטע",
-       "upload-form-label-not-own-work-local-local": "אפשר ווילט איר פרובירן [[Special:Upload|דעם גרונטלעכן ארויפלאד־בלאט]].",
+       "upload-form-label-not-own-work-local-generic-local": "אפשר ווילט איר פרובירן [[Special:Upload|דעם גרונטלעכן ארויפלאד־בלאט]].",
        "backend-fail-stream": "קען נישט מאכן שטראמען טעקע $1.",
        "backend-fail-notexists": "נישט פֿאראן די טעקע $1.",
        "backend-fail-notsame": "א נישט־אידענטישע טעקע עקזיסטירט שוין ביי \"$1\".",
index 3b75367..b12aac5 100644 (file)
        "noname": "Ẹ kò tọ́kasí orúkọ oníṣe tó ní ìbámu.",
        "loginsuccesstitle": "Ìwọlé ti yọrí sí rere",
        "loginsuccess": "'''Ẹ ti wọlé sínú {{SITENAME}} gẹ́gẹ́ bi \"$1\".'''",
-       "nosuchuser": "Kò sí oníṣe kankan pẹ̀lú orúkọ \"$1\".\nÀwọn lẹ́tà àwọn orúkọ oníṣe gbọ́dọ̀ jẹ́ irúkanna.\nẸ yẹ lẹ́tà yín wò, tàbí [[Special:UserLogin/signup|kí ẹ dá àkópamọ́ tuntun]].",
+       "nosuchuser": "Kò sí oníṣe kankan pẹ̀lú orúkọ \"$1\".\nÀwọn lẹ́tà àwọn orúkọ oníṣe gbọ́dọ̀ jẹ́ irúkanna.\nẸ yẹ lẹ́tà yín wò, tàbí [[Special:CreateAccount|kí ẹ dá àkópamọ́ tuntun]].",
        "nosuchusershort": "Kò sí oníṣe t'ón jẹ́ $1.\nẸ yẹ lẹ́tà ọ̀rọ̀ yín wò.",
        "nouserspecified": "Ẹ gbọ́dọ̀ tọ́kasí orúkọ oníṣe kan.",
        "login-userblocked": "Oníṣe yìí jẹ́ dídínà. Ìwọlé kò jẹ́ gbígbà láyè.",
        "accmailtext": "A ti fi ọ̀rọ̀ìpamọ́ àrìnàkò tí a pèsè fún [[User talk:$1|$1]] ránṣẹ́ sí $2. Ẹ le ṣe àyípadà ọ̀rọ̀ìpamọ́ fún àpamọ́ tuntun yìí ní ibi ''[[Special:ChangePassword|àyípadà ọ̀rọ̀ìpamọ́]]'' lẹ́yìn tí ẹ bá ti jáwọlé.",
        "newarticle": "(Tuntun)",
        "newarticletext": "Ẹ ti tẹ̀lé ìjápọ̀ mọ́ ojúewé tí kò sí.\nLáti dá ojúewé yí ẹ bẹ̀rẹ̀ síní tẹ́kọ sí inú àpótí ìsàlẹ̀ yí (ẹ wo [$1 ojúewé ìrànlọ́wọ́ ] fun ẹ̀kúnrẹ́rẹ́ ).\nT'óbá sepé àsìse ló gbé yin dé bi, ẹ kọn bọ́tìnì ìpadàsẹ́yìn.",
-       "anontalkpagetext": "''Ojúewé ìfọ̀rọ̀wérọ̀ yìí wà fún oníṣe aláílórúkọ tí kò tíì dá àkópamọ́, tàbí tí kò lò ó rárá.\nBí bẹ́ẹ̀ laṣe únlo àdírẹ́ẹ̀sì IP oníyenọ́mbà láti dáamọ̀.\nIrú àdírẹ́ẹ̀sì IP báun ṣeéṣe kó jẹ́ pínpínlọ̀ pẹ̀lú àwọn oníṣe míràn.\nTó bá jẹ́ pé oníṣe aláìlórúkọ ni yín, tí ẹ sì ri pé wọ́n ùnsọ̀rọ̀ tí kò kàn yín sí i yín, ẹ jọ̀wọ́ [[Special:UserLogin/signup|ẹ dá àkópamọ́ kan]] tàbí [[Special:UserLogin|kí ẹ wọlẹ́]] kó mọ́ baà sí ìdàrúpọ̀ lọ́jọ́ọwájú mọ́ àwọn oníṣe aláìlórúkọ mírán.''",
+       "anontalkpagetext": "''Ojúewé ìfọ̀rọ̀wérọ̀ yìí wà fún oníṣe aláílórúkọ tí kò tíì dá àkópamọ́, tàbí tí kò lò ó rárá.\nBí bẹ́ẹ̀ laṣe únlo àdírẹ́ẹ̀sì IP oníyenọ́mbà láti dáamọ̀.\nIrú àdírẹ́ẹ̀sì IP báun ṣeéṣe kó jẹ́ pínpínlọ̀ pẹ̀lú àwọn oníṣe míràn.\nTó bá jẹ́ pé oníṣe aláìlórúkọ ni yín, tí ẹ sì ri pé wọ́n ùnsọ̀rọ̀ tí kò kàn yín sí i yín, ẹ jọ̀wọ́ [[Special:CreateAccount|ẹ dá àkópamọ́ kan]] tàbí [[Special:UserLogin|kí ẹ wọlẹ́]] kó mọ́ baà sí ìdàrúpọ̀ lọ́jọ́ọwájú mọ́ àwọn oníṣe aláìlórúkọ mírán.''",
        "noarticletext": "Lọ́wọ́lọ́wọ́ kò sí ìkọ̀ nínú ojúewé yìí.\nẸ le [[Special:Search/{{PAGENAME}}|wá àkọlé ojúewé yìí]] nínú àwọn ojúewé mìíràn,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wá àkọọ́lẹ̀ rẹ̀], tàbí [{{fullurl:{{FULLPAGENAME}}|action=edit}} kí ẹ ṣ'àtúnṣe ojúewé òún]</span>.",
        "noarticletext-nopermission": "Lọ́wọ́lọ́wọ́ kò sí ìkọ̀ nínú ojúewé yìí.\nẸ le [[Special:Search/{{PAGENAME}}|wá àkọlé ojúewé yìí]] nínú àwọn ojúewé mìíràn, tàbí\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} wá àwọn àkọọ́lẹ̀ tó bámu]</span>, sùgbọ́n ẹ kò ní àṣẹ láti ṣ'ẹ̀dá ojúewé yìí.",
        "missing-revision": "Àtúnyẹ̀wò #$1 ojúewé tó únjẹ́ \"{{FULLPAGENAME}}\" kò sí.\n\nÈyí únsábà ṣẹlẹ̀ nítorípé ẹ tẹ̀lé ìtàn àjápọ̀ tí kò ṣiṣẹ́ mọ́ wá sí orí ojúewé tó ti jẹ́ píparẹ́.\nẸ̀kúnrẹ́rẹ́ wà nínú [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} àkọọ́lẹ̀ ìparẹ́].",
index fa2b86e..925e37f 100644 (file)
        "noname": "你未指定一個有效嘅用戶名。",
        "loginsuccesstitle": "登入成功",
        "loginsuccess": "'''「$1」登入咗{{SITENAME}}。'''",
-       "nosuchuser": "呢度冇叫做 \"$1\"嘅用戶。\n用戶名係有分大細楷嘅。\n請檢查你個名嘅輸入方法,或者[[Special:UserLogin/signup|建立一個新嘅戶口]]。",
+       "nosuchuser": "呢度冇叫做 \"$1\"嘅用戶。\n用戶名係有分大細楷嘅。\n請檢查你個名嘅輸入方法,或者[[Special:CreateAccount|建立一個新嘅戶口]]。",
        "nosuchusershort": "呢度冇叫做 \"$1\"嘅用戶。 請檢查你個名嘅輸入方法。",
        "nouserspecified": "你需要指定一個用戶名。",
        "login-userblocked": "呢位用戶封鎖咗。唔容許登入。",
        "accmailtext": "「[[User talk:$1|$1]]」嘅隨機產生密碼已經寄咗去 $2。\n\n呢個密碼可以響簽到咗之後嘅<em>[[Special:ChangePassword|改密碼]]</em> 版度改佢。",
        "newarticle": "(新)",
        "newarticletext": "你連連過嚟嘅頁面重未存在。\n要起版新嘅,請你喺下面嗰格度輸入。(睇睇[$1 自助版]拎多啲資料。)\n如果你係唔覺意嚟到呢度,撳一次你個瀏覽器'''返轉頭'''個掣。",
-       "anontalkpagetext": "----''呢度係匿名用戶嘅討論頁,佢可能係重未開戶口,或者佢重唔識開戶口。我哋會用數字表示嘅IP地址嚟代表佢。一個IP地址係可以由幾個用戶夾來用。如果你係匿名用戶,同覺得呢啲留言係同你冇關係嘅話,唔該去[[Special:UserLogin/signup|開一個新戶口]]或[[Special:UserLogin|登入]],避免喺以後嘅留言會同埋其它用戶混淆。''",
+       "anontalkpagetext": "----''呢度係匿名用戶嘅討論頁,佢可能係重未開戶口,或者佢重唔識開戶口。我哋會用數字表示嘅IP地址嚟代表佢。一個IP地址係可以由幾個用戶夾來用。如果你係匿名用戶,同覺得呢啲留言係同你冇關係嘅話,唔該去[[Special:CreateAccount|開一個新戶口]]或[[Special:UserLogin|登入]],避免喺以後嘅留言會同埋其它用戶混淆。''",
        "noarticletext": "喺呢一頁而家並冇任何嘅文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌],\n或者[{{fullurl:{{FULLPAGENAME}}|action=edit}} 編輯呢一版]</span>。",
        "noarticletext-nopermission": "呢一頁而家冇任何文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],或者<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌]</span>。",
        "missing-revision": "The revision #$1 of the page named \"{{FULLPAGENAME}}\" does not exist.\n\nThis is usually caused by following an outdated history link to a page that has been deleted.\nDetails can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].\n\n《{{FULLPAGENAME}}》嘅編輯#$1唔存在。\n\n恁通常係因為一條過徂時嘅鏈接帶徂閣下去一個已經刪除徂嘅版。\n詳情請查閱[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 刪文紀錄]。",
index ac41f0c..f29afc7 100644 (file)
        "noname": "Je ei hin heldihe gebrukersnaem opeheven.",
        "loginsuccesstitle": "Anmelden geslaegd",
        "loginsuccess": "'''Je bin noe anemeld bie {{SITENAME}} as \"$1\".'''",
-       "nosuchuser": "De gebruker \"$1\" besti nie.\nControleer de schriefwieze of [[Special:UserLogin/signup|mik een nieuwe gebruker an]].",
+       "nosuchuser": "De gebruker \"$1\" besti nie.\nControleer de schriefwieze of [[Special:CreateAccount|mik een nieuwe gebruker an]].",
        "nosuchusershort": "De gebruker \"$1\" besti nie. Controleer de schriefwieze.",
        "nouserspecified": "Je dien een gebrukersnaem op te heven.",
        "wrongpassword": "Wachtwoôrd onjuust. Probeer 't opnieuw.",
        "allpagesbadtitle": "D'n ingegeven bladzie-titel was ongeldeg of ao 'n interwiki-vòvoegsel. Meschien stae d'r eên of meer teêkens in die-an nie in titels gebruukt ore kunne.",
        "categories": "Categorieën",
        "categoriespagetext": "De wiki eit de volgende categorieën.\n[[Special:UnusedCategories|Unused categories]] are not shown here.\nAlso see [[Special:WantedCategories|wanted categories]].",
-       "special-categories-sort-count": "op antal sorteern",
-       "special-categories-sort-abc": "alfabetisch sorteern",
        "linksearch-line": "$1 ei 'n verwiezienge in $2",
        "listgrouprights-members": "(ledenlieste)",
        "emailuser": "E-mail deêze gebruker",
index 68b3945..12e1da8 100644 (file)
        "noname": "未指定有效的用户名。",
        "loginsuccesstitle": "已登录",
        "loginsuccess": "<strong>您现在已经以\"$1\"的身份登录了{{SITENAME}}。</strong>",
-       "nosuchuser": "没有名为“$1”的用户。用户名区分大小写。请检查你的拼写或[[Special:UserLogin/signup|创建新账户]]。",
+       "nosuchuser": "没有名为“$1”的用户。用户名区分大小写。请检查你的拼写或[[Special:CreateAccount|创建新账户]]。",
        "nosuchusershort": "没有名为“$1”的用户。请检查您的拼写。",
        "nouserspecified": "您必须指定一个用户名。",
        "login-userblocked": "该用户已被封禁,禁止登录。",
        "accmailtext": "为[[User talk:$1|$1]]随机生成的密码已送至$2。登录后可以在<em>[[Special:ChangePassword|更改密码]]</em>页面中修改。",
        "newarticle": "(新页面)",
        "newarticletext": "您点击了一个尚不存在的页面的链接。要创建该页面,请在下面的编辑框中输入内容(更多信息请见[$1 帮助页面])。如果您是错误地进入了此页面,请点击您的浏览器的<strong>返回</strong>按钮。",
-       "anontalkpagetext": "----\n<em>这是一个还未建立账户的匿名用户的讨论页, 因此我们只能用IP地址来与他或她联络。</em>该IP地址可能由几名用户共享。如果您是一名匿名用户并认为此页上的评语与您无关,请[[Special:UserLogin/signup|创建新账户]]或[[Special:UserLogin|登录]]以避免在未来与其他匿名用户混淆。",
+       "anontalkpagetext": "----\n<em>这是一个还未建立账户的匿名用户的讨论页, 因此我们只能用IP地址来与他或她联络。</em>该IP地址可能由几名用户共享。如果您是一名匿名用户并认为此页上的评语与您无关,请[[Special:CreateAccount|创建新账户]]或[[Special:UserLogin|登录]]以避免在未来与其他匿名用户混淆。",
        "noarticletext": "本页面目前没有内容。您可以在其他页面中[[Special:Search/{{PAGENAME}}|搜索本页标题]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜索相关日志]或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 创建本页面]</span>。",
        "noarticletext-nopermission": "本页面目前没有内容。您可以在其他页面中[[Special:Search/{{PAGENAME}}|搜索本页标题]]或<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜索相关日志]</span>,但您没有权限创建本页面。",
        "missing-revision": "“{{FULLPAGENAME}}”的版本#$1不存在。\n\n这通常是因为进入了一个已被删除的页面的历史链接。\n详细信息可以在[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 删除日志]中找到。",
        "right-override-export-depth": "导出页面,包括最多5层链接",
        "right-sendemail": "发送电子邮件给其他用户",
        "right-passwordreset": "查看密码重置电子邮件",
-       "right-managechangetags": "从数据库创建和删除[[Special:Tags|标签]]",
+       "right-managechangetags": "创建和(取消)激活[[Special:Tags|标签]]",
        "right-applychangetags": "连同某人的更改一起应用[[Special:Tags|标签]]",
        "right-changetags": "在个别修订和日志记录中添加和移除任意[[Special:Tags|标签]]",
+       "right-deletechangetags": "从数据库删除[[Special:Tags|标签]]",
        "grant-generic": "“$1”权限束",
        "grant-group-page-interaction": "与页面交互",
        "grant-group-file-interaction": "与媒体交互",
        "action-viewmyprivateinfo": "查看您的私人信息",
        "action-editmyprivateinfo": "编辑你的私人信息",
        "action-editcontentmodel": "编辑页面的内容模型",
-       "action-managechangetags": "创建和从数据库中删除标签",
+       "action-managechangetags": "创建和(取消)激活标签",
        "action-applychangetags": "连同您的更改应用标签",
        "action-changetags": "在个别修订和日志记录中添加和移除任意标签",
+       "action-deletechangetags": "从数据库删除标签",
        "nchanges": "$1次更改",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|上次访问后}}$1个",
        "enhancedrc-history": "历史",
        "upload-form-label-own-work": "这是我的作品",
        "upload-form-label-infoform-categories": "分类",
        "upload-form-label-infoform-date": "日期",
-       "upload-form-label-own-work-message-local": "我确认我正在依据{{SITENAME}}上的服务条款和许可协议方针上传文件。",
-       "upload-form-label-not-own-work-message-local": "如果您无法依据{{SITENAME}}的方针上传此文件,请关闭此对话框并尝试其他方法。",
-       "upload-form-label-not-own-work-local-local": "您也可以尝试[[Special:Upload|默认上传页面]]。",
-       "upload-form-label-own-work-message-default": "我知道我正在上传此文件至一个共享的存储库。我确认我依据这里的服务条款和许可方针做此事。",
-       "upload-form-label-not-own-work-message-default": "如果您无法依据分享存储库的方针上传此文件,请关闭此对话框并尝试其他方法。",
-       "upload-form-label-not-own-work-local-default": "如果此文件可以依据他们的方针上传的话,您也可以尝试使用[[Special:Upload|{{SITENAME}}上的上传页面]]。",
-       "upload-form-label-own-work-message-shared": "我证明我拥有此文件的版权,并不可撤销地同意采用[https://creativecommons.org/licenses/by-sa/4.0/ 知识共享 署名-相同方式共享 4.0]许可协议将此文件发布至维基共享资源,并且我同意[https://wikimediafoundation.org/wiki/Terms_of_Use 使用条款]。",
-       "upload-form-label-not-own-work-message-shared": "如果您并不拥有此文件的版权,或者您希望将其以其他许可协议发布,请考虑使用[https://commons.wikimedia.org/wiki/Special:UploadWizard 共享资源的上传向导]。",
-       "upload-form-label-not-own-work-local-shared": "如果网站允许依据他们的方针上传此文件的话,您也可以尝试使用[[Special:Upload|{{SITENAME}}上的上传页面]]。",
+       "upload-form-label-own-work-message-generic-local": "我确认我正在依据{{SITENAME}}上的服务条款和许可协议方针上传文件。",
+       "upload-form-label-not-own-work-message-generic-local": "如果您无法依据{{SITENAME}}的方针上传此文件,请关闭此对话框并尝试其他方法。",
+       "upload-form-label-not-own-work-local-generic-local": "您也可以尝试[[Special:Upload|默认上传页面]]。",
+       "upload-form-label-own-work-message-generic-foreign": "我知道我正在上传此文件至一个共享的存储库。我确认我依据这里的服务条款和许可方针做此事。",
+       "upload-form-label-not-own-work-message-generic-foreign": "如果您无法依据分享存储库的方针上传此文件,请关闭此对话框并尝试其他方法。",
+       "upload-form-label-not-own-work-local-generic-foreign": "如果此文件可以依据他们的方针上传的话,您也可以尝试使用[[Special:Upload|{{SITENAME}}上的上传页面]]。",
        "backend-fail-stream": "无法流传送文件$1。",
        "backend-fail-backup": "无法备份文件$1。",
        "backend-fail-notexists": "条目$1不存在。",
        "whatlinkshere-prev": "{{PLURAL:$1|前|前$1个}}",
        "whatlinkshere-next": "{{PLURAL:$1|后|后$1个}}",
        "whatlinkshere-links": "←链接",
-       "whatlinkshere-hideredirs": "$1重定向",
-       "whatlinkshere-hidetrans": "$1嵌入",
-       "whatlinkshere-hidelinks": "$1链接",
-       "whatlinkshere-hideimages": "$1文件链接",
+       "whatlinkshere-hideredirs": "隐藏重定向",
+       "whatlinkshere-hidetrans": "隐藏嵌入",
+       "whatlinkshere-hidelinks": "隐藏链接",
+       "whatlinkshere-hideimages": "隐藏文件链接",
        "whatlinkshere-filters": "过滤器",
        "whatlinkshere-submit": "提交",
        "autoblockid": "自动封禁#$1",
        "timezone-local": "本地",
        "duplicate-defaultsort": "<strong>警告:</strong>默认排序关键词“$2”覆盖了之前的默认排序关键词“$1”。",
        "duplicate-displaytitle": "<strong>警告:</strong>显示的标题“$2”重写了此前显示的标题“$1”。",
+       "restricted-displaytitle": "<strong>警告:</strong>显示标题“$1”被忽略,因为它不等同于页面的实际标题。",
        "invalid-indicator-name": "<strong>错误:</strong>页面状态指示器的<code>name</code>属性必须不为空。",
        "version": "版本",
        "version-extensions": "安装的扩展程序",
        "tags-delete-not-found": "标签“$1”不存在。",
        "tags-delete-too-many-uses": "“$1”标签已应用到超过$2个修订版本,这意味着它不能被删除。",
        "tags-delete-warnings-after-delete": "标签“$1”已删除,但遇到了以下{{PLURAL:$2|警告}}:",
+       "tags-delete-no-permission": "您没有权限删除更改标签。",
        "tags-activate-title": "激活标签",
        "tags-activate-question": "您将要激活标签“$1”。",
        "tags-activate-reason": "原因:",
index 2ec731c..fbe0e55 100644 (file)
@@ -71,7 +71,8 @@
                        "飞舞回堂前",
                        "Bbslam",
                        "Zerng07",
-                       "Reke"
+                       "Reke",
+                       "Kly"
                ]
        },
        "tog-underline": "底線標示連結:",
        "noname": "您輸入的使用者名稱無效。",
        "loginsuccesstitle": "已登入",
        "loginsuccess": "<strong>{{GENDER:|您|妳|你}}現在已經以 \"$1\" 的身分登入了 {{SITENAME}}。</strong>",
-       "nosuchuser": "查無使用者 \"$1\"。\n使用者名稱有大小寫區分,\n請檢查您拼寫是否正確,或者 [[Special:UserLogin/signup|建立新帳號]]。",
+       "nosuchuser": "查無使用者 \"$1\"。\n使用者名稱有大小寫區分,\n請檢查您拼寫是否正確,或者 [[Special:CreateAccount|建立新帳號]]。",
        "nosuchusershort": "查無使用者 \"$1\",\n請檢查您拼寫是否正確。",
        "nouserspecified": "您必須指定一個使用者名稱。",
        "login-userblocked": "這位使用者已被封鎖,不允許登入。",
        "accmailtext": "[[User talk:$1|$1]] 的隨機密碼已經寄送至 $2,可登入後至 <em>[[Special:ChangePassword|變更密碼]] 頁面更改</em>。",
        "newarticle": "(新)",
        "newarticletext": "您正連結至一頁不存在頁面。\n要建立該頁面,請在下方的編輯方塊中輸入內容 (詳情請參考 [$1 説明頁面]) 。\n如果您是不小心來到此頁面,請點選瀏覽器的 <strong>返回</strong> 按鈕。",
-       "anontalkpagetext": "----\n<em>此討論頁面是給尚未建立帳號的匿名使用者使用</em>\n因此我們必須使用 IP 位址來辨識身份,但相同的 IP 位址可能由許多不同的使用者所共用。\n如果您是匿名使用者並且覺得評論的內容與您無關,請 [[Special:UserLogin/signup|建立新帳號]] 或 [[Special:UserLogin|登入]] 避免與其他匿名使用者混淆。",
+       "anontalkpagetext": "----\n<em>此討論頁面是給尚未建立帳號的匿名使用者使用</em>\n因此我們必須使用 IP 位址來辨識身份,但相同的 IP 位址可能由許多不同的使用者所共用。\n如果您是匿名使用者並且覺得評論的內容與您無關,請 [[Special:CreateAccount|建立新帳號]] 或 [[Special:UserLogin|登入]] 避免與其他匿名使用者混淆。",
        "noarticletext": "此頁面目前沒有內容,您可以在其它頁面中[[Special:Search/{{PAGENAME}}|搜尋此頁面標題]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜尋相關日誌]或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 建立此頁]</span>。",
        "noarticletext-nopermission": "此頁面目前沒有內容,\n您可以在其它頁面中 [[Special:Search/{{PAGENAME}}|搜尋此頁面標題]],或 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜尋相關日誌]</span>,但您沒有權限建立此頁面。",
        "missing-revision": "頁面名稱 \"{{FULLPAGENAME}}\" 的 #$1 修訂版本不存在。\n\n通常是因連結到過期的歷史頁面,該頁面已被刪除。\n詳情請參考 [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 刪除日誌]。",
        "upload-form-label-own-work": "這是我的作品",
        "upload-form-label-infoform-categories": "分類",
        "upload-form-label-infoform-date": "日期",
-       "upload-form-label-own-work-message-local": "我確定我上傳的檔案已遵守下列 {{SITENAME}} 的服務條款與授權條款。",
-       "upload-form-label-not-own-work-message-local": "若您無法同意遵守 {{SITENAME}} 的政策上傳檔案,請關閉此對話框並嘗試其他方法。",
-       "upload-form-label-not-own-work-local-local": "您也可嘗試[[Special:Upload|預設的上傳頁面]]。",
-       "upload-form-label-own-work-message-default": "我明白我將上傳此檔案到一個共享的儲存庫,我確認已遵守本站的服務條款與授權政策。",
-       "upload-form-label-not-own-work-message-default": "若您無法同意遵守共享儲存庫的政策上傳檔案,請關閉此對話框並嘗試其他方法。",
-       "upload-form-label-not-own-work-local-default": "若此檔案可遵守該站的授權政策上傳檔案,您可能會希望直接嘗試使用 [[Special:Upload|{{SITENAME}} 的上傳頁面]]。",
-       "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 使用條款]。",
-       "upload-form-label-not-own-work-message-shared": "若您並未擁有此檔案的版權,或者您希望使用其他的授權條款發佈此檔案,請考慮使用[https://commons.wikimedia.org/wiki/Special:UploadWizard 通用上傳精靈]。",
-       "upload-form-label-not-own-work-local-shared": "若該站的授權政策允許上傳此檔案,您可能會希望直接嘗試使用 [[Special:Upload|{{SITENAME}} 的上傳頁面]]。",
+       "upload-form-label-own-work-message-generic-local": "我確定我上傳的檔案已遵守下列 {{SITENAME}} 的服務條款與授權條款。",
+       "upload-form-label-not-own-work-message-generic-local": "若您無法同意遵守 {{SITENAME}} 的政策上傳檔案,請關閉此對話框並嘗試其他方法。",
+       "upload-form-label-not-own-work-local-generic-local": "您也可嘗試[[Special:Upload|預設的上傳頁面]]。",
+       "upload-form-label-own-work-message-generic-foreign": "我明白我將上傳此檔案到一個共享的儲存庫,我確認已遵守本站的服務條款與授權政策。",
+       "upload-form-label-not-own-work-message-generic-foreign": "若您無法同意遵守共享儲存庫的政策上傳檔案,請關閉此對話框並嘗試其他方法。",
+       "upload-form-label-not-own-work-local-generic-foreign": "若此檔案可遵守該站的授權政策上傳檔案,您可能會希望直接嘗試使用 [[Special:Upload|{{SITENAME}} 的上傳頁面]]。",
        "backend-fail-stream": "無法傳輸檔案 \"$1\"。",
        "backend-fail-backup": "無法備份檔案 \"$1\"。",
        "backend-fail-notexists": "檔案 $1 不存在。",
        "whatlinkshere-prev": "前 $1 筆",
        "whatlinkshere-next": "{{PLURAL:$1|下筆|後 $1 筆}}",
        "whatlinkshere-links": "← 連結",
-       "whatlinkshere-hideredirs": "$1 重新導向",
-       "whatlinkshere-hidetrans": "$1 引用",
-       "whatlinkshere-hidelinks": "$1 連結",
+       "whatlinkshere-hideredirs": "隱藏重定向",
+       "whatlinkshere-hidetrans": "隱藏引用",
+       "whatlinkshere-hidelinks": "隱藏連結",
        "whatlinkshere-hideimages": "$1 檔案連結",
        "whatlinkshere-filters": "搜尋",
        "whatlinkshere-submit": "前往",
index 674be13..589144c 100644 (file)
@@ -407,6 +407,7 @@ $specialPageAliases = [
        'BrokenRedirects'           => [ 'BrokenRedirects' ],
        'Categories'                => [ 'Categories' ],
        'ChangeContentModel'        => [ 'ChangeContentModel' ],
+       'ChangeCredentials'         => [ 'ChangeCredentials' ],
        'ChangeEmail'               => [ 'ChangeEmail' ],
        'ChangePassword'            => [ 'ChangePassword', 'ResetPass', 'ResetPassword' ],
        'ComparePages'              => [ 'ComparePages' ],
@@ -430,6 +431,7 @@ $specialPageAliases = [
        'JavaScriptTest'            => [ 'JavaScriptTest' ],
        'BlockList'                 => [ 'BlockList', 'ListBlocks', 'IPBlockList' ],
        'LinkSearch'                => [ 'LinkSearch' ],
+       'LinkAccounts'              => [ 'LinkAccounts' ],
        'Listadmins'                => [ 'ListAdmins' ],
        'Listbots'                  => [ 'ListBots' ],
        'Listfiles'                 => [ 'ListFiles', 'FileList', 'ImageList' ],
@@ -475,6 +477,7 @@ $specialPageAliases = [
        'Recentchanges'             => [ 'RecentChanges' ],
        'Recentchangeslinked'       => [ 'RecentChangesLinked', 'RelatedChanges' ],
        'Redirect'                  => [ 'Redirect' ],
+       'RemoveCredentials'         => [ 'RemoveCredentials' ],
        'ResetTokens'               => [ 'ResetTokens' ],
        'Revisiondelete'            => [ 'RevisionDelete' ],
        'RunJobs'                   => [ 'RunJobs' ],
@@ -490,6 +493,7 @@ $specialPageAliases = [
        'Uncategorizedpages'        => [ 'UncategorizedPages' ],
        'Uncategorizedtemplates'    => [ 'UncategorizedTemplates' ],
        'Undelete'                  => [ 'Undelete' ],
+       'UnlinkAccounts'            => [ 'UnlinkAccounts' ],
        'Unlockdb'                  => [ 'UnlockDB' ],
        'Unusedcategories'          => [ 'UnusedCategories' ],
        'Unusedimages'              => [ 'UnusedFiles', 'UnusedImages' ],
index 6d9a616..922cc87 100644 (file)
@@ -34,7 +34,7 @@ require_once __DIR__ . '/Maintenance.php';
  */
 class UpdateCollation extends Maintenance {
        const BATCH_SIZE = 100; // Number of rows to process in one batch
-       const SYNC_INTERVAL = 20; // Wait for slaves after this many batches
+       const SYNC_INTERVAL = 5; // Wait for slaves after this many batches
 
        public $sizeHistogram = [];
 
@@ -70,6 +70,7 @@ TEXT
                global $wgCategoryCollation;
 
                $dbw = $this->getDB( DB_MASTER );
+               $dbr = $this->getDB( DB_SLAVE );
                $force = $this->getOption( 'force' );
                $dryRun = $this->getOption( 'dry-run' );
                $verboseStats = $this->getOption( 'verbose-stats' );
@@ -97,6 +98,7 @@ TEXT
                $options = [
                        'LIMIT' => self::BATCH_SIZE,
                        'ORDER BY' => $orderBy,
+                       'STRAIGHT_JOIN' // per T58041
                ];
 
                if ( $force || $dryRun ) {
@@ -110,7 +112,7 @@ TEXT
                                ];
                        }
 
-                       $count = $dbw->estimateRowCount(
+                       $count = $dbr->estimateRowCount(
                                'categorylinks',
                                '*',
                                $collationConds,
@@ -118,7 +120,7 @@ TEXT
                        );
                        // Improve estimate if feasible
                        if ( $count < 1000000 ) {
-                               $count = $dbw->selectField(
+                               $count = $dbr->selectField(
                                        'categorylinks',
                                        'COUNT(*)',
                                        $collationConds,
@@ -131,6 +133,7 @@ TEXT
                                return;
                        }
                        $this->output( "Fixing collation for $count rows.\n" );
+                       wfWaitForSlaves();
                }
                $count = 0;
                $batchCount = 0;
index cf2abdb..9a5931f 100644 (file)
@@ -756,6 +756,7 @@ return [
                        'de' => 'resources/lib/moment/locale/de.js',
                        'de-at' => 'resources/lib/moment/locale/de-at.js',
                        'el' => 'resources/lib/moment/locale/el.js',
+                       'en' => 'resources/src/moment-dmy.js',
                        'en-au' => 'resources/lib/moment/locale/en-au.js',
                        'en-ca' => 'resources/lib/moment/locale/en-ca.js',
                        'en-gb' => 'resources/lib/moment/locale/en-gb.js',
@@ -1169,10 +1170,14 @@ return [
                        'upload-foreign-cant-upload',
                ]
        ],
+       'mediawiki.ForeignStructuredUpload.config' => [
+               'class' => 'ResourceLoaderUploadDialogModule',
+       ],
        'mediawiki.ForeignStructuredUpload' => [
                'scripts' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js',
                'dependencies' => [
                        'mediawiki.ForeignUpload',
+                       'mediawiki.ForeignStructuredUpload.config',
                ],
        ],
        'mediawiki.Upload.Dialog' => [
@@ -1284,6 +1289,7 @@ return [
                        'mediawiki.widgets.CategorySelector',
                        'mediawiki.widgets.DateInputWidget',
                        'mediawiki.jqueryMsg',
+                       'mediawiki.api.messages',
                        'moment',
                        'mediawiki.libs.jpegmeta',
                ],
@@ -1291,15 +1297,12 @@ return [
                        'upload-form-label-own-work',
                        'upload-form-label-infoform-categories',
                        'upload-form-label-infoform-date',
-                       'upload-form-label-own-work-message-default',
-                       'upload-form-label-not-own-work-message-default',
-                       'upload-form-label-not-own-work-local-default',
-                       'upload-form-label-own-work-message-shared',
-                       'upload-form-label-not-own-work-message-shared',
-                       'upload-form-label-not-own-work-local-shared',
-                       'upload-form-label-own-work-message-local',
-                       'upload-form-label-not-own-work-message-local',
-                       'upload-form-label-not-own-work-local-local',
+                       'upload-form-label-own-work-message-generic-local',
+                       'upload-form-label-not-own-work-message-generic-local',
+                       'upload-form-label-not-own-work-local-generic-local',
+                       'upload-form-label-own-work-message-generic-foreign',
+                       'upload-form-label-not-own-work-message-generic-foreign',
+                       'upload-form-label-not-own-work-local-generic-foreign',
                ],
        ],
        'mediawiki.toc' => [
index b1a63b0..4c75e33 100644 (file)
@@ -14,7 +14,7 @@
                        $table = $( '#mw_metadata' ),
                        $tbody = $table.find( 'tbody' );
 
-               if ( !$tbody.length || !$tbody.find( '.collapsable' ).length ) {
+               if ( !$tbody.find( '.collapsable' ).length ) {
                        return;
                }
 
index cfdf797..ec6cee4 100644 (file)
@@ -534,6 +534,7 @@ table.mw_metadata caption {
 
 table.mw_metadata th {
        font-weight: normal;
+       text-align: center;
 }
 
 table.mw_metadata td {
@@ -547,7 +548,6 @@ table.mw_metadata {
 
 table.mw_metadata td,
 table.mw_metadata th {
-       text-align: center;
        border: 1px solid #aaaaaa;
        padding-left: 5px;
        padding-right: 5px;
index 58115c3..2eb84e6 100644 (file)
         * @constructor
         * @inheritdoc
         */
-       function ForeignTitle() {
-               ForeignTitle.parent.apply( this, arguments );
+       function ForeignTitle( title, namespace ) {
+               // We only need to handle categories here... but we don't know the target language.
+               // So assume that any namespace-like prefix is the 'Category' namespace...
+               title = title.replace( /^(.+?)_*:_*(.*)$/, 'Category:$2' ); // HACK
+               ForeignTitle.parent.call( this, title, namespace );
        }
        OO.inheritClass( ForeignTitle, mw.Title );
        ForeignTitle.prototype.getNamespacePrefix = function () {
index 8509fbc..16fec73 100644 (file)
                var booklet = this;
                return mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this ).then(
                        function () {
-                               // Point the CategorySelector to the right wiki
-                               return booklet.upload.getApi().then(
-                                       function ( api ) {
+                               return $.when(
+                                       // Point the CategorySelector to the right wiki
+                                       booklet.upload.getApi().then( function ( api ) {
                                                // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
                                                if ( api.apiUrl ) {
                                                        // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
                                                        booklet.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
                                                }
                                                return $.Deferred().resolve();
-                                       },
-                                       function () {
-                                               return $.Deferred().resolve();
-                                       }
+                                       } ),
+                                       // Set up booklet fields and license messages to match configuration
+                                       booklet.upload.loadConfig().then( function ( config ) {
+                                               var
+                                                       msgPromise,
+                                                       isLocal = booklet.upload.target === 'local',
+                                                       fields = config.fields,
+                                                       msgs = config.licensemessages[ isLocal ? 'local' : 'foreign' ];
+
+                                               // Hide disabled fields
+                                               booklet.descriptionField.toggle( !!fields.description );
+                                               booklet.categoriesField.toggle( !!fields.categories );
+                                               booklet.dateField.toggle( !!fields.date );
+                                               // Update form validity
+                                               booklet.onInfoFormChange();
+
+                                               // Load license messages from the remote wiki if we don't have these messages locally
+                                               // (this means that we only load messages from the foreign wiki for custom config)
+                                               if ( mw.message( 'upload-form-label-own-work-message-' + msgs ).exists() ) {
+                                                       msgPromise = $.Deferred().resolve();
+                                               } else {
+                                                       msgPromise = booklet.upload.apiPromise.then( function ( api ) {
+                                                               return api.loadMessages( [
+                                                                       'upload-form-label-own-work-message-' + msgs,
+                                                                       'upload-form-label-not-own-work-message-' + msgs,
+                                                                       'upload-form-label-not-own-work-local-' + msgs
+                                                               ] );
+                                                       } );
+                                               }
+
+                                               // Update license messages
+                                               return msgPromise.then( function () {
+                                                       booklet.$ownWorkMessage
+                                                               .msg( 'upload-form-label-own-work-message-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                                       booklet.$notOwnWorkMessage
+                                                               .msg( 'upload-form-label-not-own-work-message-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                                       booklet.$notOwnWorkLocal
+                                                               .msg( 'upload-form-label-not-own-work-local-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                               } );
+                                       } )
                                );
-                       },
-                       function () {
-                               return $.Deferred().resolve();
                        }
+               ).then(
+                       null,
+                       // Always resolve, never reject
+                       function () { return $.Deferred().resolve(); }
                );
        };
 
         * @inheritdoc
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
-               var fieldset, $ownWorkMessage, $notOwnWorkMessage,
-                       ownWorkMessage, notOwnWorkMessage, notOwnWorkLocal,
-                       validTargets = mw.config.get( 'wgForeignUploadTargets' ),
-                       target = this.target || validTargets[ 0 ] || 'local',
+               var fieldset,
                        layout = this;
 
-               // upload-form-label-own-work-message-local
-               // upload-form-label-own-work-message-shared
-               ownWorkMessage = mw.message( 'upload-form-label-own-work-message-' + target );
-               // upload-form-label-not-own-work-message-local
-               // upload-form-label-not-own-work-message-shared
-               notOwnWorkMessage = mw.message( 'upload-form-label-not-own-work-message-' + target );
-               // upload-form-label-not-own-work-local-local
-               // upload-form-label-not-own-work-local-shared
-               notOwnWorkLocal = mw.message( 'upload-form-label-not-own-work-local-' + target );
-
-               if ( !ownWorkMessage.exists() ) {
-                       ownWorkMessage = mw.message( 'upload-form-label-own-work-message-default' );
-               }
-               if ( !notOwnWorkMessage.exists() ) {
-                       notOwnWorkMessage = mw.message( 'upload-form-label-not-own-work-message-default' );
-               }
-               if ( !notOwnWorkLocal.exists() ) {
-                       notOwnWorkLocal = mw.message( 'upload-form-label-not-own-work-local-default' );
-               }
-
-               $ownWorkMessage = $( '<p>' ).append( ownWorkMessage.parseDom() )
+               // These elements are filled with text in #initialize
+               // TODO Refactor this to be in one place
+               this.$ownWorkMessage = $( '<p>' )
                        .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
-               $notOwnWorkMessage = $( '<div>' ).append(
-                       $( '<p>' ).append( notOwnWorkMessage.parseDom() ),
-                       $( '<p>' ).append( notOwnWorkLocal.parseDom() )
-               );
-               $ownWorkMessage.add( $notOwnWorkMessage ).find( 'a' ).attr( 'target', '_blank' );
+               this.$notOwnWorkMessage = $( '<p>' );
+               this.$notOwnWorkLocal = $( '<p>' );
 
                this.selectFileWidget = new OO.ui.SelectFileWidget( {
                        showDropTarget: true
                } );
                this.messageLabel = new OO.ui.LabelWidget( {
-                       label: $notOwnWorkMessage
+                       label: $( '<div>' ).append(
+                               this.$notOwnWorkMessage,
+                               this.$notOwnWorkLocal
+                       )
                } );
                this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
                        layout.messageLabel.toggle( !on );
                                align: 'inline',
                                label: $( '<div>' ).append(
                                        $( '<p>' ).text( mw.msg( 'upload-form-label-own-work' ) ),
-                                       $ownWorkMessage
+                                       this.$ownWorkMessage
                                )
                        } ),
                        new OO.ui.FieldLayout( this.messageLabel, {
                        mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
                } );
 
+               this.filenameField = new OO.ui.FieldLayout( this.filenameWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-name' ),
+                       align: 'top',
+                       classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
+                       notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
+               } );
+               this.descriptionField = new OO.ui.FieldLayout( this.descriptionWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-description' ),
+                       align: 'top',
+                       classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
+                       notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
+               } );
+               this.categoriesField = new OO.ui.FieldLayout( this.categoriesWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-categories' ),
+                       align: 'top'
+               } );
+               this.dateField = new OO.ui.FieldLayout( this.dateWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-date' ),
+                       align: 'top'
+               } );
+
                fieldset = new OO.ui.FieldsetLayout( {
                        label: mw.msg( 'upload-form-label-infoform-title' )
                } );
                fieldset.addItems( [
-                       new OO.ui.FieldLayout( this.filenameWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-name' ),
-                               align: 'top',
-                               classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
-                               notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
-                       } ),
-                       new OO.ui.FieldLayout( this.descriptionWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-description' ),
-                               align: 'top',
-                               classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
-                               notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
-                       } ),
-                       new OO.ui.FieldLayout( this.categoriesWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-categories' ),
-                               align: 'top'
-                       } ),
-                       new OO.ui.FieldLayout( this.dateWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-date' ),
-                               align: 'top'
-                       } )
+                       this.filenameField,
+                       this.descriptionField,
+                       this.categoriesField,
+                       this.dateField
                ] );
                this.infoForm = new OO.ui.FormLayout( {
                        classes: [ 'mw-upload-bookletLayout-infoForm' ],
         * @inheritdoc
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () {
-               var layout = this;
-               $.when(
-                       this.filenameWidget.getValidity(),
-                       this.descriptionWidget.getValidity(),
-                       this.dateWidget.getValidity()
-               ).done( function () {
+               var layout = this,
+                       validityPromises = [];
+
+               validityPromises.push( this.filenameWidget.getValidity() );
+               if ( this.descriptionField.isVisible() ) {
+                       validityPromises.push( this.descriptionWidget.getValidity() );
+               }
+               if ( this.dateField.isVisible() ) {
+                       validityPromises.push( this.dateWidget.getValidity() );
+               }
+
+               $.when.apply( $, validityPromises ).done( function () {
                        layout.emit( 'infoValid', true );
                } ).fail( function () {
                        layout.emit( 'infoValid', false );
index f90071c..4a0366a 100644 (file)
@@ -1,4 +1,4 @@
-( function ( mw, OO ) {
+( function ( mw, $, OO ) {
        /**
         * @class mw.ForeignStructuredUpload
         * @extends mw.ForeignUpload
                this.descriptions = [];
                this.categories = [];
 
+               // Config for uploads to local wiki.
+               // Can be overridden with foreign wiki config when #loadConfig is called.
+               this.config = mw.config.get( 'wgUploadDialog' );
+
                mw.ForeignUpload.call( this, target, apiconfig );
        }
 
        OO.inheritClass( ForeignStructuredUpload, mw.ForeignUpload );
 
+       /**
+        * Get the configuration for the form and filepage from the foreign wiki, if any, and use it for
+        * this upload.
+        *
+        * @return {jQuery.Promise} Promise returning config object
+        */
+       ForeignStructuredUpload.prototype.loadConfig = function () {
+               var deferred,
+                       upload = this;
+
+               if ( this.configPromise ) {
+                       return this.configPromise;
+               }
+
+               if ( this.target === 'local' ) {
+                       deferred = $.Deferred();
+                       setTimeout( function () {
+                               // Resolve asynchronously, so that it's harder to accidentally write synchronous code that
+                               // will break for cross-wiki uploads
+                               deferred.resolve( upload.config );
+                       } );
+                       this.configPromise = deferred.promise();
+               } else {
+                       this.configPromise = this.apiPromise.then( function ( api ) {
+                               // Get the config from the foreign wiki
+                               return api.get( {
+                                       action: 'query',
+                                       meta: 'siteinfo',
+                                       siprop: 'uploaddialog',
+                                       // For convenient true/false booleans
+                                       formatversion: 2
+                               } ).then( function ( resp ) {
+                                       // Foreign wiki might be running a pre-1.27 MediaWiki, without support for this
+                                       if ( resp.query && resp.query.uploaddialog ) {
+                                               upload.config = resp.query.uploaddialog;
+                                       }
+                                       return upload.config;
+                               } );
+                       } );
+               }
+
+               return this.configPromise;
+       };
+
        /**
         * Add categories to the upload.
         *
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getText = function () {
-               return (
-                       '== {{int:filedesc}} ==\n' +
-                       '{{Information' +
-                       '\n|description=' +
-                       this.getDescriptions() +
-                       '\n|date=' +
-                       this.getDate() +
-                       '\n|source=' +
-                       this.getSource() +
-                       '\n|author=' +
-                       this.getUser() +
-                       '\n}}\n\n' +
-                       '== {{int:license-header}} ==\n' +
-                       this.getLicense() +
-                       '\n\n' +
-                       this.getCategories()
-               );
+               return this.config.format.filepage
+                       // Replace "numbered parameters" with the given information
+                       .replace( '$DESCRIPTION', this.getDescriptions() )
+                       .replace( '$DATE', this.getDate() )
+                       .replace( '$SOURCE', this.getSource() )
+                       .replace( '$AUTHOR', this.getUser() )
+                       .replace( '$LICENSE', this.getLicense() )
+                       .replace( '$CATEGORIES', this.getCategories() );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       ForeignStructuredUpload.prototype.getComment = function () {
+               return this.config.comment
+                       .replace( '$PAGENAME', mw.config.get( 'wgPageName' ) )
+                       .replace( '$HOST', location.host );
        };
 
        /**
 
                for ( i = 0; i < this.descriptions.length; i++ ) {
                        desc = this.descriptions[ i ];
-                       templateCalls.push( '{{' + desc.language + '|1=' + desc.text + '}}' );
+                       templateCalls.push(
+                               this.config.format.description
+                                       .replace( '$LANGUAGE', desc.language )
+                                       .replace( '$TEXT', desc.text )
+                       );
                }
 
                return templateCalls.join( '\n' );
                var i, cat, categoryLinks = [];
 
                if ( this.categories.length === 0 ) {
-                       return '{{subst:unc}}';
+                       return this.config.format.uncategorized;
                }
 
                for ( i = 0; i < this.categories.length; i++ ) {
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getLicense = function () {
-               // Make sure this matches the messages for different targets in
-               // mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm
-               return this.target === 'shared' ? '{{self|cc-by-sa-4.0}}' : '';
+               return this.config.format.license;
        };
 
        /**
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getSource = function () {
-               return '{{own}}';
+               return this.config.format.ownwork;
        };
 
        /**
        };
 
        mw.ForeignStructuredUpload = ForeignStructuredUpload;
-}( mediaWiki, OO ) );
+}( mediaWiki, jQuery, OO ) );
index 1a0b59a..eeeab68 100644 (file)
                // actual API call methods to wait for the apiPromise to resolve
                // before continuing.
                mw.Upload.call( this, null );
-
-               if ( this.target !== 'local' ) {
-                       // Keep these untranslated. We don't know the content language of the foreign wiki, best to
-                       // stick to English in the text.
-                       this.setComment( 'Cross-wiki upload from ' + location.host );
-               }
        }
 
        OO.inheritClass( ForeignUpload, mw.Upload );
index 2b28cb4..7a7469a 100644 (file)
 
                return this.upload.getApi().then(
                        function ( api ) {
-                               // If the user can't upload anything, don't give them the option to.
-                               return api.getUserInfo().then(
-                                       function ( userInfo ) {
+                               return $.when(
+                                       booklet.upload.loadConfig(),
+                                       // If the user can't upload anything, don't give them the option to.
+                                       api.getUserInfo().then( function ( userInfo ) {
                                                if ( userInfo.rights.indexOf( 'upload' ) === -1 ) {
                                                        // TODO Use a better error message when not all logged-in users can upload
                                                        booklet.getPage( 'upload' ).$element.msg( 'api-error-mustbeloggedin' );
                                                }
                                                return $.Deferred().resolve();
-                                       },
-                                       function () {
-                                               return $.Deferred().resolve();
-                                       }
+                                       } )
+                               ).then(
+                                       null,
+                                       // Always resolve, never reject
+                                       function () { return $.Deferred().resolve(); }
                                );
                        },
                        function ( errorMsg ) {
index 260fd37..8f0ad6b 100644 (file)
@@ -4,16 +4,6 @@
        margin: 1em 0;
 }
 
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty,
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty .oo-ui-fieldLayout-body {
-       display: none;
-}
-
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-errors {
-       /* Override 'display: none' from above */
-       display: block;
-}
-
 .mw-htmlform-ooui .mw-htmlform-submit-buttons {
        margin-top: 1em;
 }
index e905f69..b8d0b09 100644 (file)
                        } catch ( e ) {
                                fallback = parser.settings.messages.get( key );
                                mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
+                               mw.track( 'mediawiki.jqueryMsg.error', {
+                                       messageKey: key,
+                                       errorMessage: e.message
+                               } );
                                return $( '<span>' ).text( fallback );
                        }
                };
                                return result === null ? null : result.join( '' );
                        }
 
-                       asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ );
+                       asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
                        htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
                        htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
 
                                return result;
                        }
 
+                       // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
+                       function nowiki() {
+                               var parsedResult, plainText,
+                                       result = null;
+
+                               parsedResult = sequence( [
+                                       makeStringParser( '<nowiki>' ),
+                                       // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
+                                       makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
+                                       makeStringParser( '</nowiki>' )
+                               ] );
+                               if ( parsedResult !== null ) {
+                                       plainText = parsedResult[ 1 ];
+                                       result = [ 'CONCAT' ].concat( plainText );
+                               }
+
+                               return result;
+                       }
+
                        templateName = transform(
                                // see $wgLegalTitleChars
                                // not allowing : due to the need to catch "PLURAL:$1"
                                wikilink,
                                extlink,
                                replacement,
+                               nowiki,
                                html,
                                literal
                        ] );
index cccc468..8dc8a61 100644 (file)
                                RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])',
                                RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
 
-                       return address.search( new RegExp( '^' + RE_IP_ADD + block + '$' ) ) !== -1;
+                       return ( new RegExp( '^' + RE_IP_ADD + block + '$' ).test( address ) );
                },
 
                /**
                        '[0-9A-Fa-f]{1,4}' + '(?::' + '[0-9A-Fa-f]{1,4}' + '){7}' +
                        ')';
 
-                       if ( address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1 ) {
+                       if ( new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) ) {
                                return true;
                        }
 
-                       RE_IPV6_ADD = // contains one "::" in the middle (single '::' check below)
-                               '[0-9A-Fa-f]{1,4}' + '(?:::?' + '[0-9A-Fa-f]{1,4}' + '){1,6}';
+                       // contains one "::" in the middle (single '::' check below)
+                       RE_IPV6_ADD = '[0-9A-Fa-f]{1,4}' + '(?:::?' + '[0-9A-Fa-f]{1,4}' + '){1,6}';
 
-                       return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1
-                               && address.search( /::/ ) !== -1 && address.search( /::.*::/ ) === -1;
+                       return (
+                               new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address )
+                               && /::/.test( address )
+                               && !/::.*::/.test( address )
+                       );
                },
 
                /**
diff --git a/resources/src/moment-dmy.js b/resources/src/moment-dmy.js
new file mode 100644 (file)
index 0000000..c67b93e
--- /dev/null
@@ -0,0 +1,16 @@
+// Use DMY date format for Moment.js, in accordance with MediaWiki's date formatting routines.
+// This affects English only (and languages without localisations, that fall back to English).
+// http://momentjs.com/docs/#/customization/long-date-formats/
+/*global moment */
+moment.locale( 'en', {
+       longDateFormat: {
+               // Unchanged, but have to be repeated here:
+               LT: 'h:mm A',
+               LTS: 'h:mm:ss A',
+               // Customized:
+               L: 'DD/MM/YYYY',
+               LL: 'D MMMM YYYY',
+               LLL: 'D MMMM YYYY LT',
+               LLLL: 'dddd, D MMMM YYYY LT'
+       }
+} );
index d49fcdc..9af0598 100644 (file)
@@ -1,19 +1,4 @@
-// Use DMY date format for Moment.js, in accordance with MediaWiki's date formatting routines.
-// This affects English only (and languages without localisations, that fall back to English).
-// http://momentjs.com/docs/#/customization/long-date-formats/
 /*global moment, mw */
-moment.locale( 'en', {
-       longDateFormat: {
-               // Unchanged, but have to be repeated here:
-               LT: 'h:mm A',
-               LTS: 'h:mm:ss A',
-               // Customized:
-               L: 'DD/MM/YYYY',
-               LL: 'D MMMM YYYY',
-               LLL: 'D MMMM YYYY LT',
-               LLLL: 'dddd, D MMMM YYYY LT'
-       }
-} );
 
 // HACK: Overwrite moment's i18n with MediaWiki's for the current language so that
 // wgTranslateNumerals is respected.
index 26085b8..ebb6d90 100644 (file)
@@ -65,6 +65,10 @@ $wgAutoloadClasses += [
        'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php",
        'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php",
 
+       # tests/phpunit/includes/auth
+       'MediaWiki\\Auth\\AuthenticationRequestTestCase' =>
+               "$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php",
+
        # tests/phpunit/includes/changes
        'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php",
 
index 78e5f6f..1be2d62 100644 (file)
@@ -27,6 +27,7 @@
  * @file
  * @ingroup Testing
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * @ingroup Testing
@@ -210,6 +211,7 @@ class ParserTest {
                # add a namespace shadowing a interwiki link, to test
                # proper precedence when resolving links. (bug 51680)
                $wgExtraNamespaces[100] = 'MemoryAlpha';
+               $wgExtraNamespaces[101] = 'MemoryAlpha talk';
 
                // XXX: tests won't run without this (for CACHE_DB)
                if ( $wgMainCacheType === CACHE_DB ) {
@@ -331,6 +333,18 @@ class ParserTest {
                Hooks::clear( 'InterwikiLoadPrefix' );
        }
 
+       /**
+        * Reset the Title-related services that need resetting
+        * for each test
+        */
+       public static function resetTitleServices() {
+               $services = MediaWikiServices::getInstance();
+               $services->resetServiceForTesting( 'TitleFormatter' );
+               $services->resetServiceForTesting( 'TitleParser' );
+               $services->resetServiceForTesting( '_MediaWikiTitleCodec' );
+
+       }
+
        public function setupRecorder( $options ) {
                if ( isset( $options['record'] ) ) {
                        $this->recorder = new DbTestRecorder( $this );
@@ -900,7 +914,6 @@ class ParserTest {
                        'wgExperimentalHtmlIds' => false,
                        'wgExternalLinkTarget' => false,
                        'wgHtml5' => true,
-                       'wgWellFormedXml' => true,
                        'wgAdaptiveMessageCache' => true,
                        'wgDisableLangConversion' => false,
                        'wgDisableTitleConversion' => false,
@@ -958,6 +971,8 @@ class ParserTest {
                MWTidy::destroySingleton();
                RepoGroup::destroySingleton();
 
+               self::resetTitleServices();
+
                return $context;
        }
 
index 23bdbde..7051b4f 100644 (file)
@@ -5632,7 +5632,7 @@ Parenthesis in external links, w/ transclusion or comment
 </p><p>(<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
 </p>
 !! html/parsoid
-<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[20,31,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;hi&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">hi&lt;/span>"}]]}'>http://example.com/hi</a>)</p>
+<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}&#39;>hi&lt;/span>"}]]}'>http://example.com/hi</a>)</p>
 
 <p>(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com&lt;!-- hi -->"}}'>http://example.com</a>)</p>
 !! end
@@ -5650,7 +5650,7 @@ parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 text
 <nowiki>*</nowiki>text
 <nowiki>[[foo]]</nowiki>
-<nowiki>*[[foo]]</nowiki>
+<nowiki>*</nowiki>a <nowiki>[[foo]]</nowiki>
 !! end
 
 !! test
@@ -5658,7 +5658,7 @@ mw:ExtLink -vs- mw:WikiLink (T94723)
 !! options
 parsoid=html2wt
 !! html/parsoid
-<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"piped","a":{"href":"./Foo"},"sa":{"href":"Foo"},"dsr":[0,11,6,2]}'>Bar</a>
+<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"piped","a":{"href":"./Foo"},"sa":{"href":"Foo"}}'>Bar</a>
 <a rel="mw:WikiLink" href="./Foo" title="Foo">Bar</a>
 <a rel="mw:WikiLink" href="http://en.wikipedia.org/wiki/Foo" title="Foo">Bar</a>
 <a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" title="Foo">Bar</a>
@@ -7110,6 +7110,25 @@ parsoid=html2wt
 |}
 !! end
 
+!! test
+Testing serialization after deletion in references
+!! options
+parsoid={
+  "modes": ["wt2wt"],
+  "changes": [
+    ["#x", "remove"]
+  ]
+}
+!! wikitext
+hi <ref><div id="x">ho</div></ref>
+
+<references />
+!! wikitext/edited
+hi <ref></ref>
+
+<references />
+!! end
+
 !!test
 Testing serialization after deletion of table cells
 !!options
@@ -7572,10 +7591,10 @@ Broken image links with HTML captions (bug 39700)
 <a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">abc</a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span>
-<span typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100"/></a></span>
-<span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;lt;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;&lt;&amp;quot;,&amp;quot;dsr&amp;quot;:[107,111,null,null]}\">&amp;lt;&lt;/span>"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span>
-<span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"a&lt;i data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[134,142,3,4]}\">b&lt;/i>c"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;script>&lt;/script>"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script>&amp;lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"100x100px"},{"ck":"caption","ak":"&lt;script>&lt;/script>"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script>&amp;lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"100","width":"100"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp;lt;"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=&#39;{\"src\":\"&amp;amp;lt;\",\"srcContent\":\"&amp;lt;\",\"dsr\":[107,111,null,null]}&#39;>&amp;lt;&lt;/span>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"a&lt;i>b&lt;/i>c"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"a&lt;i data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[134,142,3,4]}&#39;>b&lt;/i>c"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span></p>
 !! end
 
 !! test
@@ -8432,7 +8451,7 @@ Blah blah blah
 !! wikitext
 #REDIRECT [[{{echo|Foo}}bar]]
 !! html/parsoid
-<link typeof="mw:ExpandedAttrs" rel="mw:PageProp/redirect" href="./Foobar" data-mw='{"attribs":[[{"txt":"href"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[12,24,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;Foo&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">Foo&lt;/span>bar"}]]}'/>
+<link about="#mwt2" typeof="mw:ExpandedAttrs" rel="mw:PageProp/redirect" href="./Foobar" data-parsoid='{"a":{"href":"./Foobar"},"sa":{"href":"{{echo|Foo}}bar"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[12,24,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"Foo\"}},\"i\":0}}]}&#39;>Foo&lt;/span>bar"}]]}'/>
 !! end
 
 !! test
@@ -9912,7 +9931,7 @@ Parsoid: Page property magic word with magic word contents
 !! wikitext
 {{DISPLAYTITLE:''{{PAGENAME}}''}}
 !! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=\"{&amp;quot;dsr&amp;quot;:[15,31,2,2]}\">&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[]],&amp;quot;dsr&amp;quot;:[17,29,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;PAGENAME&amp;quot;,&amp;quot;function&amp;quot;:&amp;quot;pagename&amp;quot;},&amp;quot;params&amp;quot;:{},&amp;quot;i&amp;quot;:0}}]}\">Main Page&lt;/span>&lt;/i>"}]]}'/>
+<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:&#39;&#39;{{PAGENAME}}&#39;&#39;}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=&#39;{\"dsr\":[15,31,2,2]}&#39;>&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[17,29,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}&#39;>Main Page&lt;/span>&lt;/i>"}]]}'/>
 !! end
 
 !! test
@@ -9920,7 +9939,7 @@ Parsoid: Template-generated DISPLAYTITLE
 !! wikitext
 {{{{echo|DISPLAYTITLE}}:Foo}}
 !! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,29,null,null],"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DISPLAYTITLE}}:Foo"},"params":{},"i":0}}]}'/>
+<meta property="mw:PageProp/displaytitle" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DISPLAYTITLE}}:Foo"},"params":{},"i":0}}]}'/>
 !! end
 
 !! test
@@ -11211,10 +11230,9 @@ parsoid=wt2html
 |c
 |}
 !!html/parsoid
-<meta typeof="mw:Includes/IncludeOnly"/><meta typeof="mw:Includes/IncludeOnly/End"/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[31,38,null,null],&amp;quot;src&amp;quot;:&amp;quot;{{{b}}}&amp;quot;}\">{{{b}}}&lt;/span>"},{"html":""}]]}' data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}'>
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}' data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=&#39;{\"dsr\":[31,38,null,null],\"src\":\"{{{b}}}\"}&#39;>{{{b}}}&lt;/span>"},{"html":""}]]}'>
 <tbody><tr><td>c</td></tr>
 </tbody></table>
-
 !!end
 
 ###
@@ -11572,7 +11590,7 @@ Templates: Support for templates generating attributes and content
 <div style="background:#f9f9f9;">foo</div>
 
 !! html/parsoid
-<div style="background:#f9f9f9;" about="#mwt3" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html"}' data-mw='{"attribs":[[{"txt":"style","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[5,49,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;style{{=}}\\&amp;quot;background:&amp;amp;#35;f9f9f9;\\&amp;quot;&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">style&lt;/span>&lt;span typeof=\"mw:Nowiki\" about=\"#mwt1\" data-parsoid=\"{}\">=&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">\"background:&lt;/span>&lt;span typeof=\"mw:Entity\" about=\"#mwt1\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;#35;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;#&amp;quot;}\">#&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">f9f9f9;\"&lt;/span>"},{"html":""}]]}'>foo</div>
+<div style="background:#f9f9f9;" about="#mwt3" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html"}' data-mw='{"attribs":[[{"txt":"style","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[5,49,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"style{{=}}\\\"background:&amp;amp;#35;f9f9f9;\\\"\"}},\"i\":0}}]}&#39;>style&lt;/span>&lt;span typeof=\"mw:Nowiki\" about=\"#mwt1\" data-parsoid=\"{}\">=&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">\"background:&lt;/span>&lt;span typeof=\"mw:Entity\" about=\"#mwt1\" data-parsoid=&#39;{\"src\":\"&amp;amp;#35;\",\"srcContent\":\"#\"}&#39;>#&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">f9f9f9;\"&lt;/span>"},{"html":""}]]}'>foo</div>
 !! end
 
 !! test
@@ -12931,7 +12949,7 @@ parsoid=wt2html,wt2wt,html2html
 <div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
 
 !! html/parsoid
-<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[24,38,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"width","ak":"{{echo|137px}}"},{"ck":"caption","ak":"This is a caption"}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[24,38,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"137px\"}},\"i\":0}}]}&#39;>137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"16","width":"137"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>This is a caption</figcaption></figure>
 !! end
 
 !! test
@@ -12942,7 +12960,7 @@ parsoid=wt2html,wt2wt,html2html
 <div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
 
 !! html/parsoid
-<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt3" data-mw='{"attribs":[["thumbnail",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[18,32,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;thumb&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">thumb&lt;/span>"}],["width",{"html":"&lt;span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[33,47,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt3" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"{{echo|thumb}}"},{"ck":"width","ak":"{{echo|137px}}"},{"ck":"caption","ak":"This is a caption"}]}' data-mw='{"attribs":[["thumbnail",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,32,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"thumb\"}},\"i\":0}}]}&#39;>thumb&lt;/span>"}],["width",{"html":"&lt;span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[33,47,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"137px\"}},\"i\":0}}]}&#39;>137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"16","width":"137"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>This is a caption</figcaption></figure>
 !! end
 
 !! test
@@ -12953,7 +12971,7 @@ parsoid=wt2html,wt2wt,html2html
 <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a>
 </p>
 !! html/parsoid
-<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[18,31,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;50px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">50px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p>
+<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,31,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"50px\"}},\"i\":0}}]}&#39;>50px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 ## Parsoid does not provide editing support for images where templates produce multiple image attributes.
@@ -13323,8 +13341,9 @@ Image with wiki markup in implicit alt
 </p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"testing &lt;b data-parsoid=\"{&amp;quot;dsr&amp;quot;:[27,37,3,3]}\">bold&lt;/b> in alt"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></span></p>
-<p><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt","resource":"Image:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt"}]}' data-mw='{"caption":"testing &lt;b data-parsoid=&#39;{\"dsr\":[27,37,3,3]}&#39;>bold&lt;/b> in alt"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></span></p>
+
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"alt","ak":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt","resource":"Image:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13335,7 +13354,7 @@ Alt image option should handle most kinds of wikitext without barfing
 <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="This is a link and a bold template." src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is the image caption</div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[65,73,2,2]}\">link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[80,106,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&#39;&#39;bold template&#39;&#39;&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}&#39;>link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;#39;&amp;#39;bold template&amp;#39;&amp;#39;\"}},\"i\":0}}]}&#39;>bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
 !! end
 
 ###################
@@ -13562,7 +13581,7 @@ Frameless image caption with a free URL
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;url&amp;quot;,&amp;quot;dsr&amp;quot;:[18,36,0,0]}\">http://example.com&lt;/a>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"http://example.com"}]}' data-mw='{"caption":"&lt;a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid=&#39;{\"stx\":\"url\",\"dsr\":[18,36,0,0]}&#39;>http://example.com&lt;/a>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13672,7 +13691,7 @@ BUG 648: Frameless image caption with a link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[30,38,2,2]}\">link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,38,2,2]}&#39;>link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13683,7 +13702,7 @@ BUG 648: Frameless image caption with a link (suffix)
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[30,41,2,5],&amp;quot;tail&amp;quot;:&amp;quot;foo&amp;quot;}\">linkfoo&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]]foo in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,41,2,5],\"tail\":\"foo\"}&#39;>linkfoo&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13694,7 +13713,7 @@ BUG 648: Frameless image caption with an interwiki link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;http://www.usemod.com/cgi-bin/mb.pl?Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;MeatBall:Link&amp;quot;},&amp;quot;isIW&amp;quot;:true,&amp;quot;dsr&amp;quot;:[30,47,2,2]}\">MeatBall:Link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,47,2,2]}&#39;>MeatBall:Link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13705,7 +13724,7 @@ BUG 648: Frameless image caption with a piped interwiki link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;piped&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;http://www.usemod.com/cgi-bin/mb.pl?Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;MeatBall:Link&amp;quot;},&amp;quot;isIW&amp;quot;:true,&amp;quot;dsr&amp;quot;:[30,52,16,2]}\">link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link|link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=&#39;{\"stx\":\"piped\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,52,16,2]}&#39;>link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13713,7 +13732,7 @@ T107474: Frameless image caption with <nowiki>
 !! wikitext
 [[File:Foobar.jpg|<nowiki>text with a [[MeatBall:Link|link]] in it</nowiki>]]
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;span typeof=\"mw:Nowiki\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[18,75,8,9]}\">text with a [[MeatBall:Link|link]] in it&lt;/span>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;nowiki>text with a [[MeatBall:Link|link]] in it&lt;/nowiki>"}]}' data-mw='{"caption":"&lt;span typeof=\"mw:Nowiki\" data-parsoid=&#39;{\"dsr\":[18,75,8,9]}&#39;>text with a [[MeatBall:Link|link]] in it&lt;/span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13724,7 +13743,7 @@ Escape HTML special chars in image alt text
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="&amp; &lt; &gt; &quot;"><img alt="&amp; &lt; &gt; &quot;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&amp;amp; &amp;lt; &amp;gt; \""}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp; &lt; > \""}]}' data-mw='{"caption":"&amp;amp; &amp;lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13735,7 +13754,7 @@ BUG 499: Alt text should have &#1234;, not &amp;1234;
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;#9792;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;♀&amp;quot;,&amp;quot;dsr&amp;quot;:[18,25,null,null]}\">♀&lt;/span>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp;#9792;"}]}' data-mw='{"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=&#39;{\"src\":\"&amp;amp;#9792;\",\"srcContent\":\"♀\",\"dsr\":[18,25,null,null]}&#39;>♀&lt;/span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -14073,7 +14092,7 @@ Parsoid-specific image handling - simple image with a formatted caption
 !! wikitext
 [[File:Foobar.jpg|<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>]]
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;table data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[18,81,7,8]}\">&lt;tbody data-parsoid=\"{&amp;quot;dsr&amp;quot;:[25,73,0,0]}\">&lt;tr data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[25,54,4,5]}\">&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[29,39,4,5]}\">a&lt;/td>&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[39,49,4,5]}\">b&lt;/td>&lt;/tr>&lt;tr data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[54,73,4,5]}\">&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[58,68,4,5]}\">c&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;table>&lt;tr>&lt;td>a&lt;/td>&lt;td>b&lt;/td>&lt;/tr>&lt;tr>&lt;td>c&lt;/td>&lt;/tr>&lt;/table>"}]}' data-mw='{"caption":"&lt;table data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[18,81,7,8]}&#39;>&lt;tbody data-parsoid=&#39;{\"dsr\":[25,73,0,0]}&#39;>&lt;tr data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[25,54,4,5]}&#39;>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[29,39,4,5]}&#39;>a&lt;/td>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[39,49,4,5]}&#39;>b&lt;/td>&lt;/tr>&lt;tr data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[54,73,4,5]}&#39;>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[58,68,4,5]}&#39;>c&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -14167,7 +14186,7 @@ T93580: 2. <ref> inside inline images
 
 <references />
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: &lt;ref>foo&lt;/ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[64,78,5,6]}\" data-mw=\"{&amp;quot;name&amp;quot;:&amp;quot;ref&amp;quot;,&amp;quot;body&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;mw-reference-text-cite_note-1&amp;quot;},&amp;quot;attrs&amp;quot;:{}}\">&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=\"{&amp;quot;group&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;foo&amp;quot;,&amp;quot;hasRefInRef&amp;quot;:false,&amp;quot;dsr&amp;quot;:[64,78,5,6],&amp;quot;tmp&amp;quot;:{}}\" data-mw=\"{}\">"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: &lt;ref>foo&lt;/ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[64,78,5,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-1\"},\"attrs\":{}}&#39;>&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=&#39;{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,78,5,6]}&#39;/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol>
 !! end
@@ -14179,7 +14198,7 @@ T93580: 3. Templated <ref> inside inline images
 
 <references />
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|&lt;ref>{{echo|foo}}&lt;/ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion  mw:Extension/ref\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[64,96,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&lt;ref>{{echo|foo}}&lt;/ref>&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Transclusion mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=\"{&amp;quot;group&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;foo&amp;quot;,&amp;quot;hasRefInRef&amp;quot;:false,&amp;quot;dsr&amp;quot;:[64,96,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;tmp&amp;quot;:{}}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&lt;ref>{{echo|foo}}&lt;/ref>&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|&lt;ref>{{echo|foo}}&lt;/ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion  mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;lt;ref>{{echo|foo}}&amp;lt;/ref>\"}},\"i\":0}}]}&#39;>&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Transclusion mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=&#39;{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;lt;ref>{{echo|foo}}&amp;lt;/ref>\"}},\"i\":0}}]}&#39;/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol>
 !! end
@@ -14781,7 +14800,7 @@ Parsoid: Defaultsort (template-generated)
 !! wikitext
 {{{{echo|DEFAULTSORT}}:Foo}}
 !! html/parsoid
-<meta property="mw:PageProp/categorydefaultsort" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,28,null,null],"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DEFAULTSORT}}:Foo"},"params":{},"i":0}}]}'/>
+<meta property="mw:PageProp/categorydefaultsort" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DEFAULTSORT}}:Foo"},"params":{},"i":0}}]}'/>
 !! end
 
 ###
@@ -16010,7 +16029,7 @@ Bug 2304: HTML attribute safety (dangerous template; 2309)
 <div title=""></div>
 
 !! html/parsoid
-<div title='" onmouseover="alert(document.cookie)' about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"title":"\" onmouseover=\"alert(document.cookie)"},"sa":{"title":"{{dangerous attribute}}"}}' data-mw='{"attribs":[[{"txt":"title"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[]],&amp;quot;dsr&amp;quot;:[12,35,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;dangerous attribute&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Dangerous_attribute&amp;quot;},&amp;quot;params&amp;quot;:{},&amp;quot;i&amp;quot;:0}}]}\">\" onmouseover=\"alert(document.cookie)&lt;/span>"}]]}'></div>
+<div title='" onmouseover="alert(document.cookie)' about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"title":"\" onmouseover=\"alert(document.cookie)"},"sa":{"title":"{{dangerous attribute}}"}}' data-mw='{"attribs":[[{"txt":"title"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[12,35,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"dangerous attribute\",\"href\":\"./Template:Dangerous_attribute\"},\"params\":{},\"i\":0}}]}&#39;>\" onmouseover=\"alert(document.cookie)&lt;/span>"}]]}'></div>
 !! end
 
 !! test
@@ -16549,9 +16568,12 @@ array (
 <pre typeof="mw:Extension/tag" data-mw='{"name":"tag","attrs":{"foo":"bar"},"body":null}' data-parsoid='{}' about="#mwt2"></pre>text
 !! end
 
-# </tag> should be output literally since there is no matching tag that begins it
+## </tag> should be output literally since there is no matching tag that begins it
+## Don't expect parsoid to rt this form.
 !! test
 Parser hook: basic arguments using terminated empty elements (bug 2374)
+!! options
+parsoid=wt2html
 !! wikitext
 <tag width=200 height = "100" depth = '50' square/>
 other stuff
@@ -16569,6 +16591,28 @@ array (
 <p>other stuff
 &lt;/tag&gt;
 </p>
+!! html/parsoid
+<pre typeof="mw:Extension/tag" data-mw='{"name":"tag","attrs":{"width":"200","height":"100","depth":"50","square":""},"body":null}' about="#mwt2"></pre><p>other stuff
+&lt;/tag></p>
+!! end
+
+## Don't expect parsoid to rt this form.
+!! test
+Parser hook: Don't allow unclosed extension tags
+!! options
+parsoid=wt2html
+!! wikitext
+test <tag>123
+
+this is a '''test'''
+!! html/php
+<p>test &lt;tag&gt;123
+</p><p>this is a <b>test</b>
+</p>
+!! html/parsoid
+<p>test &lt;tag>123</p>
+
+<p>this is a <b>test</b></p>
 !! end
 
 ###
@@ -16891,7 +16935,7 @@ HTML nested bullet list, closed tags (bug 5497)
 </ul>
 </li>
 </ul>
-!! html
+!! html/php
 <ul>
 <li>One</li>
 <li>Two:
@@ -16902,6 +16946,16 @@ HTML nested bullet list, closed tags (bug 5497)
 </li>
 </ul>
 
+!! html/parsoid
+<ul data-parsoid='{"stx":"html"}'>
+<li data-parsoid='{"stx":"html"}'>One</li>
+<li data-parsoid='{"stx":"html"}'>Two:
+<ul data-parsoid='{"stx":"html"}'>
+<li data-parsoid='{"stx":"html"}'>Sub-one</li>
+<li data-parsoid='{"stx":"html"}'>Sub-two</li>
+</ul>
+</li>
+</ul>
 !! end
 
 !! test
@@ -17271,7 +17325,7 @@ Fuzz testing: image with bogus manual thumbnail
 <div class="thumb tright"><div class="thumbinner" style="width:182px;">Error creating thumbnail:   <div class="thumbcaption"></div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"manualthumb","ak":"thumbnail= "}],"dsr":[0,32,2,2]}' data-mw='{"errors":[{"key":"missing-thumbnail","message":"This thumbnail does not exist.","params":{"name":""}}],"thumb":""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{},"dsr":[2,30,null,null]}'><img resource="./File:Foobar.jpg" src="./Special:FilePath/" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"220"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure>
+<figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"manualthumb","ak":"thumbnail= "}]}' data-mw='{"errors":[{"key":"missing-thumbnail","message":"This thumbnail does not exist.","params":{"name":""}}],"thumb":""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="./Special:FilePath/" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"220"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure>
 !!end
 
 !! test
@@ -20592,6 +20646,7 @@ this is not the the title
 !! html/php
 Screen
 <p>this is not the the title
+<span class="error"><strong>Warning:</strong> Display title "whatever" was ignored since it is not equivalent to the page's actual title.</span>
 </p>
 !! end
 
@@ -20855,9 +20910,9 @@ percent-encoding and + signs in internal links (Bug 26410)
 <a href="/index.php?title=3E&amp;action=edit&amp;redlink=1" class="new" title="3E (page does not exist)">3E</a> <a href="/index.php?title=3E%2B&amp;action=edit&amp;redlink=1" class="new" title="3E+ (page does not exist)">3E+</a>
 </p>
 !! html/parsoid
-<p><a rel="mw:WikiLink" href="./User:+%25" title="User:+%">User:+%</a> <a rel="mw:WikiLink" href="Page+title%25" title="Page+title%">Page+title%</a>
-<a rel="mw:WikiLink" href="%25+" title="%+">%+</a> <a rel="mw:WikiLink" href="%25+" title="%+">%20</a> <a rel="mw:WikiLink" href="%25+" title="%+">%+ </a> <a rel="mw:WikiLink" href="%25+r" title="%+r">%+r</a>
-<a rel="mw:WikiLink" href="%25" title="%">%</a> <a rel="mw:WikiLink" href="+" title="+">+</a> <span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Bar&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;bar&amp;quot;},&amp;quot;dsr&amp;quot;:[94,101,2,2]}\">bar&lt;/a>"}'><a href="./File:%25+abc9"><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></span>
+<p><a rel="mw:WikiLink" href="./User:+%25" title="User:+%" data-parsoid='{"stx":"simple","a":{"href":"./User:+%25"},"sa":{"href":"User:+%"}}'>User:+%</a> <a rel="mw:WikiLink" href="./Page+title%25" title="Page+title%" data-parsoid='{"stx":"simple","a":{"href":"./Page+title%25"},"sa":{"href":"Page+title%"}}'>Page+title%</a>
+<a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%+</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"piped","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%20</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+ "}}'>%+ </a> <a rel="mw:WikiLink" href="./%25+r" title="%+r" data-parsoid='{"stx":"simple","a":{"href":"./%25+r"},"sa":{"href":"%+r"}}'>%+r</a>
+<a rel="mw:WikiLink" href="./%25" title="%" data-parsoid='{"stx":"simple","a":{"href":"./%25"},"sa":{"href":"%"}}'>%</a> <a rel="mw:WikiLink" href="./+" title="+" data-parsoid='{"stx":"simple","a":{"href":"./+"},"sa":{"href":"+"}}'>+</a> <span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Bar\"},\"sa\":{\"href\":\"bar\"},\"dsr\":[94,101,2,2]}&#39;>bar&lt;/a>"}'><a href="./File:%25+abc9" data-parsoid='{"a":{"href":"./File:%25+abc9"},"sa":{}}'><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></span>
 <a rel="mw:WikiLink" href="./3E" title="3E" data-parsoid='{"stx":"simple","a":{"href":"./3E"},"sa":{"href":"%33%45"}}'>3E</a> <a rel="mw:WikiLink" href="./3E+" title="3E+" data-parsoid='{"stx":"simple","a":{"href":"./3E+"},"sa":{"href":"%33%45+"}}'>3E+</a></p>
 !! end
 
@@ -22108,7 +22163,7 @@ This should just get lost.
 B <span about="#mwt4" class="mw-ref" id="cite_ref-b_2-0" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-b-2"},"attrs":{"name":"b"}}'><a href="#cite_note-b-2"><span class="mw-reflink-text">[2]</span></a></span></p>
 
 
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{},"body":{"html":"\n&lt;span about=\"#mwt8\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[59,82,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-a-1\"},\"attrs\":{\"name\":\"a\"}}&#39;>&lt;a href=\"#cite_note-a-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>\n"}}'><li about="#cite_note-a-1" id="cite_note-a-1"><a href="#cite_ref-a_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-a-1" class="mw-reference-text">foo</span></li><li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">bar</span></li>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{},"body":{"html":"\n&lt;span about=\"#mwt8\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[59,82,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-a-1\"},\"attrs\":{\"name\":\"a\"}}&#39;>&lt;a href=\"#cite_note-a-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>\n"}}'><li about="#cite_note-a-1" id="cite_note-a-1"><a href="#cite_ref-a_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-a-1" class="mw-reference-text">foo</span></li><li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">bar</span></li>
 </ol>
 !! end
 
@@ -22141,7 +22196,7 @@ B <span about="#mwt4" class="mw-ref" id="cite_ref-b_2-0" rel="dc:references" typ
 <li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo bar for a</span></li>
 </ol>
 
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt8" data-mw-group="X" data-mw='{"name":"references","attrs":{"group":"X"},"body":{"html":"\n&lt;span about=\"#mwt10\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[96,119,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-b-2\"},\"attrs\":{\"name\":\"b\"}}&#39;>&lt;a href=\"#cite_note-b-2\" style=\"counter-reset: mw-Ref 1;\" data-mw-group=\"X\">&lt;span class=\"mw-reflink-text\">[X 1]&lt;/span>&lt;/a>&lt;/span>\n"}}'>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt8" data-mw-group="X" data-mw='{"name":"references","attrs":{"group":"X"},"body":{"html":"\n&lt;span about=\"#mwt10\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[96,119,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-b-2\"},\"attrs\":{\"name\":\"b\"}}&#39;>&lt;a href=\"#cite_note-b-2\" style=\"counter-reset: mw-Ref 1;\" data-mw-group=\"X\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[X 1]&lt;/span>&lt;/a>&lt;/span>\n"}}'>
 <li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" data-mw-group="X" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">foo</span></li>
 </ol>
 !! end
@@ -22204,8 +22259,11 @@ Entities in ref name
 </ol>
 !! end
 
-# This test is wt2html only because we're permitting the serializer to produce
-# dirty diffs, normalizing the unclosed references to the self-closed version.
+## The output here may look funny, but it's what the php parser will do.  The
+## unclosed references tag becomes escaped text, and then a new references
+## tag is auto-generated.  The test is wt2html only because it roundtrips with
+## nowiki tags, and the auto-generated references tag is only dropped in
+## rtTestMode.
 !! test
 Generate references for unclosed references tag
 !! options
@@ -22215,9 +22273,10 @@ a<ref>foo</ref>
 
 <references>
 !! html/parsoid
-<p>a<span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1"><span class="mw-reflink-text">[1]</span></a></span></p>
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'>
-<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol>
+<p>a<span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></p>
+
+<p>&lt;references></p>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt3" data-mw='{"name":"references","attrs":{},"autoGenerated":true}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol>
 !! end
 
 !! test
 !! test
 2. Leading whitespace in non-indent-pre contexts should not be escaped
 !! options
-parsoid=htm2wt
+parsoid=html2wt
 !! html/parsoid
 <p>foo <span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1"><span class="mw-reflink-text">[1]</span></a></span></p>
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'>
-<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><i data-parsoid='{"dsr":[9,14,2,2]}'>a</i>
+<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><i>a</i>
  b</span></li>
 </ol>
 !! wikitext
@@ -24663,7 +24722,7 @@ T115289: Don't migrate newlines out of tables with fostered content
 !! wikitext
 <table><td></td>{{echo|<tr>[[Category:One]]}}<!--c-->[[Category:Two]]
 !! html/parsoid
-<link rel="mw:PageProp/Category" href="./Category:One" about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"stx":"simple","a":{"href":"./Category:One"},"sa":{"href":"Category:One"},"fostered":true,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["&lt;table>&lt;td>&lt;/td>",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;tr>[[Category:One]]"}},"i":0}},"&lt;!--c-->[[Category:Two]]"]}'/><link rel="mw:PageProp/Category" href="./Category:Two" about="#mwt2"/><table about="#mwt2" data-parsoid='{"stx":"html","autoInsertedEnd":true,"dsr":[0,53,7,0]}'><tbody><tr><td></td></tr><tr><!--c--></tr></tbody></table>
+<link rel="mw:PageProp/Category" href="./Category:One" about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"stx":"simple","a":{"href":"./Category:One"},"sa":{"href":"Category:One"},"fostered":true,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["&lt;table>&lt;td>&lt;/td>",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;tr>[[Category:One]]"}},"i":0}},"&lt;!--c-->[[Category:Two]]"]}'/><link rel="mw:PageProp/Category" href="./Category:Two" about="#mwt2"/><table about="#mwt2" data-parsoid='{"stx":"html","autoInsertedEnd":true}'><tbody><tr><td></td></tr><tr><!--c--></tr></tbody></table>
 !! end
 
 !! test
@@ -26689,8 +26748,8 @@ parsoid=html2wt
 !! html/parsoid
 <p>x<meta typeof="mw:Placeholder" data-parsoid='{"src":"&lt;nowiki/>"}'/><meta typeof="mw:Placeholder" data-parsoid='{"src":"&lt;nowiki/>"}'/>
 y</p>
-<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,23,null,null],"pi":[[{"k":"1","named":true,"spc":["\n"," "," ",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
-<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,24,null,null],"pi":[[{"k":"1","named":true,"spc":["\n"," "," ","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
+<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1","named":true,"spc":["\n"," "," ",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
+<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1","named":true,"spc":["\n"," "," ","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
 !! wikitext
 x
 y
index 25e0e31..9f3aa11 100644 (file)
@@ -498,6 +498,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
                $wgRequest = new FauxRequest();
                MediaWiki\Session\SessionManager::resetCache();
+               MediaWiki\Auth\AuthManager::resetCache();
 
                $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
 
index 6c92b8c..a01f2f5 100644 (file)
@@ -7,7 +7,6 @@ class HtmlTest extends MediaWikiTestCase {
                parent::setUp();
 
                $this->setMwGlobals( [
-                       'wgWellFormedXml' => false,
                        'wgUseMediaWikiUIEverywhere' => false,
                ] );
 
@@ -45,7 +44,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testElementBasics() {
                $this->assertEquals(
-                       '<img>',
+                       '<img/>',
                        Html::element( 'img', null, '' ),
                        'No close tag for short-tag elements'
                );
@@ -62,12 +61,10 @@ class HtmlTest extends MediaWikiTestCase {
                        'Close tag for empty element (array, string)'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
                $this->assertEquals(
                        '<img/>',
                        Html::element( 'img', null, '' ),
-                       'Self-closing tag for short-tag elements (wgWellFormedXml = true)'
+                       'Self-closing tag for short-tag elements'
                );
        }
 
@@ -134,22 +131,20 @@ class HtmlTest extends MediaWikiTestCase {
                );
 
                $this->assertEquals(
-                       ' selected',
+                       ' selected=""',
                        Html::expandAttributes( [ 'selected' => true ] ),
                        'Boolean attributes have no value when value is true'
                );
                $this->assertEquals(
-                       ' selected',
+                       ' selected=""',
                        Html::expandAttributes( [ 'selected' ] ),
                        'Boolean attributes have no value when value is true (passed as numerical array)'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
                $this->assertEquals(
                        ' selected=""',
                        Html::expandAttributes( [ 'selected' => true ] ),
-                       'Boolean attributes have empty string value when value is true (wgWellFormedXml)'
+                       'Boolean attributes have empty string value when value is true'
                );
        }
 
@@ -158,12 +153,12 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testExpandAttributesForNumbers() {
                $this->assertEquals(
-                       ' value=1',
+                       ' value="1"',
                        Html::expandAttributes( [ 'value' => 1 ] ),
                        'Integer value is cast to a string'
                );
                $this->assertEquals(
-                       ' value=1.1',
+                       ' value="1.1"',
                        Html::expandAttributes( [ 'value' => 1.1 ] ),
                        'Float value is cast to a string'
                );
@@ -174,7 +169,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testExpandAttributesForObjects() {
                $this->assertEquals(
-                       ' value=stringValue',
+                       ' value="stringValue"',
                        Html::expandAttributes( [ 'value' => new HtmlTestValue() ] ),
                        'Object value is converted to a string'
                );
@@ -193,43 +188,21 @@ class HtmlTest extends MediaWikiTestCase {
                        'Empty string is always quoted'
                );
                $this->assertEquals(
-                       ' key=value',
+                       ' key="value"',
                        Html::expandAttributes( [ 'key' => 'value' ] ),
                        'Simple string value needs no quotes'
                );
                $this->assertEquals(
-                       ' one=1',
+                       ' one="1"',
                        Html::expandAttributes( [ 'one' => 1 ] ),
                        'Number 1 value needs no quotes'
                );
                $this->assertEquals(
-                       ' zero=0',
+                       ' zero="0"',
                        Html::expandAttributes( [ 'zero' => 0 ] ),
                        'Number 0 value needs no quotes'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
-               $this->assertEquals(
-                       ' empty_string=""',
-                       Html::expandAttributes( [ 'empty_string' => '' ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Empty string'
-               );
-               $this->assertEquals(
-                       ' key="value"',
-                       Html::expandAttributes( [ 'key' => 'value' ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Simple string'
-               );
-               $this->assertEquals(
-                       ' one="1"',
-                       Html::expandAttributes( [ 'one' => 1 ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Number 1'
-               );
-               $this->assertEquals(
-                       ' zero="0"',
-                       Html::expandAttributes( [ 'zero' => 0 ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Number 0'
-               );
        }
 
        /**
@@ -346,48 +319,48 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testNamespaceSelector() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(),
                        'Basic namespace selector without custom options'
                );
 
                $this->assertEquals(
-                       '<label for=mw-test-namespace>Select a namespace:</label>&#160;' .
-                               '<select id=mw-test-namespace name=wpNamespace>' . "\n" .
-                               '<option value=all>all</option>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2 selected>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<label for="mw-test-namespace">Select a namespace:</label>&#160;' .
+                               '<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
+                               '<option value="all">all</option>' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2" selected="">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ],
@@ -397,24 +370,24 @@ class HtmlTest extends MediaWikiTestCase {
                );
 
                $this->assertEquals(
-                       '<label for=namespace>Select a namespace:</label>&#160;' .
-                               '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<label for="namespace">Select a namespace:</label>&#160;' .
+                               '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'label' => 'Select a namespace:' ]
@@ -425,18 +398,18 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testCanFilterOutNamespaces() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'exclude' => [ 0, 1, 3, 100, 101 ] ]
@@ -447,23 +420,23 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testCanDisableANamespaces() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option disabled value=0>(Main)</option>' . "\n" .
-                               '<option disabled value=1>Talk</option>' . "\n" .
-                               '<option disabled value=2>User</option>' . "\n" .
-                               '<option disabled value=3>User talk</option>' . "\n" .
-                               '<option disabled value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option disabled="" value="0">(Main)</option>' . "\n" .
+                               '<option disabled="" value="1">Talk</option>' . "\n" .
+                               '<option disabled="" value="2">User</option>' . "\n" .
+                               '<option disabled="" value="3">User talk</option>' . "\n" .
+                               '<option disabled="" value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector( [
                                'disable' => [ 0, 1, 2, 3, 4 ]
@@ -478,7 +451,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
                $this->assertEquals(
-                       '<input type=' . $HTML5InputType . '>',
+                       '<input type="' . $HTML5InputType . '"/>',
                        Html::element( 'input', [ 'type' => $HTML5InputType ] ),
                        'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"'
                );
@@ -528,14 +501,14 @@ class HtmlTest extends MediaWikiTestCase {
                $cases = [];
 
                # ## Generic cases, match $attribDefault static array
-               $cases[] = [ '<area>',
+               $cases[] = [ '<area/>',
                        'area', [ 'shape' => 'rect' ]
                ];
 
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'formaction' => 'GET' ]
                ];
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ]
                ];
 
@@ -553,7 +526,7 @@ class HtmlTest extends MediaWikiTestCase {
                        'canvas', [ 'width' => 300 ]
                ];
 
-               $cases[] = [ '<command>',
+               $cases[] = [ '<command/>',
                        'command', [ 'type' => 'command' ]
                ];
 
@@ -567,18 +540,18 @@ class HtmlTest extends MediaWikiTestCase {
                        'form', [ 'enctype' => 'application/x-www-form-urlencoded' ]
                ];
 
-               $cases[] = [ '<input>',
+               $cases[] = [ '<input/>',
                        'input', [ 'formaction' => 'GET' ]
                ];
-               $cases[] = [ '<input>',
+               $cases[] = [ '<input/>',
                        'input', [ 'type' => 'text' ]
                ];
 
-               $cases[] = [ '<keygen>',
+               $cases[] = [ '<keygen/>',
                        'keygen', [ 'keytype' => 'rsa' ]
                ];
 
-               $cases[] = [ '<link>',
+               $cases[] = [ '<link/>',
                        'link', [ 'media' => 'all' ]
                ];
 
@@ -604,44 +577,44 @@ class HtmlTest extends MediaWikiTestCase {
                # ## SPECIFIC CASES
 
                # <link type="text/css">
-               $cases[] = [ '<link>',
+               $cases[] = [ '<link/>',
                        'link', [ 'type' => 'text/css' ]
                ];
 
                # <input> specific handling
-               $cases[] = [ '<input type=checkbox>',
+               $cases[] = [ '<input type="checkbox"/>',
                        'input', [ 'type' => 'checkbox', 'value' => 'on' ],
                        'Default value "on" is stripped of checkboxes',
                ];
-               $cases[] = [ '<input type=radio>',
+               $cases[] = [ '<input type="radio"/>',
                        'input', [ 'type' => 'radio', 'value' => 'on' ],
                        'Default value "on" is stripped of radio buttons',
                ];
-               $cases[] = [ '<input type=submit value=Submit>',
+               $cases[] = [ '<input type="submit" value="Submit"/>',
                        'input', [ 'type' => 'submit', 'value' => 'Submit' ],
                        'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
                ];
-               $cases[] = [ '<input type=color>',
+               $cases[] = [ '<input type="color"/>',
                        'input', [ 'type' => 'color', 'value' => '' ],
                ];
-               $cases[] = [ '<input type=range>',
+               $cases[] = [ '<input type="range"/>',
                        'input', [ 'type' => 'range', 'value' => '' ],
                ];
 
                # <button> specific handling
                # see remarks on http://msdn.microsoft.com/en-us/library/ie/ms535211%28v=vs.85%29.aspx
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'type' => 'submit' ],
                        'According to standard the default type is "submit". '
                                . 'Depending on compatibility mode IE might use "button", instead.',
                ];
 
                # <select> specific handling
-               $cases[] = [ '<select multiple></select>',
+               $cases[] = [ '<select multiple=""></select>',
                        'select', [ 'size' => '4', 'multiple' => true ],
                ];
                # .. with numeric value
-               $cases[] = [ '<select multiple></select>',
+               $cases[] = [ '<select multiple=""></select>',
                        'select', [ 'size' => 4, 'multiple' => true ],
                ];
                $cases[] = [ '<select></select>',
@@ -693,7 +666,7 @@ class HtmlTest extends MediaWikiTestCase {
                        'Blacklist form validation attributes.'
                );
                $this->assertEquals(
-                       ' step=any',
+                       ' step="any"',
                        Html::expandAttributes(
                                [
                                        'min' => 1,
@@ -709,12 +682,12 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperInput() {
                $this->assertEquals(
-                       '<input type=radio value=testval name=testname>',
+                       '<input type="radio" value="testval" name="testname"/>',
                        Html::input( 'testname', 'testval', 'radio' ),
                        'Input wrapper with type and value.'
                );
                $this->assertEquals(
-                       '<input name=testname>',
+                       '<input name="testname"/>',
                        Html::input( 'testname' ),
                        'Input wrapper with all default values.'
                );
@@ -722,17 +695,17 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperCheck() {
                $this->assertEquals(
-                       '<input type=checkbox value=1 name=testname>',
+                       '<input type="checkbox" value="1" name="testname"/>',
                        Html::check( 'testname' ),
                        'Checkbox wrapper unchecked.'
                );
                $this->assertEquals(
-                       '<input checked type=checkbox value=1 name=testname>',
+                       '<input checked="" type="checkbox" value="1" name="testname"/>',
                        Html::check( 'testname', true ),
                        'Checkbox wrapper checked.'
                );
                $this->assertEquals(
-                       '<input type=checkbox value=testval name=testname>',
+                       '<input type="checkbox" value="testval" name="testname"/>',
                        Html::check( 'testname', false, [ 'value' => 'testval' ] ),
                        'Checkbox wrapper with a value override.'
                );
@@ -740,17 +713,17 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperRadio() {
                $this->assertEquals(
-                       '<input type=radio value=1 name=testname>',
+                       '<input type="radio" value="1" name="testname"/>',
                        Html::radio( 'testname' ),
                        'Radio wrapper unchecked.'
                );
                $this->assertEquals(
-                       '<input checked type=radio value=1 name=testname>',
+                       '<input checked="" type="radio" value="1" name="testname"/>',
                        Html::radio( 'testname', true ),
                        'Radio wrapper checked.'
                );
                $this->assertEquals(
-                       '<input type=radio value=testval name=testname>',
+                       '<input type="radio" value="testval" name="testname"/>',
                        Html::radio( 'testname', false, [ 'value' => 'testval' ] ),
                        'Radio wrapper with a value override.'
                );
@@ -758,7 +731,7 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperLabel() {
                $this->assertEquals(
-                       '<label for=testid>testlabel</label>',
+                       '<label for="testid">testlabel</label>',
                        Html::label( 'testlabel', 'testid' ),
                        'Label wrapper'
                );
index e50b4f1..6edf034 100644 (file)
@@ -13,7 +13,6 @@ class LinkerTest extends MediaWikiLangTestCase {
        public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                ] );
 
                $this->assertEquals( $expected,
@@ -112,7 +111,6 @@ class LinkerTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgScript' => '/wiki/index.php',
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                        'wgCapitalLinks' => true,
                        'wgConf' => $conf,
                ] );
@@ -277,7 +275,6 @@ class LinkerTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgScript' => '/wiki/index.php',
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                        'wgCapitalLinks' => true,
                        'wgConf' => $conf,
                ] );
@@ -309,4 +306,116 @@ class LinkerTest extends MediaWikiLangTestCase {
                ];
                // @codingStandardsIgnoreEnd
        }
+
+       public static function provideLinkBeginHook() {
+               // @codingStandardsIgnoreStart Generic.Files.LineLength
+               return [
+                       // Modify $html
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $html = 'foobar';
+                               },
+                               '<a href="/wiki/Special:BlankPage" title="Special:BlankPage">foobar</a>'
+                       ],
+                       // Modify $attribs
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $attribs['bar'] = 'baz';
+                               },
+                               '<a href="/wiki/Special:BlankPage" title="Special:BlankPage" bar="baz">Special:BlankPage</a>'
+                       ],
+                       // Modify $query
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $query['bar'] = 'baz';
+                               },
+                               '<a href="/w/index.php?title=Special:BlankPage&amp;bar=baz" title="Special:BlankPage">Special:BlankPage</a>'
+                       ],
+                       // Force HTTP $options
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $options = [ 'http' ];
+                               },
+                               '<a href="http://example.org/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>'
+                       ],
+                       // Force 'forcearticlepath' in $options
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $options = [ 'forcearticlepath' ];
+                                       $query['foo'] = 'bar';
+                               },
+                               '<a href="/wiki/Special:BlankPage?foo=bar" title="Special:BlankPage">Special:BlankPage</a>'
+                       ],
+                       // Abort early
+                       [
+                               function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
+                                       $ret = 'foobar';
+                                       return false;
+                               },
+                               'foobar'
+                       ],
+               ];
+               // @codingStandardsIgnoreEnd
+       }
+
+       /**
+        * @dataProvider provideLinkBeginHook
+        */
+       public function testLinkBeginHook( $callback, $expected ) {
+               $this->setMwGlobals( [
+                       'wgArticlePath' => '/wiki/$1',
+                       'wgServer' => '//example.org',
+                       'wgCanonicalServer' => 'http://example.org',
+                       'wgScriptPath' => '/w',
+                       'wgScript' => '/w/index.php',
+               ] );
+
+               $this->setMwGlobals( 'wgHooks', [ 'LinkBegin' => [ $callback ] ] );
+               $title = SpecialPage::getTitleFor( 'Blankpage' );
+               $out = Linker::link( $title );
+               $this->assertEquals( $expected, $out );
+       }
+
+       public static function provideLinkEndHook() {
+               return [
+                       // Override $html
+                       [
+                               function( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
+                                       $html = 'foobar';
+                               },
+                               '<a href="/wiki/Special:BlankPage" title="Special:BlankPage">foobar</a>'
+                       ],
+                       // Modify $attribs
+                       [
+                               function( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
+                                       $attribs['bar'] = 'baz';
+                               },
+                               '<a href="/wiki/Special:BlankPage" title="Special:BlankPage" bar="baz">Special:BlankPage</a>'
+                       ],
+                       // Fully override return value and abort hook
+                       [
+                               function( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
+                                       $ret = 'blahblahblah';
+                                       return false;
+                               },
+                               'blahblahblah'
+                       ],
+
+               ];
+       }
+
+       /**
+        * @dataProvider provideLinkEndHook
+        */
+       public function testLinkEndHook( $callback, $expected ) {
+               $this->setMwGlobals( [
+                       'wgArticlePath' => '/wiki/$1',
+               ] );
+
+               $this->setMwGlobals( 'wgHooks', [ 'LinkEnd' => [ $callback ] ] );
+
+               $title = SpecialPage::getTitleFor( 'Blankpage' );
+               $out = Linker::link( $title );
+               $this->assertEquals( $expected, $out );
+       }
 }
index 4f917a5..51ef9d7 100644 (file)
@@ -200,6 +200,10 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
 
                // All getters should be named just like the service, with "get" added.
                foreach ( $getServiceCases as $name => $case ) {
+                       if ( $name[0] === '_' ) {
+                               // Internal service, no getter
+                               continue;
+                       }
                        list( $service, $class ) = $case;
                        $getterCases[$name] = [
                                'get' . $service,
@@ -239,6 +243,10 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
                        'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
                        'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
                        'GenderCache' => [ 'GenderCache', GenderCache::class ],
+                       'LinkCache' => [ 'LinkCache', LinkCache::class ],
+                       '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
+                       'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
+                       'TitleParser' => [ 'TitleParser', TitleParser::class ],
                ];
        }
 
index 8d4a347..9934749 100644 (file)
@@ -149,14 +149,14 @@ class OutputPageTest extends MediaWikiTestCase {
                        [
                                // Don't condition wrap raw modules (like the startup module)
                                [ 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ],
-                               '<script async src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
+                               '<script async="" src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
                        ],
                        // Load module styles only
                        // This also tests the order the modules are put into the url
                        [
                                [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
 
-                               '<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback">'
+                               '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
                        ],
                        // Load private module (only=scripts)
                        [
@@ -181,7 +181,7 @@ class OutputPageTest extends MediaWikiTestCase {
                        // noscript group
                        [
                                [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
-                               '<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"></noscript>'
+                               '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
                        ],
                        // Load two modules in separate groups
                        [
@@ -210,8 +210,6 @@ class OutputPageTest extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgResourceLoaderDebug' => false,
                        'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
-                       // Affects whether CDATA is inserted
-                       'wgWellFormedXml' => false,
                ] );
                $class = new ReflectionClass( 'OutputPage' );
                $method = $class->getMethod( 'makeResourceLoaderLink' );
index 7d025d2..7850f24 100644 (file)
@@ -144,6 +144,13 @@ class TitleTest extends MediaWikiTestCase {
                                ]
                        ]
                ] );
+
+               // Reset TitleParser since we modified $wgLocalInterwikis
+               $this->setService( 'TitleParser', new MediaWikiTitleCodec(
+                               Language::factory( 'en' ),
+                               new GenderCache(),
+                               [ 'localtestiw' ]
+               ) );
        }
 
        /**
@@ -702,4 +709,42 @@ class TitleTest extends MediaWikiTestCase {
                $this->assertEquals( $title->getInterwiki(), $fragmentTitle->getInterwiki() );
                $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
        }
+
+       public function provideGetPrefixedText() {
+               return [
+                       // ns = 0
+                       [
+                               Title::makeTitle( NS_MAIN, 'Foobar' ),
+                               'Foobar'
+                       ],
+                       // ns = 2
+                       [
+                               Title::makeTitle( NS_USER, 'Foobar' ),
+                               'User:Foobar'
+                       ],
+                       // fragment not included
+                       [
+                               Title::makeTitle( NS_MAIN, 'Foobar', 'fragment' ),
+                               'Foobar'
+                       ],
+                       // ns = -2
+                       [
+                               Title::makeTitle( NS_MEDIA, 'Foobar' ),
+                               'Media:Foobar'
+                       ],
+                       // non-existent namespace
+                       [
+                               Title::makeTitle( 100000, 'Foobar' ),
+                               ':Foobar'
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Title::getPrefixedText
+        * @dataProvider provideGetPrefixedText
+        */
+       public function testGetPrefixedText( Title $title, $expected ) {
+               $this->assertEquals( $expected, $title->getPrefixedText() );
+       }
 }
index e536205..be22260 100644 (file)
@@ -13,6 +13,14 @@ class WatchedItemIntegrationTest extends MediaWikiTestCase {
                parent::setUp();
                self::$users['WatchedItemIntegrationTestUser']
                        = new TestUser( 'WatchedItemIntegrationTestUser' );
+
+               $this->hideDeprecated( 'WatchedItem::fromUserTitle' );
+               $this->hideDeprecated( 'WatchedItem::addWatch' );
+               $this->hideDeprecated( 'WatchedItem::removeWatch' );
+               $this->hideDeprecated( 'WatchedItem::isWatched' );
+               $this->hideDeprecated( 'WatchedItem::resetNotificationTimestamp' );
+               $this->hideDeprecated( 'WatchedItem::duplicateEntries' );
+               $this->hideDeprecated( 'WatchedItem::batchAddWatch' );
        }
 
        private function getUser() {
@@ -20,6 +28,7 @@ class WatchedItemIntegrationTest extends MediaWikiTestCase {
        }
 
        public function testWatchAndUnWatchItem() {
+
                $user = $this->getUser();
                $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
                // Cleanup after previous tests
index 0d10c1a..f80f512 100644 (file)
@@ -12,9 +12,6 @@ class XmlSelectTest extends MediaWikiTestCase {
 
        protected function setUp() {
                parent::setUp();
-               $this->setMwGlobals( [
-                       'wgWellFormedXml' => true,
-               ] );
                $this->select = new XmlSelect();
        }
 
index 00d429e..dbd1299 100644 (file)
@@ -30,7 +30,6 @@ class XmlTest extends MediaWikiTestCase {
 
                $this->setMwGlobals( [
                        'wgLang' => $langObj,
-                       'wgWellFormedXml' => true,
                        'wgUseMediaWikiUIEverywhere' => false,
                ] );
        }
diff --git a/tests/phpunit/includes/api/ApiCreateAccountTest.php b/tests/phpunit/includes/api/ApiCreateAccountTest.php
deleted file mode 100644 (file)
index 9a83e61..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-/**
- * @group Database
- * @group API
- * @group medium
- *
- * @covers ApiCreateAccount
- */
-class ApiCreateAccountTest extends ApiTestCase {
-       protected function setUp() {
-               parent::setUp();
-               $this->setMwGlobals( [ 'wgEnableEmail' => true ] );
-       }
-
-       /**
-        * Test the account creation API with a valid request. Also
-        * make sure the new account can log in and is valid.
-        *
-        * This test does multiple API requests so it might end up being
-        * a bit slow. Raise the default timeout.
-        * @group medium
-        */
-       public function testValid() {
-               global $wgServer;
-
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
-
-               $password = PasswordFactory::generateRandomPasswordString();
-
-               $ret = $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Apitestnew',
-                       'password' => $password,
-                       'email' => 'test@domain.test',
-                       'realname' => 'Test Name'
-               ] );
-
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertNotInternalType( 'null', $result['createaccount'] );
-
-               // Should first ask for token.
-               $a = $result['createaccount'];
-               $this->assertEquals( 'NeedToken', $a['result'] );
-               $token = $a['token'];
-
-               // Finally create the account
-               $ret = $this->doApiRequest(
-                       [
-                               'action' => 'createaccount',
-                               'name' => 'Apitestnew',
-                               'password' => $password,
-                               'token' => $token,
-                               'email' => 'test@domain.test',
-                               'realname' => 'Test Name'
-                       ],
-                       $ret[2]
-               );
-
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertEquals( 'Success', $result['createaccount']['result'] );
-
-               // Try logging in with the new user.
-               $ret = $this->doApiRequest( [
-                       'action' => 'login',
-                       'lgname' => 'Apitestnew',
-                       'lgpassword' => $password,
-               ] );
-
-               $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(
-                       [
-                               'action' => 'login',
-                               'lgtoken' => $token,
-                               'lgname' => 'Apitestnew',
-                               'lgpassword' => $password,
-                       ],
-                       $ret[2]
-               );
-
-               $result = $ret[0];
-
-               $this->assertNotInternalType( 'bool', $result );
-               $a = $result['login']['result'];
-
-               $this->assertEquals( 'Success', $a );
-
-               // log out to destroy the session
-               $ret = $this->doApiRequest(
-                       [
-                               'action' => 'logout',
-                       ],
-                       $ret[2]
-               );
-               $this->assertEquals( [], $ret[0] );
-       }
-
-       /**
-        * Make sure requests with no names are invalid.
-        * @expectedException UsageException
-        */
-       public function testNoName() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-               ] );
-       }
-
-       /**
-        * Make sure requests with no password are invalid.
-        * @expectedException UsageException
-        */
-       public function testNoPassword() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'testName',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-               ] );
-       }
-
-       /**
-        * Make sure requests with existing users are invalid.
-        * @expectedException UsageException
-        */
-       public function testExistingUser() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Apitestsysop',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-                       'email' => 'test@domain.test',
-               ] );
-       }
-
-       /**
-        * Make sure requests with invalid emails are invalid.
-        * @expectedException UsageException
-        */
-       public function testInvalidEmail() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Test User',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-                       'email' => 'invalid',
-               ] );
-       }
-}
index bcd884e..e9afd45 100644 (file)
@@ -13,6 +13,8 @@ class ApiLoginTest extends ApiTestCase {
         * Test result of attempted login with an empty username
         */
        public function testApiLoginNoName() {
+               global $wgDisableAuthManager;
+
                $session = [
                        'wsTokenSecrets' => [ 'login' => 'foobar' ],
                ];
@@ -20,11 +22,11 @@ class ApiLoginTest extends ApiTestCase {
                        'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
                        'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) )
                ], $session );
-               $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+               $this->assertEquals( $wgDisableAuthManager ? 'NoName' : 'Failed', $data[0]['login']['result'] );
        }
 
        public function testApiLoginBadPass() {
-               global $wgServer;
+               global $wgServer, $wgDisableAuthManager;
 
                $user = self::$users['sysop'];
                $user->getUser()->logout();
@@ -61,7 +63,7 @@ class ApiLoginTest extends ApiTestCase {
                $this->assertNotInternalType( "bool", $result );
                $a = $result["login"]["result"];
 
-               $this->assertEquals( "WrongPass", $a );
+               $this->assertEquals( $wgDisableAuthManager ? 'WrongPass' : 'Failed', $a );
        }
 
        public function testApiLoginGoodPass() {
index 246ea3d..ff5640a 100644 (file)
@@ -14,7 +14,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
        protected $tablesUsed = [ 'user', 'user_groups', 'user_properties' ];
 
        protected function setUp() {
-               global $wgServer;
+               global $wgServer, $wgDisableAuthManager;
 
                parent::setUp();
                self::$apiUrl = $wgServer . wfScript( 'api' );
@@ -37,7 +37,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
                ];
 
                $this->setMwGlobals( [
-                       'wgAuth' => new AuthPlugin,
+                       'wgAuth' => $wgDisableAuthManager ? new AuthPlugin : new MediaWiki\Auth\AuthManagerAuthPlugin,
                        'wgRequest' => new FauxRequest( [] ),
                        'wgUser' => self::$users['sysop']->user,
                ] );
@@ -101,6 +101,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
                $wgRequest = new FauxRequest( $params, true, $session );
                RequestContext::getMain()->setRequest( $wgRequest );
                RequestContext::getMain()->setUser( $wgUser );
+               MediaWiki\Auth\AuthManager::resetCache();
 
                // set up local environment
                $context = $this->apiContext->newTestContext( $wgRequest, $wgUser );
@@ -183,6 +184,13 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
                        $data[2]
                );
 
+               if ( $data[0]['login']['result'] === 'Success' ) {
+                       // DWIM
+                       global $wgUser;
+                       $wgUser = self::$users[$user]->getUser();
+                       RequestContext::getMain()->setUser( $wgUser );
+               }
+
                return $data;
        }
 
diff --git a/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..1ded0df
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractAuthenticationProvider
+ */
+class AbstractAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testAbstractAuthenticationProvider() {
+               $provider = $this->getMockForAbstractClass( AbstractAuthenticationProvider::class );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $obj = $this->getMockForAbstractClass( 'Psr\Log\LoggerInterface' );
+               $provider->setLogger( $obj );
+               $this->assertSame( $obj, $providerPriv->logger, 'setLogger' );
+
+               $obj = AuthManager::singleton();
+               $provider->setManager( $obj );
+               $this->assertSame( $obj, $providerPriv->manager, 'setManager' );
+
+               $obj = $this->getMockForAbstractClass( 'Config' );
+               $provider->setConfig( $obj );
+               $this->assertSame( $obj, $providerPriv->config, 'setConfig' );
+
+               $this->assertType( 'string', $provider->getUniqueId(), 'getUniqueId' );
+       }
+}
diff --git a/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..ecce932
--- /dev/null
@@ -0,0 +1,233 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider
+ */
+class AbstractPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testConstructor() {
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertTrue( $providerPriv->authoritative );
+
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class,
+                       [ [ 'authoritative' => false ] ]
+               );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertFalse( $providerPriv->authoritative );
+       }
+
+       public function testGetPasswordFactory() {
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->setConfig( \ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $obj = $providerPriv->getPasswordFactory();
+               $this->assertInstanceOf( 'PasswordFactory', $obj );
+               $this->assertSame( $obj, $providerPriv->getPasswordFactory() );
+       }
+
+       public function testGetPassword() {
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->setConfig( \ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $obj = $providerPriv->getPassword( null );
+               $this->assertInstanceOf( 'Password', $obj );
+
+               $obj = $providerPriv->getPassword( 'invalid' );
+               $this->assertInstanceOf( 'Password', $obj );
+       }
+
+       public function testGetNewPasswordExpiry() {
+               $config = new \HashConfig;
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->setConfig( new \MultiConfig( [
+                       $config,
+                       \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+               ] ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'ResetPasswordExpiration' => [] ] );
+
+               $config->set( 'PasswordExpirationDays', 0 );
+               $this->assertNull( $providerPriv->getNewPasswordExpiry( 'UTSysop' ) );
+
+               $config->set( 'PasswordExpirationDays', 5 );
+               $this->assertEquals(
+                       time() + 5 * 86400,
+                       wfTimestamp( TS_UNIX, $providerPriv->getNewPasswordExpiry( 'UTSysop' ) ),
+                       '',
+                       2 /* Fuzz */
+               );
+
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                       'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
+                               $this->assertSame( 'UTSysop', $user->getName() );
+                               $expires = '30001231235959';
+                       } ]
+               ] );
+               $this->assertEquals( '30001231235959', $providerPriv->getNewPasswordExpiry( 'UTSysop' ) );
+       }
+
+       public function testCheckPasswordValidity() {
+               $uppCalled = 0;
+               $uppStatus = \Status::newGood();
+               $this->setMwGlobals( [
+                       'wgPasswordPolicy' => [
+                               'policies' => [
+                                       'default' => [
+                                               'Check' => true,
+                                       ],
+                               ],
+                               'checks' => [
+                                       'Check' => function () use ( &$uppCalled, &$uppStatus ) {
+                                               $uppCalled++;
+                                               return $uppStatus;
+                                       },
+                               ],
+                       ]
+               ] );
+
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->setConfig( \ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $this->assertEquals( $uppStatus, $providerPriv->checkPasswordValidity( 'foo', 'bar' ) );
+
+               $uppStatus->fatal( 'arbitrary-warning' );
+               $this->assertEquals( $uppStatus, $providerPriv->checkPasswordValidity( 'foo', 'bar' ) );
+       }
+
+       public function testSetPasswordResetFlag() {
+               $config = new \HashConfig( [
+                       'InvalidPasswordReset' => true,
+               ] );
+
+               $manager = new AuthManager(
+                       new \FauxRequest(), \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+               );
+
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->setConfig( $config );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setManager( $manager );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $manager->removeAuthenticationSessionData( null );
+               $status = \Status::newGood();
+               $providerPriv->setPasswordResetFlag( 'Foo', $status );
+               $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+               $manager->removeAuthenticationSessionData( null );
+               $status = \Status::newGood();
+               $status->error( 'testing' );
+               $providerPriv->setPasswordResetFlag( 'Foo', $status );
+               $ret = $manager->getAuthenticationSessionData( 'reset-pass' );
+               $this->assertNotNull( $ret );
+               $this->assertSame( 'resetpass-validity-soft', $ret->msg->getKey() );
+               $this->assertFalse( $ret->hard );
+
+               $config->set( 'InvalidPasswordReset', false );
+               $manager->removeAuthenticationSessionData( null );
+               $providerPriv->setPasswordResetFlag( 'Foo', $status );
+               $ret = $manager->getAuthenticationSessionData( 'reset-pass' );
+               $this->assertNull( $ret );
+       }
+
+       public function testFailResponse() {
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class,
+                       [ [ 'authoritative' => false ] ]
+               );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $req = new PasswordAuthenticationRequest;
+
+               $ret = $providerPriv->failResponse( $req );
+               $this->assertSame( AuthenticationResponse::ABSTAIN, $ret->status );
+
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class,
+                       [ [ 'authoritative' => true ] ]
+               );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $req->password = '';
+               $ret = $providerPriv->failResponse( $req );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'wrongpasswordempty', $ret->message->getKey() );
+
+               $req->password = 'X';
+               $ret = $providerPriv->failResponse( $req );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        */
+       public function testGetAuthenticationRequests( $action, $response ) {
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [ new PasswordAuthenticationRequest() ] ],
+                       [ AuthManager::ACTION_CREATE, [ new PasswordAuthenticationRequest() ] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [ new PasswordAuthenticationRequest() ] ],
+                       [ AuthManager::ACTION_REMOVE, [ new PasswordAuthenticationRequest() ] ],
+               ];
+       }
+
+       public function testProviderRevokeAccessForUser() {
+               $req = new PasswordAuthenticationRequest;
+               $req->action = AuthManager::ACTION_REMOVE;
+               $req->username = 'foo';
+               $req->password = null;
+
+               $provider = $this->getMockForAbstractClass(
+                       AbstractPasswordPrimaryAuthenticationProvider::class
+               );
+               $provider->expects( $this->once() )
+                       ->method( 'providerChangeAuthenticationData' )
+                       ->with( $this->equalTo( $req ) );
+
+               $provider->providerRevokeAccessForUser( 'foo' );
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..c35430e
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPreAuthenticationProvider
+ */
+class AbstractPreAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testAbstractPreAuthenticationProvider() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockForAbstractClass( AbstractPreAuthenticationProvider::class );
+
+               $this->assertEquals(
+                       [],
+                       $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAuthentication( [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, false )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountLink( $user )
+               );
+
+               $res = AuthenticationResponse::newPass();
+               $provider->postAuthentication( $user, $res );
+               $provider->postAccountCreation( $user, $user, $res );
+               $provider->postAccountLink( $user, $res );
+       }
+}
diff --git a/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..420a330
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPrimaryAuthenticationProvider
+ */
+class AbstractPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testAbstractPrimaryAuthenticationProvider() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+
+               try {
+                       $provider->continuePrimaryAuthentication( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               try {
+                       $provider->continuePrimaryAccountCreation( $user, $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+               $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, false )
+               );
+
+               $this->assertNull(
+                       $provider->finishAccountCreation( $user, $user, AuthenticationResponse::newPass() )
+               );
+               $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
+
+               $res = AuthenticationResponse::newPass();
+               $provider->postAuthentication( $user, $res );
+               $provider->postAccountCreation( $user, $user, $res );
+               $provider->postAccountLink( $user, $res );
+
+               $provider->expects( $this->once() )
+                       ->method( 'testUserExists' )
+                       ->with( $this->equalTo( 'foo' ) )
+                       ->will( $this->returnValue( true ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'foo' ) );
+       }
+
+       public function testProviderRevokeAccessForUser() {
+               $reqs = [];
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $reqs[$i] = $this->getMock( AuthenticationRequest::class );
+                       $reqs[$i]->done = false;
+               }
+
+               $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+               $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
+                       ->with(
+                               $this->identicalTo( AuthManager::ACTION_REMOVE ),
+                               $this->identicalTo( [ 'username' => 'UTSysop' ] )
+                       )
+                       ->will( $this->returnValue( $reqs ) );
+               $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               $this->assertSame( 'UTSysop', $req->username );
+                               $this->assertFalse( $req->done );
+                               $req->done = true;
+                       } ) );
+
+               $provider->providerRevokeAccessForUser( 'UTSysop' );
+
+               foreach ( $reqs as $i => $req ) {
+                       $this->assertTrue( $req->done, "#$i" );
+               }
+       }
+
+       /**
+        * @dataProvider providePrimaryAccountLink
+        * @param string $type PrimaryAuthenticationProvider::TYPE_* constant
+        * @param string $msg Error message from beginPrimaryAccountLink
+        */
+       public function testPrimaryAccountLink( $type, $msg ) {
+               $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+               $provider->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( $type ) );
+
+               $class = AbstractPrimaryAuthenticationProvider::class;
+               $msg1 = "{$class}::beginPrimaryAccountLink $msg";
+               $msg2 = "{$class}::continuePrimaryAccountLink is not implemented.";
+
+               $user = \User::newFromName( 'Whatever' );
+
+               try {
+                       $provider->beginPrimaryAccountLink( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame( $msg1, $ex->getMessage() );
+               }
+               try {
+                       $provider->continuePrimaryAccountLink( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame( $msg2, $ex->getMessage() );
+               }
+       }
+
+       public static function providePrimaryAccountLink() {
+               return [
+                       [
+                               PrimaryAuthenticationProvider::TYPE_NONE,
+                               'should not be called on a non-link provider.',
+                       ],
+                       [
+                               PrimaryAuthenticationProvider::TYPE_CREATE,
+                               'should not be called on a non-link provider.',
+                       ],
+                       [
+                               PrimaryAuthenticationProvider::TYPE_LINK,
+                               'is not implemented.',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideProviderNormalizeUsername
+        */
+       public function testProviderNormalizeUsername( $name, $expect ) {
+               // fake interwiki map for the 'Interwiki prefix' testcase
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                       'InterwikiLoadPrefix' => [
+                               function ( $prefix, &$iwdata ) {
+                                       if ( $prefix === 'interwiki' ) {
+                                               $iwdata = [
+                                                       'iw_url' => 'http://example.com/',
+                                                       'iw_local' => 0,
+                                                       'iw_trans' => 0,
+                                               ];
+                                               return false;
+                                       }
+                               },
+                       ],
+               ] );
+
+               $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+               $this->assertSame( $expect, $provider->providerNormalizeUsername( $name ) );
+       }
+
+       public static function provideProviderNormalizeUsername() {
+               return [
+                       'Leading space' => [ ' Leading space', 'Leading space' ],
+                       'Trailing space ' => [ 'Trailing space ', 'Trailing space' ],
+                       'Namespace prefix' => [ 'Talk:Username', null ],
+                       'Interwiki prefix' => [ 'interwiki:Username', null ],
+                       'With hash' => [ 'name with # hash', null ],
+                       'Multi spaces' => [ 'Multi  spaces', 'Multi spaces' ],
+                       'Lowercase' => [ 'lowercase', 'Lowercase' ],
+                       'Invalid character' => [ 'in[]valid', null ],
+                       'With slash' => [ 'with / slash', null ],
+                       'Underscores' => [ '___under__scores___', 'Under scores' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..9cdc051
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
+ */
+class AbstractSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testAbstractSecondaryAuthenticationProvider() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockForAbstractClass( AbstractSecondaryAuthenticationProvider::class );
+
+               try {
+                       $provider->continueSecondaryAuthentication( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               try {
+                       $provider->continueSecondaryAccountCreation( $user, $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+               $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
+               $this->assertEquals(
+                       \StatusValue::newGood( 'ignored' ),
+                       $provider->providerAllowsAuthenticationDataChange( $req )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, false )
+               );
+
+               $provider->providerChangeAuthenticationData( $req );
+               $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
+
+               $res = AuthenticationResponse::newPass();
+               $provider->postAuthentication( $user, $res );
+               $provider->postAccountCreation( $user, $user, $res );
+       }
+
+       public function testProviderRevokeAccessForUser() {
+               $reqs = [];
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $reqs[$i] = $this->getMock( AuthenticationRequest::class );
+                       $reqs[$i]->done = false;
+               }
+
+               $provider = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'providerChangeAuthenticationData' ] )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
+                       ->with(
+                               $this->identicalTo( AuthManager::ACTION_REMOVE ),
+                               $this->identicalTo( [ 'username' => 'UTSysop' ] )
+                       )
+                       ->will( $this->returnValue( $reqs ) );
+               $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               $this->assertSame( 'UTSysop', $req->username );
+                               $this->assertFalse( $req->done );
+                               $req->done = true;
+                       } ) );
+
+               $provider->providerRevokeAccessForUser( 'UTSysop' );
+
+               foreach ( $reqs as $i => $req ) {
+                       $this->assertTrue( $req->done, "#$i" );
+               }
+       }
+}
diff --git a/tests/phpunit/includes/auth/AuthManagerTest.php b/tests/phpunit/includes/auth/AuthManagerTest.php
new file mode 100644 (file)
index 0000000..377abe2
--- /dev/null
@@ -0,0 +1,3654 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\Session\SessionInfo;
+use MediaWiki\Session\UserInfo;
+use Psr\Log\LogLevel;
+use StatusValue;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\AuthManager
+ */
+class AuthManagerTest extends \MediaWikiTestCase {
+       /** @var WebRequest */
+       protected $request;
+       /** @var Config */
+       protected $config;
+       /** @var \\Psr\\Log\\LoggerInterface */
+       protected $logger;
+
+       protected $preauthMocks = [];
+       protected $primaryauthMocks = [];
+       protected $secondaryauthMocks = [];
+
+       /** @var AuthManager */
+       protected $manager;
+       /** @var TestingAccessWrapper */
+       protected $managerPriv;
+
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+
+               $this->setMwGlobals( [ 'wgAuth' => null ] );
+               $this->stashMwGlobals( [ 'wgHooks' ] );
+       }
+
+       /**
+        * Sets a mock on a hook
+        * @param string $hook
+        * @param object $expect From $this->once(), $this->never(), etc.
+        * @return object $mock->expects( $expect )->method( ... ).
+        */
+       protected function hook( $hook, $expect ) {
+               global $wgHooks;
+               $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
+               $wgHooks[$hook] = [ $mock ];
+               return $mock->expects( $expect )->method( "on$hook" );
+       }
+
+       /**
+        * Unsets a hook
+        * @param string $hook
+        */
+       protected function unhook( $hook ) {
+               global $wgHooks;
+               $wgHooks[$hook] = [];
+       }
+
+       /**
+        * Ensure a value is a clean Message object
+        * @param string|Message $key
+        * @param array $params
+        * @return Message
+        */
+       protected function message( $key, $params = [] ) {
+               if ( $key === null ) {
+                       return null;
+               }
+               if ( $key instanceof \MessageSpecifier ) {
+                       $params = $key->getParams();
+                       $key = $key->getKey();
+               }
+               return new \Message( $key, $params, \Language::factory( 'en' ) );
+       }
+
+       /**
+        * Initialize the AuthManagerConfig variable in $this->config
+        *
+        * Uses data from the various 'mocks' fields.
+        */
+       protected function initializeConfig() {
+               $config = [
+                       'preauth' => [
+                       ],
+                       'primaryauth' => [
+                       ],
+                       'secondaryauth' => [
+                       ],
+               ];
+
+               foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
+                       $key = $type . 'Mocks';
+                       foreach ( $this->$key as $mock ) {
+                               $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
+                                       return $mock;
+                               } ];
+                       }
+               }
+
+               $this->config->set( 'AuthManagerConfig', $config );
+               $this->config->set( 'LanguageCode', 'en' );
+               $this->config->set( 'NewUserLog', false );
+       }
+
+       /**
+        * Initialize $this->manager
+        * @param bool $regen Force a call to $this->initializeConfig()
+        */
+       protected function initializeManager( $regen = false ) {
+               if ( $regen || !$this->config ) {
+                       $this->config = new \HashConfig();
+               }
+               if ( $regen || !$this->request ) {
+                       $this->request = new \FauxRequest();
+               }
+               if ( !$this->logger ) {
+                       $this->logger = new \TestLogger();
+               }
+
+               if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
+                       $this->initializeConfig();
+               }
+               $this->manager = new AuthManager( $this->request, $this->config );
+               $this->manager->setLogger( $this->logger );
+               $this->managerPriv = \TestingAccessWrapper::newFromObject( $this->manager );
+       }
+
+       /**
+        * Setup SessionManager with a mock session provider
+        * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
+        * @param array $methods Additional methods to mock
+        * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
+        */
+       protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
+               if ( !$this->config ) {
+                       $this->config = new \HashConfig();
+                       $this->initializeConfig();
+               }
+               $this->config->set( 'ObjectCacheSessionExpiry', 100 );
+
+               $methods[] = '__toString';
+               $methods[] = 'describe';
+               if ( $canChangeUser !== null ) {
+                       $methods[] = 'canChangeUser';
+               }
+               $provider = $this->getMockBuilder( 'DummySessionProvider' )
+                       ->setMethods( $methods )
+                       ->getMock();
+               $provider->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'MockSessionProvider' ) );
+               $provider->expects( $this->any() )->method( 'describe' )
+                       ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
+               if ( $canChangeUser !== null ) {
+                       $provider->expects( $this->any() )->method( 'canChangeUser' )
+                               ->will( $this->returnValue( $canChangeUser ) );
+               }
+               $this->config->set( 'SessionProviders', [
+                       [ 'factory' => function () use ( $provider ) {
+                               return $provider;
+                       } ],
+               ] );
+
+               $manager = new \MediaWiki\Session\SessionManager( [
+                       'config' => $this->config,
+                       'logger' => new \Psr\Log\NullLogger(),
+                       'store' => new \HashBagOStuff(),
+               ] );
+               \TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
+
+               $reset = \MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
+
+               if ( $this->request ) {
+                       $manager->getSessionForRequest( $this->request );
+               }
+
+               return [ $provider, $reset ];
+       }
+
+       public function testSingleton() {
+               // Temporarily clear out the global singleton, if any, to test creating
+               // one.
+               $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
+               $rProp->setAccessible( true );
+               $old = $rProp->getValue();
+               $cb = new \ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
+               $rProp->setValue( null );
+
+               $singleton = AuthManager::singleton();
+               $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
+               $this->assertSame( $singleton, AuthManager::singleton() );
+               $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
+               $this->assertSame(
+                       \RequestContext::getMain()->getConfig(),
+                       \TestingAccessWrapper::newFromObject( $singleton )->config
+               );
+
+               $this->setMwGlobals( [ 'wgDisableAuthManager' => true ] );
+               try {
+                       AuthManager::singleton();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame( '$wgDisableAuthManager is set', $ex->getMessage() );
+               }
+       }
+
+       public function testCanAuthenticateNow() {
+               $this->initializeManager();
+
+               list( $provider, $reset ) = $this->getMockSessionProvider( false );
+               $this->assertFalse( $this->manager->canAuthenticateNow() );
+               \ScopedCallback::consume( $reset );
+
+               list( $provider, $reset ) = $this->getMockSessionProvider( true );
+               $this->assertTrue( $this->manager->canAuthenticateNow() );
+               \ScopedCallback::consume( $reset );
+       }
+
+       public function testNormalizeUsername() {
+               $mocks = [
+                       $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+                       $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+                       $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+                       $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+               ];
+               foreach ( $mocks as $key => $mock ) {
+                       $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
+               }
+               $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+                       ->with( $this->identicalTo( 'XYZ' ) )
+                       ->willReturn( 'Foo' );
+               $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+                       ->with( $this->identicalTo( 'XYZ' ) )
+                       ->willReturn( 'Foo' );
+               $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+                       ->with( $this->identicalTo( 'XYZ' ) )
+                       ->willReturn( null );
+               $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+                       ->with( $this->identicalTo( 'XYZ' ) )
+                       ->willReturn( 'Bar!' );
+
+               $this->primaryauthMocks = $mocks;
+
+               $this->initializeManager();
+
+               $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
+       }
+
+       /**
+        * @dataProvider provideSecuritySensitiveOperationStatus
+        * @param bool $mutableSession
+        */
+       public function testSecuritySensitiveOperationStatus( $mutableSession ) {
+               $this->logger = new \Psr\Log\NullLogger();
+               $user = \User::newFromName( 'UTSysop' );
+               $provideUser = null;
+               $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
+
+               list( $provider, $reset ) = $this->getMockSessionProvider(
+                       $mutableSession, [ 'provideSessionInfo' ]
+               );
+               $provider->expects( $this->any() )->method( 'provideSessionInfo' )
+                       ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
+                               return new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                                       'provider' => $provider,
+                                       'id' => \DummySessionProvider::ID,
+                                       'persisted' => true,
+                                       'userInfo' => UserInfo::newFromUser( $provideUser, true )
+                               ] );
+                       } ) );
+               $this->initializeManager();
+
+               $this->config->set( 'ReauthenticateTime', [] );
+               $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
+               $provideUser = new \User;
+               $session = $provider->getManager()->getSessionForRequest( $this->request );
+               $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
+
+               // Anonymous user => reauth
+               $session->set( 'AuthManager:lastAuthId', 0 );
+               $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+               $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
+
+               $provideUser = $user;
+               $session = $provider->getManager()->getSessionForRequest( $this->request );
+               $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
+
+               // Error for no default (only gets thrown for non-anonymous user)
+               $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
+               $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+               try {
+                       $this->manager->securitySensitiveOperationStatus( 'foo' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               $mutableSession
+                                       ? '$wgReauthenticateTime lacks a default'
+                                       : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
+                               $ex->getMessage()
+                       );
+               }
+
+               if ( $mutableSession ) {
+                       $this->config->set( 'ReauthenticateTime', [
+                               'test' => 100,
+                               'test2' => -1,
+                               'default' => 10,
+                       ] );
+
+                       // Mismatched user ID
+                       $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
+                       $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+                       $this->assertSame(
+                               AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+                       );
+                       $this->assertSame(
+                               AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
+                       );
+                       $this->assertSame(
+                               AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
+                       );
+
+                       // Missing time
+                       $session->set( 'AuthManager:lastAuthId', $user->getId() );
+                       $session->set( 'AuthManager:lastAuthTimestamp', null );
+                       $this->assertSame(
+                               AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+                       );
+                       $this->assertSame(
+                               AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
+                       );
+                       $this->assertSame(
+                               AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
+                       );
+
+                       // Recent enough to pass
+                       $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+                       $this->assertSame(
+                               AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
+                       );
+
+                       // Not recent enough to pass
+                       $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
+                       $this->assertSame(
+                               AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+                       );
+                       // But recent enough for the 'test' operation
+                       $this->assertSame(
+                               AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
+                       );
+               } else {
+                       $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
+                               'test' => false,
+                               'default' => true,
+                       ] );
+
+                       $this->assertEquals(
+                               AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
+                       );
+
+                       $this->assertEquals(
+                               AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
+                       );
+               }
+
+               // Test hook, all three possible values
+               foreach ( [
+                       AuthManager::SEC_OK => AuthManager::SEC_OK,
+                       AuthManager::SEC_REAUTH => $reauth,
+                       AuthManager::SEC_FAIL => AuthManager::SEC_FAIL,
+               ] as $hook => $expect ) {
+                       $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
+                               ->with(
+                                       $this->anything(),
+                                       $this->anything(),
+                                       $this->callback( function ( $s ) use ( $session ) {
+                                               return $s->getId() === $session->getId();
+                                       } ),
+                                       $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
+                               )
+                               ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
+                                       $v = $hook;
+                                       return true;
+                               } ) );
+                       $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
+                       $this->assertEquals(
+                               $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
+                       );
+                       $this->assertEquals(
+                               $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
+                       );
+                       $this->unhook( 'SecuritySensitiveOperationStatus' );
+               }
+
+               \ScopedCallback::consume( $reset );
+       }
+
+       public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
+       }
+
+       public static function provideSecuritySensitiveOperationStatus() {
+               return [
+                       [ true ],
+                       [ false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserCanAuthenticate
+        * @param bool $primary1Can
+        * @param bool $primary2Can
+        * @param bool $expect
+        */
+       public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
+               $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary1' ) );
+               $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
+                       ->with( $this->equalTo( 'UTSysop' ) )
+                       ->will( $this->returnValue( $primary1Can ) );
+               $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary2' ) );
+               $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
+                       ->with( $this->equalTo( 'UTSysop' ) )
+                       ->will( $this->returnValue( $primary2Can ) );
+               $this->primaryauthMocks = [ $mock1, $mock2 ];
+
+               $this->initializeManager( true );
+               $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
+       }
+
+       public static function provideUserCanAuthenticate() {
+               return [
+                       [ false, false, false ],
+                       [ true, false, true ],
+                       [ false, true, true ],
+                       [ true, true, true ],
+               ];
+       }
+
+       public function testRevokeAccessForUser() {
+               $this->initializeManager();
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary' ) );
+               $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
+                       ->with( $this->equalTo( 'UTSysop' ) );
+               $this->primaryauthMocks = [ $mock ];
+
+               $this->initializeManager( true );
+               $this->logger->setCollect( true );
+
+               $this->manager->revokeAccessForUser( 'UTSysop' );
+
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'Revoking access for {user}' ],
+               ], $this->logger->getBuffer() );
+       }
+
+       public function testProviderCreation() {
+               $mocks = [
+                       'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
+                       'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+                       'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
+               ];
+               foreach ( $mocks as $key => $mock ) {
+                       $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
+                       $mock->expects( $this->once() )->method( 'setLogger' );
+                       $mock->expects( $this->once() )->method( 'setManager' );
+                       $mock->expects( $this->once() )->method( 'setConfig' );
+               }
+               $this->preauthMocks = [ $mocks['pre'] ];
+               $this->primaryauthMocks = [ $mocks['primary'] ];
+               $this->secondaryauthMocks = [ $mocks['secondary'] ];
+
+               // Normal operation
+               $this->initializeManager();
+               $this->assertSame(
+                       $mocks['primary'],
+                       $this->managerPriv->getAuthenticationProvider( 'primary' )
+               );
+               $this->assertSame(
+                       $mocks['secondary'],
+                       $this->managerPriv->getAuthenticationProvider( 'secondary' )
+               );
+               $this->assertSame(
+                       $mocks['pre'],
+                       $this->managerPriv->getAuthenticationProvider( 'pre' )
+               );
+               $this->assertSame(
+                       [ 'pre' => $mocks['pre'] ],
+                       $this->managerPriv->getPreAuthenticationProviders()
+               );
+               $this->assertSame(
+                       [ 'primary' => $mocks['primary'] ],
+                       $this->managerPriv->getPrimaryAuthenticationProviders()
+               );
+               $this->assertSame(
+                       [ 'secondary' => $mocks['secondary'] ],
+                       $this->managerPriv->getSecondaryAuthenticationProviders()
+               );
+
+               // Duplicate IDs
+               $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
+               $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $this->preauthMocks = [ $mock1 ];
+               $this->primaryauthMocks = [ $mock2 ];
+               $this->secondaryauthMocks = [];
+               $this->initializeManager( true );
+               try {
+                       $this->managerPriv->getAuthenticationProvider( 'Y' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $class1 = get_class( $mock1 );
+                       $class2 = get_class( $mock2 );
+                       $this->assertSame(
+                               "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
+                       );
+               }
+
+               // Wrong classes
+               $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $class = get_class( $mock );
+               $this->preauthMocks = [ $mock ];
+               $this->primaryauthMocks = [ $mock ];
+               $this->secondaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+               try {
+                       $this->managerPriv->getPreAuthenticationProviders();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $this->assertSame(
+                               "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       $this->managerPriv->getPrimaryAuthenticationProviders();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $this->assertSame(
+                               "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       $this->managerPriv->getSecondaryAuthenticationProviders();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $this->assertSame(
+                               "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
+                               $ex->getMessage()
+                       );
+               }
+
+               // Sorting
+               $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+               $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
+               $this->preauthMocks = [];
+               $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
+               $this->secondaryauthMocks = [];
+               $this->initializeConfig();
+               $config = $this->config->get( 'AuthManagerConfig' );
+
+               $this->initializeManager( false );
+               $this->assertSame(
+                       [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
+                       $this->managerPriv->getPrimaryAuthenticationProviders(),
+                       'sanity check'
+               );
+
+               $config['primaryauth']['A']['sort'] = 100;
+               $config['primaryauth']['C']['sort'] = -1;
+               $this->config->set( 'AuthManagerConfig', $config );
+               $this->initializeManager( false );
+               $this->assertSame(
+                       [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
+                       $this->managerPriv->getPrimaryAuthenticationProviders()
+               );
+       }
+
+       public function testSetDefaultUserOptions() {
+               $this->initializeManager();
+
+               $context = \RequestContext::getMain();
+               $reset = new \ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
+               $context->setLanguage( 'de' );
+               $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
+
+               $user = \User::newFromName( self::usernameForCreation() );
+               $user->addToDatabase();
+               $oldToken = $user->getToken();
+               $this->managerPriv->setDefaultUserOptions( $user, false );
+               $user->saveSettings();
+               $this->assertNotEquals( $oldToken, $user->getToken() );
+               $this->assertSame( 'zh', $user->getOption( 'language' ) );
+               $this->assertSame( 'zh', $user->getOption( 'variant' ) );
+
+               $user = \User::newFromName( self::usernameForCreation() );
+               $user->addToDatabase();
+               $oldToken = $user->getToken();
+               $this->managerPriv->setDefaultUserOptions( $user, true );
+               $user->saveSettings();
+               $this->assertNotEquals( $oldToken, $user->getToken() );
+               $this->assertSame( 'de', $user->getOption( 'language' ) );
+               $this->assertSame( 'zh', $user->getOption( 'variant' ) );
+
+               $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) );
+
+               $user = \User::newFromName( self::usernameForCreation() );
+               $user->addToDatabase();
+               $oldToken = $user->getToken();
+               $this->managerPriv->setDefaultUserOptions( $user, true );
+               $user->saveSettings();
+               $this->assertNotEquals( $oldToken, $user->getToken() );
+               $this->assertSame( 'de', $user->getOption( 'language' ) );
+               $this->assertSame( null, $user->getOption( 'variant' ) );
+       }
+
+       public function testForcePrimaryAuthenticationProviders() {
+               $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
+               $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+               $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+               $this->primaryauthMocks = [ $mockA ];
+
+               $this->logger = new \TestLogger( true );
+
+               // Test without first initializing the configured providers
+               $this->initializeManager();
+               $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
+               $this->assertSame(
+                       [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
+               );
+               $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+               $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+               $this->assertSame( [
+                       [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
+               ], $this->logger->getBuffer() );
+               $this->logger->clearBuffer();
+
+               // Test with first initializing the configured providers
+               $this->initializeManager();
+               $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+               $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+               $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
+               $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
+               $this->assertSame(
+                       [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
+               );
+               $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+               $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+               $this->assertSame( [
+                       [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
+                       [
+                               LogLevel::WARNING,
+                               'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
+                       ],
+               ], $this->logger->getBuffer() );
+               $this->logger->clearBuffer();
+
+               // Test duplicate IDs
+               $this->initializeManager();
+               try {
+                       $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $class1 = get_class( $mockB );
+                       $class2 = get_class( $mockB2 );
+                       $this->assertSame(
+                               "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
+                       );
+               }
+
+               // Wrong classes
+               $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $class = get_class( $mock );
+               try {
+                       $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \RuntimeException $ex ) {
+                       $this->assertSame(
+                               "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
+                               $ex->getMessage()
+                       );
+               }
+
+       }
+
+       public function testBeginAuthentication() {
+               $this->initializeManager();
+
+               // Immutable session
+               list( $provider, $reset ) = $this->getMockSessionProvider( false );
+               $this->hook( 'UserLoggedIn', $this->never() );
+               $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+               try {
+                       $this->manager->beginAuthentication( [], 'http://localhost/' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
+               }
+               $this->unhook( 'UserLoggedIn' );
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+               \ScopedCallback::consume( $reset );
+               $this->initializeManager( true );
+
+               // CreatedAccountAuthenticationRequest
+               $user = \User::newFromName( 'UTSysop' );
+               $reqs = [
+                       new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
+               ];
+               $this->hook( 'UserLoggedIn', $this->never() );
+               try {
+                       $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertSame(
+                               'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
+                                       'that created the account',
+                               $ex->getMessage()
+                       );
+               }
+               $this->unhook( 'UserLoggedIn' );
+
+               $this->request->getSession()->clear();
+               $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+               $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
+               $this->hook( 'UserLoggedIn', $this->once() )
+                       ->with( $this->callback( function ( $u ) use ( $user ) {
+                               return $user->getId() === $u->getId() && $user->getName() === $u->getName();
+                       } ) );
+               $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
+               $this->logger->setCollect( false );
+               $this->unhook( 'UserLoggedIn' );
+               $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+               $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+               $this->assertSame( $user->getName(), $ret->username );
+               $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
+               $this->assertEquals(
+                       time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
+                       'timestamp ±1', 1
+               );
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+               $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'Logging in {user} after account creation' ],
+               ], $this->logger->getBuffer() );
+       }
+
+       public function testCreateFromLogin() {
+               $user = \User::newFromName( 'UTSysop' );
+               $req1 = $this->getMock( AuthenticationRequest::class );
+               $req2 = $this->getMock( AuthenticationRequest::class );
+               $req3 = $this->getMock( AuthenticationRequest::class );
+               $userReq = new UsernameAuthenticationRequest;
+               $userReq->username = 'UTDummy';
+
+               // Passing one into beginAuthentication(), and an immediate FAIL
+               $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+               $this->primaryauthMocks = [ $primary ];
+               $this->initializeManager( true );
+               $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
+               $res->createRequest = $req1;
+               $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+                       ->will( $this->returnValue( $res ) );
+               $createReq = new CreateFromLoginAuthenticationRequest(
+                       null, [ $req2->getUniqueId() => $req2 ]
+               );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
+               $this->assertSame( $req1, $ret->createRequest->createRequest );
+               $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
+
+               // UI, then FAIL in beginAuthentication()
+               $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
+                       ->setMethods( [ 'continuePrimaryAuthentication' ] )
+                       ->getMockForAbstractClass();
+               $this->primaryauthMocks = [ $primary ];
+               $this->initializeManager( true );
+               $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+                       ->will( $this->returnValue(
+                               AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
+                       ) );
+               $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
+               $res->createRequest = $req2;
+               $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
+                       ->will( $this->returnValue( $res ) );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+               $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
+               $ret = $this->manager->continueAuthentication( [] );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
+               $this->assertSame( $req2, $ret->createRequest->createRequest );
+               $this->assertEquals( [], $ret->createRequest->maybeLink );
+
+               // Pass into beginAccountCreation(), no createRequest, primary needs reqs
+               $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
+                       ->setMethods( [ 'testForAccountCreation' ] )
+                       ->getMockForAbstractClass();
+               $this->primaryauthMocks = [ $primary ];
+               $this->initializeManager( true );
+               $primary->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $primary->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnValue( [ $req1 ] ) );
+               $primary->expects( $this->any() )->method( 'testForAccountCreation' )
+                       ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+               $createReq = new CreateFromLoginAuthenticationRequest(
+                       null, [ $req2->getUniqueId() => $req2 ]
+               );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAccountCreation(
+                       $user, [ $userReq, $createReq ], 'http://localhost/'
+               );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::UI, $ret->status );
+               $this->assertCount( 4, $ret->neededRequests );
+               $this->assertSame( $req1, $ret->neededRequests[0] );
+               $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
+               $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
+               $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
+               $this->assertSame( null, $ret->neededRequests[3]->createRequest );
+               $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
+
+               // Pass into beginAccountCreation(), with createRequest, primary needs reqs
+               $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAccountCreation(
+                       $user, [ $userReq, $createReq ], 'http://localhost/'
+               );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'fail', $ret->message->getKey() );
+
+               // Again, with a secondary needing reqs too
+               $secondary = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
+                       ->getMockForAbstractClass();
+               $this->secondaryauthMocks = [ $secondary ];
+               $this->initializeManager( true );
+               $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnValue( [ $req3 ] ) );
+               $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->beginAccountCreation(
+                       $user, [ $userReq, $createReq ], 'http://localhost/'
+               );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::UI, $ret->status );
+               $this->assertCount( 4, $ret->neededRequests );
+               $this->assertSame( $req3, $ret->neededRequests[0] );
+               $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
+               $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
+               $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
+               $this->assertSame( $req2, $ret->neededRequests[3]->createRequest );
+               $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
+               $this->logger->setCollect( true );
+               $ret = $this->manager->continueAccountCreation( $ret->neededRequests );
+               $this->logger->setCollect( false );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'fail', $ret->message->getKey() );
+       }
+
+       /**
+        * @dataProvider provideAuthentication
+        * @param StatusValue $preResponse
+        * @param array $primaryResponses
+        * @param array $secondaryResponses
+        * @param array $managerResponses
+        * @param bool $link Whether the primary authentication provider is a "link" provider
+        */
+       public function testAuthentication(
+               StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
+               array $managerResponses, $link = false
+       ) {
+               $this->initializeManager();
+               $user = \User::newFromName( 'UTSysop' );
+               $id = $user->getId();
+               $name = $user->getName();
+
+               // Set up lots of mocks...
+               $req = new RememberMeAuthenticationRequest;
+               $req->rememberMe = (bool)rand( 0, 1 );
+               $req->pre = $preResponse;
+               $req->primary = $primaryResponses;
+               $req->secondary = $secondaryResponses;
+               $mocks = [];
+               foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $mocks[$key . '2'] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key . '2' ) );
+                       $mocks[$key . '3'] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key . '3' ) );
+               }
+               foreach ( $mocks as $mock ) {
+                       $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                               ->will( $this->returnValue( [] ) );
+               }
+
+               $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
+                       ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
+                               $this->assertContains( $req, $reqs );
+                               return $req->pre;
+                       } ) );
+
+               $ct = count( $req->primary );
+               $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
+                       $this->assertContains( $req, $reqs );
+                       return array_shift( $req->primary );
+               } );
+               $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+                       ->method( 'beginPrimaryAuthentication' )
+                       ->will( $callback );
+               $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+                       ->method( 'continuePrimaryAuthentication' )
+                       ->will( $callback );
+               if ( $link ) {
+                       $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+                               ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               }
+
+               $ct = count( $req->secondary );
+               $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
+                       $this->assertSame( $id, $user->getId() );
+                       $this->assertSame( $name, $user->getName() );
+                       $this->assertContains( $req, $reqs );
+                       return array_shift( $req->secondary );
+               } );
+               $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
+                       ->method( 'beginSecondaryAuthentication' )
+                       ->will( $callback );
+               $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+                       ->method( 'continueSecondaryAuthentication' )
+                       ->will( $callback );
+
+               $abstain = AuthenticationResponse::newAbstain();
+               $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
+                               ->will( $this->returnValue( $abstain ) );
+               $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
+               $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
+                               ->will( $this->returnValue( $abstain ) );
+               $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
+               $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
+                               ->will( $this->returnValue( $abstain ) );
+               $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
+
+               $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+               $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
+               $this->secondaryauthMocks = [
+                       $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
+                       // So linking happens
+                       new ConfirmLinkSecondaryAuthenticationProvider,
+               ];
+               $this->initializeManager( true );
+               $this->logger->setCollect( true );
+
+               $constraint = \PHPUnit_Framework_Assert::logicalOr(
+                       $this->equalTo( AuthenticationResponse::PASS ),
+                       $this->equalTo( AuthenticationResponse::FAIL )
+               );
+               $providers = array_filter(
+                       array_merge(
+                               $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
+                       ),
+                       function ( $p ) {
+                               return is_callable( [ $p, 'expects' ] );
+                       }
+               );
+               foreach ( $providers as $p ) {
+                       $p->postCalled = false;
+                       $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
+                               ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
+                                       if ( $user !== null ) {
+                                               $this->assertInstanceOf( 'User', $user );
+                                               $this->assertSame( 'UTSysop', $user->getName() );
+                                       }
+                                       $this->assertInstanceOf( AuthenticationResponse::class, $response );
+                                       $this->assertThat( $response->status, $constraint );
+                                       $p->postCalled = $response->status;
+                               } );
+               }
+
+               $session = $this->request->getSession();
+               $session->setRememberUser( !$req->rememberMe );
+
+               foreach ( $managerResponses as $i => $response ) {
+                       $success = $response instanceof AuthenticationResponse &&
+                               $response->status === AuthenticationResponse::PASS;
+                       if ( $success ) {
+                               $this->hook( 'UserLoggedIn', $this->once() )
+                                       ->with( $this->callback( function ( $user ) use ( $id, $name ) {
+                                               return $user->getId() === $id && $user->getName() === $name;
+                                       } ) );
+                       } else {
+                               $this->hook( 'UserLoggedIn', $this->never() );
+                       }
+                       if ( $success || (
+                                       $response instanceof AuthenticationResponse &&
+                                       $response->status === AuthenticationResponse::FAIL &&
+                                       $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
+                                       $response->message->getKey() !== 'authmanager-authn-no-primary'
+                               )
+                       ) {
+                               $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
+                       } else {
+                               $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
+                       }
+
+                       $ex = null;
+                       try {
+                               if ( !$i ) {
+                                       $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
+                               } else {
+                                       $ret = $this->manager->continueAuthentication( [ $req ] );
+                               }
+                               if ( $response instanceof \Exception ) {
+                                       $this->fail( 'Expected exception not thrown', "Response $i" );
+                               }
+                       } catch ( \Exception $ex ) {
+                               if ( !$response instanceof \Exception ) {
+                                       throw $ex;
+                               }
+                               $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+                               $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
+                                       "Response $i, exception, session state" );
+                               $this->unhook( 'UserLoggedIn' );
+                               $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+                               return;
+                       }
+
+                       $this->unhook( 'UserLoggedIn' );
+                       $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+
+                       $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+                       $ret->message = $this->message( $ret->message );
+                       $this->assertEquals( $response, $ret, "Response $i, response" );
+                       if ( $success ) {
+                               $this->assertSame( $id, $session->getUser()->getId(),
+                                       "Response $i, authn" );
+                       } else {
+                               $this->assertSame( 0, $session->getUser()->getId(),
+                                       "Response $i, authn" );
+                       }
+                       if ( $success || $response->status === AuthenticationResponse::FAIL ) {
+                               $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
+                                       "Response $i, session state" );
+                               foreach ( $providers as $p ) {
+                                       $this->assertSame( $response->status, $p->postCalled,
+                                               "Response $i, post-auth callback called" );
+                               }
+                       } else {
+                               $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
+                                       "Response $i, session state" );
+                               $this->assertEquals(
+                                       $ret->neededRequests,
+                                       $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
+                                       "Response $i, continuation check"
+                               );
+                               foreach ( $providers as $p ) {
+                                       $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+                               }
+                       }
+
+                       $state = $session->getSecret( 'AuthManager::authnState' );
+                       $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
+                       if ( $link && $response->status === AuthenticationResponse::RESTART ) {
+                               $this->assertEquals(
+                                       $response->createRequest->maybeLink,
+                                       $maybeLink,
+                                       "Response $i, maybeLink"
+                               );
+                       } else {
+                               $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
+                       }
+               }
+
+               if ( $success ) {
+                       $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
+                               'rememberMe checkbox had effect' );
+               } else {
+                       $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
+                               'rememberMe checkbox wasn\'t applied' );
+               }
+       }
+
+       public function provideAuthentication() {
+               $user = \User::newFromName( 'UTSysop' );
+               $id = $user->getId();
+               $name = $user->getName();
+
+               $rememberReq = new RememberMeAuthenticationRequest;
+               $rememberReq->action = AuthManager::ACTION_LOGIN;
+
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $req->foobar = 'baz';
+               $restartResponse = AuthenticationResponse::newRestart(
+                       $this->message( 'authmanager-authn-no-local-user' )
+               );
+               $restartResponse->neededRequests = [ $rememberReq ];
+
+               $restartResponse2Pass = AuthenticationResponse::newPass( null );
+               $restartResponse2Pass->linkRequest = $req;
+               $restartResponse2 = AuthenticationResponse::newRestart(
+                       $this->message( 'authmanager-authn-no-local-user-link' )
+               );
+               $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
+                       null, [ $req->getUniqueId() => $req ]
+               );
+               $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
+
+               return [
+                       'Failure in pre-auth' => [
+                               StatusValue::newFatal( 'fail-from-pre' ),
+                               [],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+                                       AuthenticationResponse::newFail(
+                                               $this->message( 'authmanager-authn-not-in-progress' )
+                                       ),
+                               ]
+                       ],
+                       'Failure in primary' => [
+                               StatusValue::newGood(),
+                               $tmp = [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+                               ],
+                               [],
+                               $tmp
+                       ],
+                       'All primary abstain' => [
+                               StatusValue::newGood(),
+                               [
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
+                               ]
+                       ],
+                       'Primary UI, then redirect, then fail' => [
+                               StatusValue::newGood(),
+                               $tmp = [
+                                       AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+                                       AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+                               ],
+                               [],
+                               $tmp
+                       ],
+                       'Primary redirect, then abstain' => [
+                               StatusValue::newGood(),
+                               [
+                                       $tmp = AuthenticationResponse::newRedirect(
+                                               [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+                                       ),
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [],
+                               [
+                                       $tmp,
+                                       new \DomainException(
+                                               'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
+                                       )
+                               ]
+                       ],
+                       'Primary UI, then pass with no local user' => [
+                               StatusValue::newGood(),
+                               [
+                                       $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newPass( null ),
+                               ],
+                               [],
+                               [
+                                       $tmp,
+                                       $restartResponse,
+                               ]
+                       ],
+                       'Primary UI, then pass with no local user (link type)' => [
+                               StatusValue::newGood(),
+                               [
+                                       $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       $restartResponse2Pass,
+                               ],
+                               [],
+                               [
+                                       $tmp,
+                                       $restartResponse2,
+                               ],
+                               true
+                       ],
+                       'Primary pass with invalid username' => [
+                               StatusValue::newGood(),
+                               [
+                                       AuthenticationResponse::newPass( '<>' ),
+                               ],
+                               [],
+                               [
+                                       new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
+                               ]
+                       ],
+                       'Secondary fail' => [
+                               StatusValue::newGood(),
+                               [
+                                       AuthenticationResponse::newPass( $name ),
+                               ],
+                               $tmp = [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
+                               ],
+                               $tmp
+                       ],
+                       'Secondary UI, then abstain' => [
+                               StatusValue::newGood(),
+                               [
+                                       AuthenticationResponse::newPass( $name ),
+                               ],
+                               [
+                                       $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newAbstain()
+                               ],
+                               [
+                                       $tmp,
+                                       AuthenticationResponse::newPass( $name ),
+                               ]
+                       ],
+                       'Secondary pass' => [
+                               StatusValue::newGood(),
+                               [
+                                       AuthenticationResponse::newPass( $name ),
+                               ],
+                               [
+                                       AuthenticationResponse::newPass()
+                               ],
+                               [
+                                       AuthenticationResponse::newPass( $name ),
+                               ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserExists
+        * @param bool $primary1Exists
+        * @param bool $primary2Exists
+        * @param bool $expect
+        */
+       public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
+               $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary1' ) );
+               $mock1->expects( $this->any() )->method( 'testUserExists' )
+                       ->with( $this->equalTo( 'UTSysop' ) )
+                       ->will( $this->returnValue( $primary1Exists ) );
+               $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary2' ) );
+               $mock2->expects( $this->any() )->method( 'testUserExists' )
+                       ->with( $this->equalTo( 'UTSysop' ) )
+                       ->will( $this->returnValue( $primary2Exists ) );
+               $this->primaryauthMocks = [ $mock1, $mock2 ];
+
+               $this->initializeManager( true );
+               $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
+       }
+
+       public static function provideUserExists() {
+               return [
+                       [ false, false, false ],
+                       [ true, false, true ],
+                       [ false, true, true ],
+                       [ true, true, true ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAllowsAuthenticationDataChange
+        * @param StatusValue $primaryReturn
+        * @param StatusValue $secondaryReturn
+        * @param Status $expect
+        */
+       public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+               $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
+               $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                       ->with( $this->equalTo( $req ) )
+                       ->will( $this->returnValue( $primaryReturn ) );
+               $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
+               $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                       ->with( $this->equalTo( $req ) )
+                       ->will( $this->returnValue( $secondaryReturn ) );
+
+               $this->primaryauthMocks = [ $mock1 ];
+               $this->secondaryauthMocks = [ $mock2 ];
+               $this->initializeManager( true );
+               $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
+       }
+
+       public static function provideAllowsAuthenticationDataChange() {
+               $ignored = \Status::newGood( 'ignored' );
+               $ignored->warning( 'authmanager-change-not-supported' );
+
+               $okFromPrimary = StatusValue::newGood();
+               $okFromPrimary->warning( 'warning-from-primary' );
+               $okFromSecondary = StatusValue::newGood();
+               $okFromSecondary->warning( 'warning-from-secondary' );
+
+               return [
+                       [
+                               StatusValue::newGood(),
+                               StatusValue::newGood(),
+                               \Status::newGood(),
+                       ],
+                       [
+                               StatusValue::newGood(),
+                               StatusValue::newGood( 'ignore' ),
+                               \Status::newGood(),
+                       ],
+                       [
+                               StatusValue::newGood( 'ignored' ),
+                               StatusValue::newGood(),
+                               \Status::newGood(),
+                       ],
+                       [
+                               StatusValue::newGood( 'ignored' ),
+                               StatusValue::newGood( 'ignored' ),
+                               $ignored,
+                       ],
+                       [
+                               StatusValue::newFatal( 'fail from primary' ),
+                               StatusValue::newGood(),
+                               \Status::newFatal( 'fail from primary' ),
+                       ],
+                       [
+                               $okFromPrimary,
+                               StatusValue::newGood(),
+                               \Status::wrap( $okFromPrimary ),
+                       ],
+                       [
+                               StatusValue::newGood(),
+                               StatusValue::newFatal( 'fail from secondary' ),
+                               \Status::newFatal( 'fail from secondary' ),
+                       ],
+                       [
+                               StatusValue::newGood(),
+                               $okFromSecondary,
+                               \Status::wrap( $okFromSecondary ),
+                       ],
+               ];
+       }
+
+       public function testChangeAuthenticationData() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $req->username = 'UTSysop';
+
+               $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
+               $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
+                       ->with( $this->equalTo( $req ) );
+               $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
+               $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
+                       ->with( $this->equalTo( $req ) );
+
+               $this->primaryauthMocks = [ $mock1, $mock2 ];
+               $this->initializeManager( true );
+               $this->logger->setCollect( true );
+               $this->manager->changeAuthenticationData( $req );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
+               ], $this->logger->getBuffer() );
+       }
+
+       public function testCanCreateAccounts() {
+               $types = [
+                       PrimaryAuthenticationProvider::TYPE_CREATE => true,
+                       PrimaryAuthenticationProvider::TYPE_LINK => true,
+                       PrimaryAuthenticationProvider::TYPE_NONE => false,
+               ];
+
+               foreach ( $types as $type => $can ) {
+                       $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+                       $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
+                       $mock->expects( $this->any() )->method( 'accountCreationType' )
+                               ->will( $this->returnValue( $type ) );
+                       $this->primaryauthMocks = [ $mock ];
+                       $this->initializeManager( true );
+                       $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
+               }
+       }
+
+       public function testCheckAccountCreatePermissions() {
+               global $wgGroupPermissions;
+
+               $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
+
+               $this->initializeManager( true );
+
+               $wgGroupPermissions['*']['createaccount'] = true;
+               $this->assertEquals(
+                       \Status::newGood(),
+                       $this->manager->checkAccountCreatePermissions( new \User )
+               );
+
+               $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
+               $this->assertEquals(
+                       \Status::newFatal( 'readonlytext', 'Because' ),
+                       $this->manager->checkAccountCreatePermissions( new \User )
+               );
+               $this->setMwGlobals( [ 'wgReadOnly' => false ] );
+
+               $wgGroupPermissions['*']['createaccount'] = false;
+               $status = $this->manager->checkAccountCreatePermissions( new \User );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
+               $wgGroupPermissions['*']['createaccount'] = true;
+
+               $user = \User::newFromName( 'UTBlockee' );
+               if ( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
+                       $user->saveSettings();
+               }
+               $oldBlock = \Block::newFromTarget( 'UTBlockee' );
+               if ( $oldBlock ) {
+                       // An old block will prevent our new one from saving.
+                       $oldBlock->delete();
+               }
+               $blockOptions = [
+                       'address' => 'UTBlockee',
+                       'user' => $user->getID(),
+                       'reason' => __METHOD__,
+                       'expiry' => time() + 100500,
+                       'createAccount' => true,
+               ];
+               $block = new \Block( $blockOptions );
+               $block->insert();
+               $status = $this->manager->checkAccountCreatePermissions( $user );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+
+               $blockOptions = [
+                       'address' => '127.0.0.0/24',
+                       'reason' => __METHOD__,
+                       'expiry' => time() + 100500,
+                       'createAccount' => true,
+               ];
+               $block = new \Block( $blockOptions );
+               $block->insert();
+               $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
+               $status = $this->manager->checkAccountCreatePermissions( new \User );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+               \ScopedCallback::consume( $scopeVariable );
+
+               $this->setMwGlobals( [
+                       'wgEnableDnsBlacklist' => true,
+                       'wgDnsBlacklistUrls' => [
+                               'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
+                       ],
+                       'wgProxyWhitelist' => [],
+               ] );
+               $status = $this->manager->checkAccountCreatePermissions( new \User );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
+               $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
+               $status = $this->manager->checkAccountCreatePermissions( new \User );
+               $this->assertTrue( $status->isGood() );
+       }
+
+       /**
+        * @param string $uniq
+        * @return string
+        */
+       private static function usernameForCreation( $uniq = '' ) {
+               $i = 0;
+               do {
+                       $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
+               } while ( \User::newFromName( $username )->getId() !== 0 );
+               return $username;
+       }
+
+       public function testCanCreateAccount() {
+               $username = self::usernameForCreation();
+               $this->initializeManager();
+
+               $this->assertEquals(
+                       \Status::newFatal( 'authmanager-create-disabled' ),
+                       $this->manager->canCreateAccount( $username )
+               );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->assertEquals(
+                       \Status::newFatal( 'userexists' ),
+                       $this->manager->canCreateAccount( $username )
+               );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->assertEquals(
+                       \Status::newFatal( 'noname' ),
+                       $this->manager->canCreateAccount( $username . '<>' )
+               );
+
+               $this->assertEquals(
+                       \Status::newFatal( 'userexists' ),
+                       $this->manager->canCreateAccount( 'UTSysop' )
+               );
+
+               $this->assertEquals(
+                       \Status::newGood(),
+                       $this->manager->canCreateAccount( $username )
+               );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->assertEquals(
+                       \Status::newFatal( 'fail' ),
+                       $this->manager->canCreateAccount( $username )
+               );
+       }
+
+       public function testBeginAccountCreation() {
+               $creator = \User::newFromName( 'UTSysop' );
+               $userReq = new UsernameAuthenticationRequest;
+               $this->logger = new \TestLogger( false, function ( $message, $level ) {
+                       return $level === LogLevel::DEBUG ? null : $message;
+               } );
+               $this->initializeManager();
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               try {
+                       $this->manager->beginAccountCreation(
+                               $creator, [], 'http://localhost/'
+                       );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
+               }
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = self::usernameForCreation();
+               $userReq2 = new UsernameAuthenticationRequest;
+               $userReq2->username = $userReq->username . 'X';
+               $ret = $this->manager->beginAccountCreation(
+                       $creator, [ $userReq, $userReq2 ], 'http://localhost/'
+               );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+
+               $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = self::usernameForCreation();
+               $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'readonlytext', $ret->message->getKey() );
+               $this->assertSame( [ 'Because' ], $ret->message->getParams() );
+               $this->setMwGlobals( [ 'wgReadOnly' => false ] );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = self::usernameForCreation();
+               $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'userexists', $ret->message->getKey() );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = self::usernameForCreation();
+               $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'fail', $ret->message->getKey() );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = self::usernameForCreation() . '<>';
+               $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $userReq->username = $creator->getName();
+               $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'userexists', $ret->message->getKey() );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $mock->expects( $this->any() )->method( 'testForAccountCreation' )
+                       ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
+                       ->setMethods( [ 'populateUser' ] )
+                       ->getMock();
+               $req->expects( $this->any() )->method( 'populateUser' )
+                       ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
+               $userReq->username = self::usernameForCreation();
+               $ret = $this->manager->beginAccountCreation(
+                       $creator, [ $userReq, $req ], 'http://localhost/'
+               );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'populatefail', $ret->message->getKey() );
+
+               $req = new UserDataAuthenticationRequest;
+               $userReq->username = self::usernameForCreation();
+
+               $ret = $this->manager->beginAccountCreation(
+                       $creator, [ $userReq, $req ], 'http://localhost/'
+               );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'fail', $ret->message->getKey() );
+
+               $this->manager->beginAccountCreation(
+                       \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
+               );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'fail', $ret->message->getKey() );
+       }
+
+       public function testContinueAccountCreation() {
+               $creator = \User::newFromName( 'UTSysop' );
+               $username = self::usernameForCreation();
+               $this->logger = new \TestLogger( false, function ( $message, $level ) {
+                       return $level === LogLevel::DEBUG ? null : $message;
+               } );
+               $this->initializeManager();
+
+               $session = [
+                       'userid' => 0,
+                       'username' => $username,
+                       'creatorid' => 0,
+                       'creatorname' => $username,
+                       'reqs' => [],
+                       'primary' => null,
+                       'primaryResponse' => null,
+                       'secondary' => [],
+                       'ranPreTests' => true,
+               ];
+
+               $this->hook( 'LocalUserCreated', $this->never() );
+               try {
+                       $this->manager->continueAccountCreation( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
+               }
+               $this->unhook( 'LocalUserCreated' );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
+                       $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
+               );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->continueAccountCreation( [] );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'username' => "$username<>" ] + $session );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->continueAccountCreation( [] );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+               $ret = $this->manager->continueAccountCreation( [] );
+               unset( $lock );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
+               // This error shouldn't remove the existing session, because the
+               // raced-with process "owns" it.
+               $this->assertSame(
+                       $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'username' => $creator->getName() ] + $session );
+               $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->continueAccountCreation( [] );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'readonlytext', $ret->message->getKey() );
+               $this->assertSame( [ 'Because' ], $ret->message->getParams() );
+               $this->setMwGlobals( [ 'wgReadOnly' => false ] );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'username' => $creator->getName() ] + $session );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->continueAccountCreation( [] );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'userexists', $ret->message->getKey() );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'userid' => $creator->getId() ] + $session );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               try {
+                       $ret = $this->manager->continueAccountCreation( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
+               }
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $id = $creator->getId();
+               $name = $creator->getName();
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'username' => $name, 'userid' => $id + 1 ] + $session );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               try {
+                       $ret = $this->manager->continueAccountCreation( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertEquals(
+                               "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
+                       );
+               }
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+
+               $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
+                       ->setMethods( [ 'populateUser' ] )
+                       ->getMock();
+               $req->expects( $this->any() )->method( 'populateUser' )
+                       ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
+               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+                       [ 'reqs' => [ $req ] ] + $session );
+               $ret = $this->manager->continueAccountCreation( [] );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'populatefail', $ret->message->getKey() );
+               $this->assertNull(
+                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+               );
+       }
+
+       /**
+        * @dataProvider provideAccountCreation
+        * @param StatusValue $preTest
+        * @param StatusValue $primaryTest
+        * @param StatusValue $secondaryTest
+        * @param array $primaryResponses
+        * @param array $secondaryResponses
+        * @param array $managerResponses
+        */
+       public function testAccountCreation(
+               StatusValue $preTest, $primaryTest, $secondaryTest,
+               array $primaryResponses, array $secondaryResponses, array $managerResponses
+       ) {
+               $creator = \User::newFromName( 'UTSysop' );
+               $username = self::usernameForCreation();
+
+               $this->initializeManager();
+
+               // Set up lots of mocks...
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $req->preTest = $preTest;
+               $req->primaryTest = $primaryTest;
+               $req->secondaryTest = $secondaryTest;
+               $req->primary = $primaryResponses;
+               $req->secondary = $secondaryResponses;
+               $mocks = [];
+               foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
+                               ->will( $this->returnValue( StatusValue::newGood() ) );
+                       $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
+                               ->will( $this->returnCallback(
+                                       function ( $user, $creatorIn, $reqs )
+                                               use ( $username, $creator, $req, $key )
+                                       {
+                                               $this->assertSame( $username, $user->getName() );
+                                               $this->assertSame( $creator->getId(), $creatorIn->getId() );
+                                               $this->assertSame( $creator->getName(), $creatorIn->getName() );
+                                               $foundReq = false;
+                                               foreach ( $reqs as $r ) {
+                                                       $this->assertSame( $username, $r->username );
+                                                       $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+                                               }
+                                               $this->assertTrue( $foundReq, '$reqs contains $req' );
+                                               $k = $key . 'Test';
+                                               return $req->$k;
+                                       }
+                               ) );
+
+                       for ( $i = 2; $i <= 3; $i++ ) {
+                               $mocks[$key . $i] = $this->getMockForAbstractClass(
+                                       "MediaWiki\\Auth\\$class", [], "Mock$class"
+                               );
+                               $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
+                                       ->will( $this->returnValue( $key . $i ) );
+                               $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
+                                       ->will( $this->returnValue( StatusValue::newGood() ) );
+                               $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
+                                       ->will( $this->returnValue( StatusValue::newGood() ) );
+                       }
+               }
+
+               $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
+                       ->will( $this->returnValue( false ) );
+               $ct = count( $req->primary );
+               $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
+                       $this->assertSame( $username, $user->getName() );
+                       $this->assertSame( 'UTSysop', $creator->getName() );
+                       $foundReq = false;
+                       foreach ( $reqs as $r ) {
+                               $this->assertSame( $username, $r->username );
+                               $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+                       }
+                       $this->assertTrue( $foundReq, '$reqs contains $req' );
+                       return array_shift( $req->primary );
+               } );
+               $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+                       ->method( 'beginPrimaryAccountCreation' )
+                       ->will( $callback );
+               $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+                       ->method( 'continuePrimaryAccountCreation' )
+                       ->will( $callback );
+
+               $ct = count( $req->secondary );
+               $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
+                       $this->assertSame( $username, $user->getName() );
+                       $this->assertSame( 'UTSysop', $creator->getName() );
+                       $foundReq = false;
+                       foreach ( $reqs as $r ) {
+                               $this->assertSame( $username, $r->username );
+                               $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+                       }
+                       $this->assertTrue( $foundReq, '$reqs contains $req' );
+                       return array_shift( $req->secondary );
+               } );
+               $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
+                       ->method( 'beginSecondaryAccountCreation' )
+                       ->will( $callback );
+               $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+                       ->method( 'continueSecondaryAccountCreation' )
+                       ->will( $callback );
+
+               $abstain = AuthenticationResponse::newAbstain();
+               $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
+                       ->will( $this->returnValue( false ) );
+               $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
+                       ->will( $this->returnValue( $abstain ) );
+               $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
+               $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
+               $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
+                       ->will( $this->returnValue( false ) );
+               $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
+               $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
+               $mocks['secondary2']->expects( $this->atMost( 1 ) )
+                       ->method( 'beginSecondaryAccountCreation' )
+                       ->will( $this->returnValue( $abstain ) );
+               $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
+               $mocks['secondary3']->expects( $this->atMost( 1 ) )
+                       ->method( 'beginSecondaryAccountCreation' )
+                       ->will( $this->returnValue( $abstain ) );
+               $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
+
+               $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+               $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
+               $this->secondaryauthMocks = [
+                       $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
+               ];
+
+               $this->logger = new \TestLogger( true, function ( $message, $level ) {
+                       return $level === LogLevel::DEBUG ? null : $message;
+               } );
+               $expectLog = [];
+               $this->initializeManager( true );
+
+               $constraint = \PHPUnit_Framework_Assert::logicalOr(
+                       $this->equalTo( AuthenticationResponse::PASS ),
+                       $this->equalTo( AuthenticationResponse::FAIL )
+               );
+               $providers = array_merge(
+                       $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
+               );
+               foreach ( $providers as $p ) {
+                       $p->postCalled = false;
+                       $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
+                               ->willReturnCallback( function ( $user, $creator, $response )
+                                       use ( $constraint, $p, $username )
+                               {
+                                       $this->assertInstanceOf( 'User', $user );
+                                       $this->assertSame( $username, $user->getName() );
+                                       $this->assertSame( 'UTSysop', $creator->getName() );
+                                       $this->assertInstanceOf( AuthenticationResponse::class, $response );
+                                       $this->assertThat( $response->status, $constraint );
+                                       $p->postCalled = $response->status;
+                               } );
+               }
+
+               // We're testing with $wgNewUserLog = false, so assert that it worked
+               $dbw = wfGetDB( DB_MASTER );
+               $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+
+               $first = true;
+               $created = false;
+               foreach ( $managerResponses as $i => $response ) {
+                       $success = $response instanceof AuthenticationResponse &&
+                               $response->status === AuthenticationResponse::PASS;
+                       if ( $i === 'created' ) {
+                               $created = true;
+                               $this->hook( 'LocalUserCreated', $this->once() )
+                                       ->with(
+                                               $this->callback( function ( $user ) use ( $username ) {
+                                                       return $user->getName() === $username;
+                                               } ),
+                                               $this->equalTo( false )
+                                       );
+                               $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
+                       } else {
+                               $this->hook( 'LocalUserCreated', $this->never() );
+                       }
+
+                       $ex = null;
+                       try {
+                               if ( $first ) {
+                                       $userReq = new UsernameAuthenticationRequest;
+                                       $userReq->username = $username;
+                                       $ret = $this->manager->beginAccountCreation(
+                                               $creator, [ $userReq, $req ], 'http://localhost/'
+                                       );
+                               } else {
+                                       $ret = $this->manager->continueAccountCreation( [ $req ] );
+                               }
+                               if ( $response instanceof \Exception ) {
+                                       $this->fail( 'Expected exception not thrown', "Response $i" );
+                               }
+                       } catch ( \Exception $ex ) {
+                               if ( !$response instanceof \Exception ) {
+                                       throw $ex;
+                               }
+                               $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+                               $this->assertNull(
+                                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+                                       "Response $i, exception, session state"
+                               );
+                               $this->unhook( 'LocalUserCreated' );
+                               return;
+                       }
+
+                       $this->unhook( 'LocalUserCreated' );
+
+                       $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+                       if ( $success ) {
+                               $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
+                               $this->assertContains(
+                                       $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
+                                       "Response $i, login marker"
+                               );
+
+                               $expectLog[] = [
+                                       LogLevel::INFO,
+                                       "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
+                               ];
+
+                               // Set some fields in the expected $response that we couldn't
+                               // know in provideAccountCreation().
+                               $response->username = $username;
+                               $response->loginRequest = $ret->loginRequest;
+                       } else {
+                               $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
+                               $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
+                                       "Response $i, login marker" );
+                       }
+                       $ret->message = $this->message( $ret->message );
+                       $this->assertEquals( $response, $ret, "Response $i, response" );
+                       if ( $success || $response->status === AuthenticationResponse::FAIL ) {
+                               $this->assertNull(
+                                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+                                       "Response $i, session state"
+                               );
+                               foreach ( $providers as $p ) {
+                                       $this->assertSame( $response->status, $p->postCalled,
+                                               "Response $i, post-auth callback called" );
+                               }
+                       } else {
+                               $this->assertNotNull(
+                                       $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+                                       "Response $i, session state"
+                               );
+                               $this->assertEquals(
+                                       $ret->neededRequests,
+                                       $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
+                                       "Response $i, continuation check"
+                               );
+                               foreach ( $providers as $p ) {
+                                       $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+                               }
+                       }
+
+                       if ( $created ) {
+                               $this->assertNotEquals( 0, \User::idFromName( $username ) );
+                       } else {
+                               $this->assertEquals( 0, \User::idFromName( $username ) );
+                       }
+
+                       $first = false;
+               }
+
+               $this->assertSame( $expectLog, $this->logger->getBuffer() );
+
+               $this->assertSame(
+                       $maxLogId,
+                       $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
+               );
+       }
+
+       public function provideAccountCreation() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $good = StatusValue::newGood();
+
+               return [
+                       'Pre-creation test fail in pre' => [
+                               StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
+                               [],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+                               ]
+                       ],
+                       'Pre-creation test fail in primary' => [
+                               $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
+                               [],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+                               ]
+                       ],
+                       'Pre-creation test fail in secondary' => [
+                               $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
+                               [],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
+                               ]
+                       ],
+                       'Failure in primary' => [
+                               $good, $good, $good,
+                               $tmp = [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+                               ],
+                               [],
+                               $tmp
+                       ],
+                       'All primary abstain' => [
+                               $good, $good, $good,
+                               [
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
+                               ]
+                       ],
+                       'Primary UI, then redirect, then fail' => [
+                               $good, $good, $good,
+                               $tmp = [
+                                       AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+                                       AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+                               ],
+                               [],
+                               $tmp
+                       ],
+                       'Primary redirect, then abstain' => [
+                               $good, $good, $good,
+                               [
+                                       $tmp = AuthenticationResponse::newRedirect(
+                                               [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+                                       ),
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [],
+                               [
+                                       $tmp,
+                                       new \DomainException(
+                                               'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
+                                       )
+                               ]
+                       ],
+                       'Primary UI, then pass; secondary abstain' => [
+                               $good, $good, $good,
+                               [
+                                       $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newPass(),
+                               ],
+                               [
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [
+                                       $tmp1,
+                                       'created' => AuthenticationResponse::newPass( '' ),
+                               ]
+                       ],
+                       'Primary pass; secondary UI then pass' => [
+                               $good, $good, $good,
+                               [
+                                       AuthenticationResponse::newPass( '' ),
+                               ],
+                               [
+                                       $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newPass( '' ),
+                               ],
+                               [
+                                       'created' => $tmp1,
+                                       AuthenticationResponse::newPass( '' ),
+                               ]
+                       ],
+                       'Primary pass; secondary fail' => [
+                               $good, $good, $good,
+                               [
+                                       AuthenticationResponse::newPass(),
+                               ],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( '...' ) ),
+                               ],
+                               [
+                                       'created' => new \DomainException(
+                                               'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
+                                                       'Secondary providers are not allowed to fail account creation, ' .
+                                                       'that should have been done via testForAccountCreation().'
+                                       )
+                               ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAccountCreationLogging
+        * @param bool $isAnon
+        * @param string|null $logSubtype
+        */
+       public function testAccountCreationLogging( $isAnon, $logSubtype ) {
+               $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
+               $username = self::usernameForCreation();
+
+               $this->initializeManager();
+
+               // Set up lots of mocks...
+               $mock = $this->getMockForAbstractClass(
+                       "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
+               );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary' ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $mock->expects( $this->any() )->method( 'testForAccountCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )
+                       ->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
+                       ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+               $mock->expects( $this->any() )->method( 'finishAccountCreation' )
+                       ->will( $this->returnValue( $logSubtype ) );
+
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+               $this->logger->setCollect( true );
+
+               $this->config->set( 'NewUserLog', true );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+
+               $userReq = new UsernameAuthenticationRequest;
+               $userReq->username = $username;
+               $reasonReq = new CreationReasonAuthenticationRequest;
+               $reasonReq->reason = $this->toString();
+               $ret = $this->manager->beginAccountCreation(
+                       $creator, [ $userReq, $reasonReq ], 'http://localhost/'
+               );
+
+               $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+
+               $user = \User::newFromName( $username );
+               $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
+               $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
+
+               $data = \DatabaseLogEntry::getSelectQueryData();
+               $rows = iterator_to_array( $dbw->select(
+                       $data['tables'],
+                       $data['fields'],
+                       [
+                               'log_id > ' . (int)$maxLogId,
+                               'log_type' => 'newusers'
+                       ] + $data['conds'],
+                       __METHOD__,
+                       $data['options'],
+                       $data['join_conds']
+               ) );
+               $this->assertCount( 1, $rows );
+               $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
+
+               $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
+               $this->assertSame(
+                       $isAnon ? $user->getId() : $creator->getId(),
+                       $entry->getPerformer()->getId()
+               );
+               $this->assertSame(
+                       $isAnon ? $user->getName() : $creator->getName(),
+                       $entry->getPerformer()->getName()
+               );
+               $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
+               $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
+               $this->assertSame( $this->toString(), $entry->getComment() );
+       }
+
+       public static function provideAccountCreationLogging() {
+               return [
+                       [ true, null ],
+                       [ true, 'foobar' ],
+                       [ false, null ],
+                       [ false, 'byemail' ],
+               ];
+       }
+
+       public function testAutoAccountCreation() {
+               global $wgGroupPermissions, $wgHooks;
+
+               // PHPUnit seems to have a bug where it will call the ->with()
+               // callbacks for our hooks again after the test is run (WTF?), which
+               // breaks here because $username no longer matches $user by the end of
+               // the testing.
+               $workaroundPHPUnitBug = false;
+
+               $username = self::usernameForCreation();
+               $this->initializeManager();
+
+               $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
+               $wgGroupPermissions['*']['createaccount'] = true;
+               $wgGroupPermissions['*']['autocreateaccount'] = false;
+
+               \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+               $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
+
+               // Set up lots of mocks...
+               $mocks = [];
+               foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+               }
+
+               $good = StatusValue::newGood();
+               $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
+                       return $workaroundPHPUnitBug || $user->getName() === $username;
+               } );
+
+               $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
+                       ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+                       ->will( $this->onConsecutiveCalls(
+                               StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
+                               StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
+                               $good, // backoff test
+                               $good, // addToDatabase fails test
+                               $good, // addToDatabase throws test
+                               $good, // addToDatabase exists test
+                               $good, $good, $good // success
+                       ) );
+
+               $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
+                       ->will( $this->returnValue( true ) );
+               $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
+                       ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+                       ->will( $this->onConsecutiveCalls(
+                               StatusValue::newFatal( 'fail-in-primary' ), $good,
+                               $good, // backoff test
+                               $good, // addToDatabase fails test
+                               $good, // addToDatabase throws test
+                               $good, // addToDatabase exists test
+                               $good, $good, $good
+                       ) );
+               $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
+                       ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
+
+               $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
+                       ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+                       ->will( $this->onConsecutiveCalls(
+                               StatusValue::newFatal( 'fail-in-secondary' ),
+                               $good, // backoff test
+                               $good, // addToDatabase fails test
+                               $good, // addToDatabase throws test
+                               $good, // addToDatabase exists test
+                               $good, $good, $good
+                       ) );
+               $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
+                       ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
+
+               $this->preauthMocks = [ $mocks['pre'] ];
+               $this->primaryauthMocks = [ $mocks['primary'] ];
+               $this->secondaryauthMocks = [ $mocks['secondary'] ];
+               $this->initializeManager( true );
+               $session = $this->request->getSession();
+
+               $logger = new \TestLogger( true, function ( $m ) {
+                       $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
+                       return $m;
+               } );
+               $this->manager->setLogger( $logger );
+
+               try {
+                       $user = \User::newFromName( 'UTSysop' );
+                       $this->manager->autoCreateUser( $user, 'InvalidSource', true );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
+               }
+
+               // First, check an existing user
+               $session->clear();
+               $user = \User::newFromName( 'UTSysop' );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $expect = \Status::newGood();
+               $expect->warning( 'userexists' );
+               $this->assertEquals( $expect, $ret );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSysop', $user->getName() );
+               $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, '{username} already exists locally' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $session->clear();
+               $user = \User::newFromName( 'UTSysop' );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+               $this->unhook( 'LocalUserCreated' );
+               $expect = \Status::newGood();
+               $expect->warning( 'userexists' );
+               $this->assertEquals( $expect, $ret );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertSame( 'UTSysop', $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, '{username} already exists locally' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Wiki is read-only
+               $session->clear();
+               $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'readonlytext', 'Because' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->setMwGlobals( [ 'wgReadOnly' => false ] );
+
+               // Session blacklisted
+               $session->clear();
+               $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'test' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $session->clear();
+               $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Uncreatable name
+               $session->clear();
+               $user = \User::newFromName( $username . '@' );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username . '@', $user->getId() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+               // IP unable to create accounts
+               $wgGroupPermissions['*']['createaccount'] = false;
+               $wgGroupPermissions['*']['autocreateaccount'] = false;
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame(
+                       'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
+               );
+
+               // Test that both permutations of permissions are allowed
+               // (this hits the two "ok" entries in $mocks['pre'])
+               $wgGroupPermissions['*']['createaccount'] = false;
+               $wgGroupPermissions['*']['autocreateaccount'] = true;
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
+
+               $wgGroupPermissions['*']['createaccount'] = true;
+               $wgGroupPermissions['*']['autocreateaccount'] = false;
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
+               $logger->clearBuffer();
+
+               // Test lock fail
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               unset( $lock );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Test pre-authentication provider fail
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertEquals(
+                       StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+               );
+
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertEquals(
+                       StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+               );
+
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertEquals(
+                       StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+               );
+
+               // Test backoff
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+               $cache->set( $backoffKey, true );
+               $session->clear();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+               $cache->delete( $backoffKey );
+
+               // Test addToDatabase fails
+               $session->clear();
+               $user = $this->getMock( 'User', [ 'addToDatabase' ] );
+               $user->expects( $this->once() )->method( 'addToDatabase' )
+                       ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
+               $user->setName( $username );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->assertEquals( \Status::newFatal( 'because' ), $ret );
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertNotEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+                       [ LogLevel::ERROR, '{username} failed with message {message}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+               // Test addToDatabase throws an exception
+               $cache = \ObjectCache::getLocalClusterInstance();
+               $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+               $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
+               $session->clear();
+               $user = $this->getMock( 'User', [ 'addToDatabase' ] );
+               $user->expects( $this->once() )->method( 'addToDatabase' )
+                       ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
+               $user->setName( $username );
+               try {
+                       $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \Exception $ex ) {
+                       $this->assertSame( 'Excepted', $ex->getMessage() );
+               }
+               $this->assertEquals( 0, $user->getId() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+                       [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+               $this->assertNotEquals( false, $cache->get( $backoffKey ) );
+               $cache->delete( $backoffKey );
+
+               // Test addToDatabase fails because the user already exists.
+               $session->clear();
+               $user = $this->getMock( 'User', [ 'addToDatabase' ] );
+               $user->expects( $this->once() )->method( 'addToDatabase' )
+                       ->will( $this->returnCallback( function () use ( $username ) {
+                               $status = \User::newFromName( $username )->addToDatabase();
+                               $this->assertTrue( $status->isOK(), 'sanity check' );
+                               return \Status::newFatal( 'userexists' );
+                       } ) );
+               $user->setName( $username );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $expect = \Status::newGood();
+               $expect->warning( 'userexists' );
+               $this->assertEquals( $expect, $ret );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertEquals( $username, $user->getName() );
+               $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+                       [ LogLevel::INFO, '{username} already exists locally (race)' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+               // Success!
+               $session->clear();
+               $username = self::usernameForCreation();
+               $user = \User::newFromName( $username );
+               $this->hook( 'AuthPluginAutoCreate', $this->once() )
+                       ->with( $callback );
+               $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
+                               get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
+               $this->hook( 'LocalUserCreated', $this->once() )
+                       ->with( $callback, $this->equalTo( true ) );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+               $this->unhook( 'LocalUserCreated' );
+               $this->unhook( 'AuthPluginAutoCreate' );
+               $this->assertEquals( \Status::newGood(), $ret );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertEquals( $username, $user->getName() );
+               $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+               $session->clear();
+               $username = self::usernameForCreation();
+               $user = \User::newFromName( $username );
+               $this->hook( 'LocalUserCreated', $this->once() )
+                       ->with( $callback, $this->equalTo( true ) );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+               $this->unhook( 'LocalUserCreated' );
+               $this->assertEquals( \Status::newGood(), $ret );
+               $this->assertNotEquals( 0, $user->getId() );
+               $this->assertEquals( $username, $user->getName() );
+               $this->assertEquals( 0, $session->getUser()->getId() );
+               $this->assertSame( [
+                       [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+               $this->assertSame(
+                       $maxLogId,
+                       $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
+               );
+
+               $this->config->set( 'NewUserLog', true );
+               $session->clear();
+               $username = self::usernameForCreation();
+               $user = \User::newFromName( $username );
+               $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+               $this->assertEquals( \Status::newGood(), $ret );
+               $logger->clearBuffer();
+
+               $data = \DatabaseLogEntry::getSelectQueryData();
+               $rows = iterator_to_array( $dbw->select(
+                       $data['tables'],
+                       $data['fields'],
+                       [
+                               'log_id > ' . (int)$maxLogId,
+                               'log_type' => 'newusers'
+                       ] + $data['conds'],
+                       __METHOD__,
+                       $data['options'],
+                       $data['join_conds']
+               ) );
+               $this->assertCount( 1, $rows );
+               $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
+
+               $this->assertSame( 'autocreate', $entry->getSubtype() );
+               $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
+               $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
+               $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
+               $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
+
+               $workaroundPHPUnitBug = true;
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $expect
+        * @param array $state
+        */
+       public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
+               $makeReq = function ( $key ) use ( $action ) {
+                       $req = $this->getMock( AuthenticationRequest::class );
+                       $req->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $req->action = $action === AuthManager::ACTION_UNLINK ? AuthManager::ACTION_REMOVE : $action;
+                       $req->key = $key;
+                       return $req;
+               };
+               $cmpReqs = function ( $a, $b ) {
+                       $ret = strcmp( get_class( $a ), get_class( $b ) );
+                       if ( !$ret ) {
+                               $ret = strcmp( $a->key, $b->key );
+                       }
+                       return $ret;
+               };
+
+               $good = StatusValue::newGood();
+
+               $mocks = [];
+               foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                               ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
+                                       return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
+                               } ) );
+                       $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                               ->will( $this->returnValue( $good ) );
+               }
+
+               $primaries = [];
+               foreach ( [
+                       PrimaryAuthenticationProvider::TYPE_NONE,
+                       PrimaryAuthenticationProvider::TYPE_CREATE,
+                       PrimaryAuthenticationProvider::TYPE_LINK
+               ] as $type ) {
+                       $class = 'PrimaryAuthenticationProvider';
+                       $mocks["primary-$type"] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( "primary-$type" ) );
+                       $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
+                               ->will( $this->returnValue( $type ) );
+                       $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                               ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
+                                       return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
+                               } ) );
+                       $mocks["primary-$type"]->expects( $this->any() )
+                               ->method( 'providerAllowsAuthenticationDataChange' )
+                               ->will( $this->returnValue( $good ) );
+                       $this->primaryauthMocks[] = $mocks["primary-$type"];
+               }
+
+               $mocks['primary2'] = $this->getMockForAbstractClass(
+                       PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
+               );
+               $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary2' ) );
+               $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnValue( [] ) );
+               $mocks['primary2']->expects( $this->any() )
+                       ->method( 'providerAllowsAuthenticationDataChange' )
+                       ->will( $this->returnCallback( function ( $req ) use ( $good ) {
+                               return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
+                       } ) );
+               $this->primaryauthMocks[] = $mocks['primary2'];
+
+               $this->preauthMocks = [ $mocks['pre'] ];
+               $this->secondaryauthMocks = [ $mocks['secondary'] ];
+               $this->initializeManager( true );
+
+               if ( $state ) {
+                       if ( isset( $state['continueRequests'] ) ) {
+                               $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
+                       }
+                       if ( $action === AuthManager::ACTION_LOGIN_CONTINUE ) {
+                               $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
+                       } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
+                               $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
+                       } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
+                               $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
+                       }
+               }
+
+               $expectReqs = array_map( $makeReq, $expect );
+               if ( $action === AuthManager::ACTION_LOGIN ) {
+                       $req = new RememberMeAuthenticationRequest;
+                       $req->action = $action;
+                       $req->required = AuthenticationRequest::REQUIRED;
+                       $expectReqs[] = $req;
+               } elseif ( $action === AuthManager::ACTION_CREATE ) {
+                       $req = new UsernameAuthenticationRequest;
+                       $req->action = $action;
+                       $expectReqs[] = $req;
+                       $req = new UserDataAuthenticationRequest;
+                       $req->action = $action;
+                       $req->required = AuthenticationRequest::REQUIRED;
+                       $expectReqs[] = $req;
+               }
+               usort( $expectReqs, $cmpReqs );
+
+               $actual = $this->manager->getAuthenticationRequests( $action );
+               foreach ( $actual as $req ) {
+                       // Don't test this here.
+                       $req->required = AuthenticationRequest::REQUIRED;
+               }
+               usort( $actual, $cmpReqs );
+
+               $this->assertEquals( $expectReqs, $actual );
+
+               // Test CreationReasonAuthenticationRequest gets returned
+               if ( $action === AuthManager::ACTION_CREATE ) {
+                       $req = new CreationReasonAuthenticationRequest;
+                       $req->action = $action;
+                       $req->required = AuthenticationRequest::REQUIRED;
+                       $expectReqs[] = $req;
+                       usort( $expectReqs, $cmpReqs );
+
+                       $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
+                       foreach ( $actual as $req ) {
+                               // Don't test this here.
+                               $req->required = AuthenticationRequest::REQUIRED;
+                       }
+                       usort( $actual, $cmpReqs );
+
+                       $this->assertEquals( $expectReqs, $actual );
+               }
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [
+                               AuthManager::ACTION_LOGIN,
+                               [ 'pre-login', 'primary-none-login', 'primary-create-login',
+                                       'primary-link-login', 'secondary-login', 'generic' ],
+                       ],
+                       [
+                               AuthManager::ACTION_CREATE,
+                               [ 'pre-create', 'primary-none-create', 'primary-create-create',
+                                       'primary-link-create', 'secondary-create', 'generic' ],
+                       ],
+                       [
+                               AuthManager::ACTION_LINK,
+                               [ 'primary-link-link', 'generic' ],
+                       ],
+                       [
+                               AuthManager::ACTION_CHANGE,
+                               [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
+                                       'secondary-change' ],
+                       ],
+                       [
+                               AuthManager::ACTION_REMOVE,
+                               [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
+                                       'secondary-remove' ],
+                       ],
+                       [
+                               AuthManager::ACTION_UNLINK,
+                               [ 'primary-link-remove' ],
+                       ],
+                       [
+                               AuthManager::ACTION_LOGIN_CONTINUE,
+                               [],
+                       ],
+                       [
+                               AuthManager::ACTION_LOGIN_CONTINUE,
+                               $reqs = [ 'continue-login', 'foo', 'bar' ],
+                               [
+                                       'continueRequests' => $reqs,
+                               ],
+                       ],
+                       [
+                               AuthManager::ACTION_CREATE_CONTINUE,
+                               [],
+                       ],
+                       [
+                               AuthManager::ACTION_CREATE_CONTINUE,
+                               $reqs = [ 'continue-create', 'foo', 'bar' ],
+                               [
+                                       'continueRequests' => $reqs,
+                               ],
+                       ],
+                       [
+                               AuthManager::ACTION_LINK_CONTINUE,
+                               [],
+                       ],
+                       [
+                               AuthManager::ACTION_LINK_CONTINUE,
+                               $reqs = [ 'continue-link', 'foo', 'bar' ],
+                               [
+                                       'continueRequests' => $reqs,
+                               ],
+                       ],
+               ];
+       }
+
+       public function testGetAuthenticationRequestsRequired() {
+               $makeReq = function ( $key, $required ) {
+                       $req = $this->getMock( AuthenticationRequest::class );
+                       $req->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $req->action = AuthManager::ACTION_LOGIN;
+                       $req->key = $key;
+                       $req->required = $required;
+                       return $req;
+               };
+               $cmpReqs = function ( $a, $b ) {
+                       $ret = strcmp( get_class( $a ), get_class( $b ) );
+                       if ( !$ret ) {
+                               $ret = strcmp( $a->key, $b->key );
+                       }
+                       return $ret;
+               };
+
+               $good = StatusValue::newGood();
+
+               $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $primary1->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary1' ) );
+               $primary1->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+                               return [
+                                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "required", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+                                       $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
+                               ];
+                       } ) );
+
+               $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $primary2->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'primary2' ) );
+               $primary2->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+                               return [
+                                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "required2", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
+                               ];
+                       } ) );
+
+               $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+               $secondary->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'secondary' ) );
+               $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
+                       ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+                               return [
+                                       $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
+                                       $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+                                       $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+                               ];
+                       } ) );
+
+               $rememberReq = new RememberMeAuthenticationRequest;
+               $rememberReq->action = AuthManager::ACTION_LOGIN;
+
+               $this->primaryauthMocks = [ $primary1, $primary2 ];
+               $this->secondaryauthMocks = [ $secondary ];
+               $this->initializeManager( true );
+
+               $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+               $expected = [
+                       $rememberReq,
+                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
+                       $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
+                       $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+                       $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
+                       $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
+                       $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+               ];
+               usort( $actual, $cmpReqs );
+               usort( $expected, $cmpReqs );
+               $this->assertEquals( $expected, $actual );
+
+               $this->primaryauthMocks = [ $primary1 ];
+               $this->secondaryauthMocks = [ $secondary ];
+               $this->initializeManager( true );
+
+               $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+               $expected = [
+                       $rememberReq,
+                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "required", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+                       $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+               ];
+               usort( $actual, $cmpReqs );
+               usort( $expected, $cmpReqs );
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testAllowsPropertyChange() {
+               $mocks = [];
+               foreach ( [ 'primary', 'secondary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+                       $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
+                               ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
+                                       return $prop !== $key;
+                               } ) );
+               }
+
+               $this->primaryauthMocks = [ $mocks['primary'] ];
+               $this->secondaryauthMocks = [ $mocks['secondary'] ];
+               $this->initializeManager( true );
+
+               $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
+               $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
+               $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
+       }
+
+       public function testAutoCreateOnLogin() {
+               $username = self::usernameForCreation();
+
+               $req = $this->getMock( AuthenticationRequest::class );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
+               $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+                       ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+
+               $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+               $mock2->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( 'secondary' ) );
+               $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
+                       $this->returnValue(
+                               AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
+                       )
+               );
+               $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
+                       ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
+               $mock2->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+
+               $this->primaryauthMocks = [ $mock ];
+               $this->secondaryauthMocks = [ $mock2 ];
+               $this->initializeManager( true );
+               $this->manager->setLogger( new \Psr\Log\NullLogger() );
+               $session = $this->request->getSession();
+               $session->clear();
+
+               $this->assertSame( 0, \User::newFromName( $username )->getId(),
+                       'sanity check' );
+
+               $callback = $this->callback( function ( $user ) use ( $username ) {
+                       return $user->getName() === $username;
+               } );
+
+               $this->hook( 'UserLoggedIn', $this->never() );
+               $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
+               $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->unhook( 'UserLoggedIn' );
+               $this->assertSame( AuthenticationResponse::UI, $ret->status );
+
+               $id = (int)\User::newFromName( $username )->getId();
+               $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
+               $this->assertSame( 0, $session->getUser()->getId() );
+
+               $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->continueAuthentication( [] );
+               $this->unhook( 'LocalUserCreated' );
+               $this->unhook( 'UserLoggedIn' );
+               $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+               $this->assertSame( $username, $ret->username );
+               $this->assertSame( $id, $session->getUser()->getId() );
+       }
+
+       public function testAutoCreateFailOnLogin() {
+               $username = self::usernameForCreation();
+
+               $mock = $this->getMockForAbstractClass(
+                       PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
+               $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+                       ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )->method( 'testUserForCreation' )
+                       ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
+
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+               $this->manager->setLogger( new \Psr\Log\NullLogger() );
+               $session = $this->request->getSession();
+               $session->clear();
+
+               $this->assertSame( 0, $session->getUser()->getId(),
+                       'sanity check' );
+               $this->assertSame( 0, \User::newFromName( $username )->getId(),
+                       'sanity check' );
+
+               $this->hook( 'UserLoggedIn', $this->never() );
+               $this->hook( 'LocalUserCreated', $this->never() );
+               $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+               $this->unhook( 'LocalUserCreated' );
+               $this->unhook( 'UserLoggedIn' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
+
+               $this->assertSame( 0, \User::newFromName( $username )->getId() );
+               $this->assertSame( 0, $session->getUser()->getId() );
+       }
+
+       public function testAuthenticationSessionData() {
+               $this->initializeManager( true );
+
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+               $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
+               $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
+               $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
+               $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
+               $this->manager->removeAuthenticationSessionData( 'foo' );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+               $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
+               $this->manager->removeAuthenticationSessionData( 'bar' );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
+
+               $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
+               $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
+
+       }
+
+       public function testCanLinkAccounts() {
+               $types = [
+                       PrimaryAuthenticationProvider::TYPE_CREATE => true,
+                       PrimaryAuthenticationProvider::TYPE_LINK => true,
+                       PrimaryAuthenticationProvider::TYPE_NONE => false,
+               ];
+
+               foreach ( $types as $type => $can ) {
+                       $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+                       $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
+                       $mock->expects( $this->any() )->method( 'accountCreationType' )
+                               ->will( $this->returnValue( $type ) );
+                       $this->primaryauthMocks = [ $mock ];
+                       $this->initializeManager( true );
+                       $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
+               }
+       }
+
+       public function testBeginAccountLink() {
+               $user = \User::newFromName( 'UTSysop' );
+               $this->initializeManager();
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
+               try {
+                       $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
+               }
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+
+               $ret = $this->manager->beginAccountLink(
+                       \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
+               );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
+       }
+
+       public function testContinueAccountLink() {
+               $user = \User::newFromName( 'UTSysop' );
+               $this->initializeManager();
+
+               $session = [
+                       'userid' => $user->getId(),
+                       'username' => $user->getName(),
+                       'primary' => 'X',
+               ];
+
+               try {
+                       $this->manager->continueAccountLink( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \LogicException $ex ) {
+                       $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
+               }
+
+               $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+               $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+               $mock->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
+                       $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
+               );
+               $this->primaryauthMocks = [ $mock ];
+               $this->initializeManager( true );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
+               $ret = $this->manager->continueAccountLink( [] );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
+
+               $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
+                       [ 'username' => $user->getName() . '<>' ] + $session );
+               $ret = $this->manager->continueAccountLink( [] );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'noname', $ret->message->getKey() );
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+
+               $id = $user->getId();
+               $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
+                       [ 'userid' => $id + 1 ] + $session );
+               try {
+                       $ret = $this->manager->continueAccountLink( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertEquals(
+                               "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
+                               $ex->getMessage()
+                       );
+               }
+               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+       }
+
+       /**
+        * @dataProvider provideAccountLink
+        * @param StatusValue $preTest
+        * @param array $primaryResponses
+        * @param array $managerResponses
+        */
+       public function testAccountLink(
+               StatusValue $preTest, array $primaryResponses, array $managerResponses
+       ) {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $this->initializeManager();
+
+               // Set up lots of mocks...
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $req->primary = $primaryResponses;
+               $mocks = [];
+
+               foreach ( [ 'pre', 'primary' ] as $key ) {
+                       $class = ucfirst( $key ) . 'AuthenticationProvider';
+                       $mocks[$key] = $this->getMockForAbstractClass(
+                               "MediaWiki\\Auth\\$class", [], "Mock$class"
+                       );
+                       $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( $key ) );
+
+                       for ( $i = 2; $i <= 3; $i++ ) {
+                               $mocks[$key . $i] = $this->getMockForAbstractClass(
+                                       "MediaWiki\\Auth\\$class", [], "Mock$class"
+                               );
+                               $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
+                                       ->will( $this->returnValue( $key . $i ) );
+                       }
+               }
+
+               $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
+                       ->will( $this->returnCallback(
+                               function ( $u )
+                                       use ( $user, $preTest )
+                               {
+                                       $this->assertSame( $user->getId(), $u->getId() );
+                                       $this->assertSame( $user->getName(), $u->getName() );
+                                       return $preTest;
+                               }
+                       ) );
+
+               $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
+                       ->will( $this->returnValue( StatusValue::newGood() ) );
+
+               $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $ct = count( $req->primary );
+               $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
+                       $this->assertSame( $user->getId(), $u->getId() );
+                       $this->assertSame( $user->getName(), $u->getName() );
+                       $foundReq = false;
+                       foreach ( $reqs as $r ) {
+                               $this->assertSame( $user->getName(), $r->username );
+                               $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+                       }
+                       $this->assertTrue( $foundReq, '$reqs contains $req' );
+                       return array_shift( $req->primary );
+               } );
+               $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+                       ->method( 'beginPrimaryAccountLink' )
+                       ->will( $callback );
+               $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+                       ->method( 'continuePrimaryAccountLink' )
+                       ->will( $callback );
+
+               $abstain = AuthenticationResponse::newAbstain();
+               $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+               $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
+                       ->will( $this->returnValue( $abstain ) );
+               $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
+               $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
+                       ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+               $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
+               $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
+
+               $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+               $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
+               $this->logger = new \TestLogger( true, function ( $message, $level ) {
+                       return $level === LogLevel::DEBUG ? null : $message;
+               } );
+               $this->initializeManager( true );
+
+               $constraint = \PHPUnit_Framework_Assert::logicalOr(
+                       $this->equalTo( AuthenticationResponse::PASS ),
+                       $this->equalTo( AuthenticationResponse::FAIL )
+               );
+               $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
+               foreach ( $providers as $p ) {
+                       $p->postCalled = false;
+                       $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
+                               ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
+                                       $this->assertInstanceOf( 'User', $user );
+                                       $this->assertSame( 'UTSysop', $user->getName() );
+                                       $this->assertInstanceOf( AuthenticationResponse::class, $response );
+                                       $this->assertThat( $response->status, $constraint );
+                                       $p->postCalled = $response->status;
+                               } );
+               }
+
+               $first = true;
+               $created = false;
+               $expectLog = [];
+               foreach ( $managerResponses as $i => $response ) {
+                       if ( $response instanceof AuthenticationResponse &&
+                               $response->status === AuthenticationResponse::PASS
+                       ) {
+                               $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
+                       }
+
+                       $ex = null;
+                       try {
+                               if ( $first ) {
+                                       $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
+                               } else {
+                                       $ret = $this->manager->continueAccountLink( [ $req ] );
+                               }
+                               if ( $response instanceof \Exception ) {
+                                       $this->fail( 'Expected exception not thrown', "Response $i" );
+                               }
+                       } catch ( \Exception $ex ) {
+                               if ( !$response instanceof \Exception ) {
+                                       throw $ex;
+                               }
+                               $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+                               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+                                       "Response $i, exception, session state" );
+                               return;
+                       }
+
+                       $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+                       $ret->message = $this->message( $ret->message );
+                       $this->assertEquals( $response, $ret, "Response $i, response" );
+                       if ( $response->status === AuthenticationResponse::PASS ||
+                               $response->status === AuthenticationResponse::FAIL
+                       ) {
+                               $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+                                       "Response $i, session state" );
+                               foreach ( $providers as $p ) {
+                                       $this->assertSame( $response->status, $p->postCalled,
+                                               "Response $i, post-auth callback called" );
+                               }
+                       } else {
+                               $this->assertNotNull(
+                                       $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+                                       "Response $i, session state"
+                               );
+                               $this->assertEquals(
+                                       $ret->neededRequests,
+                                       $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
+                                       "Response $i, continuation check"
+                               );
+                               foreach ( $providers as $p ) {
+                                       $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+                               }
+                       }
+
+                       $first = false;
+               }
+
+               $this->assertSame( $expectLog, $this->logger->getBuffer() );
+       }
+
+       public function provideAccountLink() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $good = StatusValue::newGood();
+
+               return [
+                       'Pre-link test fail in pre' => [
+                               StatusValue::newFatal( 'fail-from-pre' ),
+                               [],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+                               ]
+                       ],
+                       'Failure in primary' => [
+                               $good,
+                               $tmp = [
+                                       AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+                               ],
+                               $tmp
+                       ],
+                       'All primary abstain' => [
+                               $good,
+                               [
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [
+                                       AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
+                               ]
+                       ],
+                       'Primary UI, then redirect, then fail' => [
+                               $good,
+                               $tmp = [
+                                       AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+                                       AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+                               ],
+                               $tmp
+                       ],
+                       'Primary redirect, then abstain' => [
+                               $good,
+                               [
+                                       $tmp = AuthenticationResponse::newRedirect(
+                                               [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+                                       ),
+                                       AuthenticationResponse::newAbstain(),
+                               ],
+                               [
+                                       $tmp,
+                                       new \DomainException(
+                                               'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
+                                       )
+                               ]
+                       ],
+                       'Primary UI, then pass' => [
+                               $good,
+                               [
+                                       $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+                                       AuthenticationResponse::newPass(),
+                               ],
+                               [
+                                       $tmp1,
+                                       AuthenticationResponse::newPass( '' ),
+                               ]
+                       ],
+                       'Primary pass' => [
+                               $good,
+                               [
+                                       AuthenticationResponse::newPass( '' ),
+                               ],
+                               [
+                                       AuthenticationResponse::newPass( '' ),
+                               ]
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..b676d69
--- /dev/null
@@ -0,0 +1,706 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthPluginPrimaryAuthenticationProvider
+ */
+class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testConstruction() {
+               $plugin = new AuthManagerAuthPlugin();
+               try {
+                       $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
+                                       'makes no sense.',
+                               $ex->getMessage()
+                       );
+               }
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       [ new PasswordAuthenticationRequest ],
+                       $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+               );
+
+               $req = $this->getMock( PasswordAuthenticationRequest::class );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, get_class( $req ) );
+               $this->assertEquals(
+                       [ $req ],
+                       $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+               );
+
+               $reqType = get_class( $this->getMock( AuthenticationRequest::class ) );
+               try {
+                       $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, $reqType );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               "$reqType is not a MediaWiki\\Auth\\PasswordAuthenticationRequest",
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testOnUserSaveSettings() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'updateExternalDB' )
+                       ->with( $this->identicalTo( $user ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               \Hooks::run( 'UserSaveSettings', [ $user ] );
+       }
+
+       public function testOnUserGroupsChanged() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'updateExternalDBGroups' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->identicalTo( [ 'added' ] ),
+                               $this->identicalTo( [ 'removed' ] )
+                       );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ] ] );
+       }
+
+       public function testOnUserLoggedIn() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->exactly( 2 ) )->method( 'updateUser' )
+                       ->with( $this->identicalTo( $user ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               \Hooks::run( 'UserLoggedIn', [ $user ] );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'updateUser' )
+                       ->will( $this->returnCallback( function ( &$user ) {
+                               $user = \User::newFromName( 'UTSysop' );
+                       } ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               try {
+                       \Hooks::run( 'UserLoggedIn', [ $user ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               get_class( $plugin ) . '::updateUser() tried to replace $user!',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testOnLocalUserCreated() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->exactly( 2 ) )->method( 'initUser' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( false ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'initUser' )
+                       ->will( $this->returnCallback( function ( &$user ) {
+                               $user = \User::newFromName( 'UTSysop' );
+                       } ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               try {
+                       \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               get_class( $plugin ) . '::initUser() tried to replace $user!',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testGetUniqueId() {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertSame(
+                       'MediaWiki\\Auth\\AuthPluginPrimaryAuthenticationProvider:' . get_class( $plugin ),
+                       $provider->getUniqueId()
+               );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        * @param bool $allowPasswordChange
+        */
+       public function testGetAuthenticationRequests( $action, $response, $allowPasswordChange ) {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'allowPasswordChange' )
+                       ->will( $this->returnValue( $allowPasswordChange ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               $arr = [ new PasswordAuthenticationRequest() ];
+               return [
+                       [ AuthManager::ACTION_LOGIN, $arr, true ],
+                       [ AuthManager::ACTION_LOGIN, $arr, false ],
+                       [ AuthManager::ACTION_CREATE, $arr, true ],
+                       [ AuthManager::ACTION_CREATE, $arr, false ],
+                       [ AuthManager::ACTION_LINK, [], true ],
+                       [ AuthManager::ACTION_LINK, [], false ],
+                       [ AuthManager::ACTION_CHANGE, $arr, true ],
+                       [ AuthManager::ACTION_CHANGE, [], false ],
+                       [ AuthManager::ACTION_REMOVE, $arr, true ],
+                       [ AuthManager::ACTION_REMOVE, [], false ],
+               ];
+       }
+
+       public function testAuthentication() {
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_LOGIN;
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->never() )->method( 'authenticate' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = 'foo';
+               $req->password = 'bar';
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'authenticate' )
+                       ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'Foo', $req ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( false ) );
+               $plugin->expects( $this->never() )->method( 'authenticate' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $pluginUser = $this->getMockBuilder( 'AuthPluginUser' )
+                       ->setMethods( [ 'isLocked' ] )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $pluginUser->expects( $this->once() )->method( 'isLocked' )
+                       ->will( $this->returnValue( true ) );
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'getUserInstance', 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'getUserInstance' )
+                       ->will( $this->returnValue( $pluginUser ) );
+               $plugin->expects( $this->never() )->method( 'authenticate' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'authenticate' )
+                       ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( false ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'authenticate', 'strict' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'authenticate' )
+                       ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( false ) );
+               $plugin->expects( $this->any() )->method( 'strict' )->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'authenticate', 'strictUserAuth' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'authenticate' )
+                       ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( false ) );
+               $plugin->expects( $this->any() )->method( 'strictUserAuth' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'domainList', 'validDomain', 'setDomain', 'userExists', 'authenticate' ] )
+                       ->getMock();
+               $plugin->expects( $this->any() )->method( 'domainList' )
+                       ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+               $plugin->expects( $this->any() )->method( 'validDomain' )
+                       ->will( $this->returnCallback( function ( $domain ) {
+                               return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+                       } ) );
+               $plugin->expects( $this->once() )->method( 'setDomain' )
+                       ->with( $this->equalTo( 'Domain2' ) );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'authenticate' )
+                       ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] );
+               $req->username = 'foo';
+               $req->password = 'bar';
+               $req->domain = 'Domain2';
+               $provider->beginPrimaryAuthentication( [ $req ] );
+       }
+
+       public function testTestUserExists() {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertTrue( $provider->testUserExists( 'foo' ) );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( false ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertFalse( $provider->testUserExists( 'foo' ) );
+       }
+
+       public function testTestUserCanAuthenticate() {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( false ) );
+               $plugin->expects( $this->never() )->method( 'getUserInstance' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) );
+
+               $pluginUser = $this->getMockBuilder( 'AuthPluginUser' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $pluginUser->expects( $this->once() )->method( 'isLocked' )
+                       ->will( $this->returnValue( true ) );
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'getUserInstance' )
+                       ->with( $this->callback( function ( $user ) {
+                               $this->assertInstanceOf( 'User', $user );
+                               $this->assertEquals( 'Foo', $user->getName() );
+                               return true;
+                       } ) )
+                       ->will( $this->returnValue( $pluginUser ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) );
+
+               $pluginUser = $this->getMockBuilder( 'AuthPluginUser' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $pluginUser->expects( $this->once() )->method( 'isLocked' )
+                       ->will( $this->returnValue( false ) );
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'userExists' )
+                       ->with( $this->equalTo( 'Foo' ) )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'getUserInstance' )
+                       ->with( $this->callback( function ( $user ) {
+                               $this->assertInstanceOf( 'User', $user );
+                               $this->assertEquals( 'Foo', $user->getName() );
+                               return true;
+                       } ) )
+                       ->will( $this->returnValue( $pluginUser ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'foo' ) );
+       }
+
+       public function testProviderRevokeAccessForUser() {
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'userExists', 'setPassword' ] )
+                       ->getMock();
+               $plugin->expects( $this->once() )->method( 'userExists' )->willReturn( true );
+               $plugin->expects( $this->once() )->method( 'setPassword' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->identicalTo( null ) )
+                       ->willReturn( true );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $provider->providerRevokeAccessForUser( 'foo' );
+
+               $plugin = $this->getMockBuilder( 'AuthPlugin' )
+                       ->setMethods( [ 'domainList', 'userExists', 'setPassword' ] )
+                       ->getMock();
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [ 'D1', 'D2', 'D3' ] );
+               $plugin->expects( $this->exactly( 3 ) )->method( 'userExists' )
+                       ->willReturnCallback( function () use ( $plugin ) {
+                               return $plugin->getDomain() !== 'D2';
+                       } );
+               $plugin->expects( $this->exactly( 2 ) )->method( 'setPassword' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->identicalTo( null ) )
+                       ->willReturnCallback( function () use ( $plugin ) {
+                               $this->assertNotEquals( 'D2', $plugin->getDomain() );
+                               return $plugin->getDomain() !== 'D1';
+                       } );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               try {
+                       $provider->providerRevokeAccessForUser( 'foo' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'AuthPlugin failed to reset password for Foo in the following domains: D1',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testProviderAllowsPropertyChange() {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'allowPropChange' )
+                       ->will( $this->returnCallback( function ( $prop ) {
+                               return $prop === 'allow';
+                       } ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertTrue( $provider->providerAllowsPropertyChange( 'allow' ) );
+               $this->assertFalse( $provider->providerAllowsPropertyChange( 'deny' ) );
+       }
+
+       /**
+        * @dataProvider provideProviderAllowsAuthenticationDataChange
+        * @param string $type
+        * @param bool|null $allow
+        * @param StatusValue $expect
+        */
+       public function testProviderAllowsAuthenticationDataChange( $type, $allow, $expect ) {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $allow === null ? $this->never() : $this->once() )
+                       ->method( 'allowPasswordChange' )->will( $this->returnValue( $allow ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               if ( $type === PasswordAuthenticationRequest::class ) {
+                       $req = new $type();
+               } else {
+                       $req = $this->getMock( $type );
+               }
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = 'UTSysop';
+               $req->password = 'Pa$$w0Rd!!!';
+               $req->retype = 'Pa$$w0Rd!!!';
+               $this->assertEquals( $expect, $provider->providerAllowsAuthenticationDataChange( $req ) );
+       }
+
+       public static function provideProviderAllowsAuthenticationDataChange() {
+               return [
+                       [ AuthenticationRequest::class, null, \StatusValue::newGood( 'ignored' ) ],
+                       [ PasswordAuthenticationRequest::class, true, \StatusValue::newGood() ],
+                       [
+                               PasswordAuthenticationRequest::class,
+                               false,
+                               \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' )
+                       ],
+               ];
+       }
+
+       public function testProviderChangeAuthenticationData() {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->never() )->method( 'setPassword' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $provider->providerChangeAuthenticationData(
+                       $this->getMock( AuthenticationRequest::class )
+               );
+
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = 'foo';
+               $req->password = 'bar';
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'setPassword' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $provider->providerChangeAuthenticationData( $req );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )->method( 'setPassword' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( false ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               try {
+                       $provider->providerChangeAuthenticationData( $req );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \ErrorPageError $e ) {
+                       $this->assertSame( 'authmanager-authplugin-setpass-failed-title', $e->title );
+                       $this->assertSame( 'authmanager-authplugin-setpass-failed-message', $e->msg );
+               }
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )
+                       ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+               $plugin->expects( $this->any() )->method( 'validDomain' )
+                       ->will( $this->returnCallback( function ( $domain ) {
+                               return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+                       } ) );
+               $plugin->expects( $this->once() )->method( 'setDomain' )
+                       ->with( $this->equalTo( 'Domain2' ) );
+               $plugin->expects( $this->once() )->method( 'setPassword' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] );
+               $req->username = 'foo';
+               $req->password = 'bar';
+               $req->domain = 'Domain2';
+               $provider->providerChangeAuthenticationData( $req );
+       }
+
+       /**
+        * @dataProvider provideAccountCreationType
+        * @param bool $can
+        * @param string $expect
+        */
+       public function testAccountCreationType( $can, $expect ) {
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->once() )
+                       ->method( 'canCreateAccounts' )->will( $this->returnValue( $can ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertSame( $expect, $provider->accountCreationType() );
+       }
+
+       public static function provideAccountCreationType() {
+               return [
+                       [ true, PrimaryAuthenticationProvider::TYPE_CREATE ],
+                       [ false, PrimaryAuthenticationProvider::TYPE_NONE ],
+               ];
+       }
+
+       public function testTestForAccountCreation() {
+               $user = \User::newFromName( 'foo' );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+       }
+
+       public function testAccountCreation() {
+               $user = \User::newFromName( 'foo' );
+               $user->setEmail( 'email' );
+               $user->setRealName( 'realname' );
+
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_CREATE;
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+                       ->will( $this->returnValue( false ) );
+               $plugin->expects( $this->never() )->method( 'addUser' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               try {
+                       $provider->beginPrimaryAccountCreation( $user, $user, [] );
+                       $this->fail( 'Expected exception was not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+                       );
+               }
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->never() )->method( 'addUser' );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = 'foo';
+               $req->password = 'bar';
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'addUser' )
+                       ->with(
+                               $this->callback( function ( $u ) {
+                                       return $u instanceof \User && $u->getName() === 'Foo';
+                               } ),
+                               $this->equalTo( 'bar' ),
+                               $this->equalTo( 'email' ),
+                               $this->equalTo( 'realname' )
+                       )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+               $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->once() )->method( 'addUser' )
+                       ->with(
+                               $this->callback( function ( $u ) {
+                                       return $u instanceof \User && $u->getName() === 'Foo';
+                               } ),
+                               $this->equalTo( 'bar' ),
+                               $this->equalTo( 'email' ),
+                               $this->equalTo( 'realname' )
+                       )
+                       ->will( $this->returnValue( false ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               $ret = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+               $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+               $this->assertSame( 'authmanager-authplugin-create-fail', $ret->message->getKey() );
+
+               $plugin = $this->getMock( 'AuthPlugin' );
+               $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+                       ->will( $this->returnValue( true ) );
+               $plugin->expects( $this->any() )->method( 'domainList' )
+                       ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+               $plugin->expects( $this->any() )->method( 'validDomain' )
+                       ->will( $this->returnCallback( function ( $domain ) {
+                               return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+                       } ) );
+               $plugin->expects( $this->once() )->method( 'setDomain' )
+                       ->with( $this->equalTo( 'Domain2' ) );
+               $plugin->expects( $this->once() )->method( 'addUser' )
+                       ->with( $this->callback( function ( $u ) {
+                               return $u instanceof \User && $u->getName() === 'Foo';
+                       } ), $this->equalTo( 'bar' ) )
+                       ->will( $this->returnValue( true ) );
+               $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+               list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] );
+               $req->username = 'foo';
+               $req->password = 'bar';
+               $req->domain = 'Domain2';
+               $provider->beginPrimaryAccountCreation( $user, $user, [ $req ] );
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/AuthenticationRequestTest.php b/tests/phpunit/includes/auth/AuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..84a0ea6
--- /dev/null
@@ -0,0 +1,514 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthenticationRequest
+ */
+class AuthenticationRequestTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testBasics() {
+               $mock = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+               $this->assertSame( get_class( $mock ), $mock->getUniqueId() );
+
+               $this->assertType( 'array', $mock->getMetadata() );
+
+               $ret = $mock->describeCredentials();
+               $this->assertInternalType( 'array', $ret );
+               $this->assertArrayHasKey( 'provider', $ret );
+               $this->assertInstanceOf( 'Message', $ret['provider'] );
+               $this->assertArrayHasKey( 'account', $ret );
+               $this->assertInstanceOf( 'Message', $ret['account'] );
+       }
+
+       public function testLoadRequestsFromSubmission() {
+               $mb = $this->getMockBuilder( AuthenticationRequest::class )
+                       ->setMethods( [ 'loadFromSubmission' ] );
+
+               $data = [ 'foo', 'bar' ];
+
+               $req1 = $mb->getMockForAbstractClass();
+               $req1->expects( $this->once() )->method( 'loadFromSubmission' )
+                       ->with( $this->identicalTo( $data ) )
+                       ->will( $this->returnValue( false ) );
+
+               $req2 = $mb->getMockForAbstractClass();
+               $req2->expects( $this->once() )->method( 'loadFromSubmission' )
+                       ->with( $this->identicalTo( $data ) )
+                       ->will( $this->returnValue( true ) );
+
+               $this->assertSame(
+                       [ $req2 ],
+                       AuthenticationRequest::loadRequestsFromSubmission( [ $req1, $req2 ], $data )
+               );
+       }
+
+       public function testGetRequestByClass() {
+               $mb = $this->getMockBuilder(
+                       AuthenticationRequest::class, 'AuthenticationRequestTest_AuthenticationRequest2'
+               );
+
+               $reqs = [
+                       $this->getMockForAbstractClass(
+                               AuthenticationRequest::class, [], 'AuthenticationRequestTest_AuthenticationRequest1'
+                       ),
+                       $mb->getMockForAbstractClass(),
+                       $mb->getMockForAbstractClass(),
+                       $this->getMockForAbstractClass(
+                               PasswordAuthenticationRequest::class, [],
+                               'AuthenticationRequestTest_PasswordAuthenticationRequest'
+                       ),
+               ];
+
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest0'
+               ) );
+               $this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest1'
+               ) );
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest2'
+               ) );
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, PasswordAuthenticationRequest::class
+               ) );
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'ClassThatDoesNotExist'
+               ) );
+
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest0', true
+               ) );
+               $this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest1', true
+               ) );
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'AuthenticationRequestTest_AuthenticationRequest2', true
+               ) );
+               $this->assertSame( $reqs[3], AuthenticationRequest::getRequestByClass(
+                       $reqs, PasswordAuthenticationRequest::class, true
+               ) );
+               $this->assertNull( AuthenticationRequest::getRequestByClass(
+                       $reqs, 'ClassThatDoesNotExist', true
+               ) );
+       }
+
+       public function testGetUsernameFromRequests() {
+               $mb = $this->getMockBuilder( AuthenticationRequest::class );
+
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $req = $mb->getMockForAbstractClass();
+                       $req->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+                               'username' => [
+                                       'type' => 'string',
+                               ],
+                       ] ) );
+                       $reqs[] = $req;
+               }
+
+               $req = $mb->getMockForAbstractClass();
+               $req->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [] ) );
+               $req->username = 'baz';
+               $reqs[] = $req;
+
+               $this->assertNull( AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+               $reqs[1]->username = 'foo';
+               $this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+               $reqs[0]->username = 'foo';
+               $reqs[2]->username = 'foo';
+               $this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+               $reqs[1]->username = 'bar';
+               try {
+                       AuthenticationRequest::getUsernameFromRequests( $reqs );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Conflicting username fields: "bar" from ' .
+                                       get_class( $reqs[1] ) . '::$username vs. "foo" from ' .
+                                       get_class( $reqs[0] ) . '::$username',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testMergeFieldInfo() {
+               $msg = wfMessage( 'foo' );
+
+               $req1 = $this->getMock( AuthenticationRequest::class );
+               $req1->required = AuthenticationRequest::REQUIRED;
+               $req1->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+                       'string1' => [
+                               'type' => 'string',
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+                       'string2' => [
+                               'type' => 'string',
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+                       'optional' => [
+                               'type' => 'string',
+                               'label' => $msg,
+                               'help' => $msg,
+                               'optional' => true,
+                       ],
+                       'select' => [
+                               'type' => 'select',
+                               'options' => [ 'foo' => $msg, 'baz' => $msg ],
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+               ] ) );
+
+               $req2 = $this->getMock( AuthenticationRequest::class );
+               $req2->required = AuthenticationRequest::REQUIRED;
+               $req2->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+                       'string1' => [
+                               'type' => 'string',
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+                       'string3' => [
+                               'type' => 'string',
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+                       'select' => [
+                               'type' => 'select',
+                               'options' => [ 'bar' => $msg, 'baz' => $msg ],
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+               ] ) );
+
+               $req3 = $this->getMock( AuthenticationRequest::class );
+               $req3->required = AuthenticationRequest::REQUIRED;
+               $req3->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+                       'string1' => [
+                               'type' => 'checkbox',
+                               'label' => $msg,
+                               'help' => $msg,
+                       ],
+               ] ) );
+
+               $req4 = $this->getMock( AuthenticationRequest::class );
+               $req4->required = AuthenticationRequest::REQUIRED;
+               $req4->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [] ) );
+
+               // Basic combining
+
+               $fields = AuthenticationRequest::mergeFieldInfo( [ $req1 ] );
+               $expect = $req1->getFieldInfo();
+               foreach ( $expect as $name => &$options ) {
+                       $options['optional'] = !empty( $options['optional'] );
+               }
+               unset( $options );
+               $this->assertEquals( $expect, $fields );
+
+               $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req4 ] );
+               $this->assertEquals( $expect, $fields );
+
+               try {
+                       AuthenticationRequest::mergeFieldInfo( [ $req1, $req3 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Field type conflict for "string1", "string" vs "checkbox"',
+                               $ex->getMessage()
+                       );
+               }
+
+               $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
+               $expect += $req2->getFieldInfo();
+               $expect['string2']['optional'] = false;
+               $expect['string3']['optional'] = false;
+               $expect['select']['options']['bar'] = $msg;
+               $this->assertEquals( $expect, $fields );
+
+               // Combining with something not required
+
+               $req1->required = AuthenticationRequest::PRIMARY_REQUIRED;
+
+               $fields = AuthenticationRequest::mergeFieldInfo( [ $req1 ] );
+               $expect = $req1->getFieldInfo();
+               foreach ( $expect as $name => &$options ) {
+                       $options['optional'] = true;
+               }
+               unset( $options );
+               $this->assertEquals( $expect, $fields );
+
+               $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
+               $expect += $req2->getFieldInfo();
+               $expect['string1']['optional'] = false;
+               $expect['string3']['optional'] = false;
+               $expect['select']['optional'] = false;
+               $expect['select']['options']['bar'] = $msg;
+               $this->assertEquals( $expect, $fields );
+       }
+
+       /**
+        * @dataProvider provideLoadFromSubmission
+        * @param array $fieldInfo
+        * @param array $data
+        * @param array|bool $expectState
+        */
+       public function testLoadFromSubmission( $fieldInfo, $data, $expectState ) {
+               $mock = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $mock->expects( $this->any() )->method( 'getFieldInfo' )
+                       ->will( $this->returnValue( $fieldInfo ) );
+
+               $ret = $mock->loadFromSubmission( $data );
+               if ( is_array( $expectState ) ) {
+                       $this->assertTrue( $ret );
+                       $expect = call_user_func( [ get_class( $mock ), '__set_state' ], $expectState );
+                       $this->assertEquals( $expect, $mock );
+               } else {
+                       $this->assertFalse( $ret );
+               }
+       }
+
+       public static function provideLoadFromSubmission() {
+               return [
+                       'No fields' => [
+                               [],
+                               $data = [ 'foo' => 'bar' ],
+                               false
+                       ],
+
+                       'Simple field' => [
+                               [
+                                       'field' => [
+                                               'type' => 'string',
+                                       ],
+                               ],
+                               $data = [ 'field' => 'string!' ],
+                               $data
+                       ],
+                       'Simple field, not supplied' => [
+                               [
+                                       'field' => [
+                                               'type' => 'string',
+                                       ],
+                               ],
+                               [],
+                               false
+                       ],
+                       'Simple field, empty' => [
+                               [
+                                       'field' => [
+                                               'type' => 'string',
+                                       ],
+                               ],
+                               [ 'field' => '' ],
+                               false
+                       ],
+                       'Simple field, optional, not supplied' => [
+                               [
+                                       'field' => [
+                                               'type' => 'string',
+                                               'optional' => true,
+                                       ],
+                               ],
+                               [],
+                               false
+                       ],
+                       'Simple field, optional, empty' => [
+                               [
+                                       'field' => [
+                                               'type' => 'string',
+                                               'optional' => true,
+                                       ],
+                               ],
+                               $data = [ 'field' => '' ],
+                               $data
+                       ],
+
+                       'Checkbox, checked' => [
+                               [
+                                       'check' => [
+                                               'type' => 'checkbox',
+                                       ],
+                               ],
+                               [ 'check' => '' ],
+                               [ 'check' => true ]
+                       ],
+                       'Checkbox, unchecked' => [
+                               [
+                                       'check' => [
+                                               'type' => 'checkbox',
+                                       ],
+                               ],
+                               [],
+                               false
+                       ],
+                       'Checkbox, optional, unchecked' => [
+                               [
+                                       'check' => [
+                                               'type' => 'checkbox',
+                                               'optional' => true,
+                                       ],
+                               ],
+                               [],
+                               [ 'check' => false ]
+                       ],
+
+                       'Button, used' => [
+                               [
+                                       'push' => [
+                                               'type' => 'button',
+                                       ],
+                               ],
+                               [ 'push' => '' ],
+                               [ 'push' => true ]
+                       ],
+                       'Button, unused' => [
+                               [
+                                       'push' => [
+                                               'type' => 'button',
+                                       ],
+                               ],
+                               [],
+                               false
+                       ],
+                       'Button, optional, unused' => [
+                               [
+                                       'push' => [
+                                               'type' => 'button',
+                                               'optional' => true,
+                                       ],
+                               ],
+                               [],
+                               [ 'push' => false ]
+                       ],
+                       'Button, image-style' => [
+                               [
+                                       'push' => [
+                                               'type' => 'button',
+                                       ],
+                               ],
+                               [ 'push_x' => 0, 'push_y' => 0 ],
+                               [ 'push' => true ]
+                       ],
+
+                       'Select' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'select',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               $data = [ 'choose' => 'foo' ],
+                               $data
+                       ],
+                       'Select, invalid choice' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'select',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               $data = [ 'choose' => 'baz' ],
+                               false
+                       ],
+                       'Multiselect (2)' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               $data = [ 'choose' => [ 'foo', 'bar' ] ],
+                               $data
+                       ],
+                       'Multiselect (1)' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               $data = [ 'choose' => [ 'bar' ] ],
+                               $data
+                       ],
+                       'Multiselect, string for some reason' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               [ 'choose' => 'foo' ],
+                               [ 'choose' => [ 'foo' ] ]
+                       ],
+                       'Multiselect, invalid choice' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               [ 'choose' => [ 'foo', 'baz' ] ],
+                               false
+                       ],
+                       'Multiselect, empty' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                       ],
+                               ],
+                               [ 'choose' => [] ],
+                               false
+                       ],
+                       'Multiselect, optional, nothing submitted' => [
+                               [
+                                       'choose' => [
+                                               'type' => 'multiselect',
+                                               'options' => [
+                                                       'foo' => wfMessage( 'mainpage' ),
+                                                       'bar' => wfMessage( 'mainpage' ),
+                                               ],
+                                               'optional' => true,
+                                       ],
+                               ],
+                               [],
+                               [ 'choose' => [] ]
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php b/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php
new file mode 100644 (file)
index 0000000..aafcd09
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ */
+abstract class AuthenticationRequestTestCase extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       abstract protected function getInstance( array $args = [] );
+
+       /**
+        * @dataProvider provideGetFieldInfo
+        */
+       public function testGetFieldInfo( array $args ) {
+               $info = $this->getInstance( $args )->getFieldInfo();
+               $this->assertType( 'array', $info );
+
+               foreach ( $info as $field => $data ) {
+                       $this->assertType( 'array', $data, "Field $field" );
+                       $this->assertArrayHasKey( 'type', $data, "Field $field" );
+                       $this->assertArrayHasKey( 'label', $data, "Field $field" );
+                       $this->assertInstanceOf( 'Message', $data['label'], "Field $field, label" );
+
+                       if ( $data['type'] !== 'null' ) {
+                               $this->assertArrayHasKey( 'help', $data, "Field $field" );
+                               $this->assertInstanceOf( 'Message', $data['help'], "Field $field, help" );
+                       }
+
+                       if ( isset( $data['optional'] ) ) {
+                               $this->assertType( 'bool', $data['optional'], "Field $field, optional" );
+                       }
+                       if ( isset( $data['image'] ) ) {
+                               $this->assertType( 'string', $data['image'], "Field $field, image" );
+                       }
+
+                       switch ( $data['type'] ) {
+                               case 'string':
+                               case 'password':
+                               case 'hidden':
+                                       break;
+                               case 'select':
+                               case 'multiselect':
+                                       $this->assertArrayHasKey( 'options', $data, "Field $field" );
+                                       $this->assertType( 'array', $data['options'], "Field $field, options" );
+                                       foreach ( $data['options'] as $val => $msg ) {
+                                               $this->assertInstanceOf( 'Message', $msg, "Field $field, option $val" );
+                                       }
+                                       break;
+                               case 'checkbox':
+                                       break;
+                               case 'button':
+                                       break;
+                               case 'null':
+                                       break;
+                               default:
+                                       $this->fail( "Field $field, unknown type " . $data['type'] );
+                                       break;
+                       }
+               }
+       }
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [] ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideLoadFromSubmission
+        * @param array $args
+        * @param array $data
+        * @param array|bool $expectState
+        */
+       public function testLoadFromSubmission( array $args, array $data, $expectState ) {
+               $instance = $this->getInstance( $args );
+               $ret = $instance->loadFromSubmission( $data );
+               if ( is_array( $expectState ) ) {
+                       $this->assertTrue( $ret );
+                       $expect = call_user_func( [ get_class( $instance ), '__set_state' ], $expectState );
+                       $this->assertEquals( $expect, $instance );
+               } else {
+                       $this->assertFalse( $ret );
+               }
+       }
+
+       abstract public function provideLoadFromSubmission();
+}
diff --git a/tests/phpunit/includes/auth/AuthenticationResponseTest.php b/tests/phpunit/includes/auth/AuthenticationResponseTest.php
new file mode 100644 (file)
index 0000000..58ff8b6
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthenticationResponse
+ */
+class AuthenticationResponseTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * @dataProvider provideConstructors
+        * @param string $constructor
+        * @param array $args
+        * @param array|Exception $expect
+        */
+       public function testConstructors( $constructor, $args, $expect ) {
+               if ( is_array( $expect ) ) {
+                       $res = new AuthenticationResponse();
+                       foreach ( $expect as $field => $value ) {
+                               $res->$field = $value;
+                       }
+                       $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                       $this->assertEquals( $res, $ret );
+               } else {
+                       try {
+                               call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \Exception $ex ) {
+                               $this->assertEquals( $expect, $ex );
+                       }
+               }
+       }
+
+       public function provideConstructors() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $msg = new \Message( 'mainpage' );
+
+               return [
+                       [ 'newPass', [], [
+                               'status' => AuthenticationResponse::PASS,
+                       ] ],
+                       [ 'newPass', [ 'name' ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+                       [ 'newPass', [ 'name', null ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+
+                       [ 'newFail', [ $msg ], [
+                               'status' => AuthenticationResponse::FAIL,
+                               'message' => $msg,
+                       ] ],
+
+                       [ 'newRestart', [ $msg ], [
+                               'status' => AuthenticationResponse::RESTART,
+                               'message' => $msg,
+                       ] ],
+
+                       [ 'newAbstain', [], [
+                               'status' => AuthenticationResponse::ABSTAIN,
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                       ] ],
+                       [ 'newUI', [ [], $msg ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+
+                       [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
+                               'status' => AuthenticationResponse::REDIRECT,
+                               'neededRequests' => [ $req ],
+                               'redirectTarget' => 'http://example.org/redir',
+                       ] ],
+                       [
+                               'newRedirect',
+                               [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
+                               [
+                                       'status' => AuthenticationResponse::REDIRECT,
+                                       'neededRequests' => [ $req ],
+                                       'redirectTarget' => 'http://example.org/redir',
+                                       'redirectApiData' => [ 'foo' => 'bar' ],
+                               ]
+                       ],
+                       [ 'newRedirect', [ [], 'http://example.org/redir' ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php b/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..3bc077c
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ButtonAuthenticationRequest
+ */
+class ButtonAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               $data = array_intersect_key( $args, [ 'name' => 1, 'label' => 1, 'help' => 1 ] );
+               return ButtonAuthenticationRequest::__set_state( $data );
+       }
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ] ]
+               ];
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ],
+                               [],
+                               false
+                       ],
+                       'Button present' => [
+                               [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ],
+                               [ 'foo' => 'Foobar' ],
+                               [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz', 'foo' => true ]
+                       ],
+               ];
+       }
+
+       public function testGetUniqueId() {
+               $req = new ButtonAuthenticationRequest( 'foo', wfMessage( 'bar' ), wfMessage( 'baz' ) );
+               $this->assertSame(
+                       'MediaWiki\\Auth\\ButtonAuthenticationRequest:foo', $req->getUniqueId()
+               );
+       }
+
+       public function testGetRequestByName() {
+               $reqs = [];
+               $reqs['testOne'] = new ButtonAuthenticationRequest(
+                       'foo', wfMessage( 'msg' ), wfMessage( 'help' )
+               );
+               $reqs[] = new ButtonAuthenticationRequest( 'bar', wfMessage( 'msg1' ), wfMessage( 'help1' ) );
+               $reqs[] = new ButtonAuthenticationRequest( 'bar', wfMessage( 'msg2' ), wfMessage( 'help2' ) );
+               $reqs['testSub'] = $this->getMockBuilder( ButtonAuthenticationRequest::class )
+                       ->setConstructorArgs( [ 'subclass', wfMessage( 'msg3' ), wfMessage( 'help3' ) ] )
+                       ->getMock();
+
+               $this->assertNull( ButtonAuthenticationRequest::getRequestByName( $reqs, 'missing' ) );
+               $this->assertSame(
+                       $reqs['testOne'], ButtonAuthenticationRequest::getRequestByName( $reqs, 'foo' )
+               );
+               $this->assertNull( ButtonAuthenticationRequest::getRequestByName( $reqs, 'bar' ) );
+               $this->assertSame(
+                       $reqs['testSub'], ButtonAuthenticationRequest::getRequestByName( $reqs, 'subclass' )
+               );
+       }
+}
diff --git a/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..f2341bc
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider
+ */
+class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testConstructor() {
+               $provider = new CheckBlocksSecondaryAuthenticationProvider();
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $config = new \HashConfig( [
+                       'BlockDisablesLogin' => false
+               ] );
+               $provider->setConfig( $config );
+               $this->assertSame( false, $providerPriv->blockDisablesLogin );
+
+               $provider = new CheckBlocksSecondaryAuthenticationProvider(
+                       [ 'blockDisablesLogin' => true ]
+               );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $config = new \HashConfig( [
+                       'BlockDisablesLogin' => false
+               ] );
+               $provider->setConfig( $config );
+               $this->assertSame( true, $providerPriv->blockDisablesLogin );
+       }
+
+       public function testBasics() {
+               $provider = new CheckBlocksSecondaryAuthenticationProvider();
+               $user = \User::newFromName( 'UTSysop' );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginSecondaryAccountCreation( $user, $user, [] )
+               );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        */
+       public function testGetAuthenticationRequests( $action, $response ) {
+               $provider = new CheckBlocksSecondaryAuthenticationProvider();
+
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       private function getBlockedUser() {
+               $user = \User::newFromName( 'UTBlockee' );
+               if ( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
+                       $user->saveSettings();
+               }
+               $oldBlock = \Block::newFromTarget( 'UTBlockee' );
+               if ( $oldBlock ) {
+                       // An old block will prevent our new one from saving.
+                       $oldBlock->delete();
+               }
+               $blockOptions = [
+                       'address' => 'UTBlockee',
+                       'user' => $user->getID(),
+                       'reason' => __METHOD__,
+                       'expiry' => time() + 100500,
+                       'createAccount' => true,
+               ];
+               $block = new \Block( $blockOptions );
+               $block->insert();
+               return $user;
+       }
+
+       public function testBeginSecondaryAuthentication() {
+               $unblockedUser = \User::newFromName( 'UTSysop' );
+               $blockedUser = $this->getBlockedUser();
+
+               $provider = new CheckBlocksSecondaryAuthenticationProvider(
+                       [ 'blockDisablesLogin' => false ]
+               );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginSecondaryAuthentication( $unblockedUser, [] )
+               );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginSecondaryAuthentication( $blockedUser, [] )
+               );
+
+               $provider = new CheckBlocksSecondaryAuthenticationProvider(
+                       [ 'blockDisablesLogin' => true ]
+               );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass(),
+                       $provider->beginSecondaryAuthentication( $unblockedUser, [] )
+               );
+               $ret = $provider->beginSecondaryAuthentication( $blockedUser, [] );
+               $this->assertEquals( AuthenticationResponse::FAIL, $ret->status );
+       }
+
+       public function testTestUserForCreation() {
+               $provider = new CheckBlocksSecondaryAuthenticationProvider(
+                       [ 'blockDisablesLogin' => false ]
+               );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig() );
+               $provider->setManager( AuthManager::singleton() );
+
+               $unblockedUser = \User::newFromName( 'UTSysop' );
+               $blockedUser = $this->getBlockedUser();
+
+               $user = \User::newFromName( 'RandomUser' );
+
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $unblockedUser, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $unblockedUser, false )
+               );
+
+               $status = $provider->testUserForCreation( $blockedUser, AuthManager::AUTOCREATE_SOURCE_SESSION );
+               $this->assertInstanceOf( 'StatusValue', $status );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+
+               $status = $provider->testUserForCreation( $blockedUser, false );
+               $this->assertInstanceOf( 'StatusValue', $status );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+       }
+
+       public function testRangeBlock() {
+               $blockOptions = [
+                       'address' => '127.0.0.0/24',
+                       'reason' => __METHOD__,
+                       'expiry' => time() + 100500,
+                       'createAccount' => true,
+               ];
+               $block = new \Block( $blockOptions );
+               $block->insert();
+               $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
+
+               $user = \User::newFromName( 'UTNormalUser' );
+               if ( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       \TestUser::setPasswordForUser( $user, 'UTNormalUserPassword' );
+                       $user->saveSettings();
+               }
+               $this->setMwGlobals( [ 'wgUser' => $user ] );
+               $newuser = \User::newFromName( 'RandomUser' );
+
+               $provider = new CheckBlocksSecondaryAuthenticationProvider(
+                       [ 'blockDisablesLogin' => true ]
+               );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig() );
+               $provider->setManager( AuthManager::singleton() );
+
+               $ret = $provider->beginSecondaryAuthentication( $user, [] );
+               $this->assertEquals( AuthenticationResponse::FAIL, $ret->status );
+
+               $status = $provider->testUserForCreation( $newuser, AuthManager::AUTOCREATE_SOURCE_SESSION );
+               $this->assertInstanceOf( 'StatusValue', $status );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+
+               $status = $provider->testUserForCreation( $newuser, false );
+               $this->assertInstanceOf( 'StatusValue', $status );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+       }
+}
diff --git a/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php b/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..f208cc4
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use InvalidArgumentException;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ConfirmLinkAuthenticationRequest
+ */
+class ConfirmLinkAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new ConfirmLinkAuthenticationRequest( $this->getLinkRequests() );
+       }
+
+       /**
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage $linkRequests must not be empty
+        */
+       public function testConstructorException() {
+               new ConfirmLinkAuthenticationRequest( [] );
+       }
+
+       /**
+        * Get requests for testing
+        * @return AuthenticationRequest[]
+        */
+       private function getLinkRequests() {
+               $reqs = [];
+
+               $mb = $this->getMockBuilder( AuthenticationRequest::class )
+                       ->setMethods( [ 'getUniqueId' ] );
+               for ( $i = 1; $i <= 3; $i++ ) {
+                       $req = $mb->getMockForAbstractClass();
+                       $req->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( "Request$i" ) );
+                       $reqs[$req->getUniqueId()] = $req;
+               }
+
+               return $reqs;
+       }
+
+       public function provideLoadFromSubmission() {
+               $reqs = $this->getLinkRequests();
+
+               return [
+                       'Empty request' => [
+                               [],
+                               [],
+                               [ 'linkRequests' => $reqs ],
+                       ],
+                       'Some confirmed' => [
+                               [],
+                               [ 'confirmedLinkIDs' => [ 'Request1', 'Request3' ] ],
+                               [ 'confirmedLinkIDs' => [ 'Request1', 'Request3' ], 'linkRequests' => $reqs ],
+                       ],
+               ];
+       }
+
+       public function testGetUniqueId() {
+               $req = new ConfirmLinkAuthenticationRequest( $this->getLinkRequests() );
+               $this->assertSame(
+                       get_class( $req ) . ':Request1|Request2|Request3',
+                       $req->getUniqueId()
+               );
+       }
+}
diff --git a/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..09d046c
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
+ */
+class ConfirmLinkSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        */
+       public function testGetAuthenticationRequests( $action, $response ) {
+               $provider = new ConfirmLinkSecondaryAuthenticationProvider();
+
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       public function testBeginSecondaryAuthentication() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::authnState' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+               $this->assertSame( $obj, $mock->beginSecondaryAuthentication( $user, [] ) );
+       }
+
+       public function testContinueSecondaryAuthentication() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = [ new \stdClass ];
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->identicalTo( 'AuthManager::authnState' ),
+                               $this->identicalTo( $reqs )
+                       )
+                       ->will( $this->returnValue( $obj ) );
+
+               $this->assertSame( $obj, $mock->continueSecondaryAuthentication( $user, $reqs ) );
+       }
+
+       public function testBeginSecondaryAccountCreation() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::accountCreationState' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+               $this->assertSame( $obj, $mock->beginSecondaryAccountCreation( $user, $user, [] ) );
+       }
+
+       public function testContinueSecondaryAccountCreation() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = [ new \stdClass ];
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->identicalTo( 'AuthManager::accountCreationState' ),
+                               $this->identicalTo( $reqs )
+                       )
+                       ->will( $this->returnValue( $obj ) );
+
+               $this->assertSame( $obj, $mock->continueSecondaryAccountCreation( $user, $user, $reqs ) );
+       }
+
+       /**
+        * Get requests for testing
+        * @return AuthenticationRequest[]
+        */
+       private function getLinkRequests() {
+               $reqs = [];
+
+               $mb = $this->getMockBuilder( AuthenticationRequest::class )
+                       ->setMethods( [ 'getUniqueId' ] );
+               for ( $i = 1; $i <= 3; $i++ ) {
+                       $req = $mb->getMockForAbstractClass();
+                       $req->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( "Request$i" ) );
+                       $req->id = $i - 1;
+                       $reqs[$req->getUniqueId()] = $req;
+               }
+
+               return $reqs;
+       }
+
+       public function testBeginLinkAttempt() {
+               $user = \User::newFromName( 'UTSysop' );
+               $provider = \TestingAccessWrapper::newFromObject(
+                       new ConfirmLinkSecondaryAuthenticationProvider
+               );
+               $request = new \FauxRequest();
+               $manager = new AuthManager( $request, \RequestContext::getMain()->getConfig() );
+               $provider->setManager( $manager );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginLinkAttempt( $user, 'state' )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [],
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginLinkAttempt( $user, 'state' )
+               );
+
+               $reqs = $this->getLinkRequests();
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs
+               ] );
+               $res = $provider->beginLinkAttempt( $user, 'state' );
+               $this->assertInstanceOf( AuthenticationResponse::class, $res );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertSame( 'authprovider-confirmlink-message', $res->message->getKey() );
+               $this->assertCount( 1, $res->neededRequests );
+               $req = $res->neededRequests[0];
+               $this->assertInstanceOf( ConfirmLinkAuthenticationRequest::class, $req );
+               $this->assertEquals( $reqs, \TestingAccessWrapper::newFromObject( $req )->linkRequests );
+       }
+
+       public function testContinueLinkAttempt() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = $this->getLinkRequests();
+
+               $done = [ false, false, false ];
+
+               // First, test the pass-through for not containing the ConfirmLinkAuthenticationRequest
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'state' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $this->assertSame(
+                       $obj,
+                       \TestingAccessWrapper::newFromObject( $mock )->continueLinkAttempt( $user, 'state', $reqs )
+               );
+
+               // Now test the actual functioning
+               $provider = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [
+                               'beginLinkAttempt', 'providerAllowsAuthenticationDataChange',
+                               'providerChangeAuthenticationData'
+                       ] )
+                       ->getMock();
+               $provider->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                       ->will( $this->returnCallback( function ( $req ) use ( $reqs ) {
+                               return $req->getUniqueId() === 'Request3'
+                                       ? \StatusValue::newFatal( 'foo' ) : \StatusValue::newGood();
+                       } ) );
+               $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) use ( &$done ) {
+                               $done[$req->id] = true;
+                       } ) );
+               $config = new \HashConfig( [
+                       'AuthManagerConfig' => [
+                               'preauth' => [],
+                               'primaryauth' => [],
+                               'secondaryauth' => [
+                                       [ 'factory' => function () use ( $provider ) {
+                                               return $provider;
+                                       } ],
+                               ],
+                       ],
+               ] );
+               $request = new \FauxRequest();
+               $manager = new AuthManager( $request, $config );
+               $provider->setManager( $manager );
+               $provider = \TestingAccessWrapper::newFromObject( $provider );
+
+               $req = new ConfirmLinkAuthenticationRequest( $reqs );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [],
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass(),
+                       $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+               $this->assertSame( [ false, false, false ], $done );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [ $reqs['Request2'] ],
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ false, true, false ], $done );
+               $done = [ false, false, false ];
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs,
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ true, true, false ], $done );
+               $done = [ false, false, false ];
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs,
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request3' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::UI, $res->status );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf( ButtonAuthenticationRequest::class, $res->neededRequests[0] );
+               $this->assertSame( [ true, false, false ], $done );
+               $done = [ false, false, false ];
+
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $res->neededRequests[0] ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ false, false, false ], $done );
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php b/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..fb0613d
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreateFromLoginAuthenticationRequest
+ */
+class CreateFromLoginAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new CreateFromLoginAuthenticationRequest(
+                       null, []
+               );
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [],
+                               [],
+                               [],
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php b/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..fc1e6f1
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreatedAccountAuthenticationRequest
+ */
+class CreatedAccountAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new CreatedAccountAuthenticationRequest( 42, 'Test' );
+       }
+
+       public function testConstructor() {
+               $ret = new CreatedAccountAuthenticationRequest( 42, 'Test' );
+               $this->assertSame( 42, $ret->id );
+               $this->assertSame( 'Test', $ret->username );
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [],
+                               [],
+                               false
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php b/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..cce1e8c
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreationReasonAuthenticationRequest
+ */
+class CreationReasonAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new CreationReasonAuthenticationRequest();
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [],
+                               [],
+                               false
+                       ],
+                       'Reason given' => [
+                               [],
+                               $data = [ 'reason' => 'Because' ],
+                               $data,
+                       ],
+                       'Reason empty' => [
+                               [],
+                               [ 'reason' => '' ],
+                               false
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..18c46f7
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Psr\Log\LoggerInterface;
+
+class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit_Framework_TestCase {
+       public function testConstructor() {
+               $config = new \HashConfig( [
+                       'EnableEmail' => true,
+                       'EmailAuthentication' => true,
+               ] );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider();
+               $provider->setConfig( $config );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertTrue( $providerPriv->sendConfirmationEmail );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => false,
+               ] );
+               $provider->setConfig( $config );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $this->assertFalse( $providerPriv->sendConfirmationEmail );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param AuthenticationRequest[] $expected
+        */
+       public function testGetAuthenticationRequests( $action, $expected ) {
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $this->assertSame( $expected, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       public function testBeginSecondaryAuthentication() {
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $this->assertEquals( AuthenticationResponse::newAbstain(),
+                       $provider->beginSecondaryAuthentication( \User::newFromName( 'Foo' ), [] ) );
+       }
+
+       public function testBeginSecondaryAccountCreation() {
+               $authManager = new AuthManager( new \FauxRequest(), new \HashConfig() );
+
+               $creator = $this->getMock( 'User' );
+               $userWithoutEmail = $this->getMock( 'User' );
+               $userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
+               $userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
+               $userWithEmailError = $this->getMock( 'User' );
+               $userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+               $userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
+                       ->willReturn( \Status::newFatal( 'fail' ) );
+               $userExpectsConfirmation = $this->getMock( 'User' );
+               $userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+                       ->willReturn( 'foo@bar.baz' );
+               $userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
+                       ->willReturn( \Status::newGood() );
+               $userNotExpectsConfirmation = $this->getMock( 'User' );
+               $userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+                       ->willReturn( 'foo@bar.baz' );
+               $userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => false,
+               ] );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userWithoutEmail, $creator, [] );
+               $provider->beginSecondaryAccountCreation( $userExpectsConfirmation, $creator, [] );
+
+               // test logging of email errors
+               $logger = $this->getMockForAbstractClass( LoggerInterface::class );
+               $logger->expects( $this->once() )->method( 'warning' );
+               $provider->setLogger( $logger );
+               $provider->beginSecondaryAccountCreation( $userWithEmailError, $creator, [] );
+
+               // test disable flag used by other providers
+               $authManager->setAuthenticationSessionData( 'no-email', true );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+
+       }
+}
diff --git a/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php b/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..edee6fc
--- /dev/null
@@ -0,0 +1,420 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\LegacyHookPreAuthenticationProvider
+ */
+class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * Get an instance of the provider
+        * @return LegacyHookPreAuthenticationProvider
+        */
+       protected function getProvider() {
+               $request = $this->getMock( 'FauxRequest', [ 'getIP' ] );
+               $request->expects( $this->any() )->method( 'getIP' )->will( $this->returnValue( '127.0.0.42' ) );
+
+               $manager = new AuthManager(
+                       $request, \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+               );
+
+               $provider = new LegacyHookPreAuthenticationProvider();
+               $provider->setManager( $manager );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig( [
+                       'PasswordAttemptThrottle' => [ 'count' => 23, 'seconds' => 42 ],
+               ] ) );
+               return $provider;
+       }
+
+       /**
+        * Sets a mock on a hook
+        * @param string $hook
+        * @param object $expect From $this->once(), $this->never(), etc.
+        * @return object $mock->expects( $expect )->method( ... ).
+        */
+       protected function hook( $hook, $expect ) {
+               $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                       $hook => [ $mock ],
+               ] );
+               return $mock->expects( $expect )->method( "on$hook" );
+       }
+
+       /**
+        * Unsets a hook
+        * @param string $hook
+        */
+       protected function unhook( $hook ) {
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                       $hook => [],
+               ] );
+       }
+
+       // Stubs for hooks taking reference parameters
+       public function onLoginUserMigrated( $user, &$msg ) {
+       }
+       public function onAbortLogin( $user, $password, &$abort, &$msg ) {
+       }
+       public function onAbortNewAccount( $user, &$abortError, &$abortStatus ) {
+       }
+       public function onAbortAutoAccount( $user, &$abortError ) {
+       }
+
+       /**
+        * @dataProvider provideTestForAuthentication
+        * @param string|null $username
+        * @param string|null $password
+        * @param string|null $msgForLoginUserMigrated
+        * @param int|null $abortForAbortLogin
+        * @param string|null $msgForAbortLogin
+        * @param string|null $failMsg
+        * @param array $failParams
+        */
+       public function testTestForAuthentication(
+               $username, $password,
+               $msgForLoginUserMigrated, $abortForAbortLogin, $msgForAbortLogin,
+               $failMsg, $failParams = []
+       ) {
+               $reqs = [];
+               if ( $username === null ) {
+                       $this->hook( 'LoginUserMigrated', $this->never() );
+                       $this->hook( 'AbortLogin', $this->never() );
+               } else {
+                       if ( $password === null ) {
+                               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+                       } else {
+                               $req = new PasswordAuthenticationRequest();
+                               $req->action = AuthManager::ACTION_LOGIN;
+                               $req->password = $password;
+                       }
+                       $req->username = $username;
+                       $reqs[get_class( $req )] = $req;
+
+                       $h = $this->hook( 'LoginUserMigrated', $this->once() );
+                       if ( $msgForLoginUserMigrated !== null ) {
+                               $h->will( $this->returnCallback(
+                                       function ( $user, &$msg ) use ( $username, $msgForLoginUserMigrated ) {
+                                               $this->assertInstanceOf( 'User', $user );
+                                               $this->assertSame( $username, $user->getName() );
+                                               $msg = $msgForLoginUserMigrated;
+                                               return false;
+                                       }
+                               ) );
+                               $this->hook( 'AbortLogin', $this->never() );
+                       } else {
+                               $h->will( $this->returnCallback(
+                                       function ( $user, &$msg ) use ( $username ) {
+                                               $this->assertInstanceOf( 'User', $user );
+                                               $this->assertSame( $username, $user->getName() );
+                                               return true;
+                                       }
+                               ) );
+                               $h2 = $this->hook( 'AbortLogin', $this->once() );
+                               if ( $abortForAbortLogin !== null ) {
+                                       $h2->will( $this->returnCallback(
+                                               function ( $user, $pass, &$abort, &$msg )
+                                                       use ( $username, $password, $abortForAbortLogin, $msgForAbortLogin )
+                                               {
+                                                       $this->assertInstanceOf( 'User', $user );
+                                                       $this->assertSame( $username, $user->getName() );
+                                                       if ( $password !== null ) {
+                                                               $this->assertSame( $password, $pass );
+                                                       } else {
+                                                               $this->assertInternalType( 'string', $pass );
+                                                       }
+                                                       $abort = $abortForAbortLogin;
+                                                       $msg = $msgForAbortLogin;
+                                                       return false;
+                                               }
+                                       ) );
+                               } else {
+                                       $h2->will( $this->returnCallback(
+                                               function ( $user, $pass, &$abort, &$msg ) use ( $username, $password ) {
+                                                       $this->assertInstanceOf( 'User', $user );
+                                                       $this->assertSame( $username, $user->getName() );
+                                                       if ( $password !== null ) {
+                                                               $this->assertSame( $password, $pass );
+                                                       } else {
+                                                               $this->assertInternalType( 'string', $pass );
+                                                       }
+                                                       return true;
+                                               }
+                                       ) );
+                               }
+                       }
+               }
+               unset( $h, $h2 );
+
+               $status = $this->getProvider()->testForAuthentication( $reqs );
+
+               $this->unhook( 'LoginUserMigrated' );
+               $this->unhook( 'AbortLogin' );
+
+               if ( $failMsg === null ) {
+                       $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+               } else {
+                       $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' );
+                       $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+                       $errors = $status->getErrors();
+                       $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' );
+                       $this->assertEquals( $failParams, $errors[0]['params'], 'should fail (params)' );
+               }
+       }
+
+       public static function provideTestForAuthentication() {
+               return [
+                       'No valid requests' => [
+                               null, null, null, null, null, null
+                       ],
+                       'No hook errors' => [
+                               'User', 'PaSsWoRd', null, null, null, null
+                       ],
+                       'No hook errors, no password' => [
+                               'User', null, null, null, null, null
+                       ],
+                       'LoginUserMigrated no message' => [
+                               'User', 'PaSsWoRd', false, null, null, 'login-migrated-generic'
+                       ],
+                       'LoginUserMigrated with message' => [
+                               'User', 'PaSsWoRd', 'LUM-abort', null, null, 'LUM-abort'
+                       ],
+                       'LoginUserMigrated with message and params' => [
+                               'User', 'PaSsWoRd', [ 'LUM-abort', 'foo' ], null, null, 'LUM-abort', [ 'foo' ]
+                       ],
+                       'AbortLogin, SUCCESS' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::SUCCESS, null, null
+                       ],
+                       'AbortLogin, NEED_TOKEN, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::NEED_TOKEN, null, 'nocookiesforlogin'
+                       ],
+                       'AbortLogin, NEED_TOKEN, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::NEED_TOKEN, 'needtoken', 'needtoken'
+                       ],
+                       'AbortLogin, WRONG_TOKEN, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_TOKEN, null, 'sessionfailure'
+                       ],
+                       'AbortLogin, WRONG_TOKEN, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_TOKEN, 'wrongtoken', 'wrongtoken'
+                       ],
+                       'AbortLogin, ILLEGAL, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::ILLEGAL, null, 'noname'
+                       ],
+                       'AbortLogin, ILLEGAL, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::ILLEGAL, 'badname', 'badname'
+                       ],
+                       'AbortLogin, NO_NAME, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::NO_NAME, null, 'noname'
+                       ],
+                       'AbortLogin, NO_NAME, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::NO_NAME, 'badname', 'badname'
+                       ],
+                       'AbortLogin, WRONG_PASS, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_PASS, null, 'wrongpassword'
+                       ],
+                       'AbortLogin, WRONG_PASS, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_PASS, 'badpass', 'badpass'
+                       ],
+                       'AbortLogin, WRONG_PLUGIN_PASS, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_PLUGIN_PASS, null, 'wrongpassword'
+                       ],
+                       'AbortLogin, WRONG_PLUGIN_PASS, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::WRONG_PLUGIN_PASS, 'badpass', 'badpass'
+                       ],
+                       'AbortLogin, NOT_EXISTS, no message' => [
+                               "User'", 'A', null, \LoginForm::NOT_EXISTS, null, 'nosuchusershort', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, NOT_EXISTS, with message' => [
+                               "User'", 'A', null, \LoginForm::NOT_EXISTS, 'badname', 'badname', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, EMPTY_PASS, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::EMPTY_PASS, null, 'wrongpasswordempty'
+                       ],
+                       'AbortLogin, EMPTY_PASS, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::EMPTY_PASS, 'badpass', 'badpass'
+                       ],
+                       'AbortLogin, RESET_PASS, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::RESET_PASS, null, 'resetpass_announce'
+                       ],
+                       'AbortLogin, RESET_PASS, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::RESET_PASS, 'resetpass', 'resetpass'
+                       ],
+                       'AbortLogin, THROTTLED, no message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::THROTTLED, null, 'login-throttled',
+                               [ \Message::durationParam( 42 ) ]
+                       ],
+                       'AbortLogin, THROTTLED, with message' => [
+                               'User', 'PaSsWoRd', null, \LoginForm::THROTTLED, 't', 't',
+                               [ \Message::durationParam( 42 ) ]
+                       ],
+                       'AbortLogin, USER_BLOCKED, no message' => [
+                               "User'", 'P', null, \LoginForm::USER_BLOCKED, null, 'login-userblocked', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, USER_BLOCKED, with message' => [
+                               "User'", 'P', null, \LoginForm::USER_BLOCKED, 'blocked', 'blocked', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, ABORTED, no message' => [
+                               "User'", 'P', null, \LoginForm::ABORTED, null, 'login-abort-generic', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, ABORTED, with message' => [
+                               "User'", 'P', null, \LoginForm::ABORTED, 'aborted', 'aborted', [ 'User&#39;' ]
+                       ],
+                       'AbortLogin, USER_MIGRATED, no message' => [
+                               'User', 'P', null, \LoginForm::USER_MIGRATED, null, 'login-migrated-generic'
+                       ],
+                       'AbortLogin, USER_MIGRATED, with message' => [
+                               'User', 'P', null, \LoginForm::USER_MIGRATED, 'migrated', 'migrated'
+                       ],
+                       'AbortLogin, USER_MIGRATED, with message and params' => [
+                               'User', 'P', null, \LoginForm::USER_MIGRATED, [ 'migrated', 'foo' ],
+                               'migrated', [ 'foo' ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestForAccountCreation
+        * @param string $msg
+        * @param Status|null $status
+        * @param StatusValue Result
+        */
+       public function testTestForAccountCreation( $msg, $status, $result ) {
+               $this->hook( 'AbortNewAccount', $this->once() )
+                       ->will( $this->returnCallback( function ( $user, &$error, &$abortStatus )
+                               use ( $msg, $status )
+                       {
+                               $this->assertInstanceOf( 'User', $user );
+                               $this->assertSame( 'User', $user->getName() );
+                               $error = $msg;
+                               $abortStatus = $status;
+                               return $error === null && $status === null;
+                       } ) );
+
+               $user = \User::newFromName( 'User' );
+               $creator = \User::newFromName( 'UTSysop' );
+               $ret = $this->getProvider()->testForAccountCreation( $user, $creator, [] );
+
+               $this->unhook( 'AbortNewAccount' );
+
+               $this->assertEquals( $result, $ret );
+       }
+
+       public static function provideTestForAccountCreation() {
+               return [
+                       'No hook errors' => [
+                               null, null, \StatusValue::newGood()
+                       ],
+                       'AbortNewAccount, old style' => [
+                               'foobar', null, \StatusValue::newFatal(
+                                       \Message::newFromKey( 'createaccount-hook-aborted' )->rawParams( 'foobar' )
+                               )
+                       ],
+                       'AbortNewAccount, new style' => [
+                               'foobar',
+                               \Status::newFatal( 'aborted!', 'param' ),
+                               \StatusValue::newFatal( 'aborted!', 'param' )
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestUserForCreation
+        * @param string|null $error
+        * @param string|null $failMsg
+        */
+       public function testTestUserForCreation( $error, $failMsg ) {
+               $this->hook( 'AbortNewAccount', $this->never() );
+               $this->hook( 'AbortAutoAccount', $this->once() )
+                       ->will( $this->returnCallback( function ( $user, &$abortError ) use ( $error ) {
+                               $this->assertInstanceOf( 'User', $user );
+                               $this->assertSame( 'UTSysop', $user->getName() );
+                               $abortError = $error;
+                               return $error === null;
+                       } ) );
+
+               $status = $this->getProvider()->testUserForCreation(
+                       \User::newFromName( 'UTSysop' ), AuthManager::AUTOCREATE_SOURCE_SESSION
+               );
+
+               $this->unhook( 'AbortNewAccount' );
+               $this->unhook( 'AbortAutoAccount' );
+
+               if ( $failMsg === null ) {
+                       $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+               } else {
+                       $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' );
+                       $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+                       $errors = $status->getErrors();
+                       $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' );
+               }
+
+               $this->hook( 'AbortAutoAccount', $this->never() );
+               $this->hook( 'AbortNewAccount', $this->once() )
+                       ->will( $this->returnCallback(
+                               function ( $user, &$abortError, &$abortStatus ) use ( $error ) {
+                                       $this->assertInstanceOf( 'User', $user );
+                                       $this->assertSame( 'UTSysop', $user->getName() );
+                                       $abortError = $error;
+                                       return $error === null;
+                               }
+                       ) );
+               $status = $this->getProvider()->testUserForCreation( \User::newFromName( 'UTSysop' ), false );
+               $this->unhook( 'AbortNewAccount' );
+               $this->unhook( 'AbortAutoAccount' );
+               if ( $failMsg === null ) {
+                       $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+               } else {
+                       $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' );
+                       $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+                       $errors = $status->getErrors();
+                       $msg = $errors[0]['message'];
+                       $this->assertInstanceOf( \Message::class, $msg );
+                       $this->assertEquals(
+                               'createaccount-hook-aborted', $msg->getKey(), 'should fail (message)'
+                       );
+               }
+
+               if ( $error !== false ) {
+                       $this->hook( 'AbortAutoAccount', $this->never() );
+                       $this->hook( 'AbortNewAccount', $this->once() )
+                               ->will( $this->returnCallback(
+                                       function ( $user, &$abortError, &$abortStatus ) use ( $error ) {
+                                               $this->assertInstanceOf( 'User', $user );
+                                               $this->assertSame( 'UTSysop', $user->getName() );
+                                               $abortStatus = $error ? \Status::newFatal( $error ) : \Status::newGood();
+                                               return $error === null;
+                                       }
+                       ) );
+                       $status = $this->getProvider()->testUserForCreation( \User::newFromName( 'UTSysop' ), false );
+                       $this->unhook( 'AbortNewAccount' );
+                       $this->unhook( 'AbortAutoAccount' );
+                       if ( $failMsg === null ) {
+                               $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+                       } else {
+                               $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' );
+                               $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+                               $errors = $status->getErrors();
+                               $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' );
+                       }
+               }
+       }
+
+       public static function provideTestUserForCreation() {
+               return [
+                       'Success' => [ null, null ],
+                       'Fail, no message' => [ false, 'login-abort-generic' ],
+                       'Fail, with message' => [ 'fail', 'fail' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..fa68dee
--- /dev/null
@@ -0,0 +1,666 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider
+ */
+class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+
+       private $manager = null;
+       private $config = null;
+       private $validity = null;
+
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * Get an instance of the provider
+        *
+        * $provider->checkPasswordValidity is mocked to return $this->validity,
+        * because we don't need to test that here.
+        *
+        * @param bool $loginOnly
+        * @return LocalPasswordPrimaryAuthenticationProvider
+        */
+       protected function getProvider( $loginOnly = false ) {
+               if ( !$this->config ) {
+                       $this->config = new \HashConfig();
+               }
+               $config = new \MultiConfig( [
+                       $this->config,
+                       \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+               ] );
+
+               if ( !$this->manager ) {
+                       $this->manager = new AuthManager( new \FauxRequest(), $config );
+               }
+               $this->validity = \Status::newGood();
+
+               $provider = $this->getMock(
+                       LocalPasswordPrimaryAuthenticationProvider::class,
+                       [ 'checkPasswordValidity' ],
+                       [ [ 'loginOnly' => $loginOnly ] ]
+               );
+               $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
+                       ->will( $this->returnCallback( function () {
+                               return $this->validity;
+                       } ) );
+               $provider->setConfig( $config );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setManager( $this->manager );
+
+               return $provider;
+       }
+
+       public function testBasics() {
+               $provider = new LocalPasswordPrimaryAuthenticationProvider();
+
+               $this->assertSame(
+                       PrimaryAuthenticationProvider::TYPE_CREATE,
+                       $provider->accountCreationType()
+               );
+
+               $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
+               $this->assertTrue( $provider->testUserExists( 'uTSysop' ) );
+               $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+               $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
+
+               $provider = new LocalPasswordPrimaryAuthenticationProvider( [ 'loginOnly' => true ] );
+
+               $this->assertSame(
+                       PrimaryAuthenticationProvider::TYPE_NONE,
+                       $provider->accountCreationType()
+               );
+
+               $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
+               $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+
+               $req = new PasswordAuthenticationRequest;
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = '<invalid>';
+               $provider->providerChangeAuthenticationData( $req );
+       }
+
+       public function testTestUserCanAuthenticate() {
+               $dbw = wfGetDB( DB_MASTER );
+               $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => 'UTSysop' ] );
+               $cb = new \ScopedCallback( function () use ( $dbw, $oldHash ) {
+                       $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => 'UTSysop' ] );
+               } );
+               $id = \User::idFromName( 'UTSysop' );
+
+               $provider = $this->getProvider();
+
+               $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
+
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
+
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'uTSysop' ) );
+
+               $dbw->update(
+                       'user',
+                       [ 'user_password' => \PasswordFactory::newInvalidPassword()->toString() ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+
+               // Really old format
+               $dbw->update(
+                       'user',
+                       [ 'user_password' => '0123456789abcdef0123456789abcdef' ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+       }
+
+       public function testSetPasswordResetFlag() {
+               // Set instance vars
+               $this->getProvider();
+
+               /// @todo: Because we're currently using User, which uses the global config...
+               $this->setMwGlobals( [ 'wgPasswordExpireGrace' => 100 ] );
+
+               $this->config->set( 'PasswordExpireGrace', 100 );
+               $this->config->set( 'InvalidPasswordReset', true );
+
+               $provider = new LocalPasswordPrimaryAuthenticationProvider();
+               $provider->setConfig( $this->config );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setManager( $this->manager );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       '*',
+                       [ 'user_name' => 'UTSysop' ],
+                       __METHOD__
+               );
+
+               $this->manager->removeAuthenticationSessionData( null );
+               $row->user_password_expires = wfTimestamp( TS_MW, time() + 200 );
+               $providerPriv->setPasswordResetFlag( 'UTSysop', \Status::newGood(), $row );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+               $this->manager->removeAuthenticationSessionData( null );
+               $row->user_password_expires = wfTimestamp( TS_MW, time() - 200 );
+               $providerPriv->setPasswordResetFlag( 'UTSysop', \Status::newGood(), $row );
+               $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+               $this->assertNotNull( $ret );
+               $this->assertSame( 'resetpass-expired', $ret->msg->getKey() );
+               $this->assertTrue( $ret->hard );
+
+               $this->manager->removeAuthenticationSessionData( null );
+               $row->user_password_expires = wfTimestamp( TS_MW, time() - 1 );
+               $providerPriv->setPasswordResetFlag( 'UTSysop', \Status::newGood(), $row );
+               $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+               $this->assertNotNull( $ret );
+               $this->assertSame( 'resetpass-expired-soft', $ret->msg->getKey() );
+               $this->assertFalse( $ret->hard );
+
+               $this->manager->removeAuthenticationSessionData( null );
+               $row->user_password_expires = null;
+               $status = \Status::newGood();
+               $status->error( 'testing' );
+               $providerPriv->setPasswordResetFlag( 'UTSysop', $status, $row );
+               $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+               $this->assertNotNull( $ret );
+               $this->assertSame( 'resetpass-validity-soft', $ret->msg->getKey() );
+               $this->assertFalse( $ret->hard );
+       }
+
+       public function testAuthentication() {
+               $dbw = wfGetDB( DB_MASTER );
+               $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => 'UTSysop' ] );
+               $cb = new \ScopedCallback( function () use ( $dbw, $oldHash ) {
+                       $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => 'UTSysop' ] );
+               } );
+               $id = \User::idFromName( 'UTSysop' );
+
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_LOGIN;
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $provider = $this->getProvider();
+
+               // General failures
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = '<invalid>';
+               $req->password = 'WhoCares';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = 'DoesNotExist';
+               $req->password = 'DoesNotExist';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               // Validation failure
+               $req->username = 'UTSysop';
+               $req->password = 'UTSysopPassword';
+               $this->validity = \Status::newFatal( 'arbitrary-failure' );
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'arbitrary-failure',
+                       $ret->message->getKey()
+               );
+
+               // Successful auth
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->validity = \Status::newGood();
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+               // Successful auth after normalizing name
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->validity = \Status::newGood();
+               $req->username = 'uTSysop';
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $req->username = 'UTSysop';
+
+               // Successful auth with reset
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->validity->error( 'arbitrary-warning' );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+               $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+               // Wrong password
+               $this->validity = \Status::newGood();
+               $req->password = 'Wrong';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
+               );
+
+               // Correct handling of legacy encodings
+               $password = ':B:salt:' . md5( 'salt-' . md5( "\xe1\xe9\xed\xf3\xfa" ) );
+               $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
+               $req->password = 'áéíóú';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
+               );
+
+               $this->config->set( 'LegacyEncoding', true );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->password = 'áéíóú Wrong';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
+               );
+
+               // Correct handling of really old password hashes
+               $this->config->set( 'PasswordSalt', false );
+               $password = md5( 'FooBar' );
+               $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
+               $req->password = 'FooBar';
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $this->config->set( 'PasswordSalt', true );
+               $password = md5( "$id-" . md5( 'FooBar' ) );
+               $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
+               $req->password = 'FooBar';
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+       }
+
+       /**
+        * @dataProvider provideProviderAllowsAuthenticationDataChange
+        * @param string $type
+        * @param string $user
+        * @param \Status $validity Result of the password validity check
+        * @param \StatusValue $expect1 Expected result with $checkData = false
+        * @param \StatusValue $expect2 Expected result with $checkData = true
+        */
+       public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status $validity,
+               \StatusValue $expect1, \StatusValue $expect2
+       ) {
+               if ( $type === PasswordAuthenticationRequest::class ) {
+                       $req = new $type();
+               } elseif ( $type === PasswordDomainAuthenticationRequest::class ) {
+                       $req = new $type( [] );
+               } else {
+                       $req = $this->getMock( $type );
+               }
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = $user;
+               $req->password = 'NewPassword';
+               $req->retype = 'NewPassword';
+
+               $provider = $this->getProvider();
+               $this->validity = $validity;
+               $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
+               $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
+
+               $req->retype = 'BadRetype';
+               $this->assertEquals(
+                       $expect1,
+                       $provider->providerAllowsAuthenticationDataChange( $req, false )
+               );
+               $this->assertEquals(
+                       $expect2->getValue() === 'ignored' ? $expect2 : \StatusValue::newFatal( 'badretype' ),
+                       $provider->providerAllowsAuthenticationDataChange( $req, true )
+               );
+
+               $provider = $this->getProvider( true );
+               $this->assertEquals(
+                       \StatusValue::newGood( 'ignored' ),
+                       $provider->providerAllowsAuthenticationDataChange( $req, true ),
+                       'loginOnly mode should claim to ignore all changes'
+               );
+       }
+
+       public static function provideProviderAllowsAuthenticationDataChange() {
+               $err = \StatusValue::newGood();
+               $err->error( 'arbitrary-warning' );
+
+               return [
+                       [ AuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+                       [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood() ],
+                       [ PasswordAuthenticationRequest::class, 'uTSysop', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood() ],
+                       [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::wrap( $err ),
+                               \StatusValue::newGood(), $err ],
+                       [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newFatal( 'arbitrary-error' ),
+                               \StatusValue::newGood(), \StatusValue::newFatal( 'arbitrary-error' ) ],
+                       [ PasswordAuthenticationRequest::class, 'DoesNotExist', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+                       [ PasswordDomainAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideProviderChangeAuthenticationData
+        * @param string $user
+        * @param string $type
+        * @param bool $loginOnly
+        * @param bool $changed
+        */
+       public function testProviderChangeAuthenticationData( $user, $type, $loginOnly, $changed ) {
+               $cuser = ucfirst( $user );
+               $oldpass = 'UTSysopPassword';
+               $newpass = 'NewPassword';
+
+               $dbw = wfGetDB( DB_MASTER );
+               $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => $cuser ] );
+               $oldExpiry = $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] );
+               $cb = new \ScopedCallback( function () use ( $dbw, $cuser, $oldHash, $oldExpiry ) {
+                       $dbw->update(
+                               'user',
+                               [
+                                       'user_password' => $oldHash,
+                                       'user_password_expires' => $oldExpiry,
+                               ],
+                               [ 'user_name' => $cuser ]
+                       );
+               } );
+
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                       'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
+                               $expires = '30001231235959';
+                       } ]
+               ] );
+
+               $provider = $this->getProvider( $loginOnly );
+
+               // Sanity check
+               $loginReq = new PasswordAuthenticationRequest();
+               $loginReq->action = AuthManager::ACTION_LOGIN;
+               $loginReq->username = $user;
+               $loginReq->password = $oldpass;
+               $loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( $cuser ),
+                       $provider->beginPrimaryAuthentication( $loginReqs ),
+                       'Sanity check'
+               );
+
+               if ( $type === PasswordAuthenticationRequest::class ) {
+                       $changeReq = new $type();
+               } else {
+                       $changeReq = $this->getMock( $type );
+               }
+               $changeReq->action = AuthManager::ACTION_CHANGE;
+               $changeReq->username = $user;
+               $changeReq->password = $newpass;
+               $provider->providerChangeAuthenticationData( $changeReq );
+
+               if ( $loginOnly ) {
+                       $old = 'fail';
+                       $new = 'fail';
+                       $expectExpiry = null;
+               } elseif ( $changed ) {
+                       $old = 'fail';
+                       $new = 'pass';
+                       $expectExpiry = '30001231235959';
+               } else {
+                       $old = 'pass';
+                       $new = 'fail';
+                       $expectExpiry = $oldExpiry;
+               }
+
+               $loginReq->password = $oldpass;
+               $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+               if ( $old === 'pass' ) {
+                       $this->assertEquals(
+                               AuthenticationResponse::newPass( $cuser ),
+                               $ret,
+                               'old password should pass'
+                       );
+               } else {
+                       $this->assertEquals(
+                               AuthenticationResponse::FAIL,
+                               $ret->status,
+                               'old password should fail'
+                       );
+                       $this->assertEquals(
+                               'wrongpassword',
+                               $ret->message->getKey(),
+                               'old password should fail'
+                       );
+               }
+
+               $loginReq->password = $newpass;
+               $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+               if ( $new === 'pass' ) {
+                       $this->assertEquals(
+                               AuthenticationResponse::newPass( $cuser ),
+                               $ret,
+                               'new password should pass'
+                       );
+               } else {
+                       $this->assertEquals(
+                               AuthenticationResponse::FAIL,
+                               $ret->status,
+                               'new password should fail'
+                       );
+                       $this->assertEquals(
+                               'wrongpassword',
+                               $ret->message->getKey(),
+                               'new password should fail'
+                       );
+               }
+
+               $this->assertSame(
+                       $expectExpiry,
+                       $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] )
+               );
+       }
+
+       public static function provideProviderChangeAuthenticationData() {
+               return [
+                       [ 'UTSysop', AuthenticationRequest::class, false, false ],
+                       [ 'UTSysop', PasswordAuthenticationRequest::class, false, true ],
+                       [ 'UTSysop', AuthenticationRequest::class, true, false ],
+                       [ 'UTSysop', PasswordAuthenticationRequest::class, true, true ],
+                       [ 'uTSysop', PasswordAuthenticationRequest::class, false, true ],
+                       [ 'uTSysop', PasswordAuthenticationRequest::class, true, true ],
+               ];
+       }
+
+       public function testTestForAccountCreation() {
+               $user = \User::newFromName( 'foo' );
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_CREATE;
+               $req->username = 'Foo';
+               $req->password = 'Bar';
+               $req->retype = 'Bar';
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $provider = $this->getProvider();
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] ),
+                       'No password request'
+               );
+
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, validated'
+               );
+
+               $req->retype = 'Baz';
+               $this->assertEquals(
+                       \StatusValue::newFatal( 'badretype' ),
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, bad retype'
+               );
+               $req->retype = 'Bar';
+
+               $this->validity->error( 'arbitrary warning' );
+               $expect = \StatusValue::newGood();
+               $expect->error( 'arbitrary warning' );
+               $this->assertEquals(
+                       $expect,
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, not validated'
+               );
+
+               $provider = $this->getProvider( true );
+               $this->validity->error( 'arbitrary warning' );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, not validated, loginOnly'
+               );
+       }
+
+       public function testAccountCreation() {
+               $user = \User::newFromName( 'Foo' );
+
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_CREATE;
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $provider = $this->getProvider( true );
+               try {
+                       $provider->beginPrimaryAccountCreation( $user, $user, [] );
+                       $this->fail( 'Expected exception was not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+                       );
+               }
+
+               try {
+                       $provider->finishAccountCreation( $user, $user, AuthenticationResponse::newPass() );
+                       $this->fail( 'Expected exception was not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+                       );
+               }
+
+               $provider = $this->getProvider( false );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = 'foo';
+               $req->password = 'bar';
+
+               $expect = AuthenticationResponse::newPass( 'Foo' );
+               $expect->createRequest = clone( $req );
+               $expect->createRequest->username = 'Foo';
+               $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
+
+               // We have to cheat a bit to avoid having to add a new user to
+               // the database to test the actual setting of the password works right
+               $dbw = wfGetDB( DB_MASTER );
+               $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => $user ] );
+               $cb = new \ScopedCallback( function () use ( $dbw, $user, $oldHash ) {
+                       $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => $user ] );
+               } );
+
+               $user = \User::newFromName( 'UTSysop' );
+               $req->username = $user->getName();
+               $req->password = 'NewPassword';
+               $expect = AuthenticationResponse::newPass( 'UTSysop' );
+               $expect->createRequest = $req;
+
+               $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+               $this->assertEquals( $expect, $res2, 'Sanity check' );
+
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals( AuthenticationResponse::FAIL, $ret->status, 'sanity check' );
+
+               $this->assertNull( $provider->finishAccountCreation( $user, $user, $res2 ) );
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' );
+
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php b/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..3387e7c
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\PasswordAuthenticationRequest
+ */
+class PasswordAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               $ret = new PasswordAuthenticationRequest();
+               $ret->action = $args[0];
+               return $ret;
+       }
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [ AuthManager::ACTION_LOGIN ] ],
+                       [ [ AuthManager::ACTION_CREATE ] ],
+                       [ [ AuthManager::ACTION_CHANGE ] ],
+                       [ [ AuthManager::ACTION_REMOVE ] ],
+               ];
+       }
+
+       public function testGetFieldInfo2() {
+               $info = [];
+               foreach ( [
+                       AuthManager::ACTION_LOGIN,
+                       AuthManager::ACTION_CREATE,
+                       AuthManager::ACTION_CHANGE,
+                       AuthManager::ACTION_REMOVE,
+               ] as $action ) {
+                       $req = new PasswordAuthenticationRequest();
+                       $req->action = $action;
+                       $info[$action] = $req->getFieldInfo();
+               }
+
+               $this->assertSame( [], $info[AuthManager::ACTION_REMOVE], 'No data needed to remove' );
+
+               $this->assertArrayNotHasKey( 'retype', $info[AuthManager::ACTION_LOGIN],
+                       'No need to retype password on login' );
+               $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CREATE],
+                       'Need to retype when creating new password' );
+               $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CHANGE],
+                       'Need to retype when changing password' );
+
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_LOGIN]['password']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['password']['label'],
+                       'Password field for change is differentiated from login'
+               );
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_CREATE]['password']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['password']['label'],
+                       'Password field for change is differentiated from create'
+               );
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_CREATE]['retype']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['retype']['label'],
+                       'Retype field for change is differentiated from create'
+               );
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [],
+                               false,
+                       ],
+                       'Empty request, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [],
+                               false,
+                       ],
+                       'Empty request, remove' => [
+                               [ AuthManager::ACTION_REMOVE ],
+                               [],
+                               false,
+                       ],
+                       'Username + password, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               $data = [ 'username' => 'User', 'password' => 'Bar' ],
+                               $data + [ 'action' => AuthManager::ACTION_LOGIN ],
+                       ],
+                       'Username + password, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar' ],
+                               false,
+                       ],
+                       'Username + password + retype' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar', 'retype' => 'baz' ],
+                               [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ],
+                       ],
+                       'Username empty, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => '', 'password' => 'Bar' ],
+                               false,
+                       ],
+                       'Username empty, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => '', 'password' => 'Bar', 'retype' => 'baz' ],
+                               [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ],
+                       ],
+                       'Password empty, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => 'User', 'password' => '' ],
+                               false,
+                       ],
+                       'Password empty, login, with retype' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => 'User', 'password' => '', 'retype' => 'baz' ],
+                               false,
+                       ],
+                       'Retype empty' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar', 'retype' => '' ],
+                               false,
+                       ],
+               ];
+       }
+
+       public function testDescribeCredentials() {
+               $req = new PasswordAuthenticationRequest;
+               $req->action = AuthManager::ACTION_LOGIN;
+               $req->username = 'UTSysop';
+               $ret = $req->describeCredentials();
+               $this->assertInternalType( 'array', $ret );
+               $this->assertArrayHasKey( 'provider', $ret );
+               $this->assertInstanceOf( 'Message', $ret['provider'] );
+               $this->assertSame( 'authmanager-provider-password', $ret['provider']->getKey() );
+               $this->assertArrayHasKey( 'account', $ret );
+               $this->assertInstanceOf( 'Message', $ret['account'] );
+               $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() );
+       }
+}
diff --git a/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php b/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..f746515
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\PasswordDomainAuthenticationRequest
+ */
+class PasswordDomainAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               $ret = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+               $ret->action = $args[0];
+               return $ret;
+       }
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [ AuthManager::ACTION_LOGIN ] ],
+                       [ [ AuthManager::ACTION_CREATE ] ],
+                       [ [ AuthManager::ACTION_CHANGE ] ],
+                       [ [ AuthManager::ACTION_REMOVE ] ],
+               ];
+       }
+
+       public function testGetFieldInfo2() {
+               $info = [];
+               foreach ( [
+                       AuthManager::ACTION_LOGIN,
+                       AuthManager::ACTION_CREATE,
+                       AuthManager::ACTION_CHANGE,
+                       AuthManager::ACTION_REMOVE,
+               ] as $action ) {
+                       $req = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+                       $req->action = $action;
+                       $info[$action] = $req->getFieldInfo();
+               }
+
+               $this->assertSame( [], $info[AuthManager::ACTION_REMOVE], 'No data needed to remove' );
+
+               $this->assertArrayNotHasKey( 'retype', $info[AuthManager::ACTION_LOGIN],
+                       'No need to retype password on login' );
+               $this->assertArrayHasKey( 'domain', $info[AuthManager::ACTION_LOGIN],
+                       'Domain needed on login' );
+               $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CREATE],
+                       'Need to retype when creating new password' );
+               $this->assertArrayHasKey( 'domain', $info[AuthManager::ACTION_CREATE],
+                       'Domain needed on account creation' );
+               $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CHANGE],
+                       'Need to retype when changing password' );
+               $this->assertArrayNotHasKey( 'domain', $info[AuthManager::ACTION_CHANGE],
+                       'Domain not needed on account creation' );
+
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_LOGIN]['password']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['password']['label'],
+                       'Password field for change is differentiated from login'
+               );
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_CREATE]['password']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['password']['label'],
+                       'Password field for change is differentiated from create'
+               );
+               $this->assertNotEquals(
+                       $info[AuthManager::ACTION_CREATE]['retype']['label'],
+                       $info[AuthManager::ACTION_CHANGE]['retype']['label'],
+                       'Retype field for change is differentiated from create'
+               );
+       }
+
+       public function provideLoadFromSubmission() {
+               $domainList = [ 'domainList' => [ 'd1', 'd2' ] ];
+               return [
+                       'Empty request, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [],
+                               false,
+                       ],
+                       'Empty request, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [],
+                               false,
+                       ],
+                       'Empty request, remove' => [
+                               [ AuthManager::ACTION_REMOVE ],
+                               [],
+                               false,
+                       ],
+                       'Username + password, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               $data = [ 'username' => 'User', 'password' => 'Bar' ],
+                               false,
+                       ],
+                       'Username + password + domain, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               $data = [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd1' ],
+                               $data + [ 'action' => AuthManager::ACTION_LOGIN ] + $domainList,
+                       ],
+                       'Username + password + bad domain, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               $data = [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd5' ],
+                               false,
+                       ],
+                       'Username + password + domain, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd1' ],
+                               false,
+                       ],
+                       'Username + password + domain + retype' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar', 'retype' => 'baz', 'domain' => 'd1' ],
+                               [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ] +
+                                       $domainList,
+                       ],
+                       'Username empty, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => '', 'password' => 'Bar', 'domain' => 'd1' ],
+                               false,
+                       ],
+                       'Username empty, change' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => '', 'password' => 'Bar', 'retype' => 'baz', 'domain' => 'd1' ],
+                               [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ] +
+                                       $domainList,
+                       ],
+                       'Password empty, login' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => 'User', 'password' => '', 'domain' => 'd1' ],
+                               false,
+                       ],
+                       'Password empty, login, with retype' => [
+                               [ AuthManager::ACTION_LOGIN ],
+                               [ 'username' => 'User', 'password' => '', 'retype' => 'baz', 'domain' => 'd1' ],
+                               false,
+                       ],
+                       'Retype empty' => [
+                               [ AuthManager::ACTION_CHANGE ],
+                               [ 'username' => 'User', 'password' => 'Bar', 'retype' => '', 'domain' => 'd1' ],
+                               false,
+                       ],
+               ];
+       }
+
+       public function testDescribeCredentials() {
+               $req = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+               $req->action = AuthManager::ACTION_LOGIN;
+               $req->username = 'UTSysop';
+               $req->domain = 'd2';
+               $ret = $req->describeCredentials();
+               $this->assertInternalType( 'array', $ret );
+               $this->assertArrayHasKey( 'provider', $ret );
+               $this->assertInstanceOf( 'Message', $ret['provider'] );
+               $this->assertSame( 'authmanager-provider-password-domain', $ret['provider']->getKey() );
+               $this->assertArrayHasKey( 'account', $ret );
+               $this->assertInstanceOf( 'Message', $ret['account'] );
+               $this->assertSame( 'authmanager-account-password-domain', $ret['account']->getKey() );
+               $this->assertSame( [ 'UTSysop', 'd2' ], $ret['account']->getParams() );
+       }
+}
diff --git a/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php b/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..3f90169
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\RememberMeAuthenticationRequest
+ */
+class RememberMeAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [ 1 ] ],
+                       [ [ null ] ],
+               ];
+       }
+
+       public function testGetFieldInfo_2() {
+               $req = new RememberMeAuthenticationRequest();
+               $reqWrapper = \TestingAccessWrapper::newFromObject( $req );
+
+               $reqWrapper->expiration = 30 * 24 * 3600;
+               $this->assertNotEmpty( $req->getFieldInfo() );
+
+               $reqWrapper->expiration = null;
+               $this->assertEmpty( $req->getFieldInfo() );
+       }
+
+       protected function getInstance( array $args = [] ) {
+               $req = new RememberMeAuthenticationRequest();
+               $reqWrapper = \TestingAccessWrapper::newFromObject( $req );
+               $reqWrapper->expiration = $args[0];
+               return $req;
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [ 30 * 24 * 3600 ],
+                               [],
+                               [ 'expiration' => 30 * 24 * 3600, 'rememberMe' => false ]
+                       ],
+                       'RememberMe present' => [
+                               [ 30 * 24 * 3600 ],
+                               [ 'rememberMe' => '' ],
+                               [ 'expiration' => 30 * 24 * 3600, 'rememberMe' => true ]
+                       ],
+                       'RememberMe present but session provider cannot remember' => [
+                               [ null ],
+                               [ 'rememberMe' => '' ],
+                               false
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..59edede
--- /dev/null
@@ -0,0 +1,313 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider
+ */
+class ResetPasswordSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        */
+       public function testGetAuthenticationRequests( $action, $response ) {
+               $provider = new ResetPasswordSecondaryAuthenticationProvider();
+
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       public function testBasics() {
+               $user = \User::newFromName( 'UTSysop' );
+               $user2 = new \User;
+               $obj = new \stdClass;
+               $reqs = [ new \stdClass ];
+
+               $mb = $this->getMockBuilder( ResetPasswordSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'tryReset' ] );
+
+               $methods = [
+                       'beginSecondaryAuthentication' => [ $user, $reqs ],
+                       'continueSecondaryAuthentication' => [ $user, $reqs ],
+                       'beginSecondaryAccountCreation' => [ $user, $user2, $reqs ],
+                       'continueSecondaryAccountCreation' => [ $user, $user2, $reqs ],
+               ];
+               foreach ( $methods as $method => $args ) {
+                       $mock = $mb->getMock();
+                       $mock->expects( $this->once() )->method( 'tryReset' )
+                               ->with( $this->identicalTo( $user ), $this->identicalTo( $reqs ) )
+                               ->will( $this->returnValue( $obj ) );
+                       $this->assertSame( $obj, call_user_func_array( [ $mock, $method ], $args ) );
+               }
+       }
+
+       public function testTryReset() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockBuilder(
+                       ResetPasswordSecondaryAuthenticationProvider::class
+               )
+                       ->setMethods( [
+                               'providerAllowsAuthenticationDataChange', 'providerChangeAuthenticationData'
+                       ] )
+                       ->getMock();
+               $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               $this->assertSame( 'UTSysop', $req->username );
+                               return $req->allow;
+                       } ) );
+               $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               $this->assertSame( 'UTSysop', $req->username );
+                               $req->done = true;
+                       } ) );
+               $config = new \HashConfig( [
+                       'AuthManagerConfig' => [
+                               'preauth' => [],
+                               'primaryauth' => [],
+                               'secondaryauth' => [
+                                       [ 'factory' => function () use ( $provider ) {
+                                               return $provider;
+                                       } ],
+                               ],
+                       ],
+               ] );
+               $manager = new AuthManager( new \FauxRequest, $config );
+               $provider->setManager( $manager );
+               $provider = \TestingAccessWrapper::newFromObject( $provider );
+
+               $msg = wfMessage( 'foo' );
+               $skipReq = new ButtonAuthenticationRequest(
+                       'skipReset',
+                       wfMessage( 'authprovider-resetpass-skip-label' ),
+                       wfMessage( 'authprovider-resetpass-skip-help' )
+               );
+               $passReq = new PasswordAuthenticationRequest();
+               $passReq->action = AuthManager::ACTION_CHANGE;
+               $passReq->password = 'Foo';
+               $passReq->retype = 'Bar';
+               $passReq->allow = \StatusValue::newGood();
+               $passReq->done = false;
+
+               $passReq2 = $this->getMockBuilder( PasswordAuthenticationRequest::class )
+                       ->enableProxyingToOriginalMethods()
+                       ->getMock();
+               $passReq2->action = AuthManager::ACTION_CHANGE;
+               $passReq2->password = 'Foo';
+               $passReq2->retype = 'Foo';
+               $passReq2->allow = \StatusValue::newGood();
+               $passReq2->done = false;
+
+               $passReq3 = new PasswordAuthenticationRequest();
+               $passReq3->action = AuthManager::ACTION_LOGIN;
+               $passReq3->password = 'Foo';
+               $passReq3->retype = 'Foo';
+               $passReq3->allow = \StatusValue::newGood();
+               $passReq3->done = false;
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->tryReset( $user, [] )
+               );
+
+               $manager->setAuthenticationSessionData( 'reset-pass', 'foo' );
+               try {
+                       $provider->tryReset( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass is not valid', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', (object)[] );
+               try {
+                       $provider->tryReset( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass msg is missing', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => 'foo',
+               ] );
+               try {
+                       $provider->tryReset( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass msg is not valid', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+               ] );
+               try {
+                       $provider->tryReset( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass hard is missing', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => true,
+                       'req' => 'foo',
+               ] );
+               try {
+                       $provider->tryReset( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass req is not valid', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => false,
+                       'req' => $passReq3,
+               ] );
+               try {
+                       $provider->tryReset( $user, [ $passReq ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \UnexpectedValueException $ex ) {
+                       $this->assertSame( 'reset-pass req is not valid', $ex->getMessage() );
+               }
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => true,
+               ] );
+               $res = $provider->tryReset( $user, [] );
+               $this->assertInstanceOf( AuthenticationResponse::class, $res );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertEquals( $msg, $res->message );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf(
+                       PasswordAuthenticationRequest::class,
+                       $res->neededRequests[0]
+               );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => false,
+                       'req' => $passReq,
+               ] );
+               $res = $provider->tryReset( $user, [] );
+               $this->assertInstanceOf( AuthenticationResponse::class, $res );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertEquals( $msg, $res->message );
+               $this->assertCount( 2, $res->neededRequests );
+               $this->assertEquals( $passReq, $res->neededRequests[0] );
+               $this->assertEquals( $skipReq, $res->neededRequests[1] );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $passReq->retype = 'Bad';
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => false,
+                       'req' => $passReq,
+               ] );
+               $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $passReq->retype = 'Bad';
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => true,
+               ] );
+               $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertSame( 'badretype', $res->message->getKey() );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf(
+                       PasswordAuthenticationRequest::class,
+                       $res->neededRequests[0]
+               );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => true,
+               ] );
+               $res = $provider->tryReset( $user, [ $skipReq, $passReq3 ] );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertEquals( $msg, $res->message );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf(
+                       PasswordAuthenticationRequest::class,
+                       $res->neededRequests[0]
+               );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $passReq->retype = $passReq->password;
+               $passReq->allow = \StatusValue::newFatal( 'arbitrary-fail' );
+               $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertSame( 'arbitrary-fail', $res->message->getKey() );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf(
+                       PasswordAuthenticationRequest::class,
+                       $res->neededRequests[0]
+               );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+
+               $passReq->allow = \StatusValue::newGood();
+               $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertTrue( $passReq->done );
+
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => false,
+                       'req' => $passReq2,
+               ] );
+               $res = $provider->tryReset( $user, [ $passReq2 ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertTrue( $passReq2->done );
+
+               $passReq->done = false;
+               $passReq2->done = false;
+               $manager->setAuthenticationSessionData( 'reset-pass', [
+                       'msg' => $msg,
+                       'hard' => false,
+                       'req' => $passReq2,
+               ] );
+               $res = $provider->tryReset( $user, [ $passReq ] );
+               $this->assertInstanceOf( AuthenticationResponse::class, $res );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertEquals( $msg, $res->message );
+               $this->assertCount( 2, $res->neededRequests );
+               $this->assertEquals( $passReq2, $res->neededRequests[0] );
+               $this->assertEquals( $skipReq, $res->neededRequests[1] );
+               $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $this->assertFalse( $passReq->done );
+               $this->assertFalse( $passReq2->done );
+       }
+}
diff --git a/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php b/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..05c5165
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\TemporaryPasswordAuthenticationRequest
+ */
+class TemporaryPasswordAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               $ret = new TemporaryPasswordAuthenticationRequest;
+               $ret->action = $args[0];
+               return $ret;
+       }
+
+       public static function provideGetFieldInfo() {
+               return [
+                       [ [ AuthManager::ACTION_CREATE ] ],
+                       [ [ AuthManager::ACTION_CHANGE ] ],
+                       [ [ AuthManager::ACTION_REMOVE ] ],
+               ];
+       }
+
+       public function testNewRandom() {
+               global $wgPasswordPolicy;
+
+               $this->stashMwGlobals( 'wgPasswordPolicy' );
+               $wgPasswordPolicy['policies']['default'] += [
+                       'MinimalPasswordLength' => 1,
+                       'MinimalPasswordLengthToLogin' => 1,
+               ];
+
+               $ret1 = TemporaryPasswordAuthenticationRequest::newRandom();
+               $ret2 = TemporaryPasswordAuthenticationRequest::newRandom();
+               $this->assertNotSame( '', $ret1->password );
+               $this->assertNotSame( '', $ret2->password );
+               $this->assertNotSame( $ret1->password, $ret2->password );
+       }
+
+       public function testNewInvalid() {
+               $ret = TemporaryPasswordAuthenticationRequest::newInvalid();
+               $this->assertNull( $ret->password );
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [ AuthManager::ACTION_REMOVE ],
+                               [],
+                               false,
+                       ],
+                       'Create, empty request' => [
+                               [ AuthManager::ACTION_CREATE ],
+                               [],
+                               false,
+                       ],
+                       'Create, mailpassword set' => [
+                               [ AuthManager::ACTION_CREATE ],
+                               [ 'mailpassword' => 1 ],
+                               [ 'mailpassword' => true, 'action' => AuthManager::ACTION_CREATE ],
+                       ],
+               ];
+       }
+
+       public function testDescribeCredentials() {
+               $req = new TemporaryPasswordAuthenticationRequest;
+               $req->action = AuthManager::ACTION_LOGIN;
+               $req->username = 'UTSysop';
+               $ret = $req->describeCredentials();
+               $this->assertInternalType( 'array', $ret );
+               $this->assertArrayHasKey( 'provider', $ret );
+               $this->assertInstanceOf( 'Message', $ret['provider'] );
+               $this->assertSame( 'authmanager-provider-temporarypassword', $ret['provider']->getKey() );
+               $this->assertArrayHasKey( 'account', $ret );
+               $this->assertInstanceOf( 'Message', $ret['account'] );
+               $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() );
+       }
+}
diff --git a/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..8d0bf96
--- /dev/null
@@ -0,0 +1,749 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider
+ */
+class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+
+       private $manager = null;
+       private $config = null;
+       private $validity = null;
+
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       /**
+        * Get an instance of the provider
+        *
+        * $provider->checkPasswordValidity is mocked to return $this->validity,
+        * because we don't need to test that here.
+        *
+        * @param array $params
+        * @return TemporaryPasswordPrimaryAuthenticationProvider
+        */
+       protected function getProvider( $params = [] ) {
+               if ( !$this->config ) {
+                       $this->config = new \HashConfig( [
+                               'EmailEnabled' => true,
+                       ] );
+               }
+               $config = new \MultiConfig( [
+                       $this->config,
+                       \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+               ] );
+
+               if ( !$this->manager ) {
+                       $this->manager = new AuthManager( new \FauxRequest(), $config );
+               }
+               $this->validity = \Status::newGood();
+
+               $mockedMethods[] = 'checkPasswordValidity';
+               $provider = $this->getMock(
+                       TemporaryPasswordPrimaryAuthenticationProvider::class,
+                       $mockedMethods,
+                       [ $params ]
+               );
+               $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
+                       ->will( $this->returnCallback( function () {
+                               return $this->validity;
+                       } ) );
+               $provider->setConfig( $config );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setManager( $this->manager );
+
+               return $provider;
+       }
+
+       protected function hookMailer( $func = null ) {
+               \Hooks::clear( 'AlternateUserMailer' );
+               if ( $func ) {
+                       \Hooks::register( 'AlternateUserMailer', $func );
+                       // Safety
+                       \Hooks::register( 'AlternateUserMailer', function () {
+                               return false;
+                       } );
+               } else {
+                       \Hooks::register( 'AlternateUserMailer', function () {
+                               $this->fail( 'AlternateUserMailer hook called unexpectedly' );
+                               return false;
+                       } );
+               }
+
+               return new \ScopedCallback( function () {
+                       \Hooks::clear( 'AlternateUserMailer' );
+                       \Hooks::register( 'AlternateUserMailer', function () {
+                               return false;
+                       } );
+               } );
+       }
+
+       public function testBasics() {
+               $provider = new TemporaryPasswordPrimaryAuthenticationProvider();
+
+               $this->assertSame(
+                       PrimaryAuthenticationProvider::TYPE_CREATE,
+                       $provider->accountCreationType()
+               );
+
+               $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
+               $this->assertTrue( $provider->testUserExists( 'uTSysop' ) );
+               $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+               $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
+
+               $req = new PasswordAuthenticationRequest;
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = '<invalid>';
+               $provider->providerChangeAuthenticationData( $req );
+       }
+
+       public function testConfig() {
+               $config = new \HashConfig( [
+                       'EnableEmail' => false,
+                       'NewPasswordExpiry' => 100,
+                       'PasswordReminderResendTime' => 101,
+               ] );
+
+               $p = \TestingAccessWrapper::newFromObject( new TemporaryPasswordPrimaryAuthenticationProvider() );
+               $p->setConfig( $config );
+               $this->assertSame( false, $p->emailEnabled );
+               $this->assertSame( 100, $p->newPasswordExpiry );
+               $this->assertSame( 101, $p->passwordReminderResendTime );
+
+               $p = \TestingAccessWrapper::newFromObject( new TemporaryPasswordPrimaryAuthenticationProvider( [
+                       'emailEnabled' => true,
+                       'newPasswordExpiry' => 42,
+                       'passwordReminderResendTime' => 43,
+               ] ) );
+               $p->setConfig( $config );
+               $this->assertSame( true, $p->emailEnabled );
+               $this->assertSame( 42, $p->newPasswordExpiry );
+               $this->assertSame( 43, $p->passwordReminderResendTime );
+       }
+
+       public function testTestUserCanAuthenticate() {
+               $dbw = wfGetDB( DB_MASTER );
+
+               $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( 'password' )->toString();
+
+               $provider = $this->getProvider();
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
+
+               $dbw->update(
+                       'user',
+                       [
+                               'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
+                               'user_newpass_time' => null,
+                       ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+
+               $dbw->update(
+                       'user',
+                       [
+                               'user_newpassword' => $pwhash,
+                               'user_newpass_time' => null,
+                       ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'uTSysop' ) );
+
+               $dbw->update(
+                       'user',
+                       [
+                               'user_newpassword' => $pwhash,
+                               'user_newpass_time' => $dbw->timestamp( time() - 10 ),
+                       ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $providerPriv->newPasswordExpiry = 100;
+               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $providerPriv->newPasswordExpiry = 1;
+               $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+
+               $dbw->update(
+                       'user',
+                       [
+                               'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
+                               'user_newpass_time' => null,
+                       ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $options
+        * @param array $expected
+        */
+       public function testGetAuthenticationRequests( $action, $options, $expected ) {
+               $actual = $this->getProvider()->getAuthenticationRequests( $action, $options );
+               foreach ( $actual as $req ) {
+                       if ( $req instanceof TemporaryPasswordAuthenticationRequest && $req->password !== null ) {
+                               $req->password = 'random';
+                       }
+               }
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               $anon = [ 'username' => null ];
+               $loggedIn = [ 'username' => 'UTSysop' ];
+
+               return [
+                       [ AuthManager::ACTION_LOGIN, $anon, [
+                               new PasswordAuthenticationRequest
+                       ] ],
+                       [ AuthManager::ACTION_LOGIN, $loggedIn, [
+                               new PasswordAuthenticationRequest
+                       ] ],
+                       [ AuthManager::ACTION_CREATE, $anon, [] ],
+                       [ AuthManager::ACTION_CREATE, $loggedIn, [
+                               new TemporaryPasswordAuthenticationRequest( 'random' )
+                       ] ],
+                       [ AuthManager::ACTION_LINK, $anon, [] ],
+                       [ AuthManager::ACTION_LINK, $loggedIn, [] ],
+                       [ AuthManager::ACTION_CHANGE, $anon, [
+                               new TemporaryPasswordAuthenticationRequest( 'random' )
+                       ] ],
+                       [ AuthManager::ACTION_CHANGE, $loggedIn, [
+                               new TemporaryPasswordAuthenticationRequest( 'random' )
+                       ] ],
+                       [ AuthManager::ACTION_REMOVE, $anon, [
+                               new TemporaryPasswordAuthenticationRequest
+                       ] ],
+                       [ AuthManager::ACTION_REMOVE, $loggedIn, [
+                               new TemporaryPasswordAuthenticationRequest
+                       ] ],
+               ];
+       }
+
+       public function testAuthentication() {
+               $password = 'TemporaryPassword';
+               $hash = ':A:' . md5( $password );
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() - 10 ) ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+
+               $req = new PasswordAuthenticationRequest();
+               $req->action = AuthManager::ACTION_LOGIN;
+               $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+               $provider = $this->getProvider();
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+
+               $providerPriv->newPasswordExpiry = 100;
+
+               // General failures
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = '<invalid>';
+               $req->password = 'WhoCares';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               $req->username = 'DoesNotExist';
+               $req->password = 'DoesNotExist';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+
+               // Validation failure
+               $req->username = 'UTSysop';
+               $req->password = $password;
+               $this->validity = \Status::newFatal( 'arbitrary-failure' );
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'arbitrary-failure',
+                       $ret->message->getKey()
+               );
+
+               // Successful auth
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->validity = \Status::newGood();
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+               $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+               $this->manager->removeAuthenticationSessionData( null );
+               $this->validity = \Status::newGood();
+               $req->username = 'uTSysop';
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       $provider->beginPrimaryAuthentication( $reqs )
+               );
+               $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+               $req->username = 'UTSysop';
+
+               // Expired password
+               $providerPriv->newPasswordExpiry = 1;
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
+               );
+
+               // Bad password
+               $providerPriv->newPasswordExpiry = 100;
+               $this->validity = \Status::newGood();
+               $req->password = 'Wrong';
+               $ret = $provider->beginPrimaryAuthentication( $reqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
+               );
+
+       }
+
+       /**
+        * @dataProvider provideProviderAllowsAuthenticationDataChange
+        * @param string $type
+        * @param string $user
+        * @param \Status $validity Result of the password validity check
+        * @param \StatusValue $expect1 Expected result with $checkData = false
+        * @param \StatusValue $expect2 Expected result with $checkData = true
+        */
+       public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status $validity,
+               \StatusValue $expect1, \StatusValue $expect2
+       ) {
+               if ( $type === PasswordAuthenticationRequest::class ||
+                       $type === TemporaryPasswordAuthenticationRequest::class
+               ) {
+                       $req = new $type();
+               } else {
+                       $req = $this->getMock( $type );
+               }
+               $req->action = AuthManager::ACTION_CHANGE;
+               $req->username = $user;
+               $req->password = 'NewPassword';
+
+               $provider = $this->getProvider();
+               $this->validity = $validity;
+               $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
+               $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
+       }
+
+       public static function provideProviderAllowsAuthenticationDataChange() {
+               $err = \StatusValue::newGood();
+               $err->error( 'arbitrary-warning' );
+
+               return [
+                       [ AuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+                       [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+                       [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood() ],
+                       [ TemporaryPasswordAuthenticationRequest::class, 'uTSysop', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood() ],
+                       [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::wrap( $err ),
+                               \StatusValue::newGood(), $err ],
+                       [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop',
+                               \Status::newFatal( 'arbitrary-error' ), \StatusValue::newGood(),
+                               \StatusValue::newFatal( 'arbitrary-error' ) ],
+                       [ TemporaryPasswordAuthenticationRequest::class, 'DoesNotExist', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+                       [ TemporaryPasswordAuthenticationRequest::class, '<invalid>', \Status::newGood(),
+                               \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideProviderChangeAuthenticationData
+        * @param string $user
+        * @param string $type
+        * @param bool $changed
+        */
+       public function testProviderChangeAuthenticationData( $user, $type, $changed ) {
+               $cuser = ucfirst( $user );
+               $oldpass = 'OldTempPassword';
+               $newpass = 'NewTempPassword';
+
+               $hash = ':A:' . md5( $oldpass );
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 10 ) ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $oldHash = $dbw->selectField( 'user', 'user_newpassword', [ 'user_name' => $cuser ] );
+               $cb = new \ScopedCallback( function () use ( $dbw, $cuser, $oldHash ) {
+                       $dbw->update( 'user', [ 'user_newpassword' => $oldHash ], [ 'user_name' => $cuser ] );
+               } );
+
+               $provider = $this->getProvider();
+
+               // Sanity check
+               $loginReq = new PasswordAuthenticationRequest();
+               $loginReq->action = AuthManager::ACTION_CHANGE;
+               $loginReq->username = $user;
+               $loginReq->password = $oldpass;
+               $loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
+               $this->assertEquals(
+                       AuthenticationResponse::newPass( $cuser ),
+                       $provider->beginPrimaryAuthentication( $loginReqs ),
+                       'Sanity check'
+               );
+
+               if ( $type === PasswordAuthenticationRequest::class ||
+                       $type === TemporaryPasswordAuthenticationRequest::class
+               ) {
+                       $changeReq = new $type();
+               } else {
+                       $changeReq = $this->getMock( $type );
+               }
+               $changeReq->action = AuthManager::ACTION_CHANGE;
+               $changeReq->username = $user;
+               $changeReq->password = $newpass;
+               $resetMailer = $this->hookMailer();
+               $provider->providerChangeAuthenticationData( $changeReq );
+               \ScopedCallback::consume( $resetMailer );
+
+               $loginReq->password = $oldpass;
+               $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+               $this->assertEquals(
+                       AuthenticationResponse::FAIL,
+                       $ret->status,
+                       'old password should fail'
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey(),
+                       'old password should fail'
+               );
+
+               $loginReq->password = $newpass;
+               $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+               if ( $changed ) {
+                       $this->assertEquals(
+                               AuthenticationResponse::newPass( $cuser ),
+                               $ret,
+                               'new password should pass'
+                       );
+                       $this->assertNotNull(
+                               $dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
+                       );
+               } else {
+                       $this->assertEquals(
+                               AuthenticationResponse::FAIL,
+                               $ret->status,
+                               'new password should fail'
+                       );
+                       $this->assertEquals(
+                               'wrongpassword',
+                               $ret->message->getKey(),
+                               'new password should fail'
+                       );
+                       $this->assertNull(
+                               $dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
+                       );
+               }
+       }
+
+       public static function provideProviderChangeAuthenticationData() {
+               return [
+                       [ 'UTSysop', AuthenticationRequest::class, false ],
+                       [ 'UTSysop', PasswordAuthenticationRequest::class, false ],
+                       [ 'UTSysop', TemporaryPasswordAuthenticationRequest::class, true ],
+               ];
+       }
+
+       public function testProviderChangeAuthenticationDataEmail() {
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       [ 'user_newpass_time' => $dbw->timestamp( time() - 5 * 3600 ) ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+
+               $user = \User::newFromName( 'UTSysop' );
+               $reset = new \ScopedCallback( function ( $email ) use ( $user ) {
+                       $user->setEmail( $email );
+                       $user->saveSettings();
+               }, [ $user->getEmail() ] );
+
+               $user->setEmail( 'test@localhost.localdomain' );
+               $user->saveSettings();
+
+               $req = TemporaryPasswordAuthenticationRequest::newRandom();
+               $req->username = $user->getName();
+               $req->mailpassword = true;
+
+               $provider = $this->getProvider( [ 'emailEnabled' => false ] );
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newFatal( 'passwordreset-emaildisabled' ), $status );
+               $req->hasBackchannel = true;
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertFalse( $status->hasMessage( 'passwordreset-emaildisabled' ) );
+               $req->hasBackchannel = false;
+
+               $provider = $this->getProvider( [ 'passwordReminderResendTime' => 10 ] );
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newFatal( 'throttled-mailpassword', 10 ), $status );
+
+               $provider = $this->getProvider( [ 'passwordReminderResendTime' => 3 ] );
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
+
+               $dbw->update(
+                       'user',
+                       [ 'user_newpass_time' => $dbw->timestamp( time() + 5 * 3600 ) ],
+                       [ 'user_name' => 'UTSysop' ]
+               );
+               $provider = $this->getProvider( [ 'passwordReminderResendTime' => 0 ] );
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
+
+               $req->caller = null;
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nocaller' ), $status );
+
+               $req->caller = '127.0.0.256';
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '127.0.0.256' ),
+                       $status );
+
+               $req->caller = '<Invalid>';
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '<Invalid>' ),
+                       $status );
+
+               $req->caller = '127.0.0.1';
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newGood(), $status );
+
+               $req->caller = 'UTSysop';
+               $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+               $this->assertEquals( \StatusValue::newGood(), $status );
+
+               $mailed = false;
+               $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
+                       use ( &$mailed, $req )
+               {
+                       $mailed = true;
+                       $this->assertSame( 'test@localhost.localdomain', $to[0]->address );
+                       $this->assertContains( $req->password, $body );
+                       return false;
+               } );
+               $provider->providerChangeAuthenticationData( $req );
+               \ScopedCallback::consume( $resetMailer );
+               $this->assertTrue( $mailed );
+
+               $priv = \TestingAccessWrapper::newFromObject( $provider );
+               $req->username = '<invalid>';
+               $status = $priv->sendPasswordResetEmail( $req );
+               $this->assertEquals( \Status::newFatal( 'noname' ), $status );
+       }
+
+       public function testTestForAccountCreation() {
+               $user = \User::newFromName( 'foo' );
+               $req = new TemporaryPasswordAuthenticationRequest();
+               $req->username = 'Foo';
+               $req->password = 'Bar';
+               $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
+
+               $provider = $this->getProvider();
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] ),
+                       'No password request'
+               );
+
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, validated'
+               );
+
+               $this->validity->error( 'arbitrary warning' );
+               $expect = \StatusValue::newGood();
+               $expect->error( 'arbitrary warning' );
+               $this->assertEquals(
+                       $expect,
+                       $provider->testForAccountCreation( $user, $user, $reqs ),
+                       'Password request, not validated'
+               );
+       }
+
+       public function testAccountCreation() {
+               $resetMailer = $this->hookMailer();
+
+               $user = \User::newFromName( 'Foo' );
+
+               $req = new TemporaryPasswordAuthenticationRequest();
+               $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
+
+               $authreq = new PasswordAuthenticationRequest();
+               $authreq->action = AuthManager::ACTION_CREATE;
+               $authreqs = [ PasswordAuthenticationRequest::class => $authreq ];
+
+               $provider = $this->getProvider();
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, [] )
+               );
+
+               $req->username = 'foo';
+               $req->password = null;
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = null;
+               $req->password = 'bar';
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+               );
+
+               $req->username = 'foo';
+               $req->password = 'bar';
+
+               $expect = AuthenticationResponse::newPass( 'Foo' );
+               $expect->createRequest = clone( $req );
+               $expect->createRequest->username = 'Foo';
+               $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
+               $this->assertNull( $this->manager->getAuthenticationSessionData( 'no-email' ) );
+
+               // We have to cheat a bit to avoid having to add a new user to
+               // the database to test the actual setting of the password works right
+               $user = \User::newFromName( 'UTSysop' );
+               $req->username = $authreq->username = $user->getName();
+               $req->password = $authreq->password = 'NewPassword';
+               $expect = AuthenticationResponse::newPass( 'UTSysop' );
+               $expect->createRequest = $req;
+
+               $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+               $this->assertEquals( $expect, $res2, 'Sanity check' );
+
+               $ret = $provider->beginPrimaryAuthentication( $authreqs );
+               $this->assertEquals( AuthenticationResponse::FAIL, $ret->status, 'sanity check' );
+
+               $this->assertSame( null, $provider->finishAccountCreation( $user, $user, $res2 ) );
+
+               $ret = $provider->beginPrimaryAuthentication( $authreqs );
+               $this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' );
+       }
+
+       public function testAccountCreationEmail() {
+               $creator = \User::newFromName( 'Foo' );
+               $user = \User::newFromName( 'UTSysop' );
+               $reset = new \ScopedCallback( function ( $email ) use ( $user ) {
+                       $user->setEmail( $email );
+                       $user->saveSettings();
+               }, [ $user->getEmail() ] );
+
+               $user->setEmail( null );
+
+               $req = TemporaryPasswordAuthenticationRequest::newRandom();
+               $req->username = $user->getName();
+               $req->mailpassword = true;
+
+               $provider = $this->getProvider( [ 'emailEnabled' => false ] );
+               $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+               $this->assertEquals( \StatusValue::newFatal( 'emaildisabled' ), $status );
+               $req->hasBackchannel = true;
+               $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+               $this->assertFalse( $status->hasMessage( 'emaildisabled' ) );
+               $req->hasBackchannel = false;
+
+               $provider = $this->getProvider( [ 'emailEnabled' => true ] );
+               $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+               $this->assertEquals( \StatusValue::newFatal( 'noemailcreate' ), $status );
+               $req->hasBackchannel = true;
+               $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+               $this->assertFalse( $status->hasMessage( 'noemailcreate' ) );
+               $req->hasBackchannel = false;
+
+               $user->setEmail( 'test@localhost.localdomain' );
+               $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+               $this->assertEquals( \StatusValue::newGood(), $status );
+
+               $mailed = false;
+               $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
+                       use ( &$mailed, $req )
+               {
+                       $mailed = true;
+                       $this->assertSame( 'test@localhost.localdomain', $to[0]->address );
+                       $this->assertContains( $req->password, $body );
+                       return false;
+               } );
+
+               $expect = AuthenticationResponse::newPass( 'UTSysop' );
+               $expect->createRequest = clone( $req );
+               $expect->createRequest->username = 'UTSysop';
+               $res = $provider->beginPrimaryAccountCreation( $user, $creator, [ $req ] );
+               $this->assertEquals( $expect, $res );
+               $this->assertTrue( $this->manager->getAuthenticationSessionData( 'no-email' ) );
+               $this->assertFalse( $mailed );
+
+               $this->assertSame( 'byemail', $provider->finishAccountCreation( $user, $creator, $res ) );
+               $this->assertTrue( $mailed );
+
+               \ScopedCallback::consume( $resetMailer );
+               $this->assertTrue( $mailed );
+       }
+
+}
diff --git a/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php b/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..8b273b5
--- /dev/null
@@ -0,0 +1,235 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\ThrottlePreAuthenticationProvider
+ */
+class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testConstructor() {
+               $provider = new ThrottlePreAuthenticationProvider();
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $config = new \HashConfig( [
+                       'AccountCreationThrottle' => 123,
+                       'PasswordAttemptThrottle' => [ [
+                               'count' => 5,
+                               'seconds' => 300,
+                       ] ],
+               ] );
+               $provider->setConfig( $config );
+               $this->assertSame( [
+                       'accountCreationThrottle' => [ [ 'count' => 123, 'seconds' => 86400 ] ],
+                       'passwordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300 ] ]
+               ], $providerPriv->throttleSettings );
+               $accountCreationThrottle = \TestingAccessWrapper::newFromObject(
+                       $providerPriv->accountCreationThrottle );
+               $this->assertSame( [ [ 'count' => 123, 'seconds' => 86400 ] ],
+                       $accountCreationThrottle->conditions );
+               $passwordAttemptThrottle = \TestingAccessWrapper::newFromObject(
+                       $providerPriv->passwordAttemptThrottle );
+               $this->assertSame( [ [ 'count' => 5, 'seconds' => 300 ] ],
+                       $passwordAttemptThrottle->conditions );
+
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
+                       'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
+               ] );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $config = new \HashConfig( [
+                       'AccountCreationThrottle' => 123,
+                       'PasswordAttemptThrottle' => [ [
+                               'count' => 5,
+                               'seconds' => 300,
+                       ] ],
+               ] );
+               $provider->setConfig( $config );
+               $this->assertSame( [
+                       'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
+                       'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
+               ], $providerPriv->throttleSettings );
+
+               $cache = new \HashBagOStuff();
+               $provider = new ThrottlePreAuthenticationProvider( [ 'cache' => $cache ] );
+               $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
+                       'PasswordAttemptThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
+               ] ) );
+               $accountCreationThrottle = \TestingAccessWrapper::newFromObject(
+                       $providerPriv->accountCreationThrottle );
+               $this->assertSame( $cache, $accountCreationThrottle->cache );
+               $passwordAttemptThrottle = \TestingAccessWrapper::newFromObject(
+                       $providerPriv->passwordAttemptThrottle );
+               $this->assertSame( $cache, $passwordAttemptThrottle->cache );
+       }
+
+       public function testDisabled() {
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'accountCreationThrottle' => [],
+                       'passwordAttemptThrottle' => [],
+                       'cache' => new \HashBagOStuff(),
+               ] );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => null,
+                       'PasswordAttemptThrottle' => null,
+               ] ) );
+               $provider->setManager( AuthManager::singleton() );
+
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation(
+                               \User::newFromName( 'Created' ),
+                               \User::newFromName( 'Creator' ),
+                               []
+                       )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAuthentication( [] )
+               );
+       }
+
+       /**
+        * @dataProvider provideTestForAccountCreation
+        * @param string $creatorname
+        * @param bool $succeed
+        * @param bool $hook
+        */
+       public function testTestForAccountCreation( $creatorname, $succeed, $hook ) {
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'accountCreationThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+                       'cache' => new \HashBagOStuff(),
+               ] );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => null,
+                       'PasswordAttemptThrottle' => null,
+               ] ) );
+               $provider->setManager( AuthManager::singleton() );
+
+               $user = \User::newFromName( 'RandomUser' );
+               $creator = \User::newFromName( $creatorname );
+               if ( $hook ) {
+                       $mock = $this->getMock( 'stdClass', [ 'onExemptFromAccountCreationThrottle' ] );
+                       $mock->expects( $this->any() )->method( 'onExemptFromAccountCreationThrottle' )
+                               ->will( $this->returnValue( false ) );
+                       $this->mergeMwGlobalArrayValue( 'wgHooks', [
+                               'ExemptFromAccountCreationThrottle' => [ $mock ],
+                       ] );
+               }
+
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       'attempt #1'
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       'attempt #2'
+               );
+               $this->assertEquals(
+                       $succeed ? \StatusValue::newGood() : \StatusValue::newFatal( 'acct_creation_throttle_hit', 2 ),
+                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       'attempt #3'
+               );
+       }
+
+       public static function provideTestForAccountCreation() {
+               return [
+                       'Normal user' => [ 'NormalUser', false, false ],
+                       'Sysop' => [ 'UTSysop', true, false ],
+                       'Normal user with hook' => [ 'NormalUser', true, true ],
+               ];
+       }
+
+       public function testTestForAuthentication() {
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+                       'cache' => new \HashBagOStuff(),
+               ] );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => null,
+                       'PasswordAttemptThrottle' => null,
+               ] ) );
+               $provider->setManager( AuthManager::singleton() );
+
+               $req = new UsernameAuthenticationRequest;
+               $req->username = 'SomeUser';
+               for ( $i = 1; $i <= 3; $i++ ) {
+                       $status = $provider->testForAuthentication( [ $req ] );
+                       $this->assertEquals( $i < 3, $status->isGood(), "attempt #$i" );
+               }
+               $this->assertCount( 1, $status->getErrors() );
+               $msg = new \Message( $status->getErrors()[0]['message'], $status->getErrors()[0]['params'] );
+               $this->assertEquals( 'login-throttled', $msg->getKey() );
+
+               $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+                       AuthenticationResponse::newFail( wfMessage( 'foo' ) ) );
+               $this->assertFalse( $provider->testForAuthentication( [ $req ] )->isGood(), 'after FAIL' );
+
+               $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+                       AuthenticationResponse::newPass() );
+               $this->assertTrue( $provider->testForAuthentication( [ $req ] )->isGood(), 'after PASS' );
+
+               $req1 = new UsernameAuthenticationRequest;
+               $req1->username = 'foo';
+               $req2 = new UsernameAuthenticationRequest;
+               $req2->username = 'bar';
+               $this->assertTrue( $provider->testForAuthentication( [ $req1, $req2 ] )->isGood() );
+
+               $req = new UsernameAuthenticationRequest;
+               $req->username = 'Some user';
+               $provider->testForAuthentication( [ $req ] );
+               $req->username = 'Some_user';
+               $provider->testForAuthentication( [ $req ] );
+               $req->username = 'some user';
+               $status = $provider->testForAuthentication( [ $req ] );
+               $this->assertFalse( $status->isGood(), 'denormalized usernames are normalized' );
+       }
+
+       public function testPostAuthentication() {
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'passwordAttemptThrottle' => [],
+                       'cache' => new \HashBagOStuff(),
+               ] );
+               $provider->setLogger( new \TestLogger );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => null,
+                       'PasswordAttemptThrottle' => null,
+               ] ) );
+               $provider->setManager( AuthManager::singleton() );
+               $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+                       AuthenticationResponse::newPass() );
+
+               $provider = new ThrottlePreAuthenticationProvider( [
+                       'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+                       'cache' => new \HashBagOStuff(),
+               ] );
+               $logger = new \TestLogger( true );
+               $provider->setLogger( $logger );
+               $provider->setConfig( new \HashConfig( [
+                       'AccountCreationThrottle' => null,
+                       'PasswordAttemptThrottle' => null,
+               ] ) );
+               $provider->setManager( AuthManager::singleton() );
+               $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+                       AuthenticationResponse::newPass() );
+               $this->assertSame( [
+                       [ \Psr\Log\LogLevel::ERROR, 'throttler data not found for {user}' ],
+               ], $logger->getBuffer() );
+       }
+}
diff --git a/tests/phpunit/includes/auth/ThrottlerTest.php b/tests/phpunit/includes/auth/ThrottlerTest.php
new file mode 100644 (file)
index 0000000..dba748b
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Psr\Log\AbstractLogger;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\Throttler
+ */
+class ThrottlerTest extends \MediaWikiTestCase {
+       protected function setUp() {
+               global $wgDisableAuthManager;
+
+               parent::setUp();
+               if ( $wgDisableAuthManager ) {
+                       $this->markTestSkipped( '$wgDisableAuthManager is set' );
+               }
+       }
+
+       public function testConstructor() {
+               $cache = new \HashBagOStuff();
+               $logger = $this->getMockBuilder( AbstractLogger::class )
+                       ->setMethods( [ 'log' ] )
+                       ->getMockForAbstractClass();
+
+               $throttler = new Throttler(
+                       [ [ 'count' => 123, 'seconds' => 456 ] ],
+                       [ 'type' => 'foo', 'cache' => $cache ]
+               );
+               $throttler->setLogger( $logger );
+               $throttlerPriv = \TestingAccessWrapper::newFromObject( $throttler );
+               $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
+               $this->assertSame( 'foo', $throttlerPriv->type );
+               $this->assertSame( $cache, $throttlerPriv->cache );
+               $this->assertSame( $logger, $throttlerPriv->logger );
+
+               $throttler = new Throttler( [ [ 'count' => 123, 'seconds' => 456 ] ] );
+               $throttler->setLogger( new NullLogger() );
+               $throttlerPriv = \TestingAccessWrapper::newFromObject( $throttler );
+               $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
+               $this->assertSame( 'custom', $throttlerPriv->type );
+               $this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
+               $this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
+
+               $this->setMwGlobals( [ 'wgPasswordAttemptThrottle' => [ [ 'count' => 321,
+                       'seconds' => 654 ] ] ] );
+               $throttler = new Throttler();
+               $throttler->setLogger( new NullLogger() );
+               $throttlerPriv = \TestingAccessWrapper::newFromObject( $throttler );
+               $this->assertSame( [ [ 'count' => 321, 'seconds' => 654 ] ], $throttlerPriv->conditions );
+               $this->assertSame( 'password', $throttlerPriv->type );
+               $this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
+               $this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
+
+               try {
+                       new Throttler( [], [ 'foo' => 1, 'bar' => 2, 'baz' => 3 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'unrecognized parameters: foo, bar, baz', $ex->getMessage() );
+               }
+       }
+
+       /**
+        * @dataProvider provideNormalizeThrottleConditions
+        */
+       public function testNormalizeThrottleConditions( $condition, $normalized ) {
+               $throttler = new Throttler( $condition );
+               $throttler->setLogger( new NullLogger() );
+               $throttlerPriv = \TestingAccessWrapper::newFromObject( $throttler );
+               $this->assertSame( $normalized, $throttlerPriv->conditions );
+       }
+
+       public function provideNormalizeThrottleConditions() {
+               return [
+                       [
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'count' => 1, 'seconds' => 2 ],
+                               [ [ 'count' => 1, 'seconds' => 2 ] ],
+                       ],
+                       [
+                               [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
+                               [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
+                       ],
+               ];
+       }
+
+       public function testNormalizeThrottleConditions2() {
+               $priv = \TestingAccessWrapper::newFromClass( Throttler::class );
+               $this->assertSame( [], $priv->normalizeThrottleConditions( null ) );
+               $this->assertSame( [], $priv->normalizeThrottleConditions( 'bad' ) );
+       }
+
+       public function testIncrease() {
+               $cache = new \HashBagOStuff();
+               $throttler = new Throttler( [
+                       [ 'count' => 2, 'seconds' => 10, ],
+                       [ 'count' => 4, 'seconds' => 15, 'allIPs' => true ],
+               ], [ 'cache' => $cache ] );
+               $throttler->setLogger( new NullLogger() );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertSame( [ 'throttleIndex' => 0, 'count' => 2, 'wait' => 10 ], $result );
+
+               $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '2.3.4.5' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
+               $this->assertSame( [ 'throttleIndex' => 1, 'count' => 4, 'wait' => 15 ], $result );
+       }
+
+       public function testZeroCount() {
+               $cache = new \HashBagOStuff();
+               $throttler = new Throttler( [ [ 'count' => 0, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+               $throttler->setLogger( new NullLogger() );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+       }
+
+       public function testNamespacing() {
+               $cache = new \HashBagOStuff();
+               $throttler1 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+                       [ 'cache' => $cache, 'type' => 'foo' ] );
+               $throttler2 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+                       [ 'cache' => $cache, 'type' => 'foo' ] );
+               $throttler3 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+                       [ 'cache' => $cache, 'type' => 'bar' ] );
+               $throttler1->setLogger( new NullLogger() );
+               $throttler2->setLogger( new NullLogger() );
+               $throttler3->setLogger( new NullLogger() );
+
+               $throttled = [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ];
+
+               $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertEquals( $throttled, $result, 'should throttle' );
+
+               $result = $throttler2->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertEquals( $throttled, $result, 'should throttle, same namespace' );
+
+               $result = $throttler3->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle, different namespace' );
+       }
+
+       public function testExpiration() {
+               $cache = $this->getMock( HashBagOStuff::class, [ 'add' ] );
+               $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+               $throttler->setLogger( new NullLogger() );
+
+               $cache->expects( $this->once() )->method( 'add' )->with( $this->anything(), 1, 10 );
+               $throttler->increase( 'SomeUser' );
+       }
+
+       /**
+        * @expectedException \InvalidArgumentException
+        */
+       public function testException() {
+               $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ] );
+               $throttler->setLogger( new NullLogger() );
+               $throttler->increase();
+       }
+
+       public function testLog() {
+               $cache = new \HashBagOStuff();
+               $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+
+               $logger = $this->getMockBuilder( AbstractLogger::class )
+                       ->setMethods( [ 'log' ] )
+                       ->getMockForAbstractClass();
+               $logger->expects( $this->never() )->method( 'log' );
+               $throttler->setLogger( $logger );
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $logger = $this->getMockBuilder( AbstractLogger::class )
+                       ->setMethods( [ 'log' ] )
+                       ->getMockForAbstractClass();
+               $logger->expects( $this->once() )->method( 'log' )->with( $this->anything(), $this->anything(), [
+                       'type' => 'custom',
+                       'index' => 0,
+                       'ip' => '1.2.3.4',
+                       'username' => 'SomeUser',
+                       'count' => 1,
+                       'expiry' => 10,
+                       'method' => 'foo',
+               ] );
+               $throttler->setLogger( $logger );
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4', 'foo' );
+               $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+       }
+
+       public function testClear() {
+               $cache = new \HashBagOStuff();
+               $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+               $throttler->setLogger( new NullLogger() );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+
+               $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+               $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+
+               $throttler->clear( 'SomeUser', '1.2.3.4' );
+
+               $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+               $this->assertFalse( $result, 'should not throttle' );
+
+               $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+               $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+       }
+}
diff --git a/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php b/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..7fe3351
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\UserDataAuthenticationRequest
+ */
+class UserDataAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new UserDataAuthenticationRequest;
+       }
+
+       protected function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( 'wgHiddenPrefs', [] );
+       }
+
+       /**
+        * @dataProvider providePopulateUser
+        * @param string $email Email to set
+        * @param string $realname Realname to set
+        * @param StatusValue $expect Expected return
+        */
+       public function testPopulateUser( $email, $realname, $expect ) {
+               $user = new \User();
+               $user->setEmail( 'default@example.com' );
+               $user->setRealName( 'Fake Name' );
+
+               $req = new UserDataAuthenticationRequest;
+               $req->email = $email;
+               $req->realname = $realname;
+               $this->assertEquals( $expect, $req->populateUser( $user ) );
+               if ( $expect->isOk() ) {
+                       $this->assertSame( $email ?: 'default@example.com', $user->getEmail() );
+                       $this->assertSame( $realname ?: 'Fake Name', $user->getRealName() );
+               }
+
+       }
+
+       public static function providePopulateUser() {
+               $good = \StatusValue::newGood();
+               return [
+                       [ 'email@example.com', 'Real Name', $good ],
+                       [ 'email@example.com', '', $good ],
+                       [ '', 'Real Name', $good ],
+                       [ '', '', $good ],
+                       [ 'invalid-email', 'Real Name', \StatusValue::newFatal( 'invalidemailaddress' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideLoadFromSubmission
+        */
+       public function testLoadFromSubmission(
+               array $args, array $data, $expectState /* $hiddenPref, $enableEmail */
+       ) {
+               list( $args, $data, $expectState, $hiddenPref, $enableEmail ) = func_get_args();
+               $this->setMwGlobals( 'wgHiddenPrefs', $hiddenPref );
+               $this->setMwGlobals( 'wgEnableEmail', $enableEmail );
+               parent::testLoadFromSubmission( $args, $data, $expectState );
+       }
+
+       public function provideLoadFromSubmission() {
+               $unhidden = [];
+               $hidden = [ 'realname' ];
+
+               return [
+                       'Empty request, unhidden, email enabled' => [
+                               [],
+                               [],
+                               false,
+                               $unhidden,
+                               true
+                       ],
+                       'email + realname, unhidden, email enabled' => [
+                               [],
+                               $data = [ 'email' => 'Email', 'realname' => 'Name' ],
+                               $data,
+                               $unhidden,
+                               true
+                       ],
+                       'email empty, unhidden, email enabled' => [
+                               [],
+                               $data = [ 'email' => '', 'realname' => 'Name' ],
+                               $data,
+                               $unhidden,
+                               true
+                       ],
+                       'email omitted, unhidden, email enabled' => [
+                               [],
+                               [ 'realname' => 'Name' ],
+                               false,
+                               $unhidden,
+                               true
+                       ],
+                       'realname empty, unhidden, email enabled' => [
+                               [],
+                               $data = [ 'email' => 'Email', 'realname' => '' ],
+                               $data,
+                               $unhidden,
+                               true
+                       ],
+                       'realname omitted, unhidden, email enabled' => [
+                               [],
+                               [ 'email' => 'Email' ],
+                               false,
+                               $unhidden,
+                               true
+                       ],
+                       'Empty request, hidden, email enabled' => [
+                               [],
+                               [],
+                               false,
+                               $hidden,
+                               true
+                       ],
+                       'email + realname, hidden, email enabled' => [
+                               [],
+                               [ 'email' => 'Email', 'realname' => 'Name' ],
+                               [ 'email' => 'Email' ],
+                               $hidden,
+                               true
+                       ],
+                       'email empty, hidden, email enabled' => [
+                               [],
+                               $data = [ 'email' => '', 'realname' => 'Name' ],
+                               [ 'email' => '' ],
+                               $hidden,
+                               true
+                       ],
+                       'email omitted, hidden, email enabled' => [
+                               [],
+                               [ 'realname' => 'Name' ],
+                               false,
+                               $hidden,
+                               true
+                       ],
+                       'realname empty, hidden, email enabled' => [
+                               [],
+                               $data = [ 'email' => 'Email', 'realname' => '' ],
+                               [ 'email' => 'Email' ],
+                               $hidden,
+                               true
+                       ],
+                       'realname omitted, hidden, email enabled' => [
+                               [],
+                               [ 'email' => 'Email' ],
+                               [ 'email' => 'Email' ],
+                               $hidden,
+                               true
+                       ],
+                       'email + realname, unhidden, email disabled' => [
+                               [],
+                               [ 'email' => 'Email', 'realname' => 'Name' ],
+                               [ 'realname' => 'Name' ],
+                               $unhidden,
+                               false
+                       ],
+                       'email omitted, unhidden, email disabled' => [
+                               [],
+                               [ 'realname' => 'Name' ],
+                               [ 'realname' => 'Name' ],
+                               $unhidden,
+                               false
+                       ],
+                       'email empty, unhidden, email disabled' => [
+                               [],
+                               [ 'email' => '', 'realname' => 'Name' ],
+                               [ 'realname' => 'Name' ],
+                               $unhidden,
+                               false
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php b/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php
new file mode 100644 (file)
index 0000000..63628dd
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\UsernameAuthenticationRequest
+ */
+class UsernameAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+       protected function getInstance( array $args = [] ) {
+               return new UsernameAuthenticationRequest();
+       }
+
+       public function provideLoadFromSubmission() {
+               return [
+                       'Empty request' => [
+                               [],
+                               [],
+                               false
+                       ],
+                       'Username' => [
+                               [],
+                               $data = [ 'username' => 'User' ],
+                               $data,
+                       ],
+                       'Username empty' => [
+                               [],
+                               [ 'username' => '' ],
+                               false
+                       ],
+               ];
+       }
+}
index 91f27fb..545b964 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+use MediaWiki\MediaWikiServices;
 
 /**
  * @group ContentHandler
@@ -36,7 +37,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                MWNamespace::getCanonicalNamespaces( true );
                $wgContLang->resetNamespaces();
                // And LinkCache
-               LinkCache::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
        }
 
        protected function tearDown() {
@@ -46,7 +47,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                MWNamespace::getCanonicalNamespaces( true );
                $wgContLang->resetNamespaces();
                // And LinkCache
-               LinkCache::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
 
                parent::tearDown();
        }
index 8a48734..de8e371 100644 (file)
@@ -6,12 +6,6 @@
  */
 class JsonContentTest extends MediaWikiLangTestCase {
 
-       protected function setUp() {
-               parent::setUp();
-
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-       }
-
        public static function provideValidConstruction() {
                return [
                        [ 'foo', false, null ],
diff --git a/tests/phpunit/includes/libs/XhprofDataTest.php b/tests/phpunit/includes/libs/XhprofDataTest.php
new file mode 100644 (file)
index 0000000..a0fb563
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @uses XhprofData
+ * @uses AutoLoader
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.25
+ */
+class XhprofDataTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @covers XhprofData::splitKey
+        * @dataProvider provideSplitKey
+        */
+       public function testSplitKey( $key, $expect ) {
+               $this->assertSame( $expect, XhprofData::splitKey( $key ) );
+       }
+
+       public function provideSplitKey() {
+               return [
+                       [ 'main()', [ null, 'main()' ] ],
+                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
+                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
+                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
+                       [ '==>bar', [ '', 'bar' ] ],
+                       [ '', [ null, '' ] ],
+               ];
+       }
+
+       /**
+        * @covers XhprofData::pruneData
+        */
+       public function testInclude() {
+               $xhprofData = $this->getXhprofDataFixture( [
+                       'include' => [ 'main()' ],
+               ] );
+               $raw = $xhprofData->getRawData();
+               $this->assertArrayHasKey( 'main()', $raw );
+               $this->assertArrayHasKey( 'main()==>foo', $raw );
+               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
+               $this->assertSame( 3, count( $raw ) );
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getInclusiveMetrics
+        */
+       public function testInclusiveMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+               ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getInclusiveMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( $type === 'array' ) {
+                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
+                                       if ( $name === 'main()' ) {
+                                               $this->assertEquals( 100, $metric[$key]['percent'] );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getCompleteMetrics
+        */
+       public function testCompleteMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+                       'calls' => 'array',
+                       'subcalls' => 'array',
+               ];
+               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+                       'exclusive' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getCompleteMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric, $name );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( in_array( $key, $statsMetrics ) ) {
+                                       $this->assertArrayStructure(
+                                               $statStruct, $metric[$key], $key
+                                       );
+                                       $this->assertLessThanOrEqual(
+                                               $metric[$key]['total'], $metric[$key]['exclusive']
+                                       );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @covers XhprofData::getCallers
+        * @covers XhprofData::getCallees
+        * @uses XhprofData
+        */
+       public function testEdges() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $this->assertSame( [], $xhprofData->getCallers( 'main()' ) );
+               $this->assertSame( [ 'foo', 'xhprof_disable' ],
+                       $xhprofData->getCallees( 'main()' )
+               );
+               $this->assertSame( [ 'main()' ],
+                       $xhprofData->getCallers( 'foo' )
+               );
+               $this->assertSame( [], $xhprofData->getCallees( 'strlen' ) );
+       }
+
+       /**
+        * @covers XhprofData::getCriticalPath
+        * @uses XhprofData
+        */
+       public function testCriticalPath() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $path = $xhprofData->getCriticalPath();
+
+               $last = null;
+               foreach ( $path as $key => $value ) {
+                       list( $func, $call ) = XhprofData::splitKey( $key );
+                       $this->assertSame( $last, $func );
+                       $last = $call;
+               }
+               $this->assertSame( $last, 'bar@1' );
+       }
+
+       /**
+        * Get an Xhprof instance that has been primed with a set of known testing
+        * data. Tests for the Xhprof class should laregly be concerned with
+        * evaluating the manipulations of the data collected by xhprof rather
+        * than the data collection process itself.
+        *
+        * The returned Xhprof instance primed will be with a data set created by
+        * running this trivial program using the PECL xhprof implementation:
+        * @code
+        * function bar( $x ) {
+        *   if ( $x > 0 ) {
+        *     bar($x - 1);
+        *   }
+        * }
+        * function foo() {
+        *   for ( $idx = 0; $idx < 2; $idx++ ) {
+        *     bar( $idx );
+        *     $x = strlen( 'abc' );
+        *   }
+        * }
+        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
+        * foo();
+        * $x = xhprof_disable();
+        * var_export( $x );
+        * @endcode
+        *
+        * @return Xhprof
+        */
+       protected function getXhprofDataFixture( array $opts = [] ) {
+               return new XhprofData( [
+                       'foo==>bar' => [
+                               'ct' => 2,
+                               'wt' => 57,
+                               'cpu' => 92,
+                               'mu' => 1896,
+                               'pmu' => 0,
+                       ],
+                       'foo==>strlen' => [
+                               'ct' => 2,
+                               'wt' => 21,
+                               'cpu' => 141,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'bar==>bar@1' => [
+                               'ct' => 1,
+                               'wt' => 18,
+                               'cpu' => 19,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'main()==>foo' => [
+                               'ct' => 1,
+                               'wt' => 304,
+                               'cpu' => 307,
+                               'mu' => 4008,
+                               'pmu' => 0,
+                       ],
+                       'main()==>xhprof_disable' => [
+                               'ct' => 1,
+                               'wt' => 8,
+                               'cpu' => 10,
+                               'mu' => 768,
+                               'pmu' => 392,
+                       ],
+                       'main()' => [
+                               'ct' => 1,
+                               'wt' => 353,
+                               'cpu' => 351,
+                               'mu' => 6112,
+                               'pmu' => 1424,
+                       ],
+               ], $opts );
+       }
+
+       /**
+        * Assert that the given array has the described structure.
+        *
+        * @param array $struct Array of key => type mappings
+        * @param array $actual Array to check
+        * @param string $label
+        */
+       protected function assertArrayStructure( $struct, $actual, $label = null ) {
+               $this->assertInternalType( 'array', $actual, $label );
+               $this->assertCount( count( $struct ), $actual, $label );
+               foreach ( $struct as $key => $type ) {
+                       $this->assertArrayHasKey( $key, $actual );
+                       $this->assertInternalType( $type, $actual[$key] );
+               }
+       }
+}
index 22925bf..6748115 100644 (file)
  * @file
  */
 
-/**
- * @uses Xhprof
- * @uses AutoLoader
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
- */
 class XhprofTest extends PHPUnit_Framework_TestCase {
-
-       public function setUp() {
-               if ( !function_exists( 'xhprof_enable' ) ) {
-                       $this->markTestSkipped( 'No xhprof support detected.' );
-               }
-       }
-
-       /**
-        * @covers Xhprof::splitKey
-        * @dataProvider provideSplitKey
-        */
-       public function testSplitKey( $key, $expect ) {
-               $this->assertSame( $expect, Xhprof::splitKey( $key ) );
-       }
-
-       public function provideSplitKey() {
-               return [
-                       [ 'main()', [ null, 'main()' ] ],
-                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
-                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
-                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
-                       [ '==>bar', [ '', 'bar' ] ],
-                       [ '', [ null, '' ] ],
-               ];
-       }
-
-       /**
-        * @covers Xhprof::__construct
-        * @covers Xhprof::stop
-        * @covers Xhprof::getRawData
-        * @dataProvider provideRawData
-        */
-       public function testRawData( $flags, $keys ) {
-               $xhprof = new Xhprof( [ 'flags' => $flags ] );
-               $raw = $xhprof->getRawData();
-               $this->assertArrayHasKey( 'main()', $raw );
-               foreach ( $keys as $key ) {
-                       $this->assertArrayHasKey( $key, $raw['main()'] );
-               }
-       }
-
-       public function provideRawData() {
-               $tests = [
-                       [ 0, [ 'ct', 'wt' ] ],
-               ];
-
-               if ( defined( 'XHPROF_FLAGS_CPU' ) && defined( 'XHPROF_FLAGS_CPU' ) ) {
-                       $tests[] = [ XHPROF_FLAGS_MEMORY, [
-                               'ct', 'wt', 'mu', 'pmu',
-                       ] ];
-                       $tests[] = [ XHPROF_FLAGS_CPU, [
-                               'ct', 'wt', 'cpu',
-                       ] ];
-                       $tests[] = [ XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU, [
-                                       'ct', 'wt', 'mu', 'pmu', 'cpu',
-                               ] ];
-               }
-
-               return $tests;
-       }
-
-       /**
-        * @covers Xhprof::pruneData
-        */
-       public function testInclude() {
-               $xhprof = $this->getXhprofFixture( [
-                       'include' => [ 'main()' ],
-               ] );
-               $raw = $xhprof->getRawData();
-               $this->assertArrayHasKey( 'main()', $raw );
-               $this->assertArrayHasKey( 'main()==>foo', $raw );
-               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
-               $this->assertSame( 3, count( $raw ) );
-       }
-
        /**
-        * Validate the structure of data returned by
-        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
+        * Trying to enable Xhprof when it is already enabled causes an exception
+        * to be thrown.
         *
-        * @covers Xhprof::getInclusiveMetrics
-        */
-       public function testInclusiveMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-               ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-               ];
-
-               $xhprof = $this->getXhprofFixture();
-               $metrics = $xhprof->getInclusiveMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( $type === 'array' ) {
-                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
-                                       if ( $name === 'main()' ) {
-                                               $this->assertEquals( 100, $metric[$key]['percent'] );
-                                       }
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Validate the structure of data returned by
-        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
-        *
-        * @covers Xhprof::getCompleteMetrics
-        */
-       public function testCompleteMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-                       'calls' => 'array',
-                       'subcalls' => 'array',
-               ];
-               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-                       'exclusive' => 'numeric',
-               ];
-
-               $xhprof = $this->getXhprofFixture();
-               $metrics = $xhprof->getCompleteMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric, $name );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( in_array( $key, $statsMetrics ) ) {
-                                       $this->assertArrayStructure(
-                                               $statStruct, $metric[$key], $key
-                                       );
-                                       $this->assertLessThanOrEqual(
-                                               $metric[$key]['total'], $metric[$key]['exclusive']
-                                       );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * @covers Xhprof::getCallers
-        * @covers Xhprof::getCallees
-        * @uses Xhprof
-        */
-       public function testEdges() {
-               $xhprof = $this->getXhprofFixture();
-               $this->assertSame( [], $xhprof->getCallers( 'main()' ) );
-               $this->assertSame( [ 'foo', 'xhprof_disable' ],
-                       $xhprof->getCallees( 'main()' )
-               );
-               $this->assertSame( [ 'main()' ],
-                       $xhprof->getCallers( 'foo' )
-               );
-               $this->assertSame( [], $xhprof->getCallees( 'strlen' ) );
-       }
-
-       /**
-        * @covers Xhprof::getCriticalPath
-        * @uses Xhprof
-        */
-       public function testCriticalPath() {
-               $xhprof = $this->getXhprofFixture();
-               $path = $xhprof->getCriticalPath();
-
-               $last = null;
-               foreach ( $path as $key => $value ) {
-                       list( $func, $call ) = Xhprof::splitKey( $key );
-                       $this->assertSame( $last, $func );
-                       $last = $call;
-               }
-               $this->assertSame( $last, 'bar@1' );
-       }
-
-       /**
-        * Get an Xhprof instance that has been primed with a set of known testing
-        * data. Tests for the Xhprof class should laregly be concerned with
-        * evaluating the manipulations of the data collected by xhprof rather
-        * than the data collection process itself.
-        *
-        * The returned Xhprof instance primed will be with a data set created by
-        * running this trivial program using the PECL xhprof implementation:
-        * @code
-        * function bar( $x ) {
-        *   if ( $x > 0 ) {
-        *     bar($x - 1);
-        *   }
-        * }
-        * function foo() {
-        *   for ( $idx = 0; $idx < 2; $idx++ ) {
-        *     bar( $idx );
-        *     $x = strlen( 'abc' );
-        *   }
-        * }
-        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
-        * foo();
-        * $x = xhprof_disable();
-        * var_export( $x );
-        * @endcode
-        *
-        * @return Xhprof
-        */
-       protected function getXhprofFixture( array $opts = [] ) {
-               $xhprof = new Xhprof( $opts );
-               $xhprof->loadRawData( [
-                       'foo==>bar' => [
-                               'ct' => 2,
-                               'wt' => 57,
-                               'cpu' => 92,
-                               'mu' => 1896,
-                               'pmu' => 0,
-                       ],
-                       'foo==>strlen' => [
-                               'ct' => 2,
-                               'wt' => 21,
-                               'cpu' => 141,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'bar==>bar@1' => [
-                               'ct' => 1,
-                               'wt' => 18,
-                               'cpu' => 19,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'main()==>foo' => [
-                               'ct' => 1,
-                               'wt' => 304,
-                               'cpu' => 307,
-                               'mu' => 4008,
-                               'pmu' => 0,
-                       ],
-                       'main()==>xhprof_disable' => [
-                               'ct' => 1,
-                               'wt' => 8,
-                               'cpu' => 10,
-                               'mu' => 768,
-                               'pmu' => 392,
-                       ],
-                       'main()' => [
-                               'ct' => 1,
-                               'wt' => 353,
-                               'cpu' => 351,
-                               'mu' => 6112,
-                               'pmu' => 1424,
-                       ],
-               ] );
-               return $xhprof;
-       }
-
-       /**
-        * Assert that the given array has the described structure.
-        *
-        * @param array $struct Array of key => type mappings
-        * @param array $actual Array to check
-        * @param string $label
-        */
-       protected function assertArrayStructure( $struct, $actual, $label = null ) {
-               $this->assertInternalType( 'array', $actual, $label );
-               $this->assertCount( count( $struct ), $actual, $label );
-               foreach ( $struct as $key => $type ) {
-                       $this->assertArrayHasKey( $key, $actual );
-                       $this->assertInternalType( $type, $actual[$key] );
-               }
+        * @expectedException        Exception
+        * @expectedExceptionMessage already enabled
+        * @covers Xhprof::enable
+        */
+       public function testEnable() {
+               $xhprof = new ReflectionClass( 'Xhprof' );
+               $enabled = $xhprof->getProperty( 'enabled' );
+               $enabled->setAccessible( true );
+               $enabled->setValue( true );
+               $xhprof->getMethod( 'enable' )->invoke( null );
        }
 }
diff --git a/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php b/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php
new file mode 100644 (file)
index 0000000..cf87a98
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * @group BagOStuff
+ */
+class RedisBagOStuffTest extends MediaWikiTestCase {
+       /** @var RedisBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->cache = TestingAccessWrapper::newFromObject( new RedisBagOStuff( [ 'servers' => [] ] ) );
+       }
+
+       /**
+        * @covers RedisBagOStuff::unserialize
+        * @dataProvider unserializeProvider
+        */
+       public function testUnserialize( $expected, $input, $message ) {
+               $actual = $this->cache->unserialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function unserializeProvider() {
+               return [
+                       [
+                               -1,
+                               '-1',
+                               'String representation of \'-1\'',
+                       ],
+                       [
+                               0,
+                               '0',
+                               'String representation of \'0\'',
+                       ],
+                       [
+                               1,
+                               '1',
+                               'String representation of \'1\'',
+                       ],
+                       [
+                               -1.0,
+                               'd:-1;',
+                               'Serialized negative double',
+                       ],
+                       [
+                               'foo',
+                               's:3:"foo";',
+                               'Serialized string',
+                       ]
+               ];
+       }
+
+       /**
+        * @covers RedisBagOStuff::serialize
+        * @dataProvider serializeProvider
+        */
+       public function testSerialize( $expected, $input, $message ) {
+               $actual = $this->cache->serialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function serializeProvider() {
+               return [
+                       [
+                               -1,
+                               -1,
+                               '-1 as integer',
+                       ],
+                       [
+                               0,
+                               0,
+                               '0 as integer',
+                       ],
+                       [
+                               1,
+                               1,
+                               '1 as integer',
+                       ],
+                       [
+                               'd:-1;',
+                               -1.0,
+                               'Negative double',
+                       ],
+                       [
+                               's:3:"2.1";',
+                               '2.1',
+                               'Decimal string',
+                       ],
+                       [
+                               's:1:"1";',
+                               '1',
+                               'String representation of 1',
+                       ],
+                       [
+                               's:3:"foo";',
+                               'foo',
+                               'String',
+                       ],
+               ];
+       }
+}
index 22bb237..c024555 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * Although marked as a stub, can work independently.
  *
@@ -101,7 +100,6 @@ class NewParserTest extends MediaWikiTestCase {
                $tmpGlobals['wgUseImageResize'] = true;
                $tmpGlobals['wgAllowExternalImages'] = true;
                $tmpGlobals['wgRawHtml'] = false;
-               $tmpGlobals['wgWellFormedXml'] = true;
                $tmpGlobals['wgExperimentalHtmlIds'] = false;
                $tmpGlobals['wgAdaptiveMessageCache'] = true;
                $tmpGlobals['wgUseDatabaseMessages'] = true;
@@ -151,7 +149,10 @@ class NewParserTest extends MediaWikiTestCase {
                $tmpGlobals['wgHooks'] = $tmpHooks;
                # add a namespace shadowing a interwiki link, to test
                # proper precedence when resolving links. (bug 51680)
-               $tmpGlobals['wgExtraNamespaces'] = [ 100 => 'MemoryAlpha' ];
+               $tmpGlobals['wgExtraNamespaces'] = [
+                       100 => 'MemoryAlpha',
+                       101 => 'MemoryAlpha_talk'
+               ];
 
                $tmpGlobals['wgLocalInterwikis'] = [ 'local', 'mi' ];
                # "extra language links"
@@ -179,6 +180,7 @@ class NewParserTest extends MediaWikiTestCase {
 
                MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
                $wgContLang->resetNamespaces(); # reset namespace cache
+               ParserTest::resetTitleServices();
        }
 
        protected function tearDown() {
@@ -433,7 +435,6 @@ class NewParserTest extends MediaWikiTestCase {
                        'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ],
                        'wgMaxTocLevel' => $maxtoclevel,
                        'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
-                       'wgWellFormedXml' => true,
                        'wgMathDirectory' => $uploadDir . '/math',
                        'wgDefaultLanguageVariant' => $variant,
                        'wgLinkHolderBatchSize' => $linkHolderBatchSize,
index 70e89d4..9600184 100644 (file)
@@ -14,7 +14,6 @@ use Psr\Log\LogLevel;
 class CookieSessionProviderTest extends MediaWikiTestCase {
 
        private function getConfig() {
-               global $wgCookieExpiration;
                return new \HashConfig( [
                        'CookiePrefix' => 'CookiePrefix',
                        'CookiePath' => 'CookiePath',
@@ -22,8 +21,9 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                        'CookieSecure' => true,
                        'CookieHttpOnly' => true,
                        'SessionName' => false,
+                       'CookieExpiration' => 100,
                        'ExtendedLoginCookies' => [ 'UserID', 'Token' ],
-                       'ExtendedLoginCookieExpiration' => $wgCookieExpiration * 2,
+                       'ExtendedLoginCookieExpiration' => 200,
                ] );
        }
 
@@ -377,8 +377,6 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
        }
 
        public function testPersistSession() {
-               $this->setMwGlobals( [ 'wgCookieExpiration' => 100 ] );
-
                $provider = new CookieSessionProvider( [
                        'priority' => 1,
                        'sessionName' => 'MySessionName',
@@ -461,7 +459,6 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
         */
        public function testCookieData( $secure, $remember ) {
                $this->setMwGlobals( [
-                       'wgCookieExpiration' => 100,
                        'wgSecureLogin' => false,
                ] );
 
@@ -783,4 +780,39 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertNull( $provider->getCookie( $request, 'Baz', 'x' ) );
        }
 
+       public function testGetRememberUserDuration() {
+               $config = $this->getConfig();
+               $provider = new CookieSessionProvider( [ 'priority' => 10 ] );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( $config );
+               $provider->setManager( SessionManager::singleton() );
+
+               $this->assertSame( 200, $provider->getRememberUserDuration() );
+
+               $config->set( 'ExtendedLoginCookieExpiration', null );
+
+               $this->assertSame( 100, $provider->getRememberUserDuration() );
+
+               $config->set( 'ExtendedLoginCookieExpiration', 0 );
+
+               $this->assertSame( null, $provider->getRememberUserDuration() );
+       }
+
+       public function testGetLoginCookieExpiration() {
+               $config = $this->getConfig();
+               $provider = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( [
+                       'priority' => 10
+               ] ) );
+               $provider->setLogger( new \Psr\Log\NullLogger() );
+               $provider->setConfig( $config );
+               $provider->setManager( SessionManager::singleton() );
+
+               $this->assertSame( 200, $provider->getLoginCookieExpiration( 'Token' ) );
+               $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User' ) );
+
+               $config->set( 'ExtendedLoginCookieExpiration', null );
+
+               $this->assertSame( 100, $provider->getLoginCookieExpiration( 'Token' ) );
+               $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User' ) );
+       }
 }
index d705fc0..78edb76 100644 (file)
@@ -249,7 +249,7 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
                        }
                        $this->assertEquals( [
                                'value' => 'true',
-                               'expire' => $remember ? 100 : null,
+                               'expire' => null,
                                'path' => 'CookiePath',
                                'domain' => 'CookieDomain',
                                'secure' => false,
index 5f387ea..e725fee 100644 (file)
@@ -868,7 +868,11 @@ class SessionManagerTest extends MediaWikiTestCase {
        }
 
        public function testAutoCreateUser() {
-               global $wgGroupPermissions;
+               global $wgGroupPermissions, $wgDisableAuthManager;
+
+               if ( !$wgDisableAuthManager ) {
+                       $this->markTestSkipped( 'AuthManager is not disabled' );
+               }
 
                \ObjectCache::$instances[__METHOD__] = new TestBagOStuff();
                $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
index f80baf2..4cbeeb9 100644 (file)
@@ -35,6 +35,8 @@ class SessionProviderTest extends MediaWikiTestCase {
 
                $this->assertSame( get_class( $provider ), (string)$provider );
 
+               $this->assertNull( $provider->getRememberUserDuration() );
+
                $this->assertNull( $provider->whyNoSession() );
 
                $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
index e321bdb..e55a3a4 100644 (file)
@@ -179,6 +179,39 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                $this->assertEquals( $expected, $actual );
        }
 
+       public static function provideGetPrefixedDBkey() {
+               return [
+                       [ NS_MAIN, 'Foo_Bar', '', '', 'en', 'Foo_Bar' ],
+                       [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', '', 'en', 'User:Hansi_Maier' ],
+
+                       // No capitalization or normalization is applied while formatting!
+                       [ NS_USER_TALK, 'hansi__maier', '', '', 'en', 'User_talk:hansi__maier' ],
+
+                       // getGenderCache() provides a mock that considers first
+                       // names ending in "a" to be female.
+                       [ NS_USER, 'Lisa_Müller', '', '', 'de', 'Benutzerin:Lisa_Müller' ],
+
+                       [ NS_MAIN, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ],
+
+                       // non-existent namespace
+                       [ 10000000, 'Foobar', '', '', 'en', ':Foobar' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetPrefixedDBkey
+        */
+       public function testGetPrefixedDBkey( $namespace, $dbkey, $fragment,
+               $interwiki, $lang, $expected
+       ) {
+               $codec = $this->makeCodec( $lang );
+               $title = new TitleValue( $namespace, $dbkey, $fragment, $interwiki );
+
+               $actual = $codec->getPrefixedDBkey( $title );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
        public static function provideGetFullText() {
                return [
                        [ NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ],
index 7922553..4dbda74 100644 (file)
@@ -42,6 +42,7 @@ class TitleValueTest extends MediaWikiTestCase {
                $title = new TitleValue( $ns, $text, $fragment, $interwiki );
 
                $this->assertEquals( $ns, $title->getNamespace() );
+               $this->assertTrue( $title->inNamespace( $ns ) );
                $this->assertEquals( $text, $title->getText() );
                $this->assertEquals( $fragment, $title->getFragment() );
                $this->assertEquals( $hasFragment, $title->hasFragment() );
diff --git a/tests/phpunit/includes/user/PasswordResetTest.php b/tests/phpunit/includes/user/PasswordResetTest.php
new file mode 100644 (file)
index 0000000..4db636b
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * @group Database
+ */
+class PasswordResetTest extends PHPUnit_Framework_TestCase {
+       /**
+        * @dataProvider provideIsAllowed
+        */
+       public function testIsAllowed( $passwordResetRoutes, $enableEmail,
+               $allowsAuthenticationDataChange, $canEditPrivate, $canSeePassword,
+               $userIsBlocked, $isAllowed, $isAllowedToDisplayPassword
+       ) {
+               $config = new HashConfig( [
+                       'PasswordResetRoutes' => $passwordResetRoutes,
+                       'EnableEmail' => $enableEmail,
+               ] );
+
+               $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor()
+                       ->getMock();
+               $authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
+                       ->willReturn( $allowsAuthenticationDataChange ? Status::newGood() : Status::newFatal( 'foo' ) );
+
+               $user = $this->getMock( User::class );
+               $user->expects( $this->any() )->method( 'getName' )->willReturn( 'Foo' );
+               $user->expects( $this->any() )->method( 'isBlocked' )->willReturn( $userIsBlocked );
+               $user->expects( $this->any() )->method( 'isAllowed' )
+                       ->will( $this->returnCallback( function ( $perm ) use ( $canEditPrivate, $canSeePassword ) {
+                               if ( $perm === 'editmyprivateinfo' ) {
+                                       return $canEditPrivate;
+                               } elseif ( $perm === 'passwordreset' ) {
+                                       return $canSeePassword;
+                               } else {
+                                       $this->fail( 'Unexpected permission check' );
+                               }
+                       } ) );
+
+               $passwordReset = new PasswordReset( $config, $authManager );
+
+               $this->assertSame( $isAllowed, $passwordReset->isAllowed( $user )->isGood() );
+               $this->assertSame( $isAllowedToDisplayPassword,
+                       $passwordReset->isAllowed( $user, true )->isGood() );
+       }
+
+       public function provideIsAllowed() {
+               return [
+                       [
+                               'passwordResetRoutes' => [],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => false,
+                               'isAllowed' => false,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => false,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => false,
+                               'isAllowed' => false,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => false,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => false,
+                               'isAllowed' => false,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => false,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => false,
+                               'isAllowed' => false,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => true,
+                               'isAllowed' => false,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => false,
+                               'userIsBlocked' => false,
+                               'isAllowed' => true,
+                               'isAllowedToDisplayPassword' => false,
+                       ],
+                       [
+                               'passwordResetRoutes' => [ 'username' => true ],
+                               'enableEmail' => true,
+                               'allowsAuthenticationDataChange' => true,
+                               'canEditPrivate' => true,
+                               'canSeePassword' => true,
+                               'userIsBlocked' => false,
+                               'isAllowed' => true,
+                               'isAllowedToDisplayPassword' => true,
+                       ],
+               ];
+       }
+
+       public function testExecute_email() {
+               $config = new HashConfig( [
+                       'PasswordResetRoutes' => [ 'username' => true, 'email' => true ],
+                       'EnableEmail' => true,
+               ] );
+
+               $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor()
+                       ->getMock();
+               $authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
+                       ->willReturn( Status::newGood() );
+               $authManager->expects( $this->exactly( 2 ) )->method( 'changeAuthenticationData' );
+
+               $request = new FauxRequest();
+               $request->setIP( '1.2.3.4' );
+               $performingUser = $this->getMock( User::class );
+               $performingUser->expects( $this->any() )->method( 'getRequest' )->willReturn( $request );
+               $performingUser->expects( $this->any() )->method( 'isAllowed' )->willReturn( true );
+
+               $targetUser1 = $this->getMock( User::class );
+               $targetUser2 = $this->getMock( User::class );
+               $targetUser1->expects( $this->any() )->method( 'getName' )->willReturn( 'User1' );
+               $targetUser2->expects( $this->any() )->method( 'getName' )->willReturn( 'User2' );
+               $targetUser1->expects( $this->any() )->method( 'getId' )->willReturn( 1 );
+               $targetUser2->expects( $this->any() )->method( 'getId' )->willReturn( 2 );
+               $targetUser1->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+               $targetUser2->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+
+               $passwordReset = $this->getMockBuilder( PasswordReset::class )
+                       ->setMethods( [ 'getUsersByEmail' ] )->setConstructorArgs( [ $config, $authManager ] )
+                       ->getMock();
+               $passwordReset->expects( $this->any() )->method( 'getUsersByEmail' )->with( 'foo@bar.baz' )
+                       ->willReturn( [ $targetUser1, $targetUser2 ] );
+
+               $status = $passwordReset->isAllowed( $performingUser );
+               $this->assertTrue( $status->isGood() );
+
+               $status = $passwordReset->execute( $performingUser, null, 'foo@bar.baz' );
+               $this->assertTrue( $status->isGood() );
+       }
+}
index d876c45..d13da60 100755 (executable)
@@ -77,6 +77,7 @@ class PHPUnitMaintClass extends Maintenance {
                global $wgDevelopmentWarnings;
                global $wgSessionProviders;
                global $wgJobTypeConf;
+               global $wgAuthManagerConfig, $wgAuth, $wgDisableAuthManager;
 
                // Inject test autoloader
                require_once __DIR__ . '/../TestsAutoLoader.php';
@@ -124,6 +125,27 @@ class PHPUnitMaintClass extends Maintenance {
                        ],
                ];
 
+               // Generic AuthManager configuration for testing
+               $wgAuthManagerConfig = [
+                       'preauth' => [],
+                       'primaryauth' => [
+                               [
+                                       'class' => MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class,
+                                       'args' => [ [
+                                               'authoritative' => false,
+                                       ] ],
+                               ],
+                               [
+                                       'class' => MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class,
+                                       'args' => [ [
+                                               'authoritative' => true,
+                                       ] ],
+                               ],
+                       ],
+                       'secondaryauth' => [],
+               ];
+               $wgAuth = $wgDisableAuthManager ? new AuthPlugin : new MediaWiki\Auth\AuthManagerAuthPlugin();
+
                // Bug 44192 Do not attempt to send a real e-mail
                Hooks::clear( 'AlternateUserMailer' );
                Hooks::register(
index ee948bb..aa68bb2 100644 (file)
        } );
 
        // HTML in wikitext
-       QUnit.test( 'HTML', 32, function ( assert ) {
+       QUnit.test( 'HTML', 33, function ( assert ) {
                mw.messages.set( 'jquerymsg-italics-msg', '<i>Very</i> important' );
 
                assertBothModes( assert, [ 'jquerymsg-italics-msg' ], mw.messages.get( 'jquerymsg-italics-msg' ), 'Simple italics unchanged' );
                        'Self-closing tags don\'t cause a parse error'
                );
 
+               mw.messages.set( 'jquerymsg-asciialphabetliteral-regression', '<b >>>="dir">asd</b>' );
+               assert.htmlEqual(
+                       formatParse( 'jquerymsg-asciialphabetliteral-regression' ),
+                       '<b>&gt;&gt;="dir"&gt;asd</b>',
+                       'Regression test for bad "asciiAlphabetLiteral" definition'
+               );
+
                mw.messages.set( 'jquerymsg-entities1', 'A&B' );
                mw.messages.set( 'jquerymsg-entities2', 'A&gt;B' );
                mw.messages.set( 'jquerymsg-entities3', 'A&rarr;B' );
                );
        } );
 
+       QUnit.test( 'Nowiki', 3, function ( assert ) {
+               mw.messages.set( 'jquerymsg-nowiki-link', 'Foo <nowiki>[[bar]]</nowiki> baz.' );
+               assert.equal(
+                       formatParse( 'jquerymsg-nowiki-link' ),
+                       'Foo [[bar]] baz.',
+                       'Link inside nowiki is not parsed'
+               );
+
+               mw.messages.set( 'jquerymsg-nowiki-htmltag', 'Foo <nowiki><b>bar</b></nowiki> baz.' );
+               assert.equal(
+                       formatParse( 'jquerymsg-nowiki-htmltag' ),
+                       'Foo &lt;b&gt;bar&lt;/b&gt; baz.',
+                       'HTML inside nowiki is not parsed and escaped'
+               );
+
+               mw.messages.set( 'jquerymsg-nowiki-template', 'Foo <nowiki>{{bar}}</nowiki> baz.' );
+               assert.equal(
+                       formatParse( 'jquerymsg-nowiki-template' ),
+                       'Foo {{bar}} baz.',
+                       'Template inside nowiki is not parsed and does not cause a parse error'
+               );
+       } );
+
        QUnit.test( 'Behavior in case of invalid wikitext', 3, function ( assert ) {
                mw.messages.set( 'invalid-wikitext', '<b>{{FAIL}}</b>' );